Stacked bar chart with (computed) average line in Plotly.js - javascript

I've got a (stacked) bar chart and I want an average line plotted on my chart.
Let's take this example:
var trace1 = {
x: ['giraffes', 'orangutans', 'monkeys'],
y: [20, 14, 23],
name: 'SF Zoo',
type: 'bar'
};
var trace2 = {
x: ['giraffes', 'orangutans', 'monkeys'],
y: [12, 18, 29],
name: 'LA Zoo',
type: 'bar'
};
var data = [trace1, trace2];
var layout = {barmode: 'stack'};
Plotly.newPlot('myDiv', data, layout, {showSendToCloud:true});
Result:
Expected output:
I've found a similar question, but in that case it was pretty easy to add a line with a 'fixed' value. In this case I've got a stacked bar chart nicolaskruchten/pivottable, so the user can easily drag and drop columns. That makes computing the average harder.
I can loop through all results and compute the average value, but since Plotly is very powerful and has something like aggregate functions, I feel like there should be a better way.
How can I add a (computed) average line to my (stacked) bar chart?

Plotly.js not provided any direct options for drawing average line.
But you can do this simple way.
//Find average value for Y
function getAverageY() {
allYValues = trace1.y.map(function (num, idx) {
return num + trace2.y[idx];
});
if (allYValues.length) {
sum = allYValues.reduce(function (a, b) {
return a + b;
});
avg = sum / allYValues.length;
}
return avg;
}
//Create average line in shape
var layout = {
barmode: 'stack',
shapes: [{
type: 'line',
xref: 'paper',
x0: 0,
y0: getAverageY(),
x1: 1,
y1: getAverageY(),
line: {
color: 'green',
width: 2,
dash: 'dot'
}
}]
};
Updated:
You need to update your graph after loading this drawing a average
line for any numbers of trace.
//Check graph is loaded
if (document.getElementById('myDiv')) {
//draw average line
drawAvgLine(document.getElementById('myDiv'))
}
function drawAvgLine(graph) {
var graphData = graph.data; //Loaded traces
//making new layout
var newLayout = {
barmode: 'stack',
shapes: [{
type: 'line',
xref: 'paper',
x0: 0,
y0: getAverageY(graphData),
x1: 1,
y1: getAverageY(graphData),
line: {
color: 'green',
width: 2,
dash: 'dot'
}
}]
};
//Update plot pass existing data
Plotly.update('myDiv', graphData, newLayout)
}
//Calculate avg value
function getAverageY(graphData) {
var total = [],
undefined;
for (var i = 0, n = graphData.length; i < n; i++) {
var arg = graphData[i].y
for (var j = 0, n1 = arg.length; j < n1; j++) {
total[j] = (total[j] == undefined ? 0 : total[j]) + arg[j];
}
}
return total.reduce(function (a, b) {
return a + b;
}) / total.length;
}

Related

Update ‘x’ and ‘y’ values of a trace using Plotly.update()

Is it possible to update the x and y properties of a trace using Plotly.update()?
Updating the marker.color property of the trace works well. But when I try updating the x or y properties the trace disappears from the graph. And there is no indication that something went wrong in the console. I would like to update the values by trace index and the update function looks like the right tool.
There may be a clue in the documentation for Plotly.react():
Important Note: In order to use this method to plot new items in arrays under data such as x or marker.color etc, these items must either have been added immutably (i.e. the identity of the parent array must have changed) or the value of layout.datarevision must have changed.
Though this may the complete unrelated because I am able to update marker.color using Plotly.update() without bumping the layout.datarevision.
Running example:
(codepen example here)
let myPlot = document.getElementById("graph");
let trace = {
type: 'scatter',
x: [0.5 * 255],
y: [0.5 * 255],
hoverinfo: "skip",
mode: 'markers',
marker: {color: "DarkSlateGrey", size: 20},
};
let layout = {
title: "The Worm Hole",
yaxis: { range: [0, 255] },
xaxis: { range: [0, 255] },
};
let config = {responsive: true};
Plotly.newPlot(myPlot, [trace], layout, config);
function updateGraphPoint(cmd) {
let x = Math.random() * 255;
let y = Math.random() * 255;
let z = Math.random() * 255;
let update = null;
if (cmd === "color") {
update = {'marker.color': `rgb(${x}, ${y}, ${z})`};
} else if (cmd === "position") {
update = {'x': [x], 'y': [y]};
}
Plotly.update(myPlot, update, {}, [0]);
}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<button onclick="updateGraphPoint('color')">Change my color!</button>
<button onclick="updateGraphPoint('position')">Change my position!</button>
<div id="graph"></div>
Note: I also asked this question on the the plotly community forum but have not gotten any responses. Maybe someone here knows.
The data update object accepts an array containing an array of new x / y values for each trace you want to update. I.e. if you only want to update one trace, you still have to provide an array containing an array for that one trace:
update = {'x': [[x]], 'y': [[y]]};
let myPlot = document.getElementById("graph");
let trace = {
type: 'scatter',
x: [0.5 * 255],
y: [0.5 * 255],
hoverinfo: "skip",
mode: 'markers',
marker: {color: "DarkSlateGrey", size: 20},
};
let layout = {
title: "The Worm Hole",
yaxis: { range: [0, 255] },
xaxis: { range: [0, 255] },
};
let config = {responsive: true};
Plotly.newPlot(myPlot, [trace], layout, config);
function updateGraphPoint(cmd) {
let x = Math.random() * 255;
let y = Math.random() * 255;
let z = Math.random() * 255;
let update = null;
if (cmd === "color") {
update = {'marker.color': `rgb(${x}, ${y}, ${z})`};
} else if (cmd === "position") {
update = {'x': [[x]], 'y': [[y]]};
}
Plotly.update(myPlot, update, {}, [0]);
}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<button onclick="updateGraphPoint('color')">Change my color!</button>
<button onclick="updateGraphPoint('position')">Change my position!</button>
<div id="graph"></div>

Chart js sum of grouped bars

I have 3 different values for each grouped bar. I want to sum those 3 values and display the sum on top of each column - Σ59 and Σ89.
I'm testing code below which is not working correctly. Instead of column values, number 59 is displayed above every single column.
formatter: function(value, ctx) {
console.log(ctx.chart.data.datasets)
let sum = 0;
for (var i = 0; i < ctx.chart.data.datasets.length; i++) {
sum += parseInt(ctx.chart.data.datasets[i].data[0]);
}
return sum;
}
you can try something like this fiddle, but i did not format and position the labels as of now, you can do it as you wish.
"onComplete": function() {
var chartInstance = this.chart,
ctx = chartInstance.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
var sums = [0, 0, 0];
this.data.datasets.forEach(function(dataset, i) {
var meta = chartInstance.controller.getDatasetMeta(i);
meta.data.forEach(function(bar, index) {
var data = dataset.data[index];
ctx.fillText(data, bar._model.x, bar._model.y);
sums[index] += data;
});
});
sums.forEach(function(v, i) {
ctx.fillText(v, 150*(i+1)+(i*50), 40);
});
}
The formatter is called for each datalabel, meaning for every bar. That's the reason why you get the same value on top of every bar.
Possible solutions would be either to plot an "invisible" bar for the combined value or check inside the formatter function if it is called for the centered bar. Then you could use mulitline labels to create a label which has the value for the current bar and the combined value as well.
Edit:
After thinking about it the easiest way would be to use mixed charts. You can just calculate your combined values for each group and add a chart for these values. For example you add a line chart with showLines: false.
new Chart('myMixedCharts', {
type: 'bar',
data: {
labels: ['foo', 'bar'],
datasets: [
{
label: "Value 1",
data: [12, 5]
},
{
label: "Value 2",
data: [7, 8]
},
{
label: "Group value",
data: [19, 13],
type: "line",
showLines: false
}
]
}
});
Or as jsFiddle

Adding annotations to Google candlestick chart (Posted solution triggers TypeError)

I am trying to add some annotations to a Google Candlestick chart. I noticed someone had already asked this same question (Adding annotations to Google Candlestick chart). The user Aperçu replied with a detailed solution to extend the chart and add annotations since the chart doesn't have any such feature built in. However, when I try this solution I get an error "TypeError: document.querySelectorAll(...)[0] is undefined"
Here is my code:
chartPoints = [
['Budget', 0, 0, 9999, 9999, 'foo1'],
['Sales', 0, 0, 123, 123, 'foo2'],
['Backlog', 123, 123, 456, 456, 'foo3'],
['Hard Forecast', 456, 456, 789, 789, 'foo4'],
['Sales to Budget', 789, 789, 1000, 1000, 'foo5']
];
var data = google.visualization.arrayToDataTable(chartPoints, true);
data.setColumnProperty(5, 'role', 'annotation');
var options = {
legend: 'none',
bar: { groupWidth: '40%', width: '100%' },
candlestick: {
fallingColor: { strokeWidth: 0, fill: '#a52714' },
risingColor: { strokeWidth: 0, fill: '#0f9d58' }
}
};
var chart = new google.visualization.CandlestickChart(document.getElementById('chart_div'));
chart.draw(data, options);
// attempt to use Aperçu's solution
const bars = document.querySelectorAll('#chart_div svg > g:nth-child(5) > g')[0].lastChild.children // this triggers a TypeError
for (var i = 0 ; i < bars.length ; i++) {
const bar = bars[i]
const { top, left, width } = bar.getBoundingClientRect()
const hint = document.createElement('div')
hint.style.top = top + 'px'
hint.style.left = left + width + 5 + 'px'
hint.classList.add('hint')
hint.innerText = rawData.filter(t => t[1])[i][0]
document.getElementById('chart_div').append(hint)
}
I want the chart to show the last piece of data next to the bars (i.e. "foo1", "foo2", etc)
each candle or bar will be represented by a <rect> element
we can use the rise and fall colors to separate the bars from other <rect> elements in the chart
there will be the same number of bars as rows in the data table
once we find the first bar, we can use rowIndex of zero to pull values from the data
we need to find the value of the rise / fall, to know where to place the annotation
then use chart methods to find the location for the annotation
getChartLayoutInterface() - Returns an object containing information about the onscreen placement of the chart and its elements.
getYLocation(position, optional_axis_index) - Returns the screen y-coordinate of position relative to the chart's container.
see following working snippet
two annotations are added
one for the difference in rise and fall
and the other for the value in the column with annotation role
google.charts.load('current', {
callback: drawChart,
packages: ['corechart']
});
function drawChart() {
var chartPoints = [
['Budget', 0, 0, 9999, 9999, 'foo1'],
['Sales', 0, 0, 123, 123, 'foo2'],
['Backlog', 123, 123, 456, 456, 'foo3'],
['Hard Forecast', 456, 456, 789, 789, 'foo4'],
['Sales to Budget', 789, 789, 1000, 1000, 'foo5']
];
var data = google.visualization.arrayToDataTable(chartPoints, true);
data.setColumnProperty(5, 'role', 'annotation');
var options = {
legend: 'none',
bar: { groupWidth: '40%', width: '100%' },
candlestick: {
fallingColor: { strokeWidth: 0, fill: '#a52714' },
risingColor: { strokeWidth: 0, fill: '#0f9d58' }
}
};
var container = document.getElementById('chart_div');
var chart = new google.visualization.CandlestickChart(container);
google.visualization.events.addListener(chart, 'ready', function () {
var annotation;
var bars;
var chartLayout;
var formatNumber;
var positionY;
var positionX;
var rowBalance;
var rowBottom;
var rowIndex;
var rowTop;
var rowValue;
var rowWidth;
chartLayout = chart.getChartLayoutInterface();
rowIndex = 0;
formatNumber = new google.visualization.NumberFormat({
pattern: '#,##0'
});
bars = container.getElementsByTagName('rect');
for (var i = 0; i < bars.length; i++) {
switch (bars[i].getAttribute('fill')) {
case '#a52714':
case '#0f9d58':
rowWidth = parseFloat(bars[i].getAttribute('width'));
if (rowWidth > 2) {
rowBottom = data.getValue(rowIndex, 1);
rowTop = data.getValue(rowIndex, 3);
rowValue = rowTop - rowBottom;
rowBalance = Math.max(rowBottom, rowTop);
positionY = chartLayout.getYLocation(rowBalance) - 6;
positionX = parseFloat(bars[i].getAttribute('x'));
// row value
annotation = container.getElementsByTagName('svg')[0].appendChild(container.getElementsByTagName('text')[0].cloneNode(true));
annotation.textContent = formatNumber.formatValue(rowValue);
annotation.setAttribute('x', (positionX + (rowWidth / 2)));
annotation.setAttribute('y', positionY);
annotation.setAttribute('font-weight', 'bold');
// annotation column
annotation = container.getElementsByTagName('svg')[0].appendChild(container.getElementsByTagName('text')[0].cloneNode(true));
annotation.textContent = data.getValue(rowIndex, 5);
annotation.setAttribute('x', (positionX + (rowWidth / 2)));
annotation.setAttribute('y', positionY - 18);
annotation.setAttribute('font-weight', 'bold');
rowIndex++;
}
break;
}
}
});
chart.draw(data, options);
}
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>

How can I make highlighted points stay after regraphing using flot?

Currently, I have a plot that supports "lockable" points. I register a plotclick event, highlight the point, and display a tooltip that I give the id of "lockedPoint" + item.dataindex. I also have a zoom feature that uses jquery.flot.selection.js. By using this, I modify my x and y axis maximums and minimums, and I replot my data (essentially throwing away the old data). I am trying to preserve the "locked points" when zooming.
One solution I have thought of is when gathering my data, I can specifically push the point into the correct place in the series that it needs to be if it is within range of the zoom and then highlight the point. However, this does not seem to be working correctly.
I store the data about "locked points" in an associative array initialized like this.
for (var i = 1; i < 5; i++){
lockedPoints["Series " + i] = [];
}
Then I allow a maximum of three items to be pushed onto each array (max of three lockable points per series). I would replace the point in the array with the new highlighted point (I believe the only thing that would change would be the dataindex). Is it possible that I could make points survive zooming by pushing them into the data series when gathering data?
function gatherData(kElement, a0Element){
//PRE: kElement is the id of the element containing the rate constant, and a0Element is the id of the element
// containing the molarity
//POST: FCTVAL is a data series in the format of {data: data, lines: {show: true}, color: "color", label: "label"}
var xData = []; //x-coordinates
var yData = []; //y-coordinates
var data = []; //array of coordinate pairs
var startingPoint; //least x-value to graph
var finishingPoint; //greatest x-value to graph
var range; //range of x-values
var interval; //value to evenly space x-values for calculations
var current; //current value of x-coordinate for which we are
// calculating a y-value
var labelParent; //parent node of x-axis labels
var k; //rate constant value
var molarValue; //molarity value
var result; //result of the rate equation
numXPoints = 1001;
if (document.getElementById("logCheck").value == "On"){ //graph logarithmically
logarithmic = true;
}
else{ //graph decay
logarithmic = false;
}
if (document.getElementById("blackWhite").value == "On"){
blackWhite = true;
}
else{
blackWhite = false;
}
k = document.getElementById(kElement).value;
molarValue = document.getElementById(a0Element).value;
startingPoint = minX; //we will say time starts at 0 always for this plot
finishingPoint = maxX;
range = finishingPoint - startingPoint; //calculated range for determining points to plot
interval = range / numXPoints; //we will graph numXPoints points
current = startingPoint;
result = molarValue * Math.pow(Math.E, (-k * current));
if (logarithmic){ //for logarithmic calculations
result = Math.log(result);
}
if (result > maxValue){ //store largest y-value
maxValue = result;
}
for (var i = 0; i < numXPoints; i++){ //store x-values and calculated y-values
// and find max y-value
xData[i] = current;
yData[i] = result;
current += interval;
result = molarValue * Math.pow(Math.E, (-k * current));
if (logarithmic){ //for logarithmic calculations
result = Math.log(result);
}
if (yData[i] > maxValue){ //store largest y-value
maxValue = yData[i];
}
if (yData[i] < minValue && minValue > -400){ //store smallest y-value
minValue = yData[i];
}
}
for (var i = 0; i < numXPoints; i++){ //combine coordinates into one series
data[i] = [xData[i], yData[i]];
}
//modified jquery.flot.js to support dashed line hover.
//values modified were pointRadius and radius in the drawPointHighlight method.
if (blackWhite == true){
switch(kElement){
case "kValue1":
return {points:{show: true, radius: 0}, data: data, lines:{show: true}, color: "black", label: "Series 1", shadowSize: 0};
break;
case "kValue2":
return {points:{show: true, radius: 0}, data: data, dashes:{show: true, dashLength: 2}, color: "black", label: "Series 2", shadowSize: 0};
break;
case "kValue3":
return {points:{show: true, radius: 0}, data: data, dashes:{show: true, dashLength: 10}, color: "black", label: "Series 3", shadowSize: 0};
break;
case "kValue4":
return {points:{show: true, radius: 0}, data: data, dashes:{show: true, dashLength: 20}, color: "black", label: "Series 4", shadowSize: 0};
break;
}
}
else{
switch (kElement){ //return proper object to match flot graph description
case "kValue1":
return {points:{show: false, radius: 0}, data: data, lines:{show: true}, color: "red", label: "Series 1"};
break;
case "kValue2":
return {points:{show: false, radius: 0}, data: data, lines:{show: true}, color: "blue", label: "Series 2"};
break;
case "kValue3":
return {points:{show: false, radius: 0}, data: data, lines:{show: true}, color: "green", label: "Series 3"};
break;
case "kValue4":
return {points:{show: false, radius: 0}, data: data, lines:{show: true}, color: "gold", label: "Series 4"};
break;
}
}
}
I can find a way to deal with the tooltip removal, but highlighting the same point is the most trivial part.
If you use a combination of setupGrid and draw, the highlights will persist between redraws and you won't need to manually reapply (like you would if you re-init).
Also, to manipulate the data between re-draws (say on the zoom selection), use a combination of getData and setData:
$("#flot_chart").bind("plotselected", function (event, ranges) {
var fr = ranges.xaxis.from;
var to = ranges.xaxis.to;
$.each(plot.getXAxes(), function(_, axis) {
var opts = axis.options;
opts.min = fr;
opts.max = to
});
//pad data points, so the hover effect doesn't look spotty
var series = plot.getData(); // get series
series[0].data = []; // clear it out
for (var i = fr; i < to; i+=(to-fr)/100){
series[0].data.push([i,Math.sin(i)]); // push in data with a suitable increment to i
}
plot.setData(series); // set the new data
plot.setupGrid(); // redo the grid
plot.draw(); // redraw the plot
plot.clearSelection();
});
Here's a fiddle example.

Dynamically draw marker on last point in highcharts

I want to draw a marker on the last point. Data source is dynamic.
Have a look at following code
$(function() {
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.marker = {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
};
a = 1;
chart.series[0].points[l - 1] = p;
chart.redraw(false);
});
var ix = 13;
var a = 0;
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
events: {
load: function() {
var series = this.series[0];
setInterval(function() {
ix++;
var vv = 500 + Math.round(Math.random() * 40);
chart.series[0].data[0].remove();
var v;
if (a == 1) v = {
y: vv,
x: ix,
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
}
else v = {
y: vv,
x: ix
}
a = 0;
series.addPoint(v);
}, 1500);
}
}
},
plotOptions: {
series: {}
},
series: [{
data: [500, 510, 540, 537, 510, 540, 537, 500, 510, 540, 537, 510, 540, 537]}]
});
});
http://jsfiddle.net/9zNUP/
On button click event I am trying to draw marker on last point which is already added to chart.
Is there a way to do that??
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.update({
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
});
a = 1;
});
solution # http://jsfiddle.net/jugal/zJZSx/
Also tidied up your code a little, removed the removal of point before adding one at the end, highcharts supports it inbuilt with the third param to addPoint as true, which denotes shift series, which removes first point and then adds the given point.
I didn't really understand what the a vv etc were, but well i didn't bother much either. I think this is enough based on what you asked for.

Categories