@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.1.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 +175 -12
- package/dist/index.js +917 -168
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import type { CompileTableOptions } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { compileTable } from '../../compile';
|
|
4
|
-
|
|
5
|
-
const sampleData = [
|
|
6
|
-
{ name: 'Alice', age: 30, score: 88.5 },
|
|
7
|
-
{ name: 'Bob', age: 25, score: 92.1 },
|
|
8
|
-
{ name: 'Charlie', age: 35, score: 76.3 },
|
|
9
|
-
{ name: 'Diana', age: 28, score: 95.0 },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
const baseSpec = {
|
|
13
|
-
type: 'table' as const,
|
|
14
|
-
data: sampleData,
|
|
15
|
-
columns: [
|
|
16
|
-
{ key: 'name', label: 'Name' },
|
|
17
|
-
{ key: 'age', label: 'Age' },
|
|
18
|
-
{ key: 'score', label: 'Score' },
|
|
19
|
-
],
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const baseOptions: CompileTableOptions = {
|
|
23
|
-
width: 800,
|
|
24
|
-
height: 600,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
describe('compileTable', () => {
|
|
28
|
-
it('produces a valid TableLayout from a basic spec', () => {
|
|
29
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
30
|
-
|
|
31
|
-
expect(layout.columns).toHaveLength(3);
|
|
32
|
-
expect(layout.rows).toHaveLength(4);
|
|
33
|
-
expect(layout.theme).toBeDefined();
|
|
34
|
-
expect(layout.chrome).toBeDefined();
|
|
35
|
-
expect(layout.search.enabled).toBe(false);
|
|
36
|
-
expect(layout.stickyFirstColumn).toBe(false);
|
|
37
|
-
expect(layout.compact).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('formats cell values correctly', () => {
|
|
41
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
42
|
-
|
|
43
|
-
// First row, first cell should be "Alice"
|
|
44
|
-
const firstRow = layout.rows[0];
|
|
45
|
-
expect(firstRow.cells[0].formattedValue).toBe('Alice');
|
|
46
|
-
// Age should be formatted as number
|
|
47
|
-
expect(firstRow.cells[1].formattedValue).toBe('30');
|
|
48
|
-
// Score should be formatted with decimal
|
|
49
|
-
expect(firstRow.cells[2].formattedValue).toBe('88.50');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('applies d3-format strings to numeric columns', () => {
|
|
53
|
-
const spec = {
|
|
54
|
-
...baseSpec,
|
|
55
|
-
columns: [{ key: 'name' }, { key: 'age', format: '.1f' }, { key: 'score', format: ',.0f' }],
|
|
56
|
-
};
|
|
57
|
-
const layout = compileTable(spec, baseOptions);
|
|
58
|
-
|
|
59
|
-
expect(layout.rows[0].cells[1].formattedValue).toBe('30.0');
|
|
60
|
-
expect(layout.rows[0].cells[2].formattedValue).toBe('89');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('right-aligns number columns by default', () => {
|
|
64
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
65
|
-
|
|
66
|
-
// name should be left, age and score should be right
|
|
67
|
-
expect(layout.columns[0].align).toBe('left');
|
|
68
|
-
expect(layout.columns[1].align).toBe('right');
|
|
69
|
-
expect(layout.columns[2].align).toBe('right');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('generates stable row IDs from data index', () => {
|
|
73
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
74
|
-
|
|
75
|
-
expect(layout.rows[0].id).toBe('0');
|
|
76
|
-
expect(layout.rows[1].id).toBe('1');
|
|
77
|
-
expect(layout.rows[2].id).toBe('2');
|
|
78
|
-
expect(layout.rows[3].id).toBe('3');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('uses rowKey for row IDs when specified', () => {
|
|
82
|
-
const spec = { ...baseSpec, rowKey: 'name' };
|
|
83
|
-
const layout = compileTable(spec, baseOptions);
|
|
84
|
-
|
|
85
|
-
expect(layout.rows[0].id).toBe('Alice');
|
|
86
|
-
expect(layout.rows[1].id).toBe('Bob');
|
|
87
|
-
expect(layout.rows[2].id).toBe('Charlie');
|
|
88
|
-
expect(layout.rows[3].id).toBe('Diana');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('applies tabular-nums font variant on number cells', () => {
|
|
92
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
93
|
-
|
|
94
|
-
// age cell should have tabular-nums
|
|
95
|
-
const ageCell = layout.rows[0].cells[1];
|
|
96
|
-
expect(ageCell.style.fontVariant).toBe('tabular-nums');
|
|
97
|
-
|
|
98
|
-
// name cell should not
|
|
99
|
-
const nameCell = layout.rows[0].cells[0];
|
|
100
|
-
expect(nameCell.style.fontVariant).toBeUndefined();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('computes reasonable column widths (header text not clipped)', () => {
|
|
104
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
105
|
-
|
|
106
|
-
for (const col of layout.columns) {
|
|
107
|
-
// Each column should be at least 60px
|
|
108
|
-
expect(col.width).toBeGreaterThanOrEqual(60);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('computes chrome from spec', () => {
|
|
113
|
-
const spec = {
|
|
114
|
-
...baseSpec,
|
|
115
|
-
chrome: { title: 'Test Table', subtitle: 'A test' },
|
|
116
|
-
};
|
|
117
|
-
const layout = compileTable(spec, baseOptions);
|
|
118
|
-
|
|
119
|
-
expect(layout.chrome.title?.text).toBe('Test Table');
|
|
120
|
-
expect(layout.chrome.subtitle?.text).toBe('A test');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('produces valid a11y metadata', () => {
|
|
124
|
-
const spec = {
|
|
125
|
-
...baseSpec,
|
|
126
|
-
chrome: { title: 'Student Scores' },
|
|
127
|
-
};
|
|
128
|
-
const layout = compileTable(spec, baseOptions);
|
|
129
|
-
|
|
130
|
-
expect(layout.a11y.caption).toBe('Table: Student Scores');
|
|
131
|
-
expect(layout.a11y.summary).toContain('3 columns');
|
|
132
|
-
expect(layout.a11y.summary).toContain('4 rows');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('handles empty data gracefully', () => {
|
|
136
|
-
// Validation catches empty data, so we test with a minimal valid dataset
|
|
137
|
-
const spec = {
|
|
138
|
-
type: 'table' as const,
|
|
139
|
-
data: [{ x: 1 }],
|
|
140
|
-
columns: [{ key: 'x' }],
|
|
141
|
-
};
|
|
142
|
-
const layout = compileTable(spec, baseOptions);
|
|
143
|
-
expect(layout.rows).toHaveLength(1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('preserves original data in row.data', () => {
|
|
147
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
148
|
-
|
|
149
|
-
expect(layout.rows[0].data).toBe(sampleData[0]);
|
|
150
|
-
expect(layout.rows[0].data.name).toBe('Alice');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('throws for non-table spec', () => {
|
|
154
|
-
expect(() =>
|
|
155
|
-
compileTable(
|
|
156
|
-
{
|
|
157
|
-
mark: 'line',
|
|
158
|
-
data: [
|
|
159
|
-
{ date: '2020-01-01', y: 2 },
|
|
160
|
-
{ date: '2021-01-01', y: 4 },
|
|
161
|
-
],
|
|
162
|
-
encoding: {
|
|
163
|
-
x: { field: 'date', type: 'temporal' },
|
|
164
|
-
y: { field: 'y', type: 'quantitative' },
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
baseOptions,
|
|
168
|
-
),
|
|
169
|
-
).toThrow('Use compileChart instead');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('determines column cellType from visual features', () => {
|
|
173
|
-
const spec = {
|
|
174
|
-
type: 'table' as const,
|
|
175
|
-
data: [
|
|
176
|
-
{
|
|
177
|
-
val: 10,
|
|
178
|
-
cat: 'A',
|
|
179
|
-
trend: [1, 2, 3],
|
|
180
|
-
bar: 50,
|
|
181
|
-
img: 'http://example.com/a.png',
|
|
182
|
-
flag: 'US',
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
columns: [
|
|
186
|
-
{ key: 'val', heatmap: { palette: 'blue' } },
|
|
187
|
-
{ key: 'cat', categoryColors: { A: '#ff0000' } },
|
|
188
|
-
{ key: 'trend', sparkline: { type: 'line' as const } },
|
|
189
|
-
{ key: 'bar', bar: {} },
|
|
190
|
-
{ key: 'img', image: {} },
|
|
191
|
-
{ key: 'flag', flag: true },
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
const layout = compileTable(spec, baseOptions);
|
|
195
|
-
|
|
196
|
-
expect(layout.columns[0].cellType).toBe('heatmap');
|
|
197
|
-
expect(layout.columns[1].cellType).toBe('category');
|
|
198
|
-
expect(layout.columns[2].cellType).toBe('sparkline');
|
|
199
|
-
expect(layout.columns[3].cellType).toBe('bar');
|
|
200
|
-
expect(layout.columns[4].cellType).toBe('image');
|
|
201
|
-
expect(layout.columns[5].cellType).toBe('flag');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('applies dark mode theme', () => {
|
|
205
|
-
const layout = compileTable(baseSpec, { ...baseOptions, darkMode: true });
|
|
206
|
-
expect(layout.theme.isDark).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('includes resolved animation when spec has animation: true', () => {
|
|
210
|
-
const layout = compileTable({ ...baseSpec, animation: true }, baseOptions);
|
|
211
|
-
expect(layout.animation).toBeDefined();
|
|
212
|
-
expect(layout.animation!.enter).toBeDefined();
|
|
213
|
-
expect(layout.animation!.enter!.duration).toBe(500);
|
|
214
|
-
expect(layout.animation!.enter!.staggerDelay).toBe(80);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('includes resolved animation with custom config', () => {
|
|
218
|
-
const layout = compileTable(
|
|
219
|
-
{ ...baseSpec, animation: { enter: { duration: 800, ease: 'snappy' } } },
|
|
220
|
-
baseOptions,
|
|
221
|
-
);
|
|
222
|
-
expect(layout.animation).toBeDefined();
|
|
223
|
-
expect(layout.animation!.enter!.duration).toBe(800);
|
|
224
|
-
expect(layout.animation!.enter!.ease).toBe('snappy');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('does not include animation when spec omits it', () => {
|
|
228
|
-
const layout = compileTable(baseSpec, baseOptions);
|
|
229
|
-
expect(layout.animation).toBeUndefined();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('does not include animation when spec has animation: false', () => {
|
|
233
|
-
const layout = compileTable({ ...baseSpec, animation: false }, baseOptions);
|
|
234
|
-
expect(layout.animation).toBeUndefined();
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { ColumnConfig } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { formatCell, formatValueForSearch } from '../format-cells';
|
|
4
|
-
|
|
5
|
-
describe('formatCell', () => {
|
|
6
|
-
it('formats null as empty string', () => {
|
|
7
|
-
const col: ColumnConfig = { key: 'x' };
|
|
8
|
-
const result = formatCell(null, col);
|
|
9
|
-
expect(result.formattedValue).toBe('');
|
|
10
|
-
expect(result.value).toBeNull();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('formats undefined as empty string', () => {
|
|
14
|
-
const col: ColumnConfig = { key: 'x' };
|
|
15
|
-
const result = formatCell(undefined, col);
|
|
16
|
-
expect(result.formattedValue).toBe('');
|
|
17
|
-
expect(result.value).toBeUndefined();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('applies d3-format string to numbers', () => {
|
|
21
|
-
const col: ColumnConfig = { key: 'x', format: ',.0f' };
|
|
22
|
-
const result = formatCell(1234567, col);
|
|
23
|
-
expect(result.formattedValue).toBe('1,234,567');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('applies d3-format with dollar sign', () => {
|
|
27
|
-
const col: ColumnConfig = { key: 'x', format: '$,.2f' };
|
|
28
|
-
const result = formatCell(1234.5, col);
|
|
29
|
-
expect(result.formattedValue).toBe('$1,234.50');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('applies d3-format with percentage', () => {
|
|
33
|
-
const col: ColumnConfig = { key: 'x', format: '.1%' };
|
|
34
|
-
const result = formatCell(0.456, col);
|
|
35
|
-
expect(result.formattedValue).toBe('45.6%');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('auto-formats numbers without explicit format', () => {
|
|
39
|
-
const col: ColumnConfig = { key: 'x' };
|
|
40
|
-
const result = formatCell(42, col);
|
|
41
|
-
// Should produce some formatted string (formatNumber)
|
|
42
|
-
expect(result.formattedValue).toBeTruthy();
|
|
43
|
-
expect(typeof result.formattedValue).toBe('string');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('formats Date values', () => {
|
|
47
|
-
const col: ColumnConfig = { key: 'x' };
|
|
48
|
-
const date = new Date('2023-06-15');
|
|
49
|
-
const result = formatCell(date, col);
|
|
50
|
-
expect(result.formattedValue).toBeTruthy();
|
|
51
|
-
expect(typeof result.formattedValue).toBe('string');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('handles NaN gracefully (not numeric, falls through to String)', () => {
|
|
55
|
-
const col: ColumnConfig = { key: 'x' };
|
|
56
|
-
const result = formatCell(NaN, col);
|
|
57
|
-
// NaN is not isFinite, so falls through to String(NaN)
|
|
58
|
-
expect(result.formattedValue).toBe('NaN');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('handles Infinity gracefully (not numeric, falls through to String)', () => {
|
|
62
|
-
const col: ColumnConfig = { key: 'x' };
|
|
63
|
-
const result = formatCell(Infinity, col);
|
|
64
|
-
expect(result.formattedValue).toBe('Infinity');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('handles -Infinity gracefully', () => {
|
|
68
|
-
const col: ColumnConfig = { key: 'x' };
|
|
69
|
-
const result = formatCell(-Infinity, col);
|
|
70
|
-
expect(result.formattedValue).toBe('-Infinity');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('does not crash on invalid d3-format string', () => {
|
|
74
|
-
const col: ColumnConfig = { key: 'x', format: '%%%invalid%%%' };
|
|
75
|
-
// Should not throw, falls through to auto-format
|
|
76
|
-
const result = formatCell(42, col);
|
|
77
|
-
expect(result.formattedValue).toBeTruthy();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('formats plain strings as-is', () => {
|
|
81
|
-
const col: ColumnConfig = { key: 'x' };
|
|
82
|
-
const result = formatCell('hello world', col);
|
|
83
|
-
expect(result.formattedValue).toBe('hello world');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('formats booleans as strings', () => {
|
|
87
|
-
const col: ColumnConfig = { key: 'x' };
|
|
88
|
-
const result = formatCell(true, col);
|
|
89
|
-
expect(result.formattedValue).toBe('true');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('preserves raw value in output', () => {
|
|
93
|
-
const col: ColumnConfig = { key: 'x' };
|
|
94
|
-
const result = formatCell(42, col);
|
|
95
|
-
expect(result.value).toBe(42);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('returns empty style object', () => {
|
|
99
|
-
const col: ColumnConfig = { key: 'x' };
|
|
100
|
-
const result = formatCell('test', col);
|
|
101
|
-
expect(result.style).toBeDefined();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('d3-format string ignored for non-numeric values', () => {
|
|
105
|
-
const col: ColumnConfig = { key: 'x', format: ',.0f' };
|
|
106
|
-
const result = formatCell('not a number', col);
|
|
107
|
-
expect(result.formattedValue).toBe('not a number');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('supports literal suffix format .0f%', () => {
|
|
111
|
-
const col: ColumnConfig = { key: 'x', format: '.0f%' };
|
|
112
|
-
const result = formatCell(32.5, col);
|
|
113
|
-
expect(result.formattedValue).toBe('33%');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('supports literal suffix format $,.2~fT', () => {
|
|
117
|
-
const col: ColumnConfig = { key: 'x', format: '$,.2~fT' };
|
|
118
|
-
const result = formatCell(3.75, col);
|
|
119
|
-
expect(result.formattedValue).toBe('$3.75T');
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('formatValueForSearch', () => {
|
|
124
|
-
it('returns empty string for null', () => {
|
|
125
|
-
const col: ColumnConfig = { key: 'x' };
|
|
126
|
-
expect(formatValueForSearch(null, col)).toBe('');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('formats numbers with d3-format for search', () => {
|
|
130
|
-
const col: ColumnConfig = { key: 'x', format: ',.0f' };
|
|
131
|
-
expect(formatValueForSearch(1234, col)).toBe('1,234');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('falls back to String for non-numeric values', () => {
|
|
135
|
-
const col: ColumnConfig = { key: 'x' };
|
|
136
|
-
expect(formatValueForSearch('hello', col)).toBe('hello');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('supports literal suffix format in search', () => {
|
|
140
|
-
const col: ColumnConfig = { key: 'x', format: '.0f%' };
|
|
141
|
-
expect(formatValueForSearch(32.5, col)).toBe('33%');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { ColumnConfig, ResolvedTheme } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { adaptTheme, contrastRatio, resolveTheme } from '@opendata-ai/openchart-core';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { computeHeatmapColors } from '../heatmap';
|
|
5
|
-
|
|
6
|
-
function getTheme(dark = false): ResolvedTheme {
|
|
7
|
-
const theme = resolveTheme();
|
|
8
|
-
return dark ? adaptTheme(theme) : theme;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('computeHeatmapColors', () => {
|
|
12
|
-
const data = [{ value: 0 }, { value: 25 }, { value: 50 }, { value: 75 }, { value: 100 }];
|
|
13
|
-
|
|
14
|
-
const column: ColumnConfig = {
|
|
15
|
-
key: 'value',
|
|
16
|
-
heatmap: { palette: 'blue' },
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
it('assigns lighter colors to lower values and darker to higher', () => {
|
|
20
|
-
const theme = getTheme();
|
|
21
|
-
const colors = computeHeatmapColors(data, column, theme, false);
|
|
22
|
-
|
|
23
|
-
// Domain-minimum row (value=0) is skipped so the normal row
|
|
24
|
-
// background shows through instead of an opaque low-end color.
|
|
25
|
-
expect(colors.size).toBe(4);
|
|
26
|
-
expect(colors.has(0)).toBe(false);
|
|
27
|
-
|
|
28
|
-
const lowBg = colors.get(1)!.backgroundColor!;
|
|
29
|
-
const highBg = colors.get(4)!.backgroundColor!;
|
|
30
|
-
|
|
31
|
-
expect(lowBg).not.toBe(highBg);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('text color meets AA contrast against background', () => {
|
|
35
|
-
const theme = getTheme();
|
|
36
|
-
const colors = computeHeatmapColors(data, column, theme, false);
|
|
37
|
-
|
|
38
|
-
for (const [, style] of colors) {
|
|
39
|
-
const bg = style.backgroundColor!;
|
|
40
|
-
const fg = style.color!;
|
|
41
|
-
const ratio = contrastRatio(fg, bg);
|
|
42
|
-
// Should pick black or white, both of which should exceed 3:1
|
|
43
|
-
expect(ratio).toBeGreaterThanOrEqual(3);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('supports custom domain', () => {
|
|
48
|
-
const col: ColumnConfig = {
|
|
49
|
-
key: 'value',
|
|
50
|
-
heatmap: { palette: 'blue', domain: [0, 200] },
|
|
51
|
-
};
|
|
52
|
-
const theme = getTheme();
|
|
53
|
-
const colors = computeHeatmapColors(data, col, theme, false);
|
|
54
|
-
|
|
55
|
-
// value=0 sits at the domain minimum and is skipped
|
|
56
|
-
expect(colors.size).toBe(4);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('values outside custom domain are clamped', () => {
|
|
60
|
-
const col: ColumnConfig = {
|
|
61
|
-
key: 'value',
|
|
62
|
-
heatmap: { palette: 'blue', domain: [25, 75] },
|
|
63
|
-
};
|
|
64
|
-
const theme = getTheme();
|
|
65
|
-
const colors = computeHeatmapColors(data, col, theme, false);
|
|
66
|
-
|
|
67
|
-
// value=0 clamps to domain[0]=25, which is the minimum, so it's skipped
|
|
68
|
-
expect(colors.has(0)).toBe(false);
|
|
69
|
-
// value=100 clamps to domain max and gets colored
|
|
70
|
-
expect(colors.has(4)).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('supports colorByField', () => {
|
|
74
|
-
const dataWithLabel = [
|
|
75
|
-
{ label: 'A', score: 10 },
|
|
76
|
-
{ label: 'B', score: 90 },
|
|
77
|
-
];
|
|
78
|
-
const col: ColumnConfig = {
|
|
79
|
-
key: 'label',
|
|
80
|
-
heatmap: { palette: 'blue', colorByField: 'score' },
|
|
81
|
-
};
|
|
82
|
-
const theme = getTheme();
|
|
83
|
-
const colors = computeHeatmapColors(dataWithLabel, col, theme, false);
|
|
84
|
-
|
|
85
|
-
// score=10 is the domain minimum and is skipped; only score=90 gets colored
|
|
86
|
-
expect(colors.size).toBe(1);
|
|
87
|
-
expect(colors.has(1)).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('returns empty map for non-numeric columns', () => {
|
|
91
|
-
const nonNumericData = [{ value: 'hello' }, { value: 'world' }];
|
|
92
|
-
const theme = getTheme();
|
|
93
|
-
const colors = computeHeatmapColors(nonNumericData, column, theme, false);
|
|
94
|
-
expect(colors.size).toBe(0);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('returns empty map when no heatmap config', () => {
|
|
98
|
-
const col: ColumnConfig = { key: 'value' };
|
|
99
|
-
const theme = getTheme();
|
|
100
|
-
const colors = computeHeatmapColors(data, col, theme, false);
|
|
101
|
-
expect(colors.size).toBe(0);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('adapts colors for dark mode', () => {
|
|
105
|
-
const theme = getTheme(true);
|
|
106
|
-
const colors = computeHeatmapColors(data, column, theme, true);
|
|
107
|
-
|
|
108
|
-
// Domain-minimum row skipped
|
|
109
|
-
expect(colors.size).toBe(4);
|
|
110
|
-
for (const [, style] of colors) {
|
|
111
|
-
const bg = style.backgroundColor!;
|
|
112
|
-
const fg = style.color!;
|
|
113
|
-
const ratio = contrastRatio(fg, bg);
|
|
114
|
-
expect(ratio).toBeGreaterThanOrEqual(3);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('custom palette arrays are adapted for dark mode', () => {
|
|
119
|
-
const col: ColumnConfig = {
|
|
120
|
-
key: 'value',
|
|
121
|
-
heatmap: { palette: ['#fca5a5', '#c44e52'], domain: [0, 100] },
|
|
122
|
-
};
|
|
123
|
-
const lightTheme = getTheme(false);
|
|
124
|
-
const darkTheme = getTheme(true);
|
|
125
|
-
const lightColors = computeHeatmapColors(data, col, lightTheme, false);
|
|
126
|
-
const darkColors = computeHeatmapColors(data, col, darkTheme, true);
|
|
127
|
-
|
|
128
|
-
// Domain-minimum row (value=0) is skipped in both modes
|
|
129
|
-
expect(lightColors.has(0)).toBe(false);
|
|
130
|
-
expect(darkColors.has(0)).toBe(false);
|
|
131
|
-
|
|
132
|
-
// The high-end color gets adapted for dark mode
|
|
133
|
-
const lightHighBg = lightColors.get(4)!.backgroundColor!;
|
|
134
|
-
const darkHighBg = darkColors.get(4)!.backgroundColor!;
|
|
135
|
-
expect(darkHighBg).not.toBe(lightHighBg);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('supports array of color stops as palette', () => {
|
|
139
|
-
const col: ColumnConfig = {
|
|
140
|
-
key: 'value',
|
|
141
|
-
heatmap: { palette: ['#ffffff', '#ff0000'] },
|
|
142
|
-
};
|
|
143
|
-
const theme = getTheme();
|
|
144
|
-
const colors = computeHeatmapColors(data, col, theme, false);
|
|
145
|
-
// Domain-minimum row skipped
|
|
146
|
-
expect(colors.size).toBe(4);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { paginateData } from '../pagination';
|
|
3
|
-
|
|
4
|
-
const data = Array.from({ length: 50 }, (_, i) => ({ id: i, value: i * 10 }));
|
|
5
|
-
|
|
6
|
-
describe('paginateData', () => {
|
|
7
|
-
it('returns the correct page slice', () => {
|
|
8
|
-
const result = paginateData(data, 0, 10);
|
|
9
|
-
expect(result.rows).toHaveLength(10);
|
|
10
|
-
expect(result.rows[0].id).toBe(0);
|
|
11
|
-
expect(result.rows[9].id).toBe(9);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('returns correct second page', () => {
|
|
15
|
-
const result = paginateData(data, 1, 10);
|
|
16
|
-
expect(result.rows).toHaveLength(10);
|
|
17
|
-
expect(result.rows[0].id).toBe(10);
|
|
18
|
-
expect(result.rows[9].id).toBe(19);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('returns correct last page (partial)', () => {
|
|
22
|
-
// 50 items, page size 15 = 4 pages (15+15+15+5)
|
|
23
|
-
const result = paginateData(data, 3, 15);
|
|
24
|
-
expect(result.rows).toHaveLength(5);
|
|
25
|
-
expect(result.rows[0].id).toBe(45);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('clamps page to valid range (too high)', () => {
|
|
29
|
-
const result = paginateData(data, 999, 10);
|
|
30
|
-
expect(result.page).toBe(4); // Last page (0-indexed)
|
|
31
|
-
expect(result.rows).toHaveLength(10);
|
|
32
|
-
expect(result.rows[0].id).toBe(40);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('clamps page to valid range (negative)', () => {
|
|
36
|
-
const result = paginateData(data, -5, 10);
|
|
37
|
-
expect(result.page).toBe(0);
|
|
38
|
-
expect(result.rows[0].id).toBe(0);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('computes totalPages correctly', () => {
|
|
42
|
-
const result = paginateData(data, 0, 10);
|
|
43
|
-
expect(result.totalPages).toBe(5);
|
|
44
|
-
expect(result.totalRows).toBe(50);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('computes totalPages for non-even division', () => {
|
|
48
|
-
const result = paginateData(data, 0, 15);
|
|
49
|
-
expect(result.totalPages).toBe(4); // ceil(50/15) = 4
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('handles disabled pagination (pageSize <= 0)', () => {
|
|
53
|
-
const result = paginateData(data, 0, 0);
|
|
54
|
-
expect(result.rows).toHaveLength(50);
|
|
55
|
-
expect(result.totalPages).toBe(1);
|
|
56
|
-
expect(result.page).toBe(0);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('handles negative pageSize as disabled', () => {
|
|
60
|
-
const result = paginateData(data, 0, -10);
|
|
61
|
-
expect(result.rows).toHaveLength(50);
|
|
62
|
-
expect(result.totalPages).toBe(1);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('handles empty data', () => {
|
|
66
|
-
const result = paginateData([], 0, 10);
|
|
67
|
-
expect(result.rows).toHaveLength(0);
|
|
68
|
-
expect(result.totalRows).toBe(0);
|
|
69
|
-
expect(result.totalPages).toBe(1);
|
|
70
|
-
expect(result.page).toBe(0);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('handles single row', () => {
|
|
74
|
-
const result = paginateData([{ id: 1 }], 0, 10);
|
|
75
|
-
expect(result.rows).toHaveLength(1);
|
|
76
|
-
expect(result.totalPages).toBe(1);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { ColumnConfig } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { buildSearchIndex, filterBySearch } from '../search';
|
|
4
|
-
|
|
5
|
-
const data = [
|
|
6
|
-
{ name: 'Alice Smith', age: 30, score: 88.5 },
|
|
7
|
-
{ name: 'Bob Johnson', age: 25, score: 92.1 },
|
|
8
|
-
{ name: 'Charlie Brown', age: 35, score: 76.3 },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
const columns: ColumnConfig[] = [{ key: 'name' }, { key: 'age' }, { key: 'score' }];
|
|
12
|
-
|
|
13
|
-
describe('buildSearchIndex', () => {
|
|
14
|
-
it('builds an index for all rows', () => {
|
|
15
|
-
const index = buildSearchIndex(data, columns);
|
|
16
|
-
expect(index.size).toBe(3);
|
|
17
|
-
expect(index.has(0)).toBe(true);
|
|
18
|
-
expect(index.has(1)).toBe(true);
|
|
19
|
-
expect(index.has(2)).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('includes all column values in the search string', () => {
|
|
23
|
-
const index = buildSearchIndex(data, columns);
|
|
24
|
-
const row0 = index.get(0)!;
|
|
25
|
-
expect(row0).toContain('alice');
|
|
26
|
-
expect(row0).toContain('30');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('lowercases all values', () => {
|
|
30
|
-
const index = buildSearchIndex(data, columns);
|
|
31
|
-
const row0 = index.get(0)!;
|
|
32
|
-
// Should be lowercase
|
|
33
|
-
expect(row0).not.toContain('Alice');
|
|
34
|
-
expect(row0).toContain('alice');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('filterBySearch', () => {
|
|
39
|
-
it('returns all data for empty query', () => {
|
|
40
|
-
const index = buildSearchIndex(data, columns);
|
|
41
|
-
const indices = [0, 1, 2];
|
|
42
|
-
const result = filterBySearch(data, '', index, indices);
|
|
43
|
-
expect(result.data).toHaveLength(3);
|
|
44
|
-
expect(result.indices).toEqual([0, 1, 2]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('returns all data for whitespace-only query', () => {
|
|
48
|
-
const index = buildSearchIndex(data, columns);
|
|
49
|
-
const indices = [0, 1, 2];
|
|
50
|
-
const result = filterBySearch(data, ' ', index, indices);
|
|
51
|
-
expect(result.data).toHaveLength(3);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('filters by substring match', () => {
|
|
55
|
-
const index = buildSearchIndex(data, columns);
|
|
56
|
-
const indices = [0, 1, 2];
|
|
57
|
-
const result = filterBySearch(data, 'alice', index, indices);
|
|
58
|
-
expect(result.data).toHaveLength(1);
|
|
59
|
-
expect(result.data[0].name).toBe('Alice Smith');
|
|
60
|
-
expect(result.indices).toEqual([0]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('is case insensitive', () => {
|
|
64
|
-
const index = buildSearchIndex(data, columns);
|
|
65
|
-
const indices = [0, 1, 2];
|
|
66
|
-
const result = filterBySearch(data, 'ALICE', index, indices);
|
|
67
|
-
expect(result.data).toHaveLength(1);
|
|
68
|
-
expect(result.data[0].name).toBe('Alice Smith');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('matches across formatted numeric values', () => {
|
|
72
|
-
const index = buildSearchIndex(data, columns);
|
|
73
|
-
const indices = [0, 1, 2];
|
|
74
|
-
// Search for a number value
|
|
75
|
-
const result = filterBySearch(data, '35', index, indices);
|
|
76
|
-
expect(result.data).toHaveLength(1);
|
|
77
|
-
expect(result.data[0].name).toBe('Charlie Brown');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('returns empty when no match', () => {
|
|
81
|
-
const index = buildSearchIndex(data, columns);
|
|
82
|
-
const indices = [0, 1, 2];
|
|
83
|
-
const result = filterBySearch(data, 'zzz_no_match', index, indices);
|
|
84
|
-
expect(result.data).toHaveLength(0);
|
|
85
|
-
expect(result.indices).toHaveLength(0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('preserves original indices through filtering', () => {
|
|
89
|
-
const index = buildSearchIndex(data, columns);
|
|
90
|
-
const indices = [0, 1, 2];
|
|
91
|
-
const result = filterBySearch(data, 'brown', index, indices);
|
|
92
|
-
expect(result.indices).toEqual([2]);
|
|
93
|
-
});
|
|
94
|
-
});
|