ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TinyBase 响应式 SVG 图表组件实战:基于 ui-react-dom-charts 在 React 中渲染数据图表

TinyBase 响应式 SVG 图表组件实战:基于 ui-react-dom-charts 在 React 中渲染数据图表
  • 数据库
  • 后端
  • 前端

【免费下载链接】tinybase

A reactive data store & sync engine.

项目地址:https://gitcode.com/gh_mirrors/ti/tinybase
点击查看免费下载

本文围绕 TinyBase 官方 "Chart Components (React)" 演示系列展开,完整讲解ui-react-dom-charts模块的用法:如何把 Store 中的 Table 或 Queries 的 ResultTable 渲染为可直接运行的 SVG 图表,如何用纯 CSS 定制图表外观、组合多条序列、处理数值/分类/时间等不同数据形状,以及如何通过XAxis/YAxis子组件覆盖坐标轴行为。读完本文,你将能基于 TinyBase 的响应式数据模型,从零搭建一套可复用、可深度定制、随数据变化自动刷新的 React 图表应用,并理解其底层实现原理。

该演示系列是一个"图表主题画廊",覆盖六个渐进式主题:基础折线图、CSS 样式定制、多序列图表组合、数据排序与类型、坐标轴覆盖、时间轴。对应的官方演示文档位于 site/demos/12_chart_components_react,模块源码位于 src/ui-react-dom-charts,完整类型声明在 src/@types/ui-react-dom-charts/index.d.ts。

快速开始:从图表应用模板起步

原文档建议直接通过官方 CLI 脚手架启动:运行npm create tinybase@latest并在交互式提示中选择Charting app模板(当前仓库版本为 9.6.0,见 package.json)。该模板会为你生成一个包含 importmap 与最小样式的可编辑图表应用,后续六个主题都可以在此基础上逐步演化。

如果要在现有页面中手工引入,则按原文档配置 importmap 与模块导入:

<script type="importmap"> { "imports": { "tinybase": "https://esm.sh/tinybase@", "tinybase/ui-react": "https://esm.sh/tinybase/ui-react@", "tinybase/ui-react-dom-charts": "https://esm.sh/tinybase/ui-react-dom-charts@", "react": "https://esm.sh/react@", "react/jsx-runtime": "https://esm.sh/react/jsx-runtime@", "react-dom/client": "https://esm.sh/react-dom/client@" } } </script>

其中tinybase提供createStore,tinybase/ui-react提供Provider与useCreateStore,tinybase/ui-react-dom-charts提供全部图表组件。@后的版本号通常由脚手架构建流程自动填充为当前版本(如9.6.0)。

六个主题及其对应的官方演示页面如下:

主题官方演示
BasicsLineChart 组件
StylingStyled LineChart 组件
Composition组合图表
Data排序与类型
Axes坐标轴覆盖
Time时间轴

第一个图表:用<LineChart />渲染 Store Table

准备数据:Store 与 Provider

图表组件本身不持有数据,而是通过 React Context 从Provider中取得 Store。顶层组件用useCreateStore惰性创建 Store 并写入一张sales表,每行代表一个月份,包含month(月份标签)、order(排序序号)与revenue(收入)三个 Cell:

const App = () => { const store = useCreateStore(() => createStore().setTable('sales', { jan: {month: 'Jan', order: 1, revenue: 12}, feb: {month: 'Feb', order: 2, revenue: 18}, mar: {month: 'Mar', order: 3, revenue: 15}, apr: {month: 'Apr', order: 4, revenue: 24}, may: {month: 'May', order: 5, revenue: 21}, jun: {month: 'Jun', order: 6, revenue: 28}, }), ); return ( <Provider store={store}> <Body /> </Provider> ); }; addEventListener('load', () => createRoot(document.body).render(<App />));

绑定图表

LineChart组件从 Provider Context 中读取 Store。使用tableId指定数据源表,再用xCellId与yCellId分别绑定 x 轴与 y 轴读取的 Cell:

const Body = () => ( <main> <LineChart className="chart" tableId="sales" xCellId="month" yCellId="revenue" sortCellId="order" /> </main> );

这里sortCellId="order"用于按orderCell 对行进行排序,确保 x 轴按 1 月到 6 月的自然顺序绘制;若不排序,行会按默认行序遍历。除了绑定 Table,图表组件同样可以绑定 Queries 的 ResultTable——只需把tableId换成queryId(配合可选的queries属性),这一分支在 src/ui-react-dom-charts/components/summary.ts 的useInitialSeriesSummary中分别通过getSortedRowIds/getResultSortedRowIds处理。

让 SVG 可见:最小必要 CSS

图表组件输出的是<svg>元素,唯一必需的 CSS 是给它一个可见的尺寸与字体。原文档给出的基础样式如下:

@font-face { font-family: Inter; src: url(https://tinybase.org/fonts/inter.woff2) format('woff2'); } * { box-sizing: border-box; } body { background: white; font-family: Inter, sans-serif; margin: 0; } main { margin: 2rem auto; max-width: 52rem; padding: 0 2rem; } .chart { display: block; font-size: 12px; height: 20rem; width: 100%; } @media (max-width: 40rem) { main { padding: 0 1rem; } }

其中.chart的display: block、width、height决定 SVG 的显示尺寸,font-size会被组件读取并用于布局计算。仅凭以上代码,即可得到一个使用默认 SVG 样式的可用折线图。

从源码看,LineChart本身是便捷封装:它先用useInitialSeriesSummary计算初始序列摘要,再把 props 拆分为"数据源 + 绑定"两部分,最终渲染为一个CartesianChart包裹的LineSeries(见 LineChart.tsx 与 props.ts)。也就是说,LineChart≈CartesianChart+ 单个LineSeries的语法糖;BarChart同理(见 BarChart.tsx)。

CSS 全面定制:样式化折线图

第二个主题在第一个 demo 基础上只改 CSS,不改任何图表 props,展示出"数据与表现分离"的设计:组件负责响应式数据与几何计算,CSS 负责全部视觉呈现。这也是"组件默认使用currentColor着色"这一设计带来的红利——在图表根元素上设置color,即可同时影响绝大多数 SVG 元素。

图表框架

.chart { + background: #f6f8fb; + border: 1px solid #d9e2ec; + border-radius: 0.5rem; + box-shadow: 0 0.75rem 2rem #26364a1a; + color: #1f2937; display: block; font-size: 12px; height: 20rem; + padding: 1rem; width: 100%; }

坐标轴与网格线

通过更具体的选择器分别调整网格、轴线、刻度文字与轴标题:

.chart { .grid { color: #d8e1eb; stroke-dasharray: 4 6; } .axes { color: #677489; .title { fill: #1f2937; font-weight: 700; } } }

数据区域:面积、折线与数据点

折线图的数据部分由三个可独立定制的部分构成:面积(area)、折线(line)与数据点(points):

.chart { .plot { .area { fill: #0ea5e9; fill-opacity: 0.12; } .line { stroke: #0284c7; stroke-linecap: round; stroke-linejoin: round; stroke-opacity: 1; stroke-width: 3; } .points { fill: white; stroke: #0284c7; stroke-width: 2; } } }

Tooltip

鼠标悬停数据点会出现的提示框同样是普通 SVG 元素,可完全定制:

.chart { .tooltip-lines { stroke: #0284c7; stroke-dasharray: 3 3; stroke-opacity: 0.35; } .tooltip { font-weight: 700; rect { fill: #172033; fill-opacity: 0.94; } text { fill: white; } } }

从源码确认这套 SVG 结构:CartesianChart渲染svg > (grid + axes + plot + tooltip)四层(见 CartesianChart.tsx);Grid输出.grid组(Grid.tsx);Axes输出.axes组并以fill={CURRENT_COLOR}(即currentColor)着色(Axes.tsx);Line输出.area、.line、.points(Line.tsx);Tooltip输出.tooltip-lines与.tooltip(Tooltip.tsx)。这也解释了为什么"只设置根元素 color 即可联动全图"。

组合图表:CartesianChart与多个序列

第三个主题展示如何用CartesianChart在一个 SVG 中组合多条序列:父组件负责共享坐标轴、网格、布局与 tooltip,每个子序列(LineSeries/BarSeries)只声明自己的 Cell 绑定、标签与 CSS 类。

导入时换成可组合的系列组件:

import React from 'react'; import {createRoot} from 'react-dom/client'; import {createStore} from 'tinybase'; import {Provider, useCreateStore} from 'tinybase/ui-react'; import { BarSeries, CartesianChart, LineSeries, } from 'tinybase/ui-react-dom-charts';

数据表中每个月份行同时携带多个 y 值 Cell,它们即可成为同一图表中的不同序列:

const App = () => { const store = useCreateStore(() => createStore().setTable('sales', { jan: {month: 'Jan', order: 1, revenue: 62, profit: 33, orders: 18}, feb: {month: 'Feb', order: 2, revenue: 48, profit: 24, orders: 14}, mar: {month: 'Mar', order: 3, revenue: 31, profit: 19, orders: 10}, apr: {month: 'Apr', order: 4, revenue: 73, profit: 41, orders: 24}, may: {month: 'May', order: 5, revenue: 39, profit: 22, orders: 12}, jun: {month: 'Jun', order: 6, revenue: 55, profit: 30, orders: 17}, }), ); return ( <Provider store={store}> <Body /> </Provider> ); }; addEventListener('load', () => createRoot(document.body).render(<App />));

图表画廊中的每个CartesianChart提供数据源与图框,子序列组件各自声明绑定:

const Body = () => ( <main> <section> <CartesianChart className="chart chart-lines" tableId="sales"> <LineSeries className="series-revenue" label="Revenue" sortCellId="order" xCellId="month" yCellId="revenue" /> <LineSeries className="series-profit" label="Profit" sortCellId="order" xCellId="month" yCellId="profit" /> </CartesianChart> </section> <section> <CartesianChart className="chart chart-bars" tableId="sales"> <BarSeries className="series-orders" label="Orders" sortCellId="order" xCellId="month" yCellId="orders" /> <BarSeries className="series-profit" label="Profit" sortCellId="order" xCellId="month" yCellId="profit" /> </CartesianChart> </section> <section> <CartesianChart className="chart chart-mixed" tableId="sales"> <BarSeries className="series-orders" label="Orders" sortCellId="order" xCellId="month" yCellId="orders" /> <LineSeries className="series-revenue" label="Revenue" sortCellId="order" xCellId="month" yCellId="revenue" /> </CartesianChart> </section> </main> );

注意label会被用作该序列在 tooltip 与 y 轴标题中的名称:从 getTitle 可以看到,多个序列的 y 轴标题会以yLabel ?? yCellId去重后用' & '拼接,例如Revenue & Profit。

因为每个序列都有独立className,CSS 可以为每个序列独立配色。示例中三张卡片布局、通用.chart样式、以及按序列定制的.series-revenue/.series-profit/.series-orders颜色规则,与混搭图表的特殊规则(.chart-mixed .series-revenue .area {fill: none})如下:

@font-face { font-family: Inter; src: url(https://tinybase.org/fonts/inter.woff2) format('woff2'); } * { box-sizing: border-box; } body { font-family: Inter, sans-serif; margin: 0; } main { display: grid; gap: 1rem; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 2rem auto; max-width: 72rem; padding: 0 2rem; section { min-width: 0; } } .chart { display: block; font-size: 12px; height: 14rem; width: 100%; .grid { color: #d8e1eb; stroke-dasharray: 4 6; } .axes { color: #677489; .title { fill: #1f2937; font-weight: 700; } } .area { fill-opacity: 0.1; } .line { fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 3; } .points { fill: white; stroke-width: 2; } } .series-revenue { .area { fill: #2b8c67; } .line, .points { stroke: #2b8c67; } } .series-profit { .area { fill: #d16b3f; } .line, .points { stroke: #d16b3f; } .bar { fill: #d16b3f; } } .series-orders { .bar { fill: #5367c9; } } .chart-mixed { .series-revenue { .area { fill: none; } .line { stroke-width: 4; } } } @media (max-width: 52rem) { main { grid-template-columns: 1fr; } }

从实现角度看组合机制:CartesianChart通过Children.forEach解析子元素,识别系列组件(带SERIES标记)、XAxis与YAxis配置组件(分别带X_AXIS/Y_AXIS标记),序列与坐标轴配置分开收集(见 CartesianChart.tsx 与 context.ts)。每个BarSeries挂载时向父组件注册(registerBarSeries),父组件据此统计柱状序列数量、为同组柱条计算分组偏移(见 Bars.tsx);每个序列通过setSeriesSummary把自身的数据范围(bounds)与 x 值集合上报告父组件,父组件合并所有序列的范围后统一计算刻度与坐标轴(CartesianChart.tsx)。

数据类型与排序:数值、ISO 日期、分类与布尔

第四个主题聚焦 x 值的常见形态:连续数值、ISO 日期字符串、有序分类标签、布尔分类。其通用规则是:x 值支持 number / string / boolean,而 y 值必须是有限数值(finite number)。从源码 data.ts 的getXValue/getYValue可以确认:x 接受有限数值、字符串、true/false,其余类型(如对象、NaN、Infinity)会被过滤;y 则只接受isFiniteNumber为真的数值,非法行会被剔除出数据点。

示例 Store 用三张表分别演示不同形状:

const App = () => { const store = useCreateStore(() => createStore() .setTable('measurements', { third: {x: 3, score: 17}, first: {x: 1, score: 8}, fifth: {x: 5, score: 27}, second: {x: 2, score: 13}, fourth: {x: 4, score: 22}, }) .setTable('daily', { d1: {day: '2026-01-01', signups: 11}, d2: {day: '2026-01-02', signups: 17}, d3: {day: '2026-01-05', signups: 21}, d4: {day: '2026-01-08', signups: 28}, }) .setTable('channels', { organic: {channel: 'Organic', rank: 1, visits: 39}, referral: {channel: 'Referral', rank: 3, visits: 24}, paid: {channel: 'Paid', rank: 2, visits: 31}, direct: {channel: 'Direct', rank: 4, visits: 18}, }) .setTable('flags', { active: {enabled: true, order: 1, accounts: 28}, inactive: {enabled: false, order: 2, accounts: 11}, }), ); return ( <Provider store={store}> <Body /> </Provider> ); }; addEventListener('load', () => createRoot(document.body).render(<App />));

对应四个图表的绑定方式:

const Body = () => ( <main> <section> <LineChart className="chart chart-numbers" tableId="measurements" xCellId="x" yCellId="score" /> </section> <section> <LineChart className="chart chart-dates" tableId="daily" xCellId="day" yCellId="signups" /> </section> <section> <BarChart className="chart chart-categories" tableId="channels" xCellId="channel" yCellId="visits" sortCellId="rank" /> </section> <section> <CartesianChart className="chart chart-booleans" tableId="flags"> <BarSeries className="series-accounts" label="Accounts" sortCellId="order" xCellId="enabled" yCellId="accounts" /> </CartesianChart> </section> </main> );

类型推断规则(与 getResolvedXScale 一致):

  • 全部为数值→ 连续线性 x 轴(LINEAR),折线按真实数值间距定位;
  • 全部为 ISO 日期字符串(形如YYYY-MM-DD或以T开头的日期时间)→ 时间轴(TIME),刻度为 UTC 时间标签;
  • 其他字符串或布尔值→ 分类轴(CATEGORY),x 值按首次出现顺序(或sortCellId排序后的行序)离散排列,布尔标签渲染为true/false;
  • 折线图仅当所有 x 值都是数值时才算连续(continuousX,见 getSeriesSummary)。

复用同一套 CSS 结构即可覆盖四种图表:

@font-face { font-family: Inter; src: url(https://tinybase.org/fonts/inter.woff2) format('woff2'); } * { box-sizing: border-box; } body { font-family: Inter, sans-serif; margin: 0; } main { display: grid; gap: 1rem; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 2rem auto; max-width: 88rem; padding: 0 2rem; section { min-width: 0; } } .chart { display: block; font-size: 12px; height: 14rem; width: 100%; .grid { color: #d8e1eb; stroke-dasharray: 4 6; } .axes { color: #677489; .title { fill: #1f2937; font-weight: 700; } } .area { fill: #2b8c67; fill-opacity: 0.12; } .line { fill: none; stroke: #2b8c67; stroke-linecap: round; stroke-linejoin: round; stroke-width: 3; } .points { fill: white; stroke: #2b8c67; stroke-width: 2; } .bar { fill: #5367c9; } &-booleans { .bar { fill: #d16b3f; } } } @media (max-width: 52rem) { main { grid-template-columns: 1fr; } }

补充一点:measurements表中行是乱序插入的,但仍会按 x 数值绘制,因为数值行在连续线性轴上按值定位;而分类轴的行顺序取决于行排序结果,这正是channels表用rank排序、flags表用order排序的原因。

坐标轴覆盖:XAxis与YAxis配置子组件

第五个主题演示XAxis/YAxis作为配置子组件(configuration children)的用法:它们不渲染独立图表,而是把标题、边界、刻度、刻度格式与 SVG 类名注入父级CartesianChart的共享坐标轴。

示例把日期存为Unix 秒级时间戳,并用XAxis显式声明其为时间刻度、格式化收到的Date对象:

const timestamp = (day) => Date.UTC(2026, 0, day) / 1000; const formatDate = (date) => date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', timeZone: 'UTC', }); const App = () => { const store = useCreateStore(() => createStore().setTable('sales', { d1: {date: timestamp(1), revenue: 18}, d2: {date: timestamp(2), revenue: 26}, d3: {date: timestamp(3), revenue: 22}, d4: {date: timestamp(4), revenue: 39}, d5: {date: timestamp(5), revenue: 44}, d6: {date: timestamp(6), revenue: 41}, d7: {date: timestamp(7), revenue: 53}, }), ); return ( <Provider store={store}> <Body /> </Provider> ); }; addEventListener('load', () => createRoot(document.body).render(<App />));

图表部分同时覆盖 x 轴与 y 轴:

const Body = () => ( <main> <CartesianChart className="chart chart-axes" tableId="sales"> <XAxis className="axis-dates" max={timestamp(7)} min={timestamp(1)} scale="time" tickFormatter={formatDate} ticks={[timestamp(1), timestamp(3), timestamp(5), timestamp(7)]} timestampUnit="second" title="Sale date" /> <YAxis className="axis-revenue" max={60} min={0} tickFormatter={(tick) => `$${tick}k`} ticks={[0, 20, 40, 60]} title="Revenue" /> <LineSeries xCellId="date" yCellId="revenue" /> </CartesianChart> </main> );

关键 props 一览(类型定义见 src/@types/ui-react-dom-charts/index.d.ts):

组件Prop说明
XAxisscale'auto' \| 'category' \| 'linear' \| 'time',显式指定 x 刻度类型,默认'auto'
XAxismin/max轴边界;time刻度下接受 number / string / Date(TimeValue)
XAxisticks显式刻度数组(TimeValue[]),传入后刻度不再自动生成
XAxistickCount自动刻度数量的目标值(默认 10,见 wilkinson.ts)
XAxistickFormatter刻度格式化;time刻度下签名为(tick: Date, timestamp: number) => string,其余为(tick: boolean \| number \| string) => string
XAxistimestampUnit'millisecond' \| 'second',声明时间戳单位(TimeXAxisProps专用)
XAxistitle轴标题,覆盖从 Cell 名推断的默认标题
XAxisclassName追加到 x 轴 SVG 组(类名形如x axis-dates)
YAxismin/max/ticks/tickCount/title/className与 x 轴对应;tickFormatter签名为(tick: number) => string

样式上,两个轴的className分别加到.x与.y组上,因此可以独立配色:

@font-face { font-family: Inter; src: url(https://tinybase.org/fonts/inter.woff2) format('woff2'); } * { box-sizing: border-box; } body { background: white; font-family: Inter, sans-serif; margin: 0; } main { margin: 2rem auto; max-width: 52rem; padding: 0 2rem; } .chart { background: #f7f9fb; border: 1px solid #dce5ee; border-radius: 0.5rem; color: #596779; display: block; font-size: 12px; height: 20rem; padding: 1rem; width: 100%; .grid { color: #d7e0ea; stroke-dasharray: 4 6; } .axes { .title { fill: #1f2937; font-weight: 700; } } .axis-dates { fill: #275f83; } .axis-revenue { fill: #6f4a1c; } .area { fill: #3b82a0; fill-opacity: 0.12; } .line { stroke: #1f7799; stroke-linecap: round; stroke-linejoin: round; stroke-width: 3; } .points { fill: white; stroke: #1f7799; stroke-width: 2; } } @media (max-width: 40rem) { main { padding: 0 1rem; } }

源码层面,XAxis/YAxis组件本身渲染为null,只是携带 props 的"标记组件"(XAxis.tsx、YAxis.tsx);CartesianChart在getParsedChildren中读取它们的 props,并在getAxisBounds中让显式min/max/ticks优先于数据推断的边界(CartesianChart.tsx)。单元测试 ui-react-dom-charts.test.tsx 验证了该行为:XAxis/YAxis的className、tickFormatter、ticks、title会完整生效,且序列缺席时轴配置也能独立渲染(uses explicit axis definitions before series data exists)。

时间轴:ISO 字符串自动识别与 Unix 时间戳显式声明

第六个主题对比时间轴的两种用法:ISO 日期字符串可被自动识别,Unix 秒级时间戳必须显式声明,否则会被当作普通的大数值线性轴处理。

const timestamp = (day) => Date.UTC(2026, 0, day) / 1000; const formatDate = (date) => date.toLocaleDateString('en-US', { day: 'numeric', month: 'short', timeZone: 'UTC', }); const App = () => { const store = useCreateStore(() => createStore() .setTable('visits', { d1: {day: '2026-01-01', visits: 42}, d2: {day: '2026-01-02', visits: 57}, d3: {day: '2026-01-05', visits: 64}, d4: {day: '2026-01-08', visits: 81}, }) .setTable('orders', { d1: {time: timestamp(1), orders: 12}, d2: {time: timestamp(2), orders: 18}, d3: {time: timestamp(5), orders: 21}, d4: {time: timestamp(8), orders: 25}, }), ); return ( <Provider store={store}> <Body /> </Provider> ); }; addEventListener('load', () => createRoot(document.body).render(<App />));

两个图表的绑定差异只在于第二个是否提供XAxis:

const Body = () => ( <main> <section> <LineChart className="chart chart-iso" tableId="visits" xCellId="day" yCellId="visits" /> </section> <section> <LineChart className="chart chart-unix" tableId="orders" xCellId="time" yCellId="orders" > <XAxis className="axis-unix" scale="time" tickFormatter={formatDate} ticks={[timestamp(1), timestamp(3), timestamp(5), timestamp(7)]} timestampUnit="second" title="Order date" /> </LineChart> </section> </main> );

注意便捷组件LineChart同样接受children——它内部会把额外子元素透传给底层的CartesianChart(见 LineChart.tsx)。

两个图表共享大部分样式,仅用不同配色区分:

@font-face { font-family: Inter; src: url(https://tinybase.org/fonts/inter.woff2) format('woff2'); } * { box-sizing: border-box; } body { background: white; font-family: Inter, sans-serif; margin: 0; } main { display: grid; gap: 1rem; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 2rem auto; max-width: 64rem; padding: 0 2rem; section { min-width: 0; } } .chart { background: #f7f9fb; border: 1px solid #dce5ee; border-radius: 0.5rem; color: #596779; display: block; font-size: 12px; height: 18rem; padding: 1rem; width: 100%; .grid { color: #d7e0ea; stroke-dasharray: 4 6; } .axes { .title { fill: #1f2937; font-weight: 700; } } .line { stroke-linecap: round; stroke-linejoin: round; stroke-opacity: 0.9; stroke-width: 3; } .points { fill: white; stroke-width: 2; } } .chart-iso { .area { fill: #3b82a0; fill-opacity: 0.12; } .line, .points { stroke: #1f7799; } } .chart-unix { .area { fill: #2b8c67; fill-opacity: 0.12; } .line, .points { stroke: #2b8c67; } .axis-unix { fill: #2f6c4c; } } @media (max-width: 52rem) { main { grid-template-columns: 1fr; padding: 0 1rem; } }

时间轴识别的底层逻辑:

  • ISO 自动识别依赖正则ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:$|T)/(data.ts):当所有 x 值都是能被normalizeTimeValue解析为有限时间戳的字符串时,getResolvedXScale判定为TIME;
  • normalizeTimeValue对数值按timestampUnit归一化(second单位 ×1000 转毫秒)、对 ISO 字符串与Date实例取毫秒时间戳(data.ts);
  • 时间刻度的自动生成由getTimeTicks完成:在预定义的 22 档时间间隔(1 秒到 100 年,见 wilkinson.ts)中按"间隔跨度、覆盖率、密度、标签间距"评分择优,同时保证刻度间距不小于标签宽度(spacing < labelSize * 1.2时淘汰)——因此刻度标签是确定性的 UTC 日期/时间格式(wilkinson.ts)。

源码视角:响应式数据流与渲染架构

结合 src/ui-react-dom-charts 源码,可以把整套图表机制归纳为三层架构:

1. 便捷组件层(LineChart/BarChart):把tableId/queryId与xCellId/yCellId/sortCellId等绑定 props 拆分给CartesianChart与内置系列,是"单序列图表"的语法糖(props.ts)。

2. 编排层(CartesianChart):通过 Context(CartesianChartContext)向下提供数据源、刻度、边界、布局与回调;向上汇总每个系列的SeriesSummary(x 范围、y 范围、x 值集合、是否连续),合并出统一 domain,并解析XAxis/YAxis配置子组件决定轴行为(context.ts、CartesianChart.tsx)。

3. 图元层(Grid/Axes/Line/Bars/Tooltip):基于统一 domain 把数据点缩放到像素坐标,输出纯 SVG。

数据响应性来自useSeriesData:它通过useSortedRowIds/useResultSortedRowIds获取有序行 id,并用useCellListener/useResultCellListener订阅 x/y Cell 变化——任何绑定 Cell 更新都会触发序列重渲染,进而把新摘要上报给父组件、由父组件增量合并 domain(series.ts)。布局方面,useLayout通过ResizeObserver观察 SVG 尺寸变化,并读取计算样式中的font-size缩放默认间距常量(DEFAULT_STYLE = [0.5, 0.5, 0.25, 3.5, 4, 1, 1]),因此图表可以自适应容器(svg.ts)。连续轴刻度采用改进的 Wilkinson 算法(NICE_NUMBERS = [1, 5, 2, 2.5, 4, 3]、权重[0.25, 0.2, 0.5, 0.05]),生成"整齐好看"的刻度步长(wilkinson.ts)。

这些行为都被单元测试覆盖:test/unit/core/ui-react/ui-react-dom-charts.test.tsx 验证了 Table/Query 两种数据源绑定、Provider与storesById上下文、XAxis/YAxis配置生效、分类轴标签与数据点对齐、ISO 字符串自动识别为时间轴、秒级时间戳归一化、确定性 UTC 刻度标签等场景;类型测试 test/unit/core/types/ui-react-dom-charts.tsx 则约束了tableId与queryId互斥、timestampUnit取值、tickFormatter签名等类型安全边界。

总结

"Chart Components (React)" 演示系列完整展示了 TinyBaseui-react-dom-charts模块的设计哲学:

  • 声明式数据绑定:图表直接消费 Store Table 或 Queries ResultTable,数据变化自动驱动 SVG 重绘,无需手动维护图表状态;
  • 数据与表现分离:几何与响应式由组件负责,currentColor+ 明确的 SVG 类名体系(.grid/.axes/.plot/.area/.line/.points/.bar/.tooltip)让全部视觉细节都可以用纯 CSS 定制;
  • 组件组合:CartesianChart编排共享坐标轴,LineSeries/BarSeries各自绑定数据,XAxis/YAxis作为配置子组件提供精细的刻度、边界与格式化控制;
  • 智能类型推断:数值→线性轴、ISO 日期字符串→时间轴、字符串/布尔→分类轴,同时允许用scale="time"等方式显式覆盖。

从基础折线图到多序列组合、从数据形状处理到时间轴与坐标轴覆盖,这六个主题构成了一条平滑的学习路径。继续深入可阅读同目录下的其他主题文档(site/demos),或直接查看 src/ui-react-dom-charts 源码与 test/unit/core/ui-react/ui-react-dom-charts.test.tsx 测试来验证更多边界行为。

  • 数据库
  • 后端
  • 前端

【免费下载链接】tinybase

A reactive data store & sync engine.

项目地址:https://gitcode.com/gh_mirrors/ti/tinybase
点击查看免费下载
上一篇:Mnemosyne TripleStore教程:为你的AI Agent构建可『时间旅行』的知识图谱
下一篇:Qwen3-ASR Web UI 实战:Gradio 在线语音转写界面与流式 Demo 一键启动教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进