@pixerate/editor-react 0.35.1 → 0.37.0

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.
@@ -1,532 +1,106 @@
1
- // src/chart/useChartEditor.ts
2
- import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
1
  import {
4
- ChartController,
5
- createChartCommands
6
- } from "@pixerate/editor/chart";
7
- function useChartController(options = {}) {
8
- const ref = useRef(null);
9
- if (!ref.current) ref.current = new ChartController(options);
10
- return ref.current;
11
- }
12
- function useChartState(controller) {
13
- return useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
14
- }
15
- function useChartEditor(controllerOrOptions = {}) {
16
- const own = useRef(null);
17
- let controller;
18
- if (controllerOrOptions instanceof ChartController) controller = controllerOrOptions;
19
- else {
20
- if (!own.current) own.current = new ChartController(controllerOrOptions);
21
- controller = own.current;
22
- }
23
- const state = useChartState(controller);
24
- const actions = useMemo(
25
- () => ({
26
- update: (patch) => controller.update(patch),
27
- setType: (type) => controller.setType(type),
28
- setValue: (seriesId, index, value) => controller.setValue(seriesId, index, value),
29
- select: (selection) => controller.select(selection),
30
- undo: () => void controller.undo(),
31
- redo: () => void controller.redo()
32
- }),
33
- [controller]
34
- );
35
- return { controller, state, spec: state.spec, selection: state.selection, ...actions };
36
- }
37
- function useChartCommands(registry, controller, options = {}) {
38
- const { prefix, group, scope } = options;
39
- useEffect(() => {
40
- if (!registry) return;
41
- return registry.register(createChartCommands(controller, { prefix, group, scope }));
42
- }, [registry, controller, prefix, group, scope]);
43
- }
44
-
45
- // src/chart/styles.ts
46
- var chartInputClass = "h-7 w-full min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50";
47
- var chartButtonClass = "inline-flex h-7 items-center justify-center gap-1 rounded-md border border-input bg-background px-2 text-xs text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
48
- var chartIconButtonClass = "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-40";
49
- var chartSegmentClass = (active) => `inline-flex h-7 flex-1 items-center justify-center rounded px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary ${active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`;
50
- var chartLabelClass = "text-[11px] font-medium text-muted-foreground";
51
- var chartSectionClass = "space-y-2 border-b border-border pb-3 last:border-b-0";
52
- var chartCellClass = "h-7 w-full min-w-[4.5rem] bg-transparent px-2 text-right text-xs tabular-nums text-foreground focus-visible:bg-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary";
2
+ ChartCanvas,
3
+ ChartCommitInput,
4
+ ChartDataGrid,
5
+ ChartInspector,
6
+ ChartPresenceLayer,
7
+ ChartSceneSvg,
8
+ chartButtonClass,
9
+ chartCellClass,
10
+ chartIconButtonClass,
11
+ chartInputClass,
12
+ chartLabelClass,
13
+ chartSectionClass,
14
+ chartSegmentClass,
15
+ renderChartMark,
16
+ useChartCommands,
17
+ useChartController,
18
+ useChartDirector,
19
+ useChartEditor,
20
+ useChartState,
21
+ useChartToolProvider
22
+ } from "../chunk-C2JWRD2B.js";
53
23
 
54
- // src/chart/ChartCanvas.tsx
55
- import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState } from "react";
56
- import {
57
- CSS_VARIABLE_CHART_THEME,
58
- chartClientToScene,
59
- chartTextBaseline,
60
- chartTextTransform,
61
- describeChartSelection,
62
- handleChartKey,
63
- layoutChart,
64
- startChartPointDrag
65
- } from "@pixerate/editor/chart";
66
- import { jsx } from "react/jsx-runtime";
67
- function renderMark(mark, fontFamily, interactive) {
68
- const common = {
69
- "data-role": mark.role,
70
- "data-chart-key": mark.target ? mark.key : void 0,
71
- "data-selected": mark.selected ? "" : void 0,
72
- opacity: mark.opacity,
73
- pointerEvents: mark.role === "selection" || !mark.target && mark.role !== "background" ? "none" : void 0,
74
- cursor: interactive && mark.target && mark.role !== "background" ? "pointer" : void 0
75
- };
76
- switch (mark.kind) {
77
- case "rect":
78
- return /* @__PURE__ */ jsx("rect", { ...common, x: mark.x, y: mark.y, width: mark.width, height: mark.height, rx: mark.radius, style: { fill: mark.fill, stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
79
- case "path":
80
- return /* @__PURE__ */ jsx(
81
- "path",
82
- {
83
- ...common,
84
- d: mark.d,
85
- strokeLinejoin: "round",
86
- strokeLinecap: "round",
87
- strokeDasharray: mark.dash,
88
- style: { fill: mark.fill ?? "none", stroke: mark.stroke, strokeWidth: mark.strokeWidth }
89
- },
90
- mark.key
91
- );
92
- case "circle":
93
- return /* @__PURE__ */ jsx("circle", { ...common, cx: mark.cx, cy: mark.cy, r: mark.r, style: { fill: mark.fill, stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
94
- case "line":
95
- return /* @__PURE__ */ jsx("line", { ...common, x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2, strokeDasharray: mark.dash, style: { stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
96
- case "text":
97
- return /* @__PURE__ */ jsx(
98
- "text",
99
- {
100
- ...common,
101
- x: mark.x,
102
- y: mark.y,
103
- textAnchor: mark.anchor,
104
- dominantBaseline: chartTextBaseline(mark.baseline),
105
- transform: chartTextTransform(mark),
106
- style: { fill: mark.fill, fontSize: mark.size, fontWeight: mark.weight, fontFamily, userSelect: "none" },
107
- children: mark.text
108
- },
109
- mark.key
110
- );
111
- }
112
- }
113
- function ChartCanvas({ controller, width = 640, height = 400, responsive = true, theme, editable = true, measureText, emptyMessage, className }) {
24
+ // src/chart/ChartEditor.tsx
25
+ import { describeChartSelection } from "@pixerate/editor/chart";
26
+ import { jsx, jsxs } from "react/jsx-runtime";
27
+ function ChartEditor({ controller, showInspector = true, showDataGrid = true, showToolbar = true, height = 380, canvasProps, className }) {
114
28
  const state = useChartState(controller);
115
- const wrapper = useRef2(null);
116
- const svg = useRef2(null);
117
- const drag = useRef2(null);
118
- const [measured, setMeasured] = useState(null);
119
- const [valueDomain, setValueDomain] = useState(null);
120
- useEffect2(() => {
121
- const el = wrapper.current;
122
- if (!responsive || !el || typeof ResizeObserver === "undefined") return;
123
- const observer = new ResizeObserver(([entry]) => {
124
- const w = Math.round(entry.contentRect.width);
125
- if (w > 0) setMeasured(w);
126
- });
127
- observer.observe(el);
128
- return () => observer.disconnect();
129
- }, [responsive]);
130
- const sceneWidth = responsive && measured || width;
131
- const fullTheme = useMemo2(() => ({ ...CSS_VARIABLE_CHART_THEME, ...theme }), [theme]);
132
- const scene = useMemo2(
133
- () => layoutChart(state.spec, { width: sceneWidth, height, theme: fullTheme, selection: editable ? state.selection : null, measureText, emptyMessage, valueDomain }),
134
- [state.spec, state.selection, sceneWidth, height, fullTheme, editable, measureText, emptyMessage, valueDomain]
135
- );
136
- const byKey = useMemo2(() => new Map(scene.marks.map((m) => [m.key, m])), [scene]);
137
- useEffect2(() => () => drag.current?.end(), []);
138
- const scenePoint = (e) => chartClientToScene(scene, svg.current.getBoundingClientRect(), e.clientX, e.clientY);
139
- const onPointerDown = (e) => {
140
- if (!editable || e.button !== 0) return;
141
- const key = e.target.closest?.("[data-chart-key]")?.getAttribute("data-chart-key");
142
- const target = key ? byKey.get(key)?.target : void 0;
143
- controller.select(target ?? null);
144
- drag.current = startChartPointDrag(controller, scene, target, scenePoint(e));
145
- if (drag.current) {
146
- setValueDomain(drag.current.valueDomain);
147
- svg.current?.setPointerCapture?.(e.pointerId);
148
- }
149
- };
150
- const onPointerMove = (e) => {
151
- if (drag.current) drag.current.move(scenePoint(e));
152
- };
153
- const endDrag = () => {
154
- drag.current?.end();
155
- drag.current = null;
156
- setValueDomain(null);
157
- };
158
- const onKeyDown = (e) => {
159
- if (editable && handleChartKey(controller, scene, e)) e.preventDefault();
160
- };
161
- const label = state.spec.title || "Chart";
162
- return /* @__PURE__ */ jsx("div", { ref: wrapper, className: `w-full ${className ?? ""}`, "data-chart-canvas": "", children: /* @__PURE__ */ jsx(
163
- "svg",
164
- {
165
- ref: svg,
166
- width: sceneWidth,
167
- height,
168
- viewBox: `0 0 ${scene.width} ${scene.height}`,
169
- role: "img",
170
- "aria-label": editable ? `${label}. ${describeChartSelection(controller, state.selection)}` : label,
171
- tabIndex: editable ? 0 : void 0,
172
- className: "block max-w-full touch-none select-none rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
173
- onPointerDown,
174
- onPointerMove,
175
- onPointerUp: endDrag,
176
- onPointerCancel: endDrag,
177
- onKeyDown,
178
- children: scene.marks.map((m) => renderMark(m, scene.theme.fontFamily, editable))
179
- }
180
- ) });
181
- }
182
-
183
- // src/chart/ChartDataGrid.tsx
184
- import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
185
- import { chartColorInputValue, chartSeriesColor, formatChartNumber } from "@pixerate/editor/chart";
186
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
187
- function ChartCommitInput({ value, onCommit, onKeyDown, onBlur, ...rest }) {
188
- const [draft, setDraft] = useState2(value);
189
- const typing = useRef3(false);
190
- useEffect3(() => {
191
- if (!typing.current) setDraft(value);
192
- }, [value]);
193
- const commit = () => {
194
- if (!typing.current) return;
195
- typing.current = false;
196
- if (draft === value) return;
197
- if (onCommit(draft) === false) setDraft(value);
198
- };
199
- return /* @__PURE__ */ jsx2(
200
- "input",
201
- {
202
- ...rest,
203
- value: draft,
204
- onChange: (e) => {
205
- typing.current = true;
206
- setDraft(e.currentTarget.value);
207
- },
208
- onBlur: (e) => {
209
- onBlur?.(e);
210
- commit();
211
- },
212
- onKeyDown: (e) => {
213
- onKeyDown?.(e);
214
- if (e.defaultPrevented) return;
215
- if (e.key === "Enter") {
216
- e.preventDefault();
217
- commit();
218
- } else if (e.key === "Escape") {
219
- typing.current = false;
220
- setDraft(value);
221
- }
222
- }
223
- }
224
- );
225
- }
226
- function ChartDataGrid({ controller, categoryHeader = "Category", readOnly = false, className }) {
227
- const { spec, selection } = useChartState(controller);
228
- const paste = (row, column) => (e) => {
229
- const text = e.clipboardData?.getData("text/plain") ?? "";
230
- if (!/[\t\n]/.test(text.replace(/\n$/, ""))) return;
231
- e.preventDefault();
232
- controller.pasteCells(row, column, text);
233
- };
234
- return /* @__PURE__ */ jsxs("div", { className: `overflow-auto rounded-md border border-border ${className ?? ""}`, "data-chart-grid": "", children: [
235
- /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-xs", children: [
236
- /* @__PURE__ */ jsx2("thead", { className: "bg-muted/50", children: /* @__PURE__ */ jsxs("tr", { children: [
237
- /* @__PURE__ */ jsx2("th", { className: "border-b border-r border-border px-2 py-1 text-left font-medium text-muted-foreground", children: categoryHeader }),
238
- spec.series.map((s, si) => {
239
- const color = chartSeriesColor(spec, si);
240
- const selected = selection?.kind === "series" && selection.seriesId === s.id;
241
- return /* @__PURE__ */ jsx2("th", { "data-series-id": s.id, className: `border-b border-r border-border px-1 py-1 font-medium ${selected ? "bg-primary/10" : ""}`, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
242
- /* @__PURE__ */ jsx2(
243
- "input",
244
- {
245
- type: "color",
246
- "aria-label": `${s.name} colour`,
247
- value: chartColorInputValue(color),
248
- disabled: readOnly,
249
- onPointerDown: () => controller.beginGesture(`color:${s.id}`),
250
- onChange: (e) => controller.updateSeries(s.id, { color: e.currentTarget.value }),
251
- onBlur: () => controller.endGesture(),
252
- className: "h-5 w-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
253
- }
254
- ),
255
- /* @__PURE__ */ jsx2(
256
- ChartCommitInput,
257
- {
258
- value: s.name,
259
- "aria-label": "Series name",
260
- onFocus: () => controller.select({ kind: "series", seriesId: s.id }),
261
- onCommit: (name) => void controller.updateSeries(s.id, { name }),
262
- className: `${chartInputClass} h-6 min-w-[5rem] border-transparent bg-transparent font-medium ${s.hidden ? "text-muted-foreground line-through" : ""}`
263
- }
264
- ),
265
- !readOnly && /* @__PURE__ */ jsxs(Fragment, { children: [
266
- /* @__PURE__ */ jsx2(
267
- "button",
268
- {
269
- type: "button",
270
- className: chartIconButtonClass,
271
- title: s.hidden ? "Show series" : "Hide series",
272
- "aria-label": s.hidden ? `Show ${s.name}` : `Hide ${s.name}`,
273
- "aria-pressed": !s.hidden,
274
- "data-action": "toggle-series",
275
- onClick: () => controller.toggleSeries(s.id),
276
- children: s.hidden ? "\u25CB" : "\u25CF"
277
- }
278
- ),
279
- /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Delete series", "aria-label": `Delete ${s.name}`, "data-action": "remove-series", onClick: () => controller.removeSeries(s.id), children: "\xD7" })
280
- ] })
281
- ] }) }, s.id);
282
- }),
283
- !readOnly && /* @__PURE__ */ jsx2("th", { className: "w-8 border-b border-border px-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Add series", "aria-label": "Add series", "data-action": "add-series", onClick: () => controller.addSeries(), children: "+" }) })
284
- ] }) }),
285
- /* @__PURE__ */ jsx2("tbody", { children: spec.categories.map((category, i) => {
286
- const rowSelected = selection?.kind === "category" && selection.index === i;
287
- return /* @__PURE__ */ jsxs("tr", { "data-category-index": i, className: rowSelected ? "bg-primary/10" : void 0, children: [
288
- /* @__PURE__ */ jsx2("th", { className: "border-b border-r border-border p-0 text-left font-normal", children: /* @__PURE__ */ jsx2(
289
- ChartCommitInput,
290
- {
291
- value: category,
292
- "aria-label": `Category ${i + 1}`,
293
- onFocus: () => controller.select({ kind: "category", index: i }),
294
- onPaste: paste(i, -1),
295
- onCommit: (label) => void controller.renameCategory(i, label),
296
- className: `${chartCellClass} text-left`
297
- }
298
- ) }),
299
- spec.series.map((s) => {
300
- const value = s.values[i];
301
- const selected = selection?.kind === "point" && selection.seriesId === s.id && selection.index === i;
302
- return /* @__PURE__ */ jsx2("td", { className: `border-b border-r border-border p-0 ${selected ? "bg-primary/10" : ""}`, children: /* @__PURE__ */ jsx2(
303
- ChartCommitInput,
304
- {
305
- value: value === null || value === void 0 ? "" : String(value),
306
- inputMode: "decimal",
307
- "aria-label": `${s.name}, ${category}`,
308
- title: value === null ? void 0 : formatChartNumber(value, spec.yAxis.format),
309
- "data-cell": `${s.id}:${i}`,
310
- onFocus: () => controller.select({ kind: "point", seriesId: s.id, index: i }),
311
- onPaste: paste(i, spec.series.indexOf(s)),
312
- onCommit: (text) => controller.setValue(s.id, i, text).ok,
313
- className: chartCellClass
314
- }
315
- ) }, s.id);
316
- }),
317
- !readOnly && /* @__PURE__ */ jsx2("td", { className: "border-b border-border px-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Delete category", "aria-label": `Delete ${category}`, "data-action": "remove-category", onClick: () => controller.removeCategory(i), children: "\xD7" }) })
318
- ] }, i);
319
- }) })
29
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-chart-editor": "", children: [
30
+ showToolbar && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "data-chart-toolbar": "", children: [
31
+ /* @__PURE__ */ jsx("button", { type: "button", className: chartButtonClass, disabled: !state.canUndo, title: state.undoLabel ? `Undo ${state.undoLabel}` : "Undo", "data-action": "undo", onClick: () => controller.undo(), children: "Undo" }),
32
+ /* @__PURE__ */ jsx("button", { type: "button", className: chartButtonClass, disabled: !state.canRedo, title: state.redoLabel ? `Redo ${state.redoLabel}` : "Redo", "data-action": "redo", onClick: () => controller.redo(), children: "Redo" }),
33
+ /* @__PURE__ */ jsx("span", { className: "ml-2 truncate text-xs text-muted-foreground", "data-chart-selection": "", "aria-live": "polite", children: describeChartSelection(controller, state.selection) })
320
34
  ] }),
321
- !readOnly && /* @__PURE__ */ jsx2("div", { className: "p-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: `${chartButtonClass} h-6 border-dashed`, "data-action": "add-category", onClick: () => controller.addCategory(), children: "+ Add category" }) })
35
+ state.issues.length > 0 && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive", "data-chart-issues": "", children: state.issues.map((i) => i.message).join(" ") }),
36
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
37
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 rounded-md border border-border bg-background p-1", children: /* @__PURE__ */ jsx(ChartCanvas, { controller, height, ...canvasProps }) }),
38
+ showInspector && /* @__PURE__ */ jsx(ChartInspector, { controller, className: "w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" })
39
+ ] }),
40
+ showDataGrid && /* @__PURE__ */ jsx(ChartDataGrid, { controller })
322
41
  ] });
323
42
  }
324
43
 
325
- // src/chart/ChartInspector.tsx
326
- import { useId } from "react";
44
+ // src/chart/ChartStreamingView.tsx
45
+ import { useEffect, useMemo, useRef, useState } from "react";
327
46
  import {
328
- CHART_CURVES,
329
- CHART_LEGEND_POSITIONS,
330
- CHART_NUMBER_STYLES,
331
- CHART_SERIES_TYPES,
332
- CHART_STACKINGS,
333
- CHART_TYPE_LABELS,
334
- CHART_TYPES,
335
- chartColorInputValue as chartColorInputValue2,
336
- chartSeriesColor as chartSeriesColor2,
337
- chartSeriesType,
338
- isCartesianChart
47
+ CSS_VARIABLE_CHART_THEME,
48
+ ChartStream,
49
+ blendChartScenes,
50
+ easeChartTween,
51
+ layoutChart,
52
+ layoutSparkline
339
53
  } from "@pixerate/editor/chart";
340
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
341
- var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
342
- function Field({ label, htmlFor, children }) {
343
- return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
344
- /* @__PURE__ */ jsx3("label", { htmlFor, className: `${chartLabelClass} w-20 shrink-0`, children: label }),
345
- /* @__PURE__ */ jsx3("div", { className: "min-w-0 flex-1", children })
346
- ] });
347
- }
348
- function Toggle({ label, checked, onChange, name }) {
349
- return /* @__PURE__ */ jsxs2("label", { className: "flex cursor-pointer items-center gap-2 text-xs text-foreground", children: [
350
- /* @__PURE__ */ jsx3("input", { type: "checkbox", className: "h-3.5 w-3.5 accent-primary", checked, "data-option": name, onChange: (e) => onChange(e.currentTarget.checked) }),
351
- label
352
- ] });
353
- }
354
- function Select({ id, value, options, onChange, name, labels }) {
355
- return /* @__PURE__ */ jsx3("select", { id, className: chartInputClass, value, "data-option": name, onChange: (e) => onChange(e.currentTarget.value), children: options.map((o) => /* @__PURE__ */ jsx3("option", { value: o, children: labels?.[o] ?? cap(o) }, o)) });
356
- }
357
- function AxisSection({ controller, axis, value, idBase, horizontal }) {
358
- const set = (patch) => controller.setAxis(axis, patch);
359
- const setFormat = (patch) => set({ format: { ...value.format, ...patch } });
360
- const bound = (key) => /* @__PURE__ */ jsx3(
361
- ChartCommitInput,
362
- {
363
- id: `${idBase}-${axis}-${key}`,
364
- value: value[key] === null ? "" : String(value[key]),
365
- placeholder: "Auto",
366
- inputMode: "decimal",
367
- "data-option": `${axis}Axis.${key}`,
368
- className: chartInputClass,
369
- onCommit: (text) => set({ [key]: text.trim() === "" ? null : Number(text) }).ok
370
- }
371
- );
372
- const name = axis === "x" ? "Category axis" : "Value axis";
373
- return /* @__PURE__ */ jsxs2("div", { className: chartSectionClass, "data-section": `axis-${axis}`, children: [
374
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
375
- /* @__PURE__ */ jsxs2("h4", { className: "text-xs font-semibold text-foreground", children: [
376
- name,
377
- /* @__PURE__ */ jsxs2("span", { className: "ml-1 font-normal text-muted-foreground", children: [
378
- "(",
379
- axis === "x" ? horizontal ? "left" : "bottom" : horizontal ? "bottom" : "left",
380
- ")"
381
- ] })
382
- ] }),
383
- /* @__PURE__ */ jsx3(Toggle, { label: "Show", name: `${axis}Axis.show`, checked: value.show, onChange: (show) => set({ show }) })
384
- ] }),
385
- /* @__PURE__ */ jsx3(Field, { label: "Title", htmlFor: `${idBase}-${axis}-title`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-${axis}-title`, value: value.title, "data-option": `${axis}Axis.title`, className: chartInputClass, onCommit: (title) => void set({ title }) }) }),
386
- /* @__PURE__ */ jsx3(Toggle, { label: "Grid lines", name: `${axis}Axis.gridlines`, checked: value.gridlines, onChange: (gridlines) => set({ gridlines }) }),
387
- axis === "y" && /* @__PURE__ */ jsxs2(Fragment2, { children: [
388
- /* @__PURE__ */ jsx3(Field, { label: "Min / max", children: /* @__PURE__ */ jsxs2("div", { className: "flex gap-1", children: [
389
- bound("min"),
390
- bound("max")
391
- ] }) }),
392
- /* @__PURE__ */ jsx3(Field, { label: "Format", htmlFor: `${idBase}-y-format`, children: /* @__PURE__ */ jsxs2("div", { className: "flex gap-1", children: [
393
- /* @__PURE__ */ jsx3(Select, { id: `${idBase}-y-format`, name: "yAxis.format.style", value: value.format.style ?? "number", options: CHART_NUMBER_STYLES, onChange: (style) => setFormat({ style }) }),
394
- /* @__PURE__ */ jsx3(
395
- ChartCommitInput,
396
- {
397
- value: value.format.decimals === void 0 ? "" : String(value.format.decimals),
398
- placeholder: "Auto",
399
- "aria-label": "Decimals",
400
- title: "Decimals",
401
- inputMode: "numeric",
402
- "data-option": "yAxis.format.decimals",
403
- className: `${chartInputClass} w-14 shrink-0`,
404
- onCommit: (text) => {
405
- const format = { ...value.format };
406
- if (text.trim() === "") delete format.decimals;
407
- else format.decimals = Number(text);
408
- return set({ format }).ok;
409
- }
410
- }
411
- )
412
- ] }) }),
413
- value.format.style === "currency" && /* @__PURE__ */ jsx3(Field, { label: "Currency", htmlFor: `${idBase}-y-currency`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-y-currency`, value: value.format.currency ?? "USD", "data-option": "yAxis.format.currency", className: chartInputClass, onCommit: (currency) => void setFormat({ currency: currency.toUpperCase() }) }) })
414
- ] })
415
- ] });
416
- }
417
- function ChartInspector({ controller, sections, className }) {
418
- const { spec, selectedSeries } = useChartState(controller);
419
- const idBase = useId();
420
- const show = (section) => !sections || sections.includes(section);
421
- const cartesian = isCartesianChart(spec.type);
422
- const types = spec.series.map((s) => chartSeriesType(spec, s));
423
- const hasBars = cartesian && types.includes("bar");
424
- const hasLines = cartesian && types.some((t) => t === "line" || t === "area");
425
- const seriesIndex = selectedSeries ? spec.series.findIndex((s) => s.id === selectedSeries.id) : -1;
426
- return /* @__PURE__ */ jsxs2("div", { className: `space-y-3 text-xs ${className ?? ""}`, "data-chart-inspector": "", children: [
427
- show("type") && /* @__PURE__ */ jsx3("div", { className: chartSectionClass, "data-section": "type", children: /* @__PURE__ */ jsx3("div", { className: "grid grid-cols-3 gap-0.5 rounded-md bg-muted p-0.5", role: "radiogroup", "aria-label": "Chart type", children: CHART_TYPES.map((type) => /* @__PURE__ */ jsx3("button", { type: "button", role: "radio", "aria-checked": spec.type === type, "data-chart-type": type, className: chartSegmentClass(spec.type === type), onClick: () => controller.setType(type), children: CHART_TYPE_LABELS[type] }, type)) }) }),
428
- show("titles") && /* @__PURE__ */ jsxs2("div", { className: chartSectionClass, "data-section": "titles", children: [
429
- /* @__PURE__ */ jsx3(Field, { label: "Title", htmlFor: `${idBase}-title`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-title`, value: spec.title, "data-option": "title", className: chartInputClass, onCommit: (title) => void controller.setTitle(title) }) }),
430
- /* @__PURE__ */ jsx3(Field, { label: "Subtitle", htmlFor: `${idBase}-subtitle`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-subtitle`, value: spec.subtitle, "data-option": "subtitle", className: chartInputClass, onCommit: (subtitle) => void controller.setSubtitle(subtitle) }) })
431
- ] }),
432
- show("options") && /* @__PURE__ */ jsxs2("div", { className: chartSectionClass, "data-section": "options", children: [
433
- cartesian && /* @__PURE__ */ jsx3(Field, { label: "Stacking", htmlFor: `${idBase}-stacking`, children: /* @__PURE__ */ jsx3(Select, { id: `${idBase}-stacking`, name: "stacking", value: spec.stacking, options: CHART_STACKINGS, labels: { none: "None", stacked: "Stacked", percent: "100%" }, onChange: (stacking) => controller.update({ stacking }) }) }),
434
- hasBars && /* @__PURE__ */ jsx3(Field, { label: "Bars", children: /* @__PURE__ */ jsx3("div", { className: "flex gap-0.5 rounded-md bg-muted p-0.5", children: ["vertical", "horizontal"].map((o) => /* @__PURE__ */ jsx3("button", { type: "button", "aria-pressed": spec.orientation === o, "data-option": `orientation.${o}`, className: chartSegmentClass(spec.orientation === o), onClick: () => controller.update({ orientation: o }), children: cap(o) }, o)) }) }),
435
- hasLines && /* @__PURE__ */ jsx3(Field, { label: "Lines", htmlFor: `${idBase}-curve`, children: /* @__PURE__ */ jsx3(Select, { id: `${idBase}-curve`, name: "curve", value: spec.curve, options: CHART_CURVES, onChange: (curve) => controller.update({ curve }) }) }),
436
- spec.type === "donut" && /* @__PURE__ */ jsx3(Field, { label: "Hole", htmlFor: `${idBase}-hole`, children: /* @__PURE__ */ jsx3(
437
- "input",
438
- {
439
- id: `${idBase}-hole`,
440
- type: "range",
441
- min: 0,
442
- max: 0.9,
443
- step: 0.05,
444
- value: spec.innerRadius,
445
- "data-option": "innerRadius",
446
- className: "w-full accent-primary",
447
- onPointerDown: () => controller.beginGesture("innerRadius"),
448
- onPointerUp: () => controller.endGesture(),
449
- onChange: (e) => controller.update({ innerRadius: Number(e.currentTarget.value) })
450
- }
451
- ) }),
452
- /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: [
453
- hasLines && /* @__PURE__ */ jsx3(Toggle, { label: "Points", name: "points", checked: spec.points, onChange: (points) => controller.update({ points }) }),
454
- /* @__PURE__ */ jsx3(Toggle, { label: "Data labels", name: "dataLabels", checked: spec.dataLabels, onChange: (dataLabels) => controller.update({ dataLabels }) })
455
- ] })
456
- ] }),
457
- show("legend") && /* @__PURE__ */ jsxs2("div", { className: chartSectionClass, "data-section": "legend", children: [
458
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
459
- /* @__PURE__ */ jsx3("h4", { className: "text-xs font-semibold text-foreground", children: "Legend" }),
460
- /* @__PURE__ */ jsx3(Toggle, { label: "Show", name: "legend.show", checked: spec.legend.show, onChange: (v) => controller.setLegend({ show: v }) })
461
- ] }),
462
- spec.legend.show && /* @__PURE__ */ jsx3("div", { className: "flex gap-0.5 rounded-md bg-muted p-0.5", children: CHART_LEGEND_POSITIONS.map((p) => /* @__PURE__ */ jsx3("button", { type: "button", "aria-pressed": spec.legend.position === p, "data-option": `legend.${p}`, className: chartSegmentClass(spec.legend.position === p), onClick: () => controller.setLegend({ position: p }), children: cap(p) }, p)) })
463
- ] }),
464
- show("axes") && cartesian && /* @__PURE__ */ jsxs2(Fragment2, { children: [
465
- /* @__PURE__ */ jsx3(AxisSection, { controller, axis: "x", value: spec.xAxis, idBase, horizontal: spec.orientation === "horizontal" }),
466
- /* @__PURE__ */ jsx3(AxisSection, { controller, axis: "y", value: spec.yAxis, idBase, horizontal: spec.orientation === "horizontal" })
467
- ] }),
468
- show("series") && selectedSeries && /* @__PURE__ */ jsxs2("div", { className: chartSectionClass, "data-section": "series", "data-series-id": selectedSeries.id, children: [
469
- /* @__PURE__ */ jsx3("h4", { className: "text-xs font-semibold text-foreground", children: "Series" }),
470
- /* @__PURE__ */ jsx3(Field, { label: "Name", htmlFor: `${idBase}-series-name`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-series-name`, value: selectedSeries.name, "data-option": "series.name", className: chartInputClass, onCommit: (name) => void controller.updateSeries(selectedSeries.id, { name }) }) }),
471
- /* @__PURE__ */ jsx3(Field, { label: "Colour", htmlFor: `${idBase}-series-color`, children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
472
- /* @__PURE__ */ jsx3(
473
- "input",
474
- {
475
- id: `${idBase}-series-color`,
476
- type: "color",
477
- value: chartColorInputValue2(chartSeriesColor2(spec, seriesIndex)),
478
- "data-option": "series.color",
479
- className: "h-7 w-9 shrink-0 cursor-pointer rounded border border-input bg-background p-0.5",
480
- onPointerDown: () => controller.beginGesture(`color:${selectedSeries.id}`),
481
- onBlur: () => controller.endGesture(),
482
- onChange: (e) => controller.updateSeries(selectedSeries.id, { color: e.currentTarget.value })
483
- }
484
- ),
485
- selectedSeries.color && /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, "data-action": "reset-color", onClick: () => controller.updateSeries(selectedSeries.id, { color: null }), children: "Palette" })
486
- ] }) }),
487
- cartesian && /* @__PURE__ */ jsx3(Field, { label: "Draw as", htmlFor: `${idBase}-series-type`, children: /* @__PURE__ */ jsxs2(
488
- "select",
489
- {
490
- id: `${idBase}-series-type`,
491
- className: chartInputClass,
492
- value: selectedSeries.type ?? "",
493
- "data-option": "series.type",
494
- onChange: (e) => controller.updateSeries(selectedSeries.id, { type: e.currentTarget.value || null }),
495
- children: [
496
- /* @__PURE__ */ jsx3("option", { value: "", children: "Chart type" }),
497
- CHART_SERIES_TYPES.map((t) => /* @__PURE__ */ jsx3("option", { value: t, children: CHART_TYPE_LABELS[t] }, t))
498
- ]
499
- }
500
- ) }),
501
- /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap items-center gap-1", children: [
502
- /* @__PURE__ */ jsx3(Toggle, { label: "Hidden", name: "series.hidden", checked: !!selectedSeries.hidden, onChange: (hidden) => controller.updateSeries(selectedSeries.id, { hidden }) }),
503
- /* @__PURE__ */ jsx3("span", { className: "flex-1" }),
504
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, disabled: seriesIndex <= 0, "aria-label": "Move series earlier", "data-action": "series-up", onClick: () => controller.moveSeries(selectedSeries.id, seriesIndex - 1), children: "\u2191" }),
505
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, disabled: seriesIndex >= spec.series.length - 1, "aria-label": "Move series later", "data-action": "series-down", onClick: () => controller.moveSeries(selectedSeries.id, seriesIndex + 1), children: "\u2193" }),
506
- /* @__PURE__ */ jsx3("button", { type: "button", className: `${chartButtonClass} text-destructive`, "data-action": "series-delete", onClick: () => controller.removeSeries(selectedSeries.id), children: "Delete" })
507
- ] })
508
- ] })
509
- ] });
54
+ import { jsx as jsx2 } from "react/jsx-runtime";
55
+ function ChartStreamingView({ source, complete = false, format = "auto", width = 640, height = 360, theme, adoptInterval = 300, tween = 450, placeholder, onIssues, className }) {
56
+ const stream = useMemo(() => new ChartStream(format), [format]);
57
+ const fullTheme = useMemo(() => ({ ...CSS_VARIABLE_CHART_THEME, ...theme }), [theme]);
58
+ const [scene, setScene] = useState(null);
59
+ const shown = useRef(null);
60
+ const lastAdopt = useRef(0);
61
+ const frame = useRef(null);
62
+ const pending = useRef(null);
63
+ useEffect(() => {
64
+ const adopt = () => {
65
+ const read = stream.update(source, complete);
66
+ if (complete && onIssues) onIssues(read.issues);
67
+ if (!read.spec) return;
68
+ const next = layoutChart(read.spec, { width, height, theme: fullTheme, valueDomain: read.valueDomain });
69
+ const from = shown.current;
70
+ lastAdopt.current = Date.now();
71
+ if (frame.current !== null) cancelAnimationFrame(frame.current);
72
+ if (!from || tween <= 0 || typeof requestAnimationFrame === "undefined") {
73
+ shown.current = next;
74
+ setScene(next);
75
+ return;
76
+ }
77
+ const start = performance.now();
78
+ const step = () => {
79
+ const t = easeChartTween((performance.now() - start) / tween);
80
+ const s = blendChartScenes(from, next, t);
81
+ shown.current = s;
82
+ setScene(s);
83
+ frame.current = t < 1 ? requestAnimationFrame(step) : null;
84
+ };
85
+ frame.current = requestAnimationFrame(step);
86
+ };
87
+ if (pending.current) clearTimeout(pending.current);
88
+ const wait = complete ? 0 : Math.max(0, adoptInterval - (Date.now() - lastAdopt.current));
89
+ if (wait === 0) adopt();
90
+ else pending.current = setTimeout(adopt, wait);
91
+ return () => {
92
+ if (pending.current) clearTimeout(pending.current);
93
+ };
94
+ }, [source, complete, stream, width, height, fullTheme, adoptInterval, tween, onIssues]);
95
+ useEffect(() => () => {
96
+ if (frame.current !== null) cancelAnimationFrame(frame.current);
97
+ }, []);
98
+ return /* @__PURE__ */ jsx2("div", { className, "data-chart-streaming": complete ? "complete" : "streaming", "aria-busy": !complete, children: scene ? /* @__PURE__ */ jsx2(ChartSceneSvg, { scene, label: complete ? void 0 : "Chart loading" }) : placeholder ?? /* @__PURE__ */ jsx2("div", { style: { height }, className: "flex items-center justify-center text-xs text-muted-foreground", children: "Drawing chart\u2026" }) });
510
99
  }
511
-
512
- // src/chart/ChartEditor.tsx
513
- import { describeChartSelection as describeChartSelection2 } from "@pixerate/editor/chart";
514
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
515
- function ChartEditor({ controller, showInspector = true, showDataGrid = true, showToolbar = true, height = 380, canvasProps, className }) {
516
- const state = useChartState(controller);
517
- return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-chart-editor": "", children: [
518
- showToolbar && /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", "data-chart-toolbar": "", children: [
519
- /* @__PURE__ */ jsx4("button", { type: "button", className: chartButtonClass, disabled: !state.canUndo, title: state.undoLabel ? `Undo ${state.undoLabel}` : "Undo", "data-action": "undo", onClick: () => controller.undo(), children: "Undo" }),
520
- /* @__PURE__ */ jsx4("button", { type: "button", className: chartButtonClass, disabled: !state.canRedo, title: state.redoLabel ? `Redo ${state.redoLabel}` : "Redo", "data-action": "redo", onClick: () => controller.redo(), children: "Redo" }),
521
- /* @__PURE__ */ jsx4("span", { className: "ml-2 truncate text-xs text-muted-foreground", "data-chart-selection": "", "aria-live": "polite", children: describeChartSelection2(controller, state.selection) })
522
- ] }),
523
- state.issues.length > 0 && /* @__PURE__ */ jsx4("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive", "data-chart-issues": "", children: state.issues.map((i) => i.message).join(" ") }),
524
- /* @__PURE__ */ jsxs3("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
525
- /* @__PURE__ */ jsx4("div", { className: "min-w-0 flex-1 rounded-md border border-border bg-background p-1", children: /* @__PURE__ */ jsx4(ChartCanvas, { controller, height, ...canvasProps }) }),
526
- showInspector && /* @__PURE__ */ jsx4(ChartInspector, { controller, className: "w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" })
527
- ] }),
528
- showDataGrid && /* @__PURE__ */ jsx4(ChartDataGrid, { controller })
529
- ] });
100
+ function ChartSparkline({ values, width = 80, height = 20, label, className, ...options }) {
101
+ const scene = useMemo(() => layoutSparkline(values, { ...options, width, height }), [values, width, height, options.kind, options.color, options.negativeColor, options.markers, options.highLow, options.min, options.max]);
102
+ const present = values.filter((v) => v !== null);
103
+ return /* @__PURE__ */ jsx2(ChartSceneSvg, { scene, label: label ?? (present.length ? `Trend from ${present[0]} to ${present[present.length - 1]}` : "No data"), className: `inline-block align-middle ${className ?? ""}` });
530
104
  }
531
105
  export {
532
106
  ChartCanvas,
@@ -534,6 +108,10 @@ export {
534
108
  ChartDataGrid,
535
109
  ChartEditor,
536
110
  ChartInspector,
111
+ ChartPresenceLayer,
112
+ ChartSceneSvg,
113
+ ChartSparkline,
114
+ ChartStreamingView,
537
115
  chartButtonClass,
538
116
  chartCellClass,
539
117
  chartIconButtonClass,
@@ -541,9 +119,12 @@ export {
541
119
  chartLabelClass,
542
120
  chartSectionClass,
543
121
  chartSegmentClass,
122
+ renderChartMark,
544
123
  useChartCommands,
545
124
  useChartController,
125
+ useChartDirector,
546
126
  useChartEditor,
547
- useChartState
127
+ useChartState,
128
+ useChartToolProvider
548
129
  };
549
130
  //# sourceMappingURL=index.js.map