Related
I have a nodes diagram in table layout.
In case I have some nodes in cell I got this table:
Image:
https://ibb.co/554y9ck
(behind Q2 there are Q0 and Q1.. they are overlapped)
How can I arrange them nicely? :)
Here is my nodesTemplate:
var nodeSimpleTemplate =
$(go.Node, "Auto",mouseEventHandlers(),
new go.Binding("row").makeTwoWay(),
new go.Binding("column", "col").makeTwoWay(),
new go.Binding("alignment", "align", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
{
//locationSpot: go.Spot.Center,
// when the user clicks on a Node, highlight all Links coming out of the node
// and all of the Nodes at the other ends of those Links.
click: function (e, node) {
var diagram = node.diagram;
diagram.startTransaction("Click simple node");
diagram.clearHighlighteds();
// #ts-ignore
node.findLinksOutOf().each(function (l) {
changeLinkCategory(e, l);
l.isHighlighted = true;
});
// #ts-ignore
node.findNodesOutOf().each(function (n) {
n.isHighlighted = true;
});
changeNodeCategory(e, node);
diagram.commitTransaction("Click simple node");
}
},
$(go.Shape, "Ellipse",
{
fill: $(go.Brush, "Linear", {0: "white", 1: "lightblue"}),
stroke: "darkblue", strokeWidth: 2
}),
$(go.Panel, "Table",
{defaultAlignment: go.Spot.Left, margin: 4},
$(go.RowColumnDefinition, {column: 1, width: 4}),
$(go.TextBlock,
{row: 0, column: 0, columnSpan: 3, alignment: go.Spot.Center},
{font: "bold 14pt sans-serif"},
new go.Binding("text", "key"))
));
var nodeDetailedTemplate =
$(go.Node, "Auto",mouseEventHandlers(),
new go.Binding("row").makeTwoWay(),
new go.Binding("column", "col").makeTwoWay(),
new go.Binding("alignment", "align", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
{
//locationSpot: go.Spot.Center,
// when the user clicks on a Node, highlight all Links coming out of the node
// and all of the Nodes at the other ends of those Links.
click: function (e, node) {
var diagram = node.diagram;
diagram.startTransaction("Click Details node");
diagram.clearHighlighteds();
// #ts-ignore
node.findLinksOutOf().each(function (l) {
changeLinkCategory(e, l);
l.isHighlighted = true;
});
// #ts-ignore
node.findNodesOutOf().each(function (n) {
n.isHighlighted = true;
});
changeNodeCategory(e, node);
diagram.commitTransaction("Click Details node");
}
},
$(go.Shape, "Ellipse",
{
fill: $(go.Brush, "Linear", {0: "white", 1: "lightblue"}),
stroke: "darkblue", strokeWidth: 2
}),
$(go.Panel, "Table",
{defaultAlignment: go.Spot.Left, margin: 4},
$(go.RowColumnDefinition, {column: 1, width: 4}),
$(go.TextBlock,
{row: 0, column: 0, columnSpan: 3, alignment: go.Spot.Center},
{font: "bold 14pt sans-serif"},
new go.Binding("text", "key")),
$(go.TextBlock, "Time: ",
{row: 1, column: 0}, {font: "bold 10pt sans-serif"}),
$(go.TextBlock,
{row: 1, column: 2},
new go.Binding("text", "time")),
$(go.TextBlock, "Parameters: ",
{row: 2, column: 0}, {font: "bold 10pt sans-serif"}),
$(go.TextBlock,
{row: 2, column: 2},
new go.Binding("text", "parameters"))
)
);
// for each of the node categories, specify which template to use
dia.nodeTemplateMap.add("simple", nodeSimpleTemplate);
dia.nodeTemplateMap.add("detailed", nodeDetailedTemplate);
Here is the diagram definition:
public initDiagram(): go.Diagram {
// define a custom ResizingTool to limit how far one can shrink a row or column
function LaneResizingTool() {
go.ResizingTool.call(this);
}
go.Diagram.inherit(LaneResizingTool, go.ResizingTool);
LaneResizingTool.prototype.computeMinSize = function() {
var diagram = this.diagram;
var lane = this.adornedObject.part; // might be row or column
var horiz = (lane.rowSpan >= 9999); // column header
var margin = diagram.nodeTemplate.margin;
var bounds = new go.Rect();
diagram.findTopLevelGroups().each(function(g) {
if (horiz ? (g.column === lane.column) : (g.row === lane.row)) {
var b = diagram.computePartsBounds(g.memberParts);
if (b.isEmpty()) return; // nothing in there? ignore it
b.unionPoint(g.location); // keep any empty space on the left and top
b.addMargin(margin); // assume the same node margin applies to all nodes
if (bounds.isEmpty()) {
bounds = b;
} else {
bounds.unionRect(b);
}
}
});
// limit the result by the standard value of computeMinSize
var msz = go.ResizingTool.prototype.computeMinSize.call(this);
if (bounds.isEmpty()) return msz;
return new go.Size(Math.max(msz.width, bounds.width), Math.max(msz.height, bounds.height));
};
LaneResizingTool.prototype.resize = function(newr) {
var lane = this.adornedObject.part;
var horiz = (lane.rowSpan >= 9999);
var lay = this.diagram.layout; // the TableLayout
if (horiz) {
var col = lane.column;
var coldef = lay.getColumnDefinition(col);
coldef.width = newr.width;
} else {
var row = lane.row;
var rowdef = lay.getRowDefinition(row);
rowdef.height = newr.height;
}
lay.invalidateLayout();
};
// end LaneResizingTool class
function AlignmentDraggingTool() {
go.DraggingTool.call(this);
}
go.Diagram.inherit(AlignmentDraggingTool, go.DraggingTool);
AlignmentDraggingTool.prototype.moveParts = function(parts, offset, check) {
go.DraggingTool.prototype.moveParts.call(this, parts, offset, check);
var tool = this;
parts.iteratorKeys.each(function(part) {
if (part instanceof go.Link) return;
var col = part.column;
var row = part.row;
if (typeof col === "number" && typeof row === "number") {
var b = computeCellBounds(col, row);
part.alignment = new go.Spot(0.5, 0.5, b.centerX, b.centerY); // offset from center of cell
}
})
}
// end AlignmentDraggingTool
// Utility functions, assuming the Diagram.layout is a TableLayout,
// and that the rows and columns are implemented as Groups
function computeCellBounds(col, row) { // this is only valid after a layout
//#ts-ignore
var coldef = dia.layout.getColumnDefinition(col);
//#ts-ignore
var rowdef = dia.layout.getRowDefinition(row);
return new go.Rect(coldef.position, rowdef.position, coldef.total, rowdef.total);
}
function findColumnGroup(col) {
var it = dia.findTopLevelGroups();
while (it.next()) {
var g = it.value;
if (g.column === col && g.rowSpan >= 9999) return g;
}
return null;
}
function findRowGroup(row) {
var it = dia.findTopLevelGroups();
while (it.next()) {
var g = it.value;
if (g.row === row && g.columnSpan >= 9999) return g;
}
return null;
}
function mouseEventHandlers() { // standard mouse drag-and-drop event handlers
return {
mouseDragEnter: function(e) { mouseInCell(e, true); },
mouseDragLeave: function(e) { mouseInCell(e, false); },
mouseDrop: function(e) { mouseDropInCell(e, e.diagram.selection); }
};
}
function mouseInCell(e, highlight) {
e.diagram.clearHighlighteds();
var col = e.diagram.layout.findColumnForDocumentX(e.documentPoint.x);
if (col < 1) col = 1; // disallow dropping in headers
var g = findColumnGroup(col);
if (g !== null) g.isHighlighted = highlight;
var row = e.diagram.layout.findRowForDocumentY(e.documentPoint.y);
if (row < 1) row = 1;
g = findRowGroup(row);
if (g !== null) g.isHighlighted = highlight;
}
function mouseDropInCell(e, coll) {
var col = e.diagram.layout.findColumnForDocumentX(e.documentPoint.x);
if (col < 1) col = 1; // disallow dropping in headers
var row = e.diagram.layout.findRowForDocumentY(e.documentPoint.y);
if (row < 1) row = 1;
coll.each(function(node) {
if (node instanceof go.Node) {
node.column = col;
node.row = row;
// adjust the alignment to the new cell's center point
var cb = computeCellBounds(col, row);
var ab = node.actualBounds.copy();
//#ts-ignore
if (ab.right > cb.right-node.margin.right) ab.x -= (ab.right - cb.right + node.margin.right);
//#ts-ignore
if (ab.left < cb.left+node.margin.left) ab.x = cb.left + node.margin.left;
//#ts-ignore
if (ab.bottom > cb.bottom-node.margin.bottom) ab.y -= (ab.bottom - cb.bottom + node.margin.bottom);
//#ts-ignore
if (ab.top < cb.top+node.margin.top) ab.y = cb.top + node.margin.top;
var off = ab.center.subtract(cb.center);
node.alignment = new go.Spot(0.5, 0.5, off.x, off.y);
}
});
dia.layoutDiagram(true);
}
const $ = go.GraphObject.make;
const dia = $(go.Diagram,{
layout: $(TableLayout,
$(go.RowColumnDefinition, { row: 0, height: 50, minimum: 50 }),
$(go.RowColumnDefinition, { column: 0, width: 100, minimum: 100 }),
// defaultStretch: go.GraphObject.Horizontal,
),
'initialContentAlignment': go.Spot.Center,
'undoManager.isEnabled': true,
resizingTool: new LaneResizingTool(),
model: $(go.GraphLinksModel,
{
linkToPortIdProperty: 'toPort',
linkFromPortIdProperty: 'fromPort',
linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
}
),
});
If you start from the Table Layout sample, you can just add this line in the Group template:
{
layout: $(go.GridLayout, { wrappingColumn: 1 })
}
Adapt the GridLayout as needed, or replace it with a different layout.
I need to center the trace because on load the trace doesn't appears and the user should move the view.
I couldn't fin any documentation or question referencing this issue.
The data is retrieved from the server via service and
I'm using Angular 7 for the front-end and plotly to draw the plot.
When the page loads the plot looks like this: Image error.
If i move the view looks like this: Image okey.
Thanks you
Sample code:
private loadPlot(): void {
const minValues = [];
const maxValues = [];
const dataForPlot = [];
let traceIndex = 0;
for (const key in this.serverData.data) {
if (key === 'filename') {
continue;
}
const values = [];
const colorsForLine = [];
const markersForLine = [];
const mean = calculateMean(this.serverData.data[key]);
const textArray = [];
this.serverData.data[key].forEach(
(element, index) => {
let marker = 'circle';
const color = getPointColor(element['nc']);
const elementText = element['value'];
const value = element['value'];
if (index === this.serverData.data[key].length - 1) {
marker = 'diamond-cross';
}
values.push(value);
colorsForLine[index] = color;
markersForLine[index] = marker;
textArray[index] = elementText + '<br>' + truncateFilename(this.serverData.data['filename'][index], 50);
}
);
minValues.push(Math.min.apply(null, values.filter((n) => !isNaN(n))));
maxValues.push(Math.max.apply(null, values.filter((n) => !isNaN(n))));
const trace = {
x: this.serverData.dates,
y: values,
type: 'scatter',
mode: 'lines+markers',
marker: {
color: colorsForLine,
symbol: markersForLine,
size: 5
},
line: {
// color: colorsForLine,
},
connectgaps: false,
name: key,
description: 'number of ' + key,
filenames: this.serverData.data['filename'],
hoverinfo: 'x+text',
hovertext: textArray
};
dataForPlot.push(trace);
traceIndex++;
}
let MINVALUEFORPLOT;
let MAXVALUEFORPLOT;
if (this.plotThreshold === undefined) {
MINVALUEFORPLOT = Math.min.apply(null, minValues) - Math.abs((Math.min.apply(null, minValues) * 0.1));
MAXVALUEFORPLOT = Math.max.apply(null, maxValues) + (Math.max.apply(null, maxValues) * 0.1);
} else {
const height = (this.layoutShapes[this.layoutShapes.length - 1]['y0'] - this.layoutShapes[this.layoutShapes.length - 1]['y1']) * 0.3;
MINVALUEFORPLOT = this.layoutShapes[this.layoutShapes.length - 1]['y1'] - height;
MAXVALUEFORPLOT = this.layoutShapes[this.layoutShapes.length - 1]['y0'] + height;
}
this.layout = {
// title: this.chart.name,
title: this.generatePlotTitle(),
shapes: [],
colorway: traceColor.colorRange,
hovermode: 'closest',
xaxis: {
nticks: 10,
},
yaxis: {
type: 'linear',
range: [MINVALUEFORPLOT, MAXVALUEFORPLOT]
},
currentDiv: 'plot'
};
this.layout.shapes = this.layoutShapes;
Plotly.react('plot', dataForPlot, this.layout);
}
I'm working with the chartJS library and trying to figure out what I need to do to get a single lines data to display in the tooltip.
For example,
I am hovering over the blue line here and see every data point at that mark. What I would like to do is see all three data points for the blue line only.
I've made some progress from chart js tooltip how to control the data that show
getPointsAtEvent: function(e) {
var pointsArray = [], eventPosition = helpers.getRelativePosition(e);
var breakLoop = 0;
helpers.each(this.datasets, function(dataset) {
helpers.each(dataset.points, function(point) {
if (point.inRange(eventPosition.x, eventPosition.y) && point.showTooltip && !point.ignore) {
if(eventPosition.y + 2 >= point.y && eventPosition.y - 2 <= point.y) {
pointsArray.push(point);
breakLoop = 1;
return false;
}
}
});
if(breakLoop) {
return false;
}
}, this);
//console.log(pointsArray);
return pointsArray;
},
Is my chart modification that will return 1 data point on the graph. I'm assuming the next step is to overwrite the showToolTip method.
If this is the only chart you have (i.e. because the following code changes some of the global chart.js elements), you can use the following bit of code
var originalMultiTooltip = Chart.MultiTooltip;
Chart.MultiTooltip = function () {
var argument = arguments[0];
// locate the series using the active point
var activeDatasetLabel = myChart.activeElements[0].datasetLabel;
myChart.datasets.forEach(function (dataset) {
if (dataset.label === activeDatasetLabel) {
// swap out the labels and colors in arguments
argument.labels = dataset.points.map(function (point) { return point.value; });
argument.legendColors = dataset.points.map(function (point) {
return {
fill: point._saved.fillColor || point.fillColor,
stroke: point._saved.strokeColor || point.strokeColor
};
});
argument.title = activeDatasetLabel;
// position it near the active point
argument.y = myChart.activeElements[0].y;
}
})
return new originalMultiTooltip(arguments[0]);
}
// this distance function returns the square of the distance if within detection range, otherwise it returns Infinity
var distance = function (chartX, chartY) {
var hitDetectionRange = this.hitDetectionRadius + this.radius;
var distance = Math.pow(chartX - this.x, 2) + Math.pow(chartY - this.y, 2);
return (distance < Math.pow(hitDetectionRange, 2)) ? distance : Infinity;
}
myChart.getPointsAtEvent = function (e) {
var pointsArray = [],
eventPosition = Chart.helpers.getRelativePosition(e);
var leastDistance = Infinity;
Chart.helpers.each(myChart.datasets, function (dataset) {
Chart.helpers.each(dataset.points, function (point) {
// our active point is the one closest to the hover event
var pointDistance = distance.call(point, eventPosition.x, eventPosition.y)
if (isFinite(pointDistance) && pointDistance < leastDistance) {
leastDistance = pointDistance;
pointsArray = [ point ];
}
});
}, myChart);
return pointsArray;
}
It does 2 things
Replaces the getPointsAtEvent to just pick one point
Wraps the MultiTooltip constructor to swap out the list of values passed with all the values from the active point's series.
Fiddle - http://jsfiddle.net/h93pyavk/
If you extend the line chart, use the code I have above, and the code I pasted below you can get the desired effect to some degree.
showTooltip: function(ChartElements, forceRedraw) { //custom edit
//we will get value from ChartElements (which should be only 1 element long in this case) and use it to match the line row we want to see.
try {
var numMatch = ChartElements[0].value;
}
catch(err) {
var isChanged = (function(Elements) {
var changed = true;
return changed;
}).call(this, ChartElements);
}
// Only redraw the chart if we've actually changed what we're hovering on.
if (typeof this.activeElements === 'undefined') this.activeElements = [];
var isChanged = (function(Elements) {
var changed = false;
if (Elements.length !== this.activeElements.length) {
changed = true;
return changed;
}
helpers.each(Elements, function(element, index) {
if (element !== this.activeElements[index]) {
changed = true;
}
}, this);
return changed;
}).call(this, ChartElements);
if (!isChanged && !forceRedraw) {
return;
} else {
this.activeElements = ChartElements;
}
this.draw();
if (this.options.customTooltips) {
this.options.customTooltips(false);
}
if (ChartElements.length > 0) {
// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
if (this.datasets && this.datasets.length > 1) {
var dataArray,
dataIndex;
for (var i = this.datasets.length - 1; i >= 0; i--) {
dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
dataIndex = helpers.indexOf(dataArray, ChartElements[0]);
if (dataIndex !== -1) {
break;
}
}
var eleLast = "";
var eleFirst = "";
var tooltipLabels = [],
tooltipColors = [],
medianPosition = (function(index) {
// Get all the points at that particular index
var Elements = [],
dataCollection,
xPositions = [],
yPositions = [],
xMax,
yMax,
xMin,
yMin;
helpers.each(this.datasets, function(dataset) {
dataCollection = dataset.points || dataset.bars || dataset.segments;
//console.log(dataset);
for(i = 0; i < dataset.points.length; i++) {
if(dataset.points[i].value === numMatch) {
for(var k = 0; k < dataset.points.length; k++) {
Elements.push(dataset.points[k]);
}
}
}
});
//save elements last label string
eleLast = Elements[Elements.length-1].label;
eleFirst = Elements[0].label;
//console.log(Elements);
helpers.each(Elements, function(element) {
if(element.value === numMatch) {
xPositions.push(element.x);
yPositions.push(element.y);
}
//Include any colour information about the element
tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
tooltipColors.push({
fill: element._saved.fillColor || element.fillColor,
stroke: element._saved.strokeColor || element.strokeColor
});
}, this);
yMin = helpers.min(yPositions);
yMax = helpers.max(yPositions);
xMin = helpers.min(xPositions);
xMax = helpers.max(xPositions);
return {
x: (xMin > this.chart.width / 2) ? xMin : xMax,
y: (yMin + yMax) / 2
};
}).call(this, dataIndex);
var newLabel = eleFirst + " to " + eleLast;
new Chart.MultiTooltip({
x: medianPosition.x,
y: medianPosition.y,
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
xOffset: this.options.tooltipXOffset,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
titleTextColor: this.options.tooltipTitleFontColor,
titleFontFamily: this.options.tooltipTitleFontFamily,
titleFontStyle: this.options.tooltipTitleFontStyle,
titleFontSize: this.options.tooltipTitleFontSize,
cornerRadius: this.options.tooltipCornerRadius,
labels: tooltipLabels,
legendColors: tooltipColors,
legendColorBackground: this.options.multiTooltipKeyBackground,
title: newLabel,
chart: this.chart,
ctx: this.chart.ctx,
custom: this.options.customTooltips
}).draw();
} else {
helpers.each(ChartElements, function(Element) {
var tooltipPosition = Element.tooltipPosition();
new Chart.Tooltip({
x: Math.round(tooltipPosition.x),
y: Math.round(tooltipPosition.y),
xPadding: this.options.tooltipXPadding,
yPadding: this.options.tooltipYPadding,
fillColor: this.options.tooltipFillColor,
textColor: this.options.tooltipFontColor,
fontFamily: this.options.tooltipFontFamily,
fontStyle: this.options.tooltipFontStyle,
fontSize: this.options.tooltipFontSize,
caretHeight: this.options.tooltipCaretSize,
cornerRadius: this.options.tooltipCornerRadius,
text: helpers.template(this.options.tooltipTemplate, Element),
chart: this.chart,
custom: this.options.customTooltips
}).draw();
}, this);
}
}
return this;
},
Obviously this is just a quick and dirty fix, if I get more time to work on it I would like to have each data point show its corresponding value above it.
I'm looking for a JavaScript charting library that supports shading the area between two lines. ChartDirector handles this quite nicely (see: http://www.advsofteng.com/gallery_line2.html - Inter-line Coloring), but I require a more interactive charting library.
I've looked into various JavaScript libraries. Flot and Highcharts come close, but still have their limitations:
Flot supports shading between two lines using the fillBetween plugin, but it does not support shading with multiple colors depending on which line is on top.
One can achieve shading between two lines with Highcharts using stacked area charts, but it does not handle the case where the two lines intersect.
Any suggestions?
I ended up using Highcharts. I took this example ElementStacks and modified it to handle the intersections. See Negative Area.
$(function() {
var Intersection = function (d1, d2) {
var self = this;
this.init = function () {
this.d1 = this.sortLine(d1);
this.d2 = this.sortLine(d2);
if (this.d1.length != this.d2.length) {
throw 'd1 and d2 expected to be same size';
}
this.dps = _.zip(d1, d2);
hasUnmatchedIndex = _.any(this.dps, function(dp_pair) {
return dp_pair[0][0] != dp_pair[1][0];
});
if (hasUnmatchedIndex)
throw 'd1 and d2 do not have same indices';
};
this.sortLine = function(line) {
return _.sortBy(line, function(dp) { return dp[0]; });
};
this.transitions = function() {
return _.map(this.dps, function(dp_pair) {
a = dp_pair[0];
b = dp_pair[1];
result = null;
if (a[1] < b[1])
result = -1;
else if (a[1] > b[1])
result = 1;
else
result = 0;
return [a[0], result];
});
};
this.dropTransitions = function() {
prev = null;
drops = [];
_.each(this.transitions(), function(curr) {
if (prev && prev[1] != curr[1] && prev[1] != 0 && curr[1] != 0)
drops.push([prev, curr])
prev = curr;
});
return drops;
};
this.data = function() {
//self = this;
_d1 = this.sortLine(this.d1.concat(this.intersections()));
_d2 = this.sortLine(this.d2.concat(this.intersections()));
d1_g = [];
d2_g = [];
d_min = [];
dps = _.zip(_d1, _d2);
_.each(dps, function(dp_pair,i) {
index = dp_pair[0][0];
dpv1 = dp_pair[0][1];
dpv2 = dp_pair[1][1];
if (dpv1 == null || dpv2 == null) {
d1_g.push([index, null]);
d2_g.push([index, null]);
} else {
diff = Math.abs(dpv1 - dpv2);
if (dpv1 > dpv2) {
d1_g.push([index, diff]);
d2_g.push([index, 0]);
} else if (dpv2 > dpv1) {
d1_g.push([index, 0]);
d2_g.push([index, diff]);
} else {
d1_g.push([index, diff]);
d2_g.push([index, diff]);
}
}
d_min.push([index, Math.min(dpv1, dpv2)]);
});
return [d1_g, d2_g, d_min];
};
this.intersections = function() {
//self = this;
return _.map(this.dropTransitions(), function(dt) {
line1 = _.filter(self.d1, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
line2 = _.filter(self.d2, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
return self.findIntersection(line1, line2);
});
};
this.findIntersection = function(line1, line2) {
eq1 = this.lineEquation(line1);
eq2 = this.lineEquation(line2);
m1 = eq1.m;
b1 = eq1.b;
m2 = eq2.m;
b2 = eq2.b;
x = (b2 - b1) / (m1 - m2)
y = (m1 * x) + b1
return [x,y];
};
this.lineEquation = function(line) {
p1 = _.map(line[0], function(n) { return parseFloat(n); });
p2 = _.map(line[1], function(n) { return parseFloat(n); });
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
m = (y1 - y2) / (x1 - x2);
b = y1 - (m*x1);
eq = {'m': m, 'b': b};
return eq;
};
this.print = function (obj) { alert(JSON.stringify(obj)); };
this.init(d1, d2);
};
var d1r = [10, 8, 7, 6, 5, 4, 3, 5, 3, 9, 10, 11, 2];
var d2r = [ 5, 6, 7, 8, 9, 10, 9, 8, 12, 3, 2, 1, 20];
var d1 = _.map(d1r, function(e,i) { return [i, e-10]; });
var d2 = _.map(d2r, function(e,i) { return [i, e-10]; });
var t = new Intersection(d1, d2);
var data = t.data();
var values = _.map(data, function(dps) {
return _.map(dps, function(dp) {
return dp[1];
});
});
var minValue = _.min(_.flatten(values));
// Need to find threshold to handle negative stacking values
var threshold = minValue < 0 ? minValue : 0;
var dp1 = t.d1;
var dp2 = t.d2;
var dp1_g = data[0];
var dp2_g = data[1];
var dp_min = data[2];
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'area',
animation: false
},
plotOptions: {
area: {
stacking: true,
lineWidth: 0,
shadow: false,
marker: {
enabled: false
},
enableMouseTracking: false,
showInLegend: false
},
line: {
zIndex: 5
},
series: {
threshold: threshold
}
},
series: [{
type: 'line',
color: 'red',
data: dp1
},{
type: 'line',
color: 'black',
data: dp2
},{
color: 'orange',
data: dp1_g
},{
color: 'grey',
data: dp2_g
},{
id: 'transparent',
color: 'rgba(255,255,255,0.0)',
data: dp_min
}]
}, function(chart){
chart.get('transparent').area.hide();
});
});
I'm looking for a JavaScript charting library that supports shading the area between two lines. ChartDirector handles this quite nicely (see: http://www.advsofteng.com/gallery_line2.html - Inter-line Coloring), but I require a more interactive charting library.
I've looked into various JavaScript libraries. Flot and Highcharts come close, but still have their limitations:
Flot supports shading between two lines using the fillBetween plugin, but it does not support shading with multiple colors depending on which line is on top.
One can achieve shading between two lines with Highcharts using stacked area charts, but it does not handle the case where the two lines intersect.
Any suggestions?
I ended up using Highcharts. I took this example ElementStacks and modified it to handle the intersections. See Negative Area.
$(function() {
var Intersection = function (d1, d2) {
var self = this;
this.init = function () {
this.d1 = this.sortLine(d1);
this.d2 = this.sortLine(d2);
if (this.d1.length != this.d2.length) {
throw 'd1 and d2 expected to be same size';
}
this.dps = _.zip(d1, d2);
hasUnmatchedIndex = _.any(this.dps, function(dp_pair) {
return dp_pair[0][0] != dp_pair[1][0];
});
if (hasUnmatchedIndex)
throw 'd1 and d2 do not have same indices';
};
this.sortLine = function(line) {
return _.sortBy(line, function(dp) { return dp[0]; });
};
this.transitions = function() {
return _.map(this.dps, function(dp_pair) {
a = dp_pair[0];
b = dp_pair[1];
result = null;
if (a[1] < b[1])
result = -1;
else if (a[1] > b[1])
result = 1;
else
result = 0;
return [a[0], result];
});
};
this.dropTransitions = function() {
prev = null;
drops = [];
_.each(this.transitions(), function(curr) {
if (prev && prev[1] != curr[1] && prev[1] != 0 && curr[1] != 0)
drops.push([prev, curr])
prev = curr;
});
return drops;
};
this.data = function() {
//self = this;
_d1 = this.sortLine(this.d1.concat(this.intersections()));
_d2 = this.sortLine(this.d2.concat(this.intersections()));
d1_g = [];
d2_g = [];
d_min = [];
dps = _.zip(_d1, _d2);
_.each(dps, function(dp_pair,i) {
index = dp_pair[0][0];
dpv1 = dp_pair[0][1];
dpv2 = dp_pair[1][1];
if (dpv1 == null || dpv2 == null) {
d1_g.push([index, null]);
d2_g.push([index, null]);
} else {
diff = Math.abs(dpv1 - dpv2);
if (dpv1 > dpv2) {
d1_g.push([index, diff]);
d2_g.push([index, 0]);
} else if (dpv2 > dpv1) {
d1_g.push([index, 0]);
d2_g.push([index, diff]);
} else {
d1_g.push([index, diff]);
d2_g.push([index, diff]);
}
}
d_min.push([index, Math.min(dpv1, dpv2)]);
});
return [d1_g, d2_g, d_min];
};
this.intersections = function() {
//self = this;
return _.map(this.dropTransitions(), function(dt) {
line1 = _.filter(self.d1, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
line2 = _.filter(self.d2, function(dp) {
return dp[0] == dt[0][0] || dp[0] == dt[1][0];
});
return self.findIntersection(line1, line2);
});
};
this.findIntersection = function(line1, line2) {
eq1 = this.lineEquation(line1);
eq2 = this.lineEquation(line2);
m1 = eq1.m;
b1 = eq1.b;
m2 = eq2.m;
b2 = eq2.b;
x = (b2 - b1) / (m1 - m2)
y = (m1 * x) + b1
return [x,y];
};
this.lineEquation = function(line) {
p1 = _.map(line[0], function(n) { return parseFloat(n); });
p2 = _.map(line[1], function(n) { return parseFloat(n); });
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
m = (y1 - y2) / (x1 - x2);
b = y1 - (m*x1);
eq = {'m': m, 'b': b};
return eq;
};
this.print = function (obj) { alert(JSON.stringify(obj)); };
this.init(d1, d2);
};
var d1r = [10, 8, 7, 6, 5, 4, 3, 5, 3, 9, 10, 11, 2];
var d2r = [ 5, 6, 7, 8, 9, 10, 9, 8, 12, 3, 2, 1, 20];
var d1 = _.map(d1r, function(e,i) { return [i, e-10]; });
var d2 = _.map(d2r, function(e,i) { return [i, e-10]; });
var t = new Intersection(d1, d2);
var data = t.data();
var values = _.map(data, function(dps) {
return _.map(dps, function(dp) {
return dp[1];
});
});
var minValue = _.min(_.flatten(values));
// Need to find threshold to handle negative stacking values
var threshold = minValue < 0 ? minValue : 0;
var dp1 = t.d1;
var dp2 = t.d2;
var dp1_g = data[0];
var dp2_g = data[1];
var dp_min = data[2];
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'area',
animation: false
},
plotOptions: {
area: {
stacking: true,
lineWidth: 0,
shadow: false,
marker: {
enabled: false
},
enableMouseTracking: false,
showInLegend: false
},
line: {
zIndex: 5
},
series: {
threshold: threshold
}
},
series: [{
type: 'line',
color: 'red',
data: dp1
},{
type: 'line',
color: 'black',
data: dp2
},{
color: 'orange',
data: dp1_g
},{
color: 'grey',
data: dp2_g
},{
id: 'transparent',
color: 'rgba(255,255,255,0.0)',
data: dp_min
}]
}, function(chart){
chart.get('transparent').area.hide();
});
});