@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/index.d.ts +175 -12
  2. package/dist/index.js +917 -168
  3. package/dist/index.js.map +1 -1
  4. package/package.json +3 -4
  5. package/src/__test-fixtures__/specs.ts +0 -182
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
  7. package/src/__tests__/annotation-behavior.test.ts +0 -852
  8. package/src/__tests__/axes.test.ts +0 -1579
  9. package/src/__tests__/compile-animation.test.ts +0 -161
  10. package/src/__tests__/compile-chart.test.ts +0 -1303
  11. package/src/__tests__/compile-footnotes.test.ts +0 -277
  12. package/src/__tests__/compile-layer.test.ts +0 -969
  13. package/src/__tests__/compile-snapshot.test.ts +0 -156
  14. package/src/__tests__/compound-labels.test.ts +0 -147
  15. package/src/__tests__/dimensions.test.ts +0 -918
  16. package/src/__tests__/encoding-sugar.test.ts +0 -390
  17. package/src/__tests__/facet.test.ts +0 -215
  18. package/src/__tests__/helpers/invariants.ts +0 -299
  19. package/src/__tests__/known-bugs.test.ts +0 -566
  20. package/src/__tests__/layout-fuzz.test.ts +0 -358
  21. package/src/__tests__/layout-invariants.test.ts +0 -535
  22. package/src/__tests__/legend.test.ts +0 -721
  23. package/src/__tests__/mark-keys.test.ts +0 -294
  24. package/src/__tests__/ranking-recipes.test.ts +0 -260
  25. package/src/__tests__/rotation.test.ts +0 -83
  26. package/src/__tests__/scales.test.ts +0 -323
  27. package/src/__tests__/series-search.test.ts +0 -92
  28. package/src/__tests__/spec-sugar.test.ts +0 -801
  29. package/src/__tests__/you-draw-it.test.ts +0 -160
  30. package/src/annotations/__tests__/compute.test.ts +0 -1672
  31. package/src/annotations/__tests__/geometry.test.ts +0 -66
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
  33. package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
  34. package/src/annotations/__tests__/placement.test.ts +0 -316
  35. package/src/annotations/__tests__/resolve-text.test.ts +0 -788
  36. package/src/annotations/__tests__/rich-text.test.ts +0 -91
  37. package/src/annotations/__tests__/thinning.test.ts +0 -285
  38. package/src/annotations/collisions.ts +0 -326
  39. package/src/annotations/compute.ts +0 -333
  40. package/src/annotations/constants.ts +0 -162
  41. package/src/annotations/geometry.ts +0 -376
  42. package/src/annotations/move.ts +0 -135
  43. package/src/annotations/placement.ts +0 -636
  44. package/src/annotations/position.ts +0 -131
  45. package/src/annotations/resolve-range.ts +0 -117
  46. package/src/annotations/resolve-refline.ts +0 -157
  47. package/src/annotations/resolve-text.ts +0 -545
  48. package/src/annotations/rich-text.ts +0 -65
  49. package/src/annotations/thinning.ts +0 -190
  50. package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
  51. package/src/barlist/compile-barlist.ts +0 -391
  52. package/src/barlist/types.ts +0 -28
  53. package/src/charts/__tests__/post-process.test.ts +0 -237
  54. package/src/charts/__tests__/registry.test.ts +0 -116
  55. package/src/charts/__tests__/utils.test.ts +0 -195
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
  57. package/src/charts/_shared/density-filter.ts +0 -26
  58. package/src/charts/_shared/format-label-value.ts +0 -16
  59. package/src/charts/bar/__tests__/compute.test.ts +0 -934
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
  61. package/src/charts/bar/__tests__/labels.test.ts +0 -292
  62. package/src/charts/bar/compute.ts +0 -539
  63. package/src/charts/bar/index.ts +0 -60
  64. package/src/charts/bar/labels.ts +0 -311
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
  67. package/src/charts/beeswarm/compute.ts +0 -203
  68. package/src/charts/beeswarm/dodge.ts +0 -155
  69. package/src/charts/beeswarm/index.ts +0 -32
  70. package/src/charts/builtin.ts +0 -75
  71. package/src/charts/calendar/__tests__/compute.test.ts +0 -417
  72. package/src/charts/calendar/compute.ts +0 -319
  73. package/src/charts/calendar/index.ts +0 -32
  74. package/src/charts/column/__tests__/compute.test.ts +0 -540
  75. package/src/charts/column/__tests__/labels.test.ts +0 -179
  76. package/src/charts/column/compute.ts +0 -493
  77. package/src/charts/column/index.ts +0 -53
  78. package/src/charts/column/labels.ts +0 -229
  79. package/src/charts/dot/__tests__/compute.test.ts +0 -380
  80. package/src/charts/dot/__tests__/labels.test.ts +0 -98
  81. package/src/charts/dot/compute.ts +0 -285
  82. package/src/charts/dot/index.ts +0 -62
  83. package/src/charts/dot/labels.ts +0 -121
  84. package/src/charts/line/__tests__/compute.test.ts +0 -1194
  85. package/src/charts/line/__tests__/curves.test.ts +0 -51
  86. package/src/charts/line/__tests__/labels.test.ts +0 -94
  87. package/src/charts/line/__tests__/mark-options.test.ts +0 -175
  88. package/src/charts/line/area.ts +0 -484
  89. package/src/charts/line/compute.ts +0 -356
  90. package/src/charts/line/curves.ts +0 -85
  91. package/src/charts/line/index.ts +0 -176
  92. package/src/charts/line/labels.ts +0 -177
  93. package/src/charts/parliament/__tests__/compute.test.ts +0 -411
  94. package/src/charts/parliament/compute.ts +0 -313
  95. package/src/charts/parliament/index.ts +0 -32
  96. package/src/charts/pie/__tests__/compute.test.ts +0 -384
  97. package/src/charts/pie/__tests__/labels.test.ts +0 -132
  98. package/src/charts/pie/compute.ts +0 -324
  99. package/src/charts/pie/index.ts +0 -49
  100. package/src/charts/pie/labels.ts +0 -143
  101. package/src/charts/post-process.ts +0 -229
  102. package/src/charts/range/__tests__/compute.test.ts +0 -486
  103. package/src/charts/range/compute.ts +0 -273
  104. package/src/charts/range/index.ts +0 -82
  105. package/src/charts/range/labels.ts +0 -169
  106. package/src/charts/rect/__tests__/compute.test.ts +0 -120
  107. package/src/charts/rect/index.ts +0 -127
  108. package/src/charts/registry.ts +0 -72
  109. package/src/charts/rule/index.ts +0 -139
  110. package/src/charts/scatter/__tests__/compute.test.ts +0 -488
  111. package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
  112. package/src/charts/scatter/compute.ts +0 -192
  113. package/src/charts/scatter/index.ts +0 -46
  114. package/src/charts/scatter/trendline.ts +0 -100
  115. package/src/charts/text/__tests__/compute.test.ts +0 -307
  116. package/src/charts/text/index.ts +0 -141
  117. package/src/charts/tick/__tests__/compute.test.ts +0 -75
  118. package/src/charts/tick/index.ts +0 -99
  119. package/src/charts/utils.ts +0 -170
  120. package/src/charts/waffle/__tests__/compute.test.ts +0 -305
  121. package/src/charts/waffle/compute.ts +0 -197
  122. package/src/charts/waffle/index.ts +0 -31
  123. package/src/compile/__tests__/color-scale-range.test.ts +0 -158
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
  125. package/src/compile/__tests__/data-clip.test.ts +0 -127
  126. package/src/compile/__tests__/fill-patterns.test.ts +0 -154
  127. package/src/compile/__tests__/size-scale.test.ts +0 -108
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
  129. package/src/compile/color-scale-range.ts +0 -101
  130. package/src/compile/data-clip.ts +0 -48
  131. package/src/compile/fill-patterns.ts +0 -74
  132. package/src/compile/layer.ts +0 -630
  133. package/src/compile/size-scale.ts +0 -141
  134. package/src/compile/spec-sugar.ts +0 -758
  135. package/src/compile/watermark-obstacle.ts +0 -54
  136. package/src/compile/you-draw-it.ts +0 -123
  137. package/src/compile.ts +0 -1503
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
  139. package/src/compiler/__tests__/animation.test.ts +0 -177
  140. package/src/compiler/__tests__/compile.test.ts +0 -87
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
  160. package/src/compiler/__tests__/keys.test.ts +0 -83
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
  162. package/src/compiler/__tests__/normalize.test.ts +0 -399
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
  165. package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
  167. package/src/compiler/__tests__/validate.test.ts +0 -629
  168. package/src/compiler/__tests__/vl-parity.test.ts +0 -68
  169. package/src/compiler/a11y-warnings.ts +0 -110
  170. package/src/compiler/animation.ts +0 -142
  171. package/src/compiler/index.ts +0 -47
  172. package/src/compiler/keys.ts +0 -55
  173. package/src/compiler/normalize.ts +0 -734
  174. package/src/compiler/sparkline-defaults.ts +0 -138
  175. package/src/compiler/types.ts +0 -241
  176. package/src/compiler/validate.ts +0 -2041
  177. package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
  178. package/src/endpoint-labels/compute.ts +0 -621
  179. package/src/endpoint-labels/constants.ts +0 -54
  180. package/src/endpoint-labels/format.ts +0 -60
  181. package/src/endpoint-labels/predict.ts +0 -139
  182. package/src/format/__tests__/field-format.test.ts +0 -164
  183. package/src/format/field-format.ts +0 -27
  184. package/src/geo/__tests__/compile-map.test.ts +0 -1128
  185. package/src/geo/compile-map.ts +0 -1376
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -65
  188. package/src/geo/types.ts +0 -26
  189. package/src/graphs/__tests__/community.test.ts +0 -228
  190. package/src/graphs/__tests__/compile-graph.test.ts +0 -396
  191. package/src/graphs/__tests__/encoding.test.ts +0 -415
  192. package/src/graphs/community.ts +0 -92
  193. package/src/graphs/compile-graph.ts +0 -329
  194. package/src/graphs/encoding.ts +0 -356
  195. package/src/graphs/types.ts +0 -106
  196. package/src/index.ts +0 -177
  197. package/src/layout/__tests__/metrics.test.ts +0 -97
  198. package/src/layout/__tests__/scales-highlight.test.ts +0 -192
  199. package/src/layout/axes/rotation.ts +0 -152
  200. package/src/layout/axes/thinning.ts +0 -98
  201. package/src/layout/axes/ticks.ts +0 -409
  202. package/src/layout/axes.ts +0 -757
  203. package/src/layout/dimensions.ts +0 -912
  204. package/src/layout/facet.ts +0 -91
  205. package/src/layout/gridlines.ts +0 -84
  206. package/src/layout/metrics.ts +0 -132
  207. package/src/layout/plan.ts +0 -631
  208. package/src/layout/scales.ts +0 -1053
  209. package/src/layout/shared.ts +0 -44
  210. package/src/legend/__tests__/continuous.test.ts +0 -318
  211. package/src/legend/__tests__/redundancy.test.ts +0 -108
  212. package/src/legend/__tests__/size.test.ts +0 -348
  213. package/src/legend/__tests__/suppression.test.ts +0 -382
  214. package/src/legend/compute.ts +0 -748
  215. package/src/legend/continuous.ts +0 -327
  216. package/src/legend/size.ts +0 -407
  217. package/src/legend/suppression.ts +0 -226
  218. package/src/legend/wrap.ts +0 -117
  219. package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
  220. package/src/sankey/__tests__/layout.test.ts +0 -165
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
  222. package/src/sankey/__tests__/node-sort.test.ts +0 -45
  223. package/src/sankey/compile-sankey.ts +0 -706
  224. package/src/sankey/layout.ts +0 -184
  225. package/src/sankey/types.ts +0 -41
  226. package/src/tables/__tests__/bar-column.test.ts +0 -147
  227. package/src/tables/__tests__/category-colors.test.ts +0 -230
  228. package/src/tables/__tests__/compile-table.test.ts +0 -236
  229. package/src/tables/__tests__/format-cells.test.ts +0 -143
  230. package/src/tables/__tests__/heatmap.test.ts +0 -148
  231. package/src/tables/__tests__/pagination.test.ts +0 -78
  232. package/src/tables/__tests__/search.test.ts +0 -94
  233. package/src/tables/__tests__/sort.test.ts +0 -107
  234. package/src/tables/__tests__/sparkline.test.ts +0 -122
  235. package/src/tables/bar-column.ts +0 -94
  236. package/src/tables/category-colors.ts +0 -78
  237. package/src/tables/compile-table.ts +0 -427
  238. package/src/tables/format-cells.ts +0 -115
  239. package/src/tables/heatmap.ts +0 -141
  240. package/src/tables/pagination.ts +0 -46
  241. package/src/tables/search.ts +0 -66
  242. package/src/tables/sort.ts +0 -69
  243. package/src/tables/sparkline.ts +0 -113
  244. package/src/tables/utils.ts +0 -16
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
  246. package/src/tilemap/compile-tilemap.ts +0 -755
  247. package/src/tilemap/layout.ts +0 -172
  248. package/src/tilemap/types.ts +0 -33
  249. package/src/tooltips/__tests__/compute.test.ts +0 -582
  250. package/src/tooltips/compute.ts +0 -700
  251. package/src/transforms/__tests__/aggregate.test.ts +0 -159
  252. package/src/transforms/__tests__/bin.test.ts +0 -88
  253. package/src/transforms/__tests__/calculate.test.ts +0 -146
  254. package/src/transforms/__tests__/conditional.test.ts +0 -127
  255. package/src/transforms/__tests__/filter-relative.test.ts +0 -202
  256. package/src/transforms/__tests__/filter.test.ts +0 -59
  257. package/src/transforms/__tests__/fold.test.ts +0 -79
  258. package/src/transforms/__tests__/index.test.ts +0 -93
  259. package/src/transforms/__tests__/predicates.test.ts +0 -193
  260. package/src/transforms/__tests__/timeunit.test.ts +0 -129
  261. package/src/transforms/__tests__/window.test.ts +0 -286
  262. package/src/transforms/aggregate.ts +0 -131
  263. package/src/transforms/bin.ts +0 -87
  264. package/src/transforms/calculate.ts +0 -60
  265. package/src/transforms/conditional.ts +0 -46
  266. package/src/transforms/filter.ts +0 -122
  267. package/src/transforms/fold.ts +0 -49
  268. package/src/transforms/index.ts +0 -60
  269. package/src/transforms/predicates.ts +0 -123
  270. package/src/transforms/timeunit.ts +0 -88
  271. 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
- }
@@ -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';