时间序列图表是用于展示基于时间的数据的专用图表。
每个数据点是一个 [timestamp_in_ms, value] 二元组。
基本折线图
展示随时间变化的多个数据系列的简单折线图。
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"
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}离散计数数据
对于请求数等离散值,设置 yAxisMinInterval=1。
这可以避免 y 轴出现小数刻度,同时对速率和其他连续值保持
默认的坐标轴行为不变。
import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
/**
* Discrete request-count chart with whole-number y-axis ticks.
*/
export function IntegerYAxisChartDemo() {
const isDarkMode = useIsDarkMode();
const data = useMemo(() => {
const end = Date.now();
const values = [0, 1, 3, 2, 5, 4, 7, 3, 6, 2, 4, 1];
return [
{
name: "Requests",
data: values.map((value, index): [number, number] => [
end - (values.length - index - 1) * 60_000,
value,
]),
color: ChartPalette.semantic("Neutral", isDarkMode),
},
];
}, [isDarkMode]);
return (
<TimeseriesChart
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
xAxisName="Time (UTC)"
yAxisName="Requests"
yAxisMinInterval={1}
/>
);
}参考标记线
使用 markers 属性在特定时间戳处渲染垂直参考线,
例如部署、发布或配置变更。标记线可以包含简短标签、用于工具
提示的描述、颜色和线条样式。
import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
export function ReferenceMarkersChartDemo() {
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],
);
const markers = useMemo(
() => [
{
timestamp: data[0].data[15][0],
label: "change a1b2c3d4",
description: "Configuration change applied",
},
{
timestamp: data[0].data[16][0],
label: "change b2c3d4e5",
description: "Routing rule updated",
},
{
timestamp: data[0].data[17][0],
label: "change c3d4e5f6",
description: "Limit adjusted",
},
{
timestamp: data[0].data[34][0],
label: "change e5f6g7h8",
description: "New version released",
lineStyle: "dotted" as const,
},
],
[data],
);
return (
<TimeseriesChart
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
markers={markers}
xAxisName="Time (UTC)"
yAxisName="Count"
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}阈值
使用 thresholds 属性在数值轴上渲染水平参考线,
例如内存上限或配额边界。阈值包含一个数值和颜色,标签可选。
import { ChartPalette, TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
export function ThresholdsChartDemo() {
const isDarkMode = useIsDarkMode();
const data = useMemo(
() => [
{
name: "Memory used",
data: buildSeriesData(0, 50, 60_000, 1),
color: ChartPalette.semantic("Neutral", isDarkMode),
},
],
[isDarkMode],
);
return (
<TimeseriesChart
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
thresholds={[
{
value: 55,
label: "Memory limit",
color: ChartPalette.semantic("Attention", isDarkMode),
},
]}
xAxisName="Time (UTC)"
yAxisName="Memory (MB)"
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}自定义时间戳与数值格式
使用 xAxisTickFormat 自定义 x 轴标签,使用
tooltipTimestampFormat 自定义标准工具提示和标记
工具提示中的时间戳。两者接收的都是毫秒级的原始时间戳,因此
可以一致地应用用户选定的时区。未提供格式化函数时,工具提示
的时间戳使用浏览器的区域设置和时区。
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 UTC timestamps and compact value formats. */
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={(timestamp) =>
new Intl.DateTimeFormat(undefined, {
timeZone: "UTC",
hour: "2-digit",
minute: "2-digit",
}).format(timestamp)
}
yAxisTickFormat={(value) => {
if (value >= 1000) return `${value / 1000}k`;
return value.toString();
}}
tooltipValueFormat={(value) => `${(value / 1000).toFixed(1)}k requests`}
tooltipTimestampFormat={(timestamp) =>
new Intl.DateTimeFormat(undefined, {
timeZone: "UTC",
dateStyle: "medium",
timeStyle: "medium",
}).format(timestamp)
}
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}渐变填充
将 gradient 设为 true,可在每条折线
系列下方渲染垂直渐变填充。填充从顶部的系列颜色渐变到底部的
透明色,让图表呈现精致的面积图观感,同时不损失各条折线的
清晰度。
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
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}不完整数据
使用 incomplete 属性标记数据可能不完整或仍在
收集中的区域。
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 }}
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}时间范围选择
通过提供 onTimeRangeChange 回调来启用时间范围
选择。用户可以在图表上点击并拖动来选取时间范围。
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()}`,
);
}}
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}工具提示页脚
使用 tooltipFooter 属性,可在标准系列工具提示的
数值下方添加简短的辅助文字。它适用于所有时间序列图表配置,
包括折线、柱状、渐变、阈值和自定义坐标轴图表。
import { ChartPalette, TimeseriesChart, ChartLegend, LayerCard } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
export function ChartExampleDemo() {
const isDarkMode = useIsDarkMode();
const data = useMemo(
() => [
{
name: "P99",
data: buildSeriesData(3, 30, 60_000, 1),
color: ChartPalette.semantic("Attention", isDarkMode),
},
{
name: "P95",
data: buildSeriesData(2, 30, 60_000, 0.6),
color: ChartPalette.semantic("Warning", isDarkMode),
},
{
name: "P75",
data: buildSeriesData(1, 30, 60_000, 0.4),
color: ChartPalette.semantic("Neutral", isDarkMode),
},
{
name: "P50",
data: buildSeriesData(0, 30, 60_000, 0.2),
color: ChartPalette.semantic("Neutral", isDarkMode),
},
],
[isDarkMode],
);
return (
<LayerCard>
<LayerCard.Secondary>Read latency</LayerCard.Secondary>
<LayerCard.Primary>
<div className="mb-2 flex gap-4 divide-x divide-kumo-hairline px-2">
<ChartLegend.LargeItem
name="P99"
color={ChartPalette.semantic("Attention", isDarkMode)}
value="124"
unit="ms"
/>
<ChartLegend.LargeItem
name="P95"
color={ChartPalette.semantic("Warning", isDarkMode)}
value="76"
unit="ms"
/>
<ChartLegend.LargeItem
name="P75"
color={ChartPalette.semantic("Neutral", isDarkMode)}
value="32"
unit="ms"
/>
<ChartLegend.LargeItem
name="P50"
color={ChartPalette.semantic("Neutral", isDarkMode)}
value="10"
unit="ms"
/>
</div>
<TimeseriesChart
xAxisName="Time (UTC)"
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
height={300}
tooltipFooter="Percentiles use a five-minute rolling window."
/>
</LayerCard.Primary>
</LayerCard>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}工具提示跟随光标
使用 tooltipFollowCursor 属性控制工具提示在哪个
轴上跟随光标。默认为 “both”,即自由跟随光标。
设为 “x” 可得到 Recharts 风格的轴锁定工具提示,
只在水平方向移动。
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>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}工具提示边界
使用 tooltipBoundary 属性把工具提示限制在特定的
容器元素内。默认情况下,工具提示会避免溢出任何会产生裁剪的
祖先元素 (滚动容器、视口)。传入一个 DOM 元素可进一步收紧
限制——当图表位于卡片或面板内、且工具提示不应溢出其外时
很有用。
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>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}柱状图
设置 type=“bar” 可把系列渲染为堆叠柱状图而非
折线。其他所有属性——坐标轴、工具提示、颜色——的行为完全
相同。
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)}
/>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}图例高亮
悬停在图例项上会高亮图表中对应的系列,并淡化其他系列。
在 ChartLegend 项上配合使用 onPointerEnter 和
onPointerLeave,以及图表 ref 上的
dispatchAction。
import { ChartPalette, TimeseriesChart, ChartLegend, LayerCard } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useMemo, useRef, useState } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
/**
* Timeseries chart with legend items that highlight the corresponding series on hover.
* Hovering a legend item dispatches a highlight action to the chart and fades the other legend items.
*/
export function LegendHighlightDemo() {
const isDarkMode = useIsDarkMode();
const chartRef = useRef<echarts.ECharts>(null);
const [hoveredSeries, setHoveredSeries] = useState<string | null>(null);
const series = useMemo(
() => [
{
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",
},
],
[isDarkMode],
);
const data = useMemo(
() =>
series.map((s, i) => ({
name: s.name,
data: buildSeriesData(3 - i, 30, 60_000, 1 - i * 0.2),
color: s.color,
})),
[series],
);
return (
<LayerCard>
<LayerCard.Secondary>Read latency</LayerCard.Secondary>
<LayerCard.Primary>
<div className="mb-2 flex divide-x divide-kumo-line px-2">
{series.map((s) => (
<ChartLegend.LargeItem
key={s.name}
name={s.name}
color={s.color}
value={s.value}
unit={s.unit}
inactive={hoveredSeries !== null && hoveredSeries !== s.name}
onPointerEnter={() => {
setHoveredSeries(s.name);
chartRef.current?.dispatchAction({
type: "highlight",
seriesName: s.name,
});
}}
onPointerLeave={() => {
setHoveredSeries(null);
chartRef.current?.dispatchAction({
type: "downplay",
seriesName: s.name,
});
}}
className="not-first:pl-4"
/>
))}
</div>
<TimeseriesChart
ref={chartRef}
xAxisName="Time (UTC)"
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
height={300}
/>
</LayerCard.Primary>
</LayerCard>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}图例点击
点击 ChartLegend 项会单独显示该系列,只展示它
并隐藏其余系列。再次点击已单独显示的系列会恢复全部系列。
import { ChartPalette, TimeseriesChart, ChartLegend, LayerCard } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useEffect, useMemo, useRef, useState } from "react";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
/**
* Timeseries chart where the legend isolates a series on click. Clicking a
* `ChartLegend` item shows only that series and hides the rest; clicking the
* already-isolated series restores all. Visibility is driven through a hidden
* ECharts legend via the `legendSelect` / `legendUnSelect` actions.
*/
export function LegendOnClickDemo() {
const isDarkMode = useIsDarkMode();
const chartRef = useRef<echarts.ECharts>(null);
const [hiddenSeries, setHiddenSeries] = useState<Record<string, boolean>>({});
const series = useMemo(
() => [
{
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",
},
],
[isDarkMode],
);
const data = useMemo(
() =>
series.map((s, i) => ({
name: s.name,
data: buildSeriesData(3 - i, 30, 60_000, 1 - i * 0.2),
color: s.color,
})),
[series],
);
// A theme switch re-inits the ECharts instance, resetting legend selection to
// all-visible. Reset our state to match so the legend doesn't desync.
useEffect(() => {
setHiddenSeries({});
}, [isDarkMode]);
// Click isolates a series: show only the clicked one and hide the rest via the
// (hidden) ECharts legend. Clicking the already-isolated series restores all.
const handleClick = (name: string) => {
const chart = chartRef.current;
if (!chart) return;
setHiddenSeries((prev) => {
// Already isolated to this series? (only it visible, everything else hidden)
const isIsolated = series.every((s) =>
s.name === name ? !prev[s.name] : prev[s.name],
);
const nextHidden: Record<string, boolean> = {};
for (const s of series) {
const shouldHide = isIsolated ? false : s.name !== name;
nextHidden[s.name] = shouldHide;
chart.dispatchAction({
type: shouldHide ? "legendUnSelect" : "legendSelect",
name: s.name,
});
}
return nextHidden;
});
};
return (
<LayerCard>
<LayerCard.Secondary>Read latency</LayerCard.Secondary>
<LayerCard.Primary>
<div className="mb-2 flex divide-x divide-kumo-line px-2">
{series.map((s) => (
<ChartLegend.LargeItem
key={s.name}
name={s.name}
color={s.color}
value={s.value}
unit={s.unit}
inactive={hiddenSeries[s.name] ?? false}
onClick={() => handleClick(s.name)}
className="not-first:pl-4"
/>
))}
</div>
<TimeseriesChart
ref={chartRef}
xAxisName="Time (UTC)"
echarts={echarts}
isDarkMode={isDarkMode}
data={data}
height={300}
enableLegendSelection
/>
</LayerCard.Primary>
</LayerCard>
);
}
function buildSeriesData(
seed = 0,
points = 50,
stepMs = 60_000,
timeScale = 1,
): [number, number][] {
const end = Date.now();
const start = end - (points - 1) * stepMs;
return Array.from({ length: points }, (_, i) => {
const ts = start + i * stepMs;
const trend = i * 0.15;
const noise = (Math.random() - 0.5) * 8;
const value = Math.round((30 + seed * 15 + trend + noise) * 100) / 100;
return [ts, value * timeScale];
});
}加载状态
将 loading 设为 true,可在数据加载
时显示与图表 type 相匹配的骨架屏。
折线图
import { TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
/**
* Line timeseries chart in loading state, showing the calm area-shaped skeleton.
*/
export function LoadingChartDemo() {
const isDarkMode = useIsDarkMode();
return (
<div className="flex w-full flex-1 flex-col">
<TimeseriesChart
echarts={echarts}
isDarkMode={isDarkMode}
xAxisName="Time (UTC)"
yAxisName="Count"
data={[]}
loading
/>
</div>
);
}柱状图
import { TimeseriesChart } from "@cloudflare/kumo";
import * as echarts from "echarts/core";
import { useIsDarkMode } from "~/lib/use-is-dark-mode";
/**
* Bar timeseries chart in loading state, showing the bar-shaped skeleton that
* matches the chart's `type="bar"` output.
*/
export function LoadingBarChartDemo() {
const isDarkMode = useIsDarkMode();
return (
<div className="flex w-full flex-1 flex-col">
<TimeseriesChart
echarts={echarts}
isDarkMode={isDarkMode}
type="bar"
xAxisName="Time (UTC)"
yAxisName="Count"
data={[]}
loading
/>
</div>
);
}