|
| 1 | +--- |
| 2 | +title: Charts |
| 3 | +description: Reactive ECharts bridge with lazy loading, auto-detection, and typed options for Pyreon. |
| 4 | +--- |
| 5 | + |
| 6 | +`@pyreon/charts` provides a reactive bridge to [Apache ECharts](https://echarts.apache.org/) for Pyreon applications. Chart modules are lazy-loaded on demand -- zero bundle cost until a chart actually renders. The Canvas renderer is used by default, with SVG available as an option. |
| 7 | + |
| 8 | +<PackageBadge name="@pyreon/charts" href="/docs/charts" /> |
| 9 | + |
| 10 | +## Installation |
| 11 | + |
| 12 | +```package-install |
| 13 | +@pyreon/charts |
| 14 | +``` |
| 15 | + |
| 16 | +## Quick Start |
| 17 | + |
| 18 | +Use the `<Chart />` component to render a chart. Pass an options function that returns a standard ECharts configuration -- signal reads inside the function are tracked for reactivity. |
| 19 | + |
| 20 | +```tsx |
| 21 | +import { signal } from '@pyreon/reactivity' |
| 22 | +import { Chart } from '@pyreon/charts' |
| 23 | + |
| 24 | +function SalesChart() { |
| 25 | + const months = signal(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']) |
| 26 | + const revenue = signal([120, 200, 150, 80, 70, 110]) |
| 27 | + |
| 28 | + return ( |
| 29 | + <Chart |
| 30 | + options={() => ({ |
| 31 | + xAxis: { type: 'category', data: months() }, |
| 32 | + yAxis: { type: 'value' }, |
| 33 | + tooltip: { trigger: 'axis' }, |
| 34 | + series: [{ name: 'Revenue', type: 'bar', data: revenue() }], |
| 35 | + })} |
| 36 | + style="height: 400px" |
| 37 | + /> |
| 38 | + ) |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +The `options` prop accepts a function (not a plain object) so that signal reads are tracked. When any signal inside the function changes, the chart re-renders automatically. |
| 43 | + |
| 44 | +## API Reference |
| 45 | + |
| 46 | +### `<Chart />` |
| 47 | + |
| 48 | +The primary component for rendering charts. |
| 49 | + |
| 50 | +| Prop | Type | Description | |
| 51 | +|------|------|-------------| |
| 52 | +| `options` | `() => EChartsOption` | Function returning ECharts configuration. Signal reads are tracked for reactivity. | |
| 53 | +| `style` | `string` | Inline style string. Must include a height (ECharts requires a sized container). | |
| 54 | +| `class` | `string` | CSS class name for the container element. | |
| 55 | +| `renderer` | `'canvas' \| 'svg'` | Rendering mode. Defaults to `'canvas'`. | |
| 56 | +| `onChartReady` | `(instance: ECharts) => void` | Callback fired after the chart instance is initialized. | |
| 57 | +| `on*` | Event handlers | ECharts event bindings, e.g. `onClick`, `onMouseover`, `onLegendSelectChanged`. | |
| 58 | + |
| 59 | +```tsx |
| 60 | +<Chart |
| 61 | + options={() => ({ /* ... */ })} |
| 62 | + style="height: 300px" |
| 63 | + renderer="svg" |
| 64 | + onClick={(params) => console.log('Clicked:', params.name)} |
| 65 | + onChartReady={(instance) => console.log('Chart ready:', instance)} |
| 66 | +/> |
| 67 | +``` |
| 68 | + |
| 69 | +### `useChart<TOption>(optionsFn, config?)` |
| 70 | + |
| 71 | +A lower-level hook for programmatic control. Returns reactive signals for the chart instance and error state. |
| 72 | + |
| 73 | +```tsx |
| 74 | +import { useChart } from '@pyreon/charts' |
| 75 | + |
| 76 | +function MyChart() { |
| 77 | + const { containerRef, instance, error } = useChart(() => ({ |
| 78 | + xAxis: { type: 'category', data: ['A', 'B', 'C'] }, |
| 79 | + yAxis: { type: 'value' }, |
| 80 | + series: [{ type: 'bar', data: [10, 20, 30] }], |
| 81 | + })) |
| 82 | + |
| 83 | + return ( |
| 84 | + <div> |
| 85 | + {() => error() ? <p class="error">{error()!.message}</p> : null} |
| 86 | + <div ref={(el) => containerRef.set(el)} style="height: 400px" /> |
| 87 | + </div> |
| 88 | + ) |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +**Config options:** |
| 93 | + |
| 94 | +| Option | Type | Default | Description | |
| 95 | +|--------|------|---------|-------------| |
| 96 | +| `renderer` | `'canvas' \| 'svg'` | `'canvas'` | Rendering mode | |
| 97 | +| `notMerge` | `boolean` | `false` | Replace options entirely instead of merging | |
| 98 | +| `lazyUpdate` | `boolean` | `false` | Defer chart update to next frame | |
| 99 | + |
| 100 | +**Return value:** |
| 101 | + |
| 102 | +| Property | Type | Description | |
| 103 | +|----------|------|-------------| |
| 104 | +| `containerRef` | `Signal<HTMLElement \| null>` | Bind to a DOM element via `ref` | |
| 105 | +| `instance` | `Signal<ECharts \| null>` | The underlying ECharts instance (available after init) | |
| 106 | +| `error` | `Signal<Error \| null>` | Error signal for init or setOption failures | |
| 107 | + |
| 108 | +### Types |
| 109 | + |
| 110 | +`@pyreon/charts` re-exports all ECharts option types for strict typing: |
| 111 | + |
| 112 | +```tsx |
| 113 | +import type { |
| 114 | + ComposeOption, |
| 115 | + BarSeriesOption, |
| 116 | + LineSeriesOption, |
| 117 | + PieSeriesOption, |
| 118 | + ScatterSeriesOption, |
| 119 | + RadarSeriesOption, |
| 120 | + HeatmapSeriesOption, |
| 121 | + TreemapSeriesOption, |
| 122 | + SankeySeriesOption, |
| 123 | + GaugeSeriesOption, |
| 124 | + FunnelSeriesOption, |
| 125 | + CandlestickSeriesOption, |
| 126 | + GraphSeriesOption, |
| 127 | +} from '@pyreon/charts' |
| 128 | +``` |
| 129 | + |
| 130 | +## Auto-Detection |
| 131 | + |
| 132 | +When you provide an options function, `@pyreon/charts` inspects the configuration to determine which ECharts modules are needed: |
| 133 | + |
| 134 | +- **Series types** (`bar`, `line`, `pie`, etc.) are detected from `series[].type` |
| 135 | +- **Components** (`tooltip`, `legend`, `dataZoom`, etc.) are detected from top-level keys |
| 136 | +- **Axis types** (`category`, `value`, `time`, `log`) are detected from axis config |
| 137 | + |
| 138 | +Only the required modules are dynamically imported. A chart with `type: 'bar'` and `tooltip` will only load the bar series renderer and tooltip component -- not the full ECharts bundle. |
| 139 | + |
| 140 | +```tsx |
| 141 | +// Only loads: BarChart, TooltipComponent, GridComponent, CanvasRenderer |
| 142 | +<Chart |
| 143 | + options={() => ({ |
| 144 | + tooltip: { trigger: 'axis' }, |
| 145 | + xAxis: { type: 'category', data: labels() }, |
| 146 | + yAxis: { type: 'value' }, |
| 147 | + series: [{ type: 'bar', data: values() }], |
| 148 | + })} |
| 149 | + style="height: 300px" |
| 150 | +/> |
| 151 | +``` |
| 152 | + |
| 153 | +## Strict Typing with ComposeOption |
| 154 | + |
| 155 | +For type-safe chart configurations, use `ComposeOption<>` to narrow the options type to only the series types you use: |
| 156 | + |
| 157 | +```tsx |
| 158 | +import { useChart } from '@pyreon/charts' |
| 159 | +import type { ComposeOption, BarSeriesOption, LineSeriesOption } from '@pyreon/charts' |
| 160 | + |
| 161 | +type DashboardOption = ComposeOption<BarSeriesOption | LineSeriesOption> |
| 162 | + |
| 163 | +function Dashboard() { |
| 164 | + const chart = useChart<DashboardOption>(() => ({ |
| 165 | + xAxis: { type: 'category', data: ['Q1', 'Q2', 'Q3', 'Q4'] }, |
| 166 | + yAxis: { type: 'value' }, |
| 167 | + series: [ |
| 168 | + { type: 'bar', data: [100, 200, 150, 300] }, |
| 169 | + { type: 'line', data: [80, 170, 130, 280] }, |
| 170 | + ], |
| 171 | + })) |
| 172 | + |
| 173 | + return <div ref={(el) => chart.containerRef.set(el)} style="height: 400px" /> |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +This gives you autocomplete and type checking for the specific series options you declared. |
| 178 | + |
| 179 | +## Manual Registration |
| 180 | + |
| 181 | +For maximum tree-shaking control, use the `@pyreon/charts/manual` entry point. This disables auto-detection and requires you to register ECharts modules explicitly: |
| 182 | + |
| 183 | +```tsx |
| 184 | +import { useChart, registerModules } from '@pyreon/charts/manual' |
| 185 | +import { BarChart, LineChart } from 'echarts/charts' |
| 186 | +import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components' |
| 187 | +import { CanvasRenderer } from 'echarts/renderers' |
| 188 | + |
| 189 | +// Register once at app startup |
| 190 | +registerModules([ |
| 191 | + BarChart, |
| 192 | + LineChart, |
| 193 | + TooltipComponent, |
| 194 | + GridComponent, |
| 195 | + LegendComponent, |
| 196 | + CanvasRenderer, |
| 197 | +]) |
| 198 | + |
| 199 | +// Then use useChart / <Chart /> as normal |
| 200 | +function MyChart() { |
| 201 | + const chart = useChart(() => ({ |
| 202 | + series: [{ type: 'bar', data: [1, 2, 3] }], |
| 203 | + })) |
| 204 | + |
| 205 | + return <div ref={(el) => chart.containerRef.set(el)} style="height: 300px" /> |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +Use manual registration when you need deterministic bundle sizes or are building a library that should not auto-import ECharts modules. |
| 210 | + |
| 211 | +## Bundle Size |
| 212 | + |
| 213 | +| Import | Approximate Size (gzipped) | |
| 214 | +|--------|---------------------------| |
| 215 | +| `@pyreon/charts` (wrapper only) | ~2 KB | |
| 216 | +| + Bar chart | ~15 KB | |
| 217 | +| + Line chart | ~18 KB | |
| 218 | +| + Pie chart | ~12 KB | |
| 219 | +| + Tooltip + Legend | ~8 KB | |
| 220 | +| Full ECharts (all modules) | ~300 KB | |
| 221 | + |
| 222 | +Auto-detection ensures you only pay for what you use. A typical dashboard with 2-3 chart types loads ~40-50 KB of ECharts code. |
| 223 | + |
| 224 | +## Error Handling |
| 225 | + |
| 226 | +Both `<Chart />` and `useChart()` expose an `error` signal that captures initialization and rendering failures: |
| 227 | + |
| 228 | +```tsx |
| 229 | +import { Chart } from '@pyreon/charts' |
| 230 | + |
| 231 | +function SafeChart() { |
| 232 | + return ( |
| 233 | + <Chart |
| 234 | + options={() => ({ |
| 235 | + series: [{ type: 'bar', data: chartData() }], |
| 236 | + })} |
| 237 | + style="height: 300px" |
| 238 | + onError={(err) => console.error('Chart error:', err)} |
| 239 | + /> |
| 240 | + ) |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +With `useChart()`, check the error signal directly: |
| 245 | + |
| 246 | +```tsx |
| 247 | +import { useChart } from '@pyreon/charts' |
| 248 | + |
| 249 | +function SafeChart() { |
| 250 | + const { containerRef, error } = useChart(() => ({ |
| 251 | + series: [{ type: 'bar', data: chartData() }], |
| 252 | + })) |
| 253 | + |
| 254 | + return ( |
| 255 | + <div> |
| 256 | + {() => error() ? ( |
| 257 | + <div class="chart-error"> |
| 258 | + <p>Failed to render chart: {error()!.message}</p> |
| 259 | + </div> |
| 260 | + ) : null} |
| 261 | + <div ref={(el) => containerRef.set(el)} style="height: 300px" /> |
| 262 | + </div> |
| 263 | + ) |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +Common error scenarios: |
| 268 | +- Container element has zero height (ECharts requires a sized container) |
| 269 | +- Invalid option structure passed to `setOption` |
| 270 | +- Network failure when lazy-loading ECharts modules |
0 commit comments