Timeseries Chart
@cloudflare/kumo

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.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Basic line chart example showing simple time-based data visualization.
 */
export function BasicLineChartDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        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),
      },
    ],
    [isDarkMode],
  );

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      data={data}
      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.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart with custom axis tick label formats for both x-axis (HH:MM) and y-axis (compact numbers).
 */
export function CustomAxisLabelFormatDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        name: "Requests",
        data: buildSeriesData(0, 50, 60_000, 1000),
        color: ChartPalette.semantic("Neutral", isDarkMode),
      },
    ],
    [isDarkMode],
  );

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      data={data}
      xAxisName="Time (UTC)"
      yAxisName="Requests"
      xAxisTickFormat={(ts) => {
        const d = new Date(ts);
        return `${d.getHours().toString().padStart(2, "0")}:${d.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.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart with gradient fill beneath each line series.
 */
export function GradientLineChartDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        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),
      },
    ],
    [isDarkMode],
  );

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      data={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.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart with incomplete data regions highlighted.
 */
export function IncompleteDataChartDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        name: "Bandwidth",
        data: buildSeriesData(0, 50, 60_000, 1),
        color: ChartPalette.categorical(0, isDarkMode),
      },
    ],
    [isDarkMode],
  );

  const incompleteTimestamp = data[0].data[data[0].data.length - 5][0];

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      data={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.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart with time range selection enabled.
 */
export function TimeRangeSelectionChartDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        name: "CPU Usage",
        data: buildSeriesData(0, 50, 60_000, 1),
        color: ChartPalette.categorical(0, isDarkMode),
      },
    ],
    [isDarkMode],
  );

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      data={data}
      xAxisName="Time (UTC)"
      yAxisName="%"
      onTimeRangeChange={(from, to) => {
        alert(
          `Selected range:\nFrom: ${new Date(from).toLocaleString()}\nTo: ${new Date(to).toLocaleString()}`,
        );
      }}
    />
  );
}

Tooltip Cursor Tracking

Use the tooltipFollowCursor prop to control which axis the tooltip tracks the cursor on. The default is “both”, which follows the cursor freely. Set it to “x” for a Recharts-style axis-locked tooltip that only moves horizontally.

Tooltip follow cursor
import { ChartPalette, TimeseriesChart, Select } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo, useState } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Interactive demo showing the `tooltipFollowCursor` prop. Use the dropdown to
 * switch between cursor-tracking modes and see how the tooltip behaves.
 */
export function TooltipFollowCursorDemo() {
  const isDarkMode = useIsDarkMode();
  const [selected, setSelected] = useState<FollowCursorOption>(FOLLOW_CURSOR_OPTIONS[0]);

  const data = useMemo(
    () => [
      {
        name: "P99",
        data: buildSeriesData(0, 50, 60_000, 1),
        color: ChartPalette.semantic("Attention", isDarkMode),
      },
      {
        name: "P50",
        data: buildSeriesData(1, 50, 60_000, 0.4),
        color: ChartPalette.semantic("Neutral", isDarkMode),
      },
    ],
    [isDarkMode],
  );

  return (
    <div className="flex w-full flex-col gap-4">
      <Select
        label="Tooltip follow cursor"
        value={selected}
        onValueChange={(v) => { if (v) setSelected(v); }}
        renderValue={(v) => v.label}
      >
        {FOLLOW_CURSOR_OPTIONS.map((opt) => (
          <Select.Option key={opt.value} value={opt}>
            {opt.label}
          </Select.Option>
        ))}
      </Select>
      <TimeseriesChart
        echarts={echarts}
        isDarkMode={isDarkMode}
        data={data}
        xAxisName="Time (UTC)"
        yAxisName="Latency (ms)"
        tooltipFollowCursor={selected.value}
      />
    </div>
  );
}

Tooltip Boundary

Use the tooltipBoundary prop to constrain the tooltip to a specific container element. By default the tooltip avoids overflowing any clipping ancestor (scroll containers, viewports). Pass a DOM element to restrict it further — useful when the chart lives inside a card or panel and the tooltip shouldn’t escape it.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useCallback, useMemo, useState } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Demo showing the `tooltipBoundary` prop. The chart is inside a small
 * scrollable container — the tooltip is constrained to stay within it
 * instead of overflowing into the surrounding page.
 */
export function TooltipBoundaryDemo() {
  const isDarkMode = useIsDarkMode();
  const [boundary, setBoundary] = useState<HTMLDivElement | null>(null);
  const boundaryRef = useCallback((el: HTMLDivElement | null) => setBoundary(el), []);

  const data = useMemo(
    () => [
      {
        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),
      },
    ],
    [isDarkMode],
  );

  return (
    <div
      ref={boundaryRef}
      className="w-full overflow-auto rounded-lg border border-kumo-line"
      style={{ height: 300 }}
    >
      <TimeseriesChart
        echarts={echarts}
        isDarkMode={isDarkMode}
        data={data}
        xAxisName="Time (UTC)"
        yAxisName="Count"
        height={280}
        tooltipBoundary={boundary ?? undefined}
      />
    </div>
  );
}

Bar Chart

Set type=“bar” to render series as stacked bars instead of lines. All other props — axes, tooltips, colors — work identically.

import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart rendered as a stacked bar chart.
 */
export function BarChartDemo() {
  const isDarkMode = useIsDarkMode();

  const data = useMemo(
    () => [
      {
        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),
      },
    ],
    [isDarkMode],
  );

  return (
    <TimeseriesChart
      echarts={echarts}
      isDarkMode={isDarkMode}
      type="bar"
      data={data}
      xAxisName="Time (UTC)"
      yAxisName="Count"
      tooltipValueFormat={(r) => r.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.

import { TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";

/**
 * Timeseries chart in loading state, showing the animated sine-wave skeleton.
 * Loads for 5 seconds then reveals the real chart. A button restarts the cycle.
 */
export function LoadingChartDemo() {
  const isDarkMode = useIsDarkMode();
  return (
    <div className="flex flex-col flex-1 w-full">
      <TimeseriesChart
        echarts={echarts}
        isDarkMode={isDarkMode}
        xAxisName="Time (UTC)"
        yAxisName="Count"
        data={[]}
        loading
      />
    </div>
  );
}