Browse Source

modularized graph2d draw styles, added point style.

v3_develop
Alex de Mulder 9 years ago
parent
commit
6a93703749
15 changed files with 24466 additions and 24051 deletions
  1. +7
    -0
      HISTORY.md
  2. +23760
    -23580
      dist/vis.js
  3. +1
    -1
      docs/graph2d.html
  4. +1
    -1
      examples/graph2d/03_groups.html
  5. +1
    -1
      examples/graph2d/05_bothAxis.html
  6. +1
    -0
      examples/graph2d/07_scrollingAndSorting.html
  7. +1
    -1
      examples/graph2d/09_external_legend.html
  8. +1
    -1
      examples/graph2d/14_toggleGroups.html
  9. +1
    -1
      examples/graph2d/16_bothAxis_titles.html
  10. +63
    -0
      examples/graph2d/18_scatterplot.html
  11. +63
    -4
      lib/timeline/component/GraphGroup.js
  12. +76
    -461
      lib/timeline/component/LineGraph.js
  13. +229
    -0
      lib/timeline/component/graph2d_types/bar.js
  14. +218
    -0
      lib/timeline/component/graph2d_types/line.js
  15. +43
    -0
      lib/timeline/component/graph2d_types/points.js

+ 7
- 0
HISTORY.md View File

@ -1,6 +1,13 @@
# vis.js history
http://visjs.org
## 2014-11-07, version 3.6.5 NOT YET RELEASED
### Graph2D
- Added points style for scatterplots and pointclouds.
- Modularized the Graph2D draw styles.
## 2014-11-07, version 3.6.4

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


+ 1
- 1
docs/graph2d.html View File

@ -349,7 +349,7 @@ The options colored in green can also be used as options for the groups. All opt
<td class="greenField">style</td>
<td>String</td>
<td>'line'</td>
<td>This allows the user to define if this should be a linegraph or a barchart. The options are: 'line' or 'bar'.</td>
<td>This allows the user to define if this should be a linegraph, barchart or pointcloud. The options are: 'line', 'bar', 'points'.</td>
</tr>
<tr>
<td class="greenField">barChart.width</td>

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

@ -32,7 +32,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['SquareShaded', 'Bar', 'Blank', 'CircleShaded'];
var names = ['SquareShaded', 'Bargraph', 'Blank', 'CircleShaded'];
var groups = new vis.DataSet();
groups.add({
id: 0,

+ 1
- 1
examples/graph2d/05_bothAxis.html View File

@ -52,7 +52,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['SquareShaded', 'Bar', 'Blank', 'CircleShaded'];
var names = ['SquareShaded', 'Bargraph', 'Blank', 'CircleShaded'];
var groups = new vis.DataSet();
groups.add({
id: 0,

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

@ -61,6 +61,7 @@
var options = {
legend: true,
sort: false,
style:'points',
defaultGroup: 'doodle',
graphHeight: '1500px',
height: '500px',

+ 1
- 1
examples/graph2d/09_external_legend.html View File

@ -209,7 +209,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['SquareShaded', 'Bar', 'Blank', 'CircleShaded'];
var names = ['SquareShaded', 'Bargraph', 'Blank', 'CircleShaded'];
var groups = new vis.DataSet();
groups.add({
id: 0,

+ 1
- 1
examples/graph2d/14_toggleGroups.html View File

@ -52,7 +52,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['SquareShaded', 'Bar', 'Blank', 'CircleShaded'];
var names = ['SquareShaded', 'Bargraph', 'Blank', 'CircleShaded'];
var groups = new vis.DataSet();
groups.add({
id: 0,

+ 1
- 1
examples/graph2d/16_bothAxis_titles.html View File

@ -83,7 +83,7 @@
<script type="text/javascript">
// create a dataSet with groups
var names = ['SquareShaded', 'Bar', 'Blank', 'CircleShaded'];
var names = ['SquareShaded', 'Bargraph', 'Blank', 'CircleShaded'];
var groups = new vis.DataSet();
groups.add({
id: 0,

+ 63
- 0
examples/graph2d/18_scatterplot.html View File

@ -0,0 +1,63 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Graph2d | Scatterplot</title>
<style type="text/css">
body, html {
font-family: sans-serif;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h2>Graph2d | Scatterplot</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
You can manually disable the automatic sorting of the datapoints by using the <code>sort</code> option. You can use this with the
<code>style: 'points'</code> option for making a scatterplot!
</div>
<pre class="prettyprint lang-js">
var options = {
sort: false,
sampling:false,
style:'points'
};
</pre>
<br />
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [];
for (var i = 0; i < 100; i++) {
items.push({x: new Date('2014-06-11').valueOf() + Math.floor(Math.random() * 30000), y: 500 + (Math.random() * 100)});
}
var dataset = new vis.DataSet(items);
var options = {
sort: false,
sampling:false,
style:'points',
dataAxis: {
customRange: {
left: {
min: 300, max: 800
}
}
},
drawPoints: {
enabled: true,
size: 6,
style: 'circle' // square, circle
},
defaultGroup: 'Scatterplot',
height: '600px'
};
var graph2d = new vis.Graph2d(container, dataset, options);
</script>
</body>
</html>

+ 63
- 4
lib/timeline/component/GraphGroup.js View File

@ -1,11 +1,18 @@
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');
/**
* @constructor Group
* @param {Number | String} groupId
* @param {Object} data
* @param {ItemSet} itemSet
* /**
* @param {object} group | the object of the group from the dataset
* @param {string} groupId | ID of the group
* @param {object} options | the default options
* @param {array} groupsUsingDefaultStyles | this array has one entree.
* It is passed as an array so it is passed by reference.
* It enumerates through the default styles
* @constructor
*/
function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
this.id = groupId;
@ -22,6 +29,11 @@ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
this.visible = group.visible === undefined ? true : group.visible;
}
/**
* this loads a reference to all items in this group into this group.
* @param {array} items
*/
GraphGroup.prototype.setItems = function(items) {
if (items != null) {
this.itemsData = items;
@ -34,10 +46,20 @@ GraphGroup.prototype.setItems = function(items) {
}
};
/**
* this is used for plotting barcharts, this way, we only have to calculate it once.
* @param 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) {
if (options !== undefined) {
var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
@ -64,8 +86,23 @@ 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);
}
};
/**
* this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph
* @param group
*/
GraphGroup.prototype.update = function(group) {
this.group = group;
this.content = group.content || 'graph';
@ -75,6 +112,17 @@ GraphGroup.prototype.update = function(group) {
this.setOptions(group.options);
};
/**
* draw the icon for the legend.
*
* @param x
* @param y
* @param JSONcontainer
* @param SVGcontainer
* @param iconWidth
* @param iconHeight
*/
GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
var fillHeight = iconHeight * 0.5;
var path, fillPath;
@ -125,7 +173,9 @@ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, icon
}
};
/**
* return the legend entree for this group.
*
* @param iconWidth
* @param iconHeight
@ -137,4 +187,13 @@ GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
}
GraphGroup.prototype.getYRange = function(groupData) {
return this.type.getYRange(groupData);
}
GraphGroup.prototype.draw = function(dataset, group, framework) {
this.type.draw(dataset, group, framework);
}
module.exports = GraphGroup;

+ 76
- 461
lib/timeline/component/LineGraph.js View File

@ -6,6 +6,7 @@ var Component = require('./Component');
var DataAxis = require('./DataAxis');
var GraphGroup = require('./GraphGroup');
var Legend = require('./Legend');
var BarGraphFunctions = require('./graph2d_types/bar');
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
@ -56,7 +57,7 @@ function LineGraph(body, options) {
left: {min:undefined, max:undefined},
right: {min:undefined, max:undefined}
}
//,
//, these options are not set by default, but this shows the format they will be in
//format: {
// left: {decimals: 2},
// right: {decimals: 2}
@ -145,7 +146,9 @@ function LineGraph(body, options) {
// create the HTML DOM
this._create();
this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
this.body.emitter.emit('change');
}
LineGraph.prototype = new Component();
@ -182,7 +185,7 @@ LineGraph.prototype._create = function(){
/**
* set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
* @param options
* @param {object} options
*/
LineGraph.prototype.setOptions = function(options) {
if (options) {
@ -251,6 +254,7 @@ LineGraph.prototype.hide = function() {
}
};
/**
* Show the component in the DOM (when not already visible).
* @return {Boolean} changed
@ -310,6 +314,7 @@ LineGraph.prototype.setItems = function(items) {
this.redraw();
};
/**
* Set groups
* @param {vis.DataSet} groups
@ -357,7 +362,7 @@ LineGraph.prototype.setGroups = function(groups) {
/**
* Update the datapoints
* Update the data
* @param [ids]
* @private
*/
@ -380,6 +385,12 @@ LineGraph.prototype._onUpdateGroups = function (groupIds) {
};
LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
/**
* this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph
* @param {Array} groupIds
* @private
*/
LineGraph.prototype._onRemoveGroups = function (groupIds) {
for (var i = 0; i < groupIds.length; i++) {
if (this.groups.hasOwnProperty(groupIds[i])) {
@ -401,8 +412,9 @@ LineGraph.prototype._onRemoveGroups = function (groupIds) {
this.redraw();
};
/**
* update a group object
* update a group object with the group dataset entree
*
* @param group
* @param groupId
@ -435,6 +447,12 @@ LineGraph.prototype._updateGroup = function (group, groupId) {
this.legendRight.redraw();
};
/**
* this updates all groups, it is used when there is an update the the itemset.
*
* @private
*/
LineGraph.prototype._updateAllGroupData = function () {
if (this.itemsData != null) {
var groupsContent = {};
@ -462,6 +480,7 @@ LineGraph.prototype._updateAllGroupData = function () {
}
};
/**
* Create or delete the group holding all ungrouped items. This group is used when
* there are no groups specified. This anonymous group is called 'graph'.
@ -564,6 +583,7 @@ LineGraph.prototype.redraw = function() {
return resized;
};
/**
* Update and redraw the graph.
*
@ -601,12 +621,17 @@ LineGraph.prototype._updateGraph = function () {
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
// fill groups data, this only loads the data we require based on the timewindow
this._getRelevantData(groupIds, groupsData, minDate, maxDate);
// apply sampling, if disabled, it will pass through this function.
this._applySampling(groupIds, groupsData);
// we transform the X coordinates to detect collisions
for (i = 0; i < groupIds.length; i++) {
preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
}
// now all needed data has been collected we start the processing.
this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
@ -631,11 +656,11 @@ LineGraph.prototype._updateGraph = function () {
// draw the groups
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (group.options.style == 'line') {
this._drawLineGraph(processedGroupData[groupIds[i]], group);
if (group.options.style != 'bar') { // bar needs to be drawn enmasse
group.draw(processedGroupData[groupIds[i]], group, this.framework);
}
}
this._drawBarGraphs(groupIds, processedGroupData);
BarGraphFunctions.draw(groupIds, processedGroupData, this.framework);
}
}
@ -644,12 +669,20 @@ LineGraph.prototype._updateGraph = function () {
};
/**
* first select and preprocess the data from the datasets.
* the groups have their preselection of data, we now loop over this data to see
* what data we need to draw. Sorted data is much faster.
* more optimization is possible by doing the sampling before and using the binary search
* to find the end date to determine the increment.
*
* @param {array} groupIds
* @param {object} groupsData
* @param {date} minDate
* @param {date} maxDate
* @private
*/
LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
// 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.
var group, i, j, item;
if (groupIds.length > 0) {
for (i = 0; i < groupIds.length; i++) {
@ -684,10 +717,15 @@ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate,
}
}
}
this._applySampling(groupIds, groupsData);
};
/**
*
* @param groupIds
* @param groupsData
* @private
*/
LineGraph.prototype._applySampling = function (groupIds, groupsData) {
var group;
if (groupIds.length > 0) {
@ -717,105 +755,43 @@ LineGraph.prototype._applySampling = function (groupIds, groupsData) {
}
};
/**
*
*
* @param {array} groupIds
* @param {object} groupsData
* @param {object} groupRanges | this is being filled here
* @private
*/
LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
var groupData, group, i,j;
var groupData, group, i;
var barCombinedDataLeft = [];
var barCombinedDataRight = [];
var barCombinedData;
var options;
if (groupIds.length > 0) {
for (i = 0; i < groupIds.length; i++) {
groupData = groupsData[groupIds[i]];
options = this.groups[groupIds[i]].options;
if (groupData.length > 0) {
group = this.groups[groupIds[i]];
if (group.options.style == 'line' || group.options.barChart.handleOverlap != 'stack') {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (j = 0; j < groupData.length; j++) {
yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
}
groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
if (options.barChart.handleOverlap == 'stack' && options.style == 'bar') {
if (options.yAxisOrientation == 'left') {barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)) ;}
else {barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData));}
}
else if (group.options.style == 'bar') {
if (group.options.yAxisOrientation == 'left') {
barCombinedData = barCombinedDataLeft;
}
else {
barCombinedData = barCombinedDataRight;
}
groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
// combine data
for (j = 0; j < groupData.length; j++) {
barCombinedData.push({
x: groupData[j].x,
y: groupData[j].y,
groupId: groupIds[i]
});
}
else {
groupRanges[groupIds[i]] = group.getYRange(groupData,groupIds[i]);
}
}
}
var intersections;
if (barCombinedDataLeft.length > 0) {
// sort by time and by group
barCombinedDataLeft.sort(function (a, b) {
if (a.x == b.x) {
return a.groupId - b.groupId;
} else {
return a.x - b.x;
}
});
intersections = {};
this._getDataIntersections(intersections, barCombinedDataLeft);
groupRanges['__barchartLeft'] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
groupRanges['__barchartLeft'].yAxisOrientation = 'left';
groupIds.push('__barchartLeft');
}
if (barCombinedDataRight.length > 0) {
// sort by time and by group
barCombinedDataRight.sort(function (a, b) {
if (a.x == b.x) {
return a.groupId - b.groupId;
} else {
return a.x - b.x;
}
});
intersections = {};
this._getDataIntersections(intersections, barCombinedDataRight);
groupRanges['__barchartRight'] = this._getStackedBarYRange(intersections, barCombinedDataRight);
groupRanges['__barchartRight'].yAxisOrientation = 'right';
groupIds.push('__barchartRight');
}
// if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft , groupRanges, groupIds, '__barchartLeft' , 'left' );
BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, '__barchartRight', 'right');
}
};
LineGraph.prototype._getStackedBarYRange = 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 {
intersections[key].accumulated += combinedData[i].y;
}
}
for (var xpos in intersections) {
if (intersections.hasOwnProperty(xpos)) {
yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
}
}
return {min: yMin, max: yMax};
};
/**
* this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
@ -895,6 +871,7 @@ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
return changeCalled;
};
/**
* This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
*
@ -921,224 +898,6 @@ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
};
/**
* draw a bar graph
*
* @param groupIds
* @param processedGroupData
*/
LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
var combinedData = [];
var intersections = {};
var coreDistance;
var key, drawData;
var group;
var i,j;
var barPoints = 0;
// combine all barchart data
for (i = 0; i < groupIds.length; i++) {
group = this.groups[groupIds[i]];
if (group.options.style == 'bar') {
if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.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,
groupId: groupIds[i]
});
barPoints += 1;
}
}
}
}
if (barPoints == 0) {return;}
// sort by time and by group
combinedData.sort(function (a, b) {
if (a.x == b.x) {
return a.groupId - b.groupId;
} else {
return a.x - b.x;
}
});
// get intersections
this._getDataIntersections(intersections, combinedData);
// plot barchart
for (i = 0; i < combinedData.length; i++) {
group = this.groups[combinedData[i].groupId];
var minWidth = 0.1 * group.options.barChart.width;
key = combinedData[i].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));}
drawData = this._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));}
drawData = this._getSafeDrawData(coreDistance, group, minWidth);
intersections[key].resolved += 1;
if (group.options.barChart.handleOverlap == 'stack') {
heightOffset = intersections[key].accumulated;
intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
}
else if (group.options.barChart.handleOverlap == 'sideBySide') {
drawData.width = drawData.width / intersections[key].amount;
drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
}
}
DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
// draw points
if (group.options.drawPoints.enabled == true) {
DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
}
}
};
/**
* Fill the intersections object with counters of how many datapoints share the same x coordinates
* @param intersections
* @param combinedData
* @private
*/
LineGraph.prototype._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, accumulated: 0};
}
intersections[combinedData[i].x].amount += 1;
}
}
};
/**
* Get the width and offset for bargraphs based on the coredistance between datapoints
*
* @param coreDistance
* @param group
* @param minWidth
* @returns {{width: Number, offset: Number}}
* @private
*/
LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
var width, offset;
if (coreDistance < group.options.barChart.width && coreDistance > 0) {
width = coreDistance < minWidth ? minWidth : coreDistance;
offset = 0; // recalculate offset with the new width;
if (group.options.barChart.align == 'left') {
offset -= 0.5 * coreDistance;
}
else if (group.options.barChart.align == 'right') {
offset += 0.5 * coreDistance;
}
}
else {
// default settings
width = group.options.barChart.width;
offset = 0;
if (group.options.barChart.align == 'left') {
offset -= 0.5 * group.options.barChart.width;
}
else if (group.options.barChart.align == 'right') {
offset += 0.5 * group.options.barChart.width;
}
}
return {width: width, offset: offset};
};
/**
* draw a line graph
*
* @param dataset
* @param group
*/
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);
if(group.style !== undefined) {
path.setAttributeNS(null, "style", group.style);
}
// construct path from dataset
if (group.options.catmullRom.enabled == true) {
d = this._catmullRom(dataset, group);
}
else {
d = this._linear(dataset);
}
// append with points for fill and finalize the path
if (group.options.shaded.enabled == true) {
var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
var dFill;
if (group.options.shaded.orientation == 'top') {
dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0;
}
else {
dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight;
}
fillPath.setAttributeNS(null, "class", group.className + " fill");
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) {
this._drawPoints(dataset, group, this.svgElements, this.svg);
}
}
}
};
/**
* draw the data points
*
* @param {Array} dataset
* @param {Object} JSONcontainer
* @param {Object} svg | SVG DOM element
* @param {GraphGroup} group
* @param {Number} [offset]
*/
LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
if (offset === undefined) {offset = 0;}
for (var i = 0; i < dataset.length; i++) {
DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, 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
@ -1163,13 +922,13 @@ LineGraph.prototype._convertXcoordinates = function (datapoints) {
};
/**
* This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
* util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
* the yAxis.
*
* @param datapoints
* @param group
* @returns {Array}
* @private
*/
@ -1194,149 +953,5 @@ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
return extractedData;
};
/**
* This uses an uniform parametrization of the CatmullRom algorithm:
* 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al.
* @param data
* @returns {string}
* @private
*/
LineGraph.prototype._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 };
d += 'C' +
bp1.x + ',' +
bp1.y + ' ' +
bp2.x + ',' +
bp2.y + ' ' +
p2.x + ',' +
p2.y + ' ';
}
return d;
};
/**
* This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
* By default, the centripetal parameterization is used because this gives the nicest results.
* These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
*
* One optimization can be used to reuse distances since this is a sliding window approach.
* @param data
* @returns {string}
* @private
*/
LineGraph.prototype._catmullRom = function(data, group) {
var alpha = group.options.catmullRom.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 ]
// [ 0 1 0 0 ]
// [ -d2pow2a/N A/N d1pow2a/N 0 ]
// [ 0 d3pow2a/M B/M -d2pow2a/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 + ' ';
}
return d;
}
};
/**
* this generates the SVG path for a linear drawing between datapoints.
* @param data
* @returns {string}
* @private
*/
LineGraph.prototype._linear = function(data) {
// linear
var d = '';
for (var i = 0; i < data.length; i++) {
if (i == 0) {
d += data[i].x + ',' + data[i].y;
}
else {
d += ' ' + data[i].x + ',' + data[i].y;
}
}
return d;
};
module.exports = LineGraph;

+ 229
- 0
lib/timeline/component/graph2d_types/bar.js View File

@ -0,0 +1,229 @@
/**
* Created by Alex on 11/11/2014.
*/
var DOMutil = require('../../../DOMutil');
var Points = require('./points');
function Bargraph(groupId, options) {
this.groupId = groupId;
this.options = options;
}
Bargraph.prototype.getYRange = function(groupData) {
if (this.options.barChart.handleOverlap != 'stack') {
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};
}
else {
var barCombinedData = [];
for (var j = 0; j < groupData.length; j++) {
barCombinedData.push({
x: groupData[j].x,
y: groupData[j].y,
groupId: this.groupId
});
}
return barCombinedData;
}
};
/**
* draw a bar graph
*
* @param groupIds
* @param processedGroupData
*/
Bargraph.draw = function (groupIds, processedGroupData, framework) {
var combinedData = [];
var intersections = {};
var coreDistance;
var key, drawData;
var group;
var i,j;
var barPoints = 0;
// combine all barchart data
for (i = 0; i < groupIds.length; i++) {
group = framework.groups[groupIds[i]];
if (group.options.style == 'bar') {
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,
groupId: groupIds[i]
});
barPoints += 1;
}
}
}
}
if (barPoints == 0) {return;}
// sort by time and by group
combinedData.sort(function (a, b) {
if (a.x == b.x) {
return a.groupId - b.groupId;
} else {
return a.x - b.x;
}
});
// get intersections
Bargraph._getDataIntersections(intersections, combinedData);
// plot barchart
for (i = 0; i < combinedData.length; i++) {
group = framework.groups[combinedData[i].groupId];
var minWidth = 0.1 * group.options.barChart.width;
key = combinedData[i].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));}
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));}
drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth);
intersections[key].resolved += 1;
if (group.options.barChart.handleOverlap == 'stack') {
heightOffset = intersections[key].accumulated;
intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
}
else if (group.options.barChart.handleOverlap == 'sideBySide') {
drawData.width = drawData.width / intersections[key].amount;
drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
}
}
DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg);
// draw points
if (group.options.drawPoints.enabled == true) {
Points.draw(dataset, group, framework, drawData.offset);
}
}
};
/**
* Fill the intersections object with counters of how many datapoints share the same x coordinates
* @param intersections
* @param combinedData
* @private
*/
Bargraph._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, accumulated: 0};
}
intersections[combinedData[i].x].amount += 1;
}
}
};
/**
* Get the width and offset for bargraphs based on the coredistance between datapoints
*
* @param coreDistance
* @param group
* @param minWidth
* @returns {{width: Number, offset: Number}}
* @private
*/
Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) {
var width, offset;
if (coreDistance < group.options.barChart.width && coreDistance > 0) {
width = coreDistance < minWidth ? minWidth : coreDistance;
offset = 0; // recalculate offset with the new width;
if (group.options.barChart.align == 'left') {
offset -= 0.5 * coreDistance;
}
else if (group.options.barChart.align == 'right') {
offset += 0.5 * coreDistance;
}
}
else {
// default settings
width = group.options.barChart.width;
offset = 0;
if (group.options.barChart.align == 'left') {
offset -= 0.5 * group.options.barChart.width;
}
else if (group.options.barChart.align == 'right') {
offset += 0.5 * group.options.barChart.width;
}
}
return {width: width, offset: offset};
};
Bargraph.getStackedBarYRange = function(barCombinedData, groupRanges, groupIds, groupLabel, orientation) {
if (barCombinedData.length > 0) {
// sort by time and by group
barCombinedData.sort(function (a, b) {
if (a.x == b.x) {
return a.groupId - b.groupId;
} else {
return a.x - b.x;
}
});
var intersections = {};
Bargraph._getDataIntersections(intersections, barCombinedData);
groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData);
groupRanges[groupLabel].yAxisOrientation = orientation;
groupIds.push(groupLabel);
}
}
Bargraph._getStackedBarYRange = 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 {
intersections[key].accumulated += combinedData[i].y;
}
}
for (var xpos in intersections) {
if (intersections.hasOwnProperty(xpos)) {
yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
}
}
return {min: yMin, max: yMax};
};
module.exports = Bargraph;

+ 218
- 0
lib/timeline/component/graph2d_types/line.js View File

@ -0,0 +1,218 @@
/**
* Created by Alex on 11/11/2014.
*/
var DOMutil = require('../../../DOMutil');
var Points = require('./points');
function Line(groupId, options) {
this.groupId = groupId;
this.options = options;
}
Line.prototype.getYRange = function(groupData) {
var yMin = groupData[0].y;
var yMax = groupData[0].y;
for (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};
};
/**
* 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.catmullRom.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) {
var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg);
var dFill;
if (group.options.shaded.orientation == 'top') {
dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0;
}
else {
dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight;
}
fillPath.setAttributeNS(null, "class", group.className + " fill");
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);
}
}
}
};
/**
* This uses an uniform parametrization of the CatmullRom algorithm:
* 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al.
* @param data
* @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 };
d += 'C' +
bp1.x + ',' +
bp1.y + ' ' +
bp2.x + ',' +
bp2.y + ' ' +
p2.x + ',' +
p2.y + ' ';
}
return d;
};
/**
* This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
* By default, the centripetal parameterization is used because this gives the nicest results.
* These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
*
* One optimization can be used to reuse distances since this is a sliding window approach.
* @param data
* @param group
* @returns {string}
* @private
*/
Line._catmullRom = function(data, group) {
var alpha = group.options.catmullRom.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 + ' ';
}
return d;
}
};
/**
* this generates the SVG path for a linear drawing between datapoints.
* @param data
* @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;
}
else {
d += ' ' + data[i].x + ',' + data[i].y;
}
}
return d;
};
module.exports = Line;

+ 43
- 0
lib/timeline/component/graph2d_types/points.js View File

@ -0,0 +1,43 @@
/**
* Created by Alex on 11/11/2014.