@opendata-ai/openchart-engine 7.2.4 → 7.4.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 +88 -39
- package/dist/index.js +4134 -2930
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +143 -78
- package/src/__tests__/axes.test.ts +129 -1
- package/src/__tests__/compile-chart.test.ts +6 -0
- package/src/__tests__/compile-layer.test.ts +49 -0
- package/src/__tests__/dimensions.test.ts +70 -2
- package/src/__tests__/helpers/invariants.ts +265 -0
- package/src/__tests__/known-bugs.test.ts +113 -0
- package/src/__tests__/layout-fuzz.test.ts +358 -0
- package/src/__tests__/layout-invariants.test.ts +535 -0
- package/src/__tests__/legend.test.ts +37 -0
- package/src/annotations/__tests__/compute.test.ts +8 -15
- package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
- package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
- package/src/annotations/__tests__/placement.test.ts +316 -0
- package/src/annotations/collisions.ts +19 -7
- package/src/annotations/compute.ts +257 -16
- package/src/annotations/geometry.ts +73 -15
- package/src/annotations/placement.ts +588 -0
- package/src/annotations/resolve-text.ts +58 -10
- package/src/charts/__tests__/post-process.test.ts +3 -3
- package/src/charts/bar/__tests__/compute.test.ts +131 -45
- package/src/charts/bar/compute.ts +44 -4
- package/src/charts/bar/index.ts +9 -2
- package/src/charts/bar/labels.ts +77 -26
- package/src/charts/column/labels.ts +29 -16
- package/src/charts/post-process.ts +21 -7
- package/src/charts/registry.ts +4 -2
- package/src/compile/layer.ts +31 -9
- package/src/compile.ts +88 -149
- package/src/index.ts +9 -0
- package/src/layout/axes/rotation.ts +36 -0
- package/src/layout/axes.ts +130 -8
- package/src/layout/dimensions.ts +80 -98
- package/src/layout/plan.ts +550 -0
- package/src/layout/scales.ts +22 -0
- package/src/layout/shared.ts +44 -0
- package/src/legend/compute.ts +315 -78
- package/src/legend/wrap.ts +11 -7
- package/src/sankey/__tests__/node-label-wrap.test.ts +2 -2
- package/src/tables/__tests__/heatmap.test.ts +24 -20
- package/src/tables/heatmap.ts +25 -9
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* optional callout connector to the data point.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type TextStyle,
|
|
6
|
+
import type {
|
|
7
|
+
Rect,
|
|
8
|
+
ResolvedAnnotation,
|
|
9
|
+
ResolvedLabel,
|
|
10
|
+
TextAnnotation,
|
|
11
|
+
TextStyle,
|
|
13
12
|
} from '@opendata-ai/openchart-core';
|
|
14
13
|
import type { ResolvedScales } from '../layout/scales';
|
|
15
14
|
import {
|
|
@@ -27,7 +26,15 @@ import {
|
|
|
27
26
|
SUBTITLE_FONT_SIZE_RATIO,
|
|
28
27
|
SUBTITLE_GAP,
|
|
29
28
|
} from './constants';
|
|
30
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type AnnotationMeasureTextFn,
|
|
31
|
+
applyOffset,
|
|
32
|
+
computeAnchorOffset,
|
|
33
|
+
computeConnectorOrigin,
|
|
34
|
+
computeTextBlockBounds,
|
|
35
|
+
heuristicMeasure,
|
|
36
|
+
unionRects,
|
|
37
|
+
} from './geometry';
|
|
31
38
|
import { resolvePosition } from './position';
|
|
32
39
|
|
|
33
40
|
/** Horizontal gap between the drop-line and the label text. */
|
|
@@ -59,6 +66,7 @@ export function resolveTextAnnotation(
|
|
|
59
66
|
scales: ResolvedScales,
|
|
60
67
|
chartArea: Rect,
|
|
61
68
|
isDark: boolean,
|
|
69
|
+
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
62
70
|
): ResolvedAnnotation | null {
|
|
63
71
|
const px = resolvePosition(annotation.x, scales.x);
|
|
64
72
|
const py = resolvePosition(annotation.y, scales.y);
|
|
@@ -78,7 +86,23 @@ export function resolveTextAnnotation(
|
|
|
78
86
|
// label sitting flush beside it. Auto-flips to the opposite side if the
|
|
79
87
|
// chosen side would overflow the chart area.
|
|
80
88
|
if (annotation.connector === 'drop-line') {
|
|
81
|
-
return resolveDropLineAnnotation(
|
|
89
|
+
return resolveDropLineAnnotation(
|
|
90
|
+
annotation,
|
|
91
|
+
px,
|
|
92
|
+
py,
|
|
93
|
+
chartArea,
|
|
94
|
+
labelStyle,
|
|
95
|
+
defaultTextFill,
|
|
96
|
+
measure,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Multi-line non-drop-line: engine sets textAnchor to 'middle' so bounds
|
|
101
|
+
// and rendering derive from the same anchor (previously the renderer
|
|
102
|
+
// forced this override, causing bounds to lie).
|
|
103
|
+
const isMultiLine = annotation.text.includes('\n');
|
|
104
|
+
if (isMultiLine) {
|
|
105
|
+
labelStyle.textAnchor = 'middle';
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
// Compute position from anchor direction + user offset
|
|
@@ -130,6 +154,8 @@ export function resolveTextAnnotation(
|
|
|
130
154
|
? { x: adjustedToRaw.x - (cdx / dist) * GAP, y: adjustedToRaw.y - (cdy / dist) * GAP }
|
|
131
155
|
: adjustedToRaw;
|
|
132
156
|
|
|
157
|
+
const labelBounds = computeTextBlockBounds(labelX, labelY, annotation.text, labelStyle, measure);
|
|
158
|
+
|
|
133
159
|
const label: ResolvedLabel = {
|
|
134
160
|
text: annotation.text,
|
|
135
161
|
x: labelX,
|
|
@@ -147,6 +173,7 @@ export function resolveTextAnnotation(
|
|
|
147
173
|
: undefined,
|
|
148
174
|
background: annotation.background,
|
|
149
175
|
halo: annotation.halo,
|
|
176
|
+
bounds: labelBounds,
|
|
150
177
|
};
|
|
151
178
|
|
|
152
179
|
// Resolve dot marker. Uses the connector's "to" endpoint coordinates
|
|
@@ -170,6 +197,7 @@ export function resolveTextAnnotation(
|
|
|
170
197
|
// Resolve subtitle. Positioned below the primary text block by
|
|
171
198
|
// (lineHeight * primaryLineCount * fontSize) + gap.
|
|
172
199
|
let subtitle: ResolvedAnnotation['subtitle'] | undefined;
|
|
200
|
+
let annotationBounds: Rect = labelBounds;
|
|
173
201
|
if (annotation.subtitle) {
|
|
174
202
|
const primaryLineCount = annotation.text.split('\n').length;
|
|
175
203
|
const subtitleFontSize = Math.round(fontSize * SUBTITLE_FONT_SIZE_RATIO);
|
|
@@ -187,6 +215,14 @@ export function resolveTextAnnotation(
|
|
|
187
215
|
y: subtitleY,
|
|
188
216
|
style: subtitleStyle,
|
|
189
217
|
};
|
|
218
|
+
const subtitleBounds = computeTextBlockBounds(
|
|
219
|
+
labelX,
|
|
220
|
+
subtitleY,
|
|
221
|
+
annotation.subtitle,
|
|
222
|
+
subtitleStyle,
|
|
223
|
+
measure,
|
|
224
|
+
);
|
|
225
|
+
annotationBounds = unionRects(labelBounds, subtitleBounds);
|
|
190
226
|
}
|
|
191
227
|
|
|
192
228
|
return {
|
|
@@ -199,6 +235,7 @@ export function resolveTextAnnotation(
|
|
|
199
235
|
zIndex: annotation.zIndex,
|
|
200
236
|
dot,
|
|
201
237
|
subtitle,
|
|
238
|
+
bounds: annotationBounds,
|
|
202
239
|
};
|
|
203
240
|
}
|
|
204
241
|
|
|
@@ -214,13 +251,14 @@ function resolveDropLineAnnotation(
|
|
|
214
251
|
chartArea: Rect,
|
|
215
252
|
labelStyle: TextStyle,
|
|
216
253
|
defaultTextFill: string,
|
|
254
|
+
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
217
255
|
): ResolvedAnnotation {
|
|
218
256
|
const fontSize = annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
219
257
|
const fontWeight = annotation.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
220
258
|
const lines = annotation.text.split('\n');
|
|
221
259
|
const estimatedWidth = Math.max(
|
|
222
260
|
0,
|
|
223
|
-
...lines.map((line) =>
|
|
261
|
+
...lines.map((line) => measure(line, { fontSize, fontWeight })),
|
|
224
262
|
);
|
|
225
263
|
|
|
226
264
|
// Pick initial side from anchor; default to 'left' (label sits to the left
|
|
@@ -261,6 +299,14 @@ function resolveDropLineAnnotation(
|
|
|
261
299
|
const from = { x: px, y: labelTopY - DROP_LINE_TOP_GAP };
|
|
262
300
|
const to = { x: px, y: py - DROP_LINE_BOTTOM_GAP };
|
|
263
301
|
|
|
302
|
+
const labelBounds = computeTextBlockBounds(
|
|
303
|
+
labelX,
|
|
304
|
+
labelBaselineY,
|
|
305
|
+
annotation.text,
|
|
306
|
+
resolvedStyle,
|
|
307
|
+
measure,
|
|
308
|
+
);
|
|
309
|
+
|
|
264
310
|
const label: ResolvedLabel = {
|
|
265
311
|
text: annotation.text,
|
|
266
312
|
x: labelX,
|
|
@@ -276,6 +322,7 @@ function resolveDropLineAnnotation(
|
|
|
276
322
|
},
|
|
277
323
|
background: annotation.background,
|
|
278
324
|
halo: annotation.halo,
|
|
325
|
+
bounds: labelBounds,
|
|
279
326
|
};
|
|
280
327
|
|
|
281
328
|
return {
|
|
@@ -286,5 +333,6 @@ function resolveDropLineAnnotation(
|
|
|
286
333
|
fill: annotation.fill,
|
|
287
334
|
opacity: annotation.opacity,
|
|
288
335
|
zIndex: annotation.zIndex,
|
|
336
|
+
bounds: labelBounds,
|
|
289
337
|
};
|
|
290
338
|
}
|
|
@@ -16,8 +16,8 @@ describe('computeMarkObstacles', () => {
|
|
|
16
16
|
const scales = { y: { type: 'linear' } } as unknown as ResolvedScales;
|
|
17
17
|
const result = computeMarkObstacles(marks, scales);
|
|
18
18
|
expect(result).toHaveLength(2);
|
|
19
|
-
expect(result[0]).toEqual({ x: 10, y: 20, width: 50, height: 30 });
|
|
20
|
-
expect(result[1]).toEqual({ x: 80, y: 20, width: 50, height: 30 });
|
|
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
21
|
});
|
|
22
22
|
|
|
23
23
|
it('returns point mark bounds as bounding box from cx/cy/r', () => {
|
|
@@ -25,7 +25,7 @@ describe('computeMarkObstacles', () => {
|
|
|
25
25
|
const scales = { y: { type: 'linear' } } as unknown as ResolvedScales;
|
|
26
26
|
const result = computeMarkObstacles(marks, scales);
|
|
27
27
|
expect(result).toHaveLength(1);
|
|
28
|
-
expect(result[0]).toEqual({ x: 90, y: 90, width: 20, height: 20 });
|
|
28
|
+
expect(result[0]).toEqual({ x: 90, y: 90, width: 20, height: 20, kind: 'mark' });
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it('returns grouped row obstacles for band-scale charts', () => {
|
|
@@ -11,6 +11,11 @@ import { computeBarLabels } from '../labels';
|
|
|
11
11
|
|
|
12
12
|
const chartArea: Rect = { x: 80, y: 20, width: 500, height: 300 };
|
|
13
13
|
|
|
14
|
+
// Default container width for tests that don't exercise the narrow-container
|
|
15
|
+
// grouped-bar reclaim gate. Wide enough (>= NARROW_VIEWPORT_MAX) that the gate
|
|
16
|
+
// stays off, matching the prior default geometry.
|
|
17
|
+
const CONTAINER_WIDTH = 800;
|
|
18
|
+
|
|
14
19
|
const fullStrategy: LayoutStrategy = {
|
|
15
20
|
labelMode: 'all',
|
|
16
21
|
legendPosition: 'right',
|
|
@@ -97,7 +102,7 @@ describe('computeBarMarks', () => {
|
|
|
97
102
|
it('produces one RectMark per data row', () => {
|
|
98
103
|
const spec = makeSimpleBarSpec();
|
|
99
104
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
100
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
105
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
101
106
|
|
|
102
107
|
expect(marks).toHaveLength(3);
|
|
103
108
|
expect(marks.every((m) => m.type === 'rect')).toBe(true);
|
|
@@ -106,7 +111,7 @@ describe('computeBarMarks', () => {
|
|
|
106
111
|
it('bars have positive width and height', () => {
|
|
107
112
|
const spec = makeSimpleBarSpec();
|
|
108
113
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
109
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
114
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
110
115
|
|
|
111
116
|
for (const mark of marks) {
|
|
112
117
|
expect(mark.width).toBeGreaterThan(0);
|
|
@@ -117,7 +122,7 @@ describe('computeBarMarks', () => {
|
|
|
117
122
|
it('wider bars correspond to larger values', () => {
|
|
118
123
|
const spec = makeSimpleBarSpec();
|
|
119
124
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
120
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
125
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
121
126
|
|
|
122
127
|
// Cherry (70) should be wider than Banana (30)
|
|
123
128
|
const cherry = marks.find((m) => m.aria.label.includes('Cherry'))!;
|
|
@@ -128,7 +133,7 @@ describe('computeBarMarks', () => {
|
|
|
128
133
|
it('bars have corner radius applied', () => {
|
|
129
134
|
const spec = makeSimpleBarSpec();
|
|
130
135
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
131
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
136
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
132
137
|
|
|
133
138
|
expect(marks[0].cornerRadius).toBe(2);
|
|
134
139
|
});
|
|
@@ -136,7 +141,7 @@ describe('computeBarMarks', () => {
|
|
|
136
141
|
it('each bar has an aria label with category and value', () => {
|
|
137
142
|
const spec = makeSimpleBarSpec();
|
|
138
143
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
139
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
144
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
140
145
|
|
|
141
146
|
expect(marks[0].aria.label).toContain('Apple');
|
|
142
147
|
expect(marks[0].aria.label).toContain('50');
|
|
@@ -153,7 +158,7 @@ describe('computeBarMarks', () => {
|
|
|
153
158
|
it('produces marks for all data rows', () => {
|
|
154
159
|
const spec = makeStackedBarSpec();
|
|
155
160
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
156
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
161
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
157
162
|
|
|
158
163
|
// 3 categories * 2 groups = 6
|
|
159
164
|
expect(marks).toHaveLength(6);
|
|
@@ -162,7 +167,7 @@ describe('computeBarMarks', () => {
|
|
|
162
167
|
it('segments within a category have different colors', () => {
|
|
163
168
|
const spec = makeStackedBarSpec();
|
|
164
169
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
165
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
170
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
166
171
|
|
|
167
172
|
// Find Q1 bars
|
|
168
173
|
const q1Marks = marks.filter((m) => m.aria.label.includes('Q1'));
|
|
@@ -173,7 +178,7 @@ describe('computeBarMarks', () => {
|
|
|
173
178
|
it('stacked segments share the same y position within a category', () => {
|
|
174
179
|
const spec = makeStackedBarSpec();
|
|
175
180
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
176
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
181
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
177
182
|
|
|
178
183
|
const q1East = marks.find(
|
|
179
184
|
(m) => m.aria.label.includes('Q1') && m.aria.label.includes('East'),
|
|
@@ -189,7 +194,7 @@ describe('computeBarMarks', () => {
|
|
|
189
194
|
it('stacked segments are placed end-to-end horizontally', () => {
|
|
190
195
|
const spec = makeStackedBarSpec();
|
|
191
196
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
192
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
197
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
193
198
|
|
|
194
199
|
const q1Marks = marks.filter((m) => m.aria.label.includes('Q1'));
|
|
195
200
|
// Second segment should start where first ends
|
|
@@ -201,7 +206,7 @@ describe('computeBarMarks', () => {
|
|
|
201
206
|
it('stacked bars have zero corner radius', () => {
|
|
202
207
|
const spec = makeStackedBarSpec();
|
|
203
208
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
204
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
209
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
205
210
|
|
|
206
211
|
for (const mark of marks) {
|
|
207
212
|
expect(mark.cornerRadius).toBe(0);
|
|
@@ -213,7 +218,7 @@ describe('computeBarMarks', () => {
|
|
|
213
218
|
it('produces marks for all data rows', () => {
|
|
214
219
|
const spec = makeGroupedBarSpec();
|
|
215
220
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
216
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
221
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
217
222
|
|
|
218
223
|
expect(marks).toHaveLength(6);
|
|
219
224
|
});
|
|
@@ -221,7 +226,7 @@ describe('computeBarMarks', () => {
|
|
|
221
226
|
it('grouped bars within a category have different y positions', () => {
|
|
222
227
|
const spec = makeGroupedBarSpec();
|
|
223
228
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
224
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
229
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
225
230
|
|
|
226
231
|
const q1East = marks.find(
|
|
227
232
|
(m) => m.aria.label.includes('Q1') && m.aria.label.includes('East'),
|
|
@@ -236,7 +241,7 @@ describe('computeBarMarks', () => {
|
|
|
236
241
|
it('grouped bars all start from baseline (not cumulative)', () => {
|
|
237
242
|
const spec = makeGroupedBarSpec();
|
|
238
243
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
239
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
244
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
240
245
|
|
|
241
246
|
const q1East = marks.find(
|
|
242
247
|
(m) => m.aria.label.includes('Q1') && m.aria.label.includes('East'),
|
|
@@ -252,7 +257,7 @@ describe('computeBarMarks', () => {
|
|
|
252
257
|
it('grouped bars have cornerRadius 2', () => {
|
|
253
258
|
const spec = makeGroupedBarSpec();
|
|
254
259
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
255
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
260
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
256
261
|
|
|
257
262
|
for (const mark of marks) {
|
|
258
263
|
expect(mark.cornerRadius).toBe(2);
|
|
@@ -262,7 +267,7 @@ describe('computeBarMarks', () => {
|
|
|
262
267
|
it('grouped bars do not set stackGroup', () => {
|
|
263
268
|
const spec = makeGroupedBarSpec();
|
|
264
269
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
265
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
270
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
266
271
|
|
|
267
272
|
for (const mark of marks) {
|
|
268
273
|
expect(mark.stackGroup).toBeUndefined();
|
|
@@ -272,13 +277,25 @@ describe('computeBarMarks', () => {
|
|
|
272
277
|
it('sub-band heights are smaller than full bandwidth (stacked bars use full height)', () => {
|
|
273
278
|
const groupedSpec = makeGroupedBarSpec();
|
|
274
279
|
const groupedScales = computeScales(groupedSpec, chartArea, groupedSpec.data);
|
|
275
|
-
const groupedMarks = computeBarMarks(
|
|
280
|
+
const groupedMarks = computeBarMarks(
|
|
281
|
+
groupedSpec,
|
|
282
|
+
groupedScales,
|
|
283
|
+
chartArea,
|
|
284
|
+
fullStrategy,
|
|
285
|
+
CONTAINER_WIDTH,
|
|
286
|
+
);
|
|
276
287
|
|
|
277
288
|
// With 2 groups, each sub-bar should be less than the full bandwidth
|
|
278
289
|
const stackedSpec = makeGroupedBarSpec();
|
|
279
290
|
(stackedSpec.encoding.x as { stack?: string }).stack = 'zero';
|
|
280
291
|
const stackedScales = computeScales(stackedSpec, chartArea, stackedSpec.data);
|
|
281
|
-
const stackedMarks = computeBarMarks(
|
|
292
|
+
const stackedMarks = computeBarMarks(
|
|
293
|
+
stackedSpec,
|
|
294
|
+
stackedScales,
|
|
295
|
+
chartArea,
|
|
296
|
+
fullStrategy,
|
|
297
|
+
CONTAINER_WIDTH,
|
|
298
|
+
);
|
|
282
299
|
|
|
283
300
|
expect(groupedMarks[0].height).toBeLessThan(stackedMarks[0].height);
|
|
284
301
|
expect(groupedMarks[0].height).toBeGreaterThan(0);
|
|
@@ -294,6 +311,57 @@ describe('computeBarMarks', () => {
|
|
|
294
311
|
// Domain should not extend to the stacked sum (115)
|
|
295
312
|
expect(domain[1]).toBeLessThanOrEqual(80); // some nice rounding above 70
|
|
296
313
|
});
|
|
314
|
+
|
|
315
|
+
// Issue 1: at tight bandwidths on narrow plots, grouped sub-bars fall below
|
|
316
|
+
// the readable floor and read as stripes. The engine reclaims reserved
|
|
317
|
+
// inter-category whitespace to keep bars >= MIN_GROUPED_BAR_THICKNESS (8px).
|
|
318
|
+
it('reclaims band whitespace so narrow-plot sub-bars meet the readable floor', () => {
|
|
319
|
+
// 11 categories x 3 series in a short, phone-width plot -> sub-bars would
|
|
320
|
+
// be ~7px without the reclaim.
|
|
321
|
+
const data = [] as { district: string; source: string; dollars: number }[];
|
|
322
|
+
for (let i = 0; i < 11; i++) {
|
|
323
|
+
for (const source of ['State', 'Local', 'Federal']) {
|
|
324
|
+
data.push({ district: `D${i}`, source, dollars: 100 + i });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const spec: NormalizedChartSpec = {
|
|
328
|
+
markType: 'bar',
|
|
329
|
+
markDef: { type: 'bar' },
|
|
330
|
+
data,
|
|
331
|
+
encoding: {
|
|
332
|
+
x: { field: 'dollars', type: 'quantitative' },
|
|
333
|
+
y: { field: 'district', type: 'nominal' },
|
|
334
|
+
color: { field: 'source', type: 'nominal' },
|
|
335
|
+
},
|
|
336
|
+
chrome: {},
|
|
337
|
+
annotations: [],
|
|
338
|
+
responsive: true,
|
|
339
|
+
theme: {},
|
|
340
|
+
darkMode: 'off',
|
|
341
|
+
labels: { density: 'auto', format: '' },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Narrow container (< NARROW_VIEWPORT_MAX): reclaim active. Same plot
|
|
345
|
+
// geometry both times; only the container width toggles the gate, proving
|
|
346
|
+
// the gate keys off the container, not the derived plot width.
|
|
347
|
+
const plotArea: Rect = { x: 120, y: 20, width: 240, height: 380 };
|
|
348
|
+
const scales = computeScales(spec, plotArea, spec.data);
|
|
349
|
+
const narrowMarks = computeBarMarks(spec, scales, plotArea, fullStrategy, 400);
|
|
350
|
+
for (const mark of narrowMarks) {
|
|
351
|
+
expect(mark.height).toBeGreaterThanOrEqual(8);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Wide container (>= NARROW_VIEWPORT_MAX): reclaim gated off, so the same
|
|
355
|
+
// tight-height plot is left untouched (bars may be thin, matching the
|
|
356
|
+
// pre-fix desktop geometry). Bars must not be *taller* than the narrow
|
|
357
|
+
// case, proving the reclaim only applies for narrow containers. A wide
|
|
358
|
+
// container with a sub-500px plot (e.g. a large left gutter) is exactly
|
|
359
|
+
// the desktop-embed case that must stay pixel-identical.
|
|
360
|
+
const wideMarks = computeBarMarks(spec, scales, plotArea, fullStrategy, 700);
|
|
361
|
+
const wideMax = Math.max(...wideMarks.map((m) => m.height));
|
|
362
|
+
const narrowMin = Math.min(...narrowMarks.map((m) => m.height));
|
|
363
|
+
expect(wideMax).toBeLessThan(narrowMin);
|
|
364
|
+
});
|
|
297
365
|
});
|
|
298
366
|
|
|
299
367
|
describe('colored (non-stacked) bars', () => {
|
|
@@ -319,7 +387,7 @@ describe('computeBarMarks', () => {
|
|
|
319
387
|
labels: { density: 'auto', format: '' },
|
|
320
388
|
};
|
|
321
389
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
322
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
390
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
323
391
|
|
|
324
392
|
expect(marks).toHaveLength(3);
|
|
325
393
|
// Each bar should have different colors
|
|
@@ -352,7 +420,7 @@ describe('computeBarMarks', () => {
|
|
|
352
420
|
labels: { density: 'auto', format: '' },
|
|
353
421
|
};
|
|
354
422
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
355
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
423
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
356
424
|
|
|
357
425
|
expect(marks).toHaveLength(2);
|
|
358
426
|
const decline = marks.find((m) => m.aria.label.includes('Decline'))!;
|
|
@@ -367,7 +435,7 @@ describe('computeBarMarks', () => {
|
|
|
367
435
|
it('negative bars extend leftward from baseline', () => {
|
|
368
436
|
const spec = makeNegativeBarSpec();
|
|
369
437
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
370
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
438
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
371
439
|
|
|
372
440
|
const decline = marks.find((m) => m.aria.label.includes('Decline'))!;
|
|
373
441
|
const growth = marks.find((m) => m.aria.label.includes('Growth'))!;
|
|
@@ -379,7 +447,7 @@ describe('computeBarMarks', () => {
|
|
|
379
447
|
it('negative bars still have positive width', () => {
|
|
380
448
|
const spec = makeNegativeBarSpec();
|
|
381
449
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
382
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
450
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
383
451
|
|
|
384
452
|
for (const mark of marks) {
|
|
385
453
|
expect(mark.width).toBeGreaterThan(0);
|
|
@@ -422,7 +490,7 @@ describe('computeBarMarks', () => {
|
|
|
422
490
|
it('groups by default: bars sit at different y positions within each category', () => {
|
|
423
491
|
const spec = makeWageSpec(false);
|
|
424
492
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
425
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
493
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
426
494
|
|
|
427
495
|
// 2 firm sizes × 2 years = 4 bars
|
|
428
496
|
expect(marks).toHaveLength(4);
|
|
@@ -447,7 +515,7 @@ describe('computeBarMarks', () => {
|
|
|
447
515
|
it('stacked with stack:zero: segments are contiguous end-to-end within each category', () => {
|
|
448
516
|
const spec = makeWageSpec(true);
|
|
449
517
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
450
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
518
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
451
519
|
|
|
452
520
|
expect(marks).toHaveLength(4);
|
|
453
521
|
|
|
@@ -462,7 +530,7 @@ describe('computeBarMarks', () => {
|
|
|
462
530
|
it('stacked with stack:zero: segments share the same y position (stacked on same row)', () => {
|
|
463
531
|
const spec = makeWageSpec(true);
|
|
464
532
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
465
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
533
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
466
534
|
|
|
467
535
|
const smallFirmMarks = marks.filter((m) => m.aria.label.includes('<5'));
|
|
468
536
|
expect(smallFirmMarks).toHaveLength(2);
|
|
@@ -472,11 +540,23 @@ describe('computeBarMarks', () => {
|
|
|
472
540
|
it('grouped vs stacked: grouped bars each start from baseline', () => {
|
|
473
541
|
const stackedSpec = makeWageSpec(true);
|
|
474
542
|
const stackedScales = computeScales(stackedSpec, chartArea, stackedSpec.data);
|
|
475
|
-
const stackedMarks = computeBarMarks(
|
|
543
|
+
const stackedMarks = computeBarMarks(
|
|
544
|
+
stackedSpec,
|
|
545
|
+
stackedScales,
|
|
546
|
+
chartArea,
|
|
547
|
+
fullStrategy,
|
|
548
|
+
CONTAINER_WIDTH,
|
|
549
|
+
);
|
|
476
550
|
|
|
477
551
|
const groupedSpec = makeWageSpec(false);
|
|
478
552
|
const groupedScales = computeScales(groupedSpec, chartArea, groupedSpec.data);
|
|
479
|
-
const groupedMarks = computeBarMarks(
|
|
553
|
+
const groupedMarks = computeBarMarks(
|
|
554
|
+
groupedSpec,
|
|
555
|
+
groupedScales,
|
|
556
|
+
chartArea,
|
|
557
|
+
fullStrategy,
|
|
558
|
+
CONTAINER_WIDTH,
|
|
559
|
+
);
|
|
480
560
|
|
|
481
561
|
expect(groupedMarks).toHaveLength(4);
|
|
482
562
|
|
|
@@ -495,7 +575,7 @@ describe('computeBarMarks', () => {
|
|
|
495
575
|
const spec = makeSimpleBarSpec();
|
|
496
576
|
(spec.encoding.x as { scale?: { domain: number[] } }).scale = { domain: [65, 95] };
|
|
497
577
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
498
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
578
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
499
579
|
|
|
500
580
|
const xScale = scales.x!.scale as (v: number) => number;
|
|
501
581
|
const left = xScale(65);
|
|
@@ -517,7 +597,7 @@ describe('computeBarMarks', () => {
|
|
|
517
597
|
// Lift values into the [65, 95] window so widths stay positive.
|
|
518
598
|
spec.data = spec.data.map((d) => ({ ...d, value: 70 + (d.value as number) / 10 }));
|
|
519
599
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
520
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
600
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
521
601
|
|
|
522
602
|
const xScale = scales.x!.scale as (v: number) => number;
|
|
523
603
|
const left = xScale(65);
|
|
@@ -533,7 +613,7 @@ describe('computeBarMarks', () => {
|
|
|
533
613
|
it('simple bars still anchor at xScale(0) for a default [0, max] domain', () => {
|
|
534
614
|
const spec = makeSimpleBarSpec();
|
|
535
615
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
536
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
616
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
537
617
|
|
|
538
618
|
const xScale = scales.x!.scale as (v: number) => number;
|
|
539
619
|
for (const mark of marks) {
|
|
@@ -544,7 +624,7 @@ describe('computeBarMarks', () => {
|
|
|
544
624
|
it('diverging bars (domain crosses zero) still anchor at xScale(0)', () => {
|
|
545
625
|
const spec = makeNegativeBarSpec();
|
|
546
626
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
547
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
627
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
548
628
|
|
|
549
629
|
const xScale = scales.x!.scale as (v: number) => number;
|
|
550
630
|
const zero = xScale(0);
|
|
@@ -574,7 +654,7 @@ describe('computeBarMarks', () => {
|
|
|
574
654
|
labels: { density: 'auto', format: '' },
|
|
575
655
|
};
|
|
576
656
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
577
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
657
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
578
658
|
expect(marks).toHaveLength(0);
|
|
579
659
|
});
|
|
580
660
|
|
|
@@ -595,7 +675,7 @@ describe('computeBarMarks', () => {
|
|
|
595
675
|
labels: { density: 'auto', format: '' },
|
|
596
676
|
};
|
|
597
677
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
598
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
678
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
599
679
|
expect(marks).toHaveLength(0);
|
|
600
680
|
});
|
|
601
681
|
});
|
|
@@ -609,7 +689,7 @@ describe('computeBarLabels', () => {
|
|
|
609
689
|
it('produces one label per bar mark', () => {
|
|
610
690
|
const spec = makeSimpleBarSpec();
|
|
611
691
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
612
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
692
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
613
693
|
const labels = computeBarLabels(marks, chartArea);
|
|
614
694
|
|
|
615
695
|
expect(labels).toHaveLength(marks.length);
|
|
@@ -618,7 +698,7 @@ describe('computeBarLabels', () => {
|
|
|
618
698
|
it('labels contain the value text', () => {
|
|
619
699
|
const spec = makeSimpleBarSpec();
|
|
620
700
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
621
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
701
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
622
702
|
const labels = computeBarLabels(marks, chartArea);
|
|
623
703
|
|
|
624
704
|
const texts = labels.map((l) => l.text);
|
|
@@ -630,7 +710,7 @@ describe('computeBarLabels', () => {
|
|
|
630
710
|
it('applies d3 label format string', () => {
|
|
631
711
|
const spec = makeSimpleBarSpec();
|
|
632
712
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
633
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
713
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
634
714
|
const labels = computeBarLabels(marks, chartArea, 'auto', '$,.0f');
|
|
635
715
|
|
|
636
716
|
const texts = labels.map((l) => l.text);
|
|
@@ -659,7 +739,7 @@ describe('computeBarLabels', () => {
|
|
|
659
739
|
labels: { density: 'all', format: '$,.2~fT' },
|
|
660
740
|
};
|
|
661
741
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
662
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
742
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
663
743
|
const labels = computeBarLabels(marks, chartArea, 'all', '$,.2~fT');
|
|
664
744
|
|
|
665
745
|
const texts = labels.map((l) => l.text);
|
|
@@ -670,7 +750,7 @@ describe('computeBarLabels', () => {
|
|
|
670
750
|
it('applies format with non-alpha suffix (e.g. "%")', () => {
|
|
671
751
|
const spec = makeSimpleBarSpec();
|
|
672
752
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
673
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
753
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
674
754
|
const labels = computeBarLabels(marks, chartArea, 'auto', '.0f%');
|
|
675
755
|
|
|
676
756
|
const texts = labels.map((l) => l.text);
|
|
@@ -701,7 +781,7 @@ describe('computeBarLabels', () => {
|
|
|
701
781
|
labels: { density: 'auto', format: '', prefix: '' },
|
|
702
782
|
};
|
|
703
783
|
const scales = computeScales(spec, smallArea, spec.data);
|
|
704
|
-
const marks = computeBarMarks(spec, scales, smallArea, fullStrategy);
|
|
784
|
+
const marks = computeBarMarks(spec, scales, smallArea, fullStrategy, CONTAINER_WIDTH);
|
|
705
785
|
const labels = computeBarLabels(
|
|
706
786
|
marks,
|
|
707
787
|
smallArea,
|
|
@@ -726,7 +806,7 @@ describe('markDef overrides', () => {
|
|
|
726
806
|
const spec = makeSimpleBarSpec();
|
|
727
807
|
spec.markDef = { type: 'bar', size: 6 };
|
|
728
808
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
729
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
809
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
730
810
|
|
|
731
811
|
for (const mark of marks) {
|
|
732
812
|
expect(mark.height).toBe(6);
|
|
@@ -735,7 +815,13 @@ describe('markDef overrides', () => {
|
|
|
735
815
|
// Bars should be centered: offset = (bandwidth - 6) / 2
|
|
736
816
|
const specDefault = makeSimpleBarSpec();
|
|
737
817
|
const scalesDefault = computeScales(specDefault, chartArea, specDefault.data);
|
|
738
|
-
const marksDefault = computeBarMarks(
|
|
818
|
+
const marksDefault = computeBarMarks(
|
|
819
|
+
specDefault,
|
|
820
|
+
scalesDefault,
|
|
821
|
+
chartArea,
|
|
822
|
+
fullStrategy,
|
|
823
|
+
CONTAINER_WIDTH,
|
|
824
|
+
);
|
|
739
825
|
for (let i = 0; i < marks.length; i++) {
|
|
740
826
|
expect(marks[i].y).toBeGreaterThan(marksDefault[i].y);
|
|
741
827
|
}
|
|
@@ -745,7 +831,7 @@ describe('markDef overrides', () => {
|
|
|
745
831
|
const spec = makeSimpleBarSpec();
|
|
746
832
|
spec.markDef = { type: 'bar', size: 9999 };
|
|
747
833
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
748
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
834
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
749
835
|
|
|
750
836
|
const bandwidth = marks[0].height;
|
|
751
837
|
// Should be capped at bandwidth, not 9999
|
|
@@ -756,7 +842,7 @@ describe('markDef overrides', () => {
|
|
|
756
842
|
const spec = makeSimpleBarSpec();
|
|
757
843
|
spec.markDef = { type: 'bar', size: 6, cornerRadius: 'pill' };
|
|
758
844
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
759
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
845
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
760
846
|
|
|
761
847
|
for (const mark of marks) {
|
|
762
848
|
expect(mark.cornerRadius).toBe(3);
|
|
@@ -767,7 +853,7 @@ describe('markDef overrides', () => {
|
|
|
767
853
|
const spec = makeSimpleBarSpec();
|
|
768
854
|
spec.markDef = { type: 'bar', cornerRadius: 8 };
|
|
769
855
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
770
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
856
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
771
857
|
|
|
772
858
|
for (const mark of marks) {
|
|
773
859
|
expect(mark.cornerRadius).toBe(8);
|
|
@@ -779,7 +865,7 @@ describe('markDef overrides', () => {
|
|
|
779
865
|
spec.markDef = { type: 'bar', size: 6 };
|
|
780
866
|
// Enable stacking (default for grouped)
|
|
781
867
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
782
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
868
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
783
869
|
|
|
784
870
|
const stackedMarks = marks.filter((m) => m.stackGroup !== undefined);
|
|
785
871
|
for (const mark of stackedMarks) {
|
|
@@ -791,7 +877,7 @@ describe('markDef overrides', () => {
|
|
|
791
877
|
const spec = makeSimpleBarSpec();
|
|
792
878
|
spec.markDef = { type: 'bar', size: 10, cornerRadius: 'pill' };
|
|
793
879
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
794
|
-
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy);
|
|
880
|
+
const marks = computeBarMarks(spec, scales, chartArea, fullStrategy, CONTAINER_WIDTH);
|
|
795
881
|
|
|
796
882
|
for (const mark of marks) {
|
|
797
883
|
expect(mark.height).toBe(10);
|