|
|
@ -4,8 +4,8 @@ |
|
|
|
* |
|
|
|
* A dynamic, browser-based visualization library. |
|
|
|
* |
|
|
|
* @version 2.0.1-SNAPSHOT |
|
|
|
* @date 2014-06-24 |
|
|
|
* @version @@version |
|
|
|
* @date @@date |
|
|
|
* |
|
|
|
* @license |
|
|
|
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
|
|
|
@ -1366,6 +1366,151 @@ util._mergeOptions = function (mergeTarget, options, option) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd |
|
|
|
* arrays. This is done by giving a boolean value true if you want to use the byEnd. |
|
|
|
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check |
|
|
|
* if the time we selected (start or end) is within the current range). |
|
|
|
* |
|
|
|
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is |
|
|
|
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest, |
|
|
|
* either the start OR end time has to be in the range. |
|
|
|
* |
|
|
|
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems |
|
|
|
* @param {{start: number, end: number}} range |
|
|
|
* @param {Boolean} byEnd |
|
|
|
* @returns {number} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
util.binarySearch = function(orderedItems, range, field, field2) { |
|
|
|
var array = orderedItems; |
|
|
|
var interval = range.end - range.start; |
|
|
|
|
|
|
|
var found = false; |
|
|
|
var low = 0; |
|
|
|
var high = array.length; |
|
|
|
var guess = Math.floor(0.5*(high+low)); |
|
|
|
var newGuess; |
|
|
|
var value; |
|
|
|
|
|
|
|
if (high == 0) {guess = -1;} |
|
|
|
else if (high == 1) { |
|
|
|
value = field2 === undefined ? array[guess][field] : array[guess][field][field2]; |
|
|
|
if ((value > range.start - interval) && (value < range.end)) { |
|
|
|
guess = 0; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = -1; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
high -= 1; |
|
|
|
while (found == false) { |
|
|
|
value = field2 === undefined ? array[guess][field] : array[guess][field][field2]; |
|
|
|
if ((value > range.start - interval) && (value < range.end)) { |
|
|
|
found = true; |
|
|
|
} |
|
|
|
else { |
|
|
|
if (value < range.start - interval) { // it is too small --> increase low
|
|
|
|
low = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
else { // it is too big --> decrease high
|
|
|
|
high = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
newGuess = Math.floor(0.5*(high+low)); |
|
|
|
// not in list;
|
|
|
|
if (guess == newGuess) { |
|
|
|
guess = -1; |
|
|
|
found = true; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = newGuess; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return guess; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd |
|
|
|
* arrays. This is done by giving a boolean value true if you want to use the byEnd. |
|
|
|
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check |
|
|
|
* if the time we selected (start or end) is within the current range). |
|
|
|
* |
|
|
|
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is |
|
|
|
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest, |
|
|
|
* either the start OR end time has to be in the range. |
|
|
|
* |
|
|
|
* @param {Array} orderedItems |
|
|
|
* @param {{start: number, end: number}} target |
|
|
|
* @param {Boolean} byEnd |
|
|
|
* @returns {number} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
util.binarySearchGeneric = function(orderedItems, target, field, sidePreference) { |
|
|
|
var array = orderedItems; |
|
|
|
var found = false; |
|
|
|
var low = 0; |
|
|
|
var high = array.length; |
|
|
|
var guess = Math.floor(0.5*(high+low)); |
|
|
|
var newGuess; |
|
|
|
var prevValue, value, nextValue; |
|
|
|
|
|
|
|
if (high == 0) {guess = -1;} |
|
|
|
else if (high == 1) { |
|
|
|
value = array[guess][field]; |
|
|
|
if (value == target) { |
|
|
|
guess = 0; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = -1; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
high -= 1; |
|
|
|
while (found == false) { |
|
|
|
prevValue = array[Math.max(0,guess - 1)][field]; |
|
|
|
value = array[guess][field]; |
|
|
|
nextValue = array[Math.min(array.length-1,guess + 1)][field]; |
|
|
|
|
|
|
|
if (value == target || prevValue < target && value > target || value < target && nextValue > target) { |
|
|
|
found = true; |
|
|
|
if (value != target) { |
|
|
|
if (sidePreference == 'before') { |
|
|
|
if (prevValue < target && value > target) { |
|
|
|
guess = Math.max(0,guess - 1); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
if (value < target && nextValue > target) { |
|
|
|
guess = Math.min(array.length-1,guess + 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
if (value < target) { // it is too small --> increase low
|
|
|
|
low = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
else { // it is too big --> decrease high
|
|
|
|
high = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
newGuess = Math.floor(0.5*(high+low)); |
|
|
|
// not in list;
|
|
|
|
if (guess == newGuess) { |
|
|
|
guess = -2; |
|
|
|
found = true; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = newGuess; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return guess; |
|
|
|
}; |
|
|
|
/** |
|
|
|
* Created by Alex on 6/20/14. |
|
|
|
*/ |
|
|
@ -1424,7 +1569,7 @@ DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) { |
|
|
|
// check if there is an redundant element
|
|
|
|
if (JSONcontainer[elementType].redundant.length > 0) { |
|
|
|
element = JSONcontainer[elementType].redundant[0]; |
|
|
|
JSONcontainer[elementType].redundant.shift() |
|
|
|
JSONcontainer[elementType].redundant.shift(); |
|
|
|
} |
|
|
|
else { |
|
|
|
// create a new element and add it to the SVG
|
|
|
@ -1449,7 +1594,7 @@ DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) { |
|
|
|
* |
|
|
|
* @param elementType |
|
|
|
* @param JSONcontainer |
|
|
|
* @param svgContainer |
|
|
|
* @param DOMContainer |
|
|
|
* @returns {*} |
|
|
|
* @private |
|
|
|
*/ |
|
|
@ -1460,7 +1605,7 @@ DOMutil.getDOMElement = function (elementType, JSONcontainer, DOMContainer) { |
|
|
|
// check if there is an redundant element
|
|
|
|
if (JSONcontainer[elementType].redundant.length > 0) { |
|
|
|
element = JSONcontainer[elementType].redundant[0]; |
|
|
|
JSONcontainer[elementType].redundant.shift() |
|
|
|
JSONcontainer[elementType].redundant.shift(); |
|
|
|
} |
|
|
|
else { |
|
|
|
// create a new element and add it to the SVG
|
|
|
@ -2747,7 +2892,7 @@ DataView.prototype.unsubscribe = DataView.prototype.off; |
|
|
|
*/ |
|
|
|
function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) { |
|
|
|
this.id = groupId; |
|
|
|
var fields = ['style','yAxisOrientation','barChart','drawPoints','shaded','catmullRom'] |
|
|
|
var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom'] |
|
|
|
this.options = util.selectiveDeepExtend(fields,{},options); |
|
|
|
this.usingDefaultStyle = group.className === undefined; |
|
|
|
this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; |
|
|
@ -2755,11 +2900,24 @@ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) { |
|
|
|
if (this.usingDefaultStyle == true) { |
|
|
|
this.groupsUsingDefaultStyles[0] += 1; |
|
|
|
} |
|
|
|
this.itemsData = []; |
|
|
|
} |
|
|
|
|
|
|
|
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 { |
|
|
|
this.itemsData = []; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
GraphGroup.prototype.setOptions = function(options) { |
|
|
|
if (options !== undefined) { |
|
|
|
var fields = ['yAxisOrientation','style','barChart']; |
|
|
|
var fields = ['yAxisOrientation','style','barChart','sort']; |
|
|
|
util.selectiveDeepExtend(fields, this.options, options); |
|
|
|
|
|
|
|
util._mergeOptions(this.options, options,'catmullRom'); |
|
|
@ -3218,12 +3376,14 @@ DataAxis.prototype.setRange = function (start, end) { |
|
|
|
* @return {boolean} Returns true if the component is resized |
|
|
|
*/ |
|
|
|
DataAxis.prototype.redraw = function () { |
|
|
|
var changeCalled = false; |
|
|
|
if (this.amountOfGroups == 0) { |
|
|
|
this.hide(); |
|
|
|
} |
|
|
|
else { |
|
|
|
|
|
|
|
this.height = this.linegraphSVG.offsetHeight; |
|
|
|
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 ? Number(this.options.width.replace("px","")) : 0; |
|
|
@ -3265,12 +3425,12 @@ DataAxis.prototype.redraw = function () { |
|
|
|
frame.style.width = this.width + 'px'; |
|
|
|
frame.style.height = this.height + "px"; |
|
|
|
} |
|
|
|
|
|
|
|
this._redrawLabels(); |
|
|
|
changeCalled = this._redrawLabels(); |
|
|
|
if (this.options.icons == true) { |
|
|
|
this._redrawGroupIcons(); |
|
|
|
} |
|
|
|
} |
|
|
|
return changeCalled; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
@ -3283,10 +3443,8 @@ DataAxis.prototype._redrawLabels = function () { |
|
|
|
var orientation = this.options['orientation']; |
|
|
|
|
|
|
|
// calculate range and step (step such that we have space for 7 characters per label)
|
|
|
|
var start = this.yRange.start; |
|
|
|
var end = this.yRange.end; |
|
|
|
var minimumStep = (this.props.majorCharHeight || 10); //in pixels
|
|
|
|
var step = new DataStep(start, end, minimumStep, this.dom.frame.offsetHeight); |
|
|
|
var step = new DataStep(this.yRange.start, this.yRange.end, minimumStep, this.dom.frame.offsetHeight); |
|
|
|
this.step = step; |
|
|
|
step.first(); |
|
|
|
|
|
|
@ -3306,6 +3464,7 @@ DataAxis.prototype._redrawLabels = function () { |
|
|
|
amountOfSteps = this.height / stepPixels; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.valueAtZero = step.marginEnd; |
|
|
|
var marginStartPos = 0; |
|
|
|
|
|
|
@ -3316,6 +3475,7 @@ DataAxis.prototype._redrawLabels = function () { |
|
|
|
this.maxLabelSize = 0; |
|
|
|
var y = 0; |
|
|
|
while (max < Math.round(amountOfSteps)) { |
|
|
|
|
|
|
|
y = Math.round(max * stepPixels); |
|
|
|
marginStartPos = max * stepPixels; |
|
|
|
var isMajor = step.isMajor(); |
|
|
@ -3340,19 +3500,29 @@ DataAxis.prototype._redrawLabels = function () { |
|
|
|
max++; |
|
|
|
} |
|
|
|
|
|
|
|
this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step); |
|
|
|
|
|
|
|
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"; |
|
|
|
this.body.emitter.emit("changed"); |
|
|
|
DOMutil.cleanupElements(this.DOMelements); |
|
|
|
this.redraw(); |
|
|
|
return; |
|
|
|
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.maxLabelSize + offset; |
|
|
|
this.options.width = this.width + "px"; |
|
|
|
DOMutil.cleanupElements(this.DOMelements); |
|
|
|
this.redraw(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
else { |
|
|
|
DOMutil.cleanupElements(this.DOMelements); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step); |
|
|
|
|
|
|
|
DOMutil.cleanupElements(this.DOMelements); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
@ -3477,6 +3647,8 @@ function Linegraph(body, options) { |
|
|
|
this.defaultOptions = { |
|
|
|
yAxisOrientation: 'left', |
|
|
|
defaultGroup: 'default', |
|
|
|
sort: true, |
|
|
|
sampling: true, |
|
|
|
graphHeight: '400px', |
|
|
|
shaded: { |
|
|
|
enabled: false, |
|
|
@ -3623,7 +3795,7 @@ Linegraph.prototype._create = function(){ |
|
|
|
*/ |
|
|
|
Linegraph.prototype.setOptions = function(options) { |
|
|
|
if (options) { |
|
|
|
var fields = ['defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis']; |
|
|
|
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'); |
|
|
@ -3790,6 +3962,7 @@ Linegraph.prototype.setGroups = function(groups) { |
|
|
|
|
|
|
|
Linegraph.prototype._onUpdate = function(ids) { |
|
|
|
this._updateUngrouped(); |
|
|
|
this._updateAllGroupData(); |
|
|
|
this._updateGraph(); |
|
|
|
this.redraw(); |
|
|
|
}; |
|
|
@ -3800,7 +3973,9 @@ Linegraph.prototype._onUpdateGroups = function (groupIds) { |
|
|
|
var group = this.groupsData.get(groupIds[i]); |
|
|
|
this._updateGroup(group, groupIds[i]); |
|
|
|
} |
|
|
|
|
|
|
|
this._updateUngrouped(); |
|
|
|
this._updateAllGroupData(); |
|
|
|
this._updateGraph(); |
|
|
|
this.redraw(); |
|
|
|
}; |
|
|
@ -3861,6 +4036,20 @@ Linegraph.prototype._updateGroup = function (group, groupId) { |
|
|
|
this.legendRight.redraw(); |
|
|
|
}; |
|
|
|
|
|
|
|
Linegraph.prototype._updateAllGroupData = function () { |
|
|
|
if (this.itemsData != null) { |
|
|
|
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"}} |
|
|
|
)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Create or delete the group holding all ungrouped items. This group is used when |
|
|
@ -3945,16 +4134,92 @@ Linegraph.prototype._updateGraph = function () { |
|
|
|
// reset the svg elements
|
|
|
|
DOMutil.prepareElements(this.svgElements); |
|
|
|
|
|
|
|
|
|
|
|
// todo: discuss with Jos why this filter is so HORRIBLY slow (factor 5!)
|
|
|
|
// groupData = group.itemsData.get({filter:
|
|
|
|
// function (item) {
|
|
|
|
// return (item.x > minDate && item.x < maxDate);
|
|
|
|
// }}
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
|
|
if (this.width != 0 && this.itemsData != null) { |
|
|
|
// look at different lines
|
|
|
|
var groupIds = this.itemsData.distinct('group'); |
|
|
|
var group; |
|
|
|
|
|
|
|
var group, groupData, preprocessedGroup, i; |
|
|
|
var preprocessedGroupData = []; |
|
|
|
var processedGroupData = []; |
|
|
|
var groupRanges = []; |
|
|
|
var changeCalled = false; |
|
|
|
|
|
|
|
// this is the range of the SVG canvas
|
|
|
|
var minDate = this.body.util.toTime(- this.body.domProps.root.width); |
|
|
|
var maxDate = this.body.util.toTime(2 * this.body.domProps.root.width); |
|
|
|
|
|
|
|
// 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) { |
|
|
|
this._updateYAxis(groupIds); |
|
|
|
for (var i = 0; i < groupIds.length; i++) { |
|
|
|
for (i = 0; i < groupIds.length; i++) { |
|
|
|
group = this.groups[groupIds[i]]; |
|
|
|
this._drawGraph(group); |
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
// 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.options)) |
|
|
|
} |
|
|
|
|
|
|
|
// 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); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -3968,13 +4233,13 @@ Linegraph.prototype._updateGraph = function () { |
|
|
|
* @param {array} groupIds |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
Linegraph.prototype._updateYAxis = function (groupIds) { |
|
|
|
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 groups are present
|
|
|
|
if (groupIds.length > 0) { |
|
|
|
for (var i = 0; i < groupIds.length; i++) { |
|
|
@ -3984,32 +4249,19 @@ Linegraph.prototype._updateYAxis = function (groupIds) { |
|
|
|
orientation = 'right'; |
|
|
|
} |
|
|
|
|
|
|
|
var view = new vis.DataSet(this.itemsData.get({filter: function (item) { |
|
|
|
return item.group == groupIds[i]; |
|
|
|
}})); |
|
|
|
minVal = view.min("y").y; |
|
|
|
maxVal = view.max("y").y; |
|
|
|
minVal = groupRanges[i].min; |
|
|
|
maxVal = groupRanges[i].max; |
|
|
|
|
|
|
|
if (orientation == 'left') { |
|
|
|
yAxisLeftUsed = true; |
|
|
|
if (minLeft > minVal) { |
|
|
|
minLeft = minVal; |
|
|
|
} |
|
|
|
if (maxLeft < maxVal) { |
|
|
|
maxLeft = maxVal; |
|
|
|
} |
|
|
|
minLeft = minLeft > minVal ? minVal : minLeft; |
|
|
|
maxLeft = maxLeft < maxVal ? maxVal : maxLeft; |
|
|
|
} |
|
|
|
else { |
|
|
|
yAxisRightUsed = true; |
|
|
|
if (minRight > minVal) { |
|
|
|
minRight = minVal; |
|
|
|
} |
|
|
|
if (maxRight < maxVal) { |
|
|
|
maxRight = maxVal; |
|
|
|
} |
|
|
|
minRight = minRight > minVal ? minVal : minRight; |
|
|
|
maxRight = maxRight < maxVal ? maxVal : maxRight; |
|
|
|
} |
|
|
|
|
|
|
|
delete view; |
|
|
|
} |
|
|
|
if (yAxisLeftUsed == true) { |
|
|
|
this.yAxisLeft.setRange(minLeft, maxLeft); |
|
|
@ -4040,13 +4292,14 @@ Linegraph.prototype._updateYAxis = function (groupIds) { |
|
|
|
if (yAxisRightUsed == true) { |
|
|
|
this.yAxisLeft.lineOffset = this.yAxisRight.width; |
|
|
|
} |
|
|
|
this.yAxisLeft.redraw(); |
|
|
|
changeCalled = this.yAxisLeft.redraw() || changeCalled; |
|
|
|
this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels; |
|
|
|
this.yAxisRight.redraw(); |
|
|
|
changeCalled = this.yAxisRight.redraw() || changeCalled; |
|
|
|
} |
|
|
|
else { |
|
|
|
this.yAxisRight.redraw(); |
|
|
|
changeCalled = this.yAxisRight.redraw() || changeCalled; |
|
|
|
} |
|
|
|
return changeCalled; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
@ -4075,31 +4328,14 @@ Linegraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* determine if the graph is a bar or line, get the group options and the datapoints. Then draw the graph. |
|
|
|
* |
|
|
|
* @param groupId |
|
|
|
*/ |
|
|
|
Linegraph.prototype._drawGraph = function (group) { |
|
|
|
var datapoints = this.itemsData.get({filter: function (item) {return item.group == group.id;}, type: {x:"Date"}}); |
|
|
|
|
|
|
|
if (group.options.style == 'line') { |
|
|
|
this._drawLineGraph(datapoints, group); |
|
|
|
} |
|
|
|
else { |
|
|
|
this._drawBarGraph(datapoints, group); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* draw a bar graph |
|
|
|
* @param datapoints |
|
|
|
* @param group |
|
|
|
*/ |
|
|
|
Linegraph.prototype._drawBarGraph = function (datapoints, group) { |
|
|
|
if (datapoints != null) { |
|
|
|
if (datapoints.length > 0) { |
|
|
|
var dataset = this._prepareData(datapoints, group.options); |
|
|
|
Linegraph.prototype._drawBarGraph = function (dataset, group) { |
|
|
|
if (dataset != null) { |
|
|
|
if (dataset.length > 0) { |
|
|
|
var width, coreDistance; |
|
|
|
var minWidth = 0.1 * group.options.barChart.width; |
|
|
|
|
|
|
@ -4128,12 +4364,11 @@ Linegraph.prototype._drawBarGraph = function (datapoints, group) { |
|
|
|
* @param datapoints |
|
|
|
* @param group |
|
|
|
*/ |
|
|
|
Linegraph.prototype._drawLineGraph = function (datapoints, group) { |
|
|
|
if (datapoints != null) { |
|
|
|
if (datapoints.length > 0) { |
|
|
|
var dataset = this._prepareData(datapoints, group.options); |
|
|
|
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); |
|
|
|
|
|
|
@ -4153,7 +4388,7 @@ Linegraph.prototype._drawLineGraph = function (datapoints, group) { |
|
|
|
dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0; |
|
|
|
} |
|
|
|
else { |
|
|
|
dFill = "M" + dataset[0].x + "," + this.svg.offsetHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + this.svg.offsetHeight; |
|
|
|
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); |
|
|
@ -4185,6 +4420,48 @@ Linegraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 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._preprocessData = function (datapoints, group) { |
|
|
|
var extractedData = []; |
|
|
|
var xValue, yValue, increment; |
|
|
|
var toScreen = this.body.util.toScreen; |
|
|
|
|
|
|
|
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 amountOfPoints = datapoints.length; |
|
|
|
var pointsPerPixel = amountOfPoints/xDistance; |
|
|
|
increment = Math.max(1,Math.round(pointsPerPixel)); |
|
|
|
} |
|
|
|
else { |
|
|
|
increment = 1; |
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
// extractedData.sort(function (a,b) {return a.x - b.x;});
|
|
|
|
return {min: yMin, max: yMax, data: extractedData}; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* 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. |
|
|
@ -4194,11 +4471,11 @@ Linegraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg) |
|
|
|
* @returns {Array} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
Linegraph.prototype._prepareData = function (datapoints, options) { |
|
|
|
Linegraph.prototype._convertYvalues = function (datapoints, options) { |
|
|
|
var extractedData = []; |
|
|
|
var xValue, yValue; |
|
|
|
var axis = this.yAxisLeft; |
|
|
|
var toScreen = this.body.util.toScreen; |
|
|
|
var svgHeight = Number(this.svg.style.height.replace("px","")); |
|
|
|
this.zeroPosition = 0; |
|
|
|
|
|
|
|
if (options.yAxisOrientation == 'right') { |
|
|
@ -4206,12 +4483,12 @@ Linegraph.prototype._prepareData = function (datapoints, options) { |
|
|
|
} |
|
|
|
|
|
|
|
for (var i = 0; i < datapoints.length; i++) { |
|
|
|
xValue = Math.round(toScreen(datapoints[i].x) + this.width - 1); |
|
|
|
xValue = datapoints[i].x; |
|
|
|
yValue = Math.round(axis.convertValue(datapoints[i].y)); |
|
|
|
extractedData.push({x: xValue, y: yValue}); |
|
|
|
} |
|
|
|
|
|
|
|
this.zeroPosition = Math.min(this.svg.offsetHeight, axis.convertValue(0)); |
|
|
|
this.zeroPosition = Math.min(svgHeight, axis.convertValue(0)); |
|
|
|
|
|
|
|
// extractedData.sort(function (a,b) {return a.x - b.x;});
|
|
|
|
return extractedData; |
|
|
@ -4251,12 +4528,12 @@ Linegraph.prototype._catmullRomUniform = function(data) { |
|
|
|
// bp0 = { x: p2.x, y: p2.y };
|
|
|
|
|
|
|
|
d += "C" + |
|
|
|
Math.round(bp1.x) + "," + |
|
|
|
Math.round(bp1.y) + " " + |
|
|
|
Math.round(bp2.x) + "," + |
|
|
|
Math.round(bp2.y) + " " + |
|
|
|
Math.round(p2.x) + "," + |
|
|
|
Math.round(p2.y) + " "; |
|
|
|
bp1.x + "," + |
|
|
|
bp1.y + " " + |
|
|
|
bp2.x + "," + |
|
|
|
bp2.y + " " + |
|
|
|
p2.x + "," + |
|
|
|
p2.y + " "; |
|
|
|
} |
|
|
|
|
|
|
|
return d; |
|
|
@ -4331,12 +4608,12 @@ Linegraph.prototype._catmullRom = function(data, group) { |
|
|
|
if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;} |
|
|
|
if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;} |
|
|
|
d += "C" + |
|
|
|
Math.round(bp1.x) + "," + |
|
|
|
Math.round(bp1.y) + " " + |
|
|
|
Math.round(bp2.x) + "," + |
|
|
|
Math.round(bp2.y) + " " + |
|
|
|
Math.round(p2.x) + "," + |
|
|
|
Math.round(p2.y) + " "; |
|
|
|
bp1.x + "," + |
|
|
|
bp1.y + " " + |
|
|
|
bp2.x + "," + |
|
|
|
bp2.y + " " + |
|
|
|
p2.x + "," + |
|
|
|
p2.y + " "; |
|
|
|
} |
|
|
|
|
|
|
|
return d; |
|
|
@ -4354,10 +4631,10 @@ Linegraph.prototype._linear = function(data) { |
|
|
|
var d = ""; |
|
|
|
for (var i = 0; i < data.length; i++) { |
|
|
|
if (i == 0) { |
|
|
|
d += Math.round(data[i].x) + "," + Math.round(data[i].y); |
|
|
|
d += data[i].x + "," + data[i].y; |
|
|
|
} |
|
|
|
else { |
|
|
|
d += " " + Math.round(data[i].x) + "," + Math.round(data[i].y); |
|
|
|
d += " " + data[i].x + "," + data[i].y; |
|
|
|
} |
|
|
|
} |
|
|
|
return d; |
|
|
@ -4427,10 +4704,11 @@ function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) { |
|
|
|
DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) { |
|
|
|
this._start = start; |
|
|
|
this._end = end; |
|
|
|
this.setFirst(); |
|
|
|
|
|
|
|
if (this.autoScale) { |
|
|
|
this.setMinimumStep(minimumStep, containerHeight, forcedStepSize); |
|
|
|
} |
|
|
|
this.setFirst(); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
@ -4447,8 +4725,13 @@ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { |
|
|
|
var minorStepIdx = -1; |
|
|
|
var magnitudefactor = Math.pow(10,orderOfMagnitude); |
|
|
|
|
|
|
|
var start = 0; |
|
|
|
if (orderOfMagnitude < 0) { |
|
|
|
start = orderOfMagnitude; |
|
|
|
} |
|
|
|
|
|
|
|
var solutionFound = false; |
|
|
|
for (var i = 0; i <= orderOfMagnitude; i++) { |
|
|
|
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]; |
|
|
@ -8928,14 +9211,14 @@ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range |
|
|
|
|
|
|
|
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
|
|
|
|
if (newVisibleItems.length == 0) { |
|
|
|
initialPosByStart = this._binarySearch(orderedItems, range, false); |
|
|
|
initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); |
|
|
|
} |
|
|
|
else { |
|
|
|
initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); |
|
|
|
} |
|
|
|
|
|
|
|
// use visible search to find a visible ItemRange (only based on endTime)
|
|
|
|
var initialPosByEnd = this._binarySearch(orderedItems, range, true); |
|
|
|
var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); |
|
|
|
|
|
|
|
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
|
|
|
|
if (initialPosByStart != -1) { |
|
|
@ -8960,72 +9243,7 @@ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range |
|
|
|
return newVisibleItems; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd |
|
|
|
* arrays. This is done by giving a boolean value true if you want to use the byEnd. |
|
|
|
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check |
|
|
|
* if the time we selected (start or end) is within the current range). |
|
|
|
* |
|
|
|
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is |
|
|
|
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest, |
|
|
|
* either the start OR end time has to be in the range. |
|
|
|
* |
|
|
|
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems |
|
|
|
* @param {{start: number, end: number}} range |
|
|
|
* @param {Boolean} byEnd |
|
|
|
* @returns {number} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
Group.prototype._binarySearch = function(orderedItems, range, byEnd) { |
|
|
|
var array = []; |
|
|
|
var byTime = byEnd ? 'end' : 'start'; |
|
|
|
if (byEnd == true) {array = orderedItems.byEnd; } |
|
|
|
else {array = orderedItems.byStart;} |
|
|
|
|
|
|
|
var interval = range.end - range.start; |
|
|
|
|
|
|
|
var found = false; |
|
|
|
var low = 0; |
|
|
|
var high = array.length; |
|
|
|
var guess = Math.floor(0.5*(high+low)); |
|
|
|
var newGuess; |
|
|
|
|
|
|
|
if (high == 0) {guess = -1;} |
|
|
|
else if (high == 1) { |
|
|
|
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { |
|
|
|
guess = 0; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = -1; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
high -= 1; |
|
|
|
while (found == false) { |
|
|
|
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { |
|
|
|
found = true; |
|
|
|
} |
|
|
|
else { |
|
|
|
if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
|
|
|
|
low = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
else { // it is too big --> decrease high
|
|
|
|
high = Math.floor(0.5*(high+low)); |
|
|
|
} |
|
|
|
newGuess = Math.floor(0.5*(high+low)); |
|
|
|
// not in list;
|
|
|
|
if (guess == newGuess) { |
|
|
|
guess = -1; |
|
|
|
found = true; |
|
|
|
} |
|
|
|
else { |
|
|
|
guess = newGuess; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return guess; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* this function checks if an item is invisible. If it is NOT we make it visible |
|
|
@ -9775,6 +9993,22 @@ Timeline.prototype._toScreen = function(time) { |
|
|
|
return (time.valueOf() - conversion.offset) * conversion.scale; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Convert a datetime (Date object) into a position on the root |
|
|
|
* This is used to get the pixel density estimate for the screen, not the center panel |
|
|
|
* @param {Date} time A date |
|
|
|
* @return {int} x The position on root in pixels which corresponds |
|
|
|
* with the given date. |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
// TODO: move this function to Range
|
|
|
|
Graph2d.prototype._toGlobalScreen = function(time) { |
|
|
|
var conversion = this.range.conversion(this.props.root.width); |
|
|
|
return (time.valueOf() - conversion.offset) * conversion.scale; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Initialize watching when option autoResize is true |
|
|
|
* @private |
|
|
@ -9970,6 +10204,7 @@ function Graph2d (container, items, options, groups) { |
|
|
|
util: { |
|
|
|
snap: null, // will be specified after TimeAxis is created
|
|
|
|
toScreen: me._toScreen.bind(me), |
|
|
|
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
|
|
|
|
toTime: me._toTime.bind(me) |
|
|
|
} |
|
|
|
}; |
|
|
@ -10005,6 +10240,7 @@ function Graph2d (container, items, options, groups) { |
|
|
|
this.setOptions(options); |
|
|
|
} |
|
|
|
|
|
|
|
// IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
|
|
|
|
if (groups) { |
|
|
|
this.setGroups(groups); |
|
|
|
} |
|
|
@ -10602,7 +10838,7 @@ Graph2d.prototype.redraw = function() { |
|
|
|
resized = component.redraw() || resized; |
|
|
|
}); |
|
|
|
if (resized) { |
|
|
|
// keep repainting until all sizes are settled
|
|
|
|
// keep redrawing until all sizes are settled
|
|
|
|
this.redraw(); |
|
|
|
} |
|
|
|
}; |
|
|
@ -10637,6 +10873,21 @@ Graph2d.prototype._toScreen = function(time) { |
|
|
|
return (time.valueOf() - conversion.offset) * conversion.scale; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Convert a datetime (Date object) into a position on the root |
|
|
|
* This is used to get the pixel density estimate for the screen, not the center panel |
|
|
|
* @param {Date} time A date |
|
|
|
* @return {int} x The position on root in pixels which corresponds |
|
|
|
* with the given date. |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
// TODO: move this function to Range
|
|
|
|
Graph2d.prototype._toGlobalScreen = function(time) { |
|
|
|
var conversion = this.range.conversion(this.props.root.width); |
|
|
|
return (time.valueOf() - conversion.offset) * conversion.scale; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Initialize watching when option autoResize is true |
|
|
|
* @private |
|
|
@ -18989,6 +19240,7 @@ function Graph (container, data, options) { |
|
|
|
// these functions are triggered when the dataset is edited
|
|
|
|
this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; |
|
|
|
|
|
|
|
|
|
|
|
// set constant values
|
|
|
|
this.constants = { |
|
|
|
nodes: { |
|
|
|