@opendata-ai/openchart-engine 6.28.6 → 7.0.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/README.md +1 -0
- package/dist/index.d.ts +8 -11
- package/dist/index.js +12296 -11337
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -0
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +452 -377
- package/src/__tests__/axes.test.ts +75 -0
- package/src/__tests__/compile-chart.test.ts +304 -0
- package/src/__tests__/dimensions.test.ts +224 -0
- package/src/__tests__/legend.test.ts +44 -3
- package/src/annotations/__tests__/compute.test.ts +111 -0
- package/src/annotations/__tests__/resolve-text.test.ts +288 -0
- package/src/annotations/constants.ts +20 -0
- package/src/annotations/resolve-text.ts +161 -7
- package/src/charts/bar/compute.ts +24 -0
- package/src/charts/bar/labels.ts +1 -0
- package/src/charts/column/compute.ts +33 -1
- package/src/charts/column/labels.ts +1 -0
- package/src/charts/dot/labels.ts +1 -0
- package/src/charts/line/__tests__/compute.test.ts +153 -3
- package/src/charts/line/area.ts +111 -23
- package/src/charts/line/compute.ts +40 -10
- package/src/charts/line/index.ts +34 -7
- package/src/charts/line/labels.ts +29 -0
- package/src/charts/pie/labels.ts +1 -0
- package/src/compile/layer.ts +497 -0
- package/src/compile.ts +211 -586
- package/src/compiler/__tests__/sparkline-defaults.test.ts +153 -0
- package/src/compiler/normalize.ts +6 -1
- package/src/compiler/sparkline-defaults.ts +138 -0
- package/src/compiler/types.ts +8 -0
- package/src/endpoint-labels/__tests__/compute.test.ts +438 -0
- package/src/endpoint-labels/compute.ts +417 -0
- package/src/endpoint-labels/constants.ts +54 -0
- package/src/endpoint-labels/format.ts +30 -0
- package/src/endpoint-labels/predict.ts +108 -0
- package/src/graphs/compile-graph.ts +1 -0
- package/src/layout/axes.ts +27 -2
- package/src/layout/dimensions.ts +270 -33
- package/src/layout/metrics.ts +118 -0
- package/src/layout/scales.ts +41 -4
- package/src/legend/__tests__/suppression.test.ts +294 -0
- package/src/legend/compute.ts +50 -40
- package/src/legend/suppression.ts +204 -0
- package/src/sankey/compile-sankey.ts +2 -0
package/src/charts/line/area.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
* Area chart mark computation.
|
|
3
3
|
*
|
|
4
4
|
* Uses D3 area() generator to produce AreaMark[] with top/bottom
|
|
5
|
-
* boundary points and SVG path strings.
|
|
6
|
-
*
|
|
5
|
+
* boundary points and SVG path strings.
|
|
6
|
+
*
|
|
7
|
+
* Multi-series behavior (v6 redesign):
|
|
8
|
+
* - Default for multi-series with `color` is **overlap** (one translucent
|
|
9
|
+
* gradient band per series, layered with low opacity).
|
|
10
|
+
* - Stacked rendering is opt-in via `encoding.y.stack: 'zero' | true | 'normalize' | 'center'`.
|
|
11
|
+
* - Single series always renders one gradient-filled area.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import type { AreaMark, DataRow, Encoding, MarkAria, Rect } from '@opendata-ai/openchart-core';
|
|
@@ -30,10 +35,76 @@ import { resolveCurve } from './curves';
|
|
|
30
35
|
|
|
31
36
|
const DEFAULT_FILL_OPACITY = 0.15;
|
|
32
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Resolve `encoding.y.stack` to a boolean: should we stack this area chart?
|
|
40
|
+
*
|
|
41
|
+
* Vega-Lite-aligned semantics:
|
|
42
|
+
* - undefined | null | false -> overlap (NEW DEFAULT for area)
|
|
43
|
+
* - true | 'zero' | 'normalize' | 'center' -> stacked
|
|
44
|
+
*
|
|
45
|
+
* This is a v6 breaking change. Previously, multi-series with `color`
|
|
46
|
+
* implicitly stacked. Now overlap is the default; stacking is opt-in.
|
|
47
|
+
*/
|
|
48
|
+
function isStacked(stackProp: unknown): boolean {
|
|
49
|
+
if (stackProp === undefined || stackProp === null || stackProp === false) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
stackProp === true ||
|
|
54
|
+
stackProp === 'zero' ||
|
|
55
|
+
stackProp === 'normalize' ||
|
|
56
|
+
stackProp === 'center'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Gradient stops calibrated by series count. Solo areas can carry richer fills
|
|
61
|
+
// because there's no overlap to manage; multi-series overlap needs lighter
|
|
62
|
+
// stops so layered bands stay legible.
|
|
63
|
+
const SOLO_GRADIENT_STOPS = [
|
|
64
|
+
{ offset: 0, opacity: 0.42 },
|
|
65
|
+
{ offset: 0.6, opacity: 0.1 },
|
|
66
|
+
{ offset: 1, opacity: 0 },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const OVERLAP_GRADIENT_STOPS = [
|
|
70
|
+
{ offset: 0, opacity: 0.22 },
|
|
71
|
+
{ offset: 0.7, opacity: 0.04 },
|
|
72
|
+
{ offset: 1, opacity: 0 },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Stacked layers sit on top of each other and need higher opacity so each
|
|
76
|
+
// band reads as a distinct quantity. A gentle top-to-bottom gradient adds
|
|
77
|
+
// depth without losing the categorical color identity.
|
|
78
|
+
const STACKED_GRADIENT_STOPS = [
|
|
79
|
+
{ offset: 0, opacity: 0.65 },
|
|
80
|
+
{ offset: 1, opacity: 0.35 },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
function buildGradientFill(
|
|
84
|
+
colorStr: string,
|
|
85
|
+
stops: ReadonlyArray<{ offset: number; opacity: number }>,
|
|
86
|
+
): import('@opendata-ai/openchart-core').GradientDef {
|
|
87
|
+
return {
|
|
88
|
+
gradient: 'linear',
|
|
89
|
+
x1: 0,
|
|
90
|
+
y1: 0,
|
|
91
|
+
x2: 0,
|
|
92
|
+
y2: 1,
|
|
93
|
+
stops: stops.map((s) => ({ offset: s.offset, color: colorStr, opacity: s.opacity })),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
33
97
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Single area (non-stacked)
|
|
98
|
+
// Single / overlap area (non-stacked)
|
|
35
99
|
// ---------------------------------------------------------------------------
|
|
36
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Compute area marks for non-stacked rendering.
|
|
103
|
+
*
|
|
104
|
+
* - With no `color` field: emits a single area with the solo gradient.
|
|
105
|
+
* - With a `color` field (overlap mode): emits one area per series, each
|
|
106
|
+
* anchored at the y-domain baseline and using the lighter overlap gradient.
|
|
107
|
+
*/
|
|
37
108
|
function computeSingleArea(
|
|
38
109
|
spec: NormalizedChartSpec,
|
|
39
110
|
scales: ResolvedScales,
|
|
@@ -69,6 +140,9 @@ function computeSingleArea(
|
|
|
69
140
|
}
|
|
70
141
|
}
|
|
71
142
|
|
|
143
|
+
const isMultiSeries = colorField !== undefined && groups.size > 1;
|
|
144
|
+
const defaultGradientStops = isMultiSeries ? OVERLAP_GRADIENT_STOPS : SOLO_GRADIENT_STOPS;
|
|
145
|
+
|
|
72
146
|
const marks: AreaMark[] = [];
|
|
73
147
|
|
|
74
148
|
for (const [seriesKey, rows] of groups) {
|
|
@@ -137,7 +211,8 @@ function computeSingleArea(
|
|
|
137
211
|
|
|
138
212
|
// Allow markDef.fill to override color with a gradient.
|
|
139
213
|
// When a gradient is provided, set fillOpacity=1 so gradient stop-opacity controls the fade.
|
|
140
|
-
// When no fill is provided, auto-generate a top-to-bottom fade gradient
|
|
214
|
+
// When no fill is provided, auto-generate a top-to-bottom fade gradient
|
|
215
|
+
// tuned to series count (lighter for overlap).
|
|
141
216
|
const markFill = spec.markDef.fill;
|
|
142
217
|
let fillValue: string | import('@opendata-ai/openchart-core').GradientDef;
|
|
143
218
|
let fillOpacity: number;
|
|
@@ -149,17 +224,7 @@ function computeSingleArea(
|
|
|
149
224
|
: (spec.markDef.opacity ?? (y2Channel ? 0.25 : DEFAULT_FILL_OPACITY));
|
|
150
225
|
} else {
|
|
151
226
|
const colorStr = getRepresentativeColor(color);
|
|
152
|
-
fillValue =
|
|
153
|
-
gradient: 'linear',
|
|
154
|
-
x1: 0,
|
|
155
|
-
y1: 0,
|
|
156
|
-
x2: 0,
|
|
157
|
-
y2: 1,
|
|
158
|
-
stops: [
|
|
159
|
-
{ offset: 0, color: colorStr, opacity: 0.12 },
|
|
160
|
-
{ offset: 1, color: colorStr, opacity: 0 },
|
|
161
|
-
],
|
|
162
|
-
};
|
|
227
|
+
fillValue = buildGradientFill(colorStr, defaultGradientStops);
|
|
163
228
|
fillOpacity = 1;
|
|
164
229
|
}
|
|
165
230
|
|
|
@@ -171,8 +236,9 @@ function computeSingleArea(
|
|
|
171
236
|
topPath: topPathStr,
|
|
172
237
|
fill: fillValue,
|
|
173
238
|
fillOpacity: fillOpacity,
|
|
174
|
-
stroke:
|
|
175
|
-
|
|
239
|
+
stroke:
|
|
240
|
+
spec.markDef.stroke ?? getRepresentativeColor(isGradientDef(fillValue) ? color : fillValue),
|
|
241
|
+
strokeWidth: spec.markDef.strokeWidth ?? 1.5,
|
|
176
242
|
seriesKey: seriesKey === '__default__' ? undefined : seriesKey,
|
|
177
243
|
data: validPoints.map((p) => p.row),
|
|
178
244
|
dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
|
|
@@ -312,14 +378,29 @@ function computeStackedArea(
|
|
|
312
378
|
label: `${seriesKey}: stacked area with ${validPoints.length} data points`,
|
|
313
379
|
};
|
|
314
380
|
|
|
381
|
+
// Per-layer top-to-bottom gradient. User-supplied markDef.fill (string or
|
|
382
|
+
// gradient) overrides the auto gradient just like in single-area mode.
|
|
383
|
+
const markFill = spec.markDef.fill;
|
|
384
|
+
let fillValue: string | import('@opendata-ai/openchart-core').GradientDef;
|
|
385
|
+
let fillOpacity: number;
|
|
386
|
+
|
|
387
|
+
if (markFill != null) {
|
|
388
|
+
fillValue = markFill;
|
|
389
|
+
fillOpacity = isGradientDef(markFill) ? 1 : (spec.markDef.opacity ?? 0.7);
|
|
390
|
+
} else {
|
|
391
|
+
const colorStr = getRepresentativeColor(color);
|
|
392
|
+
fillValue = buildGradientFill(colorStr, STACKED_GRADIENT_STOPS);
|
|
393
|
+
fillOpacity = 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
315
396
|
marks.push({
|
|
316
397
|
type: 'area',
|
|
317
398
|
topPoints,
|
|
318
399
|
bottomPoints,
|
|
319
400
|
path: pathStr,
|
|
320
401
|
topPath: topPathStr,
|
|
321
|
-
fill:
|
|
322
|
-
fillOpacity
|
|
402
|
+
fill: fillValue,
|
|
403
|
+
fillOpacity,
|
|
323
404
|
stroke: getRepresentativeColor(color),
|
|
324
405
|
strokeWidth: 1,
|
|
325
406
|
seriesKey,
|
|
@@ -348,8 +429,15 @@ function computeStackedArea(
|
|
|
348
429
|
/**
|
|
349
430
|
* Compute area marks from a normalized chart spec.
|
|
350
431
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
432
|
+
* Behavior depends on `encoding.y.stack` (Vega-Lite aligned):
|
|
433
|
+
* - `undefined | null | false` -> overlap (default for multi-series)
|
|
434
|
+
* - `true | 'zero' | 'normalize' | 'center'` -> stacked
|
|
435
|
+
*
|
|
436
|
+
* Single-series specs always render one gradient-filled area; the `stack`
|
|
437
|
+
* branch only matters when a `color` encoding is present.
|
|
438
|
+
*
|
|
439
|
+
* BREAKING CHANGE (v6): multi-series no longer auto-stacks. Pass
|
|
440
|
+
* `encoding.y.stack: 'zero'` (or `true`) to opt back into the old behavior.
|
|
353
441
|
*/
|
|
354
442
|
export function computeAreaMarks(
|
|
355
443
|
spec: NormalizedChartSpec,
|
|
@@ -357,9 +445,9 @@ export function computeAreaMarks(
|
|
|
357
445
|
chartArea: Rect,
|
|
358
446
|
): AreaMark[] {
|
|
359
447
|
const encoding = spec.encoding as Encoding;
|
|
360
|
-
const
|
|
448
|
+
const yChannel = encoding.y;
|
|
361
449
|
|
|
362
|
-
if (
|
|
450
|
+
if (yChannel && isStacked(yChannel.stack)) {
|
|
363
451
|
return computeStackedArea(spec, scales, chartArea);
|
|
364
452
|
}
|
|
365
453
|
|
|
@@ -29,13 +29,10 @@ import { resolveCurve } from './curves';
|
|
|
29
29
|
// Constants
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
-
/** Default stroke width for line marks.
|
|
32
|
+
/** Default stroke width for line marks. Sparklines use the same width so the
|
|
33
|
+
* visual weight matches the rest of the chart family. */
|
|
33
34
|
const DEFAULT_STROKE_WIDTH = 1.5;
|
|
34
35
|
|
|
35
|
-
/** Sparkline mode uses a thinner stroke since the chart area is tiny and a
|
|
36
|
-
* 2.5px line reads as clunky. 1.25px keeps the trend legible without dominating. */
|
|
37
|
-
const SPARKLINE_STROKE_WIDTH = 1.25;
|
|
38
|
-
|
|
39
36
|
/** Default radius for point marks (hover targets). */
|
|
40
37
|
const DEFAULT_POINT_RADIUS = 3;
|
|
41
38
|
|
|
@@ -77,7 +74,10 @@ export function computeLineMarks(
|
|
|
77
74
|
const color: string | GradientDef = isSequentialColor
|
|
78
75
|
? getSequentialColor(scales, _getMidValue(rows, sequentialColorField!))
|
|
79
76
|
: getColor(scales, seriesKey);
|
|
80
|
-
|
|
77
|
+
// markDef.stroke wins over the scale-derived color when set explicitly.
|
|
78
|
+
// Sparkline mode injects this via applySparklineDefaults to carry the
|
|
79
|
+
// trend color; users can also set it directly to override the palette.
|
|
80
|
+
const strokeColor = spec.markDef.stroke ?? getRepresentativeColor(color);
|
|
81
81
|
|
|
82
82
|
// Sort rows by x-axis field so lines draw left-to-right.
|
|
83
83
|
// For nominal/ordinal axes, preserve data order since there's no
|
|
@@ -178,10 +178,7 @@ export function computeLineMarks(
|
|
|
178
178
|
points: allPoints,
|
|
179
179
|
path: combinedPath,
|
|
180
180
|
stroke: strokeColor,
|
|
181
|
-
strokeWidth:
|
|
182
|
-
styleOverride?.strokeWidth ??
|
|
183
|
-
spec.markDef.strokeWidth ??
|
|
184
|
-
(spec.display === 'sparkline' ? SPARKLINE_STROKE_WIDTH : DEFAULT_STROKE_WIDTH),
|
|
181
|
+
strokeWidth: styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? DEFAULT_STROKE_WIDTH,
|
|
185
182
|
strokeDasharray,
|
|
186
183
|
opacity: styleOverride?.opacity,
|
|
187
184
|
seriesKey: seriesStyleKey,
|
|
@@ -195,10 +192,12 @@ export function computeLineMarks(
|
|
|
195
192
|
// Emit PointMark objects when markDef.point is truthy, or when sequential
|
|
196
193
|
// color is active (points carry the gradient since SVG paths are single-color).
|
|
197
194
|
const markPoint = spec.markDef.point;
|
|
195
|
+
const isSingleEndpoint = markPoint === 'last' || markPoint === 'first';
|
|
198
196
|
const showPoints =
|
|
199
197
|
markPoint === true ||
|
|
200
198
|
markPoint === 'transparent' ||
|
|
201
199
|
markPoint === 'endpoints' ||
|
|
200
|
+
isSingleEndpoint ||
|
|
202
201
|
isSequentialColor;
|
|
203
202
|
|
|
204
203
|
if (showPoints) {
|
|
@@ -211,6 +210,14 @@ export function computeLineMarks(
|
|
|
211
210
|
for (let i = 0; i < pointsWithData.length; i++) {
|
|
212
211
|
const p = pointsWithData[i];
|
|
213
212
|
const isEndpoint = i === 0 || i === lastIdx;
|
|
213
|
+
const isLast = i === lastIdx;
|
|
214
|
+
const isFirst = i === 0;
|
|
215
|
+
// Single-endpoint mode ('last' / 'first'): emit only the chosen point.
|
|
216
|
+
// Skip the others entirely instead of emitting invisible placeholders.
|
|
217
|
+
if (isSingleEndpoint) {
|
|
218
|
+
if (markPoint === 'last' && !isLast) continue;
|
|
219
|
+
if (markPoint === 'first' && !isFirst) continue;
|
|
220
|
+
}
|
|
214
221
|
const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
|
|
215
222
|
// Sequential color: each point gets colored by its data value
|
|
216
223
|
let pointColor = color;
|
|
@@ -220,6 +227,29 @@ export function computeLineMarks(
|
|
|
220
227
|
}
|
|
221
228
|
const hollow = isEndpoints && visible;
|
|
222
229
|
const pointColorStr = getRepresentativeColor(pointColor);
|
|
230
|
+
// 'last' / 'first' render as a tight filled terminator dot in the line
|
|
231
|
+
// color — no white halo, no hollow ring. Marked decorative because the
|
|
232
|
+
// data point already exists on the line and gets described by the a11y
|
|
233
|
+
// data table; the dot is purely visual.
|
|
234
|
+
if (isSingleEndpoint) {
|
|
235
|
+
marks.push({
|
|
236
|
+
type: 'point',
|
|
237
|
+
cx: p.x,
|
|
238
|
+
cy: p.y,
|
|
239
|
+
// 3.5 reads as a deliberate terminator against the 2px sparkline
|
|
240
|
+
// line — smaller dots blur into the stroke at typical card sizes.
|
|
241
|
+
r: 3.5,
|
|
242
|
+
fill: strokeColor,
|
|
243
|
+
stroke: 'transparent',
|
|
244
|
+
strokeWidth: 0,
|
|
245
|
+
fillOpacity: 1,
|
|
246
|
+
data: p.row,
|
|
247
|
+
// No label: decorative marks render with aria-hidden="true" and
|
|
248
|
+
// don't participate in the accessible data table.
|
|
249
|
+
aria: { decorative: true },
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
223
253
|
const pointMark: PointMark = {
|
|
224
254
|
type: 'point',
|
|
225
255
|
cx: p.x,
|
package/src/charts/line/index.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Line & area chart module.
|
|
3
3
|
*
|
|
4
4
|
* Exports line and area chart renderers and computation functions.
|
|
5
|
+
*
|
|
6
|
+
* Area chart multi-series defaults (v6):
|
|
7
|
+
* - Default with `color` encoding is **overlap** -- one translucent gradient
|
|
8
|
+
* band per series, all anchored at the y-domain baseline.
|
|
9
|
+
* - Stacked rendering is opt-in: set `encoding.y.stack` to `true`, `'zero'`,
|
|
10
|
+
* `'normalize'`, or `'center'`.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import type { AreaMark, LineMark, Mark } from '@opendata-ai/openchart-core';
|
|
@@ -27,8 +33,16 @@ export const lineRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _
|
|
|
27
33
|
// Extract just the line marks for label computation
|
|
28
34
|
const lineMarks = marks.filter((m): m is LineMark => m.type === 'line');
|
|
29
35
|
|
|
30
|
-
// Compute and attach labels to line marks by seriesKey lookup
|
|
31
|
-
|
|
36
|
+
// Compute and attach labels to line marks by seriesKey lookup. Passing the
|
|
37
|
+
// spec engages the shared suppression truth table so end-of-line labels
|
|
38
|
+
// hide when the legend or endpoint column is showing.
|
|
39
|
+
const labelMap = computeLineLabels(
|
|
40
|
+
lineMarks,
|
|
41
|
+
strategy,
|
|
42
|
+
spec.labels.density,
|
|
43
|
+
spec.labels.offsets,
|
|
44
|
+
spec,
|
|
45
|
+
);
|
|
32
46
|
for (const mark of marks) {
|
|
33
47
|
if (mark.type === 'line' && mark.seriesKey) {
|
|
34
48
|
const label = labelMap.get(mark.seriesKey);
|
|
@@ -48,9 +62,14 @@ export const lineRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _
|
|
|
48
62
|
/**
|
|
49
63
|
* Area chart renderer.
|
|
50
64
|
*
|
|
51
|
-
* Computes area fill marks (stacked
|
|
65
|
+
* Computes area fill marks (stacked or overlapping per `encoding.y.stack`).
|
|
52
66
|
* Also computes line marks for the top boundary and point marks
|
|
53
67
|
* for hover targets, layered on top of the areas.
|
|
68
|
+
*
|
|
69
|
+
* Lines are derived from area top boundaries whenever there's a color
|
|
70
|
+
* encoding -- this keeps each line glued to its band's top edge regardless
|
|
71
|
+
* of whether the layout is stacked (cumulative tops) or overlap (per-series
|
|
72
|
+
* raw values).
|
|
54
73
|
*/
|
|
55
74
|
export const areaRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
56
75
|
const areas = computeAreaMarks(spec, scales, chartArea);
|
|
@@ -58,8 +77,9 @@ export const areaRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _
|
|
|
58
77
|
const encoding = spec.encoding;
|
|
59
78
|
const hasColor = !!(encoding.color && 'field' in encoding.color);
|
|
60
79
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
80
|
+
// With a color encoding (stacked or overlap), derive line marks from the
|
|
81
|
+
// area tops so each line traces the upper edge of its band. For single
|
|
82
|
+
// series, compute lines normally so we get the regular line + point marks.
|
|
63
83
|
const lines = hasColor
|
|
64
84
|
? linesFromAreas(areas)
|
|
65
85
|
: computeLineMarks(spec, scales, chartArea, strategy);
|
|
@@ -73,8 +93,15 @@ export const areaRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _
|
|
|
73
93
|
// ---------------------------------------------------------------------------
|
|
74
94
|
|
|
75
95
|
/**
|
|
76
|
-
* Derive LineMark[] from
|
|
77
|
-
*
|
|
96
|
+
* Derive LineMark[] from AreaMark[] using each area's top boundary.
|
|
97
|
+
*
|
|
98
|
+
* Works for both stacked and overlap layouts:
|
|
99
|
+
* - Stacked: `topPoints` is the cumulative top edge of the layer, so the line
|
|
100
|
+
* sits on top of its band rather than the raw series value.
|
|
101
|
+
* - Overlap: `topPoints` is the series' own y value (areas all share the same
|
|
102
|
+
* baseline), so the line traces the actual data.
|
|
103
|
+
*
|
|
104
|
+
* No z-order assumption -- each area independently provides its own top.
|
|
78
105
|
*/
|
|
79
106
|
function linesFromAreas(areas: AreaMark[]): LineMark[] {
|
|
80
107
|
return areas.map((a) => ({
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
resolveCollisions,
|
|
26
26
|
} from '@opendata-ai/openchart-core';
|
|
27
27
|
|
|
28
|
+
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
29
|
+
import { countColorSeries, resolveSuppression } from '../../legend/suppression';
|
|
30
|
+
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Constants
|
|
30
33
|
// ---------------------------------------------------------------------------
|
|
@@ -56,6 +59,11 @@ const LABEL_OFFSET_X = 6;
|
|
|
56
59
|
* @param marks - Line marks (only processes marks with type === 'line').
|
|
57
60
|
* @param strategy - Layout strategy from the responsive breakpoint.
|
|
58
61
|
* @param density - Label density mode from spec.labels.density.
|
|
62
|
+
* @param labelOffsets - Per-series user offsets applied after collision resolution.
|
|
63
|
+
* @param spec - Optional normalized spec. When provided, the shared suppression
|
|
64
|
+
* truth table gates rendering: end-of-line labels are the last-resort series
|
|
65
|
+
* identifier and only render when both the traditional legend and endpoint
|
|
66
|
+
* labels column are off.
|
|
59
67
|
* @returns Map of seriesKey -> ResolvedLabel after collision detection.
|
|
60
68
|
*/
|
|
61
69
|
export function computeLineLabels(
|
|
@@ -63,6 +71,7 @@ export function computeLineLabels(
|
|
|
63
71
|
strategy: LayoutStrategy,
|
|
64
72
|
density: LabelDensity = 'auto',
|
|
65
73
|
labelOffsets?: Record<string, { dx?: number; dy?: number }>,
|
|
74
|
+
spec?: NormalizedChartSpec,
|
|
66
75
|
): Map<string, ResolvedLabel> {
|
|
67
76
|
const result = new Map<string, ResolvedLabel>();
|
|
68
77
|
|
|
@@ -74,6 +83,26 @@ export function computeLineLabels(
|
|
|
74
83
|
return result;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
// Suppression truth table: when the spec is available, defer to the shared
|
|
87
|
+
// helper. End-of-line labels render only when both the legend and endpoint
|
|
88
|
+
// column are off — they're the last-resort series identifier.
|
|
89
|
+
if (spec) {
|
|
90
|
+
// The 'none' branches above already returned, so by here labelMode and
|
|
91
|
+
// density are not 'none'. Pass false explicitly to keep the helper pure.
|
|
92
|
+
const suppression = resolveSuppression(spec, {
|
|
93
|
+
seriesCount: countColorSeries(spec),
|
|
94
|
+
labelsHiddenByStrategy: false,
|
|
95
|
+
labelsDensityNone: false,
|
|
96
|
+
});
|
|
97
|
+
if (!suppression.showEndOfLineLabels) {
|
|
98
|
+
// Single-series charts have no series-name end-of-line label to hide
|
|
99
|
+
// (computeLineLabels returns nothing when seriesKey is empty). For
|
|
100
|
+
// multi-series, the suppression result tells us to skip explicitly.
|
|
101
|
+
const isMultiSeries = !!spec.encoding.color && countColorSeries(spec) >= 2;
|
|
102
|
+
if (isMultiSeries) return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
const candidates: LabelCandidate[] = [];
|
|
78
107
|
const seriesOrder: string[] = [];
|
|
79
108
|
|
package/src/charts/pie/labels.ts
CHANGED
|
@@ -66,6 +66,7 @@ export function computePieLabels(
|
|
|
66
66
|
// Format is "Category: value (percent%)". Split on the first colon
|
|
67
67
|
// to handle category names that might contain colons.
|
|
68
68
|
const ariaLabel = mark.aria.label;
|
|
69
|
+
if (!ariaLabel) continue;
|
|
69
70
|
const firstColon = ariaLabel.indexOf(':');
|
|
70
71
|
const labelText = firstColon >= 0 ? ariaLabel.slice(0, firstColon).trim() : '';
|
|
71
72
|
if (!labelText) continue;
|