@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,179 +0,0 @@
|
|
|
1
|
-
import type { RectMark } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { buildD3Formatter } from '@opendata-ai/openchart-core';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { computeColumnLabels } from '../labels';
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Fixtures
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
const chartArea = { x: 0, y: 0, width: 400, height: 300 };
|
|
11
|
-
|
|
12
|
-
function makeMark(index: number, value: number): RectMark {
|
|
13
|
-
const height = Math.abs(value) * 5;
|
|
14
|
-
const y = value >= 0 ? 300 - height : 300;
|
|
15
|
-
return {
|
|
16
|
-
type: 'rect',
|
|
17
|
-
x: index * 80,
|
|
18
|
-
y,
|
|
19
|
-
width: 60,
|
|
20
|
-
height,
|
|
21
|
-
fill: '#4e79a7',
|
|
22
|
-
data: { category: `Cat${index}`, value },
|
|
23
|
-
aria: { label: `Cat${index}: ${value}` },
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const marks: RectMark[] = [
|
|
28
|
-
makeMark(0, 10),
|
|
29
|
-
makeMark(1, 20),
|
|
30
|
-
makeMark(2, 30),
|
|
31
|
-
makeMark(3, 40),
|
|
32
|
-
makeMark(4, 50),
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Tests
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
describe('computeColumnLabels density modes', () => {
|
|
40
|
-
it('density "auto" runs collision detection and produces labels', () => {
|
|
41
|
-
const labels = computeColumnLabels(marks, chartArea, 'auto');
|
|
42
|
-
expect(labels.length).toBeGreaterThan(0);
|
|
43
|
-
expect(labels.every((l) => typeof l.visible === 'boolean')).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('density "all" shows every label as visible', () => {
|
|
47
|
-
const labels = computeColumnLabels(marks, chartArea, 'all');
|
|
48
|
-
expect(labels).toHaveLength(marks.length);
|
|
49
|
-
expect(labels.every((l) => l.visible === true)).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('density "none" returns empty array', () => {
|
|
53
|
-
const labels = computeColumnLabels(marks, chartArea, 'none');
|
|
54
|
-
expect(labels).toHaveLength(0);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('density "endpoints" returns only first and last labels', () => {
|
|
58
|
-
const labels = computeColumnLabels(marks, chartArea, 'endpoints');
|
|
59
|
-
expect(labels).toHaveLength(2);
|
|
60
|
-
expect(labels[0].text).toBe('10');
|
|
61
|
-
expect(labels[1].text).toBe('50');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('density "endpoints" with single mark returns that mark', () => {
|
|
65
|
-
const labels = computeColumnLabels([marks[0]], chartArea, 'endpoints');
|
|
66
|
-
expect(labels).toHaveLength(1);
|
|
67
|
-
expect(labels[0].text).toBe('10');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('default density is "auto"', () => {
|
|
71
|
-
const withAuto = computeColumnLabels(marks, chartArea, 'auto');
|
|
72
|
-
const withDefault = computeColumnLabels(marks, chartArea);
|
|
73
|
-
expect(withDefault.length).toBe(withAuto.length);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('computeColumnLabels positioning', () => {
|
|
78
|
-
it('places positive value labels above the column', () => {
|
|
79
|
-
const labels = computeColumnLabels([makeMark(0, 20)], chartArea, 'all');
|
|
80
|
-
expect(labels).toHaveLength(1);
|
|
81
|
-
const mark = makeMark(0, 20);
|
|
82
|
-
// Label y should be above the column top
|
|
83
|
-
expect(labels[0].y).toBeLessThan(mark.y);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('places negative value labels below the column', () => {
|
|
87
|
-
const negativeMark = makeMark(0, -15);
|
|
88
|
-
const labels = computeColumnLabels([negativeMark], chartArea, 'all');
|
|
89
|
-
expect(labels).toHaveLength(1);
|
|
90
|
-
// Label y should be below the column bottom
|
|
91
|
-
expect(labels[0].y).toBeGreaterThan(negativeMark.y + negativeMark.height);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('centers labels horizontally on the column', () => {
|
|
95
|
-
const labels = computeColumnLabels([makeMark(0, 20)], chartArea, 'all');
|
|
96
|
-
const mark = makeMark(0, 20);
|
|
97
|
-
const markCenter = mark.x + mark.width / 2;
|
|
98
|
-
expect(labels[0].x).toBe(markCenter);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('applies labelFormat to numeric values', () => {
|
|
102
|
-
const labels = computeColumnLabels(
|
|
103
|
-
[makeMark(0, 1234)],
|
|
104
|
-
chartArea,
|
|
105
|
-
'all',
|
|
106
|
-
buildD3Formatter(',.0f'),
|
|
107
|
-
);
|
|
108
|
-
expect(labels[0].text).toBe('1,234');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('computeColumnLabels stacked segments', () => {
|
|
113
|
-
/** One segment of a stacked column, positioned by its top edge. */
|
|
114
|
-
function makeStackedMark(y: number, height: number, fill: string, value: number): RectMark {
|
|
115
|
-
return {
|
|
116
|
-
type: 'rect',
|
|
117
|
-
x: 0,
|
|
118
|
-
y,
|
|
119
|
-
width: 60,
|
|
120
|
-
height,
|
|
121
|
-
fill,
|
|
122
|
-
stackGroup: 'Cat0',
|
|
123
|
-
data: { category: 'Cat0', value },
|
|
124
|
-
aria: { label: `Cat0: ${value}` },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
it('centers the label inside its own segment, not above it', () => {
|
|
129
|
-
// Above a stacked segment's top edge is the next segment up, not the
|
|
130
|
-
// background, so the label must stay within its own band.
|
|
131
|
-
const mark = makeStackedMark(100, 80, '#0e7490', 33);
|
|
132
|
-
const labels = computeColumnLabels([mark], chartArea, 'all');
|
|
133
|
-
|
|
134
|
-
expect(labels).toHaveLength(1);
|
|
135
|
-
expect(labels[0].y).toBeGreaterThan(mark.y);
|
|
136
|
-
expect(labels[0].y).toBeLessThan(mark.y + mark.height);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('picks a fill that contrasts with its own segment', () => {
|
|
140
|
-
// A dark segment takes white text; a light segment takes near-black. The
|
|
141
|
-
// old code tinted the text with the segment's own series color, which put
|
|
142
|
-
// colored text on a colored fill.
|
|
143
|
-
const dark = computeColumnLabels([makeStackedMark(100, 80, '#0e7490', 33)], chartArea, 'all');
|
|
144
|
-
const light = computeColumnLabels([makeStackedMark(100, 80, '#e2e8f0', 33)], chartArea, 'all');
|
|
145
|
-
|
|
146
|
-
expect(dark[0].style.fill).toBe('#ffffff');
|
|
147
|
-
expect(light[0].style.fill).toBe('#111111');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('hides the label when the segment is too short to hold it', () => {
|
|
151
|
-
// A ~4% segment (Nuclear in the energy-mix demo) can't fit a 10px label
|
|
152
|
-
// without spilling into its neighbours.
|
|
153
|
-
const labels = computeColumnLabels([makeStackedMark(100, 6, '#0e7490', 4)], chartArea, 'all');
|
|
154
|
-
|
|
155
|
-
expect(labels).toHaveLength(1);
|
|
156
|
-
expect(labels[0].visible).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('an explicit labels.color still wins over the contrast pick', () => {
|
|
160
|
-
const labels = computeColumnLabels(
|
|
161
|
-
[makeStackedMark(100, 80, '#0e7490', 33)],
|
|
162
|
-
chartArea,
|
|
163
|
-
'all',
|
|
164
|
-
undefined,
|
|
165
|
-
undefined,
|
|
166
|
-
undefined,
|
|
167
|
-
'#ff00ff',
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
expect(labels[0].style.fill).toBe('#ff00ff');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('leaves unstacked columns labeling above the bar', () => {
|
|
174
|
-
const mark = makeMark(0, 20);
|
|
175
|
-
const labels = computeColumnLabels([mark], chartArea, 'all');
|
|
176
|
-
|
|
177
|
-
expect(labels[0].y).toBeLessThan(mark.y);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Column chart (vertical bars) mark computation.
|
|
3
|
-
*
|
|
4
|
-
* Takes a normalized chart spec with resolved scales and produces
|
|
5
|
-
* RectMark[] for rendering vertical columns. When a color encoding
|
|
6
|
-
* is present, columns are either stacked (cumulative heights) or grouped
|
|
7
|
-
* (side-by-side) based on the `stack` property of the quantitative channel.
|
|
8
|
-
*
|
|
9
|
-
* Shares conceptual logic with bar chart but axes are swapped:
|
|
10
|
-
* x-axis is categorical (band scale), y-axis is quantitative.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type {
|
|
14
|
-
ConditionalValueDef,
|
|
15
|
-
DataRow,
|
|
16
|
-
Encoding,
|
|
17
|
-
GradientDef,
|
|
18
|
-
LayoutStrategy,
|
|
19
|
-
MarkAria,
|
|
20
|
-
Rect,
|
|
21
|
-
RectMark,
|
|
22
|
-
} from '@opendata-ai/openchart-core';
|
|
23
|
-
import { isGradientDef } from '@opendata-ai/openchart-core';
|
|
24
|
-
import type { ScaleBand, ScaleLinear } from 'd3-scale';
|
|
25
|
-
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
26
|
-
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
27
|
-
import type { ResolvedScales } from '../../layout/scales';
|
|
28
|
-
import { isConditionalValueDef, resolveConditionalValue } from '../../transforms/conditional';
|
|
29
|
-
import { formatLabelValue } from '../_shared/format-label-value';
|
|
30
|
-
import { getColor, getSequentialColor, groupByField } from '../utils';
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Constants
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const MIN_COLUMN_HEIGHT = 1;
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Public API
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Compute vertical column marks from a normalized chart spec.
|
|
44
|
-
*
|
|
45
|
-
* X axis uses a band scale for categories. Y axis uses a linear scale
|
|
46
|
-
* for values. When a color encoding is present, columns within each
|
|
47
|
-
* category are stacked (cumulative heights).
|
|
48
|
-
*/
|
|
49
|
-
export function computeColumnMarks(
|
|
50
|
-
spec: NormalizedChartSpec,
|
|
51
|
-
scales: ResolvedScales,
|
|
52
|
-
_chartArea: Rect,
|
|
53
|
-
_strategy: LayoutStrategy,
|
|
54
|
-
): RectMark[] {
|
|
55
|
-
const encoding = spec.encoding as Encoding;
|
|
56
|
-
const xChannel = encoding.x;
|
|
57
|
-
const yChannel = encoding.y;
|
|
58
|
-
|
|
59
|
-
if (!xChannel || !yChannel || !scales.x || !scales.y) {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const xScale = scales.x.scale as ScaleBand<string>;
|
|
64
|
-
const yScale = scales.y.scale as ScaleLinear<number, number>;
|
|
65
|
-
|
|
66
|
-
// Band scale should provide bandwidth
|
|
67
|
-
if (typeof xScale.bandwidth !== 'function') {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const bandwidth = xScale.bandwidth();
|
|
72
|
-
// Baseline = pixel y where the column's bottom edge anchors. When the
|
|
73
|
-
// y-domain includes zero (the common case), this is yScale(0). Sparkline
|
|
74
|
-
// mode tightens the domain to [min, max] (zero: false) so yScale(0) lands
|
|
75
|
-
// outside the chart area; in that case we anchor to the bottom of the
|
|
76
|
-
// y-range instead, otherwise every bar would render with the same height.
|
|
77
|
-
const yDomain = yScale.domain() as [number, number];
|
|
78
|
-
const yIncludesZero = yDomain[0] <= 0 && yDomain[1] >= 0;
|
|
79
|
-
const baseline = yIncludesZero ? yScale(0) : yScale(yDomain[0]);
|
|
80
|
-
const colorEnc = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
|
|
81
|
-
const conditionalColor =
|
|
82
|
-
encoding.color && isConditionalValueDef(encoding.color)
|
|
83
|
-
? (encoding.color as ConditionalValueDef)
|
|
84
|
-
: undefined;
|
|
85
|
-
const colorField = colorEnc?.field;
|
|
86
|
-
|
|
87
|
-
const isSequentialColor = colorEnc?.type === 'quantitative';
|
|
88
|
-
|
|
89
|
-
let marks: RectMark[];
|
|
90
|
-
|
|
91
|
-
// Color encoding present: decide between colored simple columns vs stacked
|
|
92
|
-
if (colorField && !isSequentialColor) {
|
|
93
|
-
// Check if any category has multiple rows (actual stacking needed)
|
|
94
|
-
const categoryGroups = groupByField(spec.data, xChannel.field);
|
|
95
|
-
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
96
|
-
|
|
97
|
-
if (needsStacking) {
|
|
98
|
-
// stack: undefined/true/'zero'/'normalize'/'center' -> stacked (default); null/false -> grouped
|
|
99
|
-
const stackEnabled = yChannel.stack !== null && yChannel.stack !== false;
|
|
100
|
-
|
|
101
|
-
if (!stackEnabled) {
|
|
102
|
-
marks = computeGroupedColumns(
|
|
103
|
-
spec.data,
|
|
104
|
-
xChannel.field,
|
|
105
|
-
yChannel.field,
|
|
106
|
-
colorField,
|
|
107
|
-
xScale,
|
|
108
|
-
yScale,
|
|
109
|
-
bandwidth,
|
|
110
|
-
baseline,
|
|
111
|
-
scales,
|
|
112
|
-
);
|
|
113
|
-
} else {
|
|
114
|
-
const stackMode =
|
|
115
|
-
yChannel.stack === 'normalize'
|
|
116
|
-
? 'normalize'
|
|
117
|
-
: yChannel.stack === 'center'
|
|
118
|
-
? 'center'
|
|
119
|
-
: 'zero';
|
|
120
|
-
|
|
121
|
-
marks = computeStackedColumns(
|
|
122
|
-
spec.data,
|
|
123
|
-
xChannel.field,
|
|
124
|
-
yChannel.field,
|
|
125
|
-
colorField,
|
|
126
|
-
xScale,
|
|
127
|
-
yScale,
|
|
128
|
-
bandwidth,
|
|
129
|
-
baseline,
|
|
130
|
-
scales,
|
|
131
|
-
stackMode,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
// Single row per category: render like simple columns but with color from scale
|
|
136
|
-
marks = computeColoredColumns(
|
|
137
|
-
spec.data,
|
|
138
|
-
xChannel.field,
|
|
139
|
-
yChannel.field,
|
|
140
|
-
colorField,
|
|
141
|
-
xScale,
|
|
142
|
-
yScale,
|
|
143
|
-
bandwidth,
|
|
144
|
-
baseline,
|
|
145
|
-
scales,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
marks = computeSimpleColumns(
|
|
150
|
-
spec.data,
|
|
151
|
-
xChannel.field,
|
|
152
|
-
yChannel.field,
|
|
153
|
-
xScale,
|
|
154
|
-
yScale,
|
|
155
|
-
bandwidth,
|
|
156
|
-
baseline,
|
|
157
|
-
scales,
|
|
158
|
-
isSequentialColor,
|
|
159
|
-
conditionalColor,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Stamp identity keys for data-update transitions
|
|
164
|
-
const categoryField = xChannel.field;
|
|
165
|
-
stampColumnKeys(marks, categoryField, colorField);
|
|
166
|
-
|
|
167
|
-
return applyMarkDefOverrides(marks, spec, bandwidth);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Stamp stable identity keys on column marks for data-update transitions. */
|
|
171
|
-
function stampColumnKeys(marks: RectMark[], categoryField: string, colorField?: string): void {
|
|
172
|
-
const rawKeys = marks.map((m) => {
|
|
173
|
-
const series = colorField ? String(m.data[colorField] ?? '') : '';
|
|
174
|
-
const cat = serializeKeyValue(m.data[categoryField]);
|
|
175
|
-
return `${series}|${cat}`;
|
|
176
|
-
});
|
|
177
|
-
const keys = dedupeKeys(rawKeys);
|
|
178
|
-
for (let i = 0; i < marks.length; i++) {
|
|
179
|
-
marks[i].key = keys[i];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Compute simple (non-grouped) vertical columns. */
|
|
184
|
-
function computeSimpleColumns(
|
|
185
|
-
data: DataRow[],
|
|
186
|
-
categoryField: string,
|
|
187
|
-
valueField: string,
|
|
188
|
-
xScale: ScaleBand<string>,
|
|
189
|
-
yScale: ScaleLinear<number, number>,
|
|
190
|
-
bandwidth: number,
|
|
191
|
-
baseline: number,
|
|
192
|
-
scales: ResolvedScales,
|
|
193
|
-
sequentialColor = false,
|
|
194
|
-
conditionalColor?: ConditionalValueDef,
|
|
195
|
-
): RectMark[] {
|
|
196
|
-
const marks: RectMark[] = [];
|
|
197
|
-
|
|
198
|
-
for (const row of data) {
|
|
199
|
-
const category = String(row[categoryField] ?? '');
|
|
200
|
-
const value = Number(row[valueField] ?? 0);
|
|
201
|
-
if (!Number.isFinite(value)) continue;
|
|
202
|
-
|
|
203
|
-
const bandX = xScale(category);
|
|
204
|
-
if (bandX === undefined) continue;
|
|
205
|
-
|
|
206
|
-
let color: string | GradientDef;
|
|
207
|
-
if (conditionalColor) {
|
|
208
|
-
const resolved = resolveConditionalValue(row, conditionalColor);
|
|
209
|
-
if (resolved != null) {
|
|
210
|
-
color = isGradientDef(resolved) ? resolved : String(resolved);
|
|
211
|
-
} else {
|
|
212
|
-
color = getColor(scales, '__default__');
|
|
213
|
-
}
|
|
214
|
-
} else if (sequentialColor) {
|
|
215
|
-
color = getSequentialColor(scales, value);
|
|
216
|
-
} else {
|
|
217
|
-
color = getColor(scales, '__default__');
|
|
218
|
-
}
|
|
219
|
-
const yPos = yScale(value);
|
|
220
|
-
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
221
|
-
|
|
222
|
-
// For positive values, column goes upward from baseline.
|
|
223
|
-
// For negative values, column goes downward from baseline.
|
|
224
|
-
const y = value >= 0 ? yPos : baseline;
|
|
225
|
-
|
|
226
|
-
const aria: MarkAria = {
|
|
227
|
-
label: `${category}: ${formatLabelValue(value)}`,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
marks.push({
|
|
231
|
-
type: 'rect',
|
|
232
|
-
x: bandX,
|
|
233
|
-
y,
|
|
234
|
-
width: bandwidth,
|
|
235
|
-
height: columnHeight,
|
|
236
|
-
fill: color,
|
|
237
|
-
cornerRadius: 2,
|
|
238
|
-
data: row as Record<string, unknown>,
|
|
239
|
-
aria,
|
|
240
|
-
orient: 'vertical',
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return marks;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Compute colored (non-stacked) vertical columns. Used when color encoding
|
|
248
|
-
* is present but each category has only one row (e.g., diverging charts). */
|
|
249
|
-
function computeColoredColumns(
|
|
250
|
-
data: DataRow[],
|
|
251
|
-
categoryField: string,
|
|
252
|
-
valueField: string,
|
|
253
|
-
colorField: string,
|
|
254
|
-
xScale: ScaleBand<string>,
|
|
255
|
-
yScale: ScaleLinear<number, number>,
|
|
256
|
-
bandwidth: number,
|
|
257
|
-
baseline: number,
|
|
258
|
-
scales: ResolvedScales,
|
|
259
|
-
): RectMark[] {
|
|
260
|
-
const marks: RectMark[] = [];
|
|
261
|
-
|
|
262
|
-
for (const row of data) {
|
|
263
|
-
const category = String(row[categoryField] ?? '');
|
|
264
|
-
const value = Number(row[valueField] ?? 0);
|
|
265
|
-
if (!Number.isFinite(value)) continue;
|
|
266
|
-
|
|
267
|
-
const bandX = xScale(category);
|
|
268
|
-
if (bandX === undefined) continue;
|
|
269
|
-
|
|
270
|
-
const groupKey = String(row[colorField] ?? '');
|
|
271
|
-
const color = getColor(scales, groupKey);
|
|
272
|
-
const yPos = yScale(value);
|
|
273
|
-
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
274
|
-
|
|
275
|
-
const y = value >= 0 ? yPos : baseline;
|
|
276
|
-
|
|
277
|
-
const aria: MarkAria = {
|
|
278
|
-
label: `${category}, ${groupKey}: ${formatLabelValue(value)}`,
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
marks.push({
|
|
282
|
-
type: 'rect',
|
|
283
|
-
x: bandX,
|
|
284
|
-
y,
|
|
285
|
-
width: bandwidth,
|
|
286
|
-
height: columnHeight,
|
|
287
|
-
fill: color,
|
|
288
|
-
cornerRadius: 2,
|
|
289
|
-
data: row as Record<string, unknown>,
|
|
290
|
-
aria,
|
|
291
|
-
orient: 'vertical',
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return marks;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Compute grouped (dodged) vertical columns -- side-by-side within each category band. */
|
|
299
|
-
function computeGroupedColumns(
|
|
300
|
-
data: DataRow[],
|
|
301
|
-
categoryField: string,
|
|
302
|
-
valueField: string,
|
|
303
|
-
colorField: string,
|
|
304
|
-
xScale: ScaleBand<string>,
|
|
305
|
-
yScale: ScaleLinear<number, number>,
|
|
306
|
-
bandwidth: number,
|
|
307
|
-
baseline: number,
|
|
308
|
-
scales: ResolvedScales,
|
|
309
|
-
): RectMark[] {
|
|
310
|
-
const marks: RectMark[] = [];
|
|
311
|
-
const categoryGroups = groupByField(data, categoryField);
|
|
312
|
-
|
|
313
|
-
// Build a stable group order from first appearance in data (Map for O(1) lookup)
|
|
314
|
-
const groupIndexMap = new Map<string, number>();
|
|
315
|
-
for (const row of data) {
|
|
316
|
-
const key = String(row[colorField] ?? '');
|
|
317
|
-
if (!groupIndexMap.has(key)) {
|
|
318
|
-
groupIndexMap.set(key, groupIndexMap.size);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
const groupCount = groupIndexMap.size;
|
|
322
|
-
if (groupCount === 0) return marks;
|
|
323
|
-
|
|
324
|
-
// Subdivide the band width by group count with a small gap
|
|
325
|
-
const gap = Math.min(1, bandwidth * 0.05);
|
|
326
|
-
const subBandWidth = Math.max(
|
|
327
|
-
(bandwidth - gap * (groupCount - 1)) / groupCount,
|
|
328
|
-
MIN_COLUMN_HEIGHT,
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
for (const [category, rows] of categoryGroups) {
|
|
332
|
-
const bandX = xScale(category);
|
|
333
|
-
if (bandX === undefined) continue;
|
|
334
|
-
|
|
335
|
-
for (const row of rows) {
|
|
336
|
-
const groupKey = String(row[colorField] ?? '');
|
|
337
|
-
const value = Number(row[valueField] ?? 0);
|
|
338
|
-
if (!Number.isFinite(value)) continue;
|
|
339
|
-
|
|
340
|
-
const groupIndex = groupIndexMap.get(groupKey) ?? 0;
|
|
341
|
-
const color = getColor(scales, groupKey);
|
|
342
|
-
const yPos = yScale(value);
|
|
343
|
-
const columnHeight = Math.max(Math.abs(baseline - yPos), MIN_COLUMN_HEIGHT);
|
|
344
|
-
const y = value >= 0 ? yPos : baseline;
|
|
345
|
-
const subX = bandX + groupIndex * (subBandWidth + gap);
|
|
346
|
-
|
|
347
|
-
const aria: MarkAria = {
|
|
348
|
-
label: `${category}, ${groupKey}: ${formatLabelValue(value)}`,
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
marks.push({
|
|
352
|
-
type: 'rect',
|
|
353
|
-
x: subX,
|
|
354
|
-
y,
|
|
355
|
-
width: subBandWidth,
|
|
356
|
-
height: columnHeight,
|
|
357
|
-
fill: color,
|
|
358
|
-
cornerRadius: 2,
|
|
359
|
-
data: row as Record<string, unknown>,
|
|
360
|
-
aria,
|
|
361
|
-
orient: 'vertical',
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return marks;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Compute stacked vertical columns with support for zero/normalize/center modes. */
|
|
370
|
-
function computeStackedColumns(
|
|
371
|
-
data: DataRow[],
|
|
372
|
-
categoryField: string,
|
|
373
|
-
valueField: string,
|
|
374
|
-
colorField: string,
|
|
375
|
-
xScale: ScaleBand<string>,
|
|
376
|
-
yScale: ScaleLinear<number, number>,
|
|
377
|
-
bandwidth: number,
|
|
378
|
-
_baseline: number,
|
|
379
|
-
scales: ResolvedScales,
|
|
380
|
-
stackMode: 'zero' | 'normalize' | 'center' = 'zero',
|
|
381
|
-
): RectMark[] {
|
|
382
|
-
const marks: RectMark[] = [];
|
|
383
|
-
const categoryGroups = groupByField(data, categoryField);
|
|
384
|
-
|
|
385
|
-
for (const [category, rows] of categoryGroups) {
|
|
386
|
-
const bandX = xScale(category);
|
|
387
|
-
if (bandX === undefined) continue;
|
|
388
|
-
|
|
389
|
-
// Compute category total for normalize/center modes
|
|
390
|
-
let categoryTotal = 0;
|
|
391
|
-
for (const row of rows) {
|
|
392
|
-
const v = Number(row[valueField] ?? 0);
|
|
393
|
-
if (Number.isFinite(v) && v > 0) categoryTotal += v;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// For center mode, offset so the stack is centered around zero
|
|
397
|
-
let cumulativeValue = stackMode === 'center' ? -categoryTotal / 2 : 0;
|
|
398
|
-
|
|
399
|
-
for (const row of rows) {
|
|
400
|
-
const groupKey = String(row[colorField] ?? '');
|
|
401
|
-
const rawValue = Number(row[valueField] ?? 0);
|
|
402
|
-
// Stacking only applies to positive values; negative/zero rows are skipped
|
|
403
|
-
// since cumulative stacking doesn't make visual sense for mixed signs.
|
|
404
|
-
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
405
|
-
|
|
406
|
-
// For normalize mode, scale the value to a fraction of the total
|
|
407
|
-
const value =
|
|
408
|
-
stackMode === 'normalize' && categoryTotal > 0 ? rawValue / categoryTotal : rawValue;
|
|
409
|
-
|
|
410
|
-
const color = getColor(scales, groupKey);
|
|
411
|
-
|
|
412
|
-
const yTop = yScale(cumulativeValue + value);
|
|
413
|
-
const yBottom = yScale(cumulativeValue);
|
|
414
|
-
const columnHeight = Math.max(Math.abs(yBottom - yTop), MIN_COLUMN_HEIGHT);
|
|
415
|
-
|
|
416
|
-
const aria: MarkAria = {
|
|
417
|
-
label: `${category}, ${groupKey}: ${formatLabelValue(rawValue)}`,
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
marks.push({
|
|
421
|
-
type: 'rect',
|
|
422
|
-
x: bandX,
|
|
423
|
-
y: Math.min(yTop, yBottom),
|
|
424
|
-
width: bandwidth,
|
|
425
|
-
height: columnHeight,
|
|
426
|
-
fill: color,
|
|
427
|
-
cornerRadius: 0,
|
|
428
|
-
data: row as Record<string, unknown>,
|
|
429
|
-
aria,
|
|
430
|
-
orient: 'vertical',
|
|
431
|
-
stackGroup: category,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
cumulativeValue += value;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return marks;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function applyMarkDefOverrides(
|
|
442
|
-
marks: RectMark[],
|
|
443
|
-
spec: NormalizedChartSpec,
|
|
444
|
-
bandwidth: number,
|
|
445
|
-
): RectMark[] {
|
|
446
|
-
const { markDef } = spec;
|
|
447
|
-
const fixedSize = markDef.size;
|
|
448
|
-
const crSpec = markDef.cornerRadius;
|
|
449
|
-
|
|
450
|
-
if (fixedSize == null && crSpec == null) return marks;
|
|
451
|
-
|
|
452
|
-
// Identify the topmost segment per stackGroup (smallest `y` since SVG
|
|
453
|
-
// grows downward). Only that segment receives the corner rounding so
|
|
454
|
-
// the seams between stacked segments stay square and flush.
|
|
455
|
-
const topPerStack = new Map<string, RectMark>();
|
|
456
|
-
for (const mark of marks) {
|
|
457
|
-
if (mark.stackGroup === undefined) continue;
|
|
458
|
-
const current = topPerStack.get(mark.stackGroup);
|
|
459
|
-
if (!current || mark.y < current.y) {
|
|
460
|
-
topPerStack.set(mark.stackGroup, mark);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
for (const mark of marks) {
|
|
465
|
-
if (fixedSize != null && mark.stackGroup === undefined) {
|
|
466
|
-
const barWidth = Math.min(fixedSize, bandwidth);
|
|
467
|
-
const offset = (bandwidth - barWidth) / 2;
|
|
468
|
-
mark.x = mark.x + offset;
|
|
469
|
-
mark.width = barWidth;
|
|
470
|
-
}
|
|
471
|
-
const effectiveWidth = mark.width;
|
|
472
|
-
const isStacked = mark.stackGroup !== undefined;
|
|
473
|
-
const isStackTop = isStacked && topPerStack.get(mark.stackGroup!) === mark;
|
|
474
|
-
|
|
475
|
-
// Stacked segments below the top stay square. Stack top rounds only its
|
|
476
|
-
// top corners; non-stacked bars round all four.
|
|
477
|
-
if (isStacked && !isStackTop) continue;
|
|
478
|
-
|
|
479
|
-
if (crSpec === 'pill') {
|
|
480
|
-
mark.cornerRadius = effectiveWidth / 2;
|
|
481
|
-
} else if (typeof crSpec === 'number') {
|
|
482
|
-
mark.cornerRadius = crSpec;
|
|
483
|
-
} else {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (isStackTop) {
|
|
488
|
-
mark.cornerRadiusSides = { tl: true, tr: true, br: false, bl: false };
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return marks;
|
|
493
|
-
}
|