353 lines
13 KiB
JavaScript
353 lines
13 KiB
JavaScript
|
/*
|
||
|
* This file is part of Toolkit.
|
||
|
*
|
||
|
* Toolkit is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public
|
||
|
* License as published by the Free Software Foundation; either
|
||
|
* version 3 of the License, or (at your option) any later version.
|
||
|
*
|
||
|
* Toolkit is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
* Lesser General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General
|
||
|
* Public License along with this program; if not, write to the
|
||
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||
|
* Boston, MA 02110-1301 USA
|
||
|
*/
|
||
|
"use strict";
|
||
|
(function(w, TK) {
|
||
|
function range_change_cb() {
|
||
|
this.invalidate_all();
|
||
|
this.trigger_draw();
|
||
|
};
|
||
|
function transform_dots(dots) {
|
||
|
if (dots === void(0)) return "";
|
||
|
if (typeof dots === "string") return dots;
|
||
|
if (typeof dots === "object") {
|
||
|
if (Array.isArray(dots)) {
|
||
|
if (!dots.length || !dots[0]) return null;
|
||
|
var ret = { };
|
||
|
var start, stop;
|
||
|
|
||
|
for (var name in dots[0]) if (dots[0].hasOwnProperty(name)) {
|
||
|
var a = [];
|
||
|
ret[name] = a;
|
||
|
for (var i = 0; i < dots.length; i++) {
|
||
|
a[i] = dots[i][name];
|
||
|
}
|
||
|
}
|
||
|
return ret;
|
||
|
} else return dots;
|
||
|
} else {
|
||
|
TK.error("Unsupported option 'dots':", dots);
|
||
|
return "";
|
||
|
}
|
||
|
}
|
||
|
// this is not really a rounding operation but simply adds 0.5. we do this to make sure
|
||
|
// that integer pixel positions result in actual pixels, instead of being spread across
|
||
|
// two pixels with half opacity
|
||
|
function svg_round(x) {
|
||
|
x = +x;
|
||
|
return x + 0.5;
|
||
|
}
|
||
|
function svg_round_array(x) {
|
||
|
var i;
|
||
|
for (i = 0; i < x.length; i++) {
|
||
|
x[i] = +x[i] + 0.5;
|
||
|
}
|
||
|
return x;
|
||
|
}
|
||
|
function _start(d, s) {
|
||
|
var w = this.range_x.options.basis;
|
||
|
var h = this.range_y.options.basis;
|
||
|
var t = this.options.type;
|
||
|
var m = this.options.mode;
|
||
|
var x = this.range_x.val2px(d.x[0]);
|
||
|
var y = this.range_y.val2px(d.y[0]);
|
||
|
switch (m) {
|
||
|
case "bottom":
|
||
|
// fill the lower part of the graph
|
||
|
s.push(
|
||
|
"M " + svg_round(x - 1) + " ",
|
||
|
svg_round(h + 1) + " " + t + " ",
|
||
|
svg_round(x - 1) + " ",
|
||
|
svg_round(y)
|
||
|
);
|
||
|
break;
|
||
|
case "top":
|
||
|
// fill the upper part of the graph
|
||
|
s.push("M " + svg_round(x - 1) + " " + svg_round(-1),
|
||
|
" " + t + " " + svg_round(x - 1) + " ",
|
||
|
svg_round(y)
|
||
|
);
|
||
|
break;
|
||
|
case "center":
|
||
|
// fill from the mid
|
||
|
s.push(
|
||
|
"M " + svg_round(x - 1) + " ",
|
||
|
svg_round(0.5 * h)
|
||
|
);
|
||
|
break;
|
||
|
case "base":
|
||
|
// fill from variable point
|
||
|
s.push(
|
||
|
"M " + svg_round(x - 1) + " ",
|
||
|
svg_round((1 - this.options.base) * h)
|
||
|
);
|
||
|
break;
|
||
|
default:
|
||
|
TK.error("Unsupported mode:", m);
|
||
|
/* FALL THROUGH */
|
||
|
case "line":
|
||
|
// fill nothing
|
||
|
s.push("M " + svg_round(x) + " " + svg_round(y));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
function _end(d, s) {
|
||
|
var a = 0.5;
|
||
|
var h = this.range_y.options.basis;
|
||
|
var t = this.options.type;
|
||
|
var m = this.options.mode;
|
||
|
var x = this.range_x.val2px(d.x[d.x.length-1]);
|
||
|
var y = this.range_y.val2px(d.y[d.y.length-1]);
|
||
|
switch (m) {
|
||
|
case "bottom":
|
||
|
// fill the graph below
|
||
|
s.push(" " + t + " " + svg_round(x) + " " + svg_round(h + 1) + " Z");
|
||
|
break;
|
||
|
case "top":
|
||
|
// fill the upper part of the graph
|
||
|
s.push(" " + t + " " + svg_round(x + 1) + " " + svg_round(-1) + " Z");
|
||
|
break;
|
||
|
case "center":
|
||
|
// fill from mid
|
||
|
s.push(" " + t + " " + svg_round(x + 1) + " " + svg_round(0.5 * h) + " Z");
|
||
|
break;
|
||
|
case "base":
|
||
|
// fill from variable point
|
||
|
s.push(" " + t + " " + svg_round(x + 1) + " " + svg_round((-m + 1) * h) + " Z");
|
||
|
break;
|
||
|
default:
|
||
|
TK.error("Unsupported mode:", m);
|
||
|
/* FALL THROUGH */
|
||
|
case "line":
|
||
|
// fill nothing
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TK.Graph = TK.class({
|
||
|
/**
|
||
|
* TK.Graph is a single SVG path element. It provides
|
||
|
* some functions to easily draw paths inside Charts and other
|
||
|
* derivates.
|
||
|
*
|
||
|
* @class TK.Graph
|
||
|
*
|
||
|
* @param {Object} [options={ }] - An object containing initial options.
|
||
|
*
|
||
|
* @property {Function|Object} options.range_x - Callback function
|
||
|
* returning a {@link TK.Range} module for x axis or an object with options
|
||
|
* for a new {@link Range}.
|
||
|
* @property {Function|Object} options.range_y - Callback function
|
||
|
* returning a {@link TK.Range} module for y axis or an object with options
|
||
|
* for a new {@link Range}.
|
||
|
* @property {Array<Object>|String} options.dots=[] - The dots of the path.
|
||
|
* Can be a ready-to-use SVG-path-string or an array of objects like
|
||
|
* <code>{x: x, y: y [, x1, y1, x2, y2]}</code> (depending on the type).
|
||
|
* @property {String} [options.type="L"] - Type of the graph (needed values in dots object):
|
||
|
* <ul>
|
||
|
* <li><code>L</code>: normal (needs x,y)</li>
|
||
|
* <li><code>T</code>: smooth quadratic Bézier (needs x, y)</li>
|
||
|
* <li><code>H[n]</code>: smooth horizontal, [n] = smoothing factor between 1 (square) and 5 (nearly no smooth)</li>
|
||
|
* <li><code>Q</code>: quadratic Bézier (needs: x1, y1, x, y)</li>
|
||
|
* <li><code>C</code>: CurveTo (needs: x1, y1, x2, y2, x, y)</li>
|
||
|
* <li><code>S</code>: SmoothCurve (needs: x1, y1, x, y)</li>
|
||
|
* </ul>
|
||
|
* @property {String} [options.mode="line"] - Drawing mode of the graph, possible values are:
|
||
|
* <ul>
|
||
|
* <li><code>line</code>: line only</li>
|
||
|
* <li><code>bottom</code>: fill below the line</li>
|
||
|
* <li><code>top</code>: fill above the line</li>
|
||
|
* <li><code>center</code>: fill from the vertical center of the canvas</li>
|
||
|
* <li><code>base</code>: fill from a percentual position on the canvas (set with base)</li>
|
||
|
* </ul>
|
||
|
* @property {Number} [options.base=0] - If mode is <code>base</code> set the position
|
||
|
* of the base line to fill from between 0 (bottom) and 1 (top).
|
||
|
* @property {String} [options.color=""] - Set the color of the path.
|
||
|
* Better use <code>stroke</code> and <code>fill</code> via CSS.
|
||
|
* @property {Number} [options.width=0] - The width of the graph.
|
||
|
* @property {Number} [options.height=0] - The height of the graph.
|
||
|
* @property {String|Boolean} [options.key=false] - Show a description
|
||
|
* for this graph in the charts key, <code>false</code> to turn it off.
|
||
|
*
|
||
|
* @extends TK.Widget
|
||
|
*
|
||
|
* @mixes TK.Ranges
|
||
|
*/
|
||
|
_class: "Graph",
|
||
|
Extends: TK.Widget,
|
||
|
Implements: TK.Ranges,
|
||
|
_options: Object.assign(Object.create(TK.Widget.prototype._options), {
|
||
|
dots: "array",
|
||
|
type: "string",
|
||
|
mode: "string",
|
||
|
base: "number",
|
||
|
color: "string",
|
||
|
range_x: "object",
|
||
|
range_y: "object",
|
||
|
width: "number",
|
||
|
height: "number",
|
||
|
key: "string|boolean",
|
||
|
element: void(0),
|
||
|
}),
|
||
|
options: {
|
||
|
dots: null,
|
||
|
type: "L",
|
||
|
mode: "line",
|
||
|
base: 0,
|
||
|
color: "",
|
||
|
width: 0,
|
||
|
height: 0,
|
||
|
key: false
|
||
|
},
|
||
|
|
||
|
initialize: function (options) {
|
||
|
TK.Widget.prototype.initialize.call(this, options);
|
||
|
/** @member {SVGPath} TK.Graph#element - The SVG path. Has class <code>toolkit-graph</code>
|
||
|
*/
|
||
|
this.element = this.widgetize(TK.make_svg("path"), true, true, true);
|
||
|
TK.add_class(this.element, "toolkit-graph");
|
||
|
/** @member {TK.Range} TK.Graph#range_x - The range for the x axis.
|
||
|
*/
|
||
|
/** @member {TK.Range} TK.Graph#range_y - The range for the y axis.
|
||
|
*/
|
||
|
if (this.options.range_x) this.set("range_x", this.options.range_x);
|
||
|
if (this.options.range_y) this.set("range_y", this.options.range_y);
|
||
|
this.set("color", this.options.color);
|
||
|
this.set("mode", this.options.mode);
|
||
|
if (this.options.dots) this.options.dots = transform_dots(this.options.dots);
|
||
|
},
|
||
|
|
||
|
redraw: function () {
|
||
|
var I = this.invalid;
|
||
|
var O = this.options;
|
||
|
var E = this.element;
|
||
|
|
||
|
if (I.color) {
|
||
|
I.color = false;
|
||
|
E.style.stroke = O.color;
|
||
|
}
|
||
|
|
||
|
if (I.mode) {
|
||
|
I.mode = false;
|
||
|
TK.remove_class(E, "toolkit-filled");
|
||
|
TK.remove_class(E, "toolkit-outline");
|
||
|
TK.add_class(E, O.mode === "line" ? "toolkit-outline" : "toolkit-filled");
|
||
|
}
|
||
|
|
||
|
if (I.validate("dots", "type", "width", "height")) {
|
||
|
var a = 0.5;
|
||
|
var dots = O.dots;
|
||
|
var range_x = this.range_x;
|
||
|
var range_y = this.range_y;
|
||
|
var w = range_x.options.basis;
|
||
|
var h = range_y.options.basis;
|
||
|
|
||
|
if (typeof dots === "string") {
|
||
|
E.setAttribute("d", dots);
|
||
|
} else if (!dots) {
|
||
|
E.setAttribute("d", "");
|
||
|
} else {
|
||
|
var x = svg_round_array(dots.x.map(range_x.val2px));
|
||
|
var y = svg_round_array(dots.y.map(range_y.val2px));
|
||
|
var x1, x2, y1, y2;
|
||
|
// if we are drawing a line, _start will do the first point
|
||
|
var i = O.type === "line" ? 1 : 0;
|
||
|
var s = [];
|
||
|
var f;
|
||
|
|
||
|
_start.call(this, dots, s);
|
||
|
|
||
|
switch (O.type.substr(0,1)) {
|
||
|
case "L":
|
||
|
case "T":
|
||
|
for (; i < x.length; i++)
|
||
|
s.push(" " + O.type + " " + x[i] + " " + y[i]);
|
||
|
break;
|
||
|
case "Q":
|
||
|
case "S":
|
||
|
x1 = svg_round_array(dots.x1.map(range_x.val2px));
|
||
|
y1 = svg_round_array(dots.y1.map(range_y.val2px));
|
||
|
for (; i < x.length; i++)
|
||
|
s.push(" " + O.type + " "
|
||
|
+ x1[i] + "," + y1[i] + " "
|
||
|
+ x[i] + "," + y[i]);
|
||
|
break;
|
||
|
case "C":
|
||
|
x1 = svg_round_array(dots.x1.map(range_x.val2px));
|
||
|
x2 = svg_round_array(dots.x2.map(range_x.val2px));
|
||
|
y1 = svg_round_array(dots.y1.map(range_y.val2px));
|
||
|
y2 = svg_round_array(dots.y2.map(range_y.val2px));
|
||
|
for (; i < x.length; i++)
|
||
|
s.push(" " + O.type + " "
|
||
|
+ x1[i] + "," + y1[i] + " "
|
||
|
+ x2[i] + "," + y2[i] + " "
|
||
|
+ x[i] + "," + y[i]);
|
||
|
break;
|
||
|
case "H":
|
||
|
f = O.type.length > 1 ? parseFloat(O.type.substr(1)) : 3;
|
||
|
if (i === 0) {
|
||
|
i++;
|
||
|
s.push(" S" + x[0] + "," + y[0] + " " + x[0] + "," + y[0]);
|
||
|
}
|
||
|
for (; i < x.length-1; i++)
|
||
|
s.push(" S" + (x[i] - Math.round(x[i] - x[i-1])/f) + ","
|
||
|
+ y[i] + " " + x[i] + "," + y[i]);
|
||
|
if (i < x.length)
|
||
|
s.push(" S" + x[i] + "," + y[i] + " " + x[i] + "," + y[i]);
|
||
|
break;
|
||
|
default:
|
||
|
TK.error("Unsupported graph type", O.type);
|
||
|
}
|
||
|
|
||
|
_end.call(this, dots, s);
|
||
|
E.setAttribute("d", s.join(""));
|
||
|
}
|
||
|
}
|
||
|
TK.Widget.prototype.redraw.call(this);
|
||
|
},
|
||
|
|
||
|
// GETTER & SETTER
|
||
|
set: function (key, value) {
|
||
|
if (key === "dots") {
|
||
|
value = transform_dots(value);
|
||
|
}
|
||
|
TK.Widget.prototype.set.call(this, key, value);
|
||
|
switch (key) {
|
||
|
case "range_x":
|
||
|
case "range_y":
|
||
|
this.add_range(value, key);
|
||
|
value.add_event("set", range_change_cb.bind(this));
|
||
|
break;
|
||
|
case "width":
|
||
|
this.range_x.set("basis", value);
|
||
|
break;
|
||
|
case "height":
|
||
|
this.range_y.set("basis", value);
|
||
|
break;
|
||
|
case "dots":
|
||
|
/**
|
||
|
* Is fired when the graph changes
|
||
|
* @event TK.Graph#graphchanged
|
||
|
*/
|
||
|
this.fire_event("graphchanged");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
})(this, this.TK);
|