@opendata-ai/openchart-engine 8.0.0-rc.8 → 8.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/dist/index.d.ts +149 -12
- package/dist/index.js +894 -171
- 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 -2339
- 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 -626
- 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 -1450
- 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 -1004
- package/src/geo/compile-map.ts +0 -1341
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- 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 -747
- 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,356 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Line chart mark computation.
|
|
3
|
-
*
|
|
4
|
-
* Takes a normalized chart spec with resolved scales and produces
|
|
5
|
-
* LineMark[] and PointMark[] arrays for rendering. Groups data by
|
|
6
|
-
* color field for multi-series, uses D3 line() generator for SVG
|
|
7
|
-
* path computation, and handles missing data with line breaks.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
DataRow,
|
|
12
|
-
Encoding,
|
|
13
|
-
GradientDef,
|
|
14
|
-
LayoutStrategy,
|
|
15
|
-
LineMark,
|
|
16
|
-
MarkAria,
|
|
17
|
-
PointMark,
|
|
18
|
-
Rect,
|
|
19
|
-
} from '@opendata-ai/openchart-core';
|
|
20
|
-
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
21
|
-
import { line } from 'd3-shape';
|
|
22
|
-
|
|
23
|
-
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
24
|
-
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
25
|
-
import type { ResolvedScales } from '../../layout/scales';
|
|
26
|
-
import { getColor, getSequentialColor, groupByField, scaleValue, sortByField } from '../utils';
|
|
27
|
-
import { resolveCurve } from './curves';
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Constants
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/** Default stroke width for line marks. Sparklines use the same width so the
|
|
34
|
-
* visual weight matches the rest of the chart family. */
|
|
35
|
-
const DEFAULT_STROKE_WIDTH = 1.5;
|
|
36
|
-
|
|
37
|
-
/** Default radius for point marks (hover targets). */
|
|
38
|
-
const DEFAULT_POINT_RADIUS = 3;
|
|
39
|
-
|
|
40
|
-
/** Dash patterns cycled through by the strokeDash encoding, assigned in the
|
|
41
|
-
* ordinal (first-seen data) order of the field values. The first value
|
|
42
|
-
* renders solid so single-value charts keep the default look. */
|
|
43
|
-
const STROKE_DASH_PATTERNS = ['', '6 4', '2 3', '8 4 2 4', '4 4', '1 3'];
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Public API
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Compute line marks from a normalized chart spec.
|
|
51
|
-
*
|
|
52
|
-
* Produces one LineMark per series (grouped by color field) plus
|
|
53
|
-
* PointMark entries at each data point for hover targets. Missing
|
|
54
|
-
* data (null/undefined y values) breaks the line path.
|
|
55
|
-
*/
|
|
56
|
-
export function computeLineMarks(
|
|
57
|
-
spec: NormalizedChartSpec,
|
|
58
|
-
scales: ResolvedScales,
|
|
59
|
-
_chartArea: Rect,
|
|
60
|
-
_strategy: LayoutStrategy,
|
|
61
|
-
): (LineMark | PointMark)[] {
|
|
62
|
-
const encoding = spec.encoding as Encoding;
|
|
63
|
-
const xChannel = encoding.x;
|
|
64
|
-
const yChannel = encoding.y;
|
|
65
|
-
|
|
66
|
-
if (!xChannel || !yChannel || !scales.x || !scales.y) {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const colorEnc = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
|
|
71
|
-
const isSequentialColor = colorEnc?.type === 'quantitative';
|
|
72
|
-
// Sequential color: single series, per-point coloring. Categorical: group by color field.
|
|
73
|
-
const colorField = isSequentialColor ? undefined : colorEnc?.field;
|
|
74
|
-
const sequentialColorField = isSequentialColor ? colorEnc.field : undefined;
|
|
75
|
-
// strokeDash encoding: differentiate series by dash pattern (VL aligned).
|
|
76
|
-
// Without a color channel, the strokeDash field becomes the series grouping
|
|
77
|
-
// so each dash value renders as its own line. With a color channel, each
|
|
78
|
-
// series takes the dash of its first row (typically the same field).
|
|
79
|
-
const dashEnc =
|
|
80
|
-
encoding.strokeDash && 'field' in encoding.strokeDash ? encoding.strokeDash : undefined;
|
|
81
|
-
const dashField = dashEnc?.field;
|
|
82
|
-
const dashIndexByValue = new Map<string, number>();
|
|
83
|
-
if (dashField) {
|
|
84
|
-
for (const row of spec.data) {
|
|
85
|
-
const value = row[dashField];
|
|
86
|
-
if (value == null) continue;
|
|
87
|
-
const key = String(value);
|
|
88
|
-
if (!dashIndexByValue.has(key)) dashIndexByValue.set(key, dashIndexByValue.size);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const groups = groupByField(spec.data, colorField ?? dashField);
|
|
92
|
-
const mutedMarks: (LineMark | PointMark)[] = [];
|
|
93
|
-
const highlightedMarks: (LineMark | PointMark)[] = [];
|
|
94
|
-
const highlight = spec.highlight ?? [];
|
|
95
|
-
const highlightActive = highlight.length > 0;
|
|
96
|
-
|
|
97
|
-
for (const [seriesKey, rows] of groups) {
|
|
98
|
-
// For sequential color, use a mid-range color for the line stroke
|
|
99
|
-
const color: string | GradientDef = isSequentialColor
|
|
100
|
-
? getSequentialColor(scales, _getMidValue(rows, sequentialColorField!))
|
|
101
|
-
: getColor(scales, seriesKey);
|
|
102
|
-
// markDef.stroke wins over the scale-derived color when set explicitly.
|
|
103
|
-
// Sparkline mode injects this via applySparklineDefaults to carry the
|
|
104
|
-
// trend color; users can also set it directly to override the palette.
|
|
105
|
-
const strokeColor = spec.markDef.stroke ?? getRepresentativeColor(color);
|
|
106
|
-
|
|
107
|
-
// Sort rows by x-axis field so lines draw left-to-right.
|
|
108
|
-
// For nominal/ordinal axes, preserve data order since there's no
|
|
109
|
-
// natural sort and the scale domain already reflects intended order.
|
|
110
|
-
const sortedRows =
|
|
111
|
-
xChannel.type === 'nominal' || xChannel.type === 'ordinal'
|
|
112
|
-
? rows
|
|
113
|
-
: sortByField(rows, xChannel.field);
|
|
114
|
-
|
|
115
|
-
// Compute pixel positions for each data point, preserving nulls
|
|
116
|
-
// for line break handling
|
|
117
|
-
const pointsWithData: {
|
|
118
|
-
x: number;
|
|
119
|
-
y: number;
|
|
120
|
-
row: DataRow;
|
|
121
|
-
}[] = [];
|
|
122
|
-
|
|
123
|
-
// We need to track segments separated by null values
|
|
124
|
-
const segments: { x: number; y: number }[][] = [];
|
|
125
|
-
let currentSegment: { x: number; y: number }[] = [];
|
|
126
|
-
|
|
127
|
-
for (const row of sortedRows) {
|
|
128
|
-
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
129
|
-
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
130
|
-
|
|
131
|
-
if (xVal === null || yVal === null) {
|
|
132
|
-
// Break the line here. Push current segment if non-empty.
|
|
133
|
-
if (currentSegment.length > 0) {
|
|
134
|
-
segments.push(currentSegment);
|
|
135
|
-
currentSegment = [];
|
|
136
|
-
}
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const point = { x: xVal, y: yVal };
|
|
141
|
-
currentSegment.push(point);
|
|
142
|
-
pointsWithData.push({ ...point, row });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Push the last segment
|
|
146
|
-
if (currentSegment.length > 0) {
|
|
147
|
-
segments.push(currentSegment);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Build the D3 line generator with configured interpolation
|
|
151
|
-
const curve = resolveCurve(spec.markDef.interpolate);
|
|
152
|
-
const lineGenerator = line<{ x: number; y: number }>()
|
|
153
|
-
.x((d) => d.x)
|
|
154
|
-
.y((d) => d.y)
|
|
155
|
-
.curve(curve);
|
|
156
|
-
|
|
157
|
-
// Combine all segments into a single path string with M/L commands.
|
|
158
|
-
// Each segment starts a new M (moveto) command, creating line breaks
|
|
159
|
-
// where data is missing.
|
|
160
|
-
const allPoints: { x: number; y: number }[] = [];
|
|
161
|
-
const pathParts: string[] = [];
|
|
162
|
-
|
|
163
|
-
for (const segment of segments) {
|
|
164
|
-
if (segment.length === 0) continue;
|
|
165
|
-
const pathStr = lineGenerator(segment);
|
|
166
|
-
if (pathStr) {
|
|
167
|
-
pathParts.push(pathStr);
|
|
168
|
-
}
|
|
169
|
-
allPoints.push(...segment);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Skip this series if there are no valid data points
|
|
173
|
-
if (allPoints.length === 0) continue;
|
|
174
|
-
|
|
175
|
-
const ariaLabel =
|
|
176
|
-
seriesKey === '__default__'
|
|
177
|
-
? `Line with ${allPoints.length} data points`
|
|
178
|
-
: `${seriesKey}: line with ${allPoints.length} data points`;
|
|
179
|
-
|
|
180
|
-
const aria: MarkAria = {
|
|
181
|
-
label: ariaLabel,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Combine D3 curve path segments into a single path string.
|
|
185
|
-
// Each segment produces a smooth monotone curve; line breaks between
|
|
186
|
-
// segments are created by starting a new M command.
|
|
187
|
-
const combinedPath = pathParts.join(' ');
|
|
188
|
-
|
|
189
|
-
// Look up per-series style overrides
|
|
190
|
-
const seriesStyleKey = seriesKey === '__default__' ? undefined : seriesKey;
|
|
191
|
-
const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : undefined;
|
|
192
|
-
|
|
193
|
-
const isMuted = highlightActive && !highlight.includes(seriesKey);
|
|
194
|
-
let resolvedStrokeWidth =
|
|
195
|
-
styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? DEFAULT_STROKE_WIDTH;
|
|
196
|
-
if (isMuted) {
|
|
197
|
-
resolvedStrokeWidth = Math.max(1, resolvedStrokeWidth * 0.75);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Map lineStyle to SVG strokeDasharray. Explicit seriesStyles win over
|
|
201
|
-
// the strokeDash encoding.
|
|
202
|
-
let strokeDasharray: string | undefined;
|
|
203
|
-
if (styleOverride?.lineStyle === 'dashed') strokeDasharray = '6 4';
|
|
204
|
-
else if (styleOverride?.lineStyle === 'dotted') strokeDasharray = '2 3';
|
|
205
|
-
else if (dashField) {
|
|
206
|
-
const dashValue = rows[0]?.[dashField];
|
|
207
|
-
if (dashValue != null) {
|
|
208
|
-
const idx = dashIndexByValue.get(String(dashValue)) ?? 0;
|
|
209
|
-
const pattern = STROKE_DASH_PATTERNS[idx % STROKE_DASH_PATTERNS.length];
|
|
210
|
-
if (pattern) strokeDasharray = pattern;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Create the LineMark with the combined path points.
|
|
215
|
-
// The points array includes all valid points across all segments.
|
|
216
|
-
// dataPoints carries pixel coordinates + original data for voronoi tooltip overlay.
|
|
217
|
-
const keyField = encoding.key && 'field' in encoding.key ? encoding.key.field : undefined;
|
|
218
|
-
const rawPointKeys = pointsWithData.map((p) =>
|
|
219
|
-
keyField ? serializeKeyValue(p.row[keyField]) : serializeKeyValue(p.row[xChannel.field]),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const lineMark: LineMark = {
|
|
223
|
-
type: 'line',
|
|
224
|
-
key: seriesStyleKey ?? 'series',
|
|
225
|
-
pointKeys: dedupeKeys(rawPointKeys),
|
|
226
|
-
interpolate: spec.markDef.interpolate ?? 'monotone',
|
|
227
|
-
points: allPoints,
|
|
228
|
-
path: combinedPath,
|
|
229
|
-
stroke: strokeColor,
|
|
230
|
-
strokeWidth: resolvedStrokeWidth,
|
|
231
|
-
strokeDasharray,
|
|
232
|
-
opacity: styleOverride?.opacity,
|
|
233
|
-
seriesKey: seriesStyleKey,
|
|
234
|
-
data: pointsWithData.map((p) => p.row),
|
|
235
|
-
dataPoints: pointsWithData.map((p) => ({ x: p.x, y: p.y, datum: p.row })),
|
|
236
|
-
aria,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const bucket = isMuted ? mutedMarks : highlightedMarks;
|
|
240
|
-
bucket.push(lineMark);
|
|
241
|
-
|
|
242
|
-
// Emit PointMark objects when markDef.point is truthy, or when sequential
|
|
243
|
-
// color is active (points carry the gradient since SVG paths are single-color).
|
|
244
|
-
const markPoint = spec.markDef.point;
|
|
245
|
-
const isSingleEndpoint = markPoint === 'last' || markPoint === 'first';
|
|
246
|
-
const showPoints =
|
|
247
|
-
markPoint === true ||
|
|
248
|
-
markPoint === 'transparent' ||
|
|
249
|
-
markPoint === 'endpoints' ||
|
|
250
|
-
isSingleEndpoint ||
|
|
251
|
-
isSequentialColor;
|
|
252
|
-
|
|
253
|
-
if (showPoints) {
|
|
254
|
-
const isTransparent = markPoint === 'transparent';
|
|
255
|
-
const isEndpoints = markPoint === 'endpoints';
|
|
256
|
-
// Also respect per-series showPoints override
|
|
257
|
-
const seriesShowPoints = styleOverride?.showPoints !== false;
|
|
258
|
-
const lastIdx = pointsWithData.length - 1;
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < pointsWithData.length; i++) {
|
|
261
|
-
const p = pointsWithData[i];
|
|
262
|
-
const isEndpoint = i === 0 || i === lastIdx;
|
|
263
|
-
const isLast = i === lastIdx;
|
|
264
|
-
const isFirst = i === 0;
|
|
265
|
-
// Single-endpoint mode ('last' / 'first'): emit only the chosen point.
|
|
266
|
-
// Skip the others entirely instead of emitting invisible placeholders.
|
|
267
|
-
if (isSingleEndpoint) {
|
|
268
|
-
if (markPoint === 'last' && !isLast) continue;
|
|
269
|
-
if (markPoint === 'first' && !isFirst) continue;
|
|
270
|
-
}
|
|
271
|
-
const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
|
|
272
|
-
// Sequential color: each point gets colored by its data value
|
|
273
|
-
let pointColor = color;
|
|
274
|
-
if (isSequentialColor) {
|
|
275
|
-
const val = Number(p.row[sequentialColorField!]);
|
|
276
|
-
pointColor = Number.isFinite(val) ? getSequentialColor(scales, val) : color;
|
|
277
|
-
}
|
|
278
|
-
const hollow = isEndpoints && visible;
|
|
279
|
-
const pointColorStr = getRepresentativeColor(pointColor);
|
|
280
|
-
// 'last' / 'first' render as a tight filled terminator dot in the line
|
|
281
|
-
// color — no white halo, no hollow ring. Marked decorative because the
|
|
282
|
-
// data point already exists on the line and gets described by the a11y
|
|
283
|
-
// data table; the dot is purely visual.
|
|
284
|
-
const pointKey = keyField
|
|
285
|
-
? serializeKeyValue(p.row[keyField])
|
|
286
|
-
: seriesStyleKey
|
|
287
|
-
? `${seriesStyleKey}|${serializeKeyValue(p.row[xChannel.field])}`
|
|
288
|
-
: serializeKeyValue(p.row[xChannel.field]);
|
|
289
|
-
if (isSingleEndpoint) {
|
|
290
|
-
bucket.push({
|
|
291
|
-
type: 'point',
|
|
292
|
-
key: pointKey,
|
|
293
|
-
cx: p.x,
|
|
294
|
-
cy: p.y,
|
|
295
|
-
r: 3.5,
|
|
296
|
-
fill: strokeColor,
|
|
297
|
-
stroke: 'transparent',
|
|
298
|
-
strokeWidth: 0,
|
|
299
|
-
fillOpacity: 1,
|
|
300
|
-
data: p.row,
|
|
301
|
-
aria: { decorative: true },
|
|
302
|
-
});
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
const pointMark: PointMark = {
|
|
306
|
-
type: 'point',
|
|
307
|
-
key: pointKey,
|
|
308
|
-
cx: p.x,
|
|
309
|
-
cy: p.y,
|
|
310
|
-
r: visible ? DEFAULT_POINT_RADIUS : 0,
|
|
311
|
-
fill: hollow ? 'transparent' : pointColorStr,
|
|
312
|
-
stroke: hollow ? pointColorStr : visible ? '#ffffff' : 'transparent',
|
|
313
|
-
strokeWidth: visible ? 1.5 : 0,
|
|
314
|
-
fillOpacity: isTransparent ? 0 : 1,
|
|
315
|
-
data: p.row,
|
|
316
|
-
aria: {
|
|
317
|
-
label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`,
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
bucket.push(pointMark);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const allMarks = [...mutedMarks, ...highlightedMarks];
|
|
326
|
-
|
|
327
|
-
// Dedupe series-level keys across all line marks
|
|
328
|
-
const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
|
|
329
|
-
const lineKeys = dedupeKeys(lineMarks.map((m) => m.key!));
|
|
330
|
-
lineMarks.forEach((m, i) => {
|
|
331
|
-
m.key = lineKeys[i];
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Dedupe point mark keys
|
|
335
|
-
const pointMarks = allMarks.filter((m): m is PointMark => m.type === 'point');
|
|
336
|
-
if (pointMarks.length > 0) {
|
|
337
|
-
const pointKeys = dedupeKeys(pointMarks.map((m) => m.key ?? ''));
|
|
338
|
-
pointMarks.forEach((m, i) => {
|
|
339
|
-
m.key = pointKeys[i];
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return allMarks;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
|
-
// Helpers
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
|
|
350
|
-
/** Get the midpoint numeric value of a field across rows (for sequential line stroke). */
|
|
351
|
-
function _getMidValue(rows: DataRow[], field: string): number {
|
|
352
|
-
const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
|
|
353
|
-
if (values.length === 0) return 0;
|
|
354
|
-
const sorted = values.sort((a, b) => a - b);
|
|
355
|
-
return sorted[Math.floor(sorted.length / 2)];
|
|
356
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Curve interpolation mapping and path builders.
|
|
3
|
-
*
|
|
4
|
-
* Maps Vega-Lite-style interpolation strings to d3-shape curve factories.
|
|
5
|
-
* Used by both line and area chart computations.
|
|
6
|
-
*
|
|
7
|
-
* Also exports pure path-builder helpers (`buildLinePath`, `buildAreaPath`)
|
|
8
|
-
* so downstream consumers (e.g. transition interpolators) can reconstruct
|
|
9
|
-
* SVG paths from point arrays without pulling in chart-specific code.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { MarkDef, Point } from '@opendata-ai/openchart-core';
|
|
13
|
-
import type { CurveFactory } from 'd3-shape';
|
|
14
|
-
import {
|
|
15
|
-
area,
|
|
16
|
-
curveBasis,
|
|
17
|
-
curveCardinal,
|
|
18
|
-
curveLinear,
|
|
19
|
-
curveMonotoneX,
|
|
20
|
-
curveNatural,
|
|
21
|
-
curveStep,
|
|
22
|
-
curveStepAfter,
|
|
23
|
-
curveStepBefore,
|
|
24
|
-
line,
|
|
25
|
-
} from 'd3-shape';
|
|
26
|
-
|
|
27
|
-
/** Map of interpolation string names to d3 curve factories. */
|
|
28
|
-
const CURVE_MAP: Record<NonNullable<MarkDef['interpolate']>, CurveFactory> = {
|
|
29
|
-
linear: curveLinear,
|
|
30
|
-
monotone: curveMonotoneX,
|
|
31
|
-
step: curveStep,
|
|
32
|
-
'step-before': curveStepBefore,
|
|
33
|
-
'step-after': curveStepAfter,
|
|
34
|
-
basis: curveBasis,
|
|
35
|
-
cardinal: curveCardinal,
|
|
36
|
-
natural: curveNatural,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolve an interpolation string to a d3 curve factory.
|
|
41
|
-
* Defaults to `curveMonotoneX` when no interpolation is specified.
|
|
42
|
-
*/
|
|
43
|
-
export function resolveCurve(interpolate?: MarkDef['interpolate']): CurveFactory {
|
|
44
|
-
if (!interpolate) return curveMonotoneX;
|
|
45
|
-
return CURVE_MAP[interpolate] ?? curveMonotoneX;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build an SVG path string from an array of points using d3 line generator.
|
|
50
|
-
* Accepts an optional interpolation string (defaults to 'monotone').
|
|
51
|
-
*/
|
|
52
|
-
export function buildLinePath(points: Point[], interpolate?: string): string {
|
|
53
|
-
if (points.length === 0) return '';
|
|
54
|
-
const curve = resolveCurve(interpolate as MarkDef['interpolate']);
|
|
55
|
-
const generator = line<Point>()
|
|
56
|
-
.x((d) => d.x)
|
|
57
|
-
.y((d) => d.y)
|
|
58
|
-
.curve(curve);
|
|
59
|
-
return generator(points) ?? '';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build an SVG path string for a filled area from top and bottom point arrays.
|
|
64
|
-
* Accepts an optional interpolation string (defaults to 'monotone').
|
|
65
|
-
*/
|
|
66
|
-
export function buildAreaPath(
|
|
67
|
-
topPoints: Point[],
|
|
68
|
-
bottomPoints: Point[],
|
|
69
|
-
interpolate?: string,
|
|
70
|
-
): string {
|
|
71
|
-
if (topPoints.length === 0) return '';
|
|
72
|
-
const curve = resolveCurve(interpolate as MarkDef['interpolate']);
|
|
73
|
-
// Pair top and bottom points for the area generator
|
|
74
|
-
const paired = topPoints.map((tp, i) => ({
|
|
75
|
-
x: tp.x,
|
|
76
|
-
yTop: tp.y,
|
|
77
|
-
yBottom: bottomPoints[i]?.y ?? 0,
|
|
78
|
-
}));
|
|
79
|
-
const generator = area<{ x: number; yTop: number; yBottom: number }>()
|
|
80
|
-
.x((d) => d.x)
|
|
81
|
-
.y0((d) => d.yBottom)
|
|
82
|
-
.y1((d) => d.yTop)
|
|
83
|
-
.curve(curve);
|
|
84
|
-
return generator(paired) ?? '';
|
|
85
|
-
}
|
package/src/charts/line/index.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Line & area chart module.
|
|
3
|
-
*
|
|
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'`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { AreaMark, LineMark, Mark, PointMark } from '@opendata-ai/openchart-core';
|
|
14
|
-
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
15
|
-
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
16
|
-
import type { ChartRenderer } from '../registry';
|
|
17
|
-
import { computeAreaMarks } from './area';
|
|
18
|
-
import { computeLineMarks } from './compute';
|
|
19
|
-
import { computeLineLabels } from './labels';
|
|
20
|
-
|
|
21
|
-
/** Radius for area-chart data points, matching the line renderer. */
|
|
22
|
-
const AREA_POINT_RADIUS = 3;
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Line chart renderer
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Line chart renderer.
|
|
30
|
-
*
|
|
31
|
-
* Computes line marks + point marks for hover targets, then resolves
|
|
32
|
-
* end-of-line labels and attaches them to the corresponding line marks.
|
|
33
|
-
*/
|
|
34
|
-
export const lineRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
35
|
-
const marks = computeLineMarks(spec, scales, chartArea, strategy);
|
|
36
|
-
|
|
37
|
-
// Extract just the line marks for label computation
|
|
38
|
-
const lineMarks = marks.filter((m): m is LineMark => m.type === 'line');
|
|
39
|
-
|
|
40
|
-
// Compute and attach labels to line marks by seriesKey lookup. Passing the
|
|
41
|
-
// spec engages the shared suppression truth table so end-of-line labels
|
|
42
|
-
// hide when the legend or endpoint column is showing.
|
|
43
|
-
const labelMap = computeLineLabels(
|
|
44
|
-
lineMarks,
|
|
45
|
-
strategy,
|
|
46
|
-
spec.labels.density,
|
|
47
|
-
spec.labels.offsets,
|
|
48
|
-
spec,
|
|
49
|
-
);
|
|
50
|
-
for (const mark of marks) {
|
|
51
|
-
if (mark.type === 'line' && mark.seriesKey) {
|
|
52
|
-
const label = labelMap.get(mark.seriesKey);
|
|
53
|
-
if (label) {
|
|
54
|
-
mark.label = label;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return marks as Mark[];
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Area chart renderer
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Area chart renderer.
|
|
68
|
-
*
|
|
69
|
-
* Computes area fill marks (stacked or overlapping per `encoding.y.stack`).
|
|
70
|
-
* Also computes line marks for the top boundary and point marks
|
|
71
|
-
* for hover targets, layered on top of the areas.
|
|
72
|
-
*
|
|
73
|
-
* Lines are derived from area top boundaries whenever there's a color
|
|
74
|
-
* encoding -- this keeps each line glued to its band's top edge regardless
|
|
75
|
-
* of whether the layout is stacked (cumulative tops) or overlap (per-series
|
|
76
|
-
* raw values).
|
|
77
|
-
*/
|
|
78
|
-
export const areaRenderer: ChartRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
79
|
-
const areas = computeAreaMarks(spec, scales, chartArea, theme.isDark);
|
|
80
|
-
|
|
81
|
-
const encoding = spec.encoding;
|
|
82
|
-
const hasColor = !!(encoding.color && 'field' in encoding.color);
|
|
83
|
-
|
|
84
|
-
// With a color encoding (stacked or overlap), derive line marks from the
|
|
85
|
-
// area tops so each line traces the upper edge of its band. For single
|
|
86
|
-
// series, compute lines normally so we get the regular line + point marks.
|
|
87
|
-
const lines = hasColor
|
|
88
|
-
? linesFromAreas(areas)
|
|
89
|
-
: computeLineMarks(spec, scales, chartArea, strategy);
|
|
90
|
-
|
|
91
|
-
// For multi-series areas the lines are derived from area tops, which skips
|
|
92
|
-
// the point-emission path in computeLineMarks. Emit the data-point dots here
|
|
93
|
-
// so `mark.point` works on area charts the same way it does on lines.
|
|
94
|
-
// Single-series areas already get their points from computeLineMarks above.
|
|
95
|
-
const points = hasColor && spec.markDef.point ? pointsFromAreas(areas, spec.markDef.point) : [];
|
|
96
|
-
|
|
97
|
-
// Areas go first (rendered behind lines), then lines, then points on top
|
|
98
|
-
return [...areas, ...lines, ...points] as Mark[];
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Helpers
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Derive LineMark[] from AreaMark[] using each area's top boundary.
|
|
107
|
-
*
|
|
108
|
-
* Works for both stacked and overlap layouts:
|
|
109
|
-
* - Stacked: `topPoints` is the cumulative top edge of the layer, so the line
|
|
110
|
-
* sits on top of its band rather than the raw series value.
|
|
111
|
-
* - Overlap: `topPoints` is the series' own y value (areas all share the same
|
|
112
|
-
* baseline), so the line traces the actual data.
|
|
113
|
-
*
|
|
114
|
-
* No z-order assumption -- each area independently provides its own top.
|
|
115
|
-
*/
|
|
116
|
-
function linesFromAreas(areas: AreaMark[]): LineMark[] {
|
|
117
|
-
return areas.map((a) => ({
|
|
118
|
-
type: 'line' as const,
|
|
119
|
-
points: a.topPoints,
|
|
120
|
-
path: a.topPath,
|
|
121
|
-
stroke: getRepresentativeColor(a.fill),
|
|
122
|
-
strokeWidth: a.strokeWidth ?? 1,
|
|
123
|
-
seriesKey: a.seriesKey,
|
|
124
|
-
data: a.data,
|
|
125
|
-
dataPoints: a.dataPoints,
|
|
126
|
-
aria: { label: `${a.seriesKey ?? 'Series'}: line with ${a.topPoints.length} data points` },
|
|
127
|
-
}));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Derive PointMark[] sitting on each area's top boundary. Honors the same
|
|
132
|
-
* `mark.point` modes as the line renderer: `true` (filled dots), `'transparent'`
|
|
133
|
-
* (invisible hover targets), and `'endpoints'` (hollow dots at first/last only).
|
|
134
|
-
*/
|
|
135
|
-
function pointsFromAreas(
|
|
136
|
-
areas: AreaMark[],
|
|
137
|
-
pointMode: NonNullable<NormalizedChartSpec['markDef']['point']>,
|
|
138
|
-
): PointMark[] {
|
|
139
|
-
const isTransparent = pointMode === 'transparent';
|
|
140
|
-
const isEndpoints = pointMode === 'endpoints';
|
|
141
|
-
const points: PointMark[] = [];
|
|
142
|
-
|
|
143
|
-
for (const a of areas) {
|
|
144
|
-
const stroke = getRepresentativeColor(a.fill);
|
|
145
|
-
const lastIdx = a.topPoints.length - 1;
|
|
146
|
-
|
|
147
|
-
for (let i = 0; i < a.topPoints.length; i++) {
|
|
148
|
-
const pt = a.topPoints[i];
|
|
149
|
-
const isEndpoint = i === 0 || i === lastIdx;
|
|
150
|
-
const visible = !isTransparent && (!isEndpoints || isEndpoint);
|
|
151
|
-
const hollow = isEndpoints && visible;
|
|
152
|
-
points.push({
|
|
153
|
-
type: 'point',
|
|
154
|
-
cx: pt.x,
|
|
155
|
-
cy: pt.y,
|
|
156
|
-
r: visible ? AREA_POINT_RADIUS : 0,
|
|
157
|
-
fill: hollow ? 'transparent' : stroke,
|
|
158
|
-
stroke: hollow ? stroke : visible ? '#ffffff' : 'transparent',
|
|
159
|
-
strokeWidth: visible ? 1.5 : 0,
|
|
160
|
-
fillOpacity: isTransparent ? 0 : 1,
|
|
161
|
-
data: a.data[i] ?? {},
|
|
162
|
-
aria: { decorative: true },
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return points;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// Public exports
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
export { computeAreaMarks } from './area';
|
|
175
|
-
export { computeLineMarks } from './compute';
|
|
176
|
-
export { computeLineLabels } from './labels';
|