- 数据库
- 后端
- 前端
【免费下载链接】tinybase
A reactive data store & sync engine.
本文围绕 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)。
六个主题及其对应的官方演示页面如下:
| 主题 | 官方演示 |
|---|---|
| Basics | LineChart 组件 |
| Styling | Styled 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 | 说明 |
|---|---|---|
XAxis | scale | 'auto' \| 'category' \| 'linear' \| 'time',显式指定 x 刻度类型,默认'auto' |
XAxis | min/max | 轴边界;time刻度下接受 number / string / Date(TimeValue) |
XAxis | ticks | 显式刻度数组(TimeValue[]),传入后刻度不再自动生成 |
XAxis | tickCount | 自动刻度数量的目标值(默认 10,见 wilkinson.ts) |
XAxis | tickFormatter | 刻度格式化;time刻度下签名为(tick: Date, timestamp: number) => string,其余为(tick: boolean \| number \| string) => string |
XAxis | timestampUnit | 'millisecond' \| 'second',声明时间戳单位(TimeXAxisProps专用) |
XAxis | title | 轴标题,覆盖从 Cell 名推断的默认标题 |
XAxis | className | 追加到 x 轴 SVG 组(类名形如x axis-dates) |
YAxis | min/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.
相关推荐
TinyBase 图表组件组合实战:用 CartesianChart 在同一张数据表中渲染多序列图表
TinyBase 图表组件组合实战:用 CartesianChart 在同一张数据表中渲染多序列图表 本文基于 TinyBase 仓库中 ui react do
数据库后端前端TinyBase `<ValuesInHtmlTable />` (Solid) 实战:用 ui-solid-dom 组件把 Store Values 渲染成响应式 HTML 表格
TinyBase <ValuesInHtmlTable / Solid 实战:用 ui solid dom 组件把 Store Values 渲染成响应式 HT
数据库后端前端TinyBase 实战指南:使用 ValuesInHtmlTable 在 React 中渲染 Store 的 Values 数据表
TinyBase 实战指南:使用 ValuesInHtmlTable 在 React 中渲染 Store 的 Values 数据表 本文基于 TinyBase
数据库后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考