Skip to content

ExamplesShowcases

Large data

A single 50,000-point series, rendered live. LTTB downsampling is on by default past 5,000 points per series — toggle it off to draw every point and compare the render time. Downsampling is render-side only: tooltips, events, and the accessibility data table always work against the full data.

The essentials of the source (data generation elided — any [number, number | null][] works):

ts
import { createChart } from '@chartcraft/core';

declare const points: [number, number | null][]; // 50,000 epoch-ms samples

const chart = createChart(document.querySelector<HTMLElement>('#chart')!, {
  type: 'line',
  title: 'Server load — 50,000 points',
  subtitle: 'One sample per minute for ~35 days',
  data: {
    series: [{ id: 'load', name: 'CPU load (%)', data: points }],
  },
  // Numeric epoch-ms x-values are far lighter than 50k Date objects, but
  // they infer a linear axis — request the time axis explicitly.
  xAxis: { type: 'time' },
  yAxis: { label: 'Load (%)', min: 0, max: 100 },
  downsample: { enabled: true }, // the default; threshold defaults to 5000
  animation: false,
});

// Measure a render: start the clock, change options, listen for `render`.
let t0 = performance.now();
chart.on('render', () => {
  console.log(`rendered in ${(performance.now() - t0).toFixed(1)} ms`);
});

function setDownsampling(enabled: boolean) {
  t0 = performance.now();
  chart.update({ downsample: { enabled } });
}
vue
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { Chart } from '@chartcraft/vue';
import type { ChartInstance, ChartOptions } from '@chartcraft/vue';

declare const points: [number, number | null][]; // 50,000 epoch-ms samples

const downsampleOn = ref(true);
const renderMs = ref<number | null>(null);
const chartRef = ref<{ chart: ChartInstance | null } | null>(null);
let t0 = performance.now();

const options = computed<ChartOptions>(() => ({
  type: 'line',
  title: 'Server load — 50,000 points',
  data: { series: [{ id: 'load', name: 'CPU load (%)', data: points }] },
  xAxis: { type: 'time' },
  yAxis: { label: 'Load (%)', min: 0, max: 100 },
  downsample: { enabled: downsampleOn.value },
  animation: false,
}));

let off: (() => void) | undefined;
onMounted(() => {
  off = chartRef.value?.chart?.on('render', () => {
    renderMs.value = performance.now() - t0;
  });
});
onBeforeUnmount(() => off?.());

function toggle() {
  t0 = performance.now();
  downsampleOn.value = !downsampleOn.value;
}
</script>

<template>
  <button type="button" @click="toggle">
    {{ downsampleOn ? 'Disable' : 'Enable' }} downsampling
  </button>
  <span v-if="renderMs !== null">last render {{ renderMs.toFixed(1) }} ms</span>
  <Chart ref="chartRef" :options="options" style="height: 360px" />
</template>

Why this stays smooth — canvas rendering, allocation-free hot paths, and the LTTB algorithm that preserves peaks, troughs, and outliers while dropping only visually redundant points — is covered in Performance. The algorithm itself is exported as downsampleLTTB for your own pipelines.