@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,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conditional encoding evaluation.
|
|
3
|
-
*
|
|
4
|
-
* Resolves conditional value definitions per-datum by evaluating
|
|
5
|
-
* predicates and returning the first matching condition's value.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ConditionalValueDef, DataRow } from '@opendata-ai/openchart-core';
|
|
9
|
-
import { evaluatePredicate } from './predicates';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Resolve a conditional value definition for a datum.
|
|
13
|
-
*
|
|
14
|
-
* Evaluates conditions in order, returning the value from the first
|
|
15
|
-
* condition whose test passes. Falls back to the default value if
|
|
16
|
-
* no condition matches.
|
|
17
|
-
*
|
|
18
|
-
* @param datum - The data row to evaluate against.
|
|
19
|
-
* @param channelDef - The conditional value definition.
|
|
20
|
-
* @returns The resolved value, or undefined if no condition matches and no default.
|
|
21
|
-
*/
|
|
22
|
-
export function resolveConditionalValue(datum: DataRow, channelDef: ConditionalValueDef): unknown {
|
|
23
|
-
const conditions = Array.isArray(channelDef.condition)
|
|
24
|
-
? channelDef.condition
|
|
25
|
-
: [channelDef.condition];
|
|
26
|
-
|
|
27
|
-
for (const cond of conditions) {
|
|
28
|
-
if (evaluatePredicate(datum, cond.test)) {
|
|
29
|
-
// If the condition specifies a field, return the datum's field value
|
|
30
|
-
if (cond.field !== undefined) {
|
|
31
|
-
return datum[cond.field];
|
|
32
|
-
}
|
|
33
|
-
return cond.value;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// No condition matched, return default
|
|
38
|
-
return channelDef.value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if a channel definition is a ConditionalValueDef.
|
|
43
|
-
*/
|
|
44
|
-
export function isConditionalValueDef(def: unknown): def is ConditionalValueDef {
|
|
45
|
-
return def !== null && typeof def === 'object' && 'condition' in (def as Record<string, unknown>);
|
|
46
|
-
}
|
package/src/transforms/filter.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filter transform: removes rows that don't match a predicate.
|
|
3
|
-
*
|
|
4
|
-
* Supports RelativeTimeRef values on comparison properties (lt, lte, gt, gte, range).
|
|
5
|
-
* These are resolved against the data extent before filtering.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
DataRow,
|
|
10
|
-
FieldPredicate,
|
|
11
|
-
FilterPredicate,
|
|
12
|
-
RelativeTimeRef,
|
|
13
|
-
} from '@opendata-ai/openchart-core';
|
|
14
|
-
import { evaluatePredicate, isRelativeTimeRef } from './predicates';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Apply a time offset to a Date, returning the resulting timestamp.
|
|
18
|
-
*/
|
|
19
|
-
function applyOffset(anchor: Date, offset: number, unit: RelativeTimeRef['unit']): number {
|
|
20
|
-
const d = new Date(anchor.getTime());
|
|
21
|
-
switch (unit) {
|
|
22
|
-
case 'year':
|
|
23
|
-
d.setFullYear(d.getFullYear() + offset);
|
|
24
|
-
break;
|
|
25
|
-
case 'quarter':
|
|
26
|
-
d.setMonth(d.getMonth() + offset * 3);
|
|
27
|
-
break;
|
|
28
|
-
case 'month':
|
|
29
|
-
d.setMonth(d.getMonth() + offset);
|
|
30
|
-
break;
|
|
31
|
-
case 'week':
|
|
32
|
-
d.setDate(d.getDate() + offset * 7);
|
|
33
|
-
break;
|
|
34
|
-
case 'day':
|
|
35
|
-
d.setDate(d.getDate() + offset);
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
return d.getTime();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a single RelativeTimeRef against a data array.
|
|
43
|
-
* Scans the field for min/max date values, then applies the offset.
|
|
44
|
-
*/
|
|
45
|
-
function resolveRef(data: DataRow[], field: string, ref: RelativeTimeRef): number {
|
|
46
|
-
let anchorMs = ref.anchor === 'max' ? -Infinity : Infinity;
|
|
47
|
-
|
|
48
|
-
for (const row of data) {
|
|
49
|
-
const val = row[field];
|
|
50
|
-
if (val == null) continue;
|
|
51
|
-
const ms = new Date(val as string | number).getTime();
|
|
52
|
-
if (Number.isNaN(ms)) continue;
|
|
53
|
-
if (ref.anchor === 'max' && ms > anchorMs) anchorMs = ms;
|
|
54
|
-
if (ref.anchor === 'min' && ms < anchorMs) anchorMs = ms;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!Number.isFinite(anchorMs)) return 0;
|
|
58
|
-
|
|
59
|
-
return applyOffset(new Date(anchorMs), ref.offset, ref.unit);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Walk a predicate tree and resolve any RelativeTimeRef values to concrete numbers.
|
|
64
|
-
* Returns a new predicate tree (does not mutate the original).
|
|
65
|
-
*/
|
|
66
|
-
function resolveRelativeRefs(data: DataRow[], predicate: FilterPredicate): FilterPredicate {
|
|
67
|
-
if ('and' in predicate) {
|
|
68
|
-
return { and: predicate.and.map((p) => resolveRelativeRefs(data, p)) };
|
|
69
|
-
}
|
|
70
|
-
if ('or' in predicate) {
|
|
71
|
-
return { or: predicate.or.map((p) => resolveRelativeRefs(data, p)) };
|
|
72
|
-
}
|
|
73
|
-
if ('not' in predicate) {
|
|
74
|
-
return { not: resolveRelativeRefs(data, predicate.not) };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// FieldPredicate: check each comparison property for RelativeTimeRef
|
|
78
|
-
if ('field' in predicate) {
|
|
79
|
-
const fp = predicate as FieldPredicate;
|
|
80
|
-
let needsCopy = false;
|
|
81
|
-
const resolved: Partial<FieldPredicate> = {};
|
|
82
|
-
|
|
83
|
-
for (const prop of ['lt', 'lte', 'gt', 'gte'] as const) {
|
|
84
|
-
if (isRelativeTimeRef(fp[prop])) {
|
|
85
|
-
resolved[prop] = resolveRef(data, fp.field, fp[prop] as RelativeTimeRef);
|
|
86
|
-
needsCopy = true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (fp.range) {
|
|
91
|
-
const [lo, hi] = fp.range;
|
|
92
|
-
const loResolved = isRelativeTimeRef(lo) ? resolveRef(data, fp.field, lo) : lo;
|
|
93
|
-
const hiResolved = isRelativeTimeRef(hi) ? resolveRef(data, fp.field, hi) : hi;
|
|
94
|
-
if (isRelativeTimeRef(lo) || isRelativeTimeRef(hi)) {
|
|
95
|
-
resolved.range = [loResolved as number, hiResolved as number];
|
|
96
|
-
needsCopy = true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (needsCopy) {
|
|
101
|
-
return { ...fp, ...resolved };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return predicate;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Filter data rows by a predicate.
|
|
110
|
-
*
|
|
111
|
-
* If the predicate contains RelativeTimeRef values, they are resolved
|
|
112
|
-
* against the data extent first, then the standard evaluatePredicate
|
|
113
|
-
* logic runs per-row.
|
|
114
|
-
*
|
|
115
|
-
* @param data - Input rows.
|
|
116
|
-
* @param predicate - Filter predicate to evaluate per row.
|
|
117
|
-
* @returns Rows that pass the predicate.
|
|
118
|
-
*/
|
|
119
|
-
export function runFilter(data: DataRow[], predicate: FilterPredicate): DataRow[] {
|
|
120
|
-
const resolved = resolveRelativeRefs(data, predicate);
|
|
121
|
-
return data.filter((datum) => evaluatePredicate(datum, resolved));
|
|
122
|
-
}
|
package/src/transforms/fold.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fold transform: unpivots wide-format data into long format.
|
|
3
|
-
*
|
|
4
|
-
* Follows Vega-Lite fold transform conventions.
|
|
5
|
-
* For each input row, creates N new rows (one per fold field),
|
|
6
|
-
* copying all non-fold fields and adding key/value columns.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { DataRow, FoldTransform } from '@opendata-ai/openchart-core';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Apply a fold transform to data rows.
|
|
13
|
-
*
|
|
14
|
-
* For each input row, creates one output row per fold field. Each output
|
|
15
|
-
* row contains all fields from the original row (except the fold fields),
|
|
16
|
-
* plus a key field (the fold field name) and a value field (the fold field value).
|
|
17
|
-
*
|
|
18
|
-
* @param data - Input rows.
|
|
19
|
-
* @param transform - Fold transform definition.
|
|
20
|
-
* @returns Folded rows (N rows per input row, where N = fold fields count).
|
|
21
|
-
*/
|
|
22
|
-
export function runFold(data: DataRow[], transform: FoldTransform): DataRow[] {
|
|
23
|
-
const { fold } = transform;
|
|
24
|
-
const [keyAs, valueAs] = transform.as ?? ['key', 'value'];
|
|
25
|
-
const foldSet = new Set(fold);
|
|
26
|
-
|
|
27
|
-
const result: DataRow[] = [];
|
|
28
|
-
|
|
29
|
-
for (const row of data) {
|
|
30
|
-
// Copy all non-fold fields
|
|
31
|
-
const base: DataRow = {};
|
|
32
|
-
for (const [k, v] of Object.entries(row)) {
|
|
33
|
-
if (!foldSet.has(k)) {
|
|
34
|
-
base[k] = v;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Create one row per fold field
|
|
39
|
-
for (const field of fold) {
|
|
40
|
-
result.push({
|
|
41
|
-
...base,
|
|
42
|
-
[keyAs]: field,
|
|
43
|
-
[valueAs]: row[field],
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return result;
|
|
49
|
-
}
|
package/src/transforms/index.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data transform pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Runs an ordered sequence of transforms (filter, bin, calculate, timeUnit)
|
|
5
|
-
* against a data array. Each transform produces a new array, feeding into
|
|
6
|
-
* the next transform in sequence.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { DataRow, Transform } from '@opendata-ai/openchart-core';
|
|
10
|
-
import { runAggregate } from './aggregate';
|
|
11
|
-
import { runBin } from './bin';
|
|
12
|
-
import { runCalculate } from './calculate';
|
|
13
|
-
import { runFilter } from './filter';
|
|
14
|
-
import { runFold } from './fold';
|
|
15
|
-
import { runTimeUnit } from './timeunit';
|
|
16
|
-
import { runWindow } from './window';
|
|
17
|
-
|
|
18
|
-
export { runAggregate } from './aggregate';
|
|
19
|
-
export { runBin } from './bin';
|
|
20
|
-
export { runCalculate } from './calculate';
|
|
21
|
-
export { isConditionalValueDef, resolveConditionalValue } from './conditional';
|
|
22
|
-
export { runFilter } from './filter';
|
|
23
|
-
export { runFold } from './fold';
|
|
24
|
-
export { evaluatePredicate, isRelativeTimeRef } from './predicates';
|
|
25
|
-
export { runTimeUnit } from './timeunit';
|
|
26
|
-
export { runWindow } from './window';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Run a sequence of transforms against a data array.
|
|
30
|
-
*
|
|
31
|
-
* Each transform is applied in order, producing a new data array
|
|
32
|
-
* that feeds into the next transform. The original data is not mutated.
|
|
33
|
-
*
|
|
34
|
-
* @param data - Input data rows.
|
|
35
|
-
* @param transforms - Ordered array of transform definitions.
|
|
36
|
-
* @returns Transformed data rows.
|
|
37
|
-
*/
|
|
38
|
-
export function runTransforms(data: DataRow[], transforms: Transform[]): DataRow[] {
|
|
39
|
-
let result = data;
|
|
40
|
-
|
|
41
|
-
for (const transform of transforms) {
|
|
42
|
-
if ('filter' in transform) {
|
|
43
|
-
result = runFilter(result, transform.filter);
|
|
44
|
-
} else if ('bin' in transform) {
|
|
45
|
-
result = runBin(result, transform);
|
|
46
|
-
} else if ('calculate' in transform) {
|
|
47
|
-
result = runCalculate(result, transform);
|
|
48
|
-
} else if ('timeUnit' in transform) {
|
|
49
|
-
result = runTimeUnit(result, transform);
|
|
50
|
-
} else if ('aggregate' in transform) {
|
|
51
|
-
result = runAggregate(result, transform);
|
|
52
|
-
} else if ('fold' in transform) {
|
|
53
|
-
result = runFold(result, transform);
|
|
54
|
-
} else if ('window' in transform) {
|
|
55
|
-
result = runWindow(result, transform);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Predicate evaluation for filter transforms and conditional encoding.
|
|
3
|
-
*
|
|
4
|
-
* Supports field predicates (equal, lt, gt, range, oneOf, valid)
|
|
5
|
-
* and logical combinators (and, or, not).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
DataRow,
|
|
10
|
-
FieldPredicate,
|
|
11
|
-
FilterPredicate,
|
|
12
|
-
RelativeTimeRef,
|
|
13
|
-
} from '@opendata-ai/openchart-core';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Check if a predicate is a FieldPredicate (has a 'field' property).
|
|
17
|
-
*/
|
|
18
|
-
function isFieldPredicate(pred: FilterPredicate): pred is FieldPredicate {
|
|
19
|
-
return 'field' in pred;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if a value is a RelativeTimeRef (has anchor, offset, and unit properties).
|
|
24
|
-
*/
|
|
25
|
-
export function isRelativeTimeRef(value: unknown): value is RelativeTimeRef {
|
|
26
|
-
return (
|
|
27
|
-
typeof value === 'object' &&
|
|
28
|
-
value !== null &&
|
|
29
|
-
'anchor' in value &&
|
|
30
|
-
'offset' in value &&
|
|
31
|
-
'unit' in value
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Coerce a comparison value to a number. RelativeTimeRef objects that weren't
|
|
37
|
-
* resolved upstream become NaN, which makes all comparisons return false (safe).
|
|
38
|
-
*/
|
|
39
|
-
function toNum(v: number | RelativeTimeRef): number {
|
|
40
|
-
return typeof v === 'number' ? v : NaN;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Evaluate a single field predicate against a datum.
|
|
45
|
-
*/
|
|
46
|
-
function evaluateFieldPredicate(datum: DataRow, pred: FieldPredicate): boolean {
|
|
47
|
-
const value = datum[pred.field];
|
|
48
|
-
|
|
49
|
-
// valid: check for non-null, non-undefined, non-NaN
|
|
50
|
-
if (pred.valid !== undefined) {
|
|
51
|
-
const isValid = value !== null && value !== undefined && !Number.isNaN(value);
|
|
52
|
-
return pred.valid ? isValid : !isValid;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// equal: use loose equality so "181" == 181 matches across type boundaries
|
|
56
|
-
// (data values may arrive as strings from CSV/JSON parsing)
|
|
57
|
-
if (pred.equal !== undefined) {
|
|
58
|
-
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for CSV/JSON type coercion
|
|
59
|
-
return value == pred.equal;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Numeric comparisons (with date string fallback)
|
|
63
|
-
let numValue = Number(value);
|
|
64
|
-
if (Number.isNaN(numValue) && value != null) {
|
|
65
|
-
const ms = new Date(value as string | number).getTime();
|
|
66
|
-
if (!Number.isNaN(ms)) numValue = ms;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (pred.lt !== undefined) {
|
|
70
|
-
return numValue < toNum(pred.lt);
|
|
71
|
-
}
|
|
72
|
-
if (pred.lte !== undefined) {
|
|
73
|
-
return numValue <= toNum(pred.lte);
|
|
74
|
-
}
|
|
75
|
-
if (pred.gt !== undefined) {
|
|
76
|
-
return numValue > toNum(pred.gt);
|
|
77
|
-
}
|
|
78
|
-
if (pred.gte !== undefined) {
|
|
79
|
-
return numValue >= toNum(pred.gte);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// range: inclusive [min, max]
|
|
83
|
-
if (pred.range !== undefined) {
|
|
84
|
-
const [lo, hi] = pred.range;
|
|
85
|
-
return numValue >= toNum(lo) && numValue <= toNum(hi);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// oneOf: use loose equality (same rationale as equal above)
|
|
89
|
-
if (pred.oneOf !== undefined) {
|
|
90
|
-
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for CSV/JSON type coercion
|
|
91
|
-
return pred.oneOf.some((v) => v == value);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// No condition specified, default to true
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Evaluate a filter predicate (field predicate or logical combinator) against a datum.
|
|
100
|
-
*
|
|
101
|
-
* @param datum - The data row to test.
|
|
102
|
-
* @param predicate - The filter predicate to evaluate.
|
|
103
|
-
* @returns Whether the datum passes the predicate.
|
|
104
|
-
*/
|
|
105
|
-
export function evaluatePredicate(datum: DataRow, predicate: FilterPredicate): boolean {
|
|
106
|
-
if (isFieldPredicate(predicate)) {
|
|
107
|
-
return evaluateFieldPredicate(datum, predicate);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if ('and' in predicate) {
|
|
111
|
-
return predicate.and.every((p) => evaluatePredicate(datum, p));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if ('or' in predicate) {
|
|
115
|
-
return predicate.or.some((p) => evaluatePredicate(datum, p));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if ('not' in predicate) {
|
|
119
|
-
return !evaluatePredicate(datum, predicate.not);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time unit transform: extracts temporal components from date fields.
|
|
3
|
-
*
|
|
4
|
-
* Follows Vega-Lite time unit conventions.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { DataRow, TimeUnit, TimeUnitTransform } from '@opendata-ai/openchart-core';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Extract a time unit value from a Date object.
|
|
11
|
-
*/
|
|
12
|
-
function extractTimeUnit(date: Date, unit: TimeUnit): number | string {
|
|
13
|
-
switch (unit) {
|
|
14
|
-
case 'year':
|
|
15
|
-
return date.getFullYear();
|
|
16
|
-
case 'quarter':
|
|
17
|
-
return Math.floor(date.getMonth() / 3) + 1;
|
|
18
|
-
case 'month':
|
|
19
|
-
return date.getMonth(); // 0-indexed like JS Date
|
|
20
|
-
case 'week': {
|
|
21
|
-
// ISO week number
|
|
22
|
-
const d = new Date(date.getTime());
|
|
23
|
-
d.setHours(0, 0, 0, 0);
|
|
24
|
-
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
|
25
|
-
const yearStart = new Date(d.getFullYear(), 0, 1);
|
|
26
|
-
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
27
|
-
}
|
|
28
|
-
case 'day':
|
|
29
|
-
return date.getDay(); // 0 = Sunday
|
|
30
|
-
case 'dayofyear': {
|
|
31
|
-
const start = new Date(date.getFullYear(), 0, 0);
|
|
32
|
-
const diff = date.getTime() - start.getTime();
|
|
33
|
-
return Math.floor(diff / 86400000);
|
|
34
|
-
}
|
|
35
|
-
case 'date':
|
|
36
|
-
return date.getDate(); // 1-31
|
|
37
|
-
case 'hours':
|
|
38
|
-
return date.getHours();
|
|
39
|
-
case 'minutes':
|
|
40
|
-
return date.getMinutes();
|
|
41
|
-
case 'seconds':
|
|
42
|
-
return date.getSeconds();
|
|
43
|
-
case 'milliseconds':
|
|
44
|
-
return date.getMilliseconds();
|
|
45
|
-
// Compound units
|
|
46
|
-
case 'yearmonth':
|
|
47
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
48
|
-
case 'yearmonthdate':
|
|
49
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
50
|
-
case 'monthdate':
|
|
51
|
-
return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
52
|
-
case 'hoursminutes':
|
|
53
|
-
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Parse a value into a Date object.
|
|
59
|
-
* Handles Date objects, ISO strings, and numeric timestamps.
|
|
60
|
-
*/
|
|
61
|
-
function toDate(value: unknown): Date | null {
|
|
62
|
-
if (value instanceof Date) return value;
|
|
63
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
64
|
-
const d = new Date(value);
|
|
65
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Apply a time unit transform to data rows.
|
|
72
|
-
*
|
|
73
|
-
* Parses the field as a date and extracts the specified time unit,
|
|
74
|
-
* storing the result in a new field.
|
|
75
|
-
*
|
|
76
|
-
* @param data - Input rows.
|
|
77
|
-
* @param transform - Time unit transform definition.
|
|
78
|
-
* @returns New rows with the time unit field added.
|
|
79
|
-
*/
|
|
80
|
-
export function runTimeUnit(data: DataRow[], transform: TimeUnitTransform): DataRow[] {
|
|
81
|
-
return data.map((row) => {
|
|
82
|
-
const date = toDate(row[transform.field]);
|
|
83
|
-
return {
|
|
84
|
-
...row,
|
|
85
|
-
[transform.as]: date ? extractTimeUnit(date, transform.timeUnit) : null,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
}
|
package/src/transforms/window.ts
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Window transform: computes values relative to other rows in sort order
|
|
3
|
-
* within a partition (lag, lead, diff, pct_change, cumsum, rank, first_value).
|
|
4
|
-
*
|
|
5
|
-
* Follows the same grouping pattern as aggregate.ts, but preserves all
|
|
6
|
-
* input rows and appends computed fields rather than collapsing groups.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { DataRow, WindowTransform } from '@opendata-ai/openchart-core';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Build a composite group key from a row's groupby field values.
|
|
13
|
-
* Uses null-char delimiter (same convention as aggregate.ts).
|
|
14
|
-
*/
|
|
15
|
-
function groupKey(row: DataRow, groupby: string[]): string {
|
|
16
|
-
return groupby.map((f) => String(row[f] ?? '')).join('\x00');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Try to parse a value as a date, returning its timestamp if valid.
|
|
21
|
-
* Only recognizes ISO-8601 strings (must contain '-' or 'T') and numeric timestamps.
|
|
22
|
-
* Bare numeric strings like "9" or "100" are not treated as dates.
|
|
23
|
-
*/
|
|
24
|
-
function tryParseDate(val: unknown): number {
|
|
25
|
-
if (val == null) return NaN;
|
|
26
|
-
if (typeof val === 'number') return new Date(val).getTime();
|
|
27
|
-
if (typeof val === 'string' && (val.includes('-') || val.includes('T'))) {
|
|
28
|
-
const ms = new Date(val).getTime();
|
|
29
|
-
return ms;
|
|
30
|
-
}
|
|
31
|
-
return NaN;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Type-aware comparison for sorting:
|
|
36
|
-
* 1. Try ISO date parsing first
|
|
37
|
-
* 2. If both are numeric, compare as numbers
|
|
38
|
-
* 3. Otherwise lexicographic string comparison
|
|
39
|
-
*/
|
|
40
|
-
function compareValues(a: unknown, b: unknown, order: 'ascending' | 'descending'): number {
|
|
41
|
-
const dir = order === 'descending' ? -1 : 1;
|
|
42
|
-
|
|
43
|
-
// Try dates first
|
|
44
|
-
const dateA = tryParseDate(a);
|
|
45
|
-
const dateB = tryParseDate(b);
|
|
46
|
-
if (!Number.isNaN(dateA) && !Number.isNaN(dateB)) {
|
|
47
|
-
return dir * (dateA - dateB);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Try numeric comparison
|
|
51
|
-
const numA = Number(a);
|
|
52
|
-
const numB = Number(b);
|
|
53
|
-
if (Number.isFinite(numA) && Number.isFinite(numB)) {
|
|
54
|
-
return dir * (numA - numB);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Fallback to string comparison
|
|
58
|
-
return dir * String(a ?? '').localeCompare(String(b ?? ''));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Apply a window transform to data rows.
|
|
63
|
-
*
|
|
64
|
-
* Groups rows by the groupby fields, sorts within each group, computes
|
|
65
|
-
* window operations, then returns all rows in their original input order
|
|
66
|
-
* with computed fields appended.
|
|
67
|
-
*
|
|
68
|
-
* @param data - Input rows.
|
|
69
|
-
* @param transform - Window transform definition.
|
|
70
|
-
* @returns Rows with computed window fields appended.
|
|
71
|
-
*/
|
|
72
|
-
export function runWindow(data: DataRow[], transform: WindowTransform): DataRow[] {
|
|
73
|
-
if (data.length === 0) return [];
|
|
74
|
-
|
|
75
|
-
const { window: windowDefs, sort, groupby = [] } = transform;
|
|
76
|
-
|
|
77
|
-
// Track original indices so we can restore input order
|
|
78
|
-
const indexed = data.map((row, i) => ({ row, originalIndex: i }));
|
|
79
|
-
|
|
80
|
-
// Group rows by groupby fields
|
|
81
|
-
const groups = new Map<string, { row: DataRow; originalIndex: number }[]>();
|
|
82
|
-
for (const entry of indexed) {
|
|
83
|
-
const key = groupby.length > 0 ? groupKey(entry.row, groupby) : '';
|
|
84
|
-
const existing = groups.get(key);
|
|
85
|
-
if (existing) {
|
|
86
|
-
existing.push(entry);
|
|
87
|
-
} else {
|
|
88
|
-
groups.set(key, [entry]);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Build result array indexed by original position
|
|
93
|
-
const result: DataRow[] = new Array(data.length);
|
|
94
|
-
|
|
95
|
-
for (const groupEntries of groups.values()) {
|
|
96
|
-
// Sort the group entries by the sort fields
|
|
97
|
-
const sorted = [...groupEntries].sort((a, b) => {
|
|
98
|
-
for (const s of sort) {
|
|
99
|
-
const cmp = compareValues(a.row[s.field], b.row[s.field], s.order ?? 'ascending');
|
|
100
|
-
if (cmp !== 0) return cmp;
|
|
101
|
-
}
|
|
102
|
-
return 0;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Compute window operations for each row in sorted order
|
|
106
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
107
|
-
const entry = sorted[i];
|
|
108
|
-
const outRow: DataRow = { ...entry.row };
|
|
109
|
-
|
|
110
|
-
for (const def of windowDefs) {
|
|
111
|
-
const offset = def.offset ?? 1;
|
|
112
|
-
let computed: unknown = null;
|
|
113
|
-
|
|
114
|
-
switch (def.op) {
|
|
115
|
-
case 'lag': {
|
|
116
|
-
const lagIdx = i - offset;
|
|
117
|
-
computed = lagIdx >= 0 ? (sorted[lagIdx].row[def.field] ?? null) : null;
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
case 'lead': {
|
|
121
|
-
const leadIdx = i + offset;
|
|
122
|
-
computed = leadIdx < sorted.length ? (sorted[leadIdx].row[def.field] ?? null) : null;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case 'diff': {
|
|
126
|
-
const lagIdx = i - offset;
|
|
127
|
-
if (lagIdx >= 0) {
|
|
128
|
-
const current = Number(entry.row[def.field]);
|
|
129
|
-
const lagged = Number(sorted[lagIdx].row[def.field]);
|
|
130
|
-
computed =
|
|
131
|
-
Number.isFinite(current) && Number.isFinite(lagged) ? current - lagged : null;
|
|
132
|
-
}
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
case 'pct_change': {
|
|
136
|
-
const lagIdx = i - offset;
|
|
137
|
-
if (lagIdx >= 0) {
|
|
138
|
-
const current = Number(entry.row[def.field]);
|
|
139
|
-
const lagged = Number(sorted[lagIdx].row[def.field]);
|
|
140
|
-
if (Number.isFinite(current) && Number.isFinite(lagged) && lagged !== 0) {
|
|
141
|
-
computed = (current - lagged) / lagged;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
case 'cumsum': {
|
|
147
|
-
const val = Number(entry.row[def.field]);
|
|
148
|
-
const addend = Number.isFinite(val) ? val : 0;
|
|
149
|
-
if (i === 0) {
|
|
150
|
-
computed = addend;
|
|
151
|
-
} else {
|
|
152
|
-
const prev = Number(result[sorted[i - 1].originalIndex]?.[def.as] ?? 0);
|
|
153
|
-
computed = prev + addend;
|
|
154
|
-
}
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
case 'rank': {
|
|
158
|
-
let rank = i + 1;
|
|
159
|
-
for (let j = 0; j < i; j++) {
|
|
160
|
-
const isTie = sort.every(
|
|
161
|
-
(s) => String(sorted[j].row[s.field]) === String(entry.row[s.field]),
|
|
162
|
-
);
|
|
163
|
-
if (isTie) {
|
|
164
|
-
rank = result[sorted[j].originalIndex]?.[def.as] as number;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
computed = rank;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
case 'first_value': {
|
|
172
|
-
computed = sorted[0].row[def.field] ?? null;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
outRow[def.as] = computed;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
result[entry.originalIndex] = outRow;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return result;
|
|
185
|
-
}
|