Browse Source

optimized, 8 amazing examples, just docs left....

css_transitions
Alex de Mulder 10 years ago
parent
commit
146793270d
20 changed files with 999 additions and 356 deletions
  1. +418
    -166
      dist/vis.js
  2. +1
    -1
      dist/vis.min.css
  3. +13
    -13
      dist/vis.min.js
  4. +3
    -0
      examples/graph2d/01_basic.html
  5. +2
    -1
      examples/graph2d/03_groups.html
  6. +3
    -1
      examples/graph2d/07_scrollingAndSorting.html
  7. +30
    -8
      examples/graph2d/08_performance.html
  8. +87
    -0
      examples/graph2d/default.css
  9. +22
    -0
      examples/graph2d/index.html
  10. +2
    -1
      examples/timeline/03_a_lot_of_data.html
  11. +3
    -3
      src/DOMutil.js
  12. +1
    -0
      src/graph/Graph.js
  13. +8
    -2
      src/timeline/DataStep.js
  14. +18
    -1
      src/timeline/Graph2d.js
  15. +16
    -0
      src/timeline/Timeline.js
  16. +26
    -14
      src/timeline/component/DataAxis.js
  17. +15
    -2
      src/timeline/component/GraphGroup.js
  18. +3
    -68
      src/timeline/component/Group.js
  19. +182
    -75
      src/timeline/component/Linegraph.js
  20. +146
    -0
      src/util.js

+ 418
- 166
dist/vis.js View File

@ -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: {

+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 13
- 13
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 3
- 0
examples/graph2d/01_basic.html View File

@ -1,6 +1,9 @@
<!DOCTYPE HTML>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<title>Graph2d | Basic Example</title>
<style type="text/css">

+ 2
- 1
examples/graph2d/03_groups.html View File

@ -2,7 +2,8 @@
<html>
<head>
<title>Graph2d | Groups Example</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<style type="text/css">
body, html {
font-family: sans-serif;

+ 3
- 1
examples/graph2d/07_scrollingAndSorting.html View File

@ -21,7 +21,8 @@
<br /><br />
Vertical scrolling is planned, though not yet available. The graphHeight also does not conform if only the <code>height</code> is defined.
<br /><br />
Graph2D does not sort the data. It is plotted as it is inputted. If you want your data to be sorted, sort your data.
You can manually disable the automatic sorting of the datapoints by using the <code>sort</code> option. However, doing so does reduce the optimization
of the drawing so if you have a lot of points, keep <code>sort</code> turned on for the best results.
</div>
<br />
<div id="visualization"></div>
@ -59,6 +60,7 @@
var dataset = new vis.DataSet(items);
var options = {
legend: true,
sort: false,
defaultGroup: 'doodle',
graphHeight: '1500px',
height: '500px',

+ 30
- 8
examples/graph2d/08_performance.html View File

@ -21,16 +21,20 @@
<div style="width:700px; font-size:14px; text-align: justify;">
This example is a test of the performance of the Graph2D. Select the amount of datapoints you want to plot and press draw.
You can choose between the style of the points as well as the interpolation method. This can only be toggled with the buttons.
The interpolation options may not look different for this dataset but you can see their effects clearly in example 7.
<br /><br />
Linear interpolation and no points are the settings that will render quickest. By default, graph2D will downsample when there are more
than 1 point per pixel. This can be manually disabled at the cost of performance by using the <code>sampling</code> option.
</div>
<br />
<p>
<label for="count">Number of items</label>
<input id="count" value="100">
<input id="count" value="50000">
<input id="toggleInterpolation" type="button" value="toggle Interpolation">
Interpolation method: <input id="interpolation" value="linear">
<input id="togglePoints" type="button" value="toggle Points">
Points style: <input id="points" value="none"> <br/>
<input id="draw" type="button" value="draw" style="width:200px;">
Points style: <input id="points" value="none"> <br/><br/>
<input id="draw" type="button" value="draw" style="width:200px;"> <span id="description"><b>Click the draw button to load the data!</b></span>
</p>
<div id="visualization"></div>
@ -88,20 +92,37 @@
type: {start: 'ISODate', end: 'ISODate' }
});
var startPoint = now;
var endPoint = now.clone().add('days', 100);
var endPoint = now + 3600000 * 5000;
// create data
// create data -- this is seperated into 3 functions so we can update the span.
function createData() {
var span = document.getElementById("description");
span.innerHTML = 'Generating data... (just javascript, not vis.graph2D)...';
setTimeout(generateData,1);
}
function generateData() {
var count = parseInt(document.getElementById('count').value) || 100;
var newData = [];
var span = document.getElementById("description");
var start = now;
for (var i = 0; i < count; i++) {
newData.push({id: i, x: now.clone().add('days', i), y: Math.sin(0.1 * 3.14159654 * i)});
var yval = Math.sin(i/100) * Math.cos(i/50) * 50 + Math.sin(i/1000) * 50;
newData.push({id: i, x: start + 3600000 * i, y: yval});
}
span.innerHTML = 'Loading data into Graph2D...';
setTimeout(function() {loadDataIntoVis(newData);},1);
}
function loadDataIntoVis(newData) {
var span = document.getElementById("description");
dataset.clear();
dataset.add(newData);
span.innerHTML = 'Done!';
}
createData();
document.getElementById('draw').onclick = createData;
document.getElementById('toggleInterpolation').onclick = toggleInterpolation;
@ -109,7 +130,8 @@
var container = document.getElementById('visualization');
var options = {
drawPoints: false,
sampling: true,
drawPoints: {enabled:false, size:3},
catmullRom: false,
start: startPoint,
end: endPoint

+ 87
- 0
examples/graph2d/default.css View File

@ -0,0 +1,87 @@
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
body, td, th {
font-family: arial, sans-serif;
font-size: 11pt;
color: #4D4D4D;
line-height: 1.7em;
}
#container {
margin: 0 auto;
padding-bottom: 50px;
width: 900px;
}
h1 {
font-size: 180%;
font-weight: bold;
padding: 0;
margin: 1em 0 1em 0;
}
h2 {
padding-top: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #a0c0f0;
color: #2B7CE9;
}
h3 {
font-size: 140%;
}
a {
color: #2B7CE9;
text-decoration: none;
}
a:visited {
color: #2E60A4;
}
a:hover {
color: red;
text-decoration: underline;
}
hr {
border: none 0;
border-top: 1px solid #abc;
height: 1px;
}
pre {
display: block;
font-size: 10pt;
line-height: 1.5em;
font-family: monospace;
}
pre, code {
background-color: #f5f5f5;
}
table
{
border-collapse: collapse;
}
th {
font-weight: bold;
border: 1px solid lightgray;
background-color: #E5E5E5;
text-align: left;
vertical-align: top;
padding: 5px;
}
td {
border: 1px solid lightgray;
padding: 5px;
vertical-align: top;
}

+ 22
- 0
examples/graph2d/index.html View File

@ -0,0 +1,22 @@
<html>
<head>
<link rel='stylesheet' href='default.css' type='text/css'>
</head>
<body>
<div id="container">
<h1>Graph2D Examples</h1>
<p><a href="01_basic.html">01_basic.html</a></p>
<p><a href="02_bars.html">02_bars.html</a></p>
<p><a href="03_groups.html">03_groups.html</a></p>
<p><a href="04_rightAxis.html">04_rightAxis.html</a></p>
<p><a href="05_bothAxis.html">05_bothAxis.html</a></p>
<p><a href="06_interpolation.html">06_interpolation.html</a></p>
<p><a href="07_scrollingAndSorting.html">07_scrollingAndSorting.html</a></p>
<p><a href="08_performance.html">08_performance.html</a></p>
</div>
</body>
</html>

+ 2
- 1
examples/timeline/03_a_lot_of_data.html View File

@ -38,8 +38,9 @@
function createData() {
var count = parseInt(document.getElementById('count').value) || 100;
var newData = [];
var start = now;
for (var i = 0; i < count; i++) {
newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)});
newData.push({id: i, content: 'item ' + i, start: start + 24*3600*1000 * i}); // much much faster than now.clone add days
}
items.clear();
items.add(newData);

+ 3
- 3
src/DOMutil.js View File

@ -56,7 +56,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
@ -81,7 +81,7 @@ DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
*
* @param elementType
* @param JSONcontainer
* @param svgContainer
* @param DOMContainer
* @returns {*}
* @private
*/
@ -92,7 +92,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

+ 1
- 0
src/graph/Graph.js View File

@ -35,6 +35,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: {

+ 8
- 2
src/timeline/DataStep.js View File

@ -57,10 +57,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();
};
/**
@ -77,8 +78,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];

+ 18
- 1
src/timeline/Graph2d.js View File

@ -38,6 +38,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)
}
};
@ -73,6 +74,7 @@ function Graph2d (container, items, options, groups) {
this.setOptions(options);
}
// IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
if (groups) {
this.setGroups(groups);
}
@ -670,7 +672,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();
}
};
@ -705,6 +707,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

+ 16
- 0
src/timeline/Timeline.js View File

@ -699,6 +699,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

+ 26
- 14
src/timeline/component/DataAxis.js View File

@ -201,12 +201,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;
@ -248,12 +250,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;
};
/**
@ -266,10 +268,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();
@ -289,6 +289,7 @@ DataAxis.prototype._redrawLabels = function () {
amountOfSteps = this.height / stepPixels;
}
this.valueAtZero = step.marginEnd;
var marginStartPos = 0;
@ -299,6 +300,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();
@ -323,19 +325,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);
};
/**

+ 15
- 2
src/timeline/component/GraphGroup.js View File

@ -6,7 +6,7 @@
*/
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;
@ -14,11 +14,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');

+ 3
- 68
src/timeline/component/Group.js View File

@ -324,14 +324,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) {
@ -356,72 +356,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

+ 182
- 75
src/timeline/component/Linegraph.js View File

@ -7,6 +7,8 @@ function Linegraph(body, options) {
this.defaultOptions = {
yAxisOrientation: 'left',
defaultGroup: 'default',
sort: true,
sampling: true,
graphHeight: '400px',
shaded: {
enabled: false,
@ -153,7 +155,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');
@ -320,6 +322,7 @@ Linegraph.prototype.setGroups = function(groups) {
Linegraph.prototype._onUpdate = function(ids) {
this._updateUngrouped();
this._updateAllGroupData();
this._updateGraph();
this.redraw();
};
@ -330,7 +333,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();
};
@ -391,6 +396,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
@ -475,16 +494,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]];
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]];
this._drawGraph(group);
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);
}
}
}
}
@ -498,13 +593,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++) {
@ -514,32 +609,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);
@ -570,13 +652,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;
};
/**
@ -605,31 +688,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;
@ -658,12 +724,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);
@ -683,7 +748,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);
@ -715,6 +780,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.
@ -724,11 +831,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') {
@ -736,12 +843,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;
@ -781,12 +888,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;
@ -861,12 +968,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;
@ -884,10 +991,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;

+ 146
- 0
src/util.js View File

@ -1055,3 +1055,149 @@ 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;
};

Loading…
Cancel
Save