@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.20
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 +140 -2
- package/dist/index.js +1403 -210
- 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 -2339
- package/src/__tests__/annotation-behavior.test.ts +0 -721
- 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 -370
- 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 -539
- 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 -626
- package/src/compile/size-scale.ts +0 -139
- 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 -1450
- 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 -86
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -89
- 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 -733
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -1810
- 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 -621
- package/src/geo/compile-map.ts +0 -856
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- package/src/geo/types.ts +0 -24
- 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 -747
- 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,129 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runTimeUnit } from '../timeunit';
|
|
3
|
-
|
|
4
|
-
describe('runTimeUnit', () => {
|
|
5
|
-
// Use a specific date: 2024-03-15 14:30:45.123 (Friday)
|
|
6
|
-
const testDate = new Date(2024, 2, 15, 14, 30, 45, 123);
|
|
7
|
-
const data = [{ ts: testDate }];
|
|
8
|
-
|
|
9
|
-
describe('single time units', () => {
|
|
10
|
-
it('extracts year', () => {
|
|
11
|
-
const result = runTimeUnit(data, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
12
|
-
expect(result[0].y).toBe(2024);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('extracts quarter', () => {
|
|
16
|
-
const result = runTimeUnit(data, { timeUnit: 'quarter', field: 'ts', as: 'q' });
|
|
17
|
-
expect(result[0].q).toBe(1); // March is Q1
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('extracts month (0-indexed)', () => {
|
|
21
|
-
const result = runTimeUnit(data, { timeUnit: 'month', field: 'ts', as: 'm' });
|
|
22
|
-
expect(result[0].m).toBe(2); // March = 2
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('extracts day of week', () => {
|
|
26
|
-
const result = runTimeUnit(data, { timeUnit: 'day', field: 'ts', as: 'd' });
|
|
27
|
-
expect(result[0].d).toBe(5); // Friday
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('extracts date (day of month)', () => {
|
|
31
|
-
const result = runTimeUnit(data, { timeUnit: 'date', field: 'ts', as: 'd' });
|
|
32
|
-
expect(result[0].d).toBe(15);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('extracts hours', () => {
|
|
36
|
-
const result = runTimeUnit(data, { timeUnit: 'hours', field: 'ts', as: 'h' });
|
|
37
|
-
expect(result[0].h).toBe(14);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('extracts minutes', () => {
|
|
41
|
-
const result = runTimeUnit(data, { timeUnit: 'minutes', field: 'ts', as: 'min' });
|
|
42
|
-
expect(result[0].min).toBe(30);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('extracts seconds', () => {
|
|
46
|
-
const result = runTimeUnit(data, { timeUnit: 'seconds', field: 'ts', as: 's' });
|
|
47
|
-
expect(result[0].s).toBe(45);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('extracts milliseconds', () => {
|
|
51
|
-
const result = runTimeUnit(data, { timeUnit: 'milliseconds', field: 'ts', as: 'ms' });
|
|
52
|
-
expect(result[0].ms).toBe(123);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('extracts week number', () => {
|
|
56
|
-
const result = runTimeUnit(data, { timeUnit: 'week', field: 'ts', as: 'w' });
|
|
57
|
-
expect(typeof result[0].w).toBe('number');
|
|
58
|
-
expect(result[0].w).toBeGreaterThan(0);
|
|
59
|
-
expect(result[0].w).toBeLessThanOrEqual(53);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('extracts dayofyear', () => {
|
|
63
|
-
const result = runTimeUnit(data, { timeUnit: 'dayofyear', field: 'ts', as: 'doy' });
|
|
64
|
-
expect(typeof result[0].doy).toBe('number');
|
|
65
|
-
// March 15 is day 75 in a leap year (2024)
|
|
66
|
-
expect(result[0].doy).toBe(75);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('compound time units', () => {
|
|
71
|
-
it('extracts yearmonth', () => {
|
|
72
|
-
const result = runTimeUnit(data, { timeUnit: 'yearmonth', field: 'ts', as: 'ym' });
|
|
73
|
-
expect(result[0].ym).toBe('2024-03');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('extracts yearmonthdate', () => {
|
|
77
|
-
const result = runTimeUnit(data, { timeUnit: 'yearmonthdate', field: 'ts', as: 'ymd' });
|
|
78
|
-
expect(result[0].ymd).toBe('2024-03-15');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('extracts monthdate', () => {
|
|
82
|
-
const result = runTimeUnit(data, { timeUnit: 'monthdate', field: 'ts', as: 'md' });
|
|
83
|
-
expect(result[0].md).toBe('03-15');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('extracts hoursminutes', () => {
|
|
87
|
-
const result = runTimeUnit(data, { timeUnit: 'hoursminutes', field: 'ts', as: 'hm' });
|
|
88
|
-
expect(result[0].hm).toBe('14:30');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('date parsing', () => {
|
|
93
|
-
it('parses ISO string dates', () => {
|
|
94
|
-
const stringData = [{ ts: '2024-03-15T14:30:00Z' }];
|
|
95
|
-
const result = runTimeUnit(stringData, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
96
|
-
expect(result[0].y).toBe(2024);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('parses numeric timestamps', () => {
|
|
100
|
-
const numData = [{ ts: testDate.getTime() }];
|
|
101
|
-
const result = runTimeUnit(numData, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
102
|
-
expect(result[0].y).toBe(2024);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('returns null for unparseable dates', () => {
|
|
106
|
-
const badData = [{ ts: 'not-a-date' }];
|
|
107
|
-
const result = runTimeUnit(badData, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
108
|
-
expect(result[0].y).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('returns null for null values', () => {
|
|
112
|
-
const nullData = [{ ts: null }];
|
|
113
|
-
const result = runTimeUnit(nullData, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
114
|
-
expect(result[0].y).toBeNull();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('preserves existing fields', () => {
|
|
119
|
-
const extraData = [{ ts: testDate, label: 'test' }];
|
|
120
|
-
const result = runTimeUnit(extraData, { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
121
|
-
expect(result[0].label).toBe('test');
|
|
122
|
-
expect(result[0].ts).toBe(testDate);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('handles empty data', () => {
|
|
126
|
-
const result = runTimeUnit([], { timeUnit: 'year', field: 'ts', as: 'y' });
|
|
127
|
-
expect(result).toHaveLength(0);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runTransforms } from '../index';
|
|
3
|
-
|
|
4
|
-
describe('window transform', () => {
|
|
5
|
-
it('lag(1) on 5 sorted rows: first row null, rest match previous', () => {
|
|
6
|
-
const data = [
|
|
7
|
-
{ month: '2024-01', value: 10 },
|
|
8
|
-
{ month: '2024-02', value: 20 },
|
|
9
|
-
{ month: '2024-03', value: 30 },
|
|
10
|
-
{ month: '2024-04', value: 40 },
|
|
11
|
-
{ month: '2024-05', value: 50 },
|
|
12
|
-
];
|
|
13
|
-
const result = runTransforms(data, [
|
|
14
|
-
{
|
|
15
|
-
window: [{ op: 'lag', field: 'value', offset: 1, as: 'prev_value' }],
|
|
16
|
-
sort: [{ field: 'month' }],
|
|
17
|
-
},
|
|
18
|
-
]);
|
|
19
|
-
expect(result.map((r) => r.prev_value)).toEqual([null, 10, 20, 30, 40]);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('lag(12) on 24 monthly rows: rows 1-12 null, row 13 matches row 1', () => {
|
|
23
|
-
const data = Array.from({ length: 24 }, (_, i) => {
|
|
24
|
-
const d = new Date(2023, i, 1);
|
|
25
|
-
return { month: d.toISOString().slice(0, 7), value: (i + 1) * 100 };
|
|
26
|
-
});
|
|
27
|
-
const result = runTransforms(data, [
|
|
28
|
-
{
|
|
29
|
-
window: [{ op: 'lag', field: 'value', offset: 12, as: 'prev_year' }],
|
|
30
|
-
sort: [{ field: 'month' }],
|
|
31
|
-
},
|
|
32
|
-
]);
|
|
33
|
-
// First 12 rows should be null
|
|
34
|
-
for (let i = 0; i < 12; i++) {
|
|
35
|
-
expect(result[i].prev_year).toBe(null);
|
|
36
|
-
}
|
|
37
|
-
// Row 13 (index 12) should match row 1's value (100)
|
|
38
|
-
expect(result[12].prev_year).toBe(100);
|
|
39
|
-
// Row 24 (index 23) should match row 12's value (1200)
|
|
40
|
-
expect(result[23].prev_year).toBe(1200);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('pct_change with offset 1 on [100, 110, 121, 133.1]', () => {
|
|
44
|
-
const data = [
|
|
45
|
-
{ idx: 1, value: 100 },
|
|
46
|
-
{ idx: 2, value: 110 },
|
|
47
|
-
{ idx: 3, value: 121 },
|
|
48
|
-
{ idx: 4, value: 133.1 },
|
|
49
|
-
];
|
|
50
|
-
const result = runTransforms(data, [
|
|
51
|
-
{
|
|
52
|
-
window: [{ op: 'pct_change', field: 'value', offset: 1, as: 'pct' }],
|
|
53
|
-
sort: [{ field: 'idx' }],
|
|
54
|
-
},
|
|
55
|
-
]);
|
|
56
|
-
expect(result[0].pct).toBe(null);
|
|
57
|
-
expect(result[1].pct).toBeCloseTo(0.1, 5);
|
|
58
|
-
expect(result[2].pct).toBeCloseTo(0.1, 5);
|
|
59
|
-
expect(result[3].pct).toBeCloseTo(0.1, 5);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('pct_change with zero denominator returns null, not Infinity', () => {
|
|
63
|
-
const data = [
|
|
64
|
-
{ idx: 1, value: 0 },
|
|
65
|
-
{ idx: 2, value: 50 },
|
|
66
|
-
];
|
|
67
|
-
const result = runTransforms(data, [
|
|
68
|
-
{
|
|
69
|
-
window: [{ op: 'pct_change', field: 'value', offset: 1, as: 'pct' }],
|
|
70
|
-
sort: [{ field: 'idx' }],
|
|
71
|
-
},
|
|
72
|
-
]);
|
|
73
|
-
expect(result[0].pct).toBe(null);
|
|
74
|
-
expect(result[1].pct).toBe(null); // 0 denominator -> null
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('diff on [10, 15, 12, 20]', () => {
|
|
78
|
-
const data = [
|
|
79
|
-
{ idx: 1, value: 10 },
|
|
80
|
-
{ idx: 2, value: 15 },
|
|
81
|
-
{ idx: 3, value: 12 },
|
|
82
|
-
{ idx: 4, value: 20 },
|
|
83
|
-
];
|
|
84
|
-
const result = runTransforms(data, [
|
|
85
|
-
{
|
|
86
|
-
window: [{ op: 'diff', field: 'value', offset: 1, as: 'delta' }],
|
|
87
|
-
sort: [{ field: 'idx' }],
|
|
88
|
-
},
|
|
89
|
-
]);
|
|
90
|
-
expect(result.map((r) => r.delta)).toEqual([null, 5, -3, 8]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('cumsum on [1, 2, 3, 4, 5]', () => {
|
|
94
|
-
const data = [
|
|
95
|
-
{ idx: 1, value: 1 },
|
|
96
|
-
{ idx: 2, value: 2 },
|
|
97
|
-
{ idx: 3, value: 3 },
|
|
98
|
-
{ idx: 4, value: 4 },
|
|
99
|
-
{ idx: 5, value: 5 },
|
|
100
|
-
];
|
|
101
|
-
const result = runTransforms(data, [
|
|
102
|
-
{
|
|
103
|
-
window: [{ op: 'cumsum', field: 'value', as: 'running' }],
|
|
104
|
-
sort: [{ field: 'idx' }],
|
|
105
|
-
},
|
|
106
|
-
]);
|
|
107
|
-
expect(result.map((r) => r.running)).toEqual([1, 3, 6, 10, 15]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('cumsum with nulls [1, null, 3, null, 5] treats nulls as 0', () => {
|
|
111
|
-
const data = [
|
|
112
|
-
{ idx: 1, value: 1 },
|
|
113
|
-
{ idx: 2, value: null },
|
|
114
|
-
{ idx: 3, value: 3 },
|
|
115
|
-
{ idx: 4, value: null },
|
|
116
|
-
{ idx: 5, value: 5 },
|
|
117
|
-
];
|
|
118
|
-
const result = runTransforms(data, [
|
|
119
|
-
{
|
|
120
|
-
window: [{ op: 'cumsum', field: 'value', as: 'running' }],
|
|
121
|
-
sort: [{ field: 'idx' }],
|
|
122
|
-
},
|
|
123
|
-
]);
|
|
124
|
-
expect(result.map((r) => r.running)).toEqual([1, 1, 4, 4, 9]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('rank on values [30, 10, 40, 20] sorted ascending gives ranks [1, 2, 3, 4]', () => {
|
|
128
|
-
// Input is unsorted; window sorts by 'value' ascending
|
|
129
|
-
const data = [
|
|
130
|
-
{ id: 'a', value: 30 },
|
|
131
|
-
{ id: 'b', value: 10 },
|
|
132
|
-
{ id: 'c', value: 40 },
|
|
133
|
-
{ id: 'd', value: 20 },
|
|
134
|
-
];
|
|
135
|
-
const result = runTransforms(data, [
|
|
136
|
-
{
|
|
137
|
-
window: [{ op: 'rank', field: 'value', as: 'rank' }],
|
|
138
|
-
sort: [{ field: 'value', order: 'ascending' }],
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
-
// Results should be in ORIGINAL input order, so:
|
|
142
|
-
// a(30) -> rank 3, b(10) -> rank 1, c(40) -> rank 4, d(20) -> rank 2
|
|
143
|
-
expect(result.map((r) => r.rank)).toEqual([3, 1, 4, 2]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('first_value on [100, 200, 300] all get 100', () => {
|
|
147
|
-
const data = [
|
|
148
|
-
{ idx: 1, value: 100 },
|
|
149
|
-
{ idx: 2, value: 200 },
|
|
150
|
-
{ idx: 3, value: 300 },
|
|
151
|
-
];
|
|
152
|
-
const result = runTransforms(data, [
|
|
153
|
-
{
|
|
154
|
-
window: [{ op: 'first_value', field: 'value', as: 'first' }],
|
|
155
|
-
sort: [{ field: 'idx' }],
|
|
156
|
-
},
|
|
157
|
-
]);
|
|
158
|
-
expect(result.map((r) => r.first)).toEqual([100, 100, 100]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('groupby partitioning: 2 groups of 3 rows, lag(1) computed independently', () => {
|
|
162
|
-
const data = [
|
|
163
|
-
{ group: 'A', idx: 1, value: 10 },
|
|
164
|
-
{ group: 'B', idx: 1, value: 100 },
|
|
165
|
-
{ group: 'A', idx: 2, value: 20 },
|
|
166
|
-
{ group: 'B', idx: 2, value: 200 },
|
|
167
|
-
{ group: 'A', idx: 3, value: 30 },
|
|
168
|
-
{ group: 'B', idx: 3, value: 300 },
|
|
169
|
-
];
|
|
170
|
-
const result = runTransforms(data, [
|
|
171
|
-
{
|
|
172
|
-
window: [{ op: 'lag', field: 'value', offset: 1, as: 'prev' }],
|
|
173
|
-
sort: [{ field: 'idx' }],
|
|
174
|
-
groupby: ['group'],
|
|
175
|
-
},
|
|
176
|
-
]);
|
|
177
|
-
// Results in original input order
|
|
178
|
-
// A idx 1 -> null, B idx 1 -> null, A idx 2 -> 10, B idx 2 -> 100, A idx 3 -> 20, B idx 3 -> 200
|
|
179
|
-
expect(result.map((r) => r.prev)).toEqual([null, null, 10, 100, 20, 200]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('sorts ISO dates correctly before computing lag', () => {
|
|
183
|
-
// Input deliberately unsorted
|
|
184
|
-
const data = [
|
|
185
|
-
{ date: '2024-03-15', value: 300 },
|
|
186
|
-
{ date: '2024-01-10', value: 100 },
|
|
187
|
-
{ date: '2024-02-20', value: 200 },
|
|
188
|
-
];
|
|
189
|
-
const result = runTransforms(data, [
|
|
190
|
-
{
|
|
191
|
-
window: [{ op: 'lag', field: 'value', offset: 1, as: 'prev' }],
|
|
192
|
-
sort: [{ field: 'date' }],
|
|
193
|
-
},
|
|
194
|
-
]);
|
|
195
|
-
// Sorted order: Jan(100), Feb(200), Mar(300)
|
|
196
|
-
// Jan -> null, Feb -> 100, Mar -> 200
|
|
197
|
-
// Original order: Mar, Jan, Feb -> [200, null, 100]
|
|
198
|
-
expect(result.map((r) => r.prev)).toEqual([200, null, 100]);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('sorts numeric timestamp strings numerically, not lexicographically', () => {
|
|
202
|
-
// Lexicographic: "9" > "10" but numeric: 9 < 10
|
|
203
|
-
const data = [
|
|
204
|
-
{ ts: '10', value: 'b' },
|
|
205
|
-
{ ts: '9', value: 'a' },
|
|
206
|
-
{ ts: '100', value: 'c' },
|
|
207
|
-
];
|
|
208
|
-
const result = runTransforms(data, [
|
|
209
|
-
{
|
|
210
|
-
window: [{ op: 'lag', field: 'value', offset: 1, as: 'prev' }],
|
|
211
|
-
sort: [{ field: 'ts' }],
|
|
212
|
-
},
|
|
213
|
-
]);
|
|
214
|
-
// Numeric sort: 9, 10, 100 -> values: a, b, c
|
|
215
|
-
// ts=10(b) -> lag is a, ts=9(a) -> lag is null, ts=100(c) -> lag is b
|
|
216
|
-
expect(result.map((r) => r.prev)).toEqual(['a', null, 'b']);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('lead(1) on 4 rows: last row null, rest match next', () => {
|
|
220
|
-
const data = [
|
|
221
|
-
{ idx: 1, value: 10 },
|
|
222
|
-
{ idx: 2, value: 20 },
|
|
223
|
-
{ idx: 3, value: 30 },
|
|
224
|
-
{ idx: 4, value: 40 },
|
|
225
|
-
];
|
|
226
|
-
const result = runTransforms(data, [
|
|
227
|
-
{
|
|
228
|
-
window: [{ op: 'lead', field: 'value', offset: 1, as: 'next_value' }],
|
|
229
|
-
sort: [{ field: 'idx' }],
|
|
230
|
-
},
|
|
231
|
-
]);
|
|
232
|
-
expect(result.map((r) => r.next_value)).toEqual([20, 30, 40, null]);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('rank with tied values uses competition ranking', () => {
|
|
236
|
-
const data = [
|
|
237
|
-
{ id: 'a', value: 10 },
|
|
238
|
-
{ id: 'b', value: 20 },
|
|
239
|
-
{ id: 'c', value: 10 },
|
|
240
|
-
{ id: 'd', value: 30 },
|
|
241
|
-
];
|
|
242
|
-
const result = runTransforms(data, [
|
|
243
|
-
{
|
|
244
|
-
window: [{ op: 'rank', field: 'value', as: 'rank' }],
|
|
245
|
-
sort: [{ field: 'value', order: 'ascending' }],
|
|
246
|
-
},
|
|
247
|
-
]);
|
|
248
|
-
// Sorted: a(10), c(10), b(20), d(30). Tied values get same rank.
|
|
249
|
-
// Original order: a(10)->1, b(20)->3, c(10)->1, d(30)->4
|
|
250
|
-
expect(result.map((r) => r.rank)).toEqual([1, 3, 1, 4]);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('returns [] for empty data', () => {
|
|
254
|
-
const result = runTransforms(
|
|
255
|
-
[],
|
|
256
|
-
[
|
|
257
|
-
{
|
|
258
|
-
window: [{ op: 'lag', field: 'value', offset: 1, as: 'prev' }],
|
|
259
|
-
sort: [{ field: 'value' }],
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
);
|
|
263
|
-
expect(result).toEqual([]);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('window chained with filter: window then filter on computed field', () => {
|
|
267
|
-
const data = [
|
|
268
|
-
{ idx: 1, value: 100 },
|
|
269
|
-
{ idx: 2, value: 110 },
|
|
270
|
-
{ idx: 3, value: 90 },
|
|
271
|
-
{ idx: 4, value: 120 },
|
|
272
|
-
];
|
|
273
|
-
const result = runTransforms(data, [
|
|
274
|
-
{
|
|
275
|
-
window: [{ op: 'diff', field: 'value', offset: 1, as: 'delta' }],
|
|
276
|
-
sort: [{ field: 'idx' }],
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
filter: { field: 'delta', gt: 0 },
|
|
280
|
-
},
|
|
281
|
-
]);
|
|
282
|
-
// Diffs: [null, 10, -20, 30] -> filter gt 0 -> [10, 30]
|
|
283
|
-
expect(result).toHaveLength(2);
|
|
284
|
-
expect(result.map((r) => r.delta)).toEqual([10, 30]);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Aggregate transform: groups rows and computes summary statistics.
|
|
3
|
-
*
|
|
4
|
-
* Follows Vega-Lite aggregate transform conventions.
|
|
5
|
-
* Groups input data by the specified fields, then applies aggregate
|
|
6
|
-
* operations (sum, mean, count, etc.) to produce one row per group.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { AggregateOp, AggregateTransform, DataRow } from '@opendata-ai/openchart-core';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Compute a single aggregate operation over an array of numeric values.
|
|
13
|
-
* Also used by the sort-by-value sugar expansion in compile/spec-sugar.ts.
|
|
14
|
-
*/
|
|
15
|
-
export function computeAggregate(op: AggregateOp, values: number[]): number {
|
|
16
|
-
if (values.length === 0) return 0;
|
|
17
|
-
|
|
18
|
-
switch (op) {
|
|
19
|
-
case 'count':
|
|
20
|
-
return values.length;
|
|
21
|
-
case 'sum':
|
|
22
|
-
return values.reduce((a, b) => a + b, 0);
|
|
23
|
-
case 'mean': {
|
|
24
|
-
const sum = values.reduce((a, b) => a + b, 0);
|
|
25
|
-
return sum / values.length;
|
|
26
|
-
}
|
|
27
|
-
case 'median': {
|
|
28
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
29
|
-
const mid = Math.floor(sorted.length / 2);
|
|
30
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
31
|
-
}
|
|
32
|
-
case 'min':
|
|
33
|
-
return Math.min(...values);
|
|
34
|
-
case 'max':
|
|
35
|
-
return Math.max(...values);
|
|
36
|
-
case 'variance': {
|
|
37
|
-
if (values.length < 2) return 0;
|
|
38
|
-
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
39
|
-
return values.reduce((a, v) => a + (v - mean) ** 2, 0) / values.length;
|
|
40
|
-
}
|
|
41
|
-
case 'stdev': {
|
|
42
|
-
if (values.length < 2) return 0;
|
|
43
|
-
const m = values.reduce((a, b) => a + b, 0) / values.length;
|
|
44
|
-
return Math.sqrt(values.reduce((a, v) => a + (v - m) ** 2, 0) / values.length);
|
|
45
|
-
}
|
|
46
|
-
case 'q1': {
|
|
47
|
-
const s = [...values].sort((a, b) => a - b);
|
|
48
|
-
const i = (s.length - 1) * 0.25;
|
|
49
|
-
const lo = Math.floor(i);
|
|
50
|
-
const frac = i - lo;
|
|
51
|
-
return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
|
|
52
|
-
}
|
|
53
|
-
case 'q3': {
|
|
54
|
-
const s = [...values].sort((a, b) => a - b);
|
|
55
|
-
const i = (s.length - 1) * 0.75;
|
|
56
|
-
const lo = Math.floor(i);
|
|
57
|
-
const frac = i - lo;
|
|
58
|
-
return s[lo] + frac * ((s[lo + 1] ?? s[lo]) - s[lo]);
|
|
59
|
-
}
|
|
60
|
-
default:
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Build a composite group key from a row's groupby field values.
|
|
67
|
-
*/
|
|
68
|
-
function groupKey(row: DataRow, groupby: string[]): string {
|
|
69
|
-
return groupby.map((f) => String(row[f] ?? '')).join('\x00');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Apply an aggregate transform to data rows.
|
|
74
|
-
*
|
|
75
|
-
* Groups rows by the groupby fields, then computes each aggregate
|
|
76
|
-
* operation within each group. Returns one row per group containing
|
|
77
|
-
* the groupby field values plus computed aggregate fields.
|
|
78
|
-
*
|
|
79
|
-
* @param data - Input rows.
|
|
80
|
-
* @param transform - Aggregate transform definition.
|
|
81
|
-
* @returns Aggregated rows (one per group).
|
|
82
|
-
*/
|
|
83
|
-
export function runAggregate(data: DataRow[], transform: AggregateTransform): DataRow[] {
|
|
84
|
-
const { aggregate, groupby } = transform;
|
|
85
|
-
|
|
86
|
-
// Group rows by the groupby fields
|
|
87
|
-
const groups = new Map<string, DataRow[]>();
|
|
88
|
-
for (const row of data) {
|
|
89
|
-
const key = groupKey(row, groupby);
|
|
90
|
-
const existing = groups.get(key);
|
|
91
|
-
if (existing) {
|
|
92
|
-
existing.push(row);
|
|
93
|
-
} else {
|
|
94
|
-
groups.set(key, [row]);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Compute aggregates for each group
|
|
99
|
-
const result: DataRow[] = [];
|
|
100
|
-
for (const rows of groups.values()) {
|
|
101
|
-
// Start with groupby field values from the first row in the group
|
|
102
|
-
const outRow: DataRow = {};
|
|
103
|
-
for (const field of groupby) {
|
|
104
|
-
outRow[field] = rows[0][field];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Compute each aggregate operation
|
|
108
|
-
for (const agg of aggregate) {
|
|
109
|
-
// distinct counts unique raw values (not just numeric)
|
|
110
|
-
if (agg.op === 'distinct') {
|
|
111
|
-
outRow[agg.as] = new Set(rows.map((r) => r[agg.field])).size;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const values = rows
|
|
116
|
-
.map((r) => {
|
|
117
|
-
// For count, the field value doesn't matter, just count rows
|
|
118
|
-
if (agg.op === 'count') return 1;
|
|
119
|
-
const v = Number(r[agg.field]);
|
|
120
|
-
return Number.isFinite(v) ? v : NaN;
|
|
121
|
-
})
|
|
122
|
-
.filter((v) => !Number.isNaN(v));
|
|
123
|
-
|
|
124
|
-
outRow[agg.as] = computeAggregate(agg.op, values);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
result.push(outRow);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return result;
|
|
131
|
-
}
|
package/src/transforms/bin.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bin transform: discretizes a continuous field into bins.
|
|
3
|
-
*
|
|
4
|
-
* Adds bin start (and optionally bin end) fields to each row.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { BinParams, BinTransform, DataRow } from '@opendata-ai/openchart-core';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Compute a nice step size for binning.
|
|
11
|
-
*/
|
|
12
|
-
function computeStep(extent: [number, number], maxbins: number, nice: boolean): number {
|
|
13
|
-
const span = extent[1] - extent[0];
|
|
14
|
-
if (span === 0) return 1;
|
|
15
|
-
|
|
16
|
-
let step = span / maxbins;
|
|
17
|
-
|
|
18
|
-
if (nice) {
|
|
19
|
-
// Round to a nice step: 1, 2, 5, 10, 20, 50, etc.
|
|
20
|
-
const magnitude = 10 ** Math.floor(Math.log10(step));
|
|
21
|
-
const residual = step / magnitude;
|
|
22
|
-
|
|
23
|
-
if (residual <= 1.5) {
|
|
24
|
-
step = magnitude;
|
|
25
|
-
} else if (residual <= 3.5) {
|
|
26
|
-
step = 2 * magnitude;
|
|
27
|
-
} else if (residual <= 7.5) {
|
|
28
|
-
step = 5 * magnitude;
|
|
29
|
-
} else {
|
|
30
|
-
step = 10 * magnitude;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return step;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Apply a bin transform to data rows.
|
|
39
|
-
*
|
|
40
|
-
* Adds one or two fields to each row:
|
|
41
|
-
* - If `as` is a string: adds `as` with the bin start value.
|
|
42
|
-
* - If `as` is [start, end]: adds both bin start and bin end fields.
|
|
43
|
-
*
|
|
44
|
-
* @param data - Input rows.
|
|
45
|
-
* @param transform - Bin transform definition.
|
|
46
|
-
* @returns New rows with binned field(s) added.
|
|
47
|
-
*/
|
|
48
|
-
export function runBin(data: DataRow[], transform: BinTransform): DataRow[] {
|
|
49
|
-
const params: BinParams = transform.bin === true ? {} : transform.bin;
|
|
50
|
-
const maxbins = params.maxbins ?? 10;
|
|
51
|
-
const nice = params.nice ?? true;
|
|
52
|
-
const field = transform.field;
|
|
53
|
-
|
|
54
|
-
// Compute extent from data if not provided
|
|
55
|
-
let extent = params.extent;
|
|
56
|
-
if (!extent) {
|
|
57
|
-
let min = Infinity;
|
|
58
|
-
let max = -Infinity;
|
|
59
|
-
for (const row of data) {
|
|
60
|
-
const v = Number(row[field]);
|
|
61
|
-
if (Number.isFinite(v)) {
|
|
62
|
-
if (v < min) min = v;
|
|
63
|
-
if (v > max) max = v;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
extent = [min === Infinity ? 0 : min, max === -Infinity ? 0 : max];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const step = params.step ?? computeStep(extent, maxbins, nice);
|
|
70
|
-
const [startAs, endAs] = Array.isArray(transform.as) ? transform.as : [transform.as, undefined];
|
|
71
|
-
|
|
72
|
-
return data.map((row) => {
|
|
73
|
-
const v = Number(row[field]);
|
|
74
|
-
const newRow = { ...row };
|
|
75
|
-
|
|
76
|
-
if (!Number.isFinite(v)) {
|
|
77
|
-
newRow[startAs] = null;
|
|
78
|
-
if (endAs) newRow[endAs] = null;
|
|
79
|
-
} else {
|
|
80
|
-
const binStart = Math.floor((v - extent![0]) / step) * step + extent![0];
|
|
81
|
-
newRow[startAs] = binStart;
|
|
82
|
-
if (endAs) newRow[endAs] = binStart + step;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return newRow;
|
|
86
|
-
});
|
|
87
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calculate transform: adds a computed field to each row.
|
|
3
|
-
*
|
|
4
|
-
* Supports arithmetic operations on fields and constant values.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { CalculateExpression, CalculateTransform, DataRow } from '@opendata-ai/openchart-core';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Evaluate a calculate expression against a datum.
|
|
11
|
-
*/
|
|
12
|
-
function evaluateExpression(datum: DataRow, expr: CalculateExpression): number {
|
|
13
|
-
const fieldValue = Number(datum[expr.field]);
|
|
14
|
-
|
|
15
|
-
// Unary operations (single field)
|
|
16
|
-
switch (expr.op) {
|
|
17
|
-
case 'abs':
|
|
18
|
-
return Math.abs(fieldValue);
|
|
19
|
-
case 'round':
|
|
20
|
-
return Math.round(fieldValue);
|
|
21
|
-
case 'floor':
|
|
22
|
-
return Math.floor(fieldValue);
|
|
23
|
-
case 'ceil':
|
|
24
|
-
return Math.ceil(fieldValue);
|
|
25
|
-
case 'log':
|
|
26
|
-
return Math.log(fieldValue);
|
|
27
|
-
case 'sqrt':
|
|
28
|
-
return Math.sqrt(fieldValue);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Binary operations (field + field2 or field + value)
|
|
32
|
-
const operand = expr.field2 !== undefined ? Number(datum[expr.field2]) : (expr.value ?? 0);
|
|
33
|
-
|
|
34
|
-
switch (expr.op) {
|
|
35
|
-
case '+':
|
|
36
|
-
return fieldValue + operand;
|
|
37
|
-
case '-':
|
|
38
|
-
return fieldValue - operand;
|
|
39
|
-
case '*':
|
|
40
|
-
return fieldValue * operand;
|
|
41
|
-
case '/':
|
|
42
|
-
return operand === 0 ? NaN : fieldValue / operand;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Apply a calculate transform to data rows.
|
|
48
|
-
*
|
|
49
|
-
* Adds a new field with the computed value to each row.
|
|
50
|
-
*
|
|
51
|
-
* @param data - Input rows.
|
|
52
|
-
* @param transform - Calculate transform definition.
|
|
53
|
-
* @returns New rows with the calculated field added.
|
|
54
|
-
*/
|
|
55
|
-
export function runCalculate(data: DataRow[], transform: CalculateTransform): DataRow[] {
|
|
56
|
-
return data.map((row) => ({
|
|
57
|
-
...row,
|
|
58
|
-
[transform.as]: evaluateExpression(row, transform.calculate),
|
|
59
|
-
}));
|
|
60
|
-
}
|