Slow rendering of image + slider with Plotly JS - javascript

I'm trying to plot an image/heatmap with a slider that will change the opacity (of the heatmap), and a second slider that will modify a custom parameter on each "onchange" event.
Once this image/heatmap is rendered, no computation should be done, and moving the sliders should be instantaneous. But from what I have tried, moving one slider is very slow (1 second lag between each position), and uses max CPU %.
I'm looking for a JS-only solution (no Python for this part).
How to make a faster slider rendering with Plotly JS?
var z = [], steps = [], i;
for (i = 0; i < 500; i++)
z.push(Array.from({length: 600}, () => Math.floor(Math.random() * 100)));
for (i = 0; i < 100; i++)
steps.push({ label: i, method: 'restyle', args: ['line.color', 'red']});
var data = [{z: z, colorscale: 'YlGnBu', type: 'heatmap'}];
var layout = {title: '', sliders: [{
pad: {t: 5},
len: 1,
x: 0,
currentvalue: {
xanchor: 'right',
prefix: 'i: ',
font: {
color: '#888',
size: 20
}
},
steps: steps
}]};
Plotly.newPlot('myDiv', data, layout);
<script src="https://cdn.plot.ly/plotly-2.16.2.min.js"></script>
<div id="myDiv"></div>

This is because you are using method: 'restyle' with the wrong args, ie. ['line.color', 'red'] the syntax is not correct and there is no line so I guess Plotly (without knowing what to restyle exactly) just redraws the whole plot whenever the slider moves, which is slow.
Basically, you can use the same slider configuration in javascript and in python for the same task (in the end the same Plotly.js slider component will be used).
For example, one can set the opacity of an image according to the slider's position, but for the changes to be applied instantly one needs to set the proper method and args in the slider' steps configuration, excactly as explained in this post :
steps.push({
label: i,
execute: true,
method: 'restyle',
args: [{opacity: i/100}]
});
Here is a full example with two sliders : one that changes the opacity of the heatmap and another one that doesn't touch the plot but only triggers a specific handler :
const z = [];
for (let i=0; i<500; i++) {
z.push(Array.from({length: 600}, () => Math.floor(Math.random() * 100)));
}
const data = [{z: z, colorscale: 'YlGnBu', type: 'heatmap'}];
// Steps for the heatmap opacity slider
const opacity_steps = [];
for (let i = 0; i <= 100; i++) {
opacity_steps.push({
label: i + '%',
execute: true,
method: 'restyle',
args: [{opacity: i/100}]
});
}
// Steps for the custom slider
const custom_steps = [];
for (let i = 50; i <= 200; i++) {
custom_steps.push({
label: i,
execute: false,
method: 'skip',
});
}
const layout = {
title: '',
sliders: [{
name: 'opacity_slider',
steps: opacity_steps,
active: 100,
pad: {t: 30},
currentvalue: {prefix: 'opacity: '}
}, {
name: 'custom_slider',
steps: custom_steps,
pad: {t: 120},
currentvalue: {prefix: 'i: '}
}]
};
Plotly.newPlot('graph', data, layout);
// Retrieve the graph div
const gd = document.getElementById('graph');
// Attach 'plotly_sliderchange' event listener to it (note that we can't specify
// which slider the handler is taking care of using a secondary selector)
gd.on('plotly_sliderchange', function(event) {
// ... so we have to distinguish between the two sliders here.
if (event.slider.name != 'custom_slider')
return;
const slider = event.slider; // the slider emitting the event (object)
const step = event.step; // the currently active step (object)
const prev = event.previousActive; // index of the previously active step
const value = step.value; // captain obvious was here
const index = step._index; // index of the current step
// ...
});
<script src="https://cdn.plot.ly/plotly-2.16.2.min.js"></script>
<div id="graph"></div>

Related

Two heatmaps on top of each other with opacity with Plotly.JS

This bounty has ended. Answers to this question are eligible for a +100 reputation bounty. Bounty grace period ends in 21 hours.
Basj is looking for a canonical answer:
The current answer is useful. One detail: how to have one of the layers (for example, the background layer) as an RGB image instead of just a z-axis heatmap?
I'm trying to port this answer to a 100% Plotly.JS solution.
TL;DR : how to have two heatmaps on top of eacher with an opacity slider, with Plotly.JS (no Python)?
Beginning of solution, but how to add the second trace?
const z = [];
for (let i = 0; i < 500; i++)
z.push(Array.from({ length: 600 }, () => Math.floor(Math.random() * 100)));
const data = [{ z: z, colorscale: "YlGnBu", type: "heatmap" }];
const steps = [];
for (let i = 0; i <= 100; i++)
steps.push({ label: i + "%", execute: true, method: "restyle", args: [{ opacity: i / 100 }] });
const layout = { sliders: [{ name: "slider", steps: steps, active: 100 }] };
Plotly.newPlot("graph", data, layout);
<script src="https://cdn.plot.ly/plotly-2.16.2.min.js"></script>
<div id="graph"></div>
For reference: original Python solution:
from PIL import Image
import plotly.graph_objects as go
import numpy as np
import scipy.misc
imgA = scipy.misc.face()
imgB = Image.fromarray(np.random.random(imgA.shape[:2])*255).convert('RGB')
fig = go.Figure([
go.Image(name='raccoon', z=imgA, opacity=1), # trace 0
go.Image(name='noise', z=imgB, opacity=0.5) # trace 1
])
slider = {
'active': 50,
'currentvalue': {'prefix': 'Noise: '},
'steps': [{
'value': step/100,
'label': f'{step}%',
'visible': True,
'execute': True,
'method': 'restyle',
'args': [{'opacity': step/100}, [1]] # apply to trace [1] only
} for step in range(101)]
}
fig.update_layout(sliders=[slider])
fig.show(renderer='browser')
The second trace goes into the data array as well. The thing to note is that indexing matters : the trace at index 1 is drawn above the trace at index 0, and so on.
For the slider configuration, it should be the same as in python : each step change triggers the same 'restyle' method with the same arguments, ie. Plotly.restyle(graphDiv, ...args), that is, with args such that the method call matches the signature :
Plotly.restyle(graphDiv, update [, traceIndices])
Now, the most important thing is which trace (traceIndices) the slider should target, that is, which index or which name for explicitly named traces (default is all if I'm not wrong), but again here it doesn't change between Python and Javascript.
Here is a full example (play around with it on codepen.io) :
// Random z data
const w = {length: 600};
const h = {length: 400};
const z0 = Array.from(h, () => Array.from(w, () => Math.floor(Math.random() * 100)));
const z1 = Array.from(h, () => Array.from(w, () => Math.floor(Math.random() * 100)));
// Initial opacity for the trace 'above'
const op_init = 0.5;
const data = [
// Nb. Trace 1 drawn on top of trace 0
{type: 'heatmap', z: z0, colorscale: 'Greys'}, // trace 0
{type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // trace 1
];
// Steps for the opacity slider
const steps = [];
const n_steps = 100; // number of steps above step 0
for (let i = 0; i <= n_steps; i++) {
steps.push({
label: i + '%',
execute: true,
method: 'restyle',
args: [{
opacity: i/n_steps
}, [1]] // <- Nb. this applies only to trace 1
});
}
const layout = {
width: 600,
sliders: [{
steps: steps,
active: Math.round(op_init * n_steps), // slider default matches op_init
pad: {t: 30},
currentvalue: {prefix: 'opacity: '}
}]
};
Plotly.newPlot('plot', data, layout);
Image vs Heatmap
A Heatmap works only with single channel data (individual value-to-color mappings according to a given colorscale).
When working with rgb (or rgba, rgba256, hsl, hsla), one has to use the image type. The difference is that z must be a 2-dimensional array in which each element is an array of 3 or 4 numbers representing a color (the colormodel should be set accordingly).
For example, setting an rgb image made of noise as the background layer :
const z0 = Array.from(h, () => Array.from(w, () => ['r', 'g', 'b'].map(() => Math.floor(Math.random() * 255)) ));
// ...
const data = [
{type: 'image', z: z0, colormodel: 'rgb'}, // trace 0
{type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // trace 1
];
Here a second example where we have an rgb[a] image (DOM object img) and its pixel data represented as a 1-dimensional Uint8Array (uint8Arr), which need to be converted in 2d :
const z0 = [];
const nChannels = uint8Arr.length / img.width / img.height;
const chunkSize = uint8Arr.length / img.height;
const z0_model = nChannels === 4 ? 'rgba' : 'rgb';
for (let i = 0; i < uint8Arr.length; i += chunkSize) {
const chunk = uint8Arr.slice(i, i + chunkSize);
const row = [];
for (let j = 0; j < chunk.length; j += nChannels)
row.push(chunk.slice(j, j + nChannels));
z0.push(row);
}
// ...
const data = [
{type: 'image', z: z0, colormodel: z0_model}, // trace 0
{type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // trace 1
];
Nb. When you plot an image, the yaxis is automatically reversed (unless specified otherwise, which would display the image upside down). This affects the orientation of the heatmap y-labels, as they're on the same plot, but only the labels not the data.
Here is the layout settings ensuring that both traces share the same aspect ratio and that the image is oriented correctly :
const layout = {
// ...
xaxis: {anchor: 'y', scaleanchor: 'y', constrain: 'domain'},
yaxis: {anchor: 'x', autorange: 'reversed', constrain: 'domain'},
};

Spurious point is being added in the end C3 charts

I'm using C3 charts library to draw charts. I send data to the chart using two arrays, which are 'timeArray' and 'dataArray', one for the X-Axis and the other one for Y-Axis respectively. This simple logic was working fine.
Later I had to implement a change such that I had to take average of every three elements of an array and then make a new array and then plot the graph using averaged values.
I started facing a problem that a spurious point was being plotted on the graph. Whenever this error occurs, only one spurious point is added in the end. I've checked the arrays that are used to plot the graph, they do not have that spurious point. When I take the average of every three elements, I face this problem almost every time, however when I take average of 500 or 1000 points I face this error only sometimes.
As you can see in the code I have already tried removing the last point of the final array since the spurious point that was being added was always the last point in the chart. I've also tried changing the graph type, it did not help.
socket.on('get-avg-graph', function(data) {
// dataPoints = Points for Y-Axis
// mili = Points for X-Axis
var dataPoints = data.dataPoints;
var mili = data.mili;
var sumX = 0;
var sumY = 0;
var avgXGraph = 0;
var avgYGraph = 0;
var avgXArray = [];
var avgYArray = [];
for (var i = 0; i < dataPoints.length - 999; i++) {
for (var j = i; j < i + 999; j++) {
sumX = sumX + mili[j];
sumY = sumY + dataPoints[j];
}
if (sumY !== 0) {
avgXGraph = ( sumX / 1000 );
avgXArray.push(avgXGraph);
avgYGraph = ( sumY / 1000 );
avgYArray.push(avgYGraph);
sumX = 0;
sumY = 0;
avgXGraph = 0;
avgYGraph = 0;
}
}
io.emit('get-avg-graph-response', avgXArray, avgYArray);
});
socket.on('get-avg-graph-response', function(avgXArray, avgYArray) {
plot_X_axis = [];
plot_Y_axis = [];
drawChart();
avgXArray.splice( -1, 1);
avgYArray.splice( -1, 1);
plot_X_axis.push.apply(plot_X_axis, avgXArray);
plot_Y_axis.push.apply(plot_Y_axis, avgYArray);
drawChart();
});
function drawChart() {
var graphTitle = $("#test_type_show").val();
dataArray = [];
dataArray[0] = "PRESSURE";
dataArray.push.apply(dataArray, plot_Y_axis);
timeArray = [];
timeArray[0] = "TIME";
timeArray.push.apply(timeArray, plot_X_axis);
if (chart==null) {
chart = c3.generate({
bindto: '#chart1',
title: {
text: graphTitle
},
data: {
x: 'TIME',
columns: [
timeArray,
dataArray
],
type: 'spline'
},
axis: {
x: {show:false},
y: {show: true}
},
grid: {
x: {
show: true
},
y: {
show: true
}
},
point: {
show: false
}
});
} else {
chart.load({
x: 'TIME',
columns: [
timeArray,
dataArray
],
type: 'spline'
});
}
chart.internal.xAxis.g.attr('transform', "translate(0," + chart.internal.y(0) + ")");
chart.internal.yAxis.g.attr('transform', "translate(" + chart.internal.x(0) + ", 0)");
}
I expect the output of the code to be the actual graph without any spurious data added anywhere.

jqxChart with relative values

I was playing around with the waterfall series of the jqxChart.
According to its API, the following piece of code defines the values of the axis, in this case it's the y-axis:
valueAxis:
{
title: {text: 'Population<br>'},
unitInterval: 1000000,
labels:
{
formatFunction: function (value) {
return value / 1000000 + ' M';
}
}
}
Is it possible to define the intervals not with absolute values, but with relative values. So that the interval are e.g. 10% and the overall value is 100%?
Simply doing unitInterval: '10%' doesn't work.
This is how it should look like:
Here is a fiddle.
I think you're looking for these options :
logarithmicScale: true,
logarithmicScaleBase: 1.10,
Example:
valueAxis:
{
title: {text: 'Population<br>'},
logarithmicScale: true,
logarithmicScaleBase: 1.10,
labels:
{
formatFunction: function (value) {
return value / 1000000 + ' M';
}
}
},
Edit:
var accuracy = 2;
var first = data[0].population;
var last = data[data.length - 2].population;
var unit = (100 / last);
// convert raw data to differences
for (var i = 0; i < data.length - 2; i++)
data[i].population = (data[i].population * unit).toFixed(accuracy);

Chart.js turn labels into links

I'm not sure this is possible without doing something along the line of: Create links in HTML canvas but let's make sure.
Is there a way to (relatively) simply turn Chart.js labels into links? The chart in question is the radar chart: http://www.chartjs.org/docs/#radar-chart
(So far I've been using the legend for that, works fine with a little library modification, but now I should use the labels themselves.)
You can listen to the click event and then loop through all the pointLabels check if the click is in that box. If this is the case you get the corresponding label from the array containing all the labels.
Then you can open use window.location = link or window.open(link) to go to your link.
Example that searches the color on google on click:
const options = {
type: 'radar',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'orange'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'pink'
}
]
},
options: {
onClick: (evt, activeEls, chart) => {
const {
x,
y
} = evt;
let index = -1;
for (let i = 0; i < chart.scales.r._pointLabelItems.length; i++) {
const {
bottom,
top,
left,
right
} = chart.scales.r._pointLabelItems[i];
if (x >= left && x <= right && y >= top && y <= bottom) {
index = i;
break;
}
}
if (index === -1) {
return;
}
const clickedLabel = chart.scales.r._pointLabels[index];
window.open(`https://www.google.com/search?q=color%20${clickedLabel}`); // Blocked in stack snipet. See fiddle
console.log(clickedLabel)
}
}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.js"></script>
</body>
Fiddle: https://jsfiddle.net/Leelenaleee/fnqr4c7j/22/
It's really late but bencekd's answer and provided code to the very similar how to "Add Link to X-Label Chart.js" solved it for me.
Note: I'm forced to use the old Chart.js v1 and it's for a bar chart, not radar. You'll have to modify it accordingly as per the current version Chart.js. I also don't see why it shouldn't work for a radar with a little refactoring.
Code (one minor chang by me, comments by "/*" indicate my lines):
$("#canvas").click(
function(evt){
var ctx = document.getElementById("canvas").getContext("2d");
// from the endPoint we get the end of the bars area
var base = myBar.scale.endPoint;
var height = myBar.chart.height;
var width = myBar.chart.width;
// only call if event is under the xAxis
if(evt.pageY > base){
// how many xLabels we have
var count = myBar.scale.valuesCount;
var padding_left = myBar.scale.xScalePaddingLeft;
var padding_right = myBar.scale.xScalePaddingRight;
// calculate width for each label
var xwidth = (width-padding_left-padding_right)/count;
// determine what label were clicked on AND PUT IT INTO bar_index
var bar_index = (evt.offsetX - padding_left) / xwidth;
// don't call for padding areas
if(bar_index > 0 & bar_index < count){
bar_index = parseInt(bar_index);
// either get label from barChartData
//console.log("barChartData:" + barChartData.labels[bar_index]);
// or from current data
/* barChartData didn't work for me, so disabled above */
var ret = [];
for (var i = 0; i < myBar.datasets[0].bars.length; i++) {
ret.push(myBar.datasets[0].bars[i].label)
};
console.log("current data:" + ret[bar_index]);
// based on the label you can call any function
/* I made to go where I wanted here with a window.location.href
and taking the label (ret[bar_index]) as an argument */
}
}
}
);
And their provided Fiddle.

Long array of small data points is rendered as a jagged line

The Goal
I'm attempting to render a long series of data (around 200 ticks, from small float values like 1.3223) into a line chart.
The Issue
When I use a series of data that changes only a small amount (around 0.0001 every tick), the chart is rendered as very jagged (scissor like). I would like to somehow fix it to have a "saner" radius between each point on the graph.
A Good Example
On the other hand, when rendering higher values (around 1382.21) with bigger difference between ticks (from 0.01 to 0.05 +/-) the graph is rendered more smooth and aesthetically pleasing.
Edit: As user Arie Shaw pointed out, the actual low or high values don't make a difference and it remains an issue of representing small "monotonous" changes is a less jagged form.
The Code
var initChart = function(data, container) {
new Highcharts.Chart({
chart: {
type: "area",
renderTo: container,
zoomType: 'x'
},
title: {
text: ''
},
xAxis: {
labels: {
enabled: false
}
},
yAxis: {
title: {
text: ''
}
},
legend: {
enabled: false
},
color: '#A3D8FF',
plotOptions: {
area: {
fillColor: '#C6E5F4',
lineWidth: 1,
marker: {
enabled: false
},
shadow: false,
states: {
hover: {
lineWidth: 1
}
},
threshold: null
}
},
exporting: {
enabled: false
},
series: [{
name: "TEST",
data: data
}]
});
};
Both graphs, and sample data sets are presented in the following fiddle:
http://jsfiddle.net/YKbxy/2/
The problem you're experiencing is unavoidable: If you have a lot of small variations over time, the graph is going to appear jagged provided that you show each data point.
The key point is that last phrase.
One way to 'smooth out' the rough parts would be to average the data. For example:
var myData = []; //... Some array of data; assuming just numbers
var averageData = function (data, factor) {
var i, j, results = [], sum = 0, length = data.length, avgWindow;
if (!factor || factor <= 0) {
factor = 1;
}
// Create a sliding window of averages
for(i = 0; i < length; i+= factor) {
// Slice from i to factor
avgWindow = data.slice(i, i+factor);
for (j = 0; j < avgWindow.length; j++) {
sum += avgWindow[j];
}
results.push(sum / avgWindow.length)
sum = 0;
}
return results;
};
var var initChart = function(data, container) {
new Highcharts.Chart({
series: [{
name: "TEST",
data: averageData(myData, 2)
}]
});
});
This method also has the advantage that you could (potentially) reuse the function to compare the averaged data to the regular data, or even toggle between how much to average the data.
You can always use areaspline instead of area, see: http://jsfiddle.net/YKbxy/3/
why dont you treat you .00001 data as 1, so times 10000, and then write it in your legend like that.
You should even do that as a test, since if the chart looks fine then, it means there is a problem in the dataset numbers when you return it to normal, since high charts takes the difference between high and low...
Either you must approximate your data by only using a few decimal places or you must average out the values using something like:
var data = new Array(200);
var smallArray = new Array(5);
var averagedData = new Array(20);
for (var index=0; index<=averagedData.length; index++){
for(var i = 0; i<=smallArray.length; i++){
smallArray[i] = data[i + index * 5];
}
averagedData[index] = (smallArray[1] + smallArray[2] + smallArray[3] + smallArray[4] + smallArray[5])/smallArray.length;
}
Then you will only need to plot 20 averaged points on an array of 200 data points. You can change the values for what you need.
In the end the issue is in the frequency of the points or their plotting on yAxis.
When I provide more realistic positioning (e.g timestamp) it will look good.
Meaning that jaggedness is a result of the small changes over constant yAxis progression, which is most similar to nt3rp's answer

Categories

Resources