@opendata-ai/openchart-engine 8.0.0-rc.10 → 8.0.0-rc.12
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 +7 -0
- package/dist/index.js +123 -54
- 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 -359
- 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 -767
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1548
- 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 -731
- 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 -2093
- 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,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BarList compilation pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Takes a raw barlist spec, validates, normalizes, resolves theme, computes
|
|
5
|
-
* chrome, lays out rows with proportional bars, builds tooltips and a11y,
|
|
6
|
-
* and returns a BarListLayout.
|
|
7
|
-
*
|
|
8
|
-
* Pipeline:
|
|
9
|
-
* validate -> normalize -> resolve theme -> dark mode adapt ->
|
|
10
|
-
* compute chrome -> extract data -> sort/limit rows ->
|
|
11
|
-
* compute row layout -> build row marks -> tooltips -> a11y ->
|
|
12
|
-
* animation -> return BarListLayout
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type {
|
|
16
|
-
BarListLayout,
|
|
17
|
-
BarListRowMark,
|
|
18
|
-
CompileOptions,
|
|
19
|
-
ResolvedAnimation,
|
|
20
|
-
ResolvedTheme,
|
|
21
|
-
TextStyle,
|
|
22
|
-
TooltipContent,
|
|
23
|
-
TooltipField,
|
|
24
|
-
} from '@opendata-ai/openchart-core';
|
|
25
|
-
import {
|
|
26
|
-
adaptTheme,
|
|
27
|
-
computeChrome,
|
|
28
|
-
estimateTextWidth,
|
|
29
|
-
resolveTheme,
|
|
30
|
-
} from '@opendata-ai/openchart-core';
|
|
31
|
-
import { emitSpecWarnings, expandSpecSugar } from '../compile/spec-sugar';
|
|
32
|
-
import { resolveAnimation } from '../compiler/animation';
|
|
33
|
-
import { compile as compileSpec } from '../compiler/index';
|
|
34
|
-
import { resolveFieldFormatter } from '../format/field-format';
|
|
35
|
-
import type { NormalizedBarListSpec } from './types';
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Constants
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
const DEFAULT_ROW_GAP = 8;
|
|
42
|
-
const LABEL_BAR_GAP = 12;
|
|
43
|
-
const BAR_VALUE_GAP = 12;
|
|
44
|
-
const VALUE_WIDTH = 56;
|
|
45
|
-
const LABEL_FONT_SIZE = 13;
|
|
46
|
-
const LABEL_FONT_WEIGHT = 500;
|
|
47
|
-
const SUBTITLE_FONT_SIZE = 12;
|
|
48
|
-
const SUBTITLE_FONT_WEIGHT = 400;
|
|
49
|
-
const VALUE_FONT_SIZE = 12;
|
|
50
|
-
const VALUE_FONT_WEIGHT = 400;
|
|
51
|
-
|
|
52
|
-
const BARLIST_COLORS = ['#06b6d4', '#34d399', '#fbbf24', '#f472b6', '#a78bfa'];
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Public API
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
export function compileBarList(spec: unknown, options: CompileOptions): BarListLayout {
|
|
59
|
-
// Expand deprecated top-level sugar (valueFormat -> encoding.value.format)
|
|
60
|
-
// before validation, then validate + normalize via the shared pipeline.
|
|
61
|
-
const sugarWarnings: string[] = [];
|
|
62
|
-
const expandedSpec =
|
|
63
|
-
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
64
|
-
? expandSpecSugar(spec as Record<string, unknown>, sugarWarnings)
|
|
65
|
-
: spec;
|
|
66
|
-
const { spec: normalized, warnings } = compileSpec(expandedSpec);
|
|
67
|
-
emitSpecWarnings([...sugarWarnings, ...warnings], options.onWarn);
|
|
68
|
-
|
|
69
|
-
if (!('type' in normalized) || normalized.type !== 'barlist') {
|
|
70
|
-
throw new Error(
|
|
71
|
-
'compileBarList received a non-barlist spec. Use compileChart, compileTable, compileGraph, compileSankey, or compileTileMap instead.',
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const barlistSpec = normalized as NormalizedBarListSpec;
|
|
76
|
-
|
|
77
|
-
const rawWatermark = (spec as Record<string, unknown>).watermark;
|
|
78
|
-
const watermark =
|
|
79
|
-
rawWatermark !== undefined ? barlistSpec.watermark : (options.watermark ?? true);
|
|
80
|
-
|
|
81
|
-
// Resolve theme
|
|
82
|
-
const mergedThemeConfig = options.theme
|
|
83
|
-
? { ...barlistSpec.theme, ...options.theme }
|
|
84
|
-
: barlistSpec.theme;
|
|
85
|
-
const lightTheme: ResolvedTheme = resolveTheme(mergedThemeConfig);
|
|
86
|
-
let theme: ResolvedTheme = lightTheme;
|
|
87
|
-
if (options.darkMode) {
|
|
88
|
-
theme = adaptTheme(theme);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Compute chrome
|
|
92
|
-
const chrome = computeChrome(
|
|
93
|
-
{
|
|
94
|
-
title: barlistSpec.chrome.title,
|
|
95
|
-
subtitle: barlistSpec.chrome.subtitle,
|
|
96
|
-
source: barlistSpec.chrome.source,
|
|
97
|
-
byline: barlistSpec.chrome.byline,
|
|
98
|
-
footer: barlistSpec.chrome.footer,
|
|
99
|
-
},
|
|
100
|
-
theme,
|
|
101
|
-
options.width,
|
|
102
|
-
options.measureText,
|
|
103
|
-
'full',
|
|
104
|
-
undefined,
|
|
105
|
-
watermark,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Compute drawing area
|
|
109
|
-
const padding = theme.spacing.padding;
|
|
110
|
-
const fullArea = {
|
|
111
|
-
x: padding,
|
|
112
|
-
y: padding + chrome.topHeight,
|
|
113
|
-
width: options.width - padding * 2,
|
|
114
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
118
|
-
return emptyLayout(chrome, theme, options, watermark);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Extract data
|
|
122
|
-
const labelField = barlistSpec.encoding.label.field;
|
|
123
|
-
const valueField = barlistSpec.encoding.value.field;
|
|
124
|
-
const subtitleField = barlistSpec.encoding.subtitle?.field;
|
|
125
|
-
const colorField = barlistSpec.encoding.color?.field;
|
|
126
|
-
|
|
127
|
-
// Compute row dimensions up front so we can auto-cap maxItems to fit available height
|
|
128
|
-
const barHeight = barlistSpec.barHeight;
|
|
129
|
-
const cornerRadius =
|
|
130
|
-
barlistSpec.cornerRadius === 'pill' ? barHeight / 2 : barlistSpec.cornerRadius;
|
|
131
|
-
const rowContentHeight = Math.max(barHeight, LABEL_FONT_SIZE * 1.4);
|
|
132
|
-
const rowHeight = rowContentHeight + DEFAULT_ROW_GAP;
|
|
133
|
-
const maxFittingRows = Math.max(1, Math.floor(fullArea.height / rowHeight));
|
|
134
|
-
|
|
135
|
-
// Filter valid rows, sort descending, cap to the lesser of maxItems and available height
|
|
136
|
-
const validRows = barlistSpec.data
|
|
137
|
-
.filter((row) => {
|
|
138
|
-
const val = row[valueField];
|
|
139
|
-
return val !== null && val !== undefined && !Number.isNaN(Number(val));
|
|
140
|
-
})
|
|
141
|
-
.sort((a, b) => Number(b[valueField]) - Number(a[valueField]))
|
|
142
|
-
.slice(0, Math.min(barlistSpec.maxItems, maxFittingRows));
|
|
143
|
-
|
|
144
|
-
if (validRows.length === 0) {
|
|
145
|
-
return emptyLayout(chrome, theme, options, watermark);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Use absolute max so negative-only datasets produce valid proportions (e.g. [-100,-50] -> maxAbs=100)
|
|
149
|
-
const maxValue = Math.max(...validRows.map((r) => Math.abs(Number(r[valueField]))));
|
|
150
|
-
|
|
151
|
-
// Color assignment: cycle through barlist-specific palette
|
|
152
|
-
const colorMap = new Map<string, string>();
|
|
153
|
-
let colorIndex = 0;
|
|
154
|
-
const palette = BARLIST_COLORS;
|
|
155
|
-
|
|
156
|
-
function getColor(row: Record<string, unknown>, idx: number): string {
|
|
157
|
-
if (colorField) {
|
|
158
|
-
const key = String(row[colorField] ?? '');
|
|
159
|
-
if (!colorMap.has(key)) {
|
|
160
|
-
colorMap.set(key, palette[colorIndex % palette.length]);
|
|
161
|
-
colorIndex++;
|
|
162
|
-
}
|
|
163
|
-
return colorMap.get(key)!;
|
|
164
|
-
}
|
|
165
|
-
return palette[idx % palette.length];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Value formatter: prefer encoding-level format (v8 canonical), fall back to deprecated valueFormat
|
|
169
|
-
const formatter = resolveFieldFormatter({
|
|
170
|
-
surfaceFormat: barlistSpec.encoding.value.format ?? barlistSpec.valueFormat,
|
|
171
|
-
values: validRows.map((r) => r[valueField]),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Compute label width: measure all labels and use a consistent width
|
|
175
|
-
const measureText =
|
|
176
|
-
options.measureText ??
|
|
177
|
-
((text: string, fontSize: number) => ({
|
|
178
|
-
width: estimateTextWidth(text, fontSize),
|
|
179
|
-
height: fontSize,
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
|
-
// When subtitles are present the column must fit: labelText + 6px gap + subtitleText
|
|
183
|
-
const perRowLabelWidths = new Map<number, number>();
|
|
184
|
-
let maxCombinedWidth = 0;
|
|
185
|
-
for (let i = 0; i < validRows.length; i++) {
|
|
186
|
-
const row = validRows[i];
|
|
187
|
-
const label = String(row[labelField] ?? '');
|
|
188
|
-
const labelW = measureText(label, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT).width;
|
|
189
|
-
perRowLabelWidths.set(i, labelW);
|
|
190
|
-
let combined = labelW + 4;
|
|
191
|
-
if (subtitleField && row[subtitleField] != null) {
|
|
192
|
-
const subtitle = String(row[subtitleField]);
|
|
193
|
-
combined =
|
|
194
|
-
labelW + 6 + measureText(subtitle, SUBTITLE_FONT_SIZE, SUBTITLE_FONT_WEIGHT).width + 4;
|
|
195
|
-
}
|
|
196
|
-
maxCombinedWidth = Math.max(maxCombinedWidth, combined);
|
|
197
|
-
}
|
|
198
|
-
const isNarrow = fullArea.width < 400;
|
|
199
|
-
const labelBarGap = isNarrow ? 8 : LABEL_BAR_GAP;
|
|
200
|
-
const barValueGap = isNarrow ? 6 : BAR_VALUE_GAP;
|
|
201
|
-
const valueWidth = isNarrow ? 44 : VALUE_WIDTH;
|
|
202
|
-
const maxLabelPct = isNarrow ? 0.35 : 0.4;
|
|
203
|
-
const labelWidth = Math.max(50, Math.min(maxCombinedWidth, fullArea.width * maxLabelPct));
|
|
204
|
-
|
|
205
|
-
const barAreaWidth = fullArea.width - labelWidth - labelBarGap - barValueGap - valueWidth;
|
|
206
|
-
|
|
207
|
-
const labelColor = theme.colors.text;
|
|
208
|
-
const subtitleColor = options.darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
|
|
209
|
-
const valueColor = options.darkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.55)';
|
|
210
|
-
|
|
211
|
-
// Build row marks
|
|
212
|
-
const rows: BarListRowMark[] = [];
|
|
213
|
-
|
|
214
|
-
for (let i = 0; i < validRows.length; i++) {
|
|
215
|
-
const row = validRows[i];
|
|
216
|
-
const value = Number(row[valueField]);
|
|
217
|
-
const labelText = String(row[labelField] ?? '');
|
|
218
|
-
const formattedValue = formatter(value);
|
|
219
|
-
const barColor = getColor(row, i);
|
|
220
|
-
const pct = maxValue > 0 ? Math.abs(value) / maxValue : 0;
|
|
221
|
-
|
|
222
|
-
const rowY = fullArea.y + i * rowHeight;
|
|
223
|
-
const centerY = rowY + rowContentHeight / 2;
|
|
224
|
-
|
|
225
|
-
// Label (left-aligned)
|
|
226
|
-
const labelX = fullArea.x;
|
|
227
|
-
const labelStyle: TextStyle = {
|
|
228
|
-
fontFamily: theme.fonts.family,
|
|
229
|
-
fontSize: LABEL_FONT_SIZE,
|
|
230
|
-
fontWeight: LABEL_FONT_WEIGHT,
|
|
231
|
-
fill: labelColor,
|
|
232
|
-
lineHeight: 1.4,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
// Subtitle (left-aligned, positioned after this row's measured label width + gap)
|
|
236
|
-
let subtitle: BarListRowMark['subtitle'];
|
|
237
|
-
if (subtitleField && row[subtitleField] != null) {
|
|
238
|
-
const subtitleText = String(row[subtitleField]);
|
|
239
|
-
const subtitleX = labelX + (perRowLabelWidths.get(i) ?? 0) + 6;
|
|
240
|
-
subtitle = {
|
|
241
|
-
text: subtitleText,
|
|
242
|
-
x: subtitleX,
|
|
243
|
-
y: centerY,
|
|
244
|
-
style: {
|
|
245
|
-
fontFamily: theme.fonts.family,
|
|
246
|
-
fontSize: SUBTITLE_FONT_SIZE,
|
|
247
|
-
fontWeight: SUBTITLE_FONT_WEIGHT,
|
|
248
|
-
fill: subtitleColor,
|
|
249
|
-
lineHeight: 1.4,
|
|
250
|
-
},
|
|
251
|
-
visible: true,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Track (muted background bar)
|
|
256
|
-
const trackX = fullArea.x + labelWidth + labelBarGap;
|
|
257
|
-
const trackY = centerY - barHeight / 2;
|
|
258
|
-
const trackWidth = Math.max(barAreaWidth, 0);
|
|
259
|
-
|
|
260
|
-
// Fill bar (proportional width)
|
|
261
|
-
const barWidth = Math.max(pct * trackWidth, 0);
|
|
262
|
-
|
|
263
|
-
// Value label (right-aligned)
|
|
264
|
-
const valueLabelX = trackX + trackWidth + barValueGap + valueWidth;
|
|
265
|
-
const valueLabelStyle: TextStyle = {
|
|
266
|
-
fontFamily: `${theme.fonts.family}, ui-monospace, monospace`,
|
|
267
|
-
fontSize: VALUE_FONT_SIZE,
|
|
268
|
-
fontWeight: VALUE_FONT_WEIGHT,
|
|
269
|
-
fill: valueColor,
|
|
270
|
-
lineHeight: 1.4,
|
|
271
|
-
fontVariant: 'tabular-nums',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const rowMark: BarListRowMark = {
|
|
275
|
-
type: 'barlist-row',
|
|
276
|
-
index: i,
|
|
277
|
-
y: rowY,
|
|
278
|
-
height: rowHeight,
|
|
279
|
-
label: {
|
|
280
|
-
text: labelText,
|
|
281
|
-
x: labelX,
|
|
282
|
-
y: centerY,
|
|
283
|
-
style: labelStyle,
|
|
284
|
-
visible: true,
|
|
285
|
-
},
|
|
286
|
-
subtitle,
|
|
287
|
-
track: {
|
|
288
|
-
x: trackX,
|
|
289
|
-
y: trackY,
|
|
290
|
-
width: trackWidth,
|
|
291
|
-
height: barHeight,
|
|
292
|
-
cornerRadius,
|
|
293
|
-
},
|
|
294
|
-
bar: {
|
|
295
|
-
x: trackX,
|
|
296
|
-
y: trackY,
|
|
297
|
-
width: barWidth,
|
|
298
|
-
height: barHeight,
|
|
299
|
-
cornerRadius,
|
|
300
|
-
fill: barColor,
|
|
301
|
-
},
|
|
302
|
-
valueLabel: {
|
|
303
|
-
text: formattedValue,
|
|
304
|
-
x: valueLabelX,
|
|
305
|
-
y: centerY,
|
|
306
|
-
style: valueLabelStyle,
|
|
307
|
-
visible: true,
|
|
308
|
-
},
|
|
309
|
-
value,
|
|
310
|
-
formattedValue,
|
|
311
|
-
aria: {
|
|
312
|
-
role: 'listitem',
|
|
313
|
-
label: `${labelText}: ${formattedValue}`,
|
|
314
|
-
},
|
|
315
|
-
animationIndex: i,
|
|
316
|
-
data: row,
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
rows.push(rowMark);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Build tooltip descriptors.
|
|
323
|
-
// TODO: honour encoding.tooltip channel to let callers add extra fields beyond the default value field.
|
|
324
|
-
const tooltipDescriptors = new Map<string, TooltipContent>();
|
|
325
|
-
for (const row of rows) {
|
|
326
|
-
const fields: TooltipField[] = [
|
|
327
|
-
{ label: barlistSpec.encoding.value.title ?? valueField, value: row.formattedValue },
|
|
328
|
-
];
|
|
329
|
-
tooltipDescriptors.set(String(row.index), {
|
|
330
|
-
title: row.label.text,
|
|
331
|
-
fields,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Build a11y metadata
|
|
336
|
-
const a11y = {
|
|
337
|
-
altText: `Bar list showing ${rows.length} items ranked by ${valueField}`,
|
|
338
|
-
dataTableFallback: rows.map((r) => [r.label.text, r.formattedValue]),
|
|
339
|
-
role: 'list' as const,
|
|
340
|
-
keyboardNavigable: rows.length > 0,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
// Resolve animation
|
|
344
|
-
const resolvedAnimation: ResolvedAnimation | undefined = resolveAnimation(barlistSpec.animation);
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
area: fullArea,
|
|
348
|
-
chrome,
|
|
349
|
-
rows,
|
|
350
|
-
tooltipDescriptors,
|
|
351
|
-
a11y,
|
|
352
|
-
theme,
|
|
353
|
-
width: options.width,
|
|
354
|
-
height: options.height,
|
|
355
|
-
animation: resolvedAnimation,
|
|
356
|
-
watermark,
|
|
357
|
-
measureText,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
|
-
// Empty layout fallback
|
|
363
|
-
// ---------------------------------------------------------------------------
|
|
364
|
-
|
|
365
|
-
function emptyLayout(
|
|
366
|
-
chrome: ReturnType<typeof computeChrome>,
|
|
367
|
-
theme: ResolvedTheme,
|
|
368
|
-
options: CompileOptions,
|
|
369
|
-
watermark: boolean,
|
|
370
|
-
): BarListLayout {
|
|
371
|
-
return {
|
|
372
|
-
area: { x: 0, y: 0, width: 0, height: 0 },
|
|
373
|
-
chrome,
|
|
374
|
-
rows: [],
|
|
375
|
-
tooltipDescriptors: new Map(),
|
|
376
|
-
a11y: {
|
|
377
|
-
altText: 'Empty bar list',
|
|
378
|
-
dataTableFallback: [],
|
|
379
|
-
role: 'list',
|
|
380
|
-
keyboardNavigable: false,
|
|
381
|
-
},
|
|
382
|
-
theme,
|
|
383
|
-
width: options.width,
|
|
384
|
-
height: options.height,
|
|
385
|
-
watermark,
|
|
386
|
-
animation: undefined,
|
|
387
|
-
measureText:
|
|
388
|
-
options.measureText ??
|
|
389
|
-
((text, fontSize) => ({ width: estimateTextWidth(text, fontSize), height: fontSize })),
|
|
390
|
-
};
|
|
391
|
-
}
|
package/src/barlist/types.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal normalized barlist spec type used by the compilation pipeline.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
AnimationSpec,
|
|
7
|
-
BarListEncoding,
|
|
8
|
-
DarkMode,
|
|
9
|
-
DataRow,
|
|
10
|
-
ThemeConfig,
|
|
11
|
-
} from '@opendata-ai/openchart-core';
|
|
12
|
-
|
|
13
|
-
import type { NormalizedChrome } from '../compiler/types';
|
|
14
|
-
|
|
15
|
-
export interface NormalizedBarListSpec {
|
|
16
|
-
type: 'barlist';
|
|
17
|
-
data: DataRow[];
|
|
18
|
-
encoding: BarListEncoding;
|
|
19
|
-
barHeight: number;
|
|
20
|
-
cornerRadius: number | 'pill';
|
|
21
|
-
maxItems: number;
|
|
22
|
-
chrome: NormalizedChrome;
|
|
23
|
-
theme: ThemeConfig;
|
|
24
|
-
darkMode: DarkMode;
|
|
25
|
-
watermark: boolean;
|
|
26
|
-
animation?: AnimationSpec;
|
|
27
|
-
valueFormat?: string;
|
|
28
|
-
}
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import type { Mark, PointMark, RectMark, ResolvedAnimation } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import type { ResolvedScales } from '../../layout/scales';
|
|
4
|
-
import { assignAnimationIndices, computeMarkObstacles, resolveRendererKey } from '../post-process';
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// computeMarkObstacles
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
describe('computeMarkObstacles', () => {
|
|
11
|
-
it('returns individual rects for non-band rect marks', () => {
|
|
12
|
-
const marks: Mark[] = [
|
|
13
|
-
{ type: 'rect', x: 10, y: 20, width: 50, height: 30, fill: '#000' } as RectMark,
|
|
14
|
-
{ type: 'rect', x: 80, y: 20, width: 50, height: 30, fill: '#000' } as RectMark,
|
|
15
|
-
];
|
|
16
|
-
const scales = { y: { type: 'linear' } } as unknown as ResolvedScales;
|
|
17
|
-
const result = computeMarkObstacles(marks, scales);
|
|
18
|
-
expect(result).toHaveLength(2);
|
|
19
|
-
expect(result[0]).toEqual({ x: 10, y: 20, width: 50, height: 30, kind: 'mark' });
|
|
20
|
-
expect(result[1]).toEqual({ x: 80, y: 20, width: 50, height: 30, kind: 'mark' });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns point mark bounds as bounding box from cx/cy/r', () => {
|
|
24
|
-
const marks: Mark[] = [{ type: 'point', cx: 100, cy: 100, r: 10, fill: '#000' } as PointMark];
|
|
25
|
-
const scales = { y: { type: 'linear' } } as unknown as ResolvedScales;
|
|
26
|
-
const result = computeMarkObstacles(marks, scales);
|
|
27
|
-
expect(result).toHaveLength(1);
|
|
28
|
-
expect(result[0]).toEqual({ x: 90, y: 90, width: 20, height: 20, kind: 'mark' });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns grouped row obstacles for band-scale charts', () => {
|
|
32
|
-
const marks: Mark[] = [
|
|
33
|
-
{ type: 'rect', x: 10, y: 50, width: 40, height: 20, fill: '#000' } as RectMark,
|
|
34
|
-
{ type: 'rect', x: 60, y: 50, width: 30, height: 20, fill: '#000' } as RectMark,
|
|
35
|
-
];
|
|
36
|
-
const bandScale = Object.assign((v: string) => (v === 'A' ? 40 : 100), {
|
|
37
|
-
bandwidth: () => 30,
|
|
38
|
-
domain: () => ['A', 'B'],
|
|
39
|
-
});
|
|
40
|
-
const scales = { y: { type: 'band', scale: bandScale } } as unknown as ResolvedScales;
|
|
41
|
-
const result = computeMarkObstacles(marks, scales);
|
|
42
|
-
// Both marks have cy ~60, so they group into one row obstacle
|
|
43
|
-
expect(result).toHaveLength(1);
|
|
44
|
-
expect(result[0].x).toBe(10);
|
|
45
|
-
expect(result[0].width).toBe(80); // 90 - 10
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns empty array for empty marks', () => {
|
|
49
|
-
const scales = { y: { type: 'linear' } } as unknown as ResolvedScales;
|
|
50
|
-
const result = computeMarkObstacles([], scales);
|
|
51
|
-
expect(result).toHaveLength(0);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// resolveRendererKey
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
describe('resolveRendererKey', () => {
|
|
60
|
-
it('keeps bar as bar when x=quantitative, y=nominal (horizontal)', () => {
|
|
61
|
-
const encoding = {
|
|
62
|
-
x: { field: 'val', type: 'quantitative' },
|
|
63
|
-
y: { field: 'cat', type: 'nominal' },
|
|
64
|
-
};
|
|
65
|
-
expect(resolveRendererKey('bar', encoding, {})).toBe('bar');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('resolves bar to bar:vertical when x=nominal, y=quantitative', () => {
|
|
69
|
-
const encoding = {
|
|
70
|
-
x: { field: 'cat', type: 'nominal' },
|
|
71
|
-
y: { field: 'val', type: 'quantitative' },
|
|
72
|
-
};
|
|
73
|
-
expect(resolveRendererKey('bar', encoding, {})).toBe('bar:vertical');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('resolves bar to bar:vertical when x=ordinal, y=quantitative', () => {
|
|
77
|
-
const encoding = {
|
|
78
|
-
x: { field: 'cat', type: 'ordinal' },
|
|
79
|
-
y: { field: 'val', type: 'quantitative' },
|
|
80
|
-
};
|
|
81
|
-
expect(resolveRendererKey('bar', encoding, {})).toBe('bar:vertical');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('resolves bar to bar:vertical when x=temporal, y=quantitative', () => {
|
|
85
|
-
const encoding = {
|
|
86
|
-
x: { field: 'date', type: 'temporal' },
|
|
87
|
-
y: { field: 'val', type: 'quantitative' },
|
|
88
|
-
};
|
|
89
|
-
expect(resolveRendererKey('bar', encoding, {})).toBe('bar:vertical');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('resolves arc to arc:donut when innerRadius > 0', () => {
|
|
93
|
-
expect(resolveRendererKey('arc', {}, { innerRadius: 50 })).toBe('arc:donut');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('keeps arc as arc when no innerRadius', () => {
|
|
97
|
-
expect(resolveRendererKey('arc', {}, {})).toBe('arc');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('keeps arc as arc when innerRadius is 0', () => {
|
|
101
|
-
expect(resolveRendererKey('arc', {}, { innerRadius: 0 })).toBe('arc');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('passes through other mark types unchanged', () => {
|
|
105
|
-
expect(resolveRendererKey('line', {}, {})).toBe('line');
|
|
106
|
-
expect(resolveRendererKey('area', {}, {})).toBe('area');
|
|
107
|
-
expect(resolveRendererKey('point', {}, {})).toBe('point');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// assignAnimationIndices
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
describe('assignAnimationIndices', () => {
|
|
116
|
-
it('assigns sequential indices sorted by primary value for value stagger', () => {
|
|
117
|
-
const marks: Mark[] = [
|
|
118
|
-
{ type: 'rect', x: 0, y: 0, width: 10, height: 30, fill: '#000' } as RectMark,
|
|
119
|
-
{ type: 'rect', x: 0, y: 0, width: 10, height: 10, fill: '#000' } as RectMark,
|
|
120
|
-
{ type: 'rect', x: 0, y: 0, width: 10, height: 50, fill: '#000' } as RectMark,
|
|
121
|
-
];
|
|
122
|
-
const animation: ResolvedAnimation = {
|
|
123
|
-
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
124
|
-
annotationDelay: 0,
|
|
125
|
-
};
|
|
126
|
-
assignAnimationIndices(marks, animation);
|
|
127
|
-
// Sorted by height: 10, 30, 50 -> indices 0, 1, 2
|
|
128
|
-
expect(marks[0].animationIndex).toBe(1); // height 30
|
|
129
|
-
expect(marks[1].animationIndex).toBe(0); // height 10
|
|
130
|
-
expect(marks[2].animationIndex).toBe(2); // height 50
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('assigns group-based indices for stacked rects', () => {
|
|
134
|
-
const marks: Mark[] = [
|
|
135
|
-
{
|
|
136
|
-
type: 'rect',
|
|
137
|
-
x: 0,
|
|
138
|
-
y: 0,
|
|
139
|
-
width: 10,
|
|
140
|
-
height: 30,
|
|
141
|
-
fill: '#000',
|
|
142
|
-
stackGroup: 'A',
|
|
143
|
-
} as RectMark,
|
|
144
|
-
{
|
|
145
|
-
type: 'rect',
|
|
146
|
-
x: 0,
|
|
147
|
-
y: 0,
|
|
148
|
-
width: 10,
|
|
149
|
-
height: 20,
|
|
150
|
-
fill: '#000',
|
|
151
|
-
stackGroup: 'A',
|
|
152
|
-
} as RectMark,
|
|
153
|
-
{
|
|
154
|
-
type: 'rect',
|
|
155
|
-
x: 0,
|
|
156
|
-
y: 0,
|
|
157
|
-
width: 10,
|
|
158
|
-
height: 40,
|
|
159
|
-
fill: '#000',
|
|
160
|
-
stackGroup: 'B',
|
|
161
|
-
} as RectMark,
|
|
162
|
-
];
|
|
163
|
-
const animation: ResolvedAnimation = {
|
|
164
|
-
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
165
|
-
annotationDelay: 0,
|
|
166
|
-
};
|
|
167
|
-
assignAnimationIndices(marks, animation);
|
|
168
|
-
// Stack group A gets index 0, B gets index 1
|
|
169
|
-
const rectMarks = marks as RectMark[];
|
|
170
|
-
expect(rectMarks[0].animationIndex).toBe(0);
|
|
171
|
-
expect(rectMarks[0].stackPos).toBe(0);
|
|
172
|
-
expect(rectMarks[1].animationIndex).toBe(0);
|
|
173
|
-
expect(rectMarks[1].stackPos).toBe(1);
|
|
174
|
-
expect(rectMarks[2].animationIndex).toBe(1);
|
|
175
|
-
expect(rectMarks[2].stackPos).toBe(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('stack indices overwrite value-based indices', () => {
|
|
179
|
-
const marks: Mark[] = [
|
|
180
|
-
{
|
|
181
|
-
type: 'rect',
|
|
182
|
-
x: 0,
|
|
183
|
-
y: 0,
|
|
184
|
-
width: 10,
|
|
185
|
-
height: 30,
|
|
186
|
-
fill: '#000',
|
|
187
|
-
stackGroup: 'A',
|
|
188
|
-
} as RectMark,
|
|
189
|
-
{
|
|
190
|
-
type: 'rect',
|
|
191
|
-
x: 0,
|
|
192
|
-
y: 0,
|
|
193
|
-
width: 10,
|
|
194
|
-
height: 50,
|
|
195
|
-
fill: '#000',
|
|
196
|
-
stackGroup: 'A',
|
|
197
|
-
} as RectMark,
|
|
198
|
-
];
|
|
199
|
-
const animation: ResolvedAnimation = {
|
|
200
|
-
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
201
|
-
annotationDelay: 0,
|
|
202
|
-
};
|
|
203
|
-
assignAnimationIndices(marks, animation);
|
|
204
|
-
// Both should have the same group index (0), not value-sorted indices
|
|
205
|
-
expect((marks[0] as RectMark).animationIndex).toBe(0);
|
|
206
|
-
expect((marks[1] as RectMark).animationIndex).toBe(0);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('is a no-op when animation is undefined', () => {
|
|
210
|
-
const marks: Mark[] = [
|
|
211
|
-
{ type: 'rect', x: 0, y: 0, width: 10, height: 30, fill: '#000' } as RectMark,
|
|
212
|
-
];
|
|
213
|
-
assignAnimationIndices(marks, undefined);
|
|
214
|
-
expect(marks[0].animationIndex).toBeUndefined();
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('is a no-op when animation has no enter phase', () => {
|
|
218
|
-
const marks: Mark[] = [
|
|
219
|
-
{ type: 'rect', x: 0, y: 0, width: 10, height: 30, fill: '#000' } as RectMark,
|
|
220
|
-
];
|
|
221
|
-
const animation: ResolvedAnimation = {
|
|
222
|
-
annotationDelay: 0,
|
|
223
|
-
};
|
|
224
|
-
assignAnimationIndices(marks, animation);
|
|
225
|
-
expect(marks[0].animationIndex).toBeUndefined();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('handles empty marks array', () => {
|
|
229
|
-
const animation: ResolvedAnimation = {
|
|
230
|
-
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
231
|
-
annotationDelay: 0,
|
|
232
|
-
};
|
|
233
|
-
const marks: Mark[] = [];
|
|
234
|
-
assignAnimationIndices(marks, animation);
|
|
235
|
-
expect(marks).toHaveLength(0);
|
|
236
|
-
});
|
|
237
|
-
});
|