I'm trying to get maximum performance for my line charts with LightningChart JS. From the performance example, I learned the max point count I can run with the performance tester is 10 series x 1 million point each. I configure it like this .
I can run it with 30-40 refreshes / second.
Two questions:
Is there an easy way to test it with higher point counts? Any on-line demo/tester for that?
Is there general guidelines for optimizing the performance settings with this JavaScript Lightning Chart?
Test with more points
There isn't currently any online tester that has higher point counts than that. But the source code for the tester you have used is available in GitHub: https://github.com/Arction/lcjs-performance-tester
The repository has good instructions how to run it locally.
To add a test with more points you can edit the content/src/tests/default.js file and add a new test that has the point count you would like to test with. Let's say you would like to have 10 series with 2 million points each. You could add
{
key: '2M',
label: '2 M points',
code: (thickness) => ScrollingProgressiveLine(2 * 1000 * 1000, seriesCount, thickness.thickness, `${seriesCount} Scrolling Line Series 2 000 000 points each ${thickness.label}`),
defaultSelected: false
},
after line 718, between the 1M and 10M points tests. The last test on that list is excluded from being an actual runnable test.
There is also a showcase example that can be used to see how many points the chart can display while maintaining 60 fps. https://arction.github.io/lcjs-showcase-streamingData/
Optimizing performance
Set DataPattern based on the data you are using when creating a new series.
For line series it's set with dataPattern property in line series options. chart.addLineSeries({dataPattern: DataPatterns.horizontalProgressive})
Disable animations.
I have already answered how to disable animations in How to disable animations for charts in LightningChart JS?.
Disable mouse interactions.
To disable mouse interactions you can call .setMouseInteractions(false) on all axes, series and charts.
Disable Auto Cursor.
To do this call setAutoCursorMode with AutoCursorModes.disabled as a parameter.
Back story.
I am a web developer and i can pretty much do anything on the web. When i first started doing some programming it was in flash actionscript 3, and i was 14 and now i am 29 and still i haven't figure out this one thing. I have decided i am going to get to the bottom of it finally.
Goals.
To make the problem tangible.
Implement a carousel that can be swiped, autoplay, move with prev and next button, any of these animation should be interruptible and have velocity associated with it, except the mouse/finger tracking. I want it to feel natural, i want the elements moving to feel like they have weight and they are sliding into place. They need to feel real like they can be touched.
The animation needs to be additive, as i change directions and interact with it, it needs to be like how a human would expect it to be. Like they are sliding a piece of plastic on a smooth surface. It needs to snap into place when it is completed.
Now i know stack over flow it's all about show your work. I have done so many times and it all comes down to a mess. So a truce, please give comments and directions and i will update the question with the current progress and roadblocks. I will make and then i will keep it updated.
My Starting point
https://codesandbox.io/s/strange-darkness-3hmub
Concepts.
So here are things i think i need to be thinking about.
I believe there are really two types of animations i need to account for.
Tweens and Spring or animations with velocity (i don't know what i am doing).
interface {
type:"tween"|"velocity";
velocity:number; //unit per/second??? pixel
duration?:number;
from:number;
to:number;
start_time:number;
easing:(progress:number)=>number
}
Thought Process.
I think i need the animations to stack. So i need to know when the animation started and what type it is. I need to know if the new animation should completely remove all previous ones or if it should stack on top of it.
For example i could have stacks of all sorts of animations but if a use touches it then it needs to remove them.
I think i need the start time to do things such as easing functions. I want it to have weight and feel like it has inertia.
Contributing.
You may not be able to answer, but suggest, brain storm. I want to put this to bed!
I am committed!! Contribute however you can and i will do the work!!!
I think it can be accomplished using https://github.com/react-voodoo/react-voodoo ( beta, incomplete css interpolation support ),
Or directly using https://github.com/react-voodoo/tween-axis
tween-axis allow merging any numeric tween on a "tween-axis".
Once instantiated, we can use a classic goto function to update numeric props on a specified context object.
Even more, multiple tween-axis instance can update the same props.
let axis = new TweenAxis(
[
{
from : 0,
duration: 100,
target : "myTargetId",
easeFn:"easePolyOut", // https://github.com/d3/d3-ease or function
apply : {
value: 200
}
},
{
from : 0,
duration: 100,
target : "myTargetId",
apply : {
value: -100
}
}
]
),
context = {
myTargetId: {
value: 0
}
};
console.log(axis.go(.5, context));
//{ myTargetId: { value: 50 } }
console.log(axis.go(.25, context));
//{ myTargetId: { value: 25 } }
console.log(axis.go(.75, context));
//{ myTargetId: { value: 75 } }
I am trying to get a larger amount of data. Sample data is below
1850/01 -0.845 -0.922 -0.748 -1.038 -0.652 -1.379 -0.311 -1.053 -0.636 -1.418 -0.272
1850/02 -0.043 -0.113 0.047 -0.244 0.159 -0.613 0.528 -0.260 0.177 -0.653 0.569
1850/03 -0.698 -0.794 -0.633 -0.891 -0.506 -1.123 -0.274 -0.910 -0.495 -1.174 -0.229
……….
2016/12 0.795 0.746 0.828 0.756 0.834 0.586 1.005 0.731 0.848 0.575 1.010
2017/01 1.025 0.977 1.067 0.983 1.068 0.786 1.265 0.963 1.084 0.778 1.271
2017/02 1.151 1.098 1.198 1.112 1.191 0.957 1.346 1.089 1.208 0.946 1.352
which starts from 1850 until 2017 and every month. I am processing this dataset to use it in Highcharts like this
$.each(lines, function(index, row) {
var cells = row.split(','),
series = {
type: 'line',
data:[]
};
$.each(cells, function(itemNo, item) {
if (itemNo == 0) {
series.name = item;
} else {
series.data.push(parseFloat(item));
}
});
data.push(series);
});
And I use it in following way
chart = $('#container').highcharts({
chart: {
polar: true
},
series: data
});
This does work however, it is really really slow. How can I improve/enhance its performance so that I the highcharts gets loaded quickly without freezing the browser?
UPDATE
Here is my xAxis
xAxis: {
tickInterval: 1,
min: 0,
max: 12,
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
},
UPDATE 2
yAxis: {
min: -1,
max: 2.2,
endOnTick: false,
title: {
text: 'Temperature (°C)'
},
plotLines: [{
value: 2,
width: 1,
label: {
text: '2°C',
align: 'right',
y: 2
},
color: 'red'
}, {
value: 1.5,
width: 1,
label: {
text: '1.5°C',
align: 'right',
y: 30
},
color: 'red'
}],
},
I think this problem will require a combination of the solutions proposed by others. These include:
Condensing the data: If I understand correctly, then you have 167 years of data and 12 months per year. Each of these will be a series, for a total of >2000 series. I'm not sure this will create an interpretable graph, but it's also likely I misunderstand the nature of your data and how you plan to plot it.
Using the boost.js module of Highcharts: Highcharts normally renders its graphs as SVGs. My understanding of the boost.js module is that it causes some parts of the charts to be rendered on HTML5 canvas elements. The HTML5 canvas is much faster than SVG for large numbers of data points. See an empirical comparison here: SVG vs canvas: how to choose
Setting chart options to minimize resource requirements: I believe that the slowdown you're experiencing is unlikely to be due to the processing of your data. Rather, I think it's almost certainly due primarily to the rendering time required by Highcharts, and browser resources required to monitor events on all of the chart elements. By default, for instance, Highcharts plots allow you to "hover" your mouse over data points to highlight them, and they display tooltips with information about the data point. If you have a plot with thousands of data points, then this requires your browser to handle thousands of mouse events over the chart objects. Turning off this chart features should improve performance. In the demo below, I've turned off tooltips and data point highlighting using the mouse. I've also turned off the legend, to improve visibility of the chart.
You can process and update the data in chunks: In the long run, this will actually take more time than if you were to render the chunk all in one go, because Highcharts has to redraw the chart each time you add a new series. However, it might lead to a better user experience, since the page will appear more responsive. The demo below utilizes this type of approach. It allows you to set the number of lines of data to process per chunk (linesPerChunk) and the time delay between one chunk finishes processing and when you want to begin processing the next chunk (timeBetweenChunks). Ideally, timeBetweenChunks would be set to the time it takes Highcharts to render and display the last chunk, so that the computer alternates between processing data and rendering data, with no unproductive gaps in between. Ideally one could set this adaptively so that it's optimal across computers/users/browsers/etc., but I'm not sure how to do this; any ideas would be welcome. So for the moment it's just set to a constant 100 ms. With 2000 lines of data, 100 lines per chunk of data, and 100 ms between chunks, the whole thing should take ~2 s to load. The key function is plotMoreData(). After processing a chunk and adding the new series to the chart, it calls itself with a delay of timeBetweenChunks using window.setTimeout(plotMoreData, timeBetweenChunks). It then redraws the chart. When plotMoreData gets called the next time, it processes the next chunk, and so on. It stops when it's processed and displayed all the data and also updates the status message.
EDIT:
It seems the Highcharts boost module does not work with polar charts, and this is a known issue. A fix is described here: Polar Scatter Using Boost Module. I was able to get this fix to work by modifying boost.src.js (built from the Highcharts Github repository as follows:
At ~line 1380, I replaced:
if (!settings.useGPUTranslations) {
inst.skipTranslation = true;
x = xAxis.toPixels(x, true);
y = yAxis.toPixels(y, true);
}
with:
if (!settings.useGPUTranslations) {
inst.skipTranslation = true;
// Default (non-Polar) calculation
if( !series.chart.polar ) {
x = xAxis.toPixels(x, true);
y = yAxis.toPixels(y, true);
}
// Handle Polar chart coordinate conversion
else {
var polarPoint = {
plotX: xAxis.translate( x, 0, 0, 0, 1, series.options.pointPlacement, series.type === 'flags' ),
plotY: yAxis.translate( y, 0, 1, 0, 1 )
};
series.toXY( polarPoint );
x = Math.round( polarPoint.plotX );
y = Math.round( polarPoint.plotY );
}
}
This seemed to work. See the demo here: JSFiddle Polar Highcharts Boost Demo
Given the data you are displaying is not updating once a month, I feel like generating the chart for every view is a big waste of resources for your clients.
Indeed, it would be pretty easy for you to generate the chart without changing anything that you are doing now, but then extracting the resulting SVG and serving it simply to your visitors.
For that you simply have to use the getSVG method of HighCharts, that will return the SVG in a stringified form.
I don't really know if you want to have some sort of process to auto update the chart, but you could use a cron job for this purpose. Keep in mind that you would have to do something anyway even with your initial approach to update the data.
Regarding your script, first thing to notice is that you are using $.each, which is pretty bad in term of performance comparing to the vanilla equivalents. As demonstrated by this jsPerf, I get 3,649 Op/s with $.each whereas for loops could get you up to 202,755 Op/s which is insanely faster! Since you also doing a double loop, the difference would be ^2.
But again, since the data is not updated often, this part of the script could be removed entirely and converted into a JSON corresponding to the output of the function, that HighCharts could load directly, skipping the entire processing and transformation of your CSV.
If using HighCharts is not a requirement, you could use react-vis, which is a collection of React components focused around Data Visualization. It's build as an api on top of SVG, .
I've made a demo that you can checkout on CodePen with the same data as you to plot the monthly temperatures since 1850.
const {
HorizontalGridLines,
XAxis,
XYPlot,
YAxis,
LineMarkSeries,
} = reactVis
const color = d3.scaleLinear()
.domain([1850, 2017])
.range(['yellow', 'red'])
const Chart = () => (
<XYPlot width={window.innerWidth - 100} height={window.innerHeight - 100}>
<XAxis title='Month' />
<YAxis title='Temperature' />
<HorizontalGridLines />
{Object.keys(data).map(key => (
<LineMarkSeries color={color(key)} data={data[key]} key={key} />
))}
</XYPlot>
)
ReactDOM.render(<Chart />, document.querySelector('#root'))
I also use the scaleLinear method of d3 to see the change over the years since I thought it would be an interesting information to show, but it can be changed depending of your needs.
It's using SVGs, but we are also working on integration with webgl by the intermediary of deck.gl which would allow for even more optimizations gains, still not ready yet and I'm not sure you would really need that much, but worth noting.
Disclaimer: I currently work for Uber, which made deck.gl and react-vis.
new Array(length)
Specify the array's length rather than creating an empty array. Writing to an existing offset in an array is substantially faster than creating a new offset each time you write an item.
var data = new Array(lines.length); // Specify array length
$.each(lines, function(index, row) {
var cells = row.split(','),
series = {
type: 'line',
data: new Array(cells.length) // Specify array length
};
$.each(cells, function(itemNo, item) {
if (itemNo == 0) {
series.name = item;
} else {
series.data.push(parseFloat(item));
}
});
data.push(series);
});
I'd use underscore.js.
var result = _.map(lines, function (row) {
return { type: 'line', series: _.map(row.data, function (item, index) {
if (index >= 1)
return parseFloat(item);
}), name: row.data[0]}
});
(I'm writing this from my phone on a plane, so my apologies if there is are any typos or things I'm missing. Just trying to help!)
You don't show what is your choice for the xAxis, but if massaging the data in the server side is not an option what I would suggest is:
First do a call to load the data.
Show at spinning wheel where the chart is going to be displayed
Start parsing the dataset but don't load it all at once but in batches and redraw the chart as you parse the rest of the file.
This should avoid the freezing and give the visual indication that the data is being loaded.
Edit: Response to comment
I don't think is any longer an issue loading the data but on how to display the data in a meaningful way. Even if it loads in 2 seconds the result would be thousands of lines overlapping and totally unreadable.
You should default few hand picked values (ex Current month and same month and 1, 5, 10 years ago) and then allow the user to change the selection select up to a reasonable number of years/month (ex Compare July for 1980, 2010 and 2017) then plot those values updating the chart with (chart.series[0].update(....).
There were no columns in the dataset so I assumed it was a column per month but I see now that they seem to be one per row and 11 columns represent some other data.
I don't think for a polar chart doing averages or doing rollups of the data is the best way to go but allowing to compare specific years and month with the exact data, whatever those values are :)
Situation:
I think the best and more appropriate solution would be to process and serve chunks of data, it would synchronize the display of points on your Chart and avoid loading the whole data once on document load, even if your data isn't as large as you described it in your post title.
But after reading your question, your comments and other posts, I can see that you are getting all data from one file, and you aren't processing data on server side so server side chunks is not an option for you.
Alternative:
Well if you can't process your data on server side why don't you process it on client side, HighCharts has two options for you:
Data grouping:
You can follow Highstock Demos › 52,000 points with data grouping.
Lazy Async loading:
You can follow Highstock Demos › 1.7 million points with async loading.
The both Demos show how to fetch data by ranges and limit the chart to display only relevant range data points, which will improve time loading performances.
my team came across the similar problem but in our case we had 3 nested loops and when it was processing the data it was blocking the main thread which caused bad user experience. so i tackled this problem by deferring the processing data. which did the desired job of not blocking the main thread and did the processing much faster.
you can also use boost module developed by highchart (https://www.highcharts.com/blog/news/175-highcharts-performance-boost/)
I need to plot thousands of points, perhaps close to 50,000 with the dojo charting library. It works, but it's definitely very slow and lags the browser. Is there any way I can get better performance?
EDIT:
I solved by applying a render filter to the data. Essentially, I have a new item parameter called "render" which is set to false by my json source if the point is expected to overlap others. My DataSeries then queries for all points where render:true. This way all of the data is there still for non-visual sources that want all of the points, while my charts now run smoothly.
Psuedocode:
def is_overlapped(x, y, x_round, y_round)
rounded_x = round(x, x_round)
rounded_y = round(y, y_round)
hash = hash_xy(rounded_x, rounded_y)
if(#overlap_filter[hash].nil?)
#overlap_filter[hash] = true
return false
end
return true
end
x_round and y_round can be determined by the x and y ranges, say for example range / 100
I know this isn't probably exactly the answer you're looking for, but have you considered simply reducing the number of points you are plotting? I don't know the specific function of the graph(s), but I'd imagine most graphs with that many points are unnecessary; and no observer is going to be able to take that level of detail in.
Your solution could lie with graphing techniques rather than JavaScript. E.g. you could most likely vastly reduce the number of points and use a line graph instead of a scatter plot while still communicating similar levels of information to your intended target.