@particle-academy/fancy-echarts 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,18 +6,18 @@ React component library wrapping [Apache ECharts](https://echarts.apache.org/) w
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# npm
|
|
9
|
-
npm install @particle-academy/fancy-echarts
|
|
9
|
+
npm install @particle-academy/fancy-echarts
|
|
10
10
|
|
|
11
11
|
# pnpm
|
|
12
|
-
pnpm add @particle-academy/fancy-echarts
|
|
12
|
+
pnpm add @particle-academy/fancy-echarts
|
|
13
13
|
|
|
14
14
|
# yarn
|
|
15
|
-
yarn add @particle-academy/fancy-echarts
|
|
15
|
+
yarn add @particle-academy/fancy-echarts
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`echarts` is a peer dependency
|
|
18
|
+
`echarts` is a peer dependency. **npm 7+** and **yarn 3+** install peer deps automatically — nothing extra needed. **pnpm** needs `auto-install-peers=true` in `.npmrc`, or run `pnpm add echarts` once.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
3D charts (globe, surface, scatter3D, bar3D) need the optional `echarts-gl` peer dep:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install echarts-gl
|
|
@@ -25,7 +25,7 @@ npm install echarts-gl
|
|
|
25
25
|
|
|
26
26
|
**Peer dependencies:** `react >= 18`, `react-dom >= 18`, `echarts >= 5.5`, `echarts-gl >= 2.0` (optional, only needed for 3D)
|
|
27
27
|
|
|
28
|
-
> **Breaking change in 2.0** — `echarts` and `echarts-gl` moved from regular dependencies to peer dependencies.
|
|
28
|
+
> **Breaking change in 2.0** — `echarts` and `echarts-gl` moved from regular dependencies to peer dependencies. If you're on npm 7+ or yarn 3+ the upgrade is transparent. On older tooling or pnpm without auto-install, run `npm install echarts` (and `echarts-gl` if you use 3D charts) once.
|
|
29
29
|
|
|
30
30
|
## Quick Start
|
|
31
31
|
|
|
@@ -73,6 +73,177 @@ option={{
|
|
|
73
73
|
|
|
74
74
|
This is consumer responsibility — the wrapper does not introspect `option` to identify HTML-bearing fields.
|
|
75
75
|
|
|
76
|
+
## Recipes: ECharts × react-fancy
|
|
77
|
+
|
|
78
|
+
`fancy-echarts` is a thin wrapper — every interaction surface is reachable through `onEvents`, `useECharts().instance`, and `tooltip.formatter`. These recipes cover the patterns that hold up across charts.
|
|
79
|
+
|
|
80
|
+
### Wrap any chart with a Popover, ContextMenu, and Action button
|
|
81
|
+
|
|
82
|
+
The pattern that scales: a single `ChartFrame` wrapper that gives every chart an info popover next to the title, action buttons in the header, and a right-click context menu on the body. Build it once, reuse for every chart type:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Card, Popover, Action, Badge, ContextMenu, Icon, useToast } from "@particle-academy/react-fancy";
|
|
86
|
+
|
|
87
|
+
function ChartFrame({ title, info, actions = [], onExport, extraMenu, children }) {
|
|
88
|
+
const { toast } = useToast();
|
|
89
|
+
return (
|
|
90
|
+
<Card>
|
|
91
|
+
<Card.Header>
|
|
92
|
+
<div className="flex items-center justify-between">
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<h3 className="font-semibold">{title}</h3>
|
|
95
|
+
<Popover hover placement="right">
|
|
96
|
+
<Popover.Trigger>
|
|
97
|
+
<button><Icon name="info" size="sm" /></button>
|
|
98
|
+
</Popover.Trigger>
|
|
99
|
+
<Popover.Content>
|
|
100
|
+
<p className="w-64 text-sm text-zinc-500">{info}</p>
|
|
101
|
+
</Popover.Content>
|
|
102
|
+
</Popover>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="flex gap-2">
|
|
105
|
+
{actions.map((a) => <Action key={a.label} size="sm" onClick={a.onClick}>{a.label}</Action>)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</Card.Header>
|
|
109
|
+
<Card.Body>
|
|
110
|
+
<ContextMenu>
|
|
111
|
+
<ContextMenu.Trigger><div>{children}</div></ContextMenu.Trigger>
|
|
112
|
+
<ContextMenu.Content>
|
|
113
|
+
<ContextMenu.Item onClick={onExport}>Export CSV</ContextMenu.Item>
|
|
114
|
+
<ContextMenu.Item onClick={() => toast({ title: "Exported PNG" })}>Export PNG</ContextMenu.Item>
|
|
115
|
+
{extraMenu && <ContextMenu.Separator />}
|
|
116
|
+
{extraMenu}
|
|
117
|
+
</ContextMenu.Content>
|
|
118
|
+
</ContextMenu>
|
|
119
|
+
</Card.Body>
|
|
120
|
+
</Card>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
<ChartFrame title="Revenue" info="..." actions={[{ label: "Forecast", onClick: ... }]}>
|
|
125
|
+
<EChart option={lineOption} style={{ height: 320 }} />
|
|
126
|
+
</ChartFrame>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Three reasons this works: `<ContextMenu.Trigger>` wraps the chart canvas without injecting DOM into ECharts, `Popover hover` is independent of any chart event, and the chart inside is unaware of the frame.
|
|
130
|
+
|
|
131
|
+
### Right-click a data point → open a Modal drill-down
|
|
132
|
+
|
|
133
|
+
`onEvents.contextmenu` fires per-datum with the same `params` shape as `click`. Call `params.event.event.preventDefault()` to suppress the browser menu, then drive your own state.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<EChart
|
|
137
|
+
option={barOption}
|
|
138
|
+
onEvents={{
|
|
139
|
+
contextmenu: (params) => {
|
|
140
|
+
params.event.event.preventDefault();
|
|
141
|
+
const region = regions.find((r) => r.name === params.name);
|
|
142
|
+
setDrill(region); // opens a <Modal>
|
|
143
|
+
},
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
If you also wrap the chart in `<ContextMenu>` (chart-level actions), this still works — the per-bar `contextmenu` event fires *and* the wrapper menu opens. Suppress one or the other based on which event the cursor was over.
|
|
149
|
+
|
|
150
|
+
### Click-to-toast and hover-popover on a slice
|
|
151
|
+
|
|
152
|
+
`onEvents.click` for actions, native ECharts `tooltip` for hover details — they don't conflict.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<EChart
|
|
156
|
+
option={{
|
|
157
|
+
tooltip: { trigger: "item", formatter: "{b}<br/>${c}k ({d}%)" },
|
|
158
|
+
series: [{ type: "pie", data: categories }],
|
|
159
|
+
}}
|
|
160
|
+
onEvents={{
|
|
161
|
+
click: (p) => toast({ title: `${p.name}: ${p.percent}%`, variant: "info" }),
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Rich HTML tooltips with sanitized data
|
|
167
|
+
|
|
168
|
+
`tooltip.formatter` accepts a function returning an HTML string. Compose colored arrows, badges, and metadata — but always escape user input.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
tooltip: {
|
|
172
|
+
trigger: "axis",
|
|
173
|
+
formatter: (params) => {
|
|
174
|
+
const p = params[0];
|
|
175
|
+
const region = regions.find((r) => r.name === p.name);
|
|
176
|
+
const arrow = region.growth >= 0 ? "▲" : "▼";
|
|
177
|
+
const color = region.growth >= 15 ? "#10b981" : "#3b82f6";
|
|
178
|
+
return `<div style="font-weight:600">${escape(p.name)}</div>
|
|
179
|
+
<div>Revenue: <b>$${p.value.toLocaleString()}k</b></div>
|
|
180
|
+
<div style="color:${color}">${arrow} ${region.growth}% YoY</div>`;
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
For interactive content inside a tooltip (buttons that fire React state), use a `Popover` keyed off `onEvents.mouseover` instead — ECharts tooltips are detached HTML and lose React handlers.
|
|
186
|
+
|
|
187
|
+
### Per-datum styling from an array of objects
|
|
188
|
+
|
|
189
|
+
Pass `data` as `{ value, itemStyle }[]` to color each bar/slice individually based on a property of the underlying record:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
series: [{
|
|
193
|
+
type: "bar",
|
|
194
|
+
data: regions.map((r) => ({
|
|
195
|
+
value: r.value,
|
|
196
|
+
itemStyle: {
|
|
197
|
+
color: r.growth >= 15 ? "#10b981" : "#3b82f6",
|
|
198
|
+
borderRadius: [0, 6, 6, 0],
|
|
199
|
+
},
|
|
200
|
+
})),
|
|
201
|
+
}]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Wrap the page in `<Toast.Provider>` for toast feedback
|
|
205
|
+
|
|
206
|
+
`useToast()` only works inside a provider. Wrap the demo's outermost element so chart-event toasts have somewhere to render:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
export function Showcase() {
|
|
210
|
+
return (
|
|
211
|
+
<Toast.Provider position="bottom-right">
|
|
212
|
+
<ShowcaseInner />
|
|
213
|
+
</Toast.Provider>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Keep the chart `option` memoized
|
|
219
|
+
|
|
220
|
+
Re-creating the option object on every render forces ECharts to diff and reapply. `useMemo` keeps the reference stable so the chart only updates when its inputs actually change:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
const lineOption = useMemo(() => ({ /* ... */ }), [revenue, expenses]);
|
|
224
|
+
<EChart option={lineOption} />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Theme-toggling is a prop swap
|
|
228
|
+
|
|
229
|
+
Pass `theme="dark-preset"` (after `registerBuiltinThemes()`) or `theme="light"` and re-render — the wrapper rebuilds the chart with the new theme automatically. No `dispose()` calls needed.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const [theme, setTheme] = useState<"light" | "dark-preset">("light");
|
|
233
|
+
|
|
234
|
+
<Action onClick={() => setTheme((t) => t === "light" ? "dark-preset" : "light")}>
|
|
235
|
+
Toggle theme
|
|
236
|
+
</Action>
|
|
237
|
+
<EChart theme={theme} option={option} />
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Pitfalls
|
|
241
|
+
|
|
242
|
+
- **Badge takes `color`, not `variant`.** Valid colors: `zinc | red | blue | green | amber | violet | rose`. The `variant` prop selects the *style* (`soft | solid | outline`), not the semantic intent. Don't confuse this with `Toast.toast({ variant: "success" })`, which uses semantic names.
|
|
243
|
+
- **`<ContextMenu.Trigger>` needs a single DOM child.** Wrap `<EChart>` in a plain `<div>` if you have additional siblings (or none — Trigger forwards refs through the wrapper).
|
|
244
|
+
- **`params.event` vs `params.event.event`.** ECharts wraps the native event. Call `params.event.event.preventDefault()` to stop the browser context menu, not `params.event.preventDefault()`.
|
|
245
|
+
- **Don't render React inside `tooltip.formatter`.** Returning an HTML string is fine; expecting React state or handlers to attach to that HTML is not. For interactive tooltips, use `Popover` driven by `onEvents.mouseover` / `mouseout` instead.
|
|
246
|
+
|
|
76
247
|
## Documentation
|
|
77
248
|
|
|
78
249
|
Full component documentation is available in the [docs/](docs/) folder:
|