|
|
@ -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; |