diff --git a/HISTORY.md b/HISTORY.md index a85d5dc2..99d814da 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,13 +18,20 @@ http://visjs.org ### Graph2d -- Implemented a new option for `shaded.orientation` to always shade towards zero. - Thanks @ludost. -- Fixed support for using a `DataView` in Graph2d. Thanks @ludost. +- Large refactoring of Graph2d code base: + - Implemented a new option for `shaded.orientation` to always shade towards zero. + - Implemented a new option for `shaded.orientation` to follow another group (fill in between) + - Implemented line-graph stacking + - Fixed support for using a `DataView` in Graph2d. + - Implemented a new zindex option for controlling svg rendering order. + - Performance updates and fixes ### DataSet - Fixed #1487: DataSet cannot remove an item with id `0` correctly. +### DataView +- Added the map() function from DataSet. + ## 2015-11-27, version 4.10.0 diff --git a/lib/DataView.js b/lib/DataView.js index 7d5b2316..32a7325a 100644 --- a/lib/DataView.js +++ b/lib/DataView.js @@ -230,6 +230,49 @@ DataView.prototype.getIds = function (options) { return ids; }; +/** + * Map every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Object[]} mappedItems + */ +DataView.prototype.map = function (callback,options) { + var mappedItems = []; + if (this._data) { + var defaultFilter = this._options.filter; + var filter; + + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + } + } + else { + filter = options.filter; + } + } + else { + filter = defaultFilter; + } + + mappedItems = this._data.map(callback,{ + filter: filter, + order: options && options.order + }); + } + else { + mappedItems = []; + } + + return mappedItems; +}; + /** * Get the DataSet to which this DataView is connected. In case there is a chain * of multiple DataViews, the root DataSet of this chain is returned. diff --git a/lib/timeline/component/DataAxis.js b/lib/timeline/component/DataAxis.js index 02593649..4a1291a3 100644 --- a/lib/timeline/component/DataAxis.js +++ b/lib/timeline/component/DataAxis.js @@ -59,7 +59,7 @@ function DataAxis (body, options, svg, linegraphOptions) { this.setOptions(options); this.width = Number(('' + this.options.width).replace("px","")); this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; + this.height = this.linegraphSVG.getBoundingClientRect().height; this.hidden = false; this.stepPixels = 25; diff --git a/lib/timeline/component/GraphGroup.js b/lib/timeline/component/GraphGroup.js index 37917fc3..289c4f00 100644 --- a/lib/timeline/component/GraphGroup.js +++ b/lib/timeline/component/GraphGroup.js @@ -16,7 +16,7 @@ var Points = require('./graph2d_types/points'); */ function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { this.id = groupId; - var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation']; + var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation', 'zIndex']; this.options = util.selectiveBridgeObject(fields, options); this.usingDefaultStyle = group.className === undefined; this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; @@ -29,17 +29,6 @@ function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { this.visible = group.visible === undefined ? true : group.visible; } -function insertionSort (a,compare) { - for (var i = 0; i < a.length; i++) { - var k = a[i]; - for (var j = i; j > 0 && compare(k,a[j - 1])<0; j--) { - a[j] = a[j - 1]; - } - a[j] = k; - } - return a; -} - /** * this loads a reference to all items in this group into this group. * @param {array} items @@ -48,7 +37,7 @@ GraphGroup.prototype.setItems = function (items) { if (items != null) { this.itemsData = items; if (this.options.sort == true) { - insertionSort(this.itemsData,function (a, b) { + util.insertSort(this.itemsData,function (a, b) { return a.x > b.x ? 1 : -1; }); } @@ -76,7 +65,7 @@ GraphGroup.prototype.setZeroPosition = function (pos) { */ GraphGroup.prototype.setOptions = function (options) { if (options !== undefined) { - var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'excludeFromLegend', 'excludeFromStacking']; + var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'excludeFromLegend', 'excludeFromStacking', 'zIndex']; util.selectiveDeepExtend(fields, this.options, options); // if the group's drawPoints is a function delegate the callback to the onRender property @@ -145,7 +134,8 @@ GraphGroup.prototype.getLegend = function (iconWidth, iconHeight, framework, x, case "line": Lines.drawIcon(this, x, y, iconWidth, iconHeight, framework); break; - case "points": + case "points": //explicit no break + case "point": Points.drawIcon(this, x, y, iconWidth, iconHeight, framework); break; case "bar": diff --git a/lib/timeline/component/LineGraph.js b/lib/timeline/component/LineGraph.js index edf29781..bfdbaeb2 100644 --- a/lib/timeline/component/LineGraph.js +++ b/lib/timeline/component/LineGraph.js @@ -312,7 +312,6 @@ LineGraph.prototype.setItems = function (items) { ids = this.itemsData.getIds(); this._onAdd(ids); } - this.redraw(true); }; @@ -333,7 +332,9 @@ LineGraph.prototype.setGroups = function (groups) { // remove all drawn groups ids = this.groupsData.getIds(); this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw + for (var i = 0; i < ids.length; i++) { + this._removeGroup(ids[i]); + } } // replace the dataset @@ -358,7 +359,6 @@ LineGraph.prototype.setGroups = function (groups) { ids = this.groupsData.getIds(); this._onAddGroups(ids); } - this._onUpdate(); }; LineGraph.prototype._onUpdate = function (ids) { @@ -593,6 +593,31 @@ LineGraph.prototype.redraw = function (forceGraphUpdate) { }; +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 = {}; @@ -715,8 +732,10 @@ LineGraph.prototype._updateGraph = function () { } Lines.draw(paths[groupIds[i]], group, this.framework); //explicit no break; + case "point": + //explicit no break; case "points": - if (group.options.style == "points" || group.options.drawPoints.enabled == true) { + if (group.options.style == "point" || group.options.style == "points" || group.options.drawPoints.enabled == true) { Points.draw(groupsData[groupIds[i]], group, this.framework); } break; diff --git a/lib/timeline/component/graph2d_types/bar.js b/lib/timeline/component/graph2d_types/bar.js index 7b2e65f1..dfa688a6 100644 --- a/lib/timeline/component/graph2d_types/bar.js +++ b/lib/timeline/component/graph2d_types/bar.js @@ -16,6 +16,8 @@ Bargraph.drawIcon = function (group, x, y, iconWidth, iconHeight, framework) { outline.setAttributeNS(null, "class", "vis-outline"); var barWidth = Math.round(0.3 * iconWidth); + var originalWidth = group.options.barChart.width; + var scale = originalWidth / barWidth; var bar1Height = Math.round(0.4 * iconHeight); var bar2Height = Math.round(0.75 * iconHeight); @@ -28,7 +30,7 @@ Bargraph.drawIcon = function (group, x, y, iconWidth, iconHeight, framework) { var groupTemplate = { style: group.options.drawPoints.style, styles: group.options.drawPoints.styles, - size: Math.max(barWidth/5,group.options.drawPoints.size), + size: (group.options.drawPoints.size / scale), className: group.className }; DOMutil.drawPoint(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, groupTemplate, framework.svgElements, framework.svg); @@ -49,7 +51,7 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { var coreDistance; var key, drawData; var group; - var i,j; + var i, j; var barPoints = 0; // combine all barchart data @@ -61,6 +63,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { combinedData.push({ screen_x: processedGroupData[groupIds[i]][j].screen_x, screen_y: processedGroupData[groupIds[i]][j].screen_y, + x: processedGroupData[groupIds[i]][j].x, + y: processedGroupData[groupIds[i]][j].y, groupId: groupIds[i], label: processedGroupData[groupIds[i]][j].label }); @@ -70,7 +74,9 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { } } - if (barPoints === 0) {return;} + if (barPoints === 0) { + return; + } // sort by time and by group combinedData.sort(function (a, b) { @@ -93,15 +99,23 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { key = combinedData[i].screen_x; var heightOffset = 0; if (intersections[key] === undefined) { - if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].screen_x - key);} - if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].screen_x - key));} + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].screen_x - key); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].screen_x - key)); + } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); } else { var nextKey = i + (intersections[key].amount - intersections[key].resolved); var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].screen_x - key);} - if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].screen_x - key));} + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].screen_x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].screen_x - key)); + } drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); intersections[key].resolved += 1; @@ -117,17 +131,23 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) { } else if (group.options.barChart.sideBySide === true) { drawData.width = drawData.width / intersections[key].amount; - drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1)); - if (group.options.barChart.align === 'left') {drawData.offset -= 0.5*drawData.width;} - else if (group.options.barChart.align === 'right') {drawData.offset += 0.5*drawData.width;} + drawData.offset += (intersections[key].resolved) * drawData.width - (0.5 * drawData.width * (intersections[key].amount + 1)); + if (group.options.barChart.align === 'left') { + drawData.offset -= 0.5 * drawData.width; + } + else if (group.options.barChart.align === 'right') { + drawData.offset += 0.5 * drawData.width; + } } } DOMutil.drawBar(combinedData[i].screen_x + drawData.offset, combinedData[i].screen_y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].screen_y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style); // draw points if (group.options.drawPoints.enabled === true) { let pointData = { - screen_x:combinedData[i].screen_x, - screen_y:combinedData[i].screen_y - heightOffset, + screen_x: combinedData[i].screen_x, + screen_y: combinedData[i].screen_y - heightOffset, + x: combinedData[i].x, + y: combinedData[i].y, groupId: combinedData[i].groupId, label: combinedData[i].label }; @@ -156,7 +176,12 @@ Bargraph._getDataIntersections = function (intersections, combinedData) { } if (coreDistance === 0) { if (intersections[combinedData[i].screen_x] === undefined) { - intersections[combinedData[i].screen_x] = {amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0}; + intersections[combinedData[i].screen_x] = { + amount: 0, + resolved: 0, + accumulatedPositive: 0, + accumulatedNegative: 0 + }; } intersections[combinedData[i].screen_x].amount += 1; } @@ -201,13 +226,13 @@ Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { return {width: width, offset: offset}; }; -Bargraph.getStackedYRange = function(combinedData, groupRanges, groupIds, groupLabel, orientation) { +Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { if (combinedData.length > 0) { // sort by time and by group combinedData.sort(function (a, b) { if (a.screen_x === b.screen_x) { return a.groupId < b.groupId ? -1 : 1; - } + } else { return a.screen_x - b.screen_x; } diff --git a/lib/timeline/component/graph2d_types/points.js b/lib/timeline/component/graph2d_types/points.js index 4241b622..c6a05a92 100644 --- a/lib/timeline/component/graph2d_types/points.js +++ b/lib/timeline/component/graph2d_types/points.js @@ -66,7 +66,6 @@ function getCallback(framework, group) { if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') { callback = group.group.options.drawPoints.onRender; } - return callback; } diff --git a/lib/timeline/optionsGraph2d.js b/lib/timeline/optionsGraph2d.js index 883c4da1..978624a2 100644 --- a/lib/timeline/optionsGraph2d.js +++ b/lib/timeline/optionsGraph2d.js @@ -162,6 +162,7 @@ let allOptions = { zoomKey: {string: ['ctrlKey', 'altKey', 'metaKey', '']}, zoomMax: {number}, zoomMin: {number}, + zIndex: {number}, __type__: {object} }; @@ -265,7 +266,8 @@ let configureOptions = { zoomable: true, zoomKey: ['ctrlKey', 'altKey', 'metaKey', ''], zoomMax: [315360000000000, 10, 315360000000000, 1], - zoomMin: [10, 10, 315360000000000, 1] + zoomMin: [10, 10, 315360000000000, 1], + zIndex: 0 } }; diff --git a/lib/util.js b/lib/util.js index 9b1999f0..8fac4aee 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1253,6 +1253,23 @@ exports.bridgeObject = function (referenceObject) { } }; +/** + * This method provides a stable sort implementation, very fast for presorted data + * + * @param a the array + * @param a order comparator + * @returns {the array} + */ +exports.insertSort = function (a,compare) { + for (var i = 0; i < a.length; i++) { + var k = a[i]; + for (var j = i; j > 0 && compare(k,a[j - 1])<0; j--) { + a[j] = a[j - 1]; + } + a[j] = k; + } + return a; +} /** * this is used to set the options of subobjects in the options object. A requirement of these subobjects