@opendata-ai/openchart-react 6.3.0 → 6.5.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.
- package/dist/index.d.ts +17 -5
- package/dist/index.js +72 -13
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -764
- package/package.json +4 -4
- package/src/Chart.tsx +104 -18
- package/src/DataTable.tsx +1 -1
- package/src/Graph.tsx +1 -1
- package/src/__tests__/Chart.test.tsx +6 -6
- package/src/__tests__/DataTable.test.tsx +2 -2
- package/src/__tests__/Graph.test.tsx +4 -4
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-react",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"description": "React components for openchart: <Chart />, <DataTable />, <VizThemeProvider />",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@opendata-ai/openchart-core": "6.
|
|
53
|
-
"@opendata-ai/openchart-engine": "6.
|
|
54
|
-
"@opendata-ai/openchart-vanilla": "6.
|
|
52
|
+
"@opendata-ai/openchart-core": "6.5.0",
|
|
53
|
+
"@opendata-ai/openchart-engine": "6.5.0",
|
|
54
|
+
"@opendata-ai/openchart-vanilla": "6.5.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"react": ">=18.0.0",
|
package/src/Chart.tsx
CHANGED
|
@@ -10,14 +10,33 @@ import type {
|
|
|
10
10
|
ChartEventHandlers,
|
|
11
11
|
ChartSpec,
|
|
12
12
|
DarkMode,
|
|
13
|
+
ElementRef,
|
|
13
14
|
GraphSpec,
|
|
14
15
|
LayerSpec,
|
|
15
16
|
ThemeConfig,
|
|
16
17
|
} from '@opendata-ai/openchart-core';
|
|
17
18
|
import { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type CSSProperties,
|
|
21
|
+
forwardRef,
|
|
22
|
+
useCallback,
|
|
23
|
+
useEffect,
|
|
24
|
+
useImperativeHandle,
|
|
25
|
+
useRef,
|
|
26
|
+
} from 'react';
|
|
19
27
|
import { useVizDarkMode, useVizTheme } from './ThemeContext';
|
|
20
28
|
|
|
29
|
+
export interface ChartHandle {
|
|
30
|
+
/** Get the currently selected element, or null if none. */
|
|
31
|
+
getSelectedElement(): ElementRef | null;
|
|
32
|
+
/** Programmatically select an element. */
|
|
33
|
+
select(ref: ElementRef): void;
|
|
34
|
+
/** Deselect the current element. */
|
|
35
|
+
deselect(): void;
|
|
36
|
+
/** The underlying chart instance (null until mounted). */
|
|
37
|
+
readonly instance: ChartInstance | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
export interface ChartProps extends ChartEventHandlers {
|
|
22
41
|
/** The visualization spec to render. */
|
|
23
42
|
spec: ChartSpec | LayerSpec | GraphSpec;
|
|
@@ -27,6 +46,8 @@ export interface ChartProps extends ChartEventHandlers {
|
|
|
27
46
|
darkMode?: DarkMode;
|
|
28
47
|
/** Callback when a data point is clicked. @deprecated Use onMarkClick instead. */
|
|
29
48
|
onDataPointClick?: (data: Record<string, unknown>) => void;
|
|
49
|
+
/** The currently selected element (controlled). */
|
|
50
|
+
selectedElement?: ElementRef;
|
|
30
51
|
/** CSS class name for the wrapper div. */
|
|
31
52
|
className?: string;
|
|
32
53
|
/** Inline styles for the wrapper div. */
|
|
@@ -40,21 +61,28 @@ export interface ChartProps extends ChartEventHandlers {
|
|
|
40
61
|
* as SVG inside a wrapper div. Spec changes trigger re-renders via the
|
|
41
62
|
* vanilla adapter's update() method.
|
|
42
63
|
*/
|
|
43
|
-
export function Chart(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
export const Chart = forwardRef<ChartHandle, ChartProps>(function Chart(
|
|
65
|
+
{
|
|
66
|
+
spec,
|
|
67
|
+
theme: themeProp,
|
|
68
|
+
darkMode,
|
|
69
|
+
onDataPointClick,
|
|
70
|
+
onMarkClick,
|
|
71
|
+
onMarkHover,
|
|
72
|
+
onMarkLeave,
|
|
73
|
+
onLegendToggle,
|
|
74
|
+
onAnnotationClick,
|
|
75
|
+
onAnnotationEdit,
|
|
76
|
+
onEdit,
|
|
77
|
+
onSelect,
|
|
78
|
+
onDeselect,
|
|
79
|
+
onTextEdit,
|
|
80
|
+
selectedElement: selectedElementProp,
|
|
81
|
+
className,
|
|
82
|
+
style,
|
|
83
|
+
},
|
|
84
|
+
ref,
|
|
85
|
+
) {
|
|
58
86
|
const contextTheme = useVizTheme();
|
|
59
87
|
const contextDarkMode = useVizDarkMode();
|
|
60
88
|
const theme = themeProp ?? contextTheme;
|
|
@@ -75,6 +103,9 @@ export function Chart({
|
|
|
75
103
|
onAnnotationClick?: ChartProps['onAnnotationClick'];
|
|
76
104
|
onAnnotationEdit?: ChartProps['onAnnotationEdit'];
|
|
77
105
|
onEdit?: ChartProps['onEdit'];
|
|
106
|
+
onSelect?: ChartProps['onSelect'];
|
|
107
|
+
onDeselect?: ChartProps['onDeselect'];
|
|
108
|
+
onTextEdit?: ChartProps['onTextEdit'];
|
|
78
109
|
}>({});
|
|
79
110
|
handlersRef.current = {
|
|
80
111
|
onDataPointClick,
|
|
@@ -85,6 +116,9 @@ export function Chart({
|
|
|
85
116
|
onAnnotationClick,
|
|
86
117
|
onAnnotationEdit,
|
|
87
118
|
onEdit,
|
|
119
|
+
onSelect,
|
|
120
|
+
onDeselect,
|
|
121
|
+
onTextEdit,
|
|
88
122
|
};
|
|
89
123
|
|
|
90
124
|
// Stable callback wrappers that read from refs
|
|
@@ -123,6 +157,39 @@ export function Chart({
|
|
|
123
157
|
(edit: import('@opendata-ai/openchart-core').ElementEdit) => handlersRef.current.onEdit?.(edit),
|
|
124
158
|
[],
|
|
125
159
|
);
|
|
160
|
+
const stableOnSelect = useCallback(
|
|
161
|
+
(element: ElementRef) => handlersRef.current.onSelect?.(element),
|
|
162
|
+
[],
|
|
163
|
+
);
|
|
164
|
+
const stableOnDeselect = useCallback(
|
|
165
|
+
(element: ElementRef) => handlersRef.current.onDeselect?.(element),
|
|
166
|
+
[],
|
|
167
|
+
);
|
|
168
|
+
const stableOnTextEdit = useCallback(
|
|
169
|
+
(element: ElementRef, oldText: string, newText: string) =>
|
|
170
|
+
handlersRef.current.onTextEdit?.(element, oldText, newText),
|
|
171
|
+
[],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Expose imperative handle for ref-based control
|
|
175
|
+
useImperativeHandle(
|
|
176
|
+
ref,
|
|
177
|
+
() => ({
|
|
178
|
+
getSelectedElement() {
|
|
179
|
+
return chartRef.current?.getSelectedElement() ?? null;
|
|
180
|
+
},
|
|
181
|
+
select(elementRef: ElementRef) {
|
|
182
|
+
chartRef.current?.select(elementRef);
|
|
183
|
+
},
|
|
184
|
+
deselect() {
|
|
185
|
+
chartRef.current?.deselect();
|
|
186
|
+
},
|
|
187
|
+
get instance() {
|
|
188
|
+
return chartRef.current;
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
[],
|
|
192
|
+
);
|
|
126
193
|
|
|
127
194
|
// Mount chart and recreate when theme/darkMode change.
|
|
128
195
|
// Event handlers use stable refs so they don't trigger recreation.
|
|
@@ -145,6 +212,10 @@ export function Chart({
|
|
|
145
212
|
// avoids adding unstable prop references to the effect deps.
|
|
146
213
|
...(handlersRef.current.onAnnotationEdit ? { onAnnotationEdit: stableOnAnnotationEdit } : {}),
|
|
147
214
|
...(handlersRef.current.onEdit ? { onEdit: stableOnEdit } : {}),
|
|
215
|
+
...(handlersRef.current.onSelect ? { onSelect: stableOnSelect } : {}),
|
|
216
|
+
...(handlersRef.current.onDeselect ? { onDeselect: stableOnDeselect } : {}),
|
|
217
|
+
...(handlersRef.current.onTextEdit ? { onTextEdit: stableOnTextEdit } : {}),
|
|
218
|
+
...(selectedElementProp ? { selectedElement: selectedElementProp } : {}),
|
|
148
219
|
responsive: true,
|
|
149
220
|
};
|
|
150
221
|
|
|
@@ -168,6 +239,9 @@ export function Chart({
|
|
|
168
239
|
stableOnMarkHover,
|
|
169
240
|
stableOnMarkLeave,
|
|
170
241
|
stableOnAnnotationEdit,
|
|
242
|
+
stableOnSelect,
|
|
243
|
+
stableOnDeselect,
|
|
244
|
+
stableOnTextEdit,
|
|
171
245
|
]);
|
|
172
246
|
|
|
173
247
|
// Update chart when spec changes
|
|
@@ -182,11 +256,23 @@ export function Chart({
|
|
|
182
256
|
}
|
|
183
257
|
}, [spec]);
|
|
184
258
|
|
|
259
|
+
// Handle selectedElement prop changes separately (like Vue/Svelte adapters)
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const chart = chartRef.current;
|
|
262
|
+
if (!chart || !chart.select) return;
|
|
263
|
+
|
|
264
|
+
if (selectedElementProp) {
|
|
265
|
+
chart.select(selectedElementProp);
|
|
266
|
+
} else if (chart.getSelectedElement?.()) {
|
|
267
|
+
chart.deselect();
|
|
268
|
+
}
|
|
269
|
+
}, [selectedElementProp]);
|
|
270
|
+
|
|
185
271
|
return (
|
|
186
272
|
<div
|
|
187
273
|
ref={containerRef}
|
|
188
|
-
className={className ? `
|
|
274
|
+
className={className ? `oc-chart-root ${className}` : 'oc-chart-root'}
|
|
189
275
|
style={style}
|
|
190
276
|
/>
|
|
191
277
|
);
|
|
192
|
-
}
|
|
278
|
+
});
|
package/src/DataTable.tsx
CHANGED
|
@@ -158,7 +158,7 @@ export function DataTable({
|
|
|
158
158
|
return (
|
|
159
159
|
<div
|
|
160
160
|
ref={containerRef}
|
|
161
|
-
className={className ? `
|
|
161
|
+
className={className ? `oc-table-root ${className}` : 'oc-table-root'}
|
|
162
162
|
style={style}
|
|
163
163
|
/>
|
|
164
164
|
);
|
package/src/Graph.tsx
CHANGED
|
@@ -246,7 +246,7 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
|
|
|
246
246
|
return (
|
|
247
247
|
<div
|
|
248
248
|
ref={containerRef}
|
|
249
|
-
className={className ? `
|
|
249
|
+
className={className ? `oc-graph-root ${className}` : 'oc-graph-root'}
|
|
250
250
|
style={style}
|
|
251
251
|
/>
|
|
252
252
|
);
|
|
@@ -68,32 +68,32 @@ describe('<Chart />', () => {
|
|
|
68
68
|
const { container } = await renderChart({ spec: lineSpec });
|
|
69
69
|
const svg = container.querySelector('svg');
|
|
70
70
|
expect(svg).not.toBeNull();
|
|
71
|
-
expect(svg?.getAttribute('class')).toBe('
|
|
71
|
+
expect(svg?.getAttribute('class')).toBe('oc-chart');
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
it('renders chrome text elements', async () => {
|
|
75
75
|
const { container } = await renderChart({ spec: lineSpec });
|
|
76
76
|
|
|
77
|
-
const title = container.querySelector('.
|
|
77
|
+
const title = container.querySelector('.oc-title');
|
|
78
78
|
expect(title).not.toBeNull();
|
|
79
79
|
expect(title?.textContent).toBe('GDP Growth');
|
|
80
80
|
|
|
81
|
-
const subtitle = container.querySelector('.
|
|
81
|
+
const subtitle = container.querySelector('.oc-subtitle');
|
|
82
82
|
expect(subtitle?.textContent).toBe('US vs UK over time');
|
|
83
83
|
|
|
84
|
-
const source = container.querySelector('.
|
|
84
|
+
const source = container.querySelector('.oc-source');
|
|
85
85
|
expect(source?.textContent).toBe('World Bank');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('spec changes trigger re-render', async () => {
|
|
89
89
|
const { container, rerender } = await renderChart({ spec: lineSpec });
|
|
90
90
|
|
|
91
|
-
const titleBefore = container.querySelector('.
|
|
91
|
+
const titleBefore = container.querySelector('.oc-title');
|
|
92
92
|
expect(titleBefore?.textContent).toBe('GDP Growth');
|
|
93
93
|
|
|
94
94
|
rerender(<Chart spec={barSpec} />);
|
|
95
95
|
await waitFor(() => {
|
|
96
|
-
expect(container.querySelector('.
|
|
96
|
+
expect(container.querySelector('.oc-title')?.textContent).toBe('Updated Title');
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -77,12 +77,12 @@ describe('<DataTable />', () => {
|
|
|
77
77
|
it('spec changes trigger re-render', async () => {
|
|
78
78
|
const { container, rerender } = await renderTable({ spec: tableSpec });
|
|
79
79
|
|
|
80
|
-
const titleBefore = container.querySelector('.
|
|
80
|
+
const titleBefore = container.querySelector('.oc-table-title');
|
|
81
81
|
expect(titleBefore?.textContent).toBe('People Table');
|
|
82
82
|
|
|
83
83
|
rerender(<DataTable spec={updatedSpec} />);
|
|
84
84
|
await waitFor(() => {
|
|
85
|
-
expect(container.querySelector('.
|
|
85
|
+
expect(container.querySelector('.oc-table-title')?.textContent).toBe('Updated Table');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
const headersAfter = container.querySelectorAll('thead th');
|
|
@@ -73,23 +73,23 @@ describe('<Graph />', () => {
|
|
|
73
73
|
it('renders chrome text elements', async () => {
|
|
74
74
|
const { container } = await renderGraph({ spec: basicSpec });
|
|
75
75
|
|
|
76
|
-
const title = container.querySelector('.
|
|
76
|
+
const title = container.querySelector('.oc-title');
|
|
77
77
|
expect(title).not.toBeNull();
|
|
78
78
|
expect(title?.textContent).toBe('Test Graph');
|
|
79
79
|
|
|
80
|
-
const subtitle = container.querySelector('.
|
|
80
|
+
const subtitle = container.querySelector('.oc-subtitle');
|
|
81
81
|
expect(subtitle?.textContent).toBe('A simple test graph');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it('spec changes trigger re-render', async () => {
|
|
85
85
|
const { container, rerender } = await renderGraph({ spec: basicSpec });
|
|
86
86
|
|
|
87
|
-
const titleBefore = container.querySelector('.
|
|
87
|
+
const titleBefore = container.querySelector('.oc-title');
|
|
88
88
|
expect(titleBefore?.textContent).toBe('Test Graph');
|
|
89
89
|
|
|
90
90
|
rerender(<Graph spec={updatedSpec} />);
|
|
91
91
|
await waitFor(() => {
|
|
92
|
-
expect(container.querySelector('.
|
|
92
|
+
expect(container.querySelector('.oc-title')?.textContent).toBe('Updated Graph');
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
package/src/index.ts
CHANGED
|
@@ -63,7 +63,7 @@ export {
|
|
|
63
63
|
validateSpec,
|
|
64
64
|
} from '@opendata-ai/openchart-engine';
|
|
65
65
|
|
|
66
|
-
export type { ChartProps } from './Chart';
|
|
66
|
+
export type { ChartHandle, ChartProps } from './Chart';
|
|
67
67
|
// Components
|
|
68
68
|
export { Chart } from './Chart';
|
|
69
69
|
export type { DataTableProps } from './DataTable';
|