@opendata-ai/openchart-vue 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.
@@ -1,84 +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
-
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 { type CSSProperties, defineComponent, h, type PropType } from 'vue';
18
- import { BarList } from './BarList';
19
- import { Chart } from './Chart';
20
- import { DataTable } from './DataTable';
21
- import { Graph } from './Graph';
22
- import { GeoMap } from './Map';
23
- import { Sankey } from './Sankey';
24
- import { TileMap } from './TileMap';
25
-
26
- export interface VisualizationProps {
27
- spec: VizSpec;
28
- theme?: ThemeConfig;
29
- darkMode?: DarkMode;
30
- class?: string;
31
- style?: string | CSSProperties;
32
- }
33
-
34
- export const Visualization = defineComponent({
35
- name: 'Visualization',
36
- props: {
37
- spec: {
38
- type: Object as PropType<VizSpec>,
39
- required: true,
40
- },
41
- theme: {
42
- type: Object as PropType<ThemeConfig>,
43
- default: undefined,
44
- },
45
- darkMode: {
46
- type: String as PropType<DarkMode>,
47
- default: undefined,
48
- },
49
- class: {
50
- type: String,
51
- default: undefined,
52
- },
53
- style: {
54
- type: [String, Object] as PropType<string | CSSProperties>,
55
- default: undefined,
56
- },
57
- },
58
- setup(props) {
59
- return () => {
60
- const { spec, theme, darkMode, class: className, style } = props;
61
- const sharedProps = { theme, darkMode, class: className, style };
62
-
63
- if (isTableSpec(spec)) {
64
- return h(DataTable, { ...sharedProps, spec });
65
- }
66
- if (isGraphSpec(spec)) {
67
- return h(Graph, { ...sharedProps, spec });
68
- }
69
- if (isSankeySpec(spec)) {
70
- return h(Sankey, { ...sharedProps, spec });
71
- }
72
- if (isTileMapSpec(spec)) {
73
- return h(TileMap, { ...sharedProps, spec });
74
- }
75
- if (isBarListSpec(spec)) {
76
- return h(BarList, { ...sharedProps, spec });
77
- }
78
- if (isMapSpec(spec)) {
79
- return h(GeoMap, { ...sharedProps, spec });
80
- }
81
- return h(Chart, { ...sharedProps, spec });
82
- };
83
- },
84
- });
@@ -1,139 +0,0 @@
1
- import type { ChartSpec } from '@opendata-ai/openchart-core';
2
- import { flushPromises, mount } from '@vue/test-utils';
3
- import { describe, expect, it } from 'vitest';
4
- import { Chart } from '../Chart';
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: mount Chart and wait for the vanilla adapter to render
48
- // ---------------------------------------------------------------------------
49
-
50
- async function mountChart(props: {
51
- spec: ChartSpec;
52
- class?: string;
53
- darkMode?: string;
54
- style?: string | Record<string, string>;
55
- }) {
56
- const wrapper = mount(Chart, { props: props as Record<string, unknown> });
57
- await flushPromises();
58
- return wrapper;
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Tests
63
- // ---------------------------------------------------------------------------
64
-
65
- describe('Chart', () => {
66
- it('renders an SVG element', async () => {
67
- const wrapper = await mountChart({ spec: lineSpec });
68
- const svg = wrapper.find('svg');
69
- expect(svg.exists()).toBe(true);
70
- expect(svg.attributes('class')).toBe('oc-chart');
71
- wrapper.unmount();
72
- });
73
-
74
- it('renders chrome text elements', async () => {
75
- const wrapper = await mountChart({ spec: lineSpec });
76
-
77
- const title = wrapper.find('.oc-title');
78
- expect(title.exists()).toBe(true);
79
- expect(title.text()).toBe('GDP Growth');
80
-
81
- const subtitle = wrapper.find('.oc-subtitle');
82
- expect(subtitle.text()).toBe('US vs UK over time');
83
-
84
- const source = wrapper.find('.oc-source');
85
- expect(source.text()).toBe('World Bank');
86
- wrapper.unmount();
87
- });
88
-
89
- it('spec changes trigger re-render', async () => {
90
- const wrapper = await mountChart({ spec: lineSpec });
91
-
92
- const titleBefore = wrapper.find('.oc-title');
93
- expect(titleBefore.text()).toBe('GDP Growth');
94
-
95
- await wrapper.setProps({ spec: barSpec });
96
- await flushPromises();
97
-
98
- const titleAfter = wrapper.find('.oc-title');
99
- expect(titleAfter.text()).toBe('Updated Title');
100
- wrapper.unmount();
101
- });
102
-
103
- it('unmounting cleans up chart instance', async () => {
104
- const wrapper = await mountChart({ spec: lineSpec });
105
-
106
- const svgBefore = wrapper.find('svg');
107
- expect(svgBefore.exists()).toBe(true);
108
-
109
- wrapper.unmount();
110
-
111
- // After unmounting, the wrapper element should be empty
112
- expect(wrapper.find('svg').exists()).toBe(false);
113
- });
114
-
115
- it('class prop passes through to wrapper div', async () => {
116
- const wrapper = await mountChart({ spec: lineSpec, class: 'my-chart' });
117
-
118
- expect(wrapper.classes()).toContain('my-chart');
119
- wrapper.unmount();
120
- });
121
-
122
- it('renders with dark mode option', async () => {
123
- const wrapper = await mountChart({ spec: lineSpec, darkMode: 'force' });
124
-
125
- const svg = wrapper.find('svg');
126
- expect(svg.exists()).toBe(true);
127
- wrapper.unmount();
128
- });
129
-
130
- it('style prop passes through to wrapper div', async () => {
131
- const wrapper = await mountChart({
132
- spec: lineSpec,
133
- style: { border: '1px solid red' },
134
- });
135
-
136
- expect(wrapper.attributes('style')).toContain('border');
137
- wrapper.unmount();
138
- });
139
- });
@@ -1,70 +0,0 @@
1
- import type { ChartSpec } from '@opendata-ai/openchart-core';
2
- import { flushPromises, mount } from '@vue/test-utils';
3
- import { describe, expect, it } from 'vitest';
4
- import { h } from 'vue';
5
- import { ChartStory } from '../ChartStory';
6
-
7
- const baseSpec: ChartSpec = {
8
- mark: 'line',
9
- data: [
10
- { year: '2008', value: 10, country: 'US' },
11
- { year: '2012', value: 40, country: 'US' },
12
- { year: '2016', value: 25, country: 'US' },
13
- ],
14
- encoding: {
15
- x: { field: 'year', type: 'ordinal' },
16
- y: { field: 'value', type: 'quantitative' },
17
- },
18
- chrome: { title: 'Base' },
19
- };
20
-
21
- const steps = [
22
- {},
23
- { spec: { chrome: { title: 'One' } } },
24
- { spec: { chrome: { subtitle: 'Two' } } },
25
- ];
26
- const narrative = [h('p', 'Intro'), h('p', 'Middle'), h('p', 'End')];
27
-
28
- describe('ChartStory (Vue)', () => {
29
- it('renders the chart panel and one block per step', async () => {
30
- const wrapper = mount(ChartStory, { props: { spec: baseSpec, steps, narrative } });
31
- await flushPromises();
32
-
33
- expect(wrapper.find('svg').exists()).toBe(true);
34
- expect(wrapper.findAll('[data-oc-story-step]')).toHaveLength(3);
35
-
36
- wrapper.unmount();
37
- });
38
-
39
- it('drives the story from the controlled step prop', async () => {
40
- const wrapper = mount(ChartStory, { props: { spec: baseSpec, steps, narrative, step: 0 } });
41
- await flushPromises();
42
-
43
- await wrapper.setProps({ step: 2 });
44
- await flushPromises();
45
-
46
- expect(wrapper.find('.oc-title').text()).toBe('One');
47
- expect(wrapper.find('.oc-subtitle').text()).toBe('Two');
48
-
49
- wrapper.unmount();
50
- });
51
-
52
- it('exposes goTo through the component instance', async () => {
53
- const wrapper = mount(ChartStory, { props: { spec: baseSpec, steps, narrative } });
54
- await flushPromises();
55
-
56
- (wrapper.vm as unknown as { goTo(n: number): void }).goTo(1);
57
- expect(wrapper.find('.oc-title').text()).toBe('One');
58
-
59
- wrapper.unmount();
60
- });
61
-
62
- it('tears down the chart on unmount', async () => {
63
- const wrapper = mount(ChartStory, { props: { spec: baseSpec, steps, narrative } });
64
- await flushPromises();
65
- expect(wrapper.find('svg').exists()).toBe(true);
66
-
67
- wrapper.unmount();
68
- expect(wrapper.find('svg').exists()).toBe(false);
69
- });
70
- });
@@ -1,131 +0,0 @@
1
- import type { TableSpec } from '@opendata-ai/openchart-core';
2
- import { flushPromises, mount } from '@vue/test-utils';
3
- import { describe, expect, it } from 'vitest';
4
- import { DataTable } from '../DataTable';
5
-
6
- // ---------------------------------------------------------------------------
7
- // Test data
8
- // ---------------------------------------------------------------------------
9
-
10
- const tableSpec: TableSpec = {
11
- type: 'table',
12
- data: [
13
- { name: 'Alice', age: 30, city: 'Portland' },
14
- { name: 'Bob', age: 25, city: 'Seattle' },
15
- { name: 'Charlie', age: 35, city: 'Denver' },
16
- ],
17
- columns: [
18
- { key: 'name', label: 'Name' },
19
- { key: 'age', label: 'Age' },
20
- { key: 'city', label: 'City' },
21
- ],
22
- chrome: { title: 'People Table' },
23
- };
24
-
25
- const updatedSpec: TableSpec = {
26
- type: 'table',
27
- data: [
28
- { x: 1, y: 2 },
29
- { x: 3, y: 4 },
30
- ],
31
- columns: [
32
- { key: 'x', label: 'X Value' },
33
- { key: 'y', label: 'Y Value' },
34
- ],
35
- chrome: { title: 'Updated Table' },
36
- };
37
-
38
- // ---------------------------------------------------------------------------
39
- // Helper: mount DataTable and wait for the vanilla adapter to render
40
- // ---------------------------------------------------------------------------
41
-
42
- async function mountTable(props: {
43
- spec: TableSpec;
44
- class?: string;
45
- darkMode?: string;
46
- style?: string | Record<string, string>;
47
- }) {
48
- const wrapper = mount(DataTable, { props: props as Record<string, unknown> });
49
- await flushPromises();
50
- return wrapper;
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Tests
55
- // ---------------------------------------------------------------------------
56
-
57
- describe('DataTable', () => {
58
- it('renders a table', async () => {
59
- const wrapper = await mountTable({ spec: tableSpec });
60
- const table = wrapper.find('table');
61
- expect(table.exists()).toBe(true);
62
- wrapper.unmount();
63
- });
64
-
65
- it('renders correct number of columns', async () => {
66
- const wrapper = await mountTable({ spec: tableSpec });
67
- const headers = wrapper.findAll('thead th');
68
- expect(headers.length).toBe(3);
69
- wrapper.unmount();
70
- });
71
-
72
- it('renders correct number of rows', async () => {
73
- const wrapper = await mountTable({ spec: tableSpec });
74
- const rows = wrapper.findAll('tbody tr');
75
- expect(rows.length).toBe(3);
76
- wrapper.unmount();
77
- });
78
-
79
- it('spec changes trigger re-render', async () => {
80
- const wrapper = await mountTable({ spec: tableSpec });
81
-
82
- const titleBefore = wrapper.find('.oc-table-title');
83
- expect(titleBefore.text()).toBe('People Table');
84
-
85
- await wrapper.setProps({ spec: updatedSpec });
86
- await flushPromises();
87
-
88
- const titleAfter = wrapper.find('.oc-table-title');
89
- expect(titleAfter.text()).toBe('Updated Table');
90
-
91
- const headersAfter = wrapper.findAll('thead th');
92
- expect(headersAfter.length).toBe(2);
93
- wrapper.unmount();
94
- });
95
-
96
- it('unmounting cleans up', async () => {
97
- const wrapper = await mountTable({ spec: tableSpec });
98
-
99
- const tableBefore = wrapper.find('table');
100
- expect(tableBefore.exists()).toBe(true);
101
-
102
- wrapper.unmount();
103
-
104
- expect(wrapper.find('table').exists()).toBe(false);
105
- });
106
-
107
- it('class prop passes through', async () => {
108
- const wrapper = await mountTable({ spec: tableSpec, class: 'my-table' });
109
-
110
- expect(wrapper.classes()).toContain('my-table');
111
- wrapper.unmount();
112
- });
113
-
114
- it('renders with dark mode option', async () => {
115
- const wrapper = await mountTable({ spec: tableSpec, darkMode: 'force' });
116
-
117
- const table = wrapper.find('table');
118
- expect(table.exists()).toBe(true);
119
- wrapper.unmount();
120
- });
121
-
122
- it('style prop passes through to wrapper div', async () => {
123
- const wrapper = await mountTable({
124
- spec: tableSpec,
125
- style: { border: '1px solid red' },
126
- });
127
-
128
- expect(wrapper.attributes('style')).toContain('border');
129
- wrapper.unmount();
130
- });
131
- });
@@ -1,133 +0,0 @@
1
- import type { GraphSpec } from '@opendata-ai/openchart-core';
2
- import { flushPromises, mount } from '@vue/test-utils';
3
- import { describe, expect, it } from 'vitest';
4
- import { Graph } from '../Graph';
5
-
6
- // ---------------------------------------------------------------------------
7
- // Test data
8
- // ---------------------------------------------------------------------------
9
-
10
- const basicSpec: GraphSpec = {
11
- type: 'graph',
12
- nodes: [
13
- { id: 'a', label: 'Node A' },
14
- { id: 'b', label: 'Node B' },
15
- { id: 'c', label: 'Node C' },
16
- ],
17
- edges: [
18
- { source: 'a', target: 'b' },
19
- { source: 'b', target: 'c' },
20
- ],
21
- chrome: {
22
- title: 'Test Graph',
23
- subtitle: 'A simple test graph',
24
- },
25
- };
26
-
27
- const updatedSpec: GraphSpec = {
28
- type: 'graph',
29
- nodes: [
30
- { id: 'x', label: 'Node X' },
31
- { id: 'y', label: 'Node Y' },
32
- ],
33
- edges: [{ source: 'x', target: 'y' }],
34
- chrome: {
35
- title: 'Updated Graph',
36
- },
37
- };
38
-
39
- // ---------------------------------------------------------------------------
40
- // Helper: mount Graph and wait for the vanilla adapter to render
41
- // ---------------------------------------------------------------------------
42
-
43
- async function mountGraph(props: {
44
- spec: GraphSpec;
45
- class?: string;
46
- darkMode?: string;
47
- style?: string | Record<string, string>;
48
- }) {
49
- const wrapper = mount(Graph, { props: props as Record<string, unknown> });
50
- await flushPromises();
51
- return wrapper;
52
- }
53
-
54
- // ---------------------------------------------------------------------------
55
- // Tests
56
- // ---------------------------------------------------------------------------
57
-
58
- describe('Graph', () => {
59
- it('renders a container div', async () => {
60
- const wrapper = await mountGraph({ spec: basicSpec });
61
- expect(wrapper.element.tagName.toLowerCase()).toBe('div');
62
- wrapper.unmount();
63
- });
64
-
65
- it('mounts graph instance with canvas element', async () => {
66
- const wrapper = await mountGraph({ spec: basicSpec });
67
- const canvas = wrapper.find('canvas');
68
- expect(canvas.exists()).toBe(true);
69
- wrapper.unmount();
70
- });
71
-
72
- it('renders chrome text elements', async () => {
73
- const wrapper = await mountGraph({ spec: basicSpec });
74
-
75
- const title = wrapper.find('.oc-title');
76
- expect(title.exists()).toBe(true);
77
- expect(title.text()).toBe('Test Graph');
78
-
79
- const subtitle = wrapper.find('.oc-subtitle');
80
- expect(subtitle.text()).toBe('A simple test graph');
81
- wrapper.unmount();
82
- });
83
-
84
- it('spec changes trigger re-render', async () => {
85
- const wrapper = await mountGraph({ spec: basicSpec });
86
-
87
- const titleBefore = wrapper.find('.oc-title');
88
- expect(titleBefore.text()).toBe('Test Graph');
89
-
90
- await wrapper.setProps({ spec: updatedSpec });
91
- await flushPromises();
92
-
93
- const titleAfter = wrapper.find('.oc-title');
94
- expect(titleAfter.text()).toBe('Updated Graph');
95
- wrapper.unmount();
96
- });
97
-
98
- it('unmounting cleans up graph instance', async () => {
99
- const wrapper = await mountGraph({ spec: basicSpec });
100
-
101
- const canvasBefore = wrapper.find('canvas');
102
- expect(canvasBefore.exists()).toBe(true);
103
-
104
- wrapper.unmount();
105
-
106
- expect(wrapper.find('canvas').exists()).toBe(false);
107
- });
108
-
109
- it('class prop passes through to wrapper div', async () => {
110
- const wrapper = await mountGraph({ spec: basicSpec, class: 'my-graph' });
111
-
112
- expect(wrapper.classes()).toContain('my-graph');
113
- wrapper.unmount();
114
- });
115
-
116
- it('style prop passes through to wrapper div', async () => {
117
- const wrapper = await mountGraph({
118
- spec: basicSpec,
119
- style: { border: '1px solid red' },
120
- });
121
-
122
- expect(wrapper.attributes('style')).toContain('border');
123
- wrapper.unmount();
124
- });
125
-
126
- it('renders with dark mode option', async () => {
127
- const wrapper = await mountGraph({ spec: basicSpec, darkMode: 'force' });
128
-
129
- const canvas = wrapper.find('canvas');
130
- expect(canvas.exists()).toBe(true);
131
- wrapper.unmount();
132
- });
133
- });