diff --git a/HISTORY.md b/HISTORY.md
index a9a0ae64..3052ed8d 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -3,7 +3,14 @@ http://visjs.org
## not yet released, version 3.1.1
-(no changes yet)
+### General
+
+- Refactored Timeline and Graph2d to use the same core.
+
+### Graph2D
+
+- Added visible property to the groups.
+- Added getLegend() method.
## 2014-07-22, version 3.1.0
diff --git a/dist/vis.js b/dist/vis.js
index 2f0a7321..f0eb8d05 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 3.1.1-SNAPSHOT
- * @date 2014-07-22
+ * @date 2014-07-23
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -102,7 +102,7 @@ return /******/ (function(modules) { // webpackBootstrap
exports.Timeline = __webpack_require__(17);
exports.Graph2d = __webpack_require__(35);
exports.timeline = {
- DataStep: __webpack_require__(36),
+ DataStep: __webpack_require__(38),
Range: __webpack_require__(20),
stack: __webpack_require__(30),
TimeStep: __webpack_require__(25),
@@ -119,11 +119,11 @@ return /******/ (function(modules) { // webpackBootstrap
CurrentTime: __webpack_require__(26),
CustomTime: __webpack_require__(27),
DataAxis: __webpack_require__(37),
- GraphGroup: __webpack_require__(38),
+ GraphGroup: __webpack_require__(39),
Group: __webpack_require__(29),
ItemSet: __webpack_require__(28),
- Legend: __webpack_require__(39),
- LineGraph: __webpack_require__(40),
+ Legend: __webpack_require__(40),
+ LineGraph: __webpack_require__(36),
TimeAxis: __webpack_require__(24)
}
};
@@ -4114,7 +4114,7 @@ return /******/ (function(modules) { // webpackBootstrap
*/
exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
var element;
- // allocate SVG element, if it doesnt yet exist, create one.
+ // allocate DOM element, if it doesnt yet exist, create one.
if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
// check if there is an redundant element
if (JSONcontainer[elementType].redundant.length > 0) {
@@ -4180,12 +4180,14 @@ return /******/ (function(modules) { // webpackBootstrap
* @param className
*/
exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
- var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer);
- rect.setAttributeNS(null, "x", x - 0.5 * width);
- rect.setAttributeNS(null, "y", y);
- rect.setAttributeNS(null, "width", width);
- rect.setAttributeNS(null, "height", height);
- rect.setAttributeNS(null, "class", className);
+ // if (height != 0) {
+ var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer);
+ rect.setAttributeNS(null, "x", x - 0.5 * width);
+ rect.setAttributeNS(null, "y", y);
+ rect.setAttributeNS(null, "width", width);
+ rect.setAttributeNS(null, "height", height);
+ rect.setAttributeNS(null, "class", className);
+ // }
};
/***/ },
@@ -16684,7 +16686,7 @@ return /******/ (function(modules) { // webpackBootstrap
var TimeAxis = __webpack_require__(24);
var CurrentTime = __webpack_require__(26);
var CustomTime = __webpack_require__(27);
- var LineGraph = __webpack_require__(40);
+ var LineGraph = __webpack_require__(36);
/**
* Create a timeline visualization
@@ -16894,6 +16896,32 @@ return /******/ (function(modules) { // webpackBootstrap
this.linegraph.setGroups(newDataSet);
};
+ /**
+ *
+ * @param groupId
+ * @param width
+ * @param height
+ */
+ Graph2d.prototype.getLegend = function(groupId, width, height) {
+ if (width === undefined) {width = 15;}
+ if (height === undefined) {height = 15;}
+ if (this.linegraph.groups[groupId] !== undefined) {
+ return this.linegraph.groups[groupId].getLegend(width,height);
+ }
+ else {
+ return "cannot find group:" + groupId;
+ }
+ }
+
+ Graph2d.prototype.isGroupVisible = function(groupId) {
+ if (this.linegraph.groups[groupId] !== undefined) {
+ return this.linegraph.groups[groupId].visible;
+ }
+ else {
+ return false;
+ }
+ }
+
module.exports = Graph2d;
@@ -16902,2101 +16930,2130 @@ return /******/ (function(modules) { // webpackBootstrap
/* 36 */
/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(8);
+ var Component = __webpack_require__(22);
+ var DataAxis = __webpack_require__(37);
+ var GraphGroup = __webpack_require__(39);
+ var Legend = __webpack_require__(40);
+
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+
/**
- * @constructor DataStep
- * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
- * end data point. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
+ * This is the constructor of the LineGraph. It requires a Timeline body and options.
*
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ * @param body
+ * @param options
+ * @constructor
*/
- function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
- // variables
- this.current = 0;
+ function LineGraph(body, options) {
+ this.id = util.randomUUID();
+ this.body = body;
- this.autoScale = true;
- this.stepIndex = 0;
- this.step = 1;
- this.scale = 1;
+ this.defaultOptions = {
+ yAxisOrientation: 'left',
+ defaultGroup: 'default',
+ sort: true,
+ sampling: true,
+ graphHeight: '400px',
+ shaded: {
+ enabled: false,
+ orientation: 'bottom' // top, bottom
+ },
+ style: 'line', // line, bar
+ barChart: {
+ width: 50,
+ align: 'center' // left, center, right
+ },
+ catmullRom: {
+ enabled: true,
+ parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
+ alpha: 0.5
+ },
+ drawPoints: {
+ enabled: true,
+ size: 6,
+ style: 'square' // square, circle
+ },
+ dataAxis: {
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: false,
+ width: '40px',
+ visible: true
+ },
+ legend: {
+ enabled: false,
+ icons: true,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,right
+ },
+ right: {
+ visible: true,
+ position: 'top-right' // top/bottom - left,right
+ }
+ }
+ };
- this.marginStart;
- this.marginEnd;
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
+ this.groups = {};
- this.majorSteps = [1, 2, 5, 10];
- this.minorSteps = [0.25, 0.5, 1, 2];
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
- }
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
+ }
+ };
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
+ }
+ };
+ this.items = {}; // object with an Item for every data item
+ this.selection = []; // list with the ids of all selected nodes
+ this.lastStart = this.body.range.start;
+ this.touchParams = {}; // stores properties while dragging
- /**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Number} [start] The start date and time.
- * @param {Number} [end] The end date and time.
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
- */
- DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
- this._start = start;
- this._end = end;
+ this.svgElements = {};
+ this.setOptions(options);
+ this.groupsUsingDefaultStyles = [0];
- if (start == end) {
- this._start = start - 0.75;
- this._end = end + 1;
- }
+ this.body.emitter.on("rangechange",function() {
+ if (me.lastStart != 0) {
+ var offset = me.body.range.start - me.lastStart;
+ var range = me.body.range.end - me.body.range.start;
+ if (me.width != 0) {
+ var rangePerPixelInv = me.width/range;
+ var xOffset = offset * rangePerPixelInv;
+ me.svg.style.left = (-me.width - xOffset) + "px";
+ }
+ }
+ });
+ this.body.emitter.on("rangechanged", function() {
+ me.lastStart = me.body.range.start;
+ me.svg.style.left = util.option.asSize(-me.width);
+ me._updateGraph.apply(me);
+ });
- if (this.autoScale) {
- this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
- }
- this.setFirst();
- };
+ // create the HTML DOM
+ this._create();
+ this.body.emitter.emit("change");
+ }
+
+ LineGraph.prototype = new Component();
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Create the HTML DOM for the ItemSet
*/
- DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
- // round to floor
- var size = this._end - this._start;
- var safeSize = size * 1.1;
- var minimumStepValue = minimumStep * (safeSize / containerHeight);
- var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+ LineGraph.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'LineGraph';
+ this.dom.frame = frame;
- var minorStepIdx = -1;
- var magnitudefactor = Math.pow(10,orderOfMagnitude);
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "relative";
+ this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
+ this.svg.style.display = "block";
+ frame.appendChild(this.svg);
- var start = 0;
- if (orderOfMagnitude < 0) {
- start = orderOfMagnitude;
- }
+ // data axis
+ this.options.dataAxis.orientation = 'left';
+ this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
- var solutionFound = false;
- for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
- magnitudefactor = Math.pow(10,i);
- for (var j = 0; j < this.minorSteps.length; j++) {
- var stepSize = magnitudefactor * this.minorSteps[j];
- if (stepSize >= minimumStepValue) {
- solutionFound = true;
- minorStepIdx = j;
- break;
- }
- }
- if (solutionFound == true) {
- break;
- }
- }
- this.stepIndex = minorStepIdx;
- this.scale = magnitudefactor;
- this.step = magnitudefactor * this.minorSteps[minorStepIdx];
- };
+ this.options.dataAxis.orientation = 'right';
+ this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
+ delete this.options.dataAxis.orientation;
+ // legends
+ this.legendLeft = new Legend(this.body, this.options.legend, 'left');
+ this.legendRight = new Legend(this.body, this.options.legend, 'right');
+
+ this.show();
+ };
/**
- * Set the range iterator to the start date.
- */
- DataStep.prototype.first = function() {
- this.setFirst();
- };
-
- /**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
+ * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
+ * @param options
*/
- DataStep.prototype.setFirst = function() {
- var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
- var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
+ LineGraph.prototype.setOptions = function(options) {
+ if (options) {
+ var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+ util.mergeOptions(this.options, options,'legend');
- this.marginEnd = this.roundToMinor(niceEnd);
- this.marginStart = this.roundToMinor(niceStart);
- this.marginRange = this.marginEnd - this.marginStart;
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
+ }
- this.current = this.marginEnd;
+ if (this.yAxisLeft) {
+ if (options.dataAxis !== undefined) {
+ this.yAxisLeft.setOptions(this.options.dataAxis);
+ this.yAxisRight.setOptions(this.options.dataAxis);
+ }
+ }
- };
+ if (this.legendLeft) {
+ if (options.legend !== undefined) {
+ this.legendLeft.setOptions(this.options.legend);
+ this.legendRight.setOptions(this.options.legend);
+ }
+ }
- DataStep.prototype.roundToMinor = function(value) {
- var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
- if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
- return rounded + (this.scale * this.minorSteps[this.stepIndex]);
+ if (this.groups.hasOwnProperty(UNGROUPED)) {
+ this.groups[UNGROUPED].setOptions(options);
+ }
}
- else {
- return rounded;
+ if (this.dom.frame) {
+ this._updateGraph();
}
- }
-
-
- /**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
- */
- DataStep.prototype.hasNext = function () {
- return (this.current >= this.marginStart);
};
/**
- * Do the next step
+ * Hide the component from the DOM
*/
- DataStep.prototype.next = function() {
- var prev = this.current;
- this.current -= this.step;
-
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current == prev) {
- this.current = this._end;
+ LineGraph.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
};
/**
- * Do the next step
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
- DataStep.prototype.previous = function() {
- this.current += this.step;
- this.marginEnd += this.step;
- this.marginRange = this.marginEnd - this.marginStart;
+ LineGraph.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
};
-
/**
- * Get the current datetime
- * @return {String} current The current date
+ * Set items
+ * @param {vis.DataSet | null} items
*/
- DataStep.prototype.getCurrent = function() {
- var toPrecision = '' + Number(this.current).toPrecision(5);
- for (var i = toPrecision.length-1; i > 0; i--) {
- if (toPrecision[i] == "0") {
- toPrecision = toPrecision.slice(0,i);
- }
- else if (toPrecision[i] == "." || toPrecision[i] == ",") {
- toPrecision = toPrecision.slice(0,i);
- break;
- }
- else{
- break;
- }
- }
+ LineGraph.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
- return toPrecision;
- };
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
+ });
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
- /**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
- */
- DataStep.prototype.snap = function(date) {
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
+ }
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Check if the current value is a major value (for example when the step
- * is DAY, a major value is each first day of the MONTH)
- * @return {boolean} true if current date is major, else false.
+ * Set groups
+ * @param {vis.DataSet} groups
*/
- DataStep.prototype.isMajor = function() {
- return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
- };
+ LineGraph.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
- module.exports = DataStep;
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
+ }
-/***/ },
-/* 37 */
-/***/ function(module, exports, __webpack_require__) {
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Component = __webpack_require__(22);
- var DataStep = __webpack_require__(36);
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
- /**
- * A horizontal time axis
- * @param {Object} [options] See DataAxis.setOptions for the available
- * options.
- * @constructor DataAxis
- * @extends Component
- * @param body
- */
- function DataAxis (body, options, svg) {
- this.id = util.randomUUID();
- this.body = body;
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
+ }
+ this._onUpdate();
+ };
- this.defaultOptions = {
- orientation: 'left', // supported: 'left', 'right'
- showMinorLabels: true,
- showMajorLabels: true,
- icons: true,
- majorLinesOffset: 7,
- minorLinesOffset: 4,
- labelOffsetX: 10,
- labelOffsetY: 2,
- iconWidth: 20,
- width: '40px',
- visible: true
- };
- this.linegraphSVG = svg;
- this.props = {};
- this.DOMelements = { // dynamic elements
- lines: {},
- labels: {}
- };
- this.dom = {};
+ LineGraph.prototype._onUpdate = function(ids) {
+ this._updateUngrouped();
+ this._updateAllGroupData();
+ this._updateGraph();
+ this.redraw();
+ };
+ LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onUpdateGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ var group = this.groupsData.get(groupIds[i]);
+ this._updateGroup(group, groupIds[i]);
+ }
- this.range = {start:0, end:0};
-
- this.options = util.extend({}, this.defaultOptions);
- this.conversionFactor = 1;
-
- this.setOptions(options);
- this.width = Number(('' + this.options.width).replace("px",""));
- this.minWidth = this.width;
- this.height = this.linegraphSVG.offsetHeight;
-
- this.stepPixels = 25;
- this.stepPixelsForced = 25;
- this.lineOffset = 0;
- this.master = true;
- this.svgElements = {};
-
-
- this.groups = {};
- this.amountOfGroups = 0;
-
- // create the HTML DOM
- this._create();
- }
-
- DataAxis.prototype = new Component();
-
-
-
- DataAxis.prototype.addGroup = function(label, graphOptions) {
- if (!this.groups.hasOwnProperty(label)) {
- this.groups[label] = graphOptions;
- }
- this.amountOfGroups += 1;
- };
-
- DataAxis.prototype.updateGroup = function(label, graphOptions) {
- this.groups[label] = graphOptions;
- };
-
- DataAxis.prototype.removeGroup = function(label) {
- if (this.groups.hasOwnProperty(label)) {
- delete this.groups[label];
- this.amountOfGroups -= 1;
- }
+ this._updateGraph();
+ this.redraw();
};
+ LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
-
- DataAxis.prototype.setOptions = function (options) {
- if (options) {
- var redraw = false;
- if (this.options.orientation != options.orientation && options.orientation !== undefined) {
- redraw = true;
- }
- var fields = [
- 'orientation',
- 'showMinorLabels',
- 'showMajorLabels',
- 'icons',
- 'majorLinesOffset',
- 'minorLinesOffset',
- 'labelOffsetX',
- 'labelOffsetY',
- 'iconWidth',
- 'width',
- 'visible'];
- util.selectiveExtend(fields, this.options, options);
-
- this.minWidth = Number(('' + this.options.width).replace("px",""));
-
- if (redraw == true && this.dom.frame) {
- this.hide();
- this.show();
+ LineGraph.prototype._onRemoveGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (!this.groups.hasOwnProperty(groupIds[i])) {
+ if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
+ this.yAxisRight.removeGroup(groupIds[i]);
+ this.legendRight.removeGroup(groupIds[i]);
+ this.legendRight.redraw();
+ }
+ else {
+ this.yAxisLeft.removeGroup(groupIds[i]);
+ this.legendLeft.removeGroup(groupIds[i]);
+ this.legendLeft.redraw();
+ }
+ delete this.groups[groupIds[i]];
}
}
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
-
/**
- * Create the HTML DOM for the DataAxis
+ * update a group object
+ *
+ * @param group
+ * @param groupId
+ * @private
*/
- DataAxis.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.style.width = this.options.width;
- this.dom.frame.style.height = this.height;
-
- this.dom.lineContainer = document.createElement('div');
- this.dom.lineContainer.style.width = '100%';
- this.dom.lineContainer.style.height = this.height;
-
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "absolute";
- this.svg.style.top = '0px';
- this.svg.style.height = '100%';
- this.svg.style.width = '100%';
- this.svg.style.display = "block";
- this.dom.frame.appendChild(this.svg);
- };
-
- DataAxis.prototype._redrawGroupIcons = function () {
- DOMutil.prepareElements(this.svgElements);
-
- var x;
- var iconWidth = this.options.iconWidth;
- var iconHeight = 15;
- var iconOffset = 4;
- var y = iconOffset + 0.5 * iconHeight;
-
- if (this.options.orientation == 'left') {
- x = iconOffset;
+ LineGraph.prototype._updateGroup = function (group, groupId) {
+ if (!this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.addGroup(groupId, this.groups[groupId]);
+ this.legendRight.addGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
+ this.legendLeft.addGroup(groupId, this.groups[groupId]);
+ }
}
else {
- x = this.width - iconWidth - iconOffset;
+ this.groups[groupId].update(group);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
+ this.legendRight.updateGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
+ this.legendLeft.updateGroup(groupId, this.groups[groupId]);
+ }
}
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
+ };
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + iconOffset;
+ LineGraph.prototype._updateAllGroupData = function () {
+ if (this.itemsData != null) {
+ var groupsContent = {};
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupsContent[groupId] = [];
+ }
+ }
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ item.x = util.convert(item.x,"Date");
+ groupsContent[item.group].push(item);
+ }
+ }
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].setItems(groupsContent[groupId]);
+ }
}
}
-
- DOMutil.cleanupElements(this.svgElements);
};
/**
- * Create the HTML DOM for the DataAxis
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified. This anonymous group is called 'graph'.
+ * @protected
*/
- DataAxis.prototype.show = function() {
- if (!this.dom.frame.parentNode) {
- if (this.options.orientation == 'left') {
- this.body.dom.left.appendChild(this.dom.frame);
+ LineGraph.prototype._updateUngrouped = function() {
+ if (this.itemsData != null) {
+ // var t0 = new Date();
+ var group = {id: UNGROUPED, content: this.options.defaultGroup};
+ this._updateGroup(group, UNGROUPED);
+ var ungroupedCounter = 0;
+ if (this.itemsData) {
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ if (item != undefined) {
+ if (item.hasOwnProperty('group')) {
+ if (item.group === undefined) {
+ item.group = UNGROUPED;
+ }
+ }
+ else {
+ item.group = UNGROUPED;
+ }
+ ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
+ }
+ }
+ }
}
- else {
- this.body.dom.right.appendChild(this.dom.frame);
+
+ // much much slower
+ // var datapoints = this.itemsData.get({
+ // filter: function (item) {return item.group === undefined;},
+ // showInternalIds:true
+ // });
+ // if (datapoints.length > 0) {
+ // var updateQuery = [];
+ // for (var i = 0; i < datapoints.length; i++) {
+ // updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
+ // }
+ // this.itemsData.update(updateQuery, true);
+ // }
+ // var t1 = new Date();
+ // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
+ if (ungroupedCounter == 0) {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
}
+ // console.log("getting amount ungrouped",new Date() - t1);
+ // console.log("putting in ungrouped",new Date() - t0);
}
-
- if (!this.dom.lineContainer.parentNode) {
- this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
+ else {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
}
+
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
+
/**
- * Create the HTML DOM for the DataAxis
+ * Redraw the component, mandatory function
+ * @return {boolean} Returns true if the component is resized
*/
- DataAxis.prototype.hide = function() {
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- }
+ LineGraph.prototype.redraw = function() {
+ var resized = false;
- if (this.dom.lineContainer.parentNode) {
- this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
- }
- };
+ this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
+ if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
+ resized = true;
+ }
+ // check if this component is resized
+ resized = this._isResized() || resized;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.body.range.end - this.body.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- /**
- * Set a range (start and end)
- * @param end
- * @param start
- * @param end
- */
- DataAxis.prototype.setRange = function (start, end) {
- this.range.start = start;
- this.range.end = end;
+ // calculate actual size and position
+ this.width = this.dom.frame.offsetWidth;
+
+ // the svg element is three times as big as the width, this allows for fully dragging left and right
+ // without reloading the graph. the controls for this are bound to events in the constructor
+ if (resized == true) {
+ this.svg.style.width = util.option.asSize(3*this.width);
+ this.svg.style.left = util.option.asSize(-this.width);
+ }
+ if (zoomed == true) {
+ this._updateGraph();
+ }
+
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
+
+ return resized;
};
/**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
+ * Update and redraw the graph.
+ *
*/
- DataAxis.prototype.redraw = function () {
- var changeCalled = false;
- if (this.amountOfGroups == 0) {
- this.hide();
- }
- else {
- this.show();
- this.height = Number(this.linegraphSVG.style.height.replace("px",""));
- // svg offsetheight did not work in firefox and explorer...
-
- this.dom.lineContainer.style.height = this.height + 'px';
- this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
+ LineGraph.prototype._updateGraph = function () {
+ // reset the svg elements
+ DOMutil.prepareElements(this.svgElements);
- var props = this.props;
- var frame = this.dom.frame;
+ if (this.width != 0 && this.itemsData != null) {
+ var group, groupData, preprocessedGroup, i;
+ var preprocessedGroupData = [];
+ var processedGroupData = [];
+ var groupRanges = [];
+ var changeCalled = false;
- // update classname
- frame.className = 'dataaxis';
+ // getting group Ids
+ var groupIds = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupIds.push(groupId);
+ }
+ }
- // calculate character width and height
- this._calculateCharSize();
+ // this is the range of the SVG canvas
+ var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
+ var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
- var orientation = this.options.orientation;
- var showMinorLabels = this.options.showMinorLabels;
- var showMajorLabels = this.options.showMajorLabels;
+ // first select and preprocess the data from the datasets.
+ // the groups have their preselection of data, we now loop over this data to see
+ // what data we need to draw. Sorted data is much faster.
+ // more optimization is possible by doing the sampling before and using the binary search
+ // to find the end date to determine the increment.
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.visible == true) {
+ groupData = [];
+ // optimization for sorted data
+ if (group.options.sort == true) {
+ var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
+
+ for (var j = guess; j < group.itemsData.length; j++) {
+ var item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > maxDate) {
+ groupData.push(item);
+ break;
+ }
+ else {
+ groupData.push(item);
+ }
+ }
+ }
+ }
+ else {
+ for (var j = 0; j < group.itemsData.length; j++) {
+ var item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > minDate && item.x < maxDate) {
+ groupData.push(item);
+ }
+ }
+ }
+ }
+ // preprocess, split into ranges and data
+ preprocessedGroup = this._preprocessData(groupData, group);
+ groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
+ preprocessedGroupData.push(preprocessedGroup.data);
+ }
+ else {
+ groupRanges.push({});
+ preprocessedGroupData.push([]);
+ }
+ }
- // determine the width and height of the elemens for the axis
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ // update the Y axis first, we use this data to draw at the correct Y points
+ // changeCalled is required to clean the SVG on a change emit.
+ changeCalled = this._updateYAxis(groupIds, groupRanges);
+ if (changeCalled == true) {
+ DOMutil.cleanupElements(this.svgElements);
+ this.body.emitter.emit("change");
+ return;
+ }
- props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
- props.minorLineHeight = 1;
- props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
- props.majorLineHeight = 1;
+ // with the yAxis scaled correctly, use this to get the Y values of the points.
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
+ }
- // take frame offline while updating (is almost twice as fast)
- if (orientation == 'left') {
- frame.style.top = '0';
- frame.style.left = '0';
- frame.style.bottom = '';
- frame.style.width = this.width + 'px';
- frame.style.height = this.height + "px";
- }
- else { // right
- frame.style.top = '';
- frame.style.bottom = '0';
- frame.style.left = '0';
- frame.style.width = this.width + 'px';
- frame.style.height = this.height + "px";
- }
- changeCalled = this._redrawLabels();
- if (this.options.icons == true) {
- this._redrawGroupIcons();
+ // draw the groups
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.visible == true) {
+ if (group.options.style == 'line') {
+ this._drawLineGraph(processedGroupData[i], group);
+ }
+ else {
+ this._drawBarGraph (processedGroupData[i], group);
+ }
+ }
+ }
}
}
- return changeCalled;
+
+ // cleanup unused svg elements
+ DOMutil.cleanupElements(this.svgElements);
};
/**
- * Repaint major and minor text labels and vertical grid lines
+ * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
+ * @param {array} groupIds
* @private
*/
- DataAxis.prototype._redrawLabels = function () {
- DOMutil.prepareElements(this.DOMelements);
-
- var orientation = this.options['orientation'];
-
- // calculate range and step (step such that we have space for 7 characters per label)
- var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
- var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight);
- this.step = step;
- step.first();
-
- // get the distance in pixels for a step
- var stepPixels = this.dom.frame.offsetHeight / ((step.marginRange / step.step) + 1);
- this.stepPixels = stepPixels;
-
- var amountOfSteps = this.height / stepPixels;
- var stepDifference = 0;
-
- if (this.master == false) {
- stepPixels = this.stepPixelsForced;
- stepDifference = Math.round((this.height / stepPixels) - amountOfSteps);
- for (var i = 0; i < 0.5 * stepDifference; i++) {
- step.previous();
- }
- amountOfSteps = this.height / stepPixels;
- }
-
-
- this.valueAtZero = step.marginEnd;
- var marginStartPos = 0;
-
- // do not draw the first label
- var max = 1;
- step.next();
-
- this.maxLabelSize = 0;
- var y = 0;
- while (max < Math.round(amountOfSteps)) {
-
- y = Math.round(max * stepPixels);
- marginStartPos = max * stepPixels;
- var isMajor = step.isMajor();
+ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
+ var changeCalled = false;
+ var yAxisLeftUsed = false;
+ var yAxisRightUsed = false;
+ var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
+ var orientation = 'left';
- if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
- this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
- }
+ // if groups are present
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ orientation = 'left';
+ var group = this.groups[groupIds[i]];
+ if (group.visible == true) {
+ if (group.options.yAxisOrientation == 'right') {
+ orientation = 'right';
+ }
- if (isMajor && this.options['showMajorLabels'] && this.master == true ||
- this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
+ minVal = groupRanges[i].min;
+ maxVal = groupRanges[i].max;
- if (y >= 0) {
- this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
+ if (orientation == 'left') {
+ yAxisLeftUsed = true;
+ minLeft = minLeft > minVal ? minVal : minLeft;
+ maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
+ }
+ else {
+ yAxisRightUsed = true;
+ minRight = minRight > minVal ? minVal : minRight;
+ maxRight = maxRight < maxVal ? maxVal : maxRight;
+ }
}
- this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
- else {
- this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
+ console.log(yAxisRightUsed)
+ if (yAxisLeftUsed == true) {
+ this.yAxisLeft.setRange(minLeft, maxLeft);
+ }
+ if (yAxisRightUsed == true) {
+ this.yAxisRight.setRange(minRight, maxRight);
}
-
- step.next();
- max++;
}
- this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step);
+ changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
+ changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
- var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
- // this will resize the yAxis to accomodate the labels.
- if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
- this.width = this.maxLabelSize + offset;
- this.options.width = this.width + "px";
- DOMutil.cleanupElements(this.DOMelements);
- this.redraw();
- return true;
- }
- // this will resize the yAxis if it is too big for the labels.
- else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
- this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
- this.options.width = this.width + "px";
- DOMutil.cleanupElements(this.DOMelements);
- this.redraw();
- return true;
+ if (yAxisRightUsed == true && yAxisLeftUsed == true) {
+ this.yAxisLeft.drawIcons = true;
+ this.yAxisRight.drawIcons = true;
}
else {
- DOMutil.cleanupElements(this.DOMelements);
- return false;
+ this.yAxisLeft.drawIcons = false;
+ this.yAxisRight.drawIcons = false;
}
- };
- /**
- * Create a label for the axis at position x
- * @private
- * @param y
- * @param text
- * @param orientation
- * @param className
- * @param characterHeight
- */
- DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
- // reuse redundant label
- var label = DOMutil.getDOMElement('div',this.DOMelements, this.dom.frame); //this.dom.redundant.labels.shift();
- label.className = className;
- label.innerHTML = text;
+ this.yAxisRight.master = !yAxisLeftUsed;
- if (orientation == 'left') {
- label.style.left = '-' + this.options.labelOffsetX + 'px';
- label.style.textAlign = "right";
+ if (this.yAxisRight.master == false) {
+ if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
+ else {this.yAxisLeft.lineOffset = 0;}
+
+ changeCalled = this.yAxisLeft.redraw() || changeCalled;
+ this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
}
else {
- label.style.right = '-' + this.options.labelOffsetX + 'px';
- label.style.textAlign = "left";
- }
-
- label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
-
- text += '';
-
- var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
- if (this.maxLabelSize < text.length * largestWidth) {
- this.maxLabelSize = text.length * largestWidth;
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
}
+ return changeCalled;
};
/**
- * Create a minor line for the axis at position y
- * @param y
- * @param orientation
- * @param className
- * @param offset
- * @param width
+ * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
+ *
+ * @param {boolean} axisUsed
+ * @returns {boolean}
+ * @private
+ * @param axis
*/
- DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
- if (this.master == true) {
- var line = DOMutil.getDOMElement('div',this.DOMelements, this.dom.lineContainer);//this.dom.redundant.lines.shift();
- line.className = className;
- line.innerHTML = '';
-
- if (orientation == 'left') {
- line.style.left = (this.width - offset) + 'px';
+ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
+ var changed = false;
+ if (axisUsed == false) {
+ if (axis.dom.frame.parentNode) {
+ axis.hide();
+ changed = true;
}
- else {
- line.style.right = (this.width - offset) + 'px';
+ }
+ else {
+ if (!axis.dom.frame.parentNode) {
+ axis.show();
+ changed = true;
}
-
- line.style.width = width + 'px';
- line.style.top = y + 'px';
}
- };
-
-
- DataAxis.prototype.convertValue = function (value) {
- var invertedValue = this.valueAtZero - value;
- var convertedValue = invertedValue * this.conversionFactor;
- return convertedValue; // the -2 is to compensate for the borders
+ return changed;
};
/**
- * Determine the size of text on the axis (both major and minor axis).
- * The size is calculated only once and then cached in this.props.
- * @private
+ * draw a bar graph
+ * @param datapoints
+ * @param group
*/
- DataAxis.prototype._calculateCharSize = function () {
- // determine the char width and height on the minor axis
- if (!('minorCharHeight' in this.props)) {
+ LineGraph.prototype._drawBarGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var coreDistance;
+ var minWidth = 0.1 * group.options.barChart.width;
+ var offset = 0;
+ var width = group.options.barChart.width;
- var textMinor = document.createTextNode('0');
- var measureCharMinor = document.createElement('DIV');
- measureCharMinor.className = 'yAxis minor measure';
- measureCharMinor.appendChild(textMinor);
- this.dom.frame.appendChild(measureCharMinor);
+ if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
+ else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
- this.props.minorCharHeight = measureCharMinor.clientHeight;
- this.props.minorCharWidth = measureCharMinor.clientWidth;
+ for (var i = 0; i < dataset.length; i++) {
+ // dynammically downscale the width so there is no overlap up to 1/10th the original width
+ if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
+ if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
+ if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
- this.dom.frame.removeChild(measureCharMinor);
+ DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
+ }
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
+ }
+ }
}
+ };
- if (!('majorCharHeight' in this.props)) {
- var textMajor = document.createTextNode('0');
- var measureCharMajor = document.createElement('DIV');
- measureCharMajor.className = 'yAxis major measure';
- measureCharMajor.appendChild(textMajor);
- this.dom.frame.appendChild(measureCharMajor);
- this.props.majorCharHeight = measureCharMajor.clientHeight;
- this.props.majorCharWidth = measureCharMajor.clientWidth;
+ /**
+ * draw a line graph
+ *
+ * @param datapoints
+ * @param group
+ */
+ LineGraph.prototype._drawLineGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var path, d;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
+ path.setAttributeNS(null, "class", group.className);
- this.dom.frame.removeChild(measureCharMajor);
+ // construct path from dataset
+ if (group.options.catmullRom.enabled == true) {
+ d = this._catmullRom(dataset, group);
+ }
+ else {
+ d = this._linear(dataset);
+ }
+
+ // append with points for fill and finalize the path
+ if (group.options.shaded.enabled == true) {
+ var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
+ var dFill;
+ if (group.options.shaded.orientation == 'top') {
+ dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
+ }
+ else {
+ dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
+ }
+ fillPath.setAttributeNS(null, "class", group.className + " fill");
+ fillPath.setAttributeNS(null, "d", dFill);
+ }
+ // copy properties to path for drawing.
+ path.setAttributeNS(null, "d", "M" + d);
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg);
+ }
+ }
}
};
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * draw the data points
+ *
+ * @param dataset
+ * @param JSONcontainer
+ * @param svg
+ * @param group
*/
- DataAxis.prototype.snap = function(date) {
- return this.step.snap(date);
+ LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
+ if (offset === undefined) {offset = 0;}
+ for (var i = 0; i < dataset.length; i++) {
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
+ }
};
- module.exports = DataAxis;
-
-
-/***/ },
-/* 38 */
-/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
/**
- * @constructor Group
- * @param {Number | String} groupId
- * @param {Object} data
- * @param {ItemSet} itemSet
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
+ *
+ * @param datapoints
+ * @returns {Array}
+ * @private
*/
- function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
- this.id = groupId;
- var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
- this.options = util.selectiveBridgeObject(fields,options);
- this.usingDefaultStyle = group.className === undefined;
- this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
- this.zeroPosition = 0;
- this.update(group);
- if (this.usingDefaultStyle == true) {
- this.groupsUsingDefaultStyles[0] += 1;
- }
- this.itemsData = [];
- }
+ LineGraph.prototype._preprocessData = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
- GraphGroup.prototype.setItems = function(items) {
- if (items != null) {
- this.itemsData = items;
- if (this.options.sort == true) {
- this.itemsData.sort(function (a,b) {return a.x - b.x;})
- }
+ var increment = 1;
+ var amountOfPoints = datapoints.length;
+
+ var yMin = datapoints[0].y;
+ var yMax = datapoints[0].y;
+
+ // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
+ // of width changing of the yAxis.
+ if (group.options.sampling == true) {
+ var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
+ var pointsPerPixel = amountOfPoints/xDistance;
+ increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
}
- else {
- this.itemsData = [];
+
+ for (var i = 0; i < amountOfPoints; i += increment) {
+ xValue = toScreen(datapoints[i].x) + this.width - 1;
+ yValue = datapoints[i].y;
+ extractedData.push({x: xValue, y: yValue});
+ yMin = yMin > yValue ? yValue : yMin;
+ yMax = yMax < yValue ? yValue : yMax;
}
- };
- GraphGroup.prototype.setZeroPosition = function(pos) {
- this.zeroPosition = pos;
+ // extractedData.sort(function (a,b) {return a.x - b.x;});
+ return {min: yMin, max: yMax, data: extractedData};
};
- GraphGroup.prototype.setOptions = function(options) {
- if (options !== undefined) {
- var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
- util.selectiveDeepExtend(fields, this.options, options);
+ /**
+ * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp.
+ *
+ * @param datapoints
+ * @param options
+ * @returns {Array}
+ * @private
+ */
+ LineGraph.prototype._convertYvalues = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var axis = this.yAxisLeft;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
+ if (group.options.yAxisOrientation == 'right') {
+ axis = this.yAxisRight;
+ }
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = datapoints[i].x;
+ yValue = Math.round(axis.convertValue(datapoints[i].y));
+ extractedData.push({x: xValue, y: yValue});
}
- };
- GraphGroup.prototype.update = function(group) {
- this.group = group;
- this.content = group.content || 'graph';
- this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
- this.setOptions(group.options);
+ group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
+
+ // extractedData.sort(function (a,b) {return a.x - b.x;});
+ return extractedData;
};
- GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
- var fillHeight = iconHeight * 0.5;
- var path, fillPath;
- var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
- outline.setAttributeNS(null, "x", x);
- outline.setAttributeNS(null, "y", y - fillHeight);
- outline.setAttributeNS(null, "width", iconWidth);
- outline.setAttributeNS(null, "height", 2*fillHeight);
- outline.setAttributeNS(null, "class", "outline");
+ /**
+ * This uses an uniform parametrization of the CatmullRom algorithm:
+ * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._catmullRomUniform = function(data) {
+ // catmull rom
+ var p0, p1, p2, p3, bp1, bp2;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var normalization = 1/6;
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- if (this.options.style == 'line') {
- path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- path.setAttributeNS(null, "class", this.className);
- path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
- if (this.options.shaded.enabled == true) {
- fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- if (this.options.shaded.orientation == 'top') {
- fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
- "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
- }
- else {
- fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
- "L"+x+"," + (y + fillHeight) + " " +
- "L"+ (x + iconWidth) + "," + (y + fillHeight) +
- "L"+ (x + iconWidth) + ","+y);
- }
- fillPath.setAttributeNS(null, "class", this.className + " iconFill");
- }
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- if (this.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
- }
+
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ // 0 1 0 0
+ // -1/6 1 1/6 0
+ // 0 1/6 1 -1/6
+ // 0 0 1 0
+
+ // bp0 = { x: p1.x, y: p1.y };
+ bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
+ bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
+ // bp0 = { x: p2.x, y: p2.y };
+
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
+
+ return d;
+ };
+
+ /**
+ * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
+ * By default, the centripetal parameterization is used because this gives the nicest results.
+ * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
+ *
+ * One optimization can be used to reuse distances since this is a sliding window approach.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._catmullRom = function(data, group) {
+ var alpha = group.options.catmullRom.alpha;
+ if (alpha == 0 || alpha === undefined) {
+ return this._catmullRomUniform(data);
}
else {
- var barWidth = Math.round(0.3 * iconWidth);
- var bar1Height = Math.round(0.4 * iconHeight);
- var bar2Height = Math.round(0.75 * iconHeight);
+ var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
+ var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- var offset = Math.round((iconWidth - (2 * barWidth))/3);
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
- DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
+ d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
+ d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
+
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ //
+ // A = 2d1^2a + 3d1^a * d2^a + d3^2a
+ // B = 2d3^2a + 3d3^a * d2^a + d2^2a
+ //
+ // [ 0 1 0 0 ]
+ // [ -d2^2a/N A/N d1^2a/N 0 ]
+ // [ 0 d3^2a/M B/M -d2^2a/M ]
+ // [ 0 0 1 0 ]
+
+ // [ 0 1 0 0 ]
+ // [ -d2pow2a/N A/N d1pow2a/N 0 ]
+ // [ 0 d3pow2a/M B/M -d2pow2a/M ]
+ // [ 0 0 1 0 ]
+
+ d3powA = Math.pow(d3, alpha);
+ d3pow2A = Math.pow(d3,2*alpha);
+ d2powA = Math.pow(d2, alpha);
+ d2pow2A = Math.pow(d2,2*alpha);
+ d1powA = Math.pow(d1, alpha);
+ d1pow2A = Math.pow(d1,2*alpha);
+
+ A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
+ B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
+ N = 3*d1powA * (d1powA + d2powA);
+ if (N > 0) {N = 1 / N;}
+ M = 3*d3powA * (d3powA + d2powA);
+ if (M > 0) {M = 1 / M;}
+
+ bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
+ y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
+
+ bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
+ y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+
+ if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
+ if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
+
+ return d;
}
};
- module.exports = GraphGroup;
+ /**
+ * this generates the SVG path for a linear drawing between datapoints.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._linear = function(data) {
+ // linear
+ var d = "";
+ for (var i = 0; i < data.length; i++) {
+ if (i == 0) {
+ d += data[i].x + "," + data[i].y;
+ }
+ else {
+ d += " " + data[i].x + "," + data[i].y;
+ }
+ }
+ return d;
+ };
+
+ module.exports = LineGraph;
/***/ },
-/* 39 */
+/* 37 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
var DOMutil = __webpack_require__(6);
var Component = __webpack_require__(22);
+ var DataStep = __webpack_require__(38);
/**
- * Legend for Graph2d
+ * A horizontal time axis
+ * @param {Object} [options] See DataAxis.setOptions for the available
+ * options.
+ * @constructor DataAxis
+ * @extends Component
+ * @param body
*/
- function Legend(body, options, side) {
+ function DataAxis (body, options, svg) {
+ this.id = util.randomUUID();
this.body = body;
+
this.defaultOptions = {
- enabled: true,
+ orientation: 'left', // supported: 'left', 'right'
+ showMinorLabels: true,
+ showMajorLabels: true,
icons: true,
- iconSize: 20,
- iconSpacing: 6,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
- },
- right: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
- }
- }
- this.side = side;
- this.options = util.extend({},this.defaultOptions);
-
- this.svgElements = {};
+ majorLinesOffset: 7,
+ minorLinesOffset: 4,
+ labelOffsetX: 10,
+ labelOffsetY: 2,
+ iconWidth: 20,
+ width: '40px',
+ visible: true
+ };
+
+ this.linegraphSVG = svg;
+ this.props = {};
+ this.DOMelements = { // dynamic elements
+ lines: {},
+ labels: {}
+ };
+
this.dom = {};
+
+ this.range = {start:0, end:0};
+
+ this.options = util.extend({}, this.defaultOptions);
+ this.conversionFactor = 1;
+
+ this.setOptions(options);
+ this.width = Number(('' + this.options.width).replace("px",""));
+ this.minWidth = this.width;
+ this.height = this.linegraphSVG.offsetHeight;
+
+ this.stepPixels = 25;
+ this.stepPixelsForced = 25;
+ this.lineOffset = 0;
+ this.master = true;
+ this.svgElements = {};
+
+
this.groups = {};
this.amountOfGroups = 0;
- this._create();
- this.setOptions(options);
+ // create the HTML DOM
+ this._create();
}
- Legend.prototype = new Component();
+ DataAxis.prototype = new Component();
- Legend.prototype.addGroup = function(label, graphOptions) {
+
+ DataAxis.prototype.addGroup = function(label, graphOptions) {
if (!this.groups.hasOwnProperty(label)) {
this.groups[label] = graphOptions;
}
this.amountOfGroups += 1;
};
- Legend.prototype.updateGroup = function(label, graphOptions) {
+ DataAxis.prototype.updateGroup = function(label, graphOptions) {
this.groups[label] = graphOptions;
};
- Legend.prototype.removeGroup = function(label) {
+ DataAxis.prototype.removeGroup = function(label) {
if (this.groups.hasOwnProperty(label)) {
delete this.groups[label];
this.amountOfGroups -= 1;
}
};
- Legend.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.className = 'legend';
- this.dom.frame.style.position = "absolute";
- this.dom.frame.style.top = "10px";
- this.dom.frame.style.display = "block";
-
- this.dom.textArea = document.createElement('div');
- this.dom.textArea.className = 'legendText';
- this.dom.textArea.style.position = "relative";
- this.dom.textArea.style.top = "0px";
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = 'absolute';
- this.svg.style.top = 0 +'px';
- this.svg.style.width = this.options.iconSize + 5 + 'px';
+ DataAxis.prototype.setOptions = function (options) {
+ if (options) {
+ var redraw = false;
+ if (this.options.orientation != options.orientation && options.orientation !== undefined) {
+ redraw = true;
+ }
+ var fields = [
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'icons',
+ 'majorLinesOffset',
+ 'minorLinesOffset',
+ 'labelOffsetX',
+ 'labelOffsetY',
+ 'iconWidth',
+ 'width',
+ 'visible'];
+ util.selectiveExtend(fields, this.options, options);
- this.dom.frame.appendChild(this.svg);
- this.dom.frame.appendChild(this.dom.textArea);
- };
+ this.minWidth = Number(('' + this.options.width).replace("px",""));
- /**
- * Hide the component from the DOM
- */
- Legend.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
+ if (redraw == true && this.dom.frame) {
+ this.hide();
+ this.show();
+ }
}
};
+
/**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
+ * Create the HTML DOM for the DataAxis
*/
- Legend.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
- }
- };
+ DataAxis.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.style.width = this.options.width;
+ this.dom.frame.style.height = this.height;
- Legend.prototype.setOptions = function(options) {
- var fields = ['enabled','orientation','icons','left','right'];
- util.selectiveDeepExtend(fields, this.options, options);
+ this.dom.lineContainer = document.createElement('div');
+ this.dom.lineContainer.style.width = '100%';
+ this.dom.lineContainer.style.height = this.height;
+
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "absolute";
+ this.svg.style.top = '0px';
+ this.svg.style.height = '100%';
+ this.svg.style.width = '100%';
+ this.svg.style.display = "block";
+ this.dom.frame.appendChild(this.svg);
};
- Legend.prototype.redraw = function() {
- if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) {
- this.hide();
+ DataAxis.prototype._redrawGroupIcons = function () {
+ DOMutil.prepareElements(this.svgElements);
+
+ var x;
+ var iconWidth = this.options.iconWidth;
+ var iconHeight = 15;
+ var iconOffset = 4;
+ var y = iconOffset + 0.5 * iconHeight;
+
+ if (this.options.orientation == 'left') {
+ x = iconOffset;
}
else {
- this.show();
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
- this.dom.frame.style.left = '4px';
- this.dom.frame.style.textAlign = "left";
- this.dom.textArea.style.textAlign = "left";
- this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.right = '';
- this.svg.style.left = 0 +'px';
- this.svg.style.right = '';
- }
- else {
- this.dom.frame.style.right = '4px';
- this.dom.frame.style.textAlign = "right";
- this.dom.textArea.style.textAlign = "right";
- this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.left = '';
- this.svg.style.right = 0 +'px';
- this.svg.style.left = '';
- }
+ x = this.width - iconWidth - iconOffset;
+ }
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
- this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.bottom = '';
- }
- else {
- this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.top = '';
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + iconOffset;
+ }
}
+ }
- if (this.options.icons == false) {
- this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
- this.dom.textArea.style.right = '';
- this.dom.textArea.style.left = '';
- this.svg.style.width = '0px';
+ DOMutil.cleanupElements(this.svgElements);
+ };
+
+ /**
+ * Create the HTML DOM for the DataAxis
+ */
+ DataAxis.prototype.show = function() {
+ if (!this.dom.frame.parentNode) {
+ if (this.options.orientation == 'left') {
+ this.body.dom.left.appendChild(this.dom.frame);
}
else {
- this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
- this.drawLegendIcons();
+ this.body.dom.right.appendChild(this.dom.frame);
}
+ }
- var content = '';
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- content += this.groups[groupId].content + '
';
- }
- }
- this.dom.textArea.innerHTML = content;
- this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ if (!this.dom.lineContainer.parentNode) {
+ this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
}
};
- Legend.prototype.drawLegendIcons = function() {
+ /**
+ * Create the HTML DOM for the DataAxis
+ */
+ DataAxis.prototype.hide = function() {
if (this.dom.frame.parentNode) {
- DOMutil.prepareElements(this.svgElements);
- var padding = window.getComputedStyle(this.dom.frame).paddingTop;
- var iconOffset = Number(padding.replace('px',''));
- var x = iconOffset;
- var iconWidth = this.options.iconSize;
- var iconHeight = 0.75 * this.options.iconSize;
- var y = iconOffset + 0.5 * iconHeight + 3;
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
- this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+ if (this.dom.lineContainer.parentNode) {
+ this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
+ }
+ };
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + this.options.iconSpacing;
+ /**
+ * Set a range (start and end)
+ * @param end
+ * @param start
+ * @param end
+ */
+ DataAxis.prototype.setRange = function (start, end) {
+ this.range.start = start;
+ this.range.end = end;
+ };
+
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ DataAxis.prototype.redraw = function () {
+ var changeCalled = false;
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true) {
+ activeGroups++;
}
}
-
- DOMutil.cleanupElements(this.svgElements);
}
- };
+ if (this.amountOfGroups == 0 || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ this.height = Number(this.linegraphSVG.style.height.replace("px",""));
+ // svg offsetheight did not work in firefox and explorer...
- module.exports = Legend;
+ this.dom.lineContainer.style.height = this.height + 'px';
+ this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
+ var props = this.props;
+ var frame = this.dom.frame;
-/***/ },
-/* 40 */
-/***/ function(module, exports, __webpack_require__) {
+ // update classname
+ frame.className = 'dataaxis';
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var DataSet = __webpack_require__(7);
- var DataView = __webpack_require__(8);
- var Component = __webpack_require__(22);
- var DataAxis = __webpack_require__(37);
- var GraphGroup = __webpack_require__(38);
- var Legend = __webpack_require__(39);
+ // calculate character width and height
+ this._calculateCharSize();
- var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+ var orientation = this.options.orientation;
+ var showMinorLabels = this.options.showMinorLabels;
+ var showMajorLabels = this.options.showMajorLabels;
- /**
- * This is the constructor of the LineGraph. It requires a Timeline body and options.
- *
- * @param body
- * @param options
- * @constructor
- */
- function LineGraph(body, options) {
- this.id = util.randomUUID();
- this.body = body;
+ // determine the width and height of the elemens for the axis
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
- this.defaultOptions = {
- yAxisOrientation: 'left',
- defaultGroup: 'default',
- sort: true,
- sampling: true,
- graphHeight: '400px',
- shaded: {
- enabled: false,
- orientation: 'bottom' // top, bottom
- },
- style: 'line', // line, bar
- barChart: {
- width: 50,
- align: 'center' // left, center, right
- },
- catmullRom: {
- enabled: true,
- parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
- alpha: 0.5
- },
- drawPoints: {
- enabled: true,
- size: 6,
- style: 'square' // square, circle
- },
- dataAxis: {
- showMinorLabels: true,
- showMajorLabels: true,
- icons: false,
- width: '40px',
- visible: true
- },
- legend: {
- enabled: false,
- icons: true,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,right
- },
- right: {
- visible: true,
- position: 'top-right' // top/bottom - left,right
- }
+ props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
+ props.minorLineHeight = 1;
+ props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
+ props.majorLineHeight = 1;
+
+ // take frame offline while updating (is almost twice as fast)
+ if (orientation == 'left') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
}
- };
+ else { // right
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
+ }
+ changeCalled = this._redrawLabels();
+ if (this.options.icons == true) {
+ this._redrawGroupIcons();
+ }
+ }
+ return changeCalled;
+ };
- // options is shared by this ItemSet and all its items
- this.options = util.extend({}, this.defaultOptions);
- this.dom = {};
- this.props = {};
- this.hammer = null;
- this.groups = {};
+ /**
+ * Repaint major and minor text labels and vertical grid lines
+ * @private
+ */
+ DataAxis.prototype._redrawLabels = function () {
+ DOMutil.prepareElements(this.DOMelements.lines);
+ DOMutil.prepareElements(this.DOMelements.labels);
- var me = this;
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
+ var orientation = this.options['orientation'];
- // listeners for the DataSet of the items
- this.itemListeners = {
- 'add': function (event, params, senderId) {
- me._onAdd(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdate(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemove(params.items);
- }
- };
+ // calculate range and step (step such that we have space for 7 characters per label)
+ var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
+ var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight);
+ this.step = step;
+ step.first();
+ // get the distance in pixels for a step
+ var stepPixels = this.dom.frame.offsetHeight / ((step.marginRange / step.step) + 1);
+ this.stepPixels = stepPixels;
- // listeners for the DataSet of the groups
- this.groupListeners = {
- 'add': function (event, params, senderId) {
- me._onAddGroups(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdateGroups(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemoveGroups(params.items);
- }
- };
+ var amountOfSteps = this.height / stepPixels;
+ var stepDifference = 0;
- this.items = {}; // object with an Item for every data item
- this.selection = []; // list with the ids of all selected nodes
- this.lastStart = this.body.range.start;
- this.touchParams = {}; // stores properties while dragging
+ if (this.master == false) {
+ stepPixels = this.stepPixelsForced;
+ stepDifference = Math.round((this.height / stepPixels) - amountOfSteps);
+ for (var i = 0; i < 0.5 * stepDifference; i++) {
+ step.previous();
+ }
+ amountOfSteps = this.height / stepPixels;
+ }
- this.svgElements = {};
- this.setOptions(options);
- this.groupsUsingDefaultStyles = [0];
- this.body.emitter.on("rangechange",function() {
- if (me.lastStart != 0) {
- var offset = me.body.range.start - me.lastStart;
- var range = me.body.range.end - me.body.range.start;
- if (me.width != 0) {
- var rangePerPixelInv = me.width/range;
- var xOffset = offset * rangePerPixelInv;
- me.svg.style.left = (-me.width - xOffset) + "px";
- }
- }
- });
- this.body.emitter.on("rangechanged", function() {
- me.lastStart = me.body.range.start;
- me.svg.style.left = util.option.asSize(-me.width);
- me._updateGraph.apply(me);
- });
+ this.valueAtZero = step.marginEnd;
+ var marginStartPos = 0;
- // create the HTML DOM
- this._create();
- this.body.emitter.emit("change");
- }
+ // do not draw the first label
+ var max = 1;
+ step.next();
- LineGraph.prototype = new Component();
+ this.maxLabelSize = 0;
+ var y = 0;
+ while (max < Math.round(amountOfSteps)) {
- /**
- * Create the HTML DOM for the ItemSet
- */
- LineGraph.prototype._create = function(){
- var frame = document.createElement('div');
- frame.className = 'LineGraph';
- this.dom.frame = frame;
+ y = Math.round(max * stepPixels);
+ marginStartPos = max * stepPixels;
+ var isMajor = step.isMajor();
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "relative";
- this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
- this.svg.style.display = "block";
- frame.appendChild(this.svg);
+ if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
+ }
- // data axis
- this.options.dataAxis.orientation = 'left';
- this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
+ if (isMajor && this.options['showMajorLabels'] && this.master == true ||
+ this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
+ if (y >= 0) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
+ }
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
+ }
+ else {
+ this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
+ }
- this.options.dataAxis.orientation = 'right';
- this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
- delete this.options.dataAxis.orientation;
+ step.next();
+ max++;
+ }
- // legends
- this.legendLeft = new Legend(this.body, this.options.legend, 'left');
- this.legendRight = new Legend(this.body, this.options.legend, 'right');
+ this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step);
- this.show();
+ var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
+ // this will resize the yAxis to accomodate the labels.
+ if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
+ this.width = this.maxLabelSize + offset;
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ // this will resize the yAxis if it is too big for the labels.
+ else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
+ this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ else {
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ return false;
+ }
};
/**
- * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
- * @param options
- */
- LineGraph.prototype.setOptions = function(options) {
- if (options) {
- var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
- util.selectiveDeepExtend(fields, this.options, options);
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
- util.mergeOptions(this.options, options,'legend');
-
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
+ * Create a label for the axis at position x
+ * @private
+ * @param y
+ * @param text
+ * @param orientation
+ * @param className
+ * @param characterHeight
+ */
+ DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
+ // reuse redundant label
+ var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
+ label.className = className;
+ label.innerHTML = text;
+ if (orientation == 'left') {
+ label.style.left = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "right";
+ }
+ else {
+ label.style.right = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "left";
+ }
- if (this.yAxisLeft) {
- if (options.dataAxis !== undefined) {
- this.yAxisLeft.setOptions(this.options.dataAxis);
- this.yAxisRight.setOptions(this.options.dataAxis);
- }
- }
+ label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
- if (this.legendLeft) {
- if (options.legend !== undefined) {
- this.legendLeft.setOptions(this.options.legend);
- this.legendRight.setOptions(this.options.legend);
- }
- }
+ text += '';
- if (this.groups.hasOwnProperty(UNGROUPED)) {
- this.groups[UNGROUPED].setOptions(options);
- }
- }
- if (this.dom.frame) {
- this._updateGraph();
+ var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
+ if (this.maxLabelSize < text.length * largestWidth) {
+ this.maxLabelSize = text.length * largestWidth;
}
};
/**
- * Hide the component from the DOM
+ * Create a minor line for the axis at position y
+ * @param y
+ * @param orientation
+ * @param className
+ * @param offset
+ * @param width
*/
- LineGraph.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
+ DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
+ if (this.master == true) {
+ var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
+ line.className = className;
+ line.innerHTML = '';
+
+ if (orientation == 'left') {
+ line.style.left = (this.width - offset) + 'px';
+ }
+ else {
+ line.style.right = (this.width - offset) + 'px';
+ }
+
+ line.style.width = width + 'px';
+ line.style.top = y + 'px';
}
};
- /**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
- */
- LineGraph.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
- }
+
+ DataAxis.prototype.convertValue = function (value) {
+ var invertedValue = this.valueAtZero - value;
+ var convertedValue = invertedValue * this.conversionFactor;
+ return convertedValue; // the -2 is to compensate for the borders
};
/**
- * Set items
- * @param {vis.DataSet | null} items
+ * Determine the size of text on the axis (both major and minor axis).
+ * The size is calculated only once and then cached in this.props.
+ * @private
*/
- LineGraph.prototype.setItems = function(items) {
- var me = this,
- ids,
- oldItemsData = this.itemsData;
-
- // replace the dataset
- if (!items) {
- this.itemsData = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- this.itemsData = items;
- }
- else {
- throw new TypeError('Data must be an instance of DataSet or DataView');
- }
+ DataAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'yAxis minor measure';
+ measureCharMinor.appendChild(textMinor);
+ this.dom.frame.appendChild(measureCharMinor);
- if (oldItemsData) {
- // unsubscribe from old dataset
- util.forEach(this.itemListeners, function (callback, event) {
- oldItemsData.off(event, callback);
- });
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
- // remove all drawn items
- ids = oldItemsData.getIds();
- this._onRemove(ids);
+ this.dom.frame.removeChild(measureCharMinor);
}
- if (this.itemsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.itemListeners, function (callback, event) {
- me.itemsData.on(event, callback, id);
- });
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'yAxis major measure';
+ measureCharMajor.appendChild(textMajor);
+ this.dom.frame.appendChild(measureCharMajor);
- // add all new items
- ids = this.itemsData.getIds();
- this._onAdd(ids);
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
+
+ this.dom.frame.removeChild(measureCharMajor);
}
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
};
/**
- * Set groups
- * @param {vis.DataSet} groups
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
- LineGraph.prototype.setGroups = function(groups) {
- var me = this,
- ids;
+ DataAxis.prototype.snap = function(date) {
+ return this.step.snap(date);
+ };
- // unsubscribe from current dataset
- if (this.groupsData) {
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.unsubscribe(event, callback);
- });
+ module.exports = DataAxis;
- // remove all drawn groups
- ids = this.groupsData.getIds();
- this.groupsData = null;
- this._onRemoveGroups(ids); // note: this will cause a redraw
- }
- // replace the dataset
- if (!groups) {
- this.groupsData = null;
- }
- else if (groups instanceof DataSet || groups instanceof DataView) {
- this.groupsData = groups;
- }
- else {
- throw new TypeError('Data must be an instance of DataSet or DataView');
- }
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
- if (this.groupsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.on(event, callback, id);
- });
+ /**
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
+ *
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+ function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
+ // variables
+ this.current = 0;
- // draw all ms
- ids = this.groupsData.getIds();
- this._onAddGroups(ids);
- }
- this._onUpdate();
- };
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
+ this.marginStart;
+ this.marginEnd;
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
- LineGraph.prototype._onUpdate = function(ids) {
- this._updateUngrouped();
- this._updateAllGroupData();
- this._updateGraph();
- this.redraw();
- };
- LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onUpdateGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- var group = this.groupsData.get(groupIds[i]);
- this._updateGroup(group, groupIds[i]);
- }
+ this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
+ }
- this._updateGraph();
- this.redraw();
- };
- LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
- LineGraph.prototype._onRemoveGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- if (!this.groups.hasOwnProperty(groupIds[i])) {
- if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
- this.yAxisRight.removeGroup(groupIds[i]);
- this.legendRight.removeGroup(groupIds[i]);
- this.legendRight.redraw();
- }
- else {
- this.yAxisLeft.removeGroup(groupIds[i]);
- this.legendLeft.removeGroup(groupIds[i]);
- this.legendLeft.redraw();
- }
- delete this.groups[groupIds[i]];
- }
- }
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
- };
/**
- * update a group object
- *
- * @param group
- * @param groupId
- * @private
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- LineGraph.prototype._updateGroup = function (group, groupId) {
- if (!this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.addGroup(groupId, this.groups[groupId]);
- this.legendRight.addGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
- this.legendLeft.addGroup(groupId, this.groups[groupId]);
- }
- }
- else {
- this.groups[groupId].update(group);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
- this.legendRight.updateGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
- this.legendLeft.updateGroup(groupId, this.groups[groupId]);
- }
- }
- this.legendLeft.redraw();
- this.legendRight.redraw();
- };
+ DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
+ this._start = start;
+ this._end = end;
- LineGraph.prototype._updateAllGroupData = function () {
- if (this.itemsData != null) {
- // ~450 ms @ 500k
+ if (start == end) {
+ this._start = start - 0.75;
+ this._end = end + 1;
+ }
- var groupsContent = {};
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- groupsContent[groupId] = [];
- }
- }
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- item.x = util.convert(item.x,"Date");
- groupsContent[item.group].push(item);
- }
- }
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId].setItems(groupsContent[groupId]);
- }
- }
- // // ~4500ms @ 500k
- // for (var groupId in this.groups) {
- // if (this.groups.hasOwnProperty(groupId)) {
- // this.groups[groupId].setItems(this.itemsData.get({filter:
- // function (item) {
- // return (item.group == groupId);
- // }, type:{x:"Date"}}
- // ));
- // }
- // }
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
}
+ this.setFirst();
};
/**
- * Create or delete the group holding all ungrouped items. This group is used when
- * there are no groups specified. This anonymous group is called 'graph'.
- * @protected
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- LineGraph.prototype._updateUngrouped = function() {
- if (this.itemsData != null) {
- // var t0 = new Date();
- var group = {id: UNGROUPED, content: this.options.defaultGroup};
- this._updateGroup(group, UNGROUPED);
- var ungroupedCounter = 0;
- if (this.itemsData) {
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- if (item != undefined) {
- if (item.hasOwnProperty('group')) {
- if (item.group === undefined) {
- item.group = UNGROUPED;
- }
- }
- else {
- item.group = UNGROUPED;
- }
- ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
- }
- }
- }
- }
+ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.1;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
- // much much slower
- // var datapoints = this.itemsData.get({
- // filter: function (item) {return item.group === undefined;},
- // showInternalIds:true
- // });
- // if (datapoints.length > 0) {
- // var updateQuery = [];
- // for (var i = 0; i < datapoints.length; i++) {
- // updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
- // }
- // this.itemsData.update(updateQuery, true);
- // }
- // var t1 = new Date();
- // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
- if (ungroupedCounter == 0) {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
- }
- // console.log("getting amount ungrouped",new Date() - t1);
- // console.log("putting in ungrouped",new Date() - t0);
- }
- else {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
+
+ var start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
}
- this.legendLeft.redraw();
- this.legendRight.redraw();
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
+ }
+ }
+ if (solutionFound == true) {
+ break;
+ }
+ }
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
/**
- * Redraw the component, mandatory function
- * @return {boolean} Returns true if the component is resized
+ * Set the range iterator to the start date.
*/
- LineGraph.prototype.redraw = function() {
- var resized = false;
+ DataStep.prototype.first = function() {
+ this.setFirst();
+ };
- this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
- if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
- resized = true;
- }
- // check if this component is resized
- resized = this._isResized() || resized;
- // check whether zoomed (in that case we need to re-stack everything)
- var visibleInterval = this.body.range.end - this.body.range.start;
- var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
- this.lastVisibleInterval = visibleInterval;
- this.lastWidth = this.width;
+ /**
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
+ */
+ DataStep.prototype.setFirst = function() {
+ var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
+ var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
- // calculate actual size and position
- this.width = this.dom.frame.offsetWidth;
+ this.marginEnd = this.roundToMinor(niceEnd);
+ this.marginStart = this.roundToMinor(niceStart);
+ this.marginRange = this.marginEnd - this.marginStart;
- // the svg element is three times as big as the width, this allows for fully dragging left and right
- // without reloading the graph. the controls for this are bound to events in the constructor
- if (resized == true) {
- this.svg.style.width = util.option.asSize(3*this.width);
- this.svg.style.left = util.option.asSize(-this.width);
+ this.current = this.marginEnd;
+
+ };
+
+ DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
- if (zoomed == true) {
- this._updateGraph();
+ else {
+ return rounded;
}
+ }
- this.legendLeft.redraw();
- this.legendRight.redraw();
- return resized;
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
};
/**
- * Update and redraw the graph.
- *
+ * Do the next step
*/
- LineGraph.prototype._updateGraph = function () {
- // reset the svg elements
- DOMutil.prepareElements(this.svgElements);
- // // very slow...
- // groupData = group.itemsData.get({filter:
- // function (item) {
- // return (item.x > minDate && item.x < maxDate);
- // }}
- // );
-
-
- if (this.width != 0 && this.itemsData != null) {
- var group, groupData, preprocessedGroup, i;
- var preprocessedGroupData = [];
- var processedGroupData = [];
- var groupRanges = [];
- var changeCalled = false;
-
- // getting group Ids
- var groupIds = [];
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- groupIds.push(groupId);
- }
- }
+ DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
- // this is the range of the SVG canvas
- var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
- var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
+ }
+ };
- // first select and preprocess the data from the datasets.
- // the groups have their preselection of data, we now loop over this data to see
- // what data we need to draw. Sorted data is much faster.
- // more optimization is possible by doing the sampling before and using the binary search
- // to find the end date to determine the increment.
- if (groupIds.length > 0) {
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- groupData = [];
- // optimization for sorted data
- if (group.options.sort == true) {
- var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
-
- for (var j = guess; j < group.itemsData.length; j++) {
- var item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > maxDate) {
- groupData.push(item);
- break;
- }
- else {
- groupData.push(item);
- }
- }
- }
- }
- else {
- for (var j = 0; j < group.itemsData.length; j++) {
- var item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > minDate && item.x < maxDate) {
- groupData.push(item);
- }
- }
- }
- }
- // preprocess, split into ranges and data
- preprocessedGroup = this._preprocessData(groupData, group);
- groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
- preprocessedGroupData.push(preprocessedGroup.data);
- }
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+ };
- // update the Y axis first, we use this data to draw at the correct Y points
- // changeCalled is required to clean the SVG on a change emit.
- changeCalled = this._updateYAxis(groupIds, groupRanges);
- if (changeCalled == true) {
- DOMutil.cleanupElements(this.svgElements);
- this.body.emitter.emit("change");
- return;
- }
- // with the yAxis scaled correctly, use this to get the Y values of the points.
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
- }
- // draw the groups
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'line') {
- this._drawLineGraph(processedGroupData[i], group);
- }
- else {
- this._drawBarGraph (processedGroupData[i], group);
- }
- }
+ /**
+ * Get the current datetime
+ * @return {String} current The current date
+ */
+ DataStep.prototype.getCurrent = function() {
+ var toPrecision = '' + Number(this.current).toPrecision(5);
+ for (var i = toPrecision.length-1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0,i);
+ }
+ else if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0,i);
+ break;
+ }
+ else{
+ break;
}
}
- // cleanup unused svg elements
- DOMutil.cleanupElements(this.svgElements);
+ return toPrecision;
};
+
+
/**
- * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
- * @param {array} groupIds
- * @private
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
- LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
- var changeCalled = false;
- var yAxisLeftUsed = false;
- var yAxisRightUsed = false;
- var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
- var orientation = 'left';
+ DataStep.prototype.snap = function(date) {
- // if groups are present
- if (groupIds.length > 0) {
- for (var i = 0; i < groupIds.length; i++) {
- orientation = 'left';
- var group = this.groups[groupIds[i]];
- if (group.options.yAxisOrientation == 'right') {
- orientation = 'right';
- }
+ };
- minVal = groupRanges[i].min;
- maxVal = groupRanges[i].max;
+ /**
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
+ */
+ DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
+ };
- if (orientation == 'left') {
- yAxisLeftUsed = true;
- minLeft = minLeft > minVal ? minVal : minLeft;
- maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
- }
- else {
- yAxisRightUsed = true;
- minRight = minRight > minVal ? minVal : minRight;
- maxRight = maxRight < maxVal ? maxVal : maxRight;
- }
- }
- if (yAxisLeftUsed == true) {
- this.yAxisLeft.setRange(minLeft, maxLeft);
- }
- if (yAxisRightUsed == true) {
- this.yAxisRight.setRange(minRight, maxRight);
- }
- }
+ module.exports = DataStep;
- changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
- changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
- if (yAxisRightUsed == true && yAxisLeftUsed == true) {
- this.yAxisLeft.drawIcons = true;
- this.yAxisRight.drawIcons = true;
- }
- else {
- this.yAxisLeft.drawIcons = false;
- this.yAxisRight.drawIcons = false;
- }
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
- this.yAxisRight.master = !yAxisLeftUsed;
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
- if (this.yAxisRight.master == false) {
- if (yAxisRightUsed == true) {
- this.yAxisLeft.lineOffset = this.yAxisRight.width;
- }
- changeCalled = this.yAxisLeft.redraw() || changeCalled;
- this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
- changeCalled = this.yAxisRight.redraw() || changeCalled;
- }
- else {
- changeCalled = this.yAxisRight.redraw() || changeCalled;
+ /**
+ * @constructor Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
+ */
+ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
+ this.id = groupId;
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
+ this.options = util.selectiveBridgeObject(fields,options);
+ this.usingDefaultStyle = group.className === undefined;
+ this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
+ this.zeroPosition = 0;
+ this.update(group);
+ if (this.usingDefaultStyle == true) {
+ this.groupsUsingDefaultStyles[0] += 1;
}
- return changeCalled;
- };
+ this.itemsData = [];
+ this.visible = group.visible === undefined ? true : group.visible;
+ }
- /**
- * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
- *
- * @param {boolean} axisUsed
- * @returns {boolean}
- * @private
- * @param axis
- */
- LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
- var changed = false;
- if (axisUsed == false) {
- if (axis.dom.frame.parentNode) {
- axis.hide();
- changed = true;
+ GraphGroup.prototype.setItems = function(items) {
+ if (items != null) {
+ this.itemsData = items;
+ if (this.options.sort == true) {
+ this.itemsData.sort(function (a,b) {return a.x - b.x;})
}
}
else {
- if (!axis.dom.frame.parentNode) {
- axis.show();
- changed = true;
- }
+ this.itemsData = [];
}
- return changed;
};
+ GraphGroup.prototype.setZeroPosition = function(pos) {
+ this.zeroPosition = pos;
+ };
- /**
- * draw a bar graph
- * @param datapoints
- * @param group
- */
- LineGraph.prototype._drawBarGraph = function (dataset, group) {
- if (dataset != null) {
- if (dataset.length > 0) {
- var coreDistance;
- var minWidth = 0.1 * group.options.barChart.width;
- var offset = 0;
- var width = group.options.barChart.width;
-
- if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
- else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
-
- for (var i = 0; i < dataset.length; i++) {
- // dynammically downscale the width so there is no overlap up to 1/10th the original width
- if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
- if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
- if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
+ GraphGroup.prototype.setOptions = function(options) {
+ if (options !== undefined) {
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
+ util.selectiveDeepExtend(fields, this.options, options);
- DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
- }
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
- // draw points
- if (group.options.drawPoints.enabled == true) {
- this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
}
}
}
};
+ GraphGroup.prototype.update = function(group) {
+ this.group = group;
+ this.content = group.content || 'graph';
+ this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
+ this.visible = group.visible === undefined ? true : group.visible;
+ this.setOptions(group.options);
+ };
- /**
- * draw a line graph
- *
- * @param datapoints
- * @param group
- */
- LineGraph.prototype._drawLineGraph = function (dataset, group) {
- if (dataset != null) {
- if (dataset.length > 0) {
- var path, d;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
- path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
- path.setAttributeNS(null, "class", group.className);
+ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
+ var fillHeight = iconHeight * 0.5;
+ var path, fillPath;
- // construct path from dataset
- if (group.options.catmullRom.enabled == true) {
- d = this._catmullRom(dataset, group);
+ var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
+ outline.setAttributeNS(null, "x", x);
+ outline.setAttributeNS(null, "y", y - fillHeight);
+ outline.setAttributeNS(null, "width", iconWidth);
+ outline.setAttributeNS(null, "height", 2*fillHeight);
+ outline.setAttributeNS(null, "class", "outline");
+
+ if (this.options.style == 'line') {
+ path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ path.setAttributeNS(null, "class", this.className);
+ path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
+ if (this.options.shaded.enabled == true) {
+ fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ if (this.options.shaded.orientation == 'top') {
+ fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
+ "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
}
else {
- d = this._linear(dataset);
- }
-
- // append with points for fill and finalize the path
- if (group.options.shaded.enabled == true) {
- var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
- var dFill;
- if (group.options.shaded.orientation == 'top') {
- dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
- }
- else {
- dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
- }
- fillPath.setAttributeNS(null, "class", group.className + " fill");
- fillPath.setAttributeNS(null, "d", dFill);
+ fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
+ "L"+x+"," + (y + fillHeight) + " " +
+ "L"+ (x + iconWidth) + "," + (y + fillHeight) +
+ "L"+ (x + iconWidth) + ","+y);
}
- // copy properties to path for drawing.
- path.setAttributeNS(null, "d", "M" + d);
+ fillPath.setAttributeNS(null, "class", this.className + " iconFill");
+ }
- // draw points
- if (group.options.drawPoints.enabled == true) {
- this._drawPoints(dataset, group, this.svgElements, this.svg);
- }
+ if (this.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
}
}
- };
+ else {
+ var barWidth = Math.round(0.3 * iconWidth);
+ var bar1Height = Math.round(0.4 * iconHeight);
+ var bar2Height = Math.round(0.75 * iconHeight);
- /**
- * draw the data points
- *
- * @param dataset
- * @param JSONcontainer
- * @param svg
- * @param group
- */
- LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
- if (offset === undefined) {offset = 0;}
- for (var i = 0; i < dataset.length; i++) {
- DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
+ var offset = Math.round((iconWidth - (2 * barWidth))/3);
+
+ DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
}
};
-
-
/**
- * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
- * the yAxis.
*
- * @param datapoints
- * @returns {Array}
- * @private
+ * @param iconWidth
+ * @param iconHeight
+ * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
*/
- LineGraph.prototype._preprocessData = function (datapoints, group) {
- var extractedData = [];
- var xValue, yValue;
- var toScreen = this.body.util.toScreen;
-
- var increment = 1;
- var amountOfPoints = datapoints.length;
+ GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
+ return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
+ }
- var yMin = datapoints[0].y;
- var yMax = datapoints[0].y;
+ module.exports = GraphGroup;
- // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
- // of width changing of the yAxis.
- if (group.options.sampling == true) {
- var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
- var pointsPerPixel = amountOfPoints/xDistance;
- increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
- }
- for (var i = 0; i < amountOfPoints; i += increment) {
- xValue = toScreen(datapoints[i].x) + this.width - 1;
- yValue = datapoints[i].y;
- extractedData.push({x: xValue, y: yValue});
- yMin = yMin > yValue ? yValue : yMin;
- yMax = yMax < yValue ? yValue : yMax;
- }
+/***/ },
+/* 40 */
+/***/ function(module, exports, __webpack_require__) {
- // extractedData.sort(function (a,b) {return a.x - b.x;});
- return {min: yMin, max: yMax, data: extractedData};
- };
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var Component = __webpack_require__(22);
/**
- * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp.
- *
- * @param datapoints
- * @param options
- * @returns {Array}
- * @private
+ * Legend for Graph2d
*/
- LineGraph.prototype._convertYvalues = function (datapoints, group) {
- var extractedData = [];
- var xValue, yValue;
- var axis = this.yAxisLeft;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
+ function Legend(body, options, side) {
+ this.body = body;
+ this.defaultOptions = {
+ enabled: true,
+ icons: true,
+ iconSize: 20,
+ iconSpacing: 6,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ },
+ right: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ }
+ }
+ this.side = side;
+ this.options = util.extend({},this.defaultOptions);
+
+ this.svgElements = {};
+ this.dom = {};
+ this.groups = {};
+ this.amountOfGroups = 0;
+ this._create();
- if (group.options.yAxisOrientation == 'right') {
- axis = this.yAxisRight;
- }
+ this.setOptions(options);
+ }
- for (var i = 0; i < datapoints.length; i++) {
- xValue = datapoints[i].x;
- yValue = Math.round(axis.convertValue(datapoints[i].y));
- extractedData.push({x: xValue, y: yValue});
- }
+ Legend.prototype = new Component();
- group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
- // extractedData.sort(function (a,b) {return a.x - b.x;});
- return extractedData;
+ Legend.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
};
+ Legend.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
- /**
- * This uses an uniform parametrization of the CatmullRom algorithm:
- * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
- * @param data
- * @returns {string}
- * @private
- */
- LineGraph.prototype._catmullRomUniform = function(data) {
- // catmull rom
- var p0, p1, p2, p3, bp1, bp2;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var normalization = 1/6;
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ Legend.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
+ }
+ };
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
+ Legend.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.className = 'legend';
+ this.dom.frame.style.position = "absolute";
+ this.dom.frame.style.top = "10px";
+ this.dom.frame.style.display = "block";
+ this.dom.textArea = document.createElement('div');
+ this.dom.textArea.className = 'legendText';
+ this.dom.textArea.style.position = "relative";
+ this.dom.textArea.style.top = "0px";
- // Catmull-Rom to Cubic Bezier conversion matrix
- // 0 1 0 0
- // -1/6 1 1/6 0
- // 0 1/6 1 -1/6
- // 0 0 1 0
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = 'absolute';
+ this.svg.style.top = 0 +'px';
+ this.svg.style.width = this.options.iconSize + 5 + 'px';
- // bp0 = { x: p1.x, y: p1.y };
- bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
- bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
- // bp0 = { x: p2.x, y: p2.y };
+ this.dom.frame.appendChild(this.svg);
+ this.dom.frame.appendChild(this.dom.textArea);
+ };
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
+ /**
+ * Hide the component from the DOM
+ */
+ Legend.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
-
- return d;
};
/**
- * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
- * By default, the centripetal parameterization is used because this gives the nicest results.
- * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
- *
- * One optimization can be used to reuse distances since this is a sliding window approach.
- * @param data
- * @returns {string}
- * @private
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
- LineGraph.prototype._catmullRom = function(data, group) {
- var alpha = group.options.catmullRom.alpha;
- if (alpha == 0 || alpha === undefined) {
- return this._catmullRomUniform(data);
+ Legend.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
}
- else {
- var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
- var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
-
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
-
- d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
- d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
- d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
-
- // Catmull-Rom to Cubic Bezier conversion matrix
- //
- // A = 2d1^2a + 3d1^a * d2^a + d3^2a
- // B = 2d3^2a + 3d3^a * d2^a + d2^2a
- //
- // [ 0 1 0 0 ]
- // [ -d2^2a/N A/N d1^2a/N 0 ]
- // [ 0 d3^2a/M B/M -d2^2a/M ]
- // [ 0 0 1 0 ]
-
- // [ 0 1 0 0 ]
- // [ -d2pow2a/N A/N d1pow2a/N 0 ]
- // [ 0 d3pow2a/M B/M -d2pow2a/M ]
- // [ 0 0 1 0 ]
+ };
- d3powA = Math.pow(d3, alpha);
- d3pow2A = Math.pow(d3,2*alpha);
- d2powA = Math.pow(d2, alpha);
- d2pow2A = Math.pow(d2,2*alpha);
- d1powA = Math.pow(d1, alpha);
- d1pow2A = Math.pow(d1,2*alpha);
+ Legend.prototype.setOptions = function(options) {
+ var fields = ['enabled','orientation','icons','left','right'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ };
- A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
- B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
- N = 3*d1powA * (d1powA + d2powA);
- if (N > 0) {N = 1 / N;}
- M = 3*d3powA * (d3powA + d2powA);
- if (M > 0) {M = 1 / M;}
+ Legend.prototype.redraw = function() {
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true) {
+ activeGroups++;
+ }
+ }
+ }
- bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
- y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
+ if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
+ this.dom.frame.style.left = '4px';
+ this.dom.frame.style.textAlign = "left";
+ this.dom.textArea.style.textAlign = "left";
+ this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.right = '';
+ this.svg.style.left = 0 +'px';
+ this.svg.style.right = '';
+ }
+ else {
+ this.dom.frame.style.right = '4px';
+ this.dom.frame.style.textAlign = "right";
+ this.dom.textArea.style.textAlign = "right";
+ this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.left = '';
+ this.svg.style.right = 0 +'px';
+ this.svg.style.left = '';
+ }
- bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
- y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
+ this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.bottom = '';
+ }
+ else {
+ this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.top = '';
+ }
- if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
- if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
+ if (this.options.icons == false) {
+ this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
+ this.dom.textArea.style.right = '';
+ this.dom.textArea.style.left = '';
+ this.svg.style.width = '0px';
+ }
+ else {
+ this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
+ this.drawLegendIcons();
}
- return d;
+ var content = '';
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true) {
+ content += this.groups[groupId].content + '
';
+ }
+ }
+ }
+ this.dom.textArea.innerHTML = content;
+ this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
}
};
- /**
- * this generates the SVG path for a linear drawing between datapoints.
- * @param data
- * @returns {string}
- * @private
- */
- LineGraph.prototype._linear = function(data) {
- // linear
- var d = "";
- for (var i = 0; i < data.length; i++) {
- if (i == 0) {
- d += data[i].x + "," + data[i].y;
- }
- else {
- d += " " + data[i].x + "," + data[i].y;
+ Legend.prototype.drawLegendIcons = function() {
+ if (this.dom.frame.parentNode) {
+ DOMutil.prepareElements(this.svgElements);
+ var padding = window.getComputedStyle(this.dom.frame).paddingTop;
+ var iconOffset = Number(padding.replace('px',''));
+ var x = iconOffset;
+ var iconWidth = this.options.iconSize;
+ var iconHeight = 0.75 * this.options.iconSize;
+ var y = iconOffset + 0.5 * iconHeight + 3;
+
+ this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + this.options.iconSpacing;
+ }
+ }
}
+
+ DOMutil.cleanupElements(this.svgElements);
}
- return d;
};
- module.exports = LineGraph;
+ module.exports = Legend;
/***/ },
diff --git a/docs/graph2d.html b/docs/graph2d.html
index 9c3f6cfc..e849b450 100644
--- a/docs/graph2d.html
+++ b/docs/graph2d.html
@@ -174,7 +174,7 @@ var items = [
-
Like the items, groups are regular JavaScript Arrays and Objects. @@ -191,7 +191,8 @@ var items = [ var groups = new vis.DataSet(); groups.add({ id: 1, - content: 'Group 1' + content: 'Group 1', + // Optional: a field 'visible' // Optional: a field 'className' // Optional: options }) @@ -241,6 +242,12 @@ groups.add({
yAxisOrientation
option within the groups.
- We also show a few more custom styles for the graphs. Finally, the legend is manually positioned. Both the left and right axis
- have their own legend. If one of the axis is unused, the legend is not shown. The options for the legend have been split
- in a left
and a right
segment. The default position of the left axis has been changed.
-
+