@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21

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 (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,561 +0,0 @@
1
- /**
2
- * Mark rendering: dispatches to per-mark-type sub-renderers (line, area, rect,
3
- * arc, point, text, rule, tick).
4
- *
5
- * Mark renderers read module-level animation and gradient state so their
6
- * signatures stay `(mark, index) => SVGElement`. Callers must invoke
7
- * `setMarkRenderState()` before `renderMarks()` and `resetMarkRenderState()`
8
- * after.
9
- */
10
-
11
- import type {
12
- ArcMark,
13
- AreaMark,
14
- ChartLayout,
15
- LineMark,
16
- Mark,
17
- PointMark,
18
- RectMark,
19
- ResolvedAnimation,
20
- ResolvedFillPattern,
21
- RuleMarkLayout,
22
- TextMarkLayout,
23
- TickMarkLayout,
24
- } from '@opendata-ai/openchart-core';
25
- import { resolveMarkFill } from '../gradient-utils';
26
- import { resolvePatternFill } from '../pattern-utils';
27
- import { applyTextStyle, createSVGElement, setAttrs } from './svg-dom';
28
-
29
- /**
30
- * Module-level animation state. Set by the orchestrator before rendering marks
31
- * so mark renderers can read it without changing their function signatures.
32
- */
33
- let currentAnimation: ResolvedAnimation | undefined;
34
-
35
- /**
36
- * Module-level gradient map. Set by the orchestrator after building gradient defs
37
- * so mark renderers can resolve gradient fills without signature changes.
38
- */
39
- let currentGradientMap: Map<string, string> = new Map();
40
-
41
- /**
42
- * Module-level pattern map. Same lifecycle as the gradient map, built from
43
- * pattern defs (see pattern-utils.ts).
44
- */
45
- let currentPatternMap: Map<string, string> = new Map();
46
-
47
- /** Set animation + gradient + pattern state before rendering marks. */
48
- export function setMarkRenderState(state: {
49
- animation: ResolvedAnimation | undefined;
50
- gradientMap: Map<string, string>;
51
- patternMap?: Map<string, string>;
52
- }): void {
53
- currentAnimation = state.animation;
54
- currentGradientMap = state.gradientMap;
55
- currentPatternMap = state.patternMap ?? new Map();
56
- }
57
-
58
- /** Reset animation + gradient + pattern state after rendering. */
59
- export function resetMarkRenderState(): void {
60
- currentAnimation = undefined;
61
- currentGradientMap = new Map();
62
- currentPatternMap = new Map();
63
- }
64
-
65
- /**
66
- * Resolve a filled mark's paint: the pattern url when the engine assigned a
67
- * fill pattern, the plain/gradient fill otherwise.
68
- */
69
- function resolveFillOrPattern(
70
- fill: Parameters<typeof resolveMarkFill>[0],
71
- pattern: ResolvedFillPattern | undefined,
72
- ): string {
73
- if (pattern) return resolvePatternFill(pattern, currentPatternMap);
74
- return resolveMarkFill(fill, currentGradientMap);
75
- }
76
-
77
- /**
78
- * Stamp animation index attributes on a mark element when animation is enabled.
79
- * Sets `data-animation-index` (for querySelector) and `--oc-mark-index`
80
- * (for CSS calc-based stagger delay).
81
- */
82
- function stampAnimationAttrs(
83
- el: SVGElement,
84
- mark: { animationIndex?: number; key?: string },
85
- fallbackIndex: number,
86
- ): void {
87
- // Stamp stable identity key for data-update transitions (always, not just enter)
88
- if (mark.key) {
89
- el.setAttribute('data-key', mark.key);
90
- }
91
- if (!currentAnimation?.enter) return;
92
- const idx = mark.animationIndex ?? fallbackIndex;
93
- el.setAttribute('data-animation-index', String(idx));
94
- (el as SVGElement & ElementCSSInlineStyle).style.setProperty('--oc-mark-index', String(idx));
95
- }
96
-
97
- type MarkRenderer<T extends Mark> = (mark: T, index: number) => SVGElement;
98
-
99
- const markRenderers: Record<string, MarkRenderer<Mark>> = {};
100
-
101
- /**
102
- * Register a mark renderer for a specific mark type.
103
- * Built-in renderers are registered below for all chart types.
104
- */
105
- export function registerMarkRenderer<T extends Mark>(
106
- type: T['type'],
107
- renderer: MarkRenderer<T>,
108
- ): void {
109
- markRenderers[type] = renderer as MarkRenderer<Mark>;
110
- }
111
-
112
- function renderLineMark(mark: LineMark, index: number): SVGElement {
113
- const g = createSVGElement('g');
114
- g.setAttribute('data-mark-id', `line-${mark.seriesKey ?? index}`);
115
- g.setAttribute('class', 'oc-mark oc-mark-line');
116
- stampAnimationAttrs(g, mark, index);
117
-
118
- if (mark.points.length > 1) {
119
- const path = createSVGElement('path');
120
- // Use the pre-computed D3 curve path when available (smooth monotone),
121
- // otherwise fall back to straight M/L segments.
122
- const d =
123
- mark.path ?? mark.points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
124
- setAttrs(path, {
125
- d,
126
- fill: 'none',
127
- stroke: mark.stroke,
128
- 'stroke-width': mark.strokeWidth,
129
- });
130
- if (mark.strokeDasharray) {
131
- path.setAttribute('stroke-dasharray', mark.strokeDasharray);
132
- }
133
- if (mark.opacity != null) {
134
- path.setAttribute('opacity', String(mark.opacity));
135
- }
136
- // Note: line drawing animation is handled via CSS clip-path on the group,
137
- // no inline dasharray/dashoffset needed.
138
- g.appendChild(path);
139
- }
140
-
141
- // Render end-of-line label if present and visible
142
- if (mark.label?.visible) {
143
- const label = createSVGElement('text');
144
- label.setAttribute('class', 'oc-mark-label');
145
- if (mark.seriesKey) {
146
- label.setAttribute('data-series', mark.seriesKey);
147
- }
148
- setAttrs(label, { x: mark.label.x, y: mark.label.y });
149
- applyTextStyle(label, mark.label.style);
150
- label.textContent = mark.label.text;
151
- g.appendChild(label);
152
-
153
- // Render connector line if label was offset from anchor
154
- if (mark.label.connector) {
155
- const connector = createSVGElement('line');
156
- connector.setAttribute('class', 'oc-mark-connector');
157
- setAttrs(connector, {
158
- x1: mark.label.connector.from.x,
159
- y1: mark.label.connector.from.y,
160
- x2: mark.label.connector.to.x,
161
- y2: mark.label.connector.to.y,
162
- stroke: mark.label.connector.stroke,
163
- 'stroke-width': 1,
164
- 'stroke-opacity': 0.5,
165
- });
166
- g.appendChild(connector);
167
- }
168
- }
169
-
170
- return g;
171
- }
172
-
173
- function renderAreaMark(mark: AreaMark, index: number): SVGElement {
174
- const g = createSVGElement('g');
175
- g.setAttribute('data-mark-id', `area-${mark.seriesKey ?? index}`);
176
- g.setAttribute('class', 'oc-mark oc-mark-area');
177
- stampAnimationAttrs(g, mark, index);
178
-
179
- if (mark.path) {
180
- // Area fill: the full closed shape (top line + baseline), no stroke
181
- const fill = createSVGElement('path');
182
- setAttrs(fill, {
183
- d: mark.path,
184
- fill: resolveFillOrPattern(mark.fill, mark.pattern),
185
- 'fill-opacity': mark.fillOpacity,
186
- stroke: 'none',
187
- });
188
- g.appendChild(fill);
189
-
190
- // Top-line stroke: only along the data points, not the baseline
191
- if (mark.stroke && mark.topPath) {
192
- const strokePath = createSVGElement('path');
193
- strokePath.setAttribute('class', 'oc-area-top');
194
- setAttrs(strokePath, {
195
- d: mark.topPath,
196
- fill: 'none',
197
- stroke: mark.stroke,
198
- 'stroke-width': mark.strokeWidth ?? 1,
199
- });
200
- // Note: area drawing animation is handled via CSS clip-path on the group,
201
- // no inline dasharray/dashoffset needed.
202
- g.appendChild(strokePath);
203
- }
204
- }
205
-
206
- return g;
207
- }
208
-
209
- /**
210
- * Build an SVG path describing a rectangle with selectively rounded corners.
211
- * Used by stacked segments where only the leading edge (top of a vertical
212
- * stack, right of a horizontal stack) should round so the seams between
213
- * adjacent segments stay flush.
214
- */
215
- export function rectPathWithCorners(
216
- mark: RectMark,
217
- sides: NonNullable<RectMark['cornerRadiusSides']>,
218
- ): string {
219
- const { x, y, width: w, height: h } = mark;
220
- // Clamp the radius so it never exceeds half of the shorter side, otherwise
221
- // the arcs would overlap and the path would render as a degenerate shape.
222
- const r = Math.max(0, Math.min(mark.cornerRadius ?? 0, w / 2, h / 2));
223
- const tl = sides.tl ? r : 0;
224
- const tr = sides.tr ? r : 0;
225
- const br = sides.br ? r : 0;
226
- const bl = sides.bl ? r : 0;
227
- return [
228
- `M${x + tl},${y}`,
229
- `H${x + w - tr}`,
230
- tr ? `A${tr},${tr} 0 0 1 ${x + w},${y + tr}` : '',
231
- `V${y + h - br}`,
232
- br ? `A${br},${br} 0 0 1 ${x + w - br},${y + h}` : '',
233
- `H${x + bl}`,
234
- bl ? `A${bl},${bl} 0 0 1 ${x},${y + h - bl}` : '',
235
- `V${y + tl}`,
236
- tl ? `A${tl},${tl} 0 0 1 ${x + tl},${y}` : '',
237
- 'Z',
238
- ]
239
- .filter(Boolean)
240
- .join(' ');
241
- }
242
-
243
- function renderRectMark(mark: RectMark, index: number): SVGElement {
244
- const g = createSVGElement('g');
245
- g.setAttribute('data-mark-id', `rect-${index}`);
246
- g.setAttribute('class', 'oc-mark oc-mark-rect');
247
- stampAnimationAttrs(g, mark, index);
248
- // Use engine-provided orientation for animation direction
249
- if (currentAnimation?.enter && mark.orient === 'horizontal') {
250
- g.setAttribute('data-orient', 'horizontal');
251
- }
252
-
253
- // When `cornerRadiusSides` selects a subset of corners (e.g. top-only for
254
- // the topmost segment of a stacked column), SVG's `rx`/`ry` won't do —
255
- // it rounds all four corners or none. Emit a `<path>` with per-corner
256
- // arcs in that case so the stacked segments stay flush at the seam.
257
- const sides = mark.cornerRadiusSides;
258
- const partialCorners =
259
- !!sides && (!sides.tl || !sides.tr || !sides.br || !sides.bl) && !!mark.cornerRadius;
260
- const shapeEl = partialCorners ? createSVGElement('path') : createSVGElement('rect');
261
- if (partialCorners) {
262
- shapeEl.setAttribute('d', rectPathWithCorners(mark, sides));
263
- } else {
264
- setAttrs(shapeEl, {
265
- x: mark.x,
266
- y: mark.y,
267
- width: mark.width,
268
- height: mark.height,
269
- });
270
- if (mark.cornerRadius) {
271
- setAttrs(shapeEl, { rx: mark.cornerRadius, ry: mark.cornerRadius });
272
- }
273
- }
274
- shapeEl.setAttribute('fill', resolveFillOrPattern(mark.fill, mark.pattern));
275
- if (mark.stroke) {
276
- shapeEl.setAttribute('stroke', mark.stroke);
277
- }
278
- if (mark.strokeWidth) {
279
- shapeEl.setAttribute('stroke-width', String(mark.strokeWidth));
280
- }
281
- g.appendChild(shapeEl);
282
-
283
- // Labels for rect marks are rendered in a dedicated overlay group by
284
- // renderMarks() so they always paint above all bars in SVG z-order.
285
- // See the two-pass logic in renderMarks() below.
286
-
287
- return g;
288
- }
289
-
290
- function renderArcMark(mark: ArcMark, index: number): SVGElement {
291
- const g = createSVGElement('g');
292
- g.setAttribute('data-mark-id', `arc-${index}`);
293
- g.setAttribute('class', 'oc-mark oc-mark-arc');
294
- g.setAttribute('transform', `translate(${mark.center.x},${mark.center.y})`);
295
- stampAnimationAttrs(g, mark, index);
296
-
297
- const path = createSVGElement('path');
298
- setAttrs(path, {
299
- d: mark.path,
300
- fill: resolveFillOrPattern(mark.fill, mark.pattern),
301
- stroke: mark.stroke,
302
- 'stroke-width': mark.strokeWidth,
303
- });
304
- g.appendChild(path);
305
-
306
- // Render label if present and visible
307
- if (mark.label?.visible) {
308
- const label = createSVGElement('text');
309
- label.setAttribute('class', 'oc-mark-label');
310
- // Label position is in absolute coords, but we're in a translated group,
311
- // so subtract the center offset
312
- setAttrs(label, {
313
- x: mark.label.x - mark.center.x,
314
- y: mark.label.y - mark.center.y,
315
- });
316
- applyTextStyle(label, mark.label.style);
317
- label.textContent = mark.label.text;
318
- g.appendChild(label);
319
- }
320
-
321
- return g;
322
- }
323
-
324
- function renderPointMark(mark: PointMark, index: number): SVGElement {
325
- const circle = createSVGElement('circle');
326
- circle.setAttribute('data-mark-id', `point-${index}`);
327
- circle.setAttribute('class', 'oc-mark oc-mark-point');
328
- stampAnimationAttrs(circle, mark, index);
329
-
330
- setAttrs(circle, {
331
- cx: mark.cx,
332
- cy: mark.cy,
333
- r: mark.r,
334
- fill: resolveMarkFill(mark.fill, currentGradientMap),
335
- stroke: mark.stroke,
336
- 'stroke-width': mark.strokeWidth,
337
- });
338
- if (mark.fillOpacity !== undefined) {
339
- circle.setAttribute('fill-opacity', String(mark.fillOpacity));
340
- }
341
- return circle;
342
- }
343
-
344
- function renderTextMark(mark: TextMarkLayout, index: number): SVGElement {
345
- const text = createSVGElement('text');
346
- text.setAttribute('data-mark-id', `textMark-${index}`);
347
- text.setAttribute('class', 'oc-mark oc-mark-text');
348
- stampAnimationAttrs(text, mark, index);
349
-
350
- setAttrs(text, {
351
- x: mark.x,
352
- y: mark.y,
353
- 'font-size': mark.fontSize,
354
- 'text-anchor': mark.textAnchor,
355
- });
356
- // Only when set: calendar and parliament text marks hand-compute their y
357
- // against the default alphabetic baseline, so stamping one would shift them.
358
- if (mark.dominantBaseline) {
359
- text.setAttribute('dominant-baseline', mark.dominantBaseline);
360
- }
361
- (text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', mark.fill);
362
- if (mark.fontWeight) {
363
- text.setAttribute('font-weight', String(mark.fontWeight));
364
- }
365
- if (mark.fontFamily) {
366
- text.setAttribute('font-family', mark.fontFamily);
367
- }
368
- if (mark.angle) {
369
- text.setAttribute('transform', `rotate(${mark.angle}, ${mark.x}, ${mark.y})`);
370
- }
371
- text.textContent = mark.text;
372
- return text;
373
- }
374
-
375
- function renderRuleMark(mark: RuleMarkLayout, index: number): SVGElement {
376
- const line = createSVGElement('line');
377
- line.setAttribute('data-mark-id', `rule-${index}`);
378
- line.setAttribute('class', 'oc-mark oc-mark-rule');
379
- stampAnimationAttrs(line, mark, index);
380
-
381
- setAttrs(line, {
382
- x1: mark.x1,
383
- y1: mark.y1,
384
- x2: mark.x2,
385
- y2: mark.y2,
386
- stroke: mark.stroke,
387
- 'stroke-width': mark.strokeWidth,
388
- });
389
- if (mark.strokeDasharray) {
390
- line.setAttribute('stroke-dasharray', mark.strokeDasharray);
391
- }
392
- if (mark.opacity != null) {
393
- line.setAttribute('opacity', String(mark.opacity));
394
- }
395
- return line;
396
- }
397
-
398
- function renderTickMark(mark: TickMarkLayout, index: number): SVGElement {
399
- const line = createSVGElement('line');
400
- line.setAttribute('data-mark-id', `tick-${index}`);
401
- line.setAttribute('class', 'oc-mark oc-mark-tick');
402
- stampAnimationAttrs(line, mark, index);
403
-
404
- // Tick is a short line segment centered at (x, y)
405
- const half = mark.length / 2;
406
- if (mark.orient === 'vertical') {
407
- setAttrs(line, {
408
- x1: mark.x,
409
- y1: mark.y - half,
410
- x2: mark.x,
411
- y2: mark.y + half,
412
- stroke: mark.stroke,
413
- 'stroke-width': mark.strokeWidth,
414
- });
415
- } else {
416
- setAttrs(line, {
417
- x1: mark.x - half,
418
- y1: mark.y,
419
- x2: mark.x + half,
420
- y2: mark.y,
421
- stroke: mark.stroke,
422
- 'stroke-width': mark.strokeWidth,
423
- });
424
- }
425
-
426
- if (mark.opacity != null) {
427
- line.setAttribute('opacity', String(mark.opacity));
428
- }
429
- return line;
430
- }
431
-
432
- // Register built-in renderers
433
- registerMarkRenderer('line', renderLineMark as MarkRenderer<Mark>);
434
- registerMarkRenderer('area', renderAreaMark as MarkRenderer<Mark>);
435
- registerMarkRenderer('rect', renderRectMark as MarkRenderer<Mark>);
436
- registerMarkRenderer('arc', renderArcMark as MarkRenderer<Mark>);
437
- registerMarkRenderer('point', renderPointMark as MarkRenderer<Mark>);
438
- registerMarkRenderer('textMark', renderTextMark as MarkRenderer<Mark>);
439
- registerMarkRenderer('rule', renderRuleMark as MarkRenderer<Mark>);
440
- registerMarkRenderer('tick', renderTickMark as MarkRenderer<Mark>);
441
-
442
- /**
443
- * Render a single mark to an SVG element without appending it anywhere.
444
- * Used by the transition module to create ghost elements for exiting marks.
445
- */
446
- export function renderSingleMark(mark: Mark, index: number): SVGElement | undefined {
447
- const renderer = markRenderers[mark.type];
448
- if (!renderer) return undefined;
449
- return renderer(mark, index);
450
- }
451
-
452
- /** Extract series name from a mark for legend toggle matching. */
453
- function getMarkSeries(mark: Mark): string | undefined {
454
- // Line and area marks have an explicit seriesKey
455
- if (mark.type === 'line' || mark.type === 'area') {
456
- return mark.seriesKey;
457
- }
458
- // For arc marks, the category name is the first part of the aria label (before ':')
459
- if (mark.type === 'arc') {
460
- return mark.aria.label?.split(':')[0]?.trim();
461
- }
462
- // For rect/point, the aria label may be "category: value" or "category, group: value".
463
- // The series name is the category part (before the colon).
464
- if (mark.aria?.label) {
465
- const beforeColon = mark.aria.label.split(':')[0]?.trim();
466
- if (beforeColon) return beforeColon;
467
- }
468
- return undefined;
469
- }
470
-
471
- /**
472
- * Render chart marks into `parent`.
473
- *
474
- * Returns an optional overlay group containing rect mark value labels.
475
- * The caller should append this group to the outer SVG (outside any clip path)
476
- * so tall labels above near-full-height bars are not clipped by the chart-area
477
- * clip path that constrains the marks themselves.
478
- */
479
- export function renderMarks(parent: SVGElement, layout: ChartLayout): SVGElement | undefined {
480
- const g = createSVGElement('g');
481
- g.setAttribute('class', 'oc-marks');
482
-
483
- for (let i = 0; i < layout.marks.length; i++) {
484
- const mark = layout.marks[i];
485
- const renderer = markRenderers[mark.type];
486
- if (!renderer) continue;
487
-
488
- const el = renderer(mark, i);
489
- // Decorative marks (e.g. sparkline endpoint dots) are hidden from
490
- // assistive tech because they duplicate an existing data point.
491
- if (mark.aria?.decorative) {
492
- el.setAttribute('aria-hidden', 'true');
493
- } else if (mark.aria?.label) {
494
- el.setAttribute('aria-label', mark.aria.label);
495
- }
496
- // Add data-series attribute for legend toggle matching
497
- const series = getMarkSeries(mark);
498
- if (series) {
499
- el.setAttribute('data-series', series);
500
- }
501
-
502
- // For stacked segments, set stack position for sequential animation chaining.
503
- // stackPos is computed by the engine on RectMark during compilation.
504
- if (currentAnimation?.enter && mark.type === 'rect') {
505
- const rect = mark as RectMark;
506
- if (rect.stackGroup && rect.stackPos !== undefined) {
507
- el.setAttribute('data-stack-pos', String(rect.stackPos));
508
- (el as SVGElement & ElementCSSInlineStyle).style.setProperty(
509
- '--oc-stack-pos',
510
- String(rect.stackPos),
511
- );
512
- }
513
- }
514
-
515
- g.appendChild(el);
516
- }
517
-
518
- parent.appendChild(g);
519
-
520
- // Second pass: render rect mark labels in a dedicated overlay group so they
521
- // always paint above all bars in SVG z-order. On grouped column charts, a
522
- // later-rendered bar can otherwise paint over a label from an adjacent column
523
- // when the label extends into a neighbor's airspace.
524
- //
525
- // Each overlay label gets --oc-mark-index stamped directly on it so the CSS
526
- // animation delay calc (which reads that property) works without inheriting
527
- // from a parent group.
528
- let labelsGroup: SVGElement | undefined;
529
- for (let i = 0; i < layout.marks.length; i++) {
530
- const mark = layout.marks[i];
531
- if (mark.type !== 'rect') continue;
532
- const rect = mark as RectMark;
533
- if (!rect.label?.visible) continue;
534
-
535
- if (!labelsGroup) {
536
- labelsGroup = createSVGElement('g');
537
- labelsGroup.setAttribute('class', 'oc-mark-labels');
538
- }
539
-
540
- const label = createSVGElement('text');
541
- label.setAttribute('class', 'oc-mark-label');
542
- setAttrs(label, { x: rect.label.x, y: rect.label.y });
543
- applyTextStyle(label, rect.label.style);
544
- label.textContent = rect.label.text;
545
-
546
- // Stamp animation index so the CSS stagger delay works for overlay labels
547
- // (they're not children of a .oc-mark-rect group that carries --oc-mark-index).
548
- if (currentAnimation?.enter) {
549
- const idx = rect.animationIndex ?? i;
550
- label.setAttribute('data-animation-index', String(idx));
551
- (label as SVGElement & ElementCSSInlineStyle).style.setProperty(
552
- '--oc-mark-index',
553
- String(idx),
554
- );
555
- }
556
-
557
- labelsGroup.appendChild(label);
558
- }
559
-
560
- return labelsGroup;
561
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * KPI metric bar rendering. Emits a `<g class="oc-metrics">` group containing
3
- * a label/value pair per cell. Visual styling lives in `chrome.css`
4
- * (`.oc-metric-*` classes); this renderer only sets positions and structure.
5
- */
6
-
7
- import type { ChartLayout } from '@opendata-ai/openchart-core';
8
- import { createSVGElement, setAttrs } from './svg-dom';
9
-
10
- // Delta and secondary text run at a fraction of the primary value size. This
11
- // preserves the editorial mock's proportion (12px delta against a 22px value)
12
- // at any theme-driven value size, so scaling metricValue scales the whole cell.
13
- const DELTA_SIZE_RATIO = 12 / 22;
14
-
15
- export function renderMetrics(parent: SVGElement, layout: ChartLayout): void {
16
- const bar = layout.metrics;
17
- if (!bar || bar.cells.length === 0) return;
18
-
19
- // Font sizes flow from the theme. Set them inline so they override the CSS
20
- // defaults (.oc-metric-* in chrome.css) when a chart customizes the sizes.
21
- const labelSize = layout.theme.fonts.sizes.metricLabel;
22
- const valueSize = layout.theme.fonts.sizes.metricValue;
23
- const deltaSize = Math.round(valueSize * DELTA_SIZE_RATIO);
24
-
25
- const g = createSVGElement('g');
26
- g.setAttribute('class', 'oc-metrics');
27
-
28
- for (const cell of bar.cells) {
29
- const label = createSVGElement('text');
30
- label.setAttribute('class', 'oc-metric-label');
31
- setAttrs(label, { x: cell.x, y: cell.labelY, 'font-size': labelSize });
32
- label.textContent = cell.metric.label.toUpperCase();
33
- g.appendChild(label);
34
-
35
- const value = createSVGElement('text');
36
- value.setAttribute('class', 'oc-metric-value');
37
- setAttrs(value, { x: cell.x, y: cell.valueY, 'font-size': valueSize });
38
- value.textContent = cell.metric.value;
39
-
40
- if (cell.metric.delta) {
41
- const delta = createSVGElement('tspan');
42
- const tone = cell.metric.deltaTone ?? 'up';
43
- delta.setAttribute('class', tone === 'down' ? 'oc-metric-delta-down' : 'oc-metric-delta-up');
44
- delta.setAttribute('dx', '8');
45
- delta.setAttribute('font-size', String(deltaSize));
46
- delta.textContent = cell.metric.delta;
47
- value.appendChild(delta);
48
- }
49
-
50
- if (cell.metric.secondary) {
51
- const secondary = createSVGElement('tspan');
52
- secondary.setAttribute('class', 'oc-metric-secondary');
53
- secondary.setAttribute('dx', '6');
54
- secondary.setAttribute('font-size', String(deltaSize));
55
- secondary.textContent = cell.metric.secondary;
56
- value.appendChild(secondary);
57
- }
58
-
59
- g.appendChild(value);
60
- }
61
-
62
- parent.appendChild(g);
63
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Shared SVG DOM helpers used across the per-concern renderers.
3
- *
4
- * Pure, stateless utilities. No layout/theme knowledge.
5
- */
6
-
7
- import type { TextStyle } from '@opendata-ai/openchart-core';
8
-
9
- export const SVG_NS = 'http://www.w3.org/2000/svg';
10
- export const XLINK_NS = 'http://www.w3.org/1999/xlink';
11
-
12
- export function createSVGElement(tag: string): SVGElement {
13
- return document.createElementNS(SVG_NS, tag);
14
- }
15
-
16
- export function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {
17
- for (const [key, value] of Object.entries(attrs)) {
18
- el.setAttribute(key, String(value));
19
- }
20
- }
21
-
22
- export function applyTextStyle(el: SVGElement, style: TextStyle): void {
23
- // Use inline styles so engine-computed values take priority over CSS class
24
- // defaults (e.g. .oc-title { font-size: var(--oc-title-size) } would otherwise
25
- // override the responsive scaling applied by the chrome layout).
26
- const inline = (el as SVGElement & ElementCSSInlineStyle).style;
27
- inline.setProperty('fill', style.fill);
28
- inline.setProperty('font-size', `${style.fontSize}px`);
29
- inline.setProperty('font-weight', String(style.fontWeight));
30
- inline.setProperty('font-family', style.fontFamily);
31
- if (style.textAnchor) {
32
- el.setAttribute('text-anchor', style.textAnchor);
33
- }
34
- if (style.dominantBaseline) {
35
- el.setAttribute('dominant-baseline', style.dominantBaseline);
36
- }
37
- if (style.fontVariant) {
38
- el.setAttribute('font-variant', style.fontVariant);
39
- }
40
- }