The timeseries chart is a specialized chart for displaying time-based data.
Each data point is a tuple of [timestamp_in_ms, value].
Basic Line Chart
A simple line chart displaying multiple data series over time.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Requests',
data: buildSeriesData(0, 50, 60_000, 1),
color: ChartPalette.semantic('Neutral', isDarkMode)
},
{
name: 'Errors',
data: buildSeriesData(1, 50, 60_000, 0.3),
color: ChartPalette.semantic('Attention', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart {echarts} {data} {isDarkMode} xAxisName="Time (UTC)" yAxisName="Count" /> Custom X-Axis Label Format
Use the xAxisTickLabelFormat prop to control how x-axis tick
labels are rendered. The formatter receives the raw timestamp in milliseconds
and returns a display string, overriding ECharts' built-in time formatting.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { ChartPalette, TimeseriesChart } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data = $derived([
{
name: 'Requests',
data: buildSeriesData(0, 50, 60_000, 1000),
color: ChartPalette.semantic('Neutral', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart
{echarts}
{isDarkMode}
{data}
xAxisName="Time (UTC)"
yAxisName="Requests"
xAxisTickFormat={(timestamp) => {
const date = new Date(timestamp);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}}
yAxisTickFormat={(value) => {
if (value >= 1000) return `${value / 1000}k`;
return value.toString();
}}
tooltipValueFormat={(value) => `${(value / 1000).toFixed(1)}k requests`}
/> Gradient Fill
Set gradient to true to render a vertical gradient
fill beneath each line series. The fill fades from the series color at the top
to transparent at the bottom, giving the chart a polished area-chart look
without losing the clarity of individual lines.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Requests',
data: buildSeriesData(0, 50, 60_000, 1),
color: ChartPalette.semantic('Neutral', isDarkMode)
},
{
name: 'Errors',
data: buildSeriesData(1, 50, 60_000, 0.3),
color: ChartPalette.semantic('Attention', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart {echarts} {isDarkMode} {data} xAxisName="Time (UTC)" yAxisName="Count" gradient /> Incomplete Data
Use the incomplete prop to indicate regions where data may be
incomplete or still being collected.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Bandwidth',
data: buildSeriesData(0, 50, 60_000, 1),
color: ChartPalette.categorical(0, isDarkMode)
}
]);
const incompleteTimestamp = $derived(data[0].data[data[0].data.length - 5][0]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart
{echarts}
{isDarkMode}
{data}
xAxisName="Time (UTC)"
yAxisName="Mbps"
incomplete={{ after: incompleteTimestamp }}
/> Time Range Selection
Enable time range selection by providing the onTimeRangeChange callback. Users can click and drag on the chart to select a time range.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'CPU Usage',
data: buildSeriesData(0, 50, 60_000, 1),
color: ChartPalette.categorical(0, isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart
{echarts}
{isDarkMode}
{data}
xAxisName="Time (UTC)"
yAxisName="%"
onTimeRangeChange={(from, to) => {
alert(`Selected range:\nFrom: ${new Date(from).toLocaleString()}\nTo: ${new Date(to).toLocaleString()}`);
}}
/> Legend Highlight
Hovering a legend item highlights the corresponding series on the chart and
fades the others. Use onpointerenter and onpointerleave on ChartLegend items together with dispatchAction on the chart ref.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import type { EChartsType } from 'echarts/core';
import { ChartLegend, ChartPalette, LayerCard, TimeseriesChart } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
let chartRef: EChartsType | null = $state(null);
let hoveredSeries: string | null = $state(null);
const series = $derived([
{
name: 'P99',
color: ChartPalette.semantic('Attention', isDarkMode),
value: '124',
unit: 'ms'
},
{
name: 'P95',
color: ChartPalette.semantic('Warning', isDarkMode),
value: '76',
unit: 'ms'
},
{
name: 'P75',
color: ChartPalette.semantic('Neutral', isDarkMode),
value: '32',
unit: 'ms'
},
{
name: 'P50',
color: ChartPalette.semantic('Neutral', isDarkMode),
value: '10',
unit: 'ms'
}
]);
const data = $derived(
series.map((item, index) => ({
name: item.name,
data: buildSeriesData(3 - index, 30, 60_000, 1 - index * 0.2),
color: item.color
}))
);
function handlePointerEnter(name: string) {
hoveredSeries = name;
chartRef?.dispatchAction({ type: 'highlight', seriesName: name });
}
function handlePointerLeave(name: string) {
hoveredSeries = null;
chartRef?.dispatchAction({ type: 'downplay', seriesName: name });
}
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<LayerCard>
<LayerCard.Secondary>Read latency</LayerCard.Secondary>
<LayerCard.Primary>
<div class="mb-2 flex divide-x divide-kumo-line px-2">
{#each series as item (item.name)}
<ChartLegend
variant="large"
name={item.name}
color={item.color}
value={item.value}
unit={item.unit}
inactive={hoveredSeries !== null && hoveredSeries !== item.name}
onpointerenter={() => handlePointerEnter(item.name)}
onpointerleave={() => handlePointerLeave(item.name)}
class="not-first:pl-4"
/>
{/each}
</div>
<TimeseriesChart
bind:chartRef
{echarts}
xAxisName="Time (UTC)"
{isDarkMode}
{data}
height={300}
/>
</LayerCard.Primary>
</LayerCard> Legend Click
Clicking a ChartLegend item isolates that series, showing only it and hiding
the rest. Clicking the already-isolated series restores them all.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import type { EChartsType } from 'echarts/core';
import { ChartLegend, ChartPalette, LayerCard, TimeseriesChart } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
let chartRef: EChartsType | null = $state(null);
let hiddenSeries: Record<string, boolean> = $state({});
const series = $derived([
{
name: 'P99',
color: ChartPalette.semantic('Attention', isDarkMode),
value: '124',
unit: 'ms'
},
{
name: 'P95',
color: ChartPalette.semantic('Warning', isDarkMode),
value: '76',
unit: 'ms'
},
{
name: 'P75',
color: ChartPalette.semantic('Neutral', isDarkMode),
value: '32',
unit: 'ms'
},
{
name: 'P50',
color: ChartPalette.semantic('Neutral', isDarkMode),
value: '10',
unit: 'ms'
}
]);
const data = $derived(
series.map((item, index) => ({
name: item.name,
data: buildSeriesData(3 - index, 30, 60_000, 1 - index * 0.2),
color: item.color
}))
);
$effect(() => {
isDarkMode;
hiddenSeries = {};
});
function handleClick(name: string) {
if (!chartRef) return;
const isIsolated = series.every((item) => (item.name === name ? !hiddenSeries[item.name] : hiddenSeries[item.name]));
const nextHidden: Record<string, boolean> = {};
for (const item of series) {
const shouldHide = isIsolated ? false : item.name !== name;
nextHidden[item.name] = shouldHide;
chartRef.dispatchAction({
type: shouldHide ? 'legendUnSelect' : 'legendSelect',
name: item.name
});
}
hiddenSeries = nextHidden;
}
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<LayerCard>
<LayerCard.Secondary>Read latency</LayerCard.Secondary>
<LayerCard.Primary>
<div class="mb-2 flex divide-x divide-kumo-line px-2">
{#each series as item (item.name)}
<ChartLegend
variant="large"
name={item.name}
color={item.color}
value={item.value}
unit={item.unit}
inactive={hiddenSeries[item.name] ?? false}
onclick={() => handleClick(item.name)}
class="not-first:pl-4"
/>
{/each}
</div>
<TimeseriesChart
bind:chartRef
{echarts}
xAxisName="Time (UTC)"
{isDarkMode}
{data}
height={300}
enableLegendSelection
/>
</LayerCard.Primary>
</LayerCard> Tooltip Cursor Tracking
Use tooltipFollowCursor to choose whether the tooltip follows both cursor axes or only tracks the x position.
Set tooltipMode="single" when dense charts should show only the series nearest to the pointer.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Requests',
data: buildSeriesData(0, 72, 60_000, 1),
color: ChartPalette.semantic('Neutral', isDarkMode)
},
{
name: 'Edge errors',
data: buildSeriesData(1, 72, 60_000, 0.35),
color: ChartPalette.semantic('Attention', isDarkMode)
},
{
name: 'Cache hits',
data: buildSeriesData(2, 72, 60_000, 0.7),
color: ChartPalette.semantic('Success', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart
{echarts}
{data}
{isDarkMode}
tooltipMode="single"
tooltipFollowCursor="x"
xAxisName="Time (UTC)"
yAxisName="Events"
/> Tooltip Boundary
Set tooltipBoundary to keep the native tooltip within the chart container instead of letting it overflow the visual frame.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
let boundary: HTMLDivElement | undefined = $state();
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Origin requests',
data: buildSeriesData(0, 64, 60_000, 0.8),
color: ChartPalette.semantic('Neutral', isDarkMode)
},
{
name: 'Blocked requests',
data: buildSeriesData(1, 64, 60_000, 0.25),
color: ChartPalette.semantic('Attention', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<div bind:this={boundary} class="w-full overflow-hidden rounded-md border border-kumo-line p-3">
<TimeseriesChart
{echarts}
{data}
{isDarkMode}
tooltipBoundary={boundary}
tooltipMaxItems={4}
xAxisName="Time (UTC)"
yAxisName="Requests"
/>
</div> Bar Chart
Set type="bar" to render series as stacked bars instead of lines.
All other props — axes, tooltips, colors — work identically.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart, ChartPalette } from 'kumo-svelte';
import { buildSeriesData, getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
const data: { name: string; color: string; data: [number, number][] }[] = $derived([
{
name: 'Requests where age > 10',
data: buildSeriesData(0, 20, 3_600_000, 1),
color: ChartPalette.semantic('Neutral', isDarkMode)
},
{
name: 'Errors',
data: buildSeriesData(1, 20, 3_600_000, 0.3),
color: ChartPalette.semantic('Attention', isDarkMode)
}
]);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<TimeseriesChart
{echarts}
{isDarkMode}
type="bar"
{data}
xAxisName="Time (UTC)"
yAxisName="Count"
tooltipValueFormat={(value) => value.toFixed(2)}
/> Loading State
Set loading to true to display an animated sine-wave
skeleton while data is being fetched. The chart canvas is hidden until loading
completes; swap back to loading=false to reveal the chart.
<script lang="ts">
import { onMount } from 'svelte';
import * as echarts from 'echarts';
import { TimeseriesChart } from 'kumo-svelte';
import { getIsDarkMode } from './chart-color-demo-data';
let isDarkMode = $state(false);
onMount(() => {
const update = () => {
isDarkMode = getIsDarkMode();
};
const observer = new MutationObserver(update);
update();
[document.documentElement, document.body].forEach((node) => {
observer.observe(node, { attributes: true, attributeFilter: ['data-mode', 'class'] });
});
const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
mediaQuery?.addEventListener('change', update);
return () => {
observer.disconnect();
mediaQuery?.removeEventListener('change', update);
};
});
</script>
<div class="flex w-full flex-1 flex-col">
<TimeseriesChart {echarts} {isDarkMode} xAxisName="Time (UTC)" yAxisName="Count" data={[]} loading />
</div>