@opendata-ai/openchart-svelte 8.0.0-rc.2 → 8.0.0-rc.21
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/Chart.svelte +4 -0
- package/dist/Chart.svelte.d.ts +1 -0
- package/dist/Chart.svelte.d.ts.map +1 -1
- package/dist/GeoMap.svelte +8 -10
- package/dist/GeoMap.svelte.d.ts +3 -7
- package/dist/GeoMap.svelte.d.ts.map +1 -1
- package/dist/Graph.svelte +135 -12
- package/dist/Graph.svelte.d.ts +56 -3
- package/dist/Graph.svelte.d.ts.map +1 -1
- package/dist/__tests__/Graph.test.js +50 -0
- package/dist/composables/useGraph.svelte.d.ts +66 -9
- package/dist/composables/useGraph.svelte.d.ts.map +1 -1
- package/dist/composables/useGraph.svelte.js +48 -8
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +5 -6
- package/src/BarList.svelte +0 -95
- package/src/Chart.svelte +0 -192
- package/src/ChartStory.svelte +0 -116
- package/src/DataTable.svelte +0 -132
- package/src/GeoMap.svelte +0 -91
- package/src/Graph.svelte +0 -119
- package/src/Sankey.svelte +0 -94
- package/src/ThemeProvider.svelte +0 -27
- package/src/TileMap.svelte +0 -112
- package/src/Visualization.svelte +0 -54
- package/src/__tests__/Chart.test.ts +0 -134
- package/src/__tests__/ChartStory.test.ts +0 -64
- package/src/__tests__/ChartStoryHarness.svelte +0 -13
- package/src/__tests__/DataTable.test.ts +0 -126
- package/src/__tests__/Graph.test.ts +0 -130
- package/src/__tests__/ThemeContext.test.ts +0 -106
- package/src/__tests__/composables.test.ts +0 -101
- package/src/__tests__/useTableState.test.ts +0 -134
- package/src/composables/useChart.svelte.ts +0 -108
- package/src/composables/useDarkMode.svelte.ts +0 -51
- package/src/composables/useGraph.svelte.ts +0 -115
- package/src/composables/useTable.svelte.ts +0 -85
- package/src/composables/useTableState.svelte.ts +0 -78
- package/src/context.ts +0 -35
- package/src/index.ts +0 -100
- package/src/types.ts +0 -149
package/src/Graph.svelte
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Graph component: Svelte 5 wrapper around the vanilla adapter.
|
|
3
|
-
|
|
4
|
-
Mounts a graph instance on render, updates when spec changes,
|
|
5
|
-
and cleans up on unmount. All heavy lifting is done by the vanilla
|
|
6
|
-
createGraph() function.
|
|
7
|
-
|
|
8
|
-
Exposes imperative methods via component exports for programmatic
|
|
9
|
-
control (search, zoom, select).
|
|
10
|
-
-->
|
|
11
|
-
<script lang="ts">
|
|
12
|
-
import type { DarkMode, GraphSpec, ThemeConfig } from '@opendata-ai/openchart-core';
|
|
13
|
-
import {
|
|
14
|
-
createGraph,
|
|
15
|
-
type GraphInstance,
|
|
16
|
-
type GraphMountOptions,
|
|
17
|
-
} from '@opendata-ai/openchart-vanilla';
|
|
18
|
-
import { onMount, untrack } from 'svelte';
|
|
19
|
-
import { getVizDarkMode, getVizTheme } from './context.js';
|
|
20
|
-
|
|
21
|
-
let {
|
|
22
|
-
spec,
|
|
23
|
-
theme,
|
|
24
|
-
darkMode,
|
|
25
|
-
onnodeclick,
|
|
26
|
-
onnodedoubleclick,
|
|
27
|
-
onselectionchange,
|
|
28
|
-
class: className,
|
|
29
|
-
style,
|
|
30
|
-
}: {
|
|
31
|
-
spec: GraphSpec;
|
|
32
|
-
theme?: ThemeConfig;
|
|
33
|
-
darkMode?: DarkMode;
|
|
34
|
-
onnodeclick?: (node: Record<string, unknown>) => void;
|
|
35
|
-
onnodedoubleclick?: (node: Record<string, unknown>) => void;
|
|
36
|
-
onselectionchange?: (nodeIds: string[]) => void;
|
|
37
|
-
class?: string;
|
|
38
|
-
style?: string;
|
|
39
|
-
} = $props();
|
|
40
|
-
|
|
41
|
-
let containerEl: HTMLDivElement;
|
|
42
|
-
let instance: GraphInstance | null = null;
|
|
43
|
-
|
|
44
|
-
const ctxTheme = getVizTheme();
|
|
45
|
-
const ctxDarkMode = getVizDarkMode();
|
|
46
|
-
|
|
47
|
-
onMount(() => {
|
|
48
|
-
return () => {
|
|
49
|
-
instance?.destroy();
|
|
50
|
-
instance = null;
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
let prevSpec = '';
|
|
55
|
-
|
|
56
|
-
// Effect 1: Mount/recreate graph on theme/darkMode changes.
|
|
57
|
-
$effect(() => {
|
|
58
|
-
const resolvedTheme = theme ?? ctxTheme?.();
|
|
59
|
-
const resolvedDarkMode = darkMode ?? ctxDarkMode?.();
|
|
60
|
-
const currentSpec = untrack(() => spec);
|
|
61
|
-
|
|
62
|
-
instance?.destroy();
|
|
63
|
-
|
|
64
|
-
const options: GraphMountOptions = {
|
|
65
|
-
theme: resolvedTheme,
|
|
66
|
-
darkMode: resolvedDarkMode,
|
|
67
|
-
onNodeClick: (node: Record<string, unknown>) => untrack(() => onnodeclick)?.(node),
|
|
68
|
-
onNodeDoubleClick: (node: Record<string, unknown>) => untrack(() => onnodedoubleclick)?.(node),
|
|
69
|
-
onSelectionChange: (nodeIds: string[]) => untrack(() => onselectionchange)?.(nodeIds),
|
|
70
|
-
responsive: true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
instance = createGraph(containerEl, currentSpec, options);
|
|
74
|
-
prevSpec = JSON.stringify(currentSpec);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Effect 2: Update graph when spec changes (no destroy/recreate).
|
|
78
|
-
$effect(() => {
|
|
79
|
-
const currentSpec = spec;
|
|
80
|
-
if (!instance) return;
|
|
81
|
-
|
|
82
|
-
const specString = JSON.stringify(currentSpec);
|
|
83
|
-
if (specString !== prevSpec) {
|
|
84
|
-
prevSpec = specString;
|
|
85
|
-
instance.update(currentSpec);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Imperative methods exposed via component exports
|
|
90
|
-
export function search(query: string): void {
|
|
91
|
-
instance?.search(query);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function clearSearch(): void {
|
|
95
|
-
instance?.clearSearch();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function zoomToFit(): void {
|
|
99
|
-
instance?.zoomToFit();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function zoomToNode(nodeId: string): void {
|
|
103
|
-
instance?.zoomToNode(nodeId);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function selectNode(nodeId: string): void {
|
|
107
|
-
instance?.selectNode(nodeId);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function getSelectedNodes(): string[] {
|
|
111
|
-
return instance?.getSelectedNodes() ?? [];
|
|
112
|
-
}
|
|
113
|
-
</script>
|
|
114
|
-
|
|
115
|
-
<div
|
|
116
|
-
bind:this={containerEl}
|
|
117
|
-
class={className ? `oc-graph-root ${className}` : 'oc-graph-root'}
|
|
118
|
-
{style}
|
|
119
|
-
></div>
|
package/src/Sankey.svelte
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Sankey component: Svelte 5 wrapper around the vanilla adapter.
|
|
3
|
-
|
|
4
|
-
Mounts a sankey instance on render, updates when spec changes,
|
|
5
|
-
and cleans up on unmount. All heavy lifting is done by the vanilla
|
|
6
|
-
createSankey() function.
|
|
7
|
-
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import type { DarkMode, SankeySpec, ThemeConfig } from '@opendata-ai/openchart-core';
|
|
10
|
-
import {
|
|
11
|
-
createSankey,
|
|
12
|
-
type SankeyInstance,
|
|
13
|
-
type SankeyMountOptions,
|
|
14
|
-
} from '@opendata-ai/openchart-vanilla';
|
|
15
|
-
import { onMount, untrack } from 'svelte';
|
|
16
|
-
import { getVizDarkMode, getVizTheme } from './context.js';
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
spec,
|
|
20
|
-
theme,
|
|
21
|
-
darkMode,
|
|
22
|
-
onnodeclick,
|
|
23
|
-
onlinkclick,
|
|
24
|
-
onnodehover,
|
|
25
|
-
onlinkhover,
|
|
26
|
-
class: className,
|
|
27
|
-
style,
|
|
28
|
-
}: {
|
|
29
|
-
spec: SankeySpec;
|
|
30
|
-
theme?: ThemeConfig;
|
|
31
|
-
darkMode?: DarkMode;
|
|
32
|
-
onnodeclick?: (node: Record<string, unknown>) => void;
|
|
33
|
-
onlinkclick?: (link: Record<string, unknown>) => void;
|
|
34
|
-
onnodehover?: (node: Record<string, unknown> | null) => void;
|
|
35
|
-
onlinkhover?: (link: Record<string, unknown> | null) => void;
|
|
36
|
-
class?: string;
|
|
37
|
-
style?: string;
|
|
38
|
-
} = $props();
|
|
39
|
-
|
|
40
|
-
let containerEl: HTMLDivElement;
|
|
41
|
-
let instance: SankeyInstance | null = null;
|
|
42
|
-
|
|
43
|
-
const ctxTheme = getVizTheme();
|
|
44
|
-
const ctxDarkMode = getVizDarkMode();
|
|
45
|
-
|
|
46
|
-
onMount(() => {
|
|
47
|
-
return () => {
|
|
48
|
-
instance?.destroy();
|
|
49
|
-
instance = null;
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
let prevSpec = '';
|
|
54
|
-
|
|
55
|
-
// Effect 1: Mount/recreate sankey on theme/darkMode changes.
|
|
56
|
-
$effect(() => {
|
|
57
|
-
const resolvedTheme = theme ?? ctxTheme?.();
|
|
58
|
-
const resolvedDarkMode = darkMode ?? ctxDarkMode?.();
|
|
59
|
-
const currentSpec = untrack(() => spec);
|
|
60
|
-
|
|
61
|
-
instance?.destroy();
|
|
62
|
-
|
|
63
|
-
const options: SankeyMountOptions = {
|
|
64
|
-
theme: resolvedTheme,
|
|
65
|
-
darkMode: resolvedDarkMode,
|
|
66
|
-
onNodeClick: (node: Record<string, unknown>) => untrack(() => onnodeclick)?.(node),
|
|
67
|
-
onLinkClick: (link: Record<string, unknown>) => untrack(() => onlinkclick)?.(link),
|
|
68
|
-
onNodeHover: (node: Record<string, unknown> | null) => untrack(() => onnodehover)?.(node),
|
|
69
|
-
onLinkHover: (link: Record<string, unknown> | null) => untrack(() => onlinkhover)?.(link),
|
|
70
|
-
responsive: true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
instance = createSankey(containerEl, currentSpec, options);
|
|
74
|
-
prevSpec = JSON.stringify(currentSpec);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Effect 2: Update sankey when spec changes (no destroy/recreate).
|
|
78
|
-
$effect(() => {
|
|
79
|
-
const currentSpec = spec;
|
|
80
|
-
if (!instance) return;
|
|
81
|
-
|
|
82
|
-
const specString = JSON.stringify(currentSpec);
|
|
83
|
-
if (specString !== prevSpec) {
|
|
84
|
-
prevSpec = specString;
|
|
85
|
-
instance.update(currentSpec);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
</script>
|
|
89
|
-
|
|
90
|
-
<div
|
|
91
|
-
bind:this={containerEl}
|
|
92
|
-
class={className ? `oc-sankey-root ${className}` : 'oc-sankey-root'}
|
|
93
|
-
{style}
|
|
94
|
-
></div>
|
package/src/ThemeProvider.svelte
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
VizThemeProvider: provides a theme and dark mode preference to all
|
|
3
|
-
descendant Chart, DataTable, and Graph components.
|
|
4
|
-
|
|
5
|
-
Components use the context values as fallbacks when no explicit
|
|
6
|
-
`theme` or `darkMode` prop is passed.
|
|
7
|
-
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import type { DarkMode, ThemeConfig } from '@opendata-ai/openchart-core';
|
|
10
|
-
import type { Snippet } from 'svelte';
|
|
11
|
-
import { setVizDarkMode, setVizTheme } from './context.js';
|
|
12
|
-
|
|
13
|
-
let {
|
|
14
|
-
theme,
|
|
15
|
-
darkMode,
|
|
16
|
-
children,
|
|
17
|
-
}: {
|
|
18
|
-
theme: ThemeConfig | undefined;
|
|
19
|
-
darkMode?: DarkMode;
|
|
20
|
-
children: Snippet;
|
|
21
|
-
} = $props();
|
|
22
|
-
|
|
23
|
-
setVizTheme(() => theme);
|
|
24
|
-
setVizDarkMode(() => darkMode);
|
|
25
|
-
</script>
|
|
26
|
-
|
|
27
|
-
{@render children()}
|
package/src/TileMap.svelte
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
TileMap component: Svelte 5 wrapper around the vanilla adapter.
|
|
3
|
-
|
|
4
|
-
Mounts a tilemap instance on render, updates when spec changes,
|
|
5
|
-
and cleans up on unmount. All heavy lifting is done by the vanilla
|
|
6
|
-
createTileMap() function.
|
|
7
|
-
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import type { DarkMode, ThemeConfig, TileMapSpec } from '@opendata-ai/openchart-core';
|
|
10
|
-
import {
|
|
11
|
-
createTileMap,
|
|
12
|
-
type TileMapInstance,
|
|
13
|
-
type TileMapMountOptions,
|
|
14
|
-
} from '@opendata-ai/openchart-vanilla';
|
|
15
|
-
import { onMount, untrack } from 'svelte';
|
|
16
|
-
import { getVizDarkMode, getVizTheme } from './context.js';
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
spec,
|
|
20
|
-
theme,
|
|
21
|
-
darkMode,
|
|
22
|
-
ontileclick,
|
|
23
|
-
ontilehover,
|
|
24
|
-
class: className,
|
|
25
|
-
style,
|
|
26
|
-
}: {
|
|
27
|
-
spec: TileMapSpec;
|
|
28
|
-
theme?: ThemeConfig;
|
|
29
|
-
darkMode?: DarkMode;
|
|
30
|
-
ontileclick?: (tile: {
|
|
31
|
-
stateCode: string;
|
|
32
|
-
stateName: string;
|
|
33
|
-
value: number | null;
|
|
34
|
-
data: Record<string, unknown>;
|
|
35
|
-
}) => void;
|
|
36
|
-
ontilehover?: (
|
|
37
|
-
tile: {
|
|
38
|
-
stateCode: string;
|
|
39
|
-
stateName: string;
|
|
40
|
-
value: number | null;
|
|
41
|
-
data: Record<string, unknown>;
|
|
42
|
-
} | null,
|
|
43
|
-
) => void;
|
|
44
|
-
class?: string;
|
|
45
|
-
style?: string;
|
|
46
|
-
} = $props();
|
|
47
|
-
|
|
48
|
-
let containerEl: HTMLDivElement;
|
|
49
|
-
let instance: TileMapInstance | null = null;
|
|
50
|
-
|
|
51
|
-
const ctxTheme = getVizTheme();
|
|
52
|
-
const ctxDarkMode = getVizDarkMode();
|
|
53
|
-
|
|
54
|
-
onMount(() => {
|
|
55
|
-
return () => {
|
|
56
|
-
instance?.destroy();
|
|
57
|
-
instance = null;
|
|
58
|
-
};
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
let prevSpec = '';
|
|
62
|
-
|
|
63
|
-
// Effect 1: Mount/recreate tilemap on theme/darkMode changes.
|
|
64
|
-
$effect(() => {
|
|
65
|
-
const resolvedTheme = theme ?? ctxTheme?.();
|
|
66
|
-
const resolvedDarkMode = darkMode ?? ctxDarkMode?.();
|
|
67
|
-
const currentSpec = untrack(() => spec);
|
|
68
|
-
|
|
69
|
-
instance?.destroy();
|
|
70
|
-
|
|
71
|
-
const options: TileMapMountOptions = {
|
|
72
|
-
theme: resolvedTheme,
|
|
73
|
-
darkMode: resolvedDarkMode,
|
|
74
|
-
onTileClick: (tile: {
|
|
75
|
-
stateCode: string;
|
|
76
|
-
stateName: string;
|
|
77
|
-
value: number | null;
|
|
78
|
-
data: Record<string, unknown>;
|
|
79
|
-
}) => untrack(() => ontileclick)?.(tile),
|
|
80
|
-
onTileHover: (
|
|
81
|
-
tile: {
|
|
82
|
-
stateCode: string;
|
|
83
|
-
stateName: string;
|
|
84
|
-
value: number | null;
|
|
85
|
-
data: Record<string, unknown>;
|
|
86
|
-
} | null,
|
|
87
|
-
) => untrack(() => ontilehover)?.(tile),
|
|
88
|
-
responsive: true,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
instance = createTileMap(containerEl, currentSpec, options);
|
|
92
|
-
prevSpec = JSON.stringify(currentSpec);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Effect 2: Update tilemap when spec changes (no destroy/recreate).
|
|
96
|
-
$effect(() => {
|
|
97
|
-
const currentSpec = spec;
|
|
98
|
-
if (!instance) return;
|
|
99
|
-
|
|
100
|
-
const specString = JSON.stringify(currentSpec);
|
|
101
|
-
if (specString !== prevSpec) {
|
|
102
|
-
prevSpec = specString;
|
|
103
|
-
instance.update(currentSpec);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
</script>
|
|
107
|
-
|
|
108
|
-
<div
|
|
109
|
-
bind:this={containerEl}
|
|
110
|
-
class={className ? `oc-tilemap-root ${className}` : 'oc-tilemap-root'}
|
|
111
|
-
{style}
|
|
112
|
-
></div>
|
package/src/Visualization.svelte
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Visualization routing component: renders Chart, DataTable, or Graph
|
|
3
|
-
based on the spec type. Use this when rendering arbitrary VizSpec values.
|
|
4
|
-
|
|
5
|
-
For event handlers, use the specific component (Chart, DataTable, Graph) directly.
|
|
6
|
-
-->
|
|
7
|
-
<script lang="ts">
|
|
8
|
-
import type { DarkMode, ThemeConfig, VizSpec } from '@opendata-ai/openchart-core';
|
|
9
|
-
import {
|
|
10
|
-
isBarListSpec,
|
|
11
|
-
isGraphSpec,
|
|
12
|
-
isMapSpec,
|
|
13
|
-
isSankeySpec,
|
|
14
|
-
isTableSpec,
|
|
15
|
-
isTileMapSpec,
|
|
16
|
-
} from '@opendata-ai/openchart-core';
|
|
17
|
-
import BarList from './BarList.svelte';
|
|
18
|
-
import Chart from './Chart.svelte';
|
|
19
|
-
import DataTable from './DataTable.svelte';
|
|
20
|
-
import GeoMap from './GeoMap.svelte';
|
|
21
|
-
import Graph from './Graph.svelte';
|
|
22
|
-
import Sankey from './Sankey.svelte';
|
|
23
|
-
import TileMap from './TileMap.svelte';
|
|
24
|
-
|
|
25
|
-
let {
|
|
26
|
-
spec,
|
|
27
|
-
theme,
|
|
28
|
-
darkMode,
|
|
29
|
-
class: className,
|
|
30
|
-
style,
|
|
31
|
-
}: {
|
|
32
|
-
spec: VizSpec;
|
|
33
|
-
theme?: ThemeConfig;
|
|
34
|
-
darkMode?: DarkMode;
|
|
35
|
-
class?: string;
|
|
36
|
-
style?: string;
|
|
37
|
-
} = $props();
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
{#if isTableSpec(spec)}
|
|
41
|
-
<DataTable {spec} {theme} {darkMode} class={className} {style} />
|
|
42
|
-
{:else if isGraphSpec(spec)}
|
|
43
|
-
<Graph {spec} {theme} {darkMode} class={className} {style} />
|
|
44
|
-
{:else if isSankeySpec(spec)}
|
|
45
|
-
<Sankey {spec} {theme} {darkMode} class={className} {style} />
|
|
46
|
-
{:else if isTileMapSpec(spec)}
|
|
47
|
-
<TileMap {spec} {theme} {darkMode} class={className} {style} />
|
|
48
|
-
{:else if isBarListSpec(spec)}
|
|
49
|
-
<BarList {spec} {theme} {darkMode} class={className} {style} />
|
|
50
|
-
{:else if isMapSpec(spec)}
|
|
51
|
-
<GeoMap {spec} {theme} {darkMode} class={className} {style} />
|
|
52
|
-
{:else}
|
|
53
|
-
<Chart {spec} {theme} {darkMode} class={className} {style} />
|
|
54
|
-
{/if}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { ChartSpec } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { cleanup, render, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import Chart from '../Chart.svelte';
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Test data
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
const lineSpec: ChartSpec = {
|
|
11
|
-
mark: 'line',
|
|
12
|
-
data: [
|
|
13
|
-
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
14
|
-
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
15
|
-
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
16
|
-
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
17
|
-
],
|
|
18
|
-
encoding: {
|
|
19
|
-
x: { field: 'date', type: 'temporal' },
|
|
20
|
-
y: { field: 'value', type: 'quantitative' },
|
|
21
|
-
color: { field: 'country', type: 'nominal' },
|
|
22
|
-
},
|
|
23
|
-
chrome: {
|
|
24
|
-
title: 'GDP Growth',
|
|
25
|
-
subtitle: 'US vs UK over time',
|
|
26
|
-
source: 'World Bank',
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const barSpec: ChartSpec = {
|
|
31
|
-
mark: 'bar',
|
|
32
|
-
data: [
|
|
33
|
-
{ name: 'A', value: 10 },
|
|
34
|
-
{ name: 'B', value: 30 },
|
|
35
|
-
{ name: 'C', value: 20 },
|
|
36
|
-
],
|
|
37
|
-
encoding: {
|
|
38
|
-
x: { field: 'value', type: 'quantitative' },
|
|
39
|
-
y: { field: 'name', type: 'nominal' },
|
|
40
|
-
},
|
|
41
|
-
chrome: {
|
|
42
|
-
title: 'Updated Title',
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Helper: render Chart and wait for SVG to appear ($effect is deferred)
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
async function renderChart(props: { spec: ChartSpec; [key: string]: unknown }) {
|
|
51
|
-
const result = render(Chart, { props });
|
|
52
|
-
await waitFor(() => {
|
|
53
|
-
expect(result.container.querySelector('svg')).not.toBeNull();
|
|
54
|
-
});
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Tests
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
cleanup();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('<Chart />', () => {
|
|
67
|
-
it('renders an SVG element', async () => {
|
|
68
|
-
const { container } = await renderChart({ spec: lineSpec });
|
|
69
|
-
const svg = container.querySelector('svg');
|
|
70
|
-
expect(svg).not.toBeNull();
|
|
71
|
-
expect(svg?.getAttribute('class')).toBe('oc-chart');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('renders chrome text elements', async () => {
|
|
75
|
-
const { container } = await renderChart({ spec: lineSpec });
|
|
76
|
-
|
|
77
|
-
const title = container.querySelector('.oc-title');
|
|
78
|
-
expect(title).not.toBeNull();
|
|
79
|
-
expect(title?.textContent).toBe('GDP Growth');
|
|
80
|
-
|
|
81
|
-
const subtitle = container.querySelector('.oc-subtitle');
|
|
82
|
-
expect(subtitle?.textContent).toBe('US vs UK over time');
|
|
83
|
-
|
|
84
|
-
const source = container.querySelector('.oc-source');
|
|
85
|
-
expect(source?.textContent).toBe('World Bank');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('spec changes trigger re-render', async () => {
|
|
89
|
-
const { container, rerender } = await renderChart({ spec: lineSpec });
|
|
90
|
-
|
|
91
|
-
const titleBefore = container.querySelector('.oc-title');
|
|
92
|
-
expect(titleBefore?.textContent).toBe('GDP Growth');
|
|
93
|
-
|
|
94
|
-
await rerender({ spec: barSpec });
|
|
95
|
-
await waitFor(() => {
|
|
96
|
-
expect(container.querySelector('.oc-title')?.textContent).toBe('Updated Title');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('unmounting cleans up chart instance', async () => {
|
|
101
|
-
const { container, unmount } = await renderChart({ spec: lineSpec });
|
|
102
|
-
|
|
103
|
-
const svgBefore = container.querySelector('svg');
|
|
104
|
-
expect(svgBefore).not.toBeNull();
|
|
105
|
-
|
|
106
|
-
unmount();
|
|
107
|
-
|
|
108
|
-
expect(container.querySelector('svg')).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('className prop passes through to wrapper div', async () => {
|
|
112
|
-
const { container } = await renderChart({ spec: lineSpec, class: 'my-chart' });
|
|
113
|
-
|
|
114
|
-
const wrapper = container.firstElementChild as HTMLElement;
|
|
115
|
-
expect(wrapper?.className).toContain('my-chart');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('renders with dark mode option', async () => {
|
|
119
|
-
const { container } = await renderChart({ spec: lineSpec, darkMode: 'force' });
|
|
120
|
-
|
|
121
|
-
const svg = container.querySelector('svg');
|
|
122
|
-
expect(svg).not.toBeNull();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('style prop passes through to wrapper div', async () => {
|
|
126
|
-
const { container } = await renderChart({
|
|
127
|
-
spec: lineSpec,
|
|
128
|
-
style: 'border: 1px solid red',
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const wrapper = container.firstElementChild as HTMLElement;
|
|
132
|
-
expect(wrapper?.style.border).toBe('1px solid red');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { ChartSpec } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { cleanup, render, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import Harness from './ChartStoryHarness.svelte';
|
|
5
|
-
|
|
6
|
-
const baseSpec: ChartSpec = {
|
|
7
|
-
mark: 'line',
|
|
8
|
-
data: [
|
|
9
|
-
{ year: '2008', value: 10, country: 'US' },
|
|
10
|
-
{ year: '2012', value: 40, country: 'US' },
|
|
11
|
-
{ year: '2016', value: 25, country: 'US' },
|
|
12
|
-
],
|
|
13
|
-
encoding: {
|
|
14
|
-
x: { field: 'year', type: 'ordinal' },
|
|
15
|
-
y: { field: 'value', type: 'quantitative' },
|
|
16
|
-
},
|
|
17
|
-
chrome: { title: 'Base' },
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const steps = [
|
|
21
|
-
{},
|
|
22
|
-
{ spec: { chrome: { title: 'One' } } },
|
|
23
|
-
{ spec: { chrome: { subtitle: 'Two' } } },
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
describe('ChartStory (Svelte)', () => {
|
|
27
|
-
afterEach(cleanup);
|
|
28
|
-
|
|
29
|
-
it('renders the chart panel and one block per step', async () => {
|
|
30
|
-
const { container } = render(Harness, { props: { spec: baseSpec, steps } });
|
|
31
|
-
|
|
32
|
-
await waitFor(() => {
|
|
33
|
-
expect(container.querySelector('svg')).not.toBeNull();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(container.querySelectorAll('[data-oc-story-step]')).toHaveLength(3);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('drives the story from the controlled step prop', async () => {
|
|
40
|
-
const { container, rerender } = render(Harness, { props: { spec: baseSpec, steps, step: 0 } });
|
|
41
|
-
|
|
42
|
-
await waitFor(() => {
|
|
43
|
-
expect(container.querySelector('svg')).not.toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
await rerender({ spec: baseSpec, steps, step: 2 });
|
|
47
|
-
|
|
48
|
-
await waitFor(() => {
|
|
49
|
-
expect(container.querySelector('.oc-title')?.textContent).toBe('One');
|
|
50
|
-
expect(container.querySelector('.oc-subtitle')?.textContent).toBe('Two');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('tears down the chart on unmount', async () => {
|
|
55
|
-
const { container, unmount } = render(Harness, { props: { spec: baseSpec, steps } });
|
|
56
|
-
|
|
57
|
-
await waitFor(() => {
|
|
58
|
-
expect(container.querySelector('svg')).not.toBeNull();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
unmount();
|
|
62
|
-
expect(container.querySelector('svg')).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<!-- Test harness: supplies narrative snippets to ChartStory. -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
import type { ChartSpec } from '@opendata-ai/openchart-core';
|
|
4
|
-
import ChartStory from '../ChartStory.svelte';
|
|
5
|
-
|
|
6
|
-
let { spec, steps, step }: { spec: ChartSpec; steps: unknown[]; step?: number } = $props();
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
{#snippet intro()}<p>Intro</p>{/snippet}
|
|
10
|
-
{#snippet middle()}<p>Middle</p>{/snippet}
|
|
11
|
-
{#snippet end()}<p>End</p>{/snippet}
|
|
12
|
-
|
|
13
|
-
<ChartStory {spec} steps={steps as never} narrative={[intro, middle, end]} {step} />
|