DEV Community

Aaron Estrada Poggio
Aaron Estrada Poggio

Posted on

How to Make Large Time-Series Charts Smooth in Vue.js + ApexCharts (and fix Zoom & Scroll behavior issues)

Over the last few weeks, I've been working on a front-end application project built with Vue.js and ApexCharts that displays time-series data about different sensors installed in different rooms and buildings.

ApexCharts is an excellent library for creating interactive charts, and integrating it in [Vue.js (https://vuejs.org) is really a piece of cake. However, when it comes to displaying a time-series chart with thousands of points, the performance can suffer, sometimes causing the page to freeze during the rendering or when the user zooms or navigates through the data.

Downsampling data

To solve this issue, it is recommended to downsample the data to reduce the number of displayed points, especially when the selected time range is large enough that highly granular data is unnecessary. If the user later needs to inspect detailed data, the application can switch back to raw data after zooming in.

The criteria for switching between downsampled and raw data depends on the use case. It can be based on the zoom level, the selected date range, or the number of points to display.

The problem

Switching between raw and downsampled data also requires filtering the points to display according to the selected time range. And this is where the problem appears: once you filter data, the zooming and scrolling events of the chart stop working properly when using the mouse scroll. In the era of AI and agent-based coding, solving the issue might seem trivial. But after several attempts and different interactions, I could not find a complete working example that fully
solved the problem.

The solution

Here, I would like to propose a solution that includes the use of downsampled data when the number of displayed points is high enough to avoid performance issues with a line chart and switches to raw data otherwise. The solution is based on the zoomed, scrolled and beforeResetZoom chart events while still relying on the reactive state of the Vue.js component.

To make the solution work correctly, it is also necessary to include a hidden dataset containing the minimum and maximum dates of the complete time series, both with null values. This hidden dataset allows the chart to zoom out completely and pan across the entire x-axis using both the chart controls and the mouse wheel.

Without this additional dataset, these interactions only work correctly when using the built-in chart buttons.

How to do it?

The setup

In a real application, the data usually comes from an API or another external source. After loading the data, each dataset can be downsampled and stored in the component’s reactive state.

Alternatively, downsampling could also be performed in the backend before sending the data to the frontend, depending on how much control you have over the application architecture.

For this example, I generate three random temperature datasets with 10,000 points each and downsample them using the Largest-Triangle Three-Buckets (LTTB) algorithm provided by the downsample package, reducing each dataset to 700 points.

import {LTTB} from 'downsample/methods/LTTB';

let northStationData = generateTemperaturePoints({
    startDate: new Date('2026-01-01T00:00:00'),
    count: 10000,
    intervalMs: 60 * 1000,
    baseTemp: 15,
    dailyAmplitude: 6.5,
    noise: 1.1,
    warmingTrendPerHour: 0.0021
});

let northStationLTTB = LTTB(northStationData, 700);
Enter fullscreen mode Exit fullscreen mode

We also need to store the minimum and maximum dates across all datasets, since they will later be used to create the hidden dataset.

const allX = [northStationData, cityCenterData, southStationData].flatMap(s => s.map(p => p.x));

let minimumDate = Math.min(...allX);
let maximumDate = Math.max(...allX);

this.ranges = {
    minimumDate: minimumDate,
    maximumDate: maximumDate,
};
Enter fullscreen mode Exit fullscreen mode

Computed properties

Dataset with minimum and maximum dates

Once the minimum and maximum dates are stored in the reactive state, we can create a computed property that returns a dataset containing only those boundary dates.

plotMinMaxDataset()
{
    return {
        name: 'rangeMinMaxInvisible',
        data: [
            {x: new Date(this.ranges.minimumDate).getTime(), y: null},
            {x: new Date(this.ranges.maximumDate).getTime(), y: null},
        ],
        showInLegend: false,
    };
}
Enter fullscreen mode Exit fullscreen mode

Initial datasets

When displaying the chart for the first time, we can create another computed property that returns the downsampled datasets together with the hidden boundary dataset.

plotSeries()
{
    let seriesData = [];
    Object.keys(this.data).forEach(key => {
        seriesData.push({
            name: `${this.data[key].label} (downsampled)`,
            data: toRaw(this.data[key].downsample),
        })
    });

    /**
     * Add the minimum and maximum points available in the datasets
     * to avoid zooming and scrolling to lose the minimum and maximum ranges.
     */
    seriesData.push(toRaw(this.plotMinMaxDataset));
    return seriesData;
}
Enter fullscreen mode Exit fullscreen mode

Methods

Switching between raw and downsampled data

This method is called whenever the user zooms or scrolls through the chart. It filters the data according to the selected range and switches between raw and downsampled datasets based on a defined criterion. At the end, it also includes the dataset with the minimum and maximum dates to avoid zooming and scrolling to lose the original boundaries.

In this example, the criterion is the number of points available in the filtered raw dataset.

plotRangeOnChange(chartContext, minDate, maxDate)
{
    let dataUpdate = [];

    Object.keys(this.data).forEach(key => {
        let dataTypeName = 'raw'
        let dataRaw = pointFilter(this.data[key].raw, minDate, maxDate);

        /**
         * Based on the number of points in the raw dataset after filtering,
         * decide whether to keep raw or downsampled datasets.
         *
         * The criteria in this case is to verify the number of points, but it could
         * also be the range of dates.
         */
        if (dataRaw.length > 1000) {
            dataRaw = pointFilter(this.data[key].downsample, minDate, maxDate);
            dataTypeName = 'downsampled';
        }

        dataUpdate.push({
            name: `${this.data[key].label} (${dataTypeName})`,
            data: toRaw(dataRaw),
        });
    });

    /**
     * Add the minimum and maximum points available in the datasets
     * to avoid zooming and scrolling to lose the minimum and maximum ranges.
     */
    dataUpdate.push(toRaw(this.plotMinMaxDataset));

    chartContext.updateOptions({
        series: toRaw(dataUpdate),
        xaxis: {
            min: minDate,
            max: maxDate,
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Configuring the chart

We need to configure the zoomed and scrolled events to trigger the switch between raw and downsampled data using the method defined above. We also configure the beforeResetZoom event to restore the chart to the full available date range.

These events provide:

  1. chartContext, which gives access to the chart instance.
  2. xaxis, which contains the currently visible minimum and maximum dates.

At this stage, we also hide the auxiliary hidden dataset from the legend and remove its marker by filtering the series name in the legend.formatter of the plot options and setting its marker size to 0.

let markersSize = [];
Object.keys(this.data).forEach(key => {
    markersSize.push(8);
});

markersSize.push(0);

let plotOptions = {
    chart: {
        id: 'vuechart-example',
        events: {
            /**
             * Update data based on the selected range in the x-axis when events occur
             */
            zoomed: (chartContext, {xaxis}) => this.plotRangeOnChange(chartContext, xaxis.min, xaxis.max),
            scrolled: (chartContext, {xaxis}) => this.plotRangeOnChange(chartContext, xaxis.min, xaxis.max),
            beforeResetZoom: (chartContext) => this.plotRangeOnChange(chartContext, null, null),
        },
    },
    legend: {
        formatter: function (seriesName) {
            /**
             * Hide the marker containing the minimum and maximum values in the x-axis range
             */
            return seriesName === 'rangeMinMaxInvisible' ? null : seriesName;
        },
        markers: {
            size: markersSize
        },
    },
    /* ... */
}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

In this article, I showed how to use downsampling data in a Vue.js + ApexCharts application to reduce the number of points to display and improve the user experience, while still allowing to zoom in, zoom out and scroll over the chart without encountering problems with the mouse wheel.

The complete code of this example is available in the GitHub repository vue-apexcharts-downsample-zoom.

You can also try the live demo here.

Leave a comment if you have any questions or suggestions to improve, I'll be happy to interact with you!

Let's make the world a better place together, one snippet at a time!

Top comments (0)