Fill / Shade a chart above a specific Y value in PlotlyJS - javascript

I would like to fill until a specific Y value in PlotlyJS. This is as far as I got from the PlotlyJS docs: Fiddle
{
"x": [
"2016-01-31T00:03:57.000Z",
"2016-02-12T04:35:26.000Z"
],
"y": [
100,
100
],
"fill": "tonexty",
"fillcolor": "#8adcb3"
}
In the documentation, there seems to be two options:
tonexty - Fills as below. The problem is 'tonexty' is a bit limiting - The use case is 'filling until the line', so shading ONLY above 110. Example:
tozeroy - Fills till zero:
Also, do you need to introduce a new trace in order to create a fill?
This means that if I have a chart as follows (with only one trace but a threshold line as a shape): I need to introduce another trace just to create a fill. Maybe there's something I missed in the docs, or this is the wrong approach altogether.
So, how do you fill an area in a trace above a specific Y value in PlotlyJS?

A solution is to use multiple traces.
Split all your traces between ones which are above 0 and ones which are not.
When you are done you can fill them (or not) with the 'tozeroy' value.
The following jsfiddle shows a working example.
The code is as following :
HTML:
<div id="myDiv" style="width:600px;height:250px;"></div>
JS:
var data = [
{
x: ['A', 'B', 'C', 'D'],
y: [1, 3, 6, 0],
fill: 'tozeroy',
fillcolor: '#8adcb3'
},
{
x: ['D', 'F', 'G', 'I'],
y: [0, -3, -2, 0],
fill: 'toself'
},
{
x: ['I', 'J', 'K'],
y: [0, 5, 7],
fill: 'tozeroy',
fillcolor: '#0adcb3'
}
];
Plotly.newPlot('myDiv', data);
The result looks as following :

Here is another solution exploiting Plotly's fill: "toself". The idea is to create a closed line trace which encloses the area above the threshold and the markers of the main line. Works for threshold values above zero and for numerical x-values.
The helper traces have their legend hidden and are grouped with the main trace, thereby preventing ugly artifacts when toggling the legend.
The function checks for each x-y-pair if the y-value is above the threshold, if yes
check if there is already a segment above the threshold and use this one OR create a new sgement
the segement starts from the y-value of the threshold and the intermediate x-value from the point above the threshold and the one before.
each segment is terminated with an y-value which is equal to the threshol and the x-value which the mean of the last point in the segment and the next one
The function itself can be surely written in a nicer way but it's just a proof-of-concept.
function dataToTraces(data, threshold) {
var fillers = [];
var emptyFiller = {
x: [],
y: [],
fill: "toself",
mode: "lines",
line: {
width: 0
},
opacity: 0.5,
fillcolor: "#8adcb3",
showlegend: false,
legendgroup: "main"
}
fillers.push(emptyFiller);
for (var i = 0; i < data.y.length; i += 1) {
if (data.y[i] >= threshold) {
if (i !== 0 && data.y[i - 1] < threshold) {
fillers[fillers.length - 1].x.push(data.x[i - 1] + (threshold - data.y[i - 1]) / (data.y[i] - data.y[i - 1]));
fillers[fillers.length - 1].y.push(threshold);
}
fillers[fillers.length - 1].x.push(data.x[i]);
fillers[fillers.length - 1].y.push(data.y[i]);
} else if (fillers[fillers.length - 1].x.length > 0) {
if (i !== 0 && data.y[i - 1] !== threshold) {
fillers[fillers.length - 1].x.push(data.x[i - 1] + (threshold - data.y[i - 1]) / (data.y[i] - data.y[i - 1]));
fillers[fillers.length - 1].y.push(threshold);
}
fillers.push(emptyFiller);
}
}
return fillers;
}
var data = [{
x: [0, 1, 2, 3, 4, 5, 6, 7, 8],
y: [1, 3, 6, 2, -1, 5, 1, 3, 0],
name: "main",
legendgroup: "main"
}];
var fillers = dataToTraces(data[0], 2);
Plotly.newPlot("myDiv", data.concat(fillers));
<div id="myDiv"></div>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>

Related

Plotly.js Adding Point on Click 2022

I am interested in creating an interactive plot in plotly.js, where a user can click on the plot and add a new point at that position. Essentially this means retrieving the plot coordinates of a mouse click. Most of the built-in click events deal with capturing a click on an already plotted marker, rather than an arbitrary position within the plot.
The following question deals with this issue, Plotly.js create a point on click. However, the code appears to have been made obsolete by changes to plotly.js at some point. An example of the answer to that question was demo'd on codepen. It appears that the xy mouse position detection now only updates within the toolbar region. My guess is that there was a renaming in the various components that make up the plot.
Link to non-functioning code from the comments of that answers:
https://codepen.io/circleoncircles/pen/abObLLE
var traces = [{
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
mode: 'markers',
type: 'scatter'
}];
traces.push({
x: [2, 3, 4, 5],
y: [16, 5, 11, 9],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [1, 2, 3, 4],
y: [12, 9, 15, 12],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [],
y: [],
mode: 'markers',
type: 'scatter'
});
var myPlot = document.getElementById('myPlot')
Plotly.newPlot('myPlot', traces, {hovermode: 'closest'});
Number.prototype.between = function(min, max) {
return this >= min && this <= max;
};
Plotly.d3.select(".plotly").on('click', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
Plotly.extendTraces(myPlot, {
x: [
[x]
],
y: [
[y]
]
}, [3]);
}
});
Plotly.d3.select(".plotly").on('mousemove', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
console.log("Location X:"+x+" Y"+y)
document.getElementById("xvalue").value = x;
document.getElementById("yvalue").value = y;
}
});
Does anybody have an updated method for this?
I think I've got it. I would still consider this relatively hacky, so if someone else has a better way to handle it, I'm all ears and would be happy to assign a better answer.
All you really need to do is get the bounding box of the element that represents the plot field. It appears that one such element is that with the class gridlayer. Then I grabbed its bounding rect.
So the computation lines in the above would become:
var bgrect = document.getElementsByClassName('gridlayer')[0].getBoundingClientRect();
var x = ((e.x - bgrect['x']) / (bgrect['width'])) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.y - bgrect['y']) / (bgrect['height'])) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1];
One additional change is necessary if you want to use the latest version of plotly. d3 is no longer embedded in plotly, so it's necessary to get d3 from CDN (the most recent version I found to work for this is v5).
<script src="https://cdn.plot.ly/plotly-2.11.0.min.js"></script>
<script src="//d3js.org/d3.v5.min.js"></script>
In the script you just reference d3 at the root then:
d3.select(".plotly").on('click', function(d, i) {
var e = d3.event;
I revised the codepen example that was posted previously to this new method https://codepen.io/jranalli/pen/eYyVVgr

plotly colorscale in scatter data plot

Using marker:{color:x} in javascript plotly (http://jsfiddle.net/d8bt1qof/), I can color-code my
data:
But how can I change the colorscale?
Different colorscales seems to be available (https://plotly.com/javascript/colorscales/), but the usage is only explained for heatmap plots. And adding colorscale: 'Portland' seems not to work.
scattergl trace markers can also have a colorschale. I found a reference for it in the documentation here:
colorscale
Parent: data[type=scattergl].marker
Type: colorscale
Sets the colorscale. Has an effect only if in marker.coloris set to a numerical array. The colorscale must be an array containing arrays mapping a normalized value to an rgb, rgba, hex, hsl, hsv, or named color string. At minimum, a mapping for the lowest (0) and highest (1) values are required. For example, [[0, 'rgb(0,0,255)'], [1, 'rgb(255,0,0)']]. To control the bounds of the colorscale in color space, usemarker.cmin and marker.cmax. Alternatively, colorscale may be a palette name string of the following list: Greys,YlGnBu,Greens,YlOrRd,Bluered,RdBu,Reds,Blues,Picnic,Rainbow,Portland,Jet,Hot,Blackbody,Earth,Electric,Viridis,Cividis.
So an example based on your fiddle you could look like this:
var x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var trace1 = {
x: x,
y: x,
mode: 'markers',
marker: {
size: 20,
color: x,
colorscale: 'Greens'
},
};
Plotly.newPlot('myDiv', [trace1], {});
Here is an implementation for a custom colorscale based on the viridis colour scale R users will be familiar with.
var x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var y = vector_normalise(x);
var trace1 = {
x: x,
y: x,
mode: 'markers',
marker: {
colorscale: [
[0.000, "rgb(68, 1, 84)"],
[0.111, "rgb(72, 40, 120)"],
[0.222, "rgb(62, 74, 137)"],
[0.333, "rgb(49, 104, 142)"],
[0.444, "rgb(38, 130, 142)"],
[0.556, "rgb(31, 158, 137)"],
[0.667, "rgb(53, 183, 121)"],
[0.778, "rgb(109, 205, 89)"],
[0.889, "rgb(180, 222, 44)"],
[1.000, "rgb(253, 231, 37)"]
],
color: y,
size: 20,
},
};
Plotly.newPlot('myDiv', [trace1], {});
Below is my normalisation function, I have left it verbose to help with understanding. The input vec could be overwritten and returned to reduce local variables if desired.
function vector_normalise(vec) {
var vmin = Math.min(...vec);
var vmax = Math.max(...vec);
// calculate the delta to save time with big arrays
var vdelta = vmax - vmin;
// create an empty array to return
var vec_ret = [];
// push doesn't seem to like inline functions
var vnorm;
// iterate over the array/vector
vec.forEach(value => {
vnorm = (value - vmin) / vdelta;
vec_ret.push(vnorm);
})
return vec_ret
}
Edit: Turns out Viridis is one of the existing available palettes... 😉

Geometry - Calculate distance of point from line

I want to calculate the distance of a point from a line defined by 2 points.
I am using javascript and thats what I came up with using wikipedia: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
function distance(point1, point2, x0, y0) {
return ((Math.abs((point2.y - point1.y) * x0 -
(point2.x - point1.x) * y0 +
point2.x * point1.y -
point2.y * point1.x)) /
(Math.pow((Math.pow(point2.y - point1.y, 2) +
Math.pow(point2.x - point1.x, 2)),
0.5)));
}
The problem is that It doesn't seem accurate since if I enter these parameters :
alert(distance({ x: 1, y: 1 }, { x: 2, y: 2 }, 1, 0));
It returns 1/sqrt(2) instead of returning 1 (which is the distance between the point (1, 0) and the line at point (1, 1)
EDIT : I understand the code above does not do what I wanted it to do. It caulcated from a point to a line represented by 2 point but the line is INFINITE (I wanted something more like a vector which has 2 end-point)
I found the answer here
I think 1/sqrt(2) = 0.7071... is quite right. See the image:
Edit:
var board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-1, 3, 3, 0],
keepaspectratio: true,
axis: true
});
var f1 = function(x) {
return x;
};
board.create('functiongraph', [f1]);
board.create('point', [1, 1], {
size: 4,
name: '1,1'
});
board.create('point', [2, 2], {
size: 4,
name: '2,2'
});
var p1 = board.create('point', [1, 0], {
size: 4,
name: '1,0'
});
var p2 = board.create('point', [0.5, 0.5], {
size: 0,
name: '1,0'
});
var li2 = board.create('line', [p1, p2], {
straightFirst: false,
straightLast: false,
strokeWidth: 2,
dash: 2
});
<html>
<head>
<link rel="stylesheet" type="text/css" href="http://jsxgraph.uni-bayreuth.de/distrib/jsxgraph.css" />
<script type="text/javascript" src="http://jsxgraph.uni-bayreuth.de/distrib/jsxgraphcore.js"></script>
</head>
<body>
<div id="jxgbox" class="jxgbox" style="width:500px; height:600px;"></div>
</body>
</html>
To add a mathematical point of view, here follows a mathematical "proof" of why your function is correct:
The shortest distance between a point and a line is the distance on the perpendicular line to the point. In this case we know that the angle between the line and the x-axis is 45 degrees and we know that the distance from origo to (1, 0) equals 1.
Using:
... we see that your result is correct, since sin(angle) = the opposite side of a right-angled triangle / hypotenuse.

Weird chart maximum axis x value (Real Challenge)

Here you can see a chart created using graphael. http://jsfiddle.net/aNJxf/4/
It is shown with it's y axis correctly.
The first y value is 0.03100 and the maximum value at y axis is at 0.031
If we change the value to 0.03104 the maximum value at y axis becomes 1.03 and now all our points are in the bottom.
If we add another 0.00001, which makes that value 0.03105, the maximum at the axis y becomes 0.531 and now our points are shown at the wrong position of the chart.
It seems that something is going wrong while graphael calculates the maximum y axis value.
Why this happens? And how we can fix that?
The code that I have pasted there is
var r = Raphael("holder"),
txtattr = { font: "12px sans-serif" };
var x = [], y = [], y2 = [], y3 = [];
for (var i = 0; i < 1e6; i++) {
x[i] = i * 10;
y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3;
}
var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]];
var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]];
var xVals =[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58];
var yVals = [0.03100,0.02259,0.02623,0.01967,0.01967,0.00788,0.02217,0.0137,0.01237,0.01764,0.0131,0.00942,0.0076,0.01463,0.02882,0.02093,0.02502,0.01961,0.01551,0.02227,0.0164,0.0191,0.00774,0.03076,0.0281,0.01338,0.02763,0.02334,0.00557,0.00023,0.01523,0.0263,0.03077,0.02404,0.02492,0.01954,0.01954,0.02337,0.01715,0.02271,0.00815,0.01343,0.00985,0.01837,0.00749,0.02967,0.01156,0.0083,0.00209,0.01538,0.01348,0.01353];
//r.text(160, 10, "Symbols, axis and hover effect").attr(txtattr);
var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines.symbols.attr({ r: 3 });
Thanks
Sorry, I'm not real familiar with gRaphael, but I did find that converting your yVals into whole numbers (by multiplying each by 1e5) seemed to rid your chart of the awkward behavior.
This suggests that it could be related to the algorithm gRaphael uses to find the max axis value (as you ask in your related question) when your values are small decimal values (and alter at even more significant digits).
I know there are inherent issues with float precision, but I can't be sure that applies to your case, or that your values are low enough to consider this.
Not the best workaround, but if it would be feasible for you, you could display the yValues in an order of magnitude larger, and remind the viewer that they are actually smaller than presented. For example, your chart could go from 0 to 3100 and remind your viewer that the scale is scale * 1e-5.

Add noise to chart (Google charts api)

I have to display around 20 lines within a line chart with Google's Line Chart. It might happen, that these lines overlap. What is the best way to add noise to the data so all the lines are visible.
the values for cat1, cat2 and cat3 are the same but I want that the it is evident from the image, that they are really close - so my idea is that the lines should not overlap but be a bit apart. The user cannot assume, that all the values overlap since for some events, let's say D, the values might be missing.
Given this chart:
function drawVisualization() {
// Create and populate the data table.
var data = google.visualization.arrayToDataTable([
['x', '#1', '#2', '#3'],
['A', 1, 1, 1],
['B', 2, 2, 2],
['C', 3, 3, 3],
['D', 4, 4, 4],
['E', 5, 5, 5],
['F', 6, 6, 6],
['G', 7, 7, 7],
['H', 8, 8, 8],
['I', 9, 9, 9],
]);
// Create and draw the visualization.
new google.visualization.LineChart(document.getElementById('visualization')).
draw(data, {width: 500, height: 400,
vAxis: {maxValue: 10}}
);
}
One way is just to add a consistent +/- per series:
function drawVisualization() {
// Create and populate the data table.
var data = google.visualization.arrayToDataTable([
['x', '#1', '#2', '#3'],
['A', 1, 1, 1],
['B', 2, 2, 2],
['C', 3, 3, 3],
['D', 4, 4, 4],
['E', 5, 5, 5],
['F', 6, 6, 6],
['G', 7, 7, 7],
['H', 8, 8, 8],
['I', 9, 9, 9],
]);
for (var i = 1;i < data.getNumberOfColumns();i++) {
// Algorithm to add +/- 0.1 for each series
var dither = Math.round((i - 1)/2)/5;
if ( (i - 1) % 2 == 0 ) {
dither = dither * -1;
}
for (var j = 0;j < data.getNumberOfRows();j++){
// Add dither to series to display differently, but keep same data for tooltip
data.setCell(j, i, data.getValue(j, i) + dither, data.getValue(j, i) + '', undefined)
}
}
// Create and draw the visualization.
new google.visualization.LineChart(document.getElementById('visualization')).
draw(data, {width: 500, height: 400,
vAxis: {maxValue: 10}}
);
}
The issue with this method is that if the values of your axes or data change significantly, you won't be able to see the gap (because the resolution of the screen won't be big enough). To get around this issue, we would need to manually set the min/max values of the axes in order to be able to come up with an appropriate factor. For instance, from this answer we can take the following algorithm to determine min and max axes values that approximate what google will set for us automagically:
// Take the Max/Min of all data values in all graphs
var totalMax = 345;
var totalMin = -123;
// Figure out the largest number (positive or negative)
var biggestNumber = Math.max(Math.abs(totalMax),Math.abs(totalMin));
// Round to an exponent of 10 appropriate for the biggest number
var roundingExp = Math.floor(Math.log(biggestNumber) / Math.LN10);
var roundingDec = Math.pow(10,roundingExp);
// Round your max and min to the nearest exponent of 10
var newMax = Math.ceil(totalMax/roundingDec)*roundingDec;
var newMin = Math.floor(totalMin/roundingDec)*roundingDec;
// Determine the range of your values
var range = newMax - newMin;
// Define the number of gridlines (default 5)
var gridlines = 5;
// Determine an appropriate gap between gridlines
var interval = range / (gridlines - 1);
// Round that interval up to the exponent of 10
var newInterval = Math.ceil(interval/roundingDec)*roundingDec;
// Re-round your max and min to the new interval
var finalMax = Math.ceil(totalMax/newInterval)*newInterval;
var finalMin = Math.floor(totalMin/newInterval)*newInterval;
We can add this all together, and see that it will even work if all the values are increased in factor by 10 (which wouldn't have worked with the hard-coded version):
function drawVisualization() {
// Create and populate the data table.
var data = google.visualization.arrayToDataTable([
['x', '#1', '#2', '#3'],
['A', 10, 10, 10],
['B', 20, 20, 20],
['C', 30, 30, 30],
['D', 40, 40, 40],
['E', 50, 50, 50],
['F', 60, 60, 60],
['G', 70, 70, 70],
['H', 80, 80, 80],
['I', 90, 90, 90],
]);
// Get max and min values for the data table
var totalMin = data.getValue(0,1);
var totalMax = data.getValue(0,1);
for (var i = 1;i < data.getNumberOfColumns();i++) {
for (var j = 0;j < data.getNumberOfRows();j++){
if ( data.getValue(j, i) < totalMin ) {
totalMin = data.getValue(j, i);
}
if ( data.getValue(j, i) > totalMax ) {
totalMax = data.getValue(j, i);
}
}
}
// Calculate grid line axes and min/max settings
// Figure out the largest number (positive or negative)
var biggestNumber = Math.max(Math.abs(totalMax),Math.abs(totalMin));
// Round to an exponent of 10 appropriate for the biggest number
var roundingExp = Math.floor(Math.log(biggestNumber) / Math.LN10);
var roundingDec = Math.pow(10,roundingExp);
// Round your max and min to the nearest exponent of 10
var newMax = Math.ceil(totalMax/roundingDec)*roundingDec;
var newMin = Math.floor(totalMin/roundingDec)*roundingDec;
// Determine the range of your values
var range = newMax - newMin;
// Define the number of gridlines (default 5)
var gridlines = 5;
// Determine an appropriate gap between gridlines
var interval = range / (gridlines - 1);
// Round that interval up to the exponent of 10
var newInterval = Math.ceil(interval/roundingDec)*roundingDec;
// Re-round your max and min to the new interval
var finalMax = Math.ceil(totalMax/newInterval)*newInterval;
var finalMin = Math.floor(totalMin/newInterval)*newInterval;
// Calculate Dither
for (var i = 1;i < data.getNumberOfColumns();i++) {
// Algorithm to add +/- 0.1 for each series
var dither = Math.round((i - 1)/2)/(10/newInterval);
if ( (i - 1) % 2 == 0 ) {
dither = dither * -1;
}
for (var j = 0;j < data.getNumberOfRows();j++){
// Add dither to series to display differently, but keep same data for tooltip
data.setCell(j, i, data.getValue(j, i) + dither, data.getValue(j, i) + '', undefined)
}
}
// Create and draw the visualization.
new google.visualization.LineChart(document.getElementById('visualization')).
draw(data, {width: 500, height: 400,
vAxis: {minValue: finalMin, maxValue: finalMax}}
);
}

Categories