Browse Source

Merge pull request #1495 from almende/newShading

Cleanup, performance and new shading & stacking
codeClimate
Alex 8 years ago
parent
commit
6be82f3594
11 changed files with 603 additions and 564 deletions
  1. +12
    -3
      docs/graph2d/index.html
  2. +1
    -1
      examples/graph2d/06_interpolation.html
  3. +14
    -13
      examples/graph2d/17_dynamicStyling.html
  4. +48
    -22
      examples/graph2d/20_shading.html
  5. +68
    -69
      lib/timeline/component/GraphGroup.js
  6. +1
    -1
      lib/timeline/component/Legend.js
  7. +228
    -128
      lib/timeline/component/LineGraph.js
  8. +31
    -58
      lib/timeline/component/graph2d_types/bar.js
  9. +195
    -248
      lib/timeline/component/graph2d_types/line.js
  10. +2
    -19
      lib/timeline/component/graph2d_types/points.js
  11. +3
    -2
      lib/timeline/optionsGraph2d.js

+ 12
- 3
docs/graph2d/index.html View File

@ -758,9 +758,18 @@ onRender: function(item, group, graph2d) {
<tr parent="shaded" class="hidden">
<td class="greenField indent">shaded.orientation</td>
<td>String</td>
<td>'zero'</td>
<td>This determines if the shaded area is at the bottom or at the top of the curve, or always towards the zero-axis of the graph. The options are 'zero', 'bottom' or 'top'.
See <a href="../../examples/graph2d/20_shading.html">Example 20</a> what these options look like.</td>
<td>'bottom'</td>
<td>This determines if the shaded area is at the bottom or at the top of the curve, or always towards the zero-axis of the graph.
The options are 'zero', 'bottom', 'top', or the special case of 'group'. If group is chosen, the option groupId is required.
See <a href="../../examples/graph2d/20_shading.html">Example 20</a> what these options look like.
</td>
</tr>
<tr parent="shaded" class="hidden">
<td class="greenField indent">shaded.groupId</td>
<td>String</td>
<td>undefined</td>
<td>The id of the group which should be used as the other shading limit.
</td>
</tr>
<tr parent="shaded" class="hidden">
<td class="greenField indent">shaded.style</td>

+ 1
- 1
examples/graph2d/06_interpolation.html View File

@ -88,7 +88,7 @@
}
var options = {
dataPoints: false,
drawPoints: false,
dataAxis: {visible: false},
legend: true,
start: '2014-06-11',

+ 14
- 13
examples/graph2d/17_dynamicStyling.html View File

@ -79,7 +79,8 @@
<select id="fill" onchange="updateStyle()">
<option value="">none</option>
<option value="top">top</option>
<option value="bottom" selected="selected">bottom</option>
<option value="bottom">bottom</option>
<option value="zero" selected="selected">zero</option>
</select>
</td>
</tr>
@ -98,16 +99,16 @@
<td>Fill Opacity</td>
<td>
<select id="fillopacity" onchange="updateStyle()">
<option value="opacity:0.1;">0.1</option>
<option value="opacity:0.2;">0.2</option>
<option value="opacity:0.3;">0.3</option>
<option value="opacity:0.4;">0.4</option>
<option value="opacity:0.5;">0.5</option>
<option value="opacity:0.6;" selected="selected">0.6</option>
<option value="opacity:0.7;">0.7</option>
<option value="opacity:0.8;">0.8</option>
<option value="opacity:0.9;">0.9</option>
<option value="opacity:1;">1</option>
<option value="fill-opacity:0.1;">0.1</option>
<option value="fill-opacity:0.2;">0.2</option>
<option value="fill-opacity:0.3;">0.3</option>
<option value="fill-opacity:0.4;">0.4</option>
<option value="fill-opacity:0.5;">0.5</option>
<option value="fill-opacity:0.6;" selected="selected">0.6</option>
<option value="fill-opacity:0.7;">0.7</option>
<option value="fill-opacity:0.8;">0.8</option>
<option value="fill-opacity:0.9;">0.9</option>
<option value="fill-opacity:1;">1</option>
</select>
</td>
</tr>
@ -187,7 +188,7 @@
{x: '2014-06-11', y: 10, group: 0},
{x: '2014-06-12', y: 25, group: 0},
{x: '2014-06-13', y: 30, group: 0},
{x: '2014-06-14', y: 10, group: 0},
{x: '2014-06-14', y: -10, group: 0},
{x: '2014-06-15', y: 15, group: 0},
{x: '2014-06-16', y: 30, group: 0}
];
@ -210,7 +211,7 @@
style: 'square' // square, circle
},
shaded: {
orientation: 'bottom' // top, bottom
orientation: 'zero' // top, bottom
}
}
};

+ 48
- 22
examples/graph2d/20_shading.html View File

@ -24,7 +24,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['top', 'bottom', 'zero', 'none'];
var names = ['top', 'bottom', 'zero', 'none', 'group', 'none'];
var groups = new vis.DataSet();
groups.add({
id: 0,
@ -55,34 +55,60 @@
groups.add({
id: 3,
content: names[3],
options: {
excludeFromLegend: true
}
});
groups.add({
id: 4,
content: names[4],
options: {
shaded: {
orientation: 'group',
groupId: '3'
}
}
});
groups.add({
id: 5,
content: names[5]
});
var container = document.getElementById('visualization');
var items = [
{x: '2014-06-10', y: 0, group: 0},
{x: '2014-06-11', y: 15, group: 0},
{x: '2014-06-13', y: -15, group: 0},
{x: '2014-06-14', y: 0, group: 0},
{x: '2014-06-15', y: 0, group: 1},
{x: '2014-06-16', y: 15, group: 1},
{x: '2014-06-17', y: -15, group: 1},
{x: '2014-06-18', y: 0, group: 1},
{x: '2014-06-19', y: 0, group: 2},
{x: '2014-06-20', y: 15, group: 2},
{x: '2014-06-21', y: -15, group: 2},
{x: '2014-06-22', y: 0, group: 2},
{x: '2014-06-23', y: 0, group: 3},
{x: '2014-06-24', y: 15, group: 3},
{x: '2014-06-25', y: -15, group: 3},
{x: '2014-06-26', y: 0, group: 3},
];
var items = [
{x: '2014-06-11', y: 0, group: 0},
{x: '2014-06-12', y: 15, group: 0},
{x: '2014-06-13', y: -15, group: 0},
{x: '2014-06-14', y: 0, group: 0},
{x: '2014-06-15', y: 0, group: 1},
{x: '2014-06-16', y: 15, group: 1},
{x: '2014-06-17', y: -15, group: 1},
{x: '2014-06-18', y: 0, group: 1},
{x: '2014-06-19', y: 0, group: 2},
{x: '2014-06-20', y: 15, group: 2},
{x: '2014-06-21', y: -15, group: 2},
{x: '2014-06-22', y: 0, group: 2},
{x: '2014-06-23', y: -2, group: 3},
{x: '2014-06-24', y: 13, group: 3},
{x: '2014-06-25', y: -17, group: 3},
{x: '2014-06-26', y: -2, group: 3},
{x: '2014-06-23', y: 2, group: 4},
{x: '2014-06-24', y: 17, group: 4},
{x: '2014-06-25', y: -13, group: 4},
{x: '2014-06-26', y: 2, group: 4},
{x: '2014-06-27', y: 0, group: 5},
{x: '2014-06-28', y: 15, group: 5},
{x: '2014-06-29', y: -15, group: 5},
{x: '2014-06-30', y: 0, group: 5}
];
var dataset = new vis.DataSet(items);
var options = {
legend: true,
start: '2014-06-05',
end: '2014-06-29'
start: '2014-06-07',
end: '2014-07-03'
};
var graph2d = new vis.Graph2d(container, dataset, groups, options);

+ 68
- 69
lib/timeline/component/GraphGroup.js View File

@ -1,8 +1,5 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var Line = require('./graph2d_types/line');
var Bar = require('./graph2d_types/bar');
var Points = require('./graph2d_types/points');
/**
* /**
@ -14,10 +11,10 @@ var Points = require('./graph2d_types/points');
* It enumerates through the default styles
* @constructor
*/
function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) {
this.id = groupId;
var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','interpolation']
this.options = util.selectiveBridgeObject(fields,options);
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation'];
this.options = util.selectiveBridgeObject(fields, options);
this.usingDefaultStyle = group.className === undefined;
this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
this.zeroPosition = 0;
@ -29,20 +26,28 @@ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
this.visible = group.visible === undefined ? true : group.visible;
}
function insertionSort (a,compare) {
for (var i = 0; i < a.length; i++) {
var k = a[i];
for (var j = i; j > 0 && compare(k,a[j - 1])<0; j--) {
a[j] = a[j - 1];
}
a[j] = k;
}
return a;
}
/**
* this loads a reference to all items in this group into this group.
* @param {array} items
*/
GraphGroup.prototype.setItems = function(items) {
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;})
}
// typecast all items to numbers. Takes around 10ms for 500.000 items
for (var i = 0; i < this.itemsData.length; i++) {
this.itemsData[i].y = Number(this.itemsData[i].y);
insertionSort(this.itemsData,function (a, b) {
return a.x > b.x ? 1 : -1;
});
}
}
else {
@ -50,35 +55,37 @@ GraphGroup.prototype.setItems = function(items) {
}
};
GraphGroup.prototype.getItems = function () {
return this.itemsData;
}
/**
* this is used for plotting barcharts, this way, we only have to calculate it once.
* this is used for barcharts and shading, this way, we only have to calculate it once.
* @param pos
*/
GraphGroup.prototype.setZeroPosition = function(pos) {
GraphGroup.prototype.setZeroPosition = function (pos) {
this.zeroPosition = pos;
};
/**
* set the options of the graph group over the default options.
* @param options
*/
GraphGroup.prototype.setOptions = function(options) {
GraphGroup.prototype.setOptions = function (options) {
if (options !== undefined) {
var fields = ['sampling','style','sort','yAxisOrientation','barChart','excludeFromLegend'];
var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'excludeFromLegend'];
util.selectiveDeepExtend(fields, this.options, options);
// if the group's drawPoints is a function delegate the callback to the onRender property
if (typeof options.drawPoints == 'function') {
options.drawPoints = {
onRender: options.drawPoints
}
options.drawPoints = {
onRender: options.drawPoints
}
}
util.mergeOptions(this.options, options,'interpolation');
util.mergeOptions(this.options, options,'drawPoints');
util.mergeOptions(this.options, options,'shaded');
util.mergeOptions(this.options, options, 'interpolation');
util.mergeOptions(this.options, options, 'drawPoints');
util.mergeOptions(this.options, options, 'shaded');
if (options.interpolation) {
if (typeof options.interpolation == 'object') {
@ -97,16 +104,6 @@ GraphGroup.prototype.setOptions = function(options) {
}
}
}
if (this.options.style == 'line') {
this.type = new Line(this.id, this.options);
}
else if (this.options.style == 'bar') {
this.type = new Bar(this.id, this.options);
}
else if (this.options.style == 'points') {
this.type = new Points(this.id, this.options);
}
};
@ -114,7 +111,7 @@ GraphGroup.prototype.setOptions = function(options) {
* this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph
* @param group
*/
GraphGroup.prototype.update = function(group) {
GraphGroup.prototype.update = function (group) {
this.group = group;
this.content = group.content || 'graph';
this.className = group.className || this.className || 'vis-graph-group' + this.groupsUsingDefaultStyles[0] % 10;
@ -124,6 +121,8 @@ GraphGroup.prototype.update = function(group) {
};
//TODO: move these render functions into the type specific files and call them from LineGraph
/**
* draw the icon for the legend.
*
@ -134,7 +133,7 @@ GraphGroup.prototype.update = function(group) {
* @param iconWidth
* @param iconHeight
*/
GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
var fillHeight = iconHeight * 0.5;
var path, fillPath;
@ -142,40 +141,43 @@ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, icon
outline.setAttributeNS(null, "x", x);
outline.setAttributeNS(null, "y", y - fillHeight);
outline.setAttributeNS(null, "width", iconWidth);
outline.setAttributeNS(null, "height", 2*fillHeight);
outline.setAttributeNS(null, "height", 2 * fillHeight);
outline.setAttributeNS(null, "class", "vis-outline");
if (this.options.style == 'line') {
path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
path.setAttributeNS(null, "class", this.className);
if(this.style !== undefined) {
if (this.style !== undefined) {
path.setAttributeNS(null, "style", this.style);
}
path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
path.setAttributeNS(null, "d", "M" + x + "," + y + " L" + (x + iconWidth) + "," + y + "");
if (this.options.shaded.enabled == true) {
fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
if (this.options.shaded.orientation == 'top') {
fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
"L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
fillPath.setAttributeNS(null, "d", "M" + x + ", " + (y - fillHeight) +
"L" + x + "," + y + " L" + (x + iconWidth) + "," + y + " L" + (x + iconWidth) + "," + (y - fillHeight));
}
else {
fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
"L"+x+"," + (y + fillHeight) + " " +
"L"+ (x + iconWidth) + "," + (y + fillHeight) +
"L"+ (x + iconWidth) + ","+y);
fillPath.setAttributeNS(null, "d", "M" + x + "," + y + " " +
"L" + x + "," + (y + fillHeight) + " " +
"L" + (x + iconWidth) + "," + (y + fillHeight) +
"L" + (x + iconWidth) + "," + y);
}
fillPath.setAttributeNS(null, "class", this.className + " vis-icon-fill");
if (this.options.shaded.style !== undefined && this.options.shaded.style !== "") {
fillPath.setAttributeNS(null, "style", this.options.shaded.style);
}
}
if (this.options.drawPoints.enabled == true) {
var groupTemplate = {
style: this.options.drawPoints.style,
styles: this.options.drawPoints.styles,
size:this.options.drawPoints.size,
className: this.className
};
DOMutil.drawPoint(x + 0.5 * iconWidth, y, groupTemplate, JSONcontainer, SVGcontainer);
var groupTemplate = {
style: this.options.drawPoints.style,
styles: this.options.drawPoints.styles,
size: this.options.drawPoints.size,
className: this.className
};
DOMutil.drawPoint(x + 0.5 * iconWidth, y, groupTemplate, JSONcontainer, SVGcontainer);
}
}
else {
@ -183,10 +185,10 @@ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, icon
var bar1Height = Math.round(0.4 * iconHeight);
var bar2Height = Math.round(0.75 * iconHeight);
var offset = Math.round((iconWidth - (2 * barWidth))/3);
var offset = Math.round((iconWidth - (2 * barWidth)) / 3);
DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style);
DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style);
DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style);
DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style);
}
};
@ -198,23 +200,20 @@ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, icon
* @param iconHeight
* @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
*/
GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
};
GraphGroup.prototype.getYRange = function(groupData) {
return this.type.getYRange(groupData);
};
GraphGroup.prototype.getData = function(groupData) {
return this.type.getData(groupData);
GraphGroup.prototype.getLegend = function (iconWidth, iconHeight) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', "svg");
this.drawIcon(0, 0.5 * iconHeight, [], svg, iconWidth, iconHeight);
return {icon: svg, label: this.content, orientation: this.options.yAxisOrientation};
};
GraphGroup.prototype.draw = function(dataset, group, framework) {
this.type.draw(dataset, group, framework);
GraphGroup.prototype.getYRange = function (groupData) {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (var j = 0; j < groupData.length; j++) {
yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
module.exports = GraphGroup;

+ 1
- 1
lib/timeline/component/Legend.js View File

@ -187,7 +187,7 @@ Legend.prototype.drawLegendIcons = function() {
var groupArray = Object.keys(this.groups);
groupArray.sort(function (a,b) {
return (a < b ? -1 : 1);
})
});
// this resets the elements so the order is maintained
DOMutil.resetElements(this.svgElements);

+ 228
- 128
lib/timeline/component/LineGraph.js View File

@ -6,8 +6,9 @@ var Component = require('./Component');
var DataAxis = require('./DataAxis');
var GraphGroup = require('./GraphGroup');
var Legend = require('./Legend');
var BarFunctions = require('./graph2d_types/bar');
var LineFunctions = require('./graph2d_types/line');
var Bars = require('./graph2d_types/bar');
var Lines = require('./graph2d_types/line');
var Points = require('./graph2d_types/points');
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
@ -27,11 +28,11 @@ function LineGraph(body, options) {
defaultGroup: 'default',
sort: true,
sampling: true,
stack:false,
stack: false,
graphHeight: '400px',
shaded: {
enabled: false,
orientation: 'bottom' // top, bottom
orientation: 'bottom' // top, bottom, zero
},
style: 'line', // line, bar
barChart: {
@ -56,15 +57,19 @@ function LineGraph(body, options) {
width: '40px',
visible: true,
alignZeros: true,
left:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
title: {text:undefined,style:undefined}
left: {
range: {min: undefined, max: undefined},
format: function (value) {
return value;
},
title: {text: undefined, style: undefined}
},
right:{
range: {min:undefined,max:undefined},
format: function (value) {return value;},
title: {text:undefined,style:undefined}
right: {
range: {min: undefined, max: undefined},
format: function (value) {
return value;
},
title: {text: undefined, style: undefined}
}
},
legend: {
@ -133,10 +138,10 @@ function LineGraph(body, options) {
this.setOptions(options);
this.groupsUsingDefaultStyles = [0];
this.COUNTER = 0;
this.body.emitter.on('rangechanged', function() {
this.body.emitter.on('rangechanged', function () {
me.lastStart = me.body.range.start;
me.svg.style.left = util.option.asSize(-me.props.width);
me.redraw.call(me,true);
me.redraw.call(me, true);
});
// create the HTML DOM
@ -151,15 +156,15 @@ LineGraph.prototype = new Component();
/**
* Create the HTML DOM for the ItemSet
*/
LineGraph.prototype._create = function(){
LineGraph.prototype._create = function () {
var frame = document.createElement('div');
frame.className = 'vis-line-graph';
this.dom.frame = frame;
// create svg element for graph drawing.
this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg.style.position = 'relative';
this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px';
this.svg.style.display = 'block';
frame.appendChild(this.svg);
@ -182,23 +187,23 @@ LineGraph.prototype._create = function(){
* set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
* @param {object} options
*/
LineGraph.prototype.setOptions = function(options) {
LineGraph.prototype.setOptions = function (options) {
if (options) {
var fields = ['sampling','defaultGroup','stack','height','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups'];
if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) {
this.updateSVGheight = true;
this.updateSVGheightOnResize = true;
}
else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) {
if (parseInt((options.graphHeight + '').replace("px",'')) < this.body.domProps.centerContainer.height) {
if (parseInt((options.graphHeight + '').replace("px", '')) < this.body.domProps.centerContainer.height) {
this.updateSVGheight = true;
}
}
util.selectiveDeepExtend(fields, this.options, options);
util.mergeOptions(this.options, options,'interpolation');
util.mergeOptions(this.options, options,'drawPoints');
util.mergeOptions(this.options, options,'shaded');
util.mergeOptions(this.options, options,'legend');
util.mergeOptions(this.options, options, 'interpolation');
util.mergeOptions(this.options, options, 'drawPoints');
util.mergeOptions(this.options, options, 'shaded');
util.mergeOptions(this.options, options, 'legend');
if (options.interpolation) {
if (typeof options.interpolation == 'object') {
@ -245,7 +250,7 @@ LineGraph.prototype.setOptions = function(options) {
/**
* Hide the component from the DOM
*/
LineGraph.prototype.hide = function() {
LineGraph.prototype.hide = function () {
// remove the frame containing the items
if (this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
@ -257,7 +262,7 @@ LineGraph.prototype.hide = function() {
* Show the component in the DOM (when not already visible).
* @return {Boolean} changed
*/
LineGraph.prototype.show = function() {
LineGraph.prototype.show = function () {
// show frame containing the items
if (!this.dom.frame.parentNode) {
this.body.dom.center.appendChild(this.dom.frame);
@ -269,7 +274,7 @@ LineGraph.prototype.show = function() {
* Set items
* @param {vis.DataSet | null} items
*/
LineGraph.prototype.setItems = function(items) {
LineGraph.prototype.setItems = function (items) {
var me = this,
ids,
oldItemsData = this.itemsData;
@ -315,7 +320,7 @@ LineGraph.prototype.setItems = function(items) {
* Set groups
* @param {vis.DataSet} groups
*/
LineGraph.prototype.setGroups = function(groups) {
LineGraph.prototype.setGroups = function (groups) {
var me = this;
var ids;
@ -362,20 +367,23 @@ LineGraph.prototype.setGroups = function(groups) {
* @param [ids]
* @private
*/
LineGraph.prototype._onUpdate = function(ids) {
LineGraph.prototype._onUpdate = function (ids) {
this._updateAllGroupData();
this.redraw(true);
};
LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
LineGraph.prototype._onUpdateGroups = function (groupIds) {
for (var i = 0; i < groupIds.length; i++) {
var group = this.groupsData.get(groupIds[i]);
this._updateGroup(group, groupIds[i]);
}
LineGraph.prototype._onAdd = function (ids) {
this._onUpdate(ids);
};
LineGraph.prototype._onRemove = function (ids) {
this._onUpdate(ids);
};
LineGraph.prototype._onUpdateGroups = function (groupIds) {
this._updateAllGroupData();
this.redraw(true);
};
LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
LineGraph.prototype._onAddGroups = function (groupIds) {
this._onUpdateGroups(groupIds);
};
/**
@ -446,19 +454,38 @@ LineGraph.prototype._updateGroup = function (group, groupId) {
LineGraph.prototype._updateAllGroupData = function () {
if (this.itemsData != null) {
var groupsContent = {};
this.itemsData.get().forEach(function(item){
var items = this.itemsData.get();
//pre-Determine array sizes, for more efficient memory claim
var groupCounts = {};
for (var i = 0; i < items.length; i++) {
var item = items[i];
var groupId = item.group;
if (groupId === null || groupId === undefined) {
groupId = UNGROUPED;
}
groupCounts.hasOwnProperty(groupId) ? groupCounts[groupId]++ : groupCounts[groupId] = 1;
}
//Now insert data into the arrays.
for (var i = 0; i < items.length; i++) {
var item = items[i];
var groupId = item.group;
if (groupId === null || groupId === undefined) {
groupId = UNGROUPED;
}
if (groupsContent[groupId] === undefined) {
groupsContent[groupId] = [];
if (!groupsContent.hasOwnProperty(groupId)) {
groupsContent[groupId] = new Array(groupCounts[groupId]);
}
var extended = Object.create(item);
//Copy data (because of unmodifiable DataView input.
var extended = util.bridgeObject(item);
extended.x = util.convert(item.x, 'Date');
groupsContent[groupId].push(extended);
});
//Update legendas and axis
extended.orginalY = item.y; //real Y
// typecast all items to numbers. Takes around 10ms for 500.000 items
extended.y = Number(item.y);
var index= groupsContent[groupId].length - groupCounts[groupId]--;
groupsContent[groupId][index] = extended;
}
//Update legendas, style and axis
for (var groupId in groupsContent) {
if (groupsContent.hasOwnProperty(groupId)) {
if (groupsContent[groupId].length == 0) {
@ -466,13 +493,14 @@ LineGraph.prototype._updateAllGroupData = function () {
this._onRemoveGroups([groupId]);
}
} else {
if (!this.groups.hasOwnProperty(groupId)) {
var group = {id: groupId, content: this.options.defaultGroup};
if (this.groupsData && this.groupsData.hasOwnProperty(groupId)) {
group = this.groupsData[groupId];
}
this._updateGroup(group, groupId);
var group = undefined;
if (this.groupsData != undefined) {
group = this.groupsData.get(groupId);
}
if (group == undefined) {
group = {id: groupId, content: this.options.defaultGroup + groupId};
}
this._updateGroup(group, groupId);
this.groups[groupId].setItems(groupsContent[groupId]);
}
}
@ -484,14 +512,14 @@ LineGraph.prototype._updateAllGroupData = function () {
* Redraw the component, mandatory function
* @return {boolean} Returns true if the component is resized
*/
LineGraph.prototype.redraw = function(forceGraphUpdate) {
LineGraph.prototype.redraw = function (forceGraphUpdate) {
var resized = false;
// calculate actual size and position
this.props.width = this.dom.frame.offsetWidth;
this.props.height = this.body.domProps.centerContainer.height
- this.body.domProps.border.top
- this.body.domProps.border.bottom;
- this.body.domProps.border.top
- this.body.domProps.border.bottom;
// update the graph if there is no lastWidth or with, used for the initial draw
if (this.lastWidth === undefined && this.props.width) {
@ -510,7 +538,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
// the svg element is three times as big as the width, this allows for fully dragging left and right
// without reloading the graph. the controls for this are bound to events in the constructor
if (resized == true) {
this.svg.style.width = util.option.asSize(3*this.props.width);
this.svg.style.width = util.option.asSize(3 * this.props.width);
this.svg.style.left = util.option.asSize(-this.props.width);
// if the height of the graph is set as proportional, change the height of the svg
@ -528,7 +556,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
this.updateSVGheight = false;
}
else {
this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px';
}
// zoomed is here to ensure that animations are shown correctly.
@ -541,7 +569,7 @@ LineGraph.prototype.redraw = function(forceGraphUpdate) {
var offset = this.body.range.start - this.lastStart;
var range = this.body.range.end - this.body.range.start;
if (this.props.width != 0) {
var rangePerPixelInv = this.props.width/range;
var rangePerPixelInv = this.props.width / range;
var xOffset = offset * rangePerPixelInv;
this.svg.style.left = (-this.props.width - xOffset) + 'px';
}
@ -563,10 +591,11 @@ LineGraph.prototype._updateGraph = function () {
DOMutil.prepareElements(this.svgElements);
if (this.props.width != 0 && this.itemsData != null) {
var group, i;
var preprocessedGroupData = {};
var processedGroupData = {};
var groupRanges = {};
var changeCalled = false;
// this is the range of the SVG canvas
var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width);
var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
// getting group Ids
var groupIds = [];
@ -579,10 +608,8 @@ LineGraph.prototype._updateGraph = function () {
}
}
if (groupIds.length > 0) {
// this is the range of the SVG canvas
var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width);
var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
var groupsData = {};
// fill groups data, this only loads the data we require based on the timewindow
this._getRelevantData(groupIds, groupsData, minDate, maxDate);
@ -591,11 +618,11 @@ LineGraph.prototype._updateGraph = function () {
// we transform the X coordinates to detect collisions
for (i = 0; i < groupIds.length; i++) {
preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
this._convertXcoordinates(groupsData[groupIds[i]]);
}
// now all needed data has been collected we start the processing.
this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
this._getYRanges(groupIds, groupsData, groupRanges);
// 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.
@ -618,17 +645,64 @@ LineGraph.prototype._updateGraph = function () {
// 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[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
if (this.options.stack === true && this.options.style === 'line' && i > 0) {
this._stack(groupsData[groupIds[i]], groupsData[groupIds[i - 1]]);
}
this._convertYcoordinates(groupsData[groupIds[i]], group);
}
// draw the groups
BarFunctions.draw(groupIds, processedGroupData, this.framework);
//Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines.
var paths = {};
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (group.options.style != 'bar') { // bar needs to be drawn enmasse
group.draw(processedGroupData[groupIds[i]], group, this.framework);
if (group.options.style === 'line' && group.options.shaded.enabled == true) {
var dataset = groupsData[groupIds[i]];
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(dataset, group);
}
if (group.options.shaded.orientation === "group") {
var subGroupId = group.options.shaded.groupId;
if (groupIds.indexOf(subGroupId) === -1) {
console.log("Unknown shading group target given:" + subGroupId);
continue;
}
if (!paths.hasOwnProperty(subGroupId)) {
paths[subGroupId] = Lines.calcPath(groupsData[subGroupId], this.groups[subGroupId]);
}
Lines.drawShading(paths[groupIds[i]], group, paths[subGroupId], this.framework);
}
else {
Lines.drawShading(paths[groupIds[i]], group, undefined, this.framework);
}
}
}
// draw the groups, calculating paths if still necessary.
Bars.draw(groupIds, groupsData, this.framework);
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (groupsData[groupIds[i]].length > 0) {
switch (group.options.style) {
case "line":
if (!paths.hasOwnProperty(groupIds[i])) {
paths[groupIds[i]] = Lines.calcPath(groupsData[groupIds[i]], group);
}
Lines.draw(paths[groupIds[i]], group, this.framework);
//explicit no break;
case "points":
if (group.options.style == "points" || group.options.drawPoints.enabled == true) {
Points.draw(groupsData[groupIds[i]], group, this.framework);
}
break;
case "bar":
// bar needs to be drawn enmasse
//explicit no break
default:
//do nothing...
}
}
}
}
}
}
@ -638,6 +712,51 @@ LineGraph.prototype._updateGraph = function () {
return false;
};
LineGraph.prototype._stack = function (data, subData) {
var index, dx, dy, subPrevPoint, subNextPoint;
index = 0;
// for each data point we look for a matching on in the set below
for (var j = 0; j < data.length; j++) {
subPrevPoint = undefined;
subNextPoint = undefined;
// we look for time matches or a before-after point
for (var k = index; k < subData.length; k++) {
// if times match exactly
if (subData[k].x === data[j].x) {
subPrevPoint = subData[k];
subNextPoint = subData[k];
index = k;
break;
}
else if (subData[k].x > data[j].x) { // overshoot
subNextPoint = subData[k];
if (k == 0) {
subPrevPoint = subNextPoint;
}
else {
subPrevPoint = subData[k - 1];
}
index = k;
break;
}
}
// in case the last data point has been used, we assume it stays like this.
if (subNextPoint === undefined) {
subPrevPoint = subData[subData.length - 1];
subNextPoint = subData[subData.length - 1];
}
// linear interpolation
dx = subNextPoint.x - subPrevPoint.x;
dy = subNextPoint.y - subPrevPoint.y;
if (dx == 0) {
data[j].y = data[j].orginalY + subNextPoint.y;
}
else {
data[j].y = data[j].orginalY + (dy / dx) * (data[j].x - subPrevPoint.x) + subPrevPoint.y; // ax + b where b is data[j].y
}
}
}
/**
* first select and preprocess the data from the datasets.
@ -657,33 +776,24 @@ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate,
if (groupIds.length > 0) {
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
groupsData[groupIds[i]] = [];
var dataContainer = groupsData[groupIds[i]];
var itemsData = group.getItems();
// optimization for sorted data
if (group.options.sort == true) {
var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before'));
for (j = guess; j < group.itemsData.length; j++) {
var first = Math.max(0, util.binarySearchValue(itemsData, minDate, 'x', 'before'));
var last = Math.min(itemsData.length, util.binarySearchValue(itemsData, maxDate, 'x', 'after'));
if (last < 0) {
last = itemsData.length;
}
var dataContainer = new Array(last-first);
for (j = first; j < last; j++) {
item = group.itemsData[j];
if (item !== undefined) {
if (item.x > maxDate) {
dataContainer.push(item);
break;
}
else {
dataContainer.push(item);
}
}
dataContainer[j-first] = item;
}
groupsData[groupIds[i]] = dataContainer;
}
else {
for (j = 0; j < group.itemsData.length; j++) {
item = group.itemsData[j];
if (item !== undefined) {
if (item.x > minDate && item.x < maxDate) {
dataContainer.push(item);
}
}
}
// If unsorted data, all data is relevant, just returning entire structure
groupsData[groupIds[i]] = group.itemsData;
}
}
}
@ -713,12 +823,12 @@ LineGraph.prototype._applySampling = function (groupIds, groupsData) {
var pointsPerPixel = amountOfPoints / xDistance;
increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
var sampledData = [];
var sampledData = new Array(amountOfPoints);
for (var j = 0; j < amountOfPoints; j += increment) {
sampledData.push(dataContainer[j]);
var idx = Math.round(j/increment);
sampledData[idx]=dataContainer[j];
}
groupsData[groupIds[i]] = sampledData;
groupsData[groupIds[i]] = sampledData.splice(0,Math.round(amountOfPoints/increment));
}
}
}
@ -747,22 +857,22 @@ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
group = this.groups[groupIds[i]];
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
if (options.stack === true && options.style === 'bar') {
if (options.yAxisOrientation === 'left') {combinedDataLeft = combinedDataLeft .concat(group.getData(groupData));}
else {combinedDataRight = combinedDataRight.concat(group.getData(groupData));}
if (options.yAxisOrientation === 'left') {
combinedDataLeft = combinedDataLeft.concat(group.getItems());
}
else {
combinedDataRight = combinedDataRight.concat(group.getItems());
}
}
else {
groupRanges[groupIds[i]] = group.getYRange(groupData,groupIds[i]);
groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]);
}
}
}
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
BarFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__barStackLeft' , 'left' );
BarFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right');
// if line graphs are stacked, their range need to be handled differently and accumulated over all groups.
//LineFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__lineStackLeft' , 'left' );
//LineFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__lineStackRight', 'right');
Bars.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left');
Bars.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right');
}
};
@ -823,7 +933,7 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
this.yAxisRight.setRange(minRight, maxRight);
}
}
resized = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || resized;
resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized;
resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized;
if (yAxisRightUsed == true && yAxisLeftUsed == true) {
@ -836,8 +946,12 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
}
this.yAxisRight.master = !yAxisLeftUsed;
if (this.yAxisRight.master == false) {
if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
else {this.yAxisLeft.lineOffset = 0;}
if (yAxisRightUsed == true) {
this.yAxisLeft.lineOffset = this.yAxisRight.width;
}
else {
this.yAxisLeft.lineOffset = 0;
}
resized = this.yAxisLeft.redraw() || resized;
this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels;
@ -850,9 +964,11 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
}
// clean the accumulated lists
var tempGroups = ['__barStackLeft','__barStackRight','__lineStackLeft','__lineStackRight'];
var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight'];
for (var i = 0; i < tempGroups.length; i++) {
if (groupIds.indexOf(tempGroups[i]) != -1) {groupIds.splice(groupIds.indexOf(tempGroups[i]),1);}
if (groupIds.indexOf(tempGroups[i]) != -1) {
groupIds.splice(groupIds.indexOf(tempGroups[i]), 1);
}
}
return resized;
@ -871,7 +987,7 @@ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
var changed = false;
if (axisUsed == false) {
if (axis.dom.frame.parentNode && axis.hidden == false) {
axis.hide()
axis.hide();
changed = true;
}
}
@ -895,17 +1011,11 @@ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
* @private
*/
LineGraph.prototype._convertXcoordinates = function (datapoints) {
var extractedData = [];
var xValue, yValue;
var toScreen = this.body.util.toScreen;
for (var i = 0; i < datapoints.length; i++) {
xValue = toScreen(datapoints[i].x) + this.props.width;
yValue = datapoints[i].y;
extractedData.push({x: xValue, y: yValue});
datapoints[i].screen_x = toScreen(datapoints[i].x) + this.props.width;
datapoints[i].screen_y = datapoints[i].y; //starting point for range calculations
}
return extractedData;
};
@ -920,25 +1030,15 @@ LineGraph.prototype._convertXcoordinates = function (datapoints) {
* @private
*/
LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
var extractedData = [];
var xValue, yValue;
var toScreen = this.body.util.toScreen;
var axis = this.yAxisLeft;
var svgHeight = Number(this.svg.style.height.replace('px',''));
var svgHeight = Number(this.svg.style.height.replace('px', ''));
if (group.options.yAxisOrientation == 'right') {
axis = this.yAxisRight;
}
for (var i = 0; i < datapoints.length; i++) {
var labelValue = datapoints[i].label ? datapoints[i].label : null;
xValue = toScreen(datapoints[i].x) + this.props.width;
yValue = Math.round(axis.convertValue(datapoints[i].y));
extractedData.push({x: xValue, y: yValue, label:labelValue});
datapoints[i].screen_y = Math.round(axis.convertValue(datapoints[i].y));
}
group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
return extractedData;
};

+ 31
- 58
lib/timeline/component/graph2d_types/bar.js View File

@ -2,35 +2,8 @@ var DOMutil = require('../../../DOMutil');
var Points = require('./points');
function Bargraph(groupId, options) {
this.groupId = groupId;
this.options = options;
}
Bargraph.prototype.getYRange = function(groupData) {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (var j = 0; j < groupData.length; j++) {
yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
Bargraph.prototype.getData = function(groupData) {
var combinedData = [];
for (var j = 0; j < groupData.length; j++) {
combinedData.push({
x: groupData[j].x,
y: groupData[j].y,
groupId: this.groupId
});
}
return combinedData;
}
/**
* draw a bar graph
*
@ -53,8 +26,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] === true)) {
for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
combinedData.push({
x: processedGroupData[groupIds[i]][j].x,
y: processedGroupData[groupIds[i]][j].y,
screen_x: processedGroupData[groupIds[i]][j].screen_x,
screen_y: processedGroupData[groupIds[i]][j].screen_y,
groupId: groupIds[i],
label: processedGroupData[groupIds[i]][j].label
});
@ -68,11 +41,11 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
// sort by time and by group
combinedData.sort(function (a, b) {
if (a.x === b.x) {
if (a.screen_x === b.screen_x) {
return a.groupId < b.groupId ? -1 : 1;
}
else {
return a.x - b.x;
return a.screen_x - b.screen_x;
}
});
@ -84,29 +57,29 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
group = framework.groups[combinedData[i].groupId];
var minWidth = 0.1 * group.options.barChart.width;
key = combinedData[i].x;
key = combinedData[i].screen_x;
var heightOffset = 0;
if (intersections[key] === undefined) {
if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].screen_x - key);}
if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].screen_x - key));}
drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
}
else {
var nextKey = i + (intersections[key].amount - intersections[key].resolved);
var prevKey = i - (intersections[key].resolved + 1);
if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].screen_x - key);}
if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].screen_x - key));}
drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
intersections[key].resolved += 1;
if (group.options.stack === true) {
if (combinedData[i].y < group.zeroPosition) {
if (combinedData[i].screen_y < group.zeroPosition) {
heightOffset = intersections[key].accumulatedNegative;
intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y;
intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].screen_y;
}
else {
heightOffset = intersections[key].accumulatedPositive;
intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y;
intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].screen_y;
}
}
else if (group.options.barChart.sideBySide === true) {
@ -116,14 +89,14 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
else if (group.options.barChart.align === 'right') {drawData.offset += 0.5*drawData.width;}
}
}
DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style);
DOMutil.drawBar(combinedData[i].screen_x + drawData.offset, combinedData[i].screen_y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].screen_y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style);
// draw points
if (group.options.drawPoints.enabled === true) {
let pointData = {
x:combinedData[i].x + drawData.offset,
y:combinedData[i].y - heightOffset,
screen_x:combinedData[i].screen_x,
screen_y:combinedData[i].screen_y - heightOffset,
groupId: combinedData[i].groupId,
label: combinedData[i].label,
label: combinedData[i].label
};
Points.draw([pointData], group, framework, drawData.offset);
//DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
@ -143,16 +116,16 @@ Bargraph._getDataIntersections = function (intersections, combinedData) {
var coreDistance;
for (var i = 0; i < combinedData.length; i++) {
if (i + 1 < combinedData.length) {
coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
coreDistance = Math.abs(combinedData[i + 1].screen_x - combinedData[i].screen_x);
}
if (i > 0) {
coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].screen_x - combinedData[i].screen_x));
}
if (coreDistance === 0) {
if (intersections[combinedData[i].x] === undefined) {
intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0};
if (intersections[combinedData[i].screen_x] === undefined) {
intersections[combinedData[i].screen_x] = {amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0};
}
intersections[combinedData[i].x].amount += 1;
intersections[combinedData[i].screen_x].amount += 1;
}
}
};
@ -199,11 +172,11 @@ Bargraph.getStackedYRange = function(combinedData, groupRanges, groupIds, groupL
if (combinedData.length > 0) {
// sort by time and by group
combinedData.sort(function (a, b) {
if (a.x === b.x) {
if (a.screen_x === b.screen_x) {
return a.groupId < b.groupId ? -1 : 1;
}
else {
return a.x - b.x;
return a.screen_x - b.screen_x;
}
});
var intersections = {};
@ -217,20 +190,20 @@ Bargraph.getStackedYRange = function(combinedData, groupRanges, groupIds, groupL
Bargraph._getStackedYRange = function (intersections, combinedData) {
var key;
var yMin = combinedData[0].y;
var yMax = combinedData[0].y;
var yMin = combinedData[0].screen_y;
var yMax = combinedData[0].screen_y;
for (var i = 0; i < combinedData.length; i++) {
key = combinedData[i].x;
key = combinedData[i].screen_x;
if (intersections[key] === undefined) {
yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
yMin = yMin > combinedData[i].screen_y ? combinedData[i].screen_y : yMin;
yMax = yMax < combinedData[i].screen_y ? combinedData[i].screen_y : yMax;
}
else {
if (combinedData[i].y < 0) {
intersections[key].accumulatedNegative += combinedData[i].y;
if (combinedData[i].screen_y < 0) {
intersections[key].accumulatedNegative += combinedData[i].screen_y;
}
else {
intersections[key].accumulatedPositive += combinedData[i].y;
intersections[key].accumulatedPositive += combinedData[i].screen_y;
}
}
}

+ 195
- 248
lib/timeline/component/graph2d_types/line.js View File

@ -1,167 +1,107 @@
var DOMutil = require('../../../DOMutil');
var Points = require('./points');
function Line(groupId, options) {
this.groupId = groupId;
this.options = options;
}
Line.prototype.getData = function(groupData) {
var combinedData = [];
for (var j = 0; j < groupData.length; j++) {
combinedData.push({
x: groupData[j].x,
y: groupData[j].y,
groupId: this.groupId
});
}
return combinedData;
}
Line.prototype.getYRange = function(groupData) {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (var j = 0; j < groupData.length; j++) {
yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
Line.getStackedYRange = function(combinedData, groupRanges, groupIds, groupLabel, orientation) {
if (combinedData.length > 0) {
// sort by time and by group
combinedData.sort(function (a, b) {
if (a.x === b.x) {
return a.groupId < b.groupId ? -1 : 1;
}
else {
return a.x - b.x;
}
});
var intersections = {};
Line._getDataIntersections(intersections, combinedData);
groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData);
groupRanges[groupLabel].yAxisOrientation = orientation;
groupIds.push(groupLabel);
}
}
Line._getStackedYRange = function (intersections, combinedData) {
var key;
var yMin = combinedData[0].y;
var yMax = combinedData[0].y;
for (var i = 0; i < combinedData.length; i++) {
key = combinedData[i].x;
if (intersections[key] === undefined) {
yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
}
else {
if (combinedData[i].y < 0) {
intersections[key].accumulatedNegative += combinedData[i].y;
}
else {
intersections[key].accumulatedPositive += combinedData[i].y;
}
}
}
for (var xpos in intersections) {
if (intersections.hasOwnProperty(xpos)) {
yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin;
yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin;
yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax;
yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax;
}
}
return {min: yMin, max: yMax};
};
/**
* Fill the intersections object with counters of how many datapoints share the same x coordinates
* @param intersections
* @param combinedData
* @private
*/
Line._getDataIntersections = function (intersections, combinedData) {
// get intersections
var coreDistance;
for (var i = 0; i < combinedData.length; i++) {
if (i + 1 < combinedData.length) {
coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
}
if (i > 0) {
coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
}
if (coreDistance === 0) {
if (intersections[combinedData[i].x] === undefined) {
intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0};
}
intersections[combinedData[i].x].amount += 1;
Line.calcPath = function (dataset, group) {
if (dataset != null) {
if (dataset.length > 0) {
var d = [];
// construct path from dataset
if (group.options.interpolation.enabled == true) {
d = Line._catmullRom(dataset, group);
}
else {
d = Line._linear(dataset);
}
return d;
}
}
}
};
/**
* draw a line graph
*
* @param dataset
* @param group
*/
Line.prototype.draw = function (dataset, group, framework) {
if (dataset != null) {
if (dataset.length > 0) {
var path, d;
var svgHeight = Number(framework.svg.style.height.replace('px',''));
path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
path.setAttributeNS(null, "class", group.className);
if(group.style !== undefined) {
path.setAttributeNS(null, "style", group.style);
}
// construct path from dataset
if (group.options.interpolation.enabled == true) {
d = Line._catmullRom(dataset, group);
}
else {
d = Line._linear(dataset);
}
}
// append with points for fill and finalize the path
if (group.options.shaded.enabled == true) {
Line.drawShading = function (pathArray, group, subPathArray, framework) {
// append shading to the path
if (group.options.shaded.enabled == true) {
var svgHeight = Number(framework.svg.style.height.replace('px',''));
var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
var type = "L";
if (group.options.interpolation.enabled == true){
type = "C";
}
var dFill;
var zero = 0;
if (group.options.shaded.orientation == 'top') {
zero = 0;
zero = 0;
}
else if (group.options.shaded.orientation == 'bottom') {
zero = svgHeight;
zero = svgHeight;
}
else {
zero = Math.min(Math.max(0,group.zeroPosition),svgHeight);
zero = Math.min(Math.max(0, group.zeroPosition), svgHeight);
}
var dFill = 'M' + dataset[0].x + ',' + zero + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + zero;
if (group.options.shaded.orientation == 'group' && (subPathArray != null && subPathArray != undefined)) {
dFill = 'M' + pathArray[0][0]+ ","+pathArray[0][1] + " " +
this.serializePath(pathArray,type,false) +
' L'+ subPathArray[subPathArray.length-1][0]+ "," + subPathArray[subPathArray.length-1][1] + " " +
this.serializePath(subPathArray,type,true) +
subPathArray[0][0]+ ","+subPathArray[0][1] + " Z";
}
else {
dFill = 'M' + pathArray[0][0]+ ","+pathArray[0][1] + " " +
this.serializePath(pathArray,type,false) +
' V' + zero + ' H'+ pathArray[0][0] + " Z";
}
fillPath.setAttributeNS(null, 'class', group.className + ' vis-fill');
if(group.options.shaded.style !== undefined) {
fillPath.setAttributeNS(null, 'style', group.options.shaded.style);
if (group.options.shaded.style !== undefined) {
fillPath.setAttributeNS(null, 'style', group.options.shaded.style);
}
fillPath.setAttributeNS(null, 'd', dFill);
}
// copy properties to path for drawing.
path.setAttributeNS(null, 'd', 'M' + d);
}
}
// draw points
if (group.options.drawPoints.enabled == true) {
Points.draw(dataset, group, framework);
}
/**
* draw a line graph
*
* @param dataset
* @param group
*/
Line.draw = function (pathArray, group, framework) {
if (pathArray != null && pathArray != undefined) {
var path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
path.setAttributeNS(null, "class", group.className);
if (group.style !== undefined) {
path.setAttributeNS(null, "style", group.style);
}
var type = "L";
if (group.options.interpolation.enabled == true){
type = "C";
}
// copy properties to path for drawing.
path.setAttributeNS(null, 'd', 'M' + pathArray[0][0]+ ","+pathArray[0][1] + " " + this.serializePath(pathArray,type,false));
}
}
};
Line.serializePath = function(pathArray,type,inverse){
if (pathArray.length <= 2){
//Too little data to create a path.
return "";
}
var d = type;
if (inverse){
for (var i = pathArray.length-2; i > 0; i--){
d += pathArray[i][0] + "," + pathArray[i][1] + " ";
}
}
else {
for (var i = 1; i < pathArray.length; i++){
d += pathArray[i][0] + "," + pathArray[i][1] + " ";
}
}
return d;
}
/**
* This uses an uniform parametrization of the interpolation algorithm:
@ -170,41 +110,44 @@ Line.prototype.draw = function (dataset, group, framework) {
* @returns {string}
* @private
*/
Line._catmullRomUniform = function(data) {
// catmull rom
var p0, p1, p2, p3, bp1, bp2;
var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
var normalization = 1/6;
var length = data.length;
for (var i = 0; i < length - 1; i++) {
p0 = (i == 0) ? data[0] : data[i-1];
p1 = data[i];
p2 = data[i+1];
p3 = (i + 2 < length) ? data[i+2] : p2;
// Catmull-Rom to Cubic Bezier conversion matrix
// 0 1 0 0
// -1/6 1 1/6 0
// 0 1/6 1 -1/6
// 0 0 1 0
// bp0 = { x: p1.x, y: p1.y };
bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
// bp0 = { x: p2.x, y: p2.y };
Line._catmullRomUniform = function (data) {
// catmull rom
var p0, p1, p2, p3, bp1, bp2;
var d = [];
d.push( [ Math.round(data[0].screen_x) , Math.round(data[0].screen_y) ]);
var normalization = 1 / 6;
var length = data.length;
for (var i = 0; i < length - 1; i++) {
d += 'C' +
bp1.x + ',' +
bp1.y + ' ' +
bp2.x + ',' +
bp2.y + ' ' +
p2.x + ',' +
p2.y + ' ';
}
p0 = (i == 0) ? data[0] : data[i - 1];
p1 = data[i];
p2 = data[i + 1];
p3 = (i + 2 < length) ? data[i + 2] : p2;
// Catmull-Rom to Cubic Bezier conversion matrix
// 0 1 0 0
// -1/6 1 1/6 0
// 0 1/6 1 -1/6
// 0 0 1 0
// bp0 = { x: p1.x, y: p1.y };
bp1 = {
screen_x: ((-p0.screen_x + 6 * p1.screen_x + p2.screen_x) * normalization),
screen_y: ((-p0.screen_y + 6 * p1.screen_y + p2.screen_y) * normalization)
};
bp2 = {
screen_x: (( p1.screen_x + 6 * p2.screen_x - p3.screen_x) * normalization),
screen_y: (( p1.screen_y + 6 * p2.screen_y - p3.screen_y) * normalization)
};
// bp0 = { x: p2.x, y: p2.y };
d.push( [ bp1.screen_x , bp1.screen_y ]);
d.push( [ bp2.screen_x , bp2.screen_y ]);
d.push( [ p2.screen_x , p2.screen_y ]);
}
return d;
return d;
};
/**
@ -218,70 +161,79 @@ Line._catmullRomUniform = function(data) {
* @returns {string}
* @private
*/
Line._catmullRom = function(data, group) {
var alpha = group.options.interpolation.alpha;
if (alpha == 0 || alpha === undefined) {
return this._catmullRomUniform(data);
}
else {
var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
var length = data.length;
for (var i = 0; i < length - 1; i++) {
p0 = (i == 0) ? data[0] : data[i-1];
p1 = data[i];
p2 = data[i+1];
p3 = (i + 2 < length) ? data[i+2] : p2;
d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
// Catmull-Rom to Cubic Bezier conversion matrix
// A = 2d1^2a + 3d1^a * d2^a + d3^2a
// B = 2d3^2a + 3d3^a * d2^a + d2^2a
// [ 0 1 0 0 ]
// [ -d2^2a /N A/N d1^2a /N 0 ]
// [ 0 d3^2a /M B/M -d2^2a /M ]
// [ 0 0 1 0 ]
d3powA = Math.pow(d3, alpha);
d3pow2A = Math.pow(d3,2*alpha);
d2powA = Math.pow(d2, alpha);
d2pow2A = Math.pow(d2,2*alpha);
d1powA = Math.pow(d1, alpha);
d1pow2A = Math.pow(d1,2*alpha);
A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
N = 3*d1powA * (d1powA + d2powA);
if (N > 0) {N = 1 / N;}
M = 3*d3powA * (d3powA + d2powA);
if (M > 0) {M = 1 / M;}
bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
d += 'C' +
bp1.x + ',' +
bp1.y + ' ' +
bp2.x + ',' +
bp2.y + ' ' +
p2.x + ',' +
p2.y + ' ';
Line._catmullRom = function (data, group) {
var alpha = group.options.interpolation.alpha;
if (alpha == 0 || alpha === undefined) {
return this._catmullRomUniform(data);
}
else {
var p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M;
var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
var d = [];
d.push( [ Math.round(data[0].screen_x) , Math.round(data[0].screen_y) ]);
var length = data.length;
for (var i = 0; i < length - 1; i++) {
p0 = (i == 0) ? data[0] : data[i - 1];
p1 = data[i];
p2 = data[i + 1];
p3 = (i + 2 < length) ? data[i + 2] : p2;
d1 = Math.sqrt(Math.pow(p0.screen_x - p1.screen_x, 2) + Math.pow(p0.screen_y - p1.screen_y, 2));
d2 = Math.sqrt(Math.pow(p1.screen_x - p2.screen_x, 2) + Math.pow(p1.screen_y - p2.screen_y, 2));
d3 = Math.sqrt(Math.pow(p2.screen_x - p3.screen_x, 2) + Math.pow(p2.screen_y - p3.screen_y, 2));
// Catmull-Rom to Cubic Bezier conversion matrix
// A = 2d1^2a + 3d1^a * d2^a + d3^2a
// B = 2d3^2a + 3d3^a * d2^a + d2^2a
// [ 0 1 0 0 ]
// [ -d2^2a /N A/N d1^2a /N 0 ]
// [ 0 d3^2a /M B/M -d2^2a /M ]
// [ 0 0 1 0 ]
d3powA = Math.pow(d3, alpha);
d3pow2A = Math.pow(d3, 2 * alpha);
d2powA = Math.pow(d2, alpha);
d2pow2A = Math.pow(d2, 2 * alpha);
d1powA = Math.pow(d1, alpha);
d1pow2A = Math.pow(d1, 2 * alpha);
A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A;
B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A;
N = 3 * d1powA * (d1powA + d2powA);
if (N > 0) {
N = 1 / N;
}
M = 3 * d3powA * (d3powA + d2powA);
if (M > 0) {
M = 1 / M;
}
bp1 = {
screen_x: ((-d2pow2A * p0.screen_x + A * p1.screen_x + d1pow2A * p2.screen_x) * N),
screen_y: ((-d2pow2A * p0.screen_y + A * p1.screen_y + d1pow2A * p2.screen_y) * N)
};
bp2 = {
screen_x: (( d3pow2A * p1.screen_x + B * p2.screen_x - d2pow2A * p3.screen_x) * M),
screen_y: (( d3pow2A * p1.screen_y + B * p2.screen_y - d2pow2A * p3.screen_y) * M)
};
if (bp1.screen_x == 0 && bp1.screen_y == 0) {
bp1 = p1;
}
if (bp2.screen_x == 0 && bp2.screen_y == 0) {
bp2 = p2;
}
d.push( [ bp1.screen_x , bp1.screen_y ]);
d.push( [ bp2.screen_x , bp2.screen_y ]);
d.push( [ p2.screen_x , p2.screen_y ]);
}
return d;
}
return d;
}
};
/**
@ -290,18 +242,13 @@ Line._catmullRom = function(data, group) {
* @returns {string}
* @private
*/
Line._linear = function(data) {
// linear
var d = '';
for (var i = 0; i < data.length; i++) {
if (i == 0) {
d += data[i].x + ',' + data[i].y;
Line._linear = function (data) {
// linear
var d = [];
for (var i = 0; i < data.length; i++) {
d.push([ data[i].screen_x , data[i].screen_y ]);
}
else {
d += ' ' + data[i].x + ',' + data[i].y;
}
}
return d;
return d;
};
module.exports = Line;

+ 2
- 19
lib/timeline/component/graph2d_types/points.js View File

@ -1,25 +1,8 @@
var DOMutil = require('../../../DOMutil');
function Points(groupId, options) {
this.groupId = groupId;
this.options = options;
}
Points.prototype.getYRange = function(groupData) {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (var j = 0; j < groupData.length; j++) {
yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
return {min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation};
};
Points.prototype.draw = function(dataset, group, framework, offset) {
Points.draw(dataset, group, framework, offset);
};
/**
* draw the data points
*
@ -36,12 +19,12 @@ Points.draw = function (dataset, group, framework, offset) {
for (var i = 0; i < dataset.length; i++) {
if (!callback) {
// draw the point the simple way.
DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label);
DOMutil.drawPoint(dataset[i].screen_x + offset, dataset[i].screen_y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label);
}
else {
var callbackResult = callback(dataset[i], group, framework); // result might be true, false or an object
if (callbackResult === true || typeof callbackResult === 'object') {
DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label);
DOMutil.drawPoint(dataset[i].screen_x + offset, dataset[i].screen_y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label);
}
}
}

+ 3
- 2
lib/timeline/optionsGraph2d.js View File

@ -33,7 +33,8 @@ let allOptions = {
graphHeight: {string, number},
shaded: {
enabled: {boolean},
orientation: {string:['bottom','top','zero']}, // top, bottom, zero
orientation: {string:['bottom','top','zero','group']}, // top, bottom, zero, group
groupId: {object},
__type__: {boolean,object}
},
style: {string:['line','bar','points']}, // line, bar
@ -172,7 +173,7 @@ let configureOptions = {
stack:false,
shaded: {
enabled: false,
orientation: ['zero','top','bottom'] // zero, top, bottom
orientation: ['zero','top','bottom','group'] // zero, top, bottom
},
style: ['line','bar','points'], // line, bar
barChart: {

Loading…
Cancel
Save