var util = require('../../util'); var DOMutil = require('../../DOMutil'); var DataSet = require('../../DataSet'); var DataView = require('../../DataView'); var Component = require('./Component'); var DataAxis = require('./DataAxis'); var GraphGroup = require('./GraphGroup'); var Legend = require('./Legend'); var Bars = require('./graph2d_types/bar'); var Lines = require('./graph2d_types/line'); var Points = require('./graph2d_types/points'); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items /** * 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; this.defaultOptions = { yAxisOrientation: 'left', defaultGroup: 'default', sort: true, sampling: true, stack: false, graphHeight: '400px', shaded: { enabled: false, orientation: 'bottom' // top, bottom, zero }, style: 'line', // line, bar barChart: { width: 50, sideBySide: false, align: 'center' // left, center, right }, interpolation: { 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: {}, //Defaults are done on DataAxis level legend: {}, //Defaults are done on Legend level groups: { visibility: {} } }; // options is shared by this lineGraph and all its items this.options = util.extend({}, this.defaultOptions); this.dom = {}; this.props = {}; this.hammer = null; this.groups = {}; this.abortedGraphUpdate = false; this.updateSVGheight = false; this.updateSVGheightOnResize = false; this.forceGraphUpdate = true; var me = this; this.itemsData = null; // DataSet this.groupsData = null; // DataSet // 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 this.svgElements = {}; this.setOptions(options); this.groupsUsingDefaultStyles = [0]; this.body.emitter.on('rangechanged', function () { me.lastStart = me.body.range.start; me.svg.style.left = util.option.asSize(-me.props.width); me.forceGraphUpdate = true; //Is this local redraw necessary? (Core also does a change event!) me.redraw.call(me); }); // create the HTML DOM this._create(); this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups}; } LineGraph.prototype = new Component(); /** * Create the HTML DOM for the ItemSet */ LineGraph.prototype._create = function () { var frame = document.createElement('div'); frame.className = 'vis-line-graph'; this.dom.frame = frame; // 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); // data axis this.options.dataAxis.orientation = 'left'; this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); this.options.dataAxis.orientation = 'right'; this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); delete this.options.dataAxis.orientation; // legends this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups); this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups); this.show(); }; /** * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. * @param {object} options */ LineGraph.prototype.setOptions = function (options) { if (options) { var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups']; if (options.graphHeight === undefined && options.height !== undefined) { this.updateSVGheight = true; this.updateSVGheightOnResize = true; } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { if (parseInt((options.graphHeight + '').replace("px", '')) < this.body.domProps.centerContainer.height) { this.updateSVGheight = true; } } util.selectiveDeepExtend(fields, this.options, options); util.mergeOptions(this.options, options, 'interpolation'); util.mergeOptions(this.options, options, 'drawPoints'); util.mergeOptions(this.options, options, 'shaded'); util.mergeOptions(this.options, options, 'legend'); if (options.interpolation) { if (typeof options.interpolation == 'object') { if (options.interpolation.parametrization) { if (options.interpolation.parametrization == 'uniform') { this.options.interpolation.alpha = 0; } else if (options.interpolation.parametrization == 'chordal') { this.options.interpolation.alpha = 1.0; } else { this.options.interpolation.parametrization = 'centripetal'; this.options.interpolation.alpha = 0.5; } } } } 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); } } if (this.groups.hasOwnProperty(UNGROUPED)) { this.groups[UNGROUPED].setOptions(options); } } // this is used to redraw the graph if the visibility of the groups is changed. if (this.dom.frame) { //not on initial run? this.forceGraphUpdate=true; this.body.emitter.emit("change"); } }; /** * Hide the component from the DOM */ LineGraph.prototype.hide = function () { // remove the frame containing the items if (this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame); } }; /** * 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); } }; /** * Set items * @param {vis.DataSet | null} items */ 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'); } 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); } 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); } }; /** * Set groups * @param {vis.DataSet} groups */ LineGraph.prototype.setGroups = function (groups) { var me = this; var ids; // unsubscribe from current dataset if (this.groupsData) { util.forEach(this.groupListeners, function (callback, event) { me.groupsData.off(event, callback); }); // remove all drawn groups ids = this.groupsData.getIds(); this.groupsData = null; for (var i = 0; i < ids.length; i++) { this._removeGroup(ids[i]); } } // 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'); } if (this.groupsData) { // subscribe to new dataset var id = this.id; util.forEach(this.groupListeners, function (callback, event) { me.groupsData.on(event, callback, id); }); // draw all ms ids = this.groupsData.getIds(); this._onAddGroups(ids); } }; LineGraph.prototype._onUpdate = function (ids) { this._updateAllGroupData(); }; LineGraph.prototype._onAdd = function (ids) { this._onUpdate(ids); }; LineGraph.prototype._onRemove = function (ids) { this._onUpdate(ids); }; LineGraph.prototype._onUpdateGroups = function (groupIds) { this._updateAllGroupData(); }; LineGraph.prototype._onAddGroups = function (groupIds) { this._onUpdateGroups(groupIds); }; /** * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph * @param {Array} groupIds * @private */ LineGraph.prototype._onRemoveGroups = function (groupIds) { for (var i = 0; i < groupIds.length; i++) { this._removeGroup(groupIds[i]); } this.forceGraphUpdate = true; this.body.emitter.emit("change"); }; /** * this cleans the group out off the legends and the dataaxis * @param groupId * @private */ LineGraph.prototype._removeGroup = function (groupId) { if (this.groups.hasOwnProperty(groupId)) { if (this.groups[groupId].options.yAxisOrientation == 'right') { this.yAxisRight.removeGroup(groupId); this.legendRight.removeGroup(groupId); this.legendRight.redraw(); } else { this.yAxisLeft.removeGroup(groupId); this.legendLeft.removeGroup(groupId); this.legendLeft.redraw(); } delete this.groups[groupId]; } } /** * update a group object with the group dataset entree * * @param group * @param groupId * @private */ 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]); //If yAxisOrientation changed, clean out the group from the other axis. this.yAxisLeft.removeGroup(groupId); this.legendLeft.removeGroup(groupId); } else { this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); this.legendLeft.updateGroup(groupId, this.groups[groupId]); //If yAxisOrientation changed, clean out the group from the other axis. this.yAxisRight.removeGroup(groupId); this.legendRight.removeGroup(groupId); } } this.legendLeft.redraw(); this.legendRight.redraw(); }; /** * this updates all groups, it is used when there is an update the the itemset. * * @private */ LineGraph.prototype._updateAllGroupData = function () { if (this.itemsData != null) { var groupsContent = {}; var items = this.itemsData.get(); //pre-Determine array sizes, for more efficient memory claim var groupCounts = {}; for (var i = 0; i < items.length; i++) { var item = items[i]; var groupId = item.group; if (groupId === null || groupId === undefined) { groupId = UNGROUPED; } groupCounts.hasOwnProperty(groupId) ? groupCounts[groupId]++ : groupCounts[groupId] = 1; } //Now insert data into the arrays. for (var i = 0; i < items.length; i++) { var item = items[i]; var groupId = item.group; if (groupId === null || groupId === undefined) { groupId = UNGROUPED; } if (!groupsContent.hasOwnProperty(groupId)) { groupsContent[groupId] = new Array(groupCounts[groupId]); } //Copy data (because of unmodifiable DataView input. var extended = util.bridgeObject(item); extended.x = util.convert(item.x, 'Date'); extended.orginalY = item.y; //real Y // typecast all items to numbers. Takes around 10ms for 500.000 items extended.y = Number(item.y); var index= groupsContent[groupId].length - groupCounts[groupId]--; groupsContent[groupId][index] = extended; } //Make sure all groups are present, to allow removal of old groups for (var groupId in this.groups){ if (this.groups.hasOwnProperty(groupId)){ if (!groupsContent.hasOwnProperty(groupId)) { groupsContent[groupId] = new Array(0); } } } //Update legendas, style and axis for (var groupId in groupsContent) { if (groupsContent.hasOwnProperty(groupId)) { if (groupsContent[groupId].length == 0) { if (this.groups.hasOwnProperty(groupId)) { this._removeGroup(groupId); } } else { var group = undefined; if (this.groupsData != undefined) { group = this.groupsData.get(groupId); } if (group == undefined) { group = {id: groupId, content: this.options.defaultGroup + groupId}; } this._updateGroup(group, groupId); this.groups[groupId].setItems(groupsContent[groupId]); } } } this.forceGraphUpdate = true; this.body.emitter.emit("change"); } }; /** * Redraw the component, mandatory function * @return {boolean} Returns true if the component is resized */ LineGraph.prototype.redraw = function () { var resized = false; // calculate actual size and position this.props.width = this.dom.frame.offsetWidth; this.props.height = this.body.domProps.centerContainer.height - this.body.domProps.border.top - this.body.domProps.border.bottom; // update the graph if there is no lastWidth or width, used for the initial draw if (this.lastWidth === undefined && this.props.width) { this.forceGraphUpdate = 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.lastVisibleInterval = visibleInterval; // 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.props.width); this.svg.style.left = util.option.asSize(-this.props.width); // if the height of the graph is set as proportional, change the height of the svg if ((this.options.height + '').indexOf("%") != -1 || this.updateSVGheightOnResize == true) { this.updateSVGheight = true; } } // update the height of the graph on each redraw of the graph. if (this.updateSVGheight == true) { if (this.options.graphHeight != this.props.height + 'px') { this.options.graphHeight = this.props.height + 'px'; this.svg.style.height = this.props.height + 'px'; } this.updateSVGheight = false; } else { this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px'; } // zoomed is here to ensure that animations are shown correctly. if (resized == true || zoomed == true || this.abortedGraphUpdate == true || this.forceGraphUpdate == true) { resized = this._updateGraph() || resized; this.forceGraphUpdate = false; } else { // move the whole svg while dragging if (this.lastStart != 0) { var offset = this.body.range.start - this.lastStart; var range = this.body.range.end - this.body.range.start; if (this.props.width != 0) { var rangePerPixelInv = this.props.width / range; var xOffset = offset * rangePerPixelInv; this.svg.style.left = (-this.props.width - xOffset) + 'px'; } } } this.legendLeft.redraw(); this.legendRight.redraw(); return resized; }; LineGraph.prototype._getSortedGroupIds = function(){ // getting group Ids var grouplist = []; for (var groupId in this.groups) { if (this.groups.hasOwnProperty(groupId)) { var group = this.groups[groupId]; if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) { grouplist.push({id:groupId,zIndex:group.options.zIndex}); } } } util.insertSort(grouplist,function(a,b){ var az = a.zIndex; var bz = b.zIndex; if (az === undefined) az=0; if (bz === undefined) bz=0; return az==bz? 0: (az 0) { var groupsData = {}; // fill groups data, this only loads the data we require based on the timewindow this._getRelevantData(groupIds, groupsData, minDate, maxDate); // apply sampling, if disabled, it will pass through this function. this._applySampling(groupIds, groupsData); // we transform the X coordinates to detect collisions for (i = 0; i < groupIds.length; i++) { this._convertXcoordinates(groupsData[groupIds[i]]); } // now all needed data has been collected we start the processing. this._getYRanges(groupIds, groupsData, groupRanges); // update the Y axis first, we use this data to draw at the correct Y points changeCalled = this._updateYAxis(groupIds, groupRanges); // at changeCalled, abort this update cycle as the graph needs another update with new Width input from the Redraw container. // Cleanup SVG elements on abort. if (changeCalled == true) { DOMutil.cleanupElements(this.svgElements); this.abortedGraphUpdate = true; return true; } this.abortedGraphUpdate = false; // With the yAxis scaled correctly, use this to get the Y values of the points. var below = undefined; for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (this.options.stack === true && this.options.style === 'line') { if (group.options.excludeFromStacking == undefined || !group.options.excludeFromStacking) { if (below != undefined) { this._stack(groupsData[group.id], groupsData[below.id]); if (group.options.shaded.enabled == true && group.options.shaded.orientation !== "group"){ if (group.options.shaded.orientation == "top" && below.options.shaded.orientation !== "group"){ below.options.shaded.orientation="group"; below.options.shaded.groupId=group.id; } else { group.options.shaded.orientation="group"; group.options.shaded.groupId=below.id; } } } below = group; } } this._convertYcoordinates(groupsData[groupIds[i]], group); } //Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines. var paths = {}; for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (group.options.style === 'line' && group.options.shaded.enabled == true) { var dataset = groupsData[groupIds[i]]; if (dataset == null || dataset.length == 0) { continue; } if (!paths.hasOwnProperty(groupIds[i])) { paths[groupIds[i]] = Lines.calcPath(dataset, group); } if (group.options.shaded.orientation === "group") { var subGroupId = group.options.shaded.groupId; if (groupIds.indexOf(subGroupId) === -1) { console.log(group.id + ": Unknown shading group target given:" + subGroupId); continue; } if (!paths.hasOwnProperty(subGroupId)) { paths[subGroupId] = Lines.calcPath(groupsData[subGroupId], this.groups[subGroupId]); } Lines.drawShading(paths[groupIds[i]], group, paths[subGroupId], this.framework); } else { Lines.drawShading(paths[groupIds[i]], group, undefined, this.framework); } } } // draw the groups, calculating paths if still necessary. Bars.draw(groupIds, groupsData, this.framework); for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (groupsData[groupIds[i]].length > 0) { switch (group.options.style) { case "line": if (!paths.hasOwnProperty(groupIds[i])) { paths[groupIds[i]] = Lines.calcPath(groupsData[groupIds[i]], group); } Lines.draw(paths[groupIds[i]], group, this.framework); //explicit no break; case "point": //explicit no break; case "points": if (group.options.style == "point" || group.options.style == "points" || group.options.drawPoints.enabled == true) { Points.draw(groupsData[groupIds[i]], group, this.framework); } break; case "bar": // bar needs to be drawn enmasse //explicit no break default: //do nothing... } } } } } // cleanup unused svg elements DOMutil.cleanupElements(this.svgElements); return false; }; LineGraph.prototype._stack = function (data, subData) { var index, dx, dy, subPrevPoint, subNextPoint; index = 0; // for each data point we look for a matching on in the set below for (var j = 0; j < data.length; j++) { subPrevPoint = undefined; subNextPoint = undefined; // we look for time matches or a before-after point for (var k = index; k < subData.length; k++) { // if times match exactly if (subData[k].x === data[j].x) { subPrevPoint = subData[k]; subNextPoint = subData[k]; index = k; break; } else if (subData[k].x > data[j].x) { // overshoot subNextPoint = subData[k]; if (k == 0) { subPrevPoint = subNextPoint; } else { subPrevPoint = subData[k - 1]; } index = k; break; } } // in case the last data point has been used, we assume it stays like this. if (subNextPoint === undefined) { subPrevPoint = subData[subData.length - 1]; subNextPoint = subData[subData.length - 1]; } // linear interpolation dx = subNextPoint.x - subPrevPoint.x; dy = subNextPoint.y - subPrevPoint.y; if (dx == 0) { data[j].y = data[j].orginalY + subNextPoint.y; } else { data[j].y = data[j].orginalY + (dy / dx) * (data[j].x - subPrevPoint.x) + subPrevPoint.y; // ax + b where b is data[j].y } } } /** * 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. * * @param {array} groupIds * @param {object} groupsData * @param {date} minDate * @param {date} maxDate * @private */ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { var group, i, j, item; if (groupIds.length > 0) { for (i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; var itemsData = group.getItems(); // optimization for sorted data if (group.options.sort == true) { var first = Math.max(0, util.binarySearchValue(itemsData, minDate, 'x', 'before')); var last = Math.min(itemsData.length, util.binarySearchValue(itemsData, maxDate, 'x', 'after')+1); if (last <= 0) { last = itemsData.length; } var dataContainer = new Array(last-first); for (j = first; j < last; j++) { item = group.itemsData[j]; dataContainer[j-first] = item; } groupsData[groupIds[i]] = dataContainer; } else { // If unsorted data, all data is relevant, just returning entire structure groupsData[groupIds[i]] = group.itemsData; } } } }; /** * * @param groupIds * @param groupsData * @private */ LineGraph.prototype._applySampling = function (groupIds, groupsData) { var group; if (groupIds.length > 0) { for (var i = 0; i < groupIds.length; i++) { group = this.groups[groupIds[i]]; if (group.options.sampling == true) { var dataContainer = groupsData[groupIds[i]]; if (dataContainer.length > 0) { var increment = 1; var amountOfPoints = dataContainer.length; // 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. var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x); var pointsPerPixel = amountOfPoints / xDistance; increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel))); var sampledData = new Array(amountOfPoints); for (var j = 0; j < amountOfPoints; j += increment) { var idx = Math.round(j/increment); sampledData[idx]=dataContainer[j]; } groupsData[groupIds[i]] = sampledData.splice(0,Math.round(amountOfPoints/increment)); } } } } }; /** * * * @param {array} groupIds * @param {object} groupsData * @param {object} groupRanges | this is being filled here * @private */ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { var groupData, group, i; var combinedDataLeft = []; var combinedDataRight = []; var options; if (groupIds.length > 0) { for (i = 0; i < groupIds.length; i++) { groupData = groupsData[groupIds[i]]; options = this.groups[groupIds[i]].options; if (groupData.length > 0) { group = this.groups[groupIds[i]]; // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. if (options.stack === true && options.style === 'bar') { if (options.yAxisOrientation === 'left') { combinedDataLeft = combinedDataLeft.concat(group.getItems()); } else { combinedDataRight = combinedDataRight.concat(group.getItems()); } } else { groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); } } } // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. Bars.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left'); Bars.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right'); } }; /** * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden. * @param {Array} groupIds * @param {Object} groupRanges * @private */ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { var resized = false; var yAxisLeftUsed = false; var yAxisRightUsed = false; var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal; // if groups are present if (groupIds.length > 0) { // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. for (var i = 0; i < groupIds.length; i++) { var group = this.groups[groupIds[i]]; if (group && group.options.yAxisOrientation != 'right') { yAxisLeftUsed = true; minLeft = 1e9; maxLeft = -1e9; } else if (group && group.options.yAxisOrientation) { yAxisRightUsed = true; minRight = 1e9; maxRight = -1e9; } } // if there are items: for (var i = 0; i < groupIds.length; i++) { if (groupRanges.hasOwnProperty(groupIds[i])) { if (groupRanges[groupIds[i]].ignore !== true) { minVal = groupRanges[groupIds[i]].min; maxVal = groupRanges[groupIds[i]].max; if (groupRanges[groupIds[i]].yAxisOrientation != 'right') { 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); } } resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; if (yAxisRightUsed == true && yAxisLeftUsed == true) { this.yAxisLeft.drawIcons = true; this.yAxisRight.drawIcons = true; } else { this.yAxisLeft.drawIcons = false; this.yAxisRight.drawIcons = false; } this.yAxisRight.master = !yAxisLeftUsed; if (this.yAxisRight.master == false) { if (yAxisRightUsed == true) { this.yAxisLeft.lineOffset = this.yAxisRight.width; } else { this.yAxisLeft.lineOffset = 0; } resized = this.yAxisLeft.redraw() || resized; this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels; this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; this.yAxisRight.amountOfSteps = this.yAxisLeft.amountOfSteps; resized = this.yAxisRight.redraw() || resized; } else { resized = this.yAxisRight.redraw() || resized; } // clean the accumulated lists var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight']; for (var i = 0; i < tempGroups.length; i++) { if (groupIds.indexOf(tempGroups[i]) != -1) { groupIds.splice(groupIds.indexOf(tempGroups[i]), 1); } } return resized; }; /** * 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.hidden == false) { axis.hide(); changed = true; } } else { if (!axis.dom.frame.parentNode && axis.hidden == true) { axis.show(); changed = true; } } return changed; }; /** * 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 */ LineGraph.prototype._convertXcoordinates = function (datapoints) { var toScreen = this.body.util.toScreen; for (var i = 0; i < datapoints.length; i++) { datapoints[i].screen_x = toScreen(datapoints[i].x) + this.props.width; datapoints[i].screen_y = datapoints[i].y; //starting point for range calculations } }; /** * 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 * @param group * @returns {Array} * @private */ LineGraph.prototype._convertYcoordinates = function (datapoints, group) { var axis = this.yAxisLeft; var svgHeight = Number(this.svg.style.height.replace('px', '')); if (group.options.yAxisOrientation == 'right') { axis = this.yAxisRight; } for (var i = 0; i < datapoints.length; i++) { datapoints[i].screen_y = Math.round(axis.convertValue(datapoints[i].y)); } group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); }; module.exports = LineGraph;