@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.10

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 (59) hide show
  1. package/dist/index.d.ts +1348 -132
  2. package/dist/index.js +556 -48
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +2 -1
  5. package/dist/styles.css.map +1 -0
  6. package/package.json +9 -3
  7. package/schema/chart.schema.json +9086 -0
  8. package/schema/table.schema.json +861 -0
  9. package/schema/vizspec.schema.json +17922 -0
  10. package/src/accessibility/alt-text.ts +39 -1
  11. package/src/accessibility/aria.ts +4 -2
  12. package/src/colors/__tests__/colorblind.test.ts +12 -0
  13. package/src/colors/colorblind.ts +11 -2
  14. package/src/colors/contrast.ts +11 -0
  15. package/src/colors/index.ts +9 -1
  16. package/src/colors/palettes.ts +47 -0
  17. package/src/helpers/__tests__/deep-merge.test.ts +54 -0
  18. package/src/helpers/__tests__/spec-builders.test.ts +3 -3
  19. package/src/helpers/deep-merge.ts +29 -0
  20. package/src/helpers/spec-builders.ts +7 -7
  21. package/src/index.ts +29 -1
  22. package/src/labels/collision.ts +1 -0
  23. package/src/layout/chrome.ts +24 -0
  24. package/src/layout/index.ts +1 -1
  25. package/src/locale/__tests__/format.test.ts +347 -11
  26. package/src/locale/format.ts +243 -18
  27. package/src/locale/index.ts +10 -1
  28. package/src/responsive/breakpoints.ts +7 -2
  29. package/src/responsive/index.ts +2 -0
  30. package/src/responsive/metrics.ts +77 -10
  31. package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
  32. package/src/schema/__tests__/schema.test.ts +179 -0
  33. package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
  34. package/src/styles/animation.css +211 -168
  35. package/src/styles/base.css +89 -62
  36. package/src/styles/chrome.css +173 -179
  37. package/src/styles/dark.css +38 -35
  38. package/src/styles/graph.css +116 -111
  39. package/src/styles/index.css +5 -0
  40. package/src/styles/keyframes.css +130 -99
  41. package/src/styles/legend.css +14 -12
  42. package/src/styles/reduced-motion.css +61 -40
  43. package/src/styles/series-search.css +153 -0
  44. package/src/styles/sparkline.css +14 -12
  45. package/src/styles/table-animation.css +82 -80
  46. package/src/styles/table.css +314 -302
  47. package/src/styles/token-definitions.ts +199 -0
  48. package/src/styles/tokens.css +132 -128
  49. package/src/styles/tooltip.css +109 -115
  50. package/src/styles/you-draw-it.css +126 -0
  51. package/src/types/__tests__/encoding.test.ts +86 -17
  52. package/src/types/__tests__/spec.test.ts +16 -12
  53. package/src/types/encoding.ts +69 -25
  54. package/src/types/events.ts +107 -13
  55. package/src/types/index.ts +42 -3
  56. package/src/types/layout.ts +484 -11
  57. package/src/types/schema-roots.ts +32 -0
  58. package/src/types/spec.ts +718 -87
  59. package/src/types/table.ts +3 -2
package/src/types/spec.ts CHANGED
@@ -32,6 +32,10 @@ import type { SeriesStrategy, TokenValue } from './theme';
32
32
  * - 'rule': reference lines as data marks
33
33
  * - 'tick': strip/rug plot marks
34
34
  * - 'rect': heatmaps and 2D binned plots
35
+ * - 'range': dumbbell / arrow / range-bar plots spanning two values per category
36
+ * - 'waffle': unit grids ("x of 100") for part-to-whole counts
37
+ * - 'calendar': GitHub-style calendar heatmap (weeks x weekdays, daily dates)
38
+ * - 'parliament': hemicycle seat-dot chart for election/legislature results
35
39
  */
36
40
  export type MarkType =
37
41
  | 'bar'
@@ -44,10 +48,12 @@ export type MarkType =
44
48
  | 'rule'
45
49
  | 'tick'
46
50
  | 'rect'
47
- | 'lollipop';
48
-
49
- /** @deprecated Use MarkType instead. Kept for internal migration references. */
50
- export type ChartType = MarkType;
51
+ | 'lollipop'
52
+ | 'beeswarm'
53
+ | 'range'
54
+ | 'waffle'
55
+ | 'calendar'
56
+ | 'parliament';
51
57
 
52
58
  // ---------------------------------------------------------------------------
53
59
  // Gradient definitions (Vega-aligned)
@@ -174,6 +180,19 @@ export interface MarkDef {
174
180
  innerRadius?: number;
175
181
  /** Outer radius for arc marks. */
176
182
  outerRadius?: number;
183
+ /**
184
+ * Start angle in radians for arc marks. Defaults to 0 (full pie/donut).
185
+ * Combined with `endAngle`, restricts the pie/donut to a partial sweep,
186
+ * e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut
187
+ * (election-style results donut). Angles follow d3's convention: 0 is
188
+ * straight up, increasing clockwise. Only meaningful when `type` is `'arc'`.
189
+ */
190
+ startAngle?: number;
191
+ /**
192
+ * End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut).
193
+ * See `startAngle`. Only meaningful when `type` is `'arc'`.
194
+ */
195
+ endAngle?: number;
177
196
  /** Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness. */
178
197
  cornerRadius?: number | 'pill';
179
198
  /** Fixed bar thickness in pixels for bar/column marks. When set, bars are this height (horizontal) or width (vertical), centered within the band. */
@@ -200,6 +219,99 @@ export interface MarkDef {
200
219
  * two competing diagonals.
201
220
  */
202
221
  trendline?: boolean;
222
+ /**
223
+ * Horizontal pixel offset from the data anchor. Only meaningful when `type`
224
+ * is `'text'`. Use it to lift a label clear of the mark it annotates instead
225
+ * of shifting the underlying data value.
226
+ */
227
+ dx?: number;
228
+ /**
229
+ * Vertical pixel offset from the data anchor (negative = up). Only meaningful
230
+ * when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten
231
+ * pixels above its point.
232
+ */
233
+ dy?: number;
234
+ /** Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`. */
235
+ align?: 'left' | 'center' | 'right';
236
+ /** Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`. */
237
+ baseline?: 'top' | 'middle' | 'bottom';
238
+ /**
239
+ * Font size in pixels for text marks. Ignored when the `size` encoding channel
240
+ * is present (which scales font size from a data field). `size` is already
241
+ * taken by bar thickness, hence the distinct name.
242
+ */
243
+ fontSize?: number;
244
+ /**
245
+ * Pattern-fill reinforcement for filled marks (bar, area, arc).
246
+ *
247
+ * - `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots,
248
+ * crosshatch, vertical lines) layered over each series color so series
249
+ * remain distinguishable without color vision. Marks too small to show
250
+ * a legible pattern keep their solid fill.
251
+ * - `'none'` (default): solid fills only.
252
+ */
253
+ fillPattern?: 'auto' | 'none';
254
+ /**
255
+ * Visual style for range marks. Only meaningful when `type` is `'range'`.
256
+ * - 'dumbbell' (default): a dot at each end joined by a connector line.
257
+ * The start dot is muted, the end dot carries the accent color.
258
+ * - 'arrow': a line with an arrowhead at the x2/y2 end, the strongest
259
+ * "change between two points in time" form.
260
+ * - 'bar': a plain floating range bar spanning start to end.
261
+ */
262
+ style?: 'dumbbell' | 'arrow' | 'bar';
263
+ /**
264
+ * Color range marks by direction of change. Only meaningful when `type` is
265
+ * `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's
266
+ * `positive` semantic color and decreases use `negative`. A field-based
267
+ * `encoding.color` takes precedence over direction coloring.
268
+ */
269
+ colorByDirection?: boolean;
270
+ /**
271
+ * Total number of cells in a waffle grid. Only meaningful when `type` is
272
+ * `'waffle'`. Category values normalize to this many cells via
273
+ * largest-remainder rounding so the grid always sums exactly. Default 100.
274
+ */
275
+ units?: number;
276
+ /**
277
+ * Number of columns in a waffle grid. Only meaningful when `type` is
278
+ * `'waffle'`. Rows derive from `units / columns`. Default 10.
279
+ */
280
+ columns?: number;
281
+ /**
282
+ * First day of the week for calendar marks. Only meaningful when `type` is
283
+ * `'calendar'`. Controls which weekday occupies the top row of each band.
284
+ * Defaults to 'monday' (ISO week convention).
285
+ */
286
+ weekStart?: 'monday' | 'sunday';
287
+ /**
288
+ * Corner radius in pixels for calendar day cells. Only meaningful when
289
+ * `type` is `'calendar'`. Defaults to 1.
290
+ */
291
+ cellRadius?: number;
292
+ /**
293
+ * Seat layout shape for parliament marks. Only meaningful when `type` is
294
+ * `'parliament'`. `'hemicycle'` (default, and currently the only shape)
295
+ * packs seats into concentric semicircular arcs, party-grouped left to
296
+ * right.
297
+ */
298
+ shape?: 'hemicycle';
299
+ /**
300
+ * Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes
301
+ * dots to fill the available hemicycle rings for the given seat count.
302
+ * Only meaningful when `type` is `'parliament'`.
303
+ */
304
+ seatRadius?: number | 'auto';
305
+ /**
306
+ * The majority-threshold line and "N to win" label on parliament marks. Only
307
+ * meaningful when `type` is `'parliament'`. `true` (default) draws the line at
308
+ * a simple majority (half the seats plus one); `false` hides it. The object
309
+ * form overrides the threshold and label: `{ seats }` sets a custom seat count
310
+ * (e.g. a two-thirds supermajority) and `{ label }` overrides the default
311
+ * "N to win" text. A `seats` value outside `1..totalSeats` warns and falls
312
+ * back to the simple-majority default.
313
+ */
314
+ majorityLine?: boolean | { seats?: number; label?: string };
203
315
  }
204
316
 
205
317
  // ---------------------------------------------------------------------------
@@ -227,7 +339,11 @@ export type AggregateOp =
227
339
  export interface AxisConfig {
228
340
  /** Axis title text. If omitted, the field name is used. */
229
341
  title?: string;
230
- /** Number format string (d3-format). e.g. ",.0f" for comma-separated integers. */
342
+ /**
343
+ * Number format: d3-format string (e.g. ",.0f"), or semantic keyword
344
+ * `'percent'` / `'currency'`. When omitted, compact notation (1k, 2.5M)
345
+ * is applied automatically.
346
+ */
231
347
  format?: string;
232
348
  /** Override tick count. Engine picks a sensible default if omitted. */
233
349
  tickCount?: number;
@@ -271,8 +387,12 @@ export interface AxisConfig {
271
387
 
272
388
  /** Scale configuration for an encoding channel. */
273
389
  export interface ScaleConfig {
274
- /** Explicit domain override. Auto-derived from data if omitted. */
275
- domain?: [number, number] | string[];
390
+ /**
391
+ * Explicit domain override. Auto-derived from data if omitted.
392
+ * Continuous scales take `[min, max]`; threshold scales take the full
393
+ * list of class break values; categorical scales take the value list.
394
+ */
395
+ domain?: number[] | string[];
276
396
  /** Scale type override. Usually inferred from field type. */
277
397
  type?: ScaleType;
278
398
  /** Whether to nice-ify the domain for clean tick values. Defaults to true. */
@@ -283,6 +403,15 @@ export interface ScaleConfig {
283
403
  clip?: boolean;
284
404
  /** Explicit range override. */
285
405
  range?: unknown[];
406
+ /**
407
+ * Named color scheme (Vega-Lite aligned). Resolved against the core palette
408
+ * registry (sequential: blue, green, orange, purple, teal; diverging:
409
+ * redBlue, brownTeal; categorical) during the pre-validation sugar
410
+ * expansion; common VL names (blues, greens, category10, ...) are accepted
411
+ * as aliases. Unknown names fail validation with the supported list.
412
+ * An explicit `range` wins over `scheme`.
413
+ */
414
+ scheme?: string;
286
415
  /** Reverse the range direction. */
287
416
  reverse?: boolean;
288
417
  /** Clamp output to the range. */
@@ -317,6 +446,30 @@ export type ScaleType =
317
446
  | 'quantize'
318
447
  | 'threshold';
319
448
 
449
+ /**
450
+ * Sort a categorical domain by the aggregated values of another field
451
+ * (Vega-Lite EncodingSortField).
452
+ */
453
+ export interface SortByField {
454
+ /** Data field whose values determine the category order. */
455
+ field: string;
456
+ /** Aggregate applied per category before comparing. Defaults to 'sum'. */
457
+ op?: AggregateOp;
458
+ /** Sort direction. Defaults to 'ascending'. */
459
+ order?: 'ascending' | 'descending';
460
+ }
461
+
462
+ /**
463
+ * A constant value definition for an encoding channel (Vega-Lite aligned).
464
+ * The VL idiom for a fixed visual property, e.g. `color: { value: '#1b7fa3' }`.
465
+ * Expanded to the matching mark-level property (fill/stroke, size, opacity)
466
+ * during the pre-validation sugar expansion.
467
+ */
468
+ export interface ValueDef {
469
+ /** The constant value: CSS color string, size number, opacity (0-1), or gradient. */
470
+ value: string | number | boolean | null | GradientDef;
471
+ }
472
+
320
473
  /**
321
474
  * A single encoding channel mapping a data field to a visual property.
322
475
  *
@@ -348,8 +501,11 @@ export interface EncodingChannel<TData extends DataRow = DataRow> {
348
501
  type: FieldType;
349
502
  /** Optional aggregate to apply before encoding. */
350
503
  aggregate?: AggregateOp;
351
- /** Axis configuration. Set to `false` to suppress axis entirely (no space reserved). */
352
- axis?: AxisConfig | false;
504
+ /**
505
+ * Axis configuration. Set to `false` to suppress axis entirely (no space
506
+ * reserved). `null` is accepted as a VL-aligned alias for `false`.
507
+ */
508
+ axis?: AxisConfig | false | null;
353
509
  /** Scale configuration. */
354
510
  scale?: ScaleConfig;
355
511
  /**
@@ -361,26 +517,28 @@ export interface EncodingChannel<TData extends DataRow = DataRow> {
361
517
  * The string forms are still the canonical input — the boolean shorthand is
362
518
  * normalized to the matching string before reaching layout.
363
519
  *
364
- * - undefined: chart-type default (see below)
520
+ * - undefined: chart-type default (see below) -- stacked for bar/column/area
365
521
  * - true | 'zero': stack from zero baseline
366
522
  * - 'normalize': stack and normalize to fraction of total (0-1 per category)
367
523
  * - 'center': center stacks around zero (streamgraph style)
368
- * - null | false: no stacking -- renders overlap (area) or grouped/dodged (bar)
524
+ * - null | false: no stacking -- renders grouped/dodged (bar) or overlap (area)
369
525
  *
370
526
  * **Defaults differ by chart type:**
371
- * - **Bar/Column**: defaults to grouped (side-by-side). Use `stack: 'zero'` (or `true`) for stacked bars.
372
- * - **Area**: defaults to overlap (v6 breaking change). Use `stack: 'zero'` (or `true`)
373
- * to opt into stacked areas. Each overlapping series renders as a translucent
374
- * gradient band anchored at the y-domain baseline.
527
+ * - **Bar/Column**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use
528
+ * `stack: null` (or `false`) for grouped/side-by-side bars.
529
+ * - **Area**: defaults to stacked (`'zero'`), Vega-Lite aligned. Use
530
+ * `stack: null` (or `false`) to opt into overlap rendering, where each
531
+ * series renders as a translucent gradient band anchored at the
532
+ * y-domain baseline.
375
533
  * - **Line**: stacking is not applied (lines always overlap).
376
534
  *
377
535
  * @example
378
- * // Stacked horizontal bars (opt-in; default is grouped):
379
- * "x": { "field": "revenue", "type": "quantitative", "stack": "zero" }
536
+ * // Grouped horizontal bars (opt-out; default is stacked):
537
+ * "x": { "field": "revenue", "type": "quantitative", "stack": null }
380
538
  *
381
539
  * @example
382
- * // Stacked area (opt-in; default is overlap):
383
- * "y": { "field": "value", "type": "quantitative", "stack": "zero" }
540
+ * // Overlap area (opt-out; default is stacked):
541
+ * "y": { "field": "value", "type": "quantitative", "stack": null }
384
542
  */
385
543
  stack?: boolean | 'zero' | 'normalize' | 'center' | null;
386
544
  /**
@@ -403,8 +561,22 @@ export interface EncodingChannel<TData extends DataRow = DataRow> {
403
561
  * - 'descending': sort domain values descending
404
562
  * - null: use data order (no sorting)
405
563
  * - undefined: ascending (VL default)
564
+ * - 'x' | '-x' | 'y' | '-y': sort by another channel's values ('-' prefix
565
+ * for descending). The VL ranked-bar idiom: `sort: '-y'` orders categories
566
+ * by value, largest first. Resolved into an explicit domain during the
567
+ * pre-validation sugar expansion.
568
+ * - string[]: explicit value order. Listed values come first (in the given
569
+ * order); unlisted data values follow in data order.
570
+ * - SortByField: sort by the aggregated values of another field.
406
571
  */
407
- sort?: 'ascending' | 'descending' | null;
572
+ sort?: 'ascending' | 'descending' | null | 'x' | '-x' | 'y' | '-y' | string[] | SortByField;
573
+ /**
574
+ * Legend configuration for this channel (Vega-Lite aligned).
575
+ * Only meaningful on the `color` channel. `null` hides the legend (the VL
576
+ * idiom for `legend.show = false`); a config object merges into the
577
+ * top-level `legend` (top-level keys win on conflict).
578
+ */
579
+ legend?: LegendConfig | null;
408
580
  /**
409
581
  * Display title override (Vega-Lite aligned).
410
582
  * Used as the label in tooltips instead of the raw field name.
@@ -413,7 +585,8 @@ export interface EncodingChannel<TData extends DataRow = DataRow> {
413
585
  title?: string;
414
586
  /**
415
587
  * Format string for values (Vega-Lite aligned).
416
- * For quantitative fields: d3-format string (e.g. ",.0f", "$,.2f").
588
+ * For quantitative fields: d3-format string (e.g. ",.0f", "$,.2f"),
589
+ * or semantic keyword `'percent'` / `'currency'`.
417
590
  * For temporal fields: d3-time-format string (e.g. "%Y", "%b %d").
418
591
  * Used in tooltips; `axis.format` takes precedence for axis tick labels.
419
592
  */
@@ -475,14 +648,16 @@ export interface Encoding<TData extends DataRow = DataRow> {
475
648
  /**
476
649
  * Color channel. Required for arc marks (determines pie/donut slice coloring).
477
650
  * Optional for all other marks -- used for series differentiation on multi-series charts,
478
- * or heatmap intensity. Accepts a conditional definition to apply colors based on data predicates.
651
+ * or heatmap intensity. Accepts a conditional definition to apply colors based on data predicates,
652
+ * or a bare `{ value }` constant (VL aligned; expanded to the mark-level fill/stroke).
479
653
  */
480
- color?: EncodingChannel<TData> | ConditionalValueDef<TData>;
654
+ color?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;
481
655
  /**
482
656
  * Size channel. Used by point/bubble charts to scale dot area by a quantitative field.
483
- * Accepts a conditional definition to vary size based on data predicates.
657
+ * Accepts a conditional definition to vary size based on data predicates,
658
+ * or a bare `{ value }` constant (VL aligned; expanded to `mark.size`).
484
659
  */
485
- size?: EncodingChannel<TData> | ConditionalValueDef<TData>;
660
+ size?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;
486
661
  /**
487
662
  * Detail channel. Groups data into multiple series without mapping to a visual property.
488
663
  * Useful when you want separate lines per category but don't need the color to differ.
@@ -506,17 +681,14 @@ export interface Encoding<TData extends DataRow = DataRow> {
506
681
  y2?: EncodingChannel<TData>;
507
682
  /**
508
683
  * Data-driven opacity (0-1 range). Accepts a conditional definition to vary opacity
509
- * based on data predicates (e.g., highlight selected points).
684
+ * based on data predicates (e.g., highlight selected points), or a bare `{ value }`
685
+ * constant (VL aligned; expanded to `mark.opacity`).
510
686
  */
511
- opacity?: EncodingChannel<TData> | ConditionalValueDef<TData>;
687
+ opacity?: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef;
512
688
  /**
513
- * Point shape encoding. Valid values: 'circle', 'square', 'diamond', 'triangle-up',
514
- * 'triangle-down', 'cross'. Used on point/scatter marks to differentiate series by shape.
515
- */
516
- shape?: EncodingChannel<TData>;
517
- /**
518
- * Stroke dash pattern encoding. Maps a nominal field to different dash patterns
519
- * on line marks. Useful when color alone doesn't distinguish series well.
689
+ * Stroke dash pattern encoding. Maps a nominal/ordinal field to different
690
+ * dash patterns on line and rule marks. Useful when color alone doesn't
691
+ * distinguish series well.
520
692
  */
521
693
  strokeDash?: EncodingChannel<TData>;
522
694
  /** Rotation angle encoding for point marks. Maps a quantitative field to 0-360 degrees. */
@@ -531,25 +703,15 @@ export interface Encoding<TData extends DataRow = DataRow> {
531
703
  * multi-field tooltips. Independent of the x/y/color encoding.
532
704
  */
533
705
  tooltip?: EncodingChannel<TData> | EncodingChannel<TData>[];
534
- /** Hyperlink encoding. Maps a field containing URLs to clickable marks. */
535
- href?: EncodingChannel<TData>;
536
- /**
537
- * Drawing order. Controls z-order and stacking sort order for bar/area marks.
538
- * Lower values are drawn first (behind higher values).
539
- */
540
- order?: EncodingChannel<TData>;
541
706
  /**
542
- * Angular position for arc marks (pie/donut).
543
- * Optional -- defaults to the `y` channel value when omitted.
544
- * Not used by any other mark type.
707
+ * Angular position (slice value) for arc marks (pie/donut), the canonical
708
+ * value channel in v8. Waffle and parliament marks accept it too (the same
709
+ * part-to-whole value channel). `y` is still accepted on these marks as a
710
+ * deprecated alias for backward compatibility (a compile warning is emitted
711
+ * and `y` is populated from `theta` internally). Not used by any other mark
712
+ * type.
545
713
  */
546
714
  theta?: EncodingChannel<TData>;
547
- /**
548
- * Radial distance from center for arc marks.
549
- * Optional -- only meaningful on donut charts (controls inner radius boundary).
550
- * Not used by any other mark type.
551
- */
552
- radius?: EncodingChannel<TData>;
553
715
  /**
554
716
  * Facet channel: partitions data into a grid of small-multiple panels.
555
717
  * Each unique value of the facet field produces one panel. Panels share
@@ -557,6 +719,20 @@ export interface Encoding<TData extends DataRow = DataRow> {
557
719
  * per-panel axes.
558
720
  */
559
721
  facet?: FacetChannel<TData>;
722
+ /**
723
+ * Row facet channel: partitions data into vertically-stacked panels.
724
+ * Each unique value of the row field produces one panel, arranged in a
725
+ * single column. Shares the x-axis scale by default; y-axis is independent.
726
+ * Mutually exclusive with `facet` and `column`.
727
+ */
728
+ row?: FacetChannel<TData>;
729
+ /**
730
+ * Column facet channel: partitions data into side-by-side panels.
731
+ * Each unique value of the column field produces one panel, arranged in a
732
+ * single row. Shares the y-axis scale by default; x-axis is independent.
733
+ * Mutually exclusive with `facet` and `row`.
734
+ */
735
+ column?: FacetChannel<TData>;
560
736
  }
561
737
 
562
738
  // ---------------------------------------------------------------------------
@@ -682,15 +858,26 @@ export interface AnnotationOffset {
682
858
  /** Anchor direction for annotation label placement relative to the data point. */
683
859
  export type AnnotationAnchor = 'top' | 'bottom' | 'left' | 'right' | 'auto';
684
860
 
685
- /** Style overrides for the dot marker drawn at the connector's data-point endpoint. */
861
+ /** Connector type for annotation labels. */
862
+ export type ConnectorType = 'straight' | 'curve' | 'drop-line';
863
+
864
+ /** Object form of the connector config, allowing arrow control. */
865
+ export interface ConnectorConfig {
866
+ /** Connector style. */
867
+ type: ConnectorType;
868
+ /** Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head. */
869
+ arrow?: boolean;
870
+ }
871
+
872
+ /** Style overrides for the dot marker drawn at the data point. */
686
873
  export interface AnnotationDot {
687
- /** Circle radius in pixels. Default 5. */
874
+ /** Circle radius in pixels. Default 4. */
688
875
  radius?: number;
689
876
  /** Fill color. Defaults to theme background for an "open ring" look. */
690
877
  fill?: string;
691
- /** Stroke color. Defaults to theme text color. */
878
+ /** Stroke color. Defaults to the connector's resolved stroke, so marker and leader read as one system. */
692
879
  stroke?: string;
693
- /** Stroke width in pixels. Default 2. */
880
+ /** Stroke width in pixels. Default 1.5. */
694
881
  strokeWidth?: number;
695
882
  }
696
883
 
@@ -722,7 +909,13 @@ export interface TextAnnotation extends AnnotationBase {
722
909
  x: string | number;
723
910
  /** Y-axis data value or position. */
724
911
  y: string | number;
725
- /** The annotation text. Required for text annotations. */
912
+ /**
913
+ * The annotation text. Required for text annotations.
914
+ *
915
+ * Supports inline `**bold**` spans, so emphasis lands on the key phrase rather
916
+ * than the whole block: `'Inflation peaked at **8.5%**'`. Unmatched `**` (and
917
+ * empty `****`) render literally. `\n` starts a new line.
918
+ */
726
919
  text: string;
727
920
  /** Thinning priority (lower = kept longer at narrow widths). When omitted, spec order is used. */
728
921
  priority?: number;
@@ -730,12 +923,23 @@ export interface TextAnnotation extends AnnotationBase {
730
923
  * Optional muted second-tone text rendered below the primary `text`.
731
924
  * Used for supporting context (e.g. methodology, source). Newlines in
732
925
  * `text` still produce multi-line primary; subtitle is a separate block.
926
+ *
927
+ * Supports the same inline `**bold**` spans as `text`. When a subtitle is
928
+ * present and no `fontWeight` is set, the primary `text` resolves to weight
929
+ * 700 — the lede + context stack.
733
930
  */
734
931
  subtitle?: string;
735
932
  /**
736
- * Optional dot marker drawn at the connector's data-point endpoint.
737
- * `true` enables the default open-ring style. Pass an object to override
738
- * radius, fill, stroke, or strokeWidth.
933
+ * Open-ring marker drawn on the data point itself (not on the pulled-back
934
+ * connector tip).
935
+ *
936
+ * Left unset, a default marker appears whenever a connector is enabled and
937
+ * carries no arrowhead — the leader points, the ring lands. An arrowed
938
+ * connector gets no marker unless you ask for one: the head already marks the
939
+ * spot. `dot: false` is always bare; an explicit `dot` always wins.
940
+ *
941
+ * `true` uses the default style; pass an object to override radius, fill,
942
+ * stroke, or strokeWidth.
739
943
  */
740
944
  dot?: boolean | AnnotationDot;
741
945
  /** Font size override. */
@@ -748,13 +952,19 @@ export interface TextAnnotation extends AnnotationBase {
748
952
  anchor?: AnnotationAnchor;
749
953
  /**
750
954
  * Connector from label to anchor point.
751
- * - `true` (default): straight line
752
- * - `'straight'`: straight line (alias of `true`)
955
+ * - `true` (default): straight line, no arrow
956
+ * - `'straight'`: straight line, no arrow
753
957
  * - `'curve'`: curved arrow with arrowhead
754
- * - `'drop-line'`: vertical line through the data point's x; label sits beside the line and auto-flips to the opposite side if it would overflow the chart area
958
+ * - `'drop-line'`: vertical line through the data point's x
959
+ * - `{ type, arrow? }`: object form for explicit arrow control
755
960
  * - `false`: no connector
961
+ *
962
+ * The connector leaves the text block on the side facing the data point, with
963
+ * a small standoff gap. A leader shorter than 8px is dropped as noise (the
964
+ * marker alone reads better), as is a connector whose target sits inside the
965
+ * text block. Widen `offset` if you want the line back.
756
966
  */
757
- connector?: boolean | 'straight' | 'curve' | 'drop-line';
967
+ connector?: boolean | ConnectorType | ConnectorConfig;
758
968
  /** Per-endpoint offsets for the connector line. Allows fine-tuning where the connector starts and ends. */
759
969
  connectorOffset?: {
760
970
  /** Offset for the label-end of the connector. */
@@ -762,8 +972,14 @@ export interface TextAnnotation extends AnnotationBase {
762
972
  /** Offset for the data-point-end of the connector. */
763
973
  to?: AnnotationOffset;
764
974
  };
765
- /** Background color behind the text. Useful for readability over chart lines. */
766
- background?: string;
975
+ /**
976
+ * Background plate behind the text, for readability over chart lines.
977
+ *
978
+ * `true` uses the theme surface, so the plate follows light/dark mode. A
979
+ * color string sets it explicitly. Prefer `true`: a hardcoded '#ffffff' stays
980
+ * white in dark mode and the text turns light-gray-on-white.
981
+ */
982
+ background?: string | true;
767
983
  /** Whether to show the paint-order stroke halo behind text. Default true. Set false for white text on colored backgrounds. */
768
984
  halo?: boolean;
769
985
  }
@@ -807,7 +1023,14 @@ export interface RangeAnnotation extends AnnotationBase {
807
1023
  * Useful for baselines (zero), targets, or thresholds.
808
1024
  */
809
1025
  export interface RefLineAnnotation extends AnnotationBase {
810
- type: 'refline' | 'rule';
1026
+ /**
1027
+ * Discriminant. `'refline'` is canonical and the only value in the type.
1028
+ * `'rule'` (it collides with the `rule` mark type) is still accepted at
1029
+ * runtime for backward compatibility -- the spec-sugar pass rewrites it to
1030
+ * `'refline'` with a deprecation warning before validation -- but it is not
1031
+ * part of this type. Use `'refline'`.
1032
+ */
1033
+ type: 'refline';
811
1034
  /** X-axis value for a vertical reference line. */
812
1035
  x?: string | number;
813
1036
  /** Y-axis value for a horizontal reference line. */
@@ -1007,7 +1230,11 @@ export interface LabelConfig {
1007
1230
  suffix?: string;
1008
1231
  /** Fixed CSS color for all labels. Overrides the default fill-derived color. */
1009
1232
  color?: string;
1010
- /** Per-series pixel offsets for fine-tuning label positions, keyed by series name. */
1233
+ /**
1234
+ * Per-series pixel offsets for fine-tuning label positions, keyed by series name.
1235
+ * @deprecated Per-series pixel offsets are a workaround for collision bugs
1236
+ * now fixed by the layout engine. Will be removed in a future version.
1237
+ */
1011
1238
  offsets?: Record<string, AnnotationOffset>;
1012
1239
  /** Font size in pixels for bar/column value labels. */
1013
1240
  fontSize?: number;
@@ -1038,6 +1265,46 @@ export interface LegendConfig {
1038
1265
  exclude?: string[];
1039
1266
  }
1040
1267
 
1268
+ /**
1269
+ * Configuration for the searchable series highlight input (`seriesSearch`).
1270
+ * The boolean form (`seriesSearch: true`) uses the defaults.
1271
+ */
1272
+ export interface SeriesSearchConfig {
1273
+ /** Placeholder text for the search input. Defaults to "Find a series". */
1274
+ placeholder?: string;
1275
+ }
1276
+
1277
+ /**
1278
+ * Configuration for the "you draw it" interactive engagement format
1279
+ * (`youDrawIt`). The reader draws their guess of the trend from `from` to the
1280
+ * right edge before the real line reveals. Line marks only, single-series only.
1281
+ */
1282
+ export interface YouDrawItConfig {
1283
+ /**
1284
+ * The x value (matching `encoding.x`'s domain) where drawing starts. Data
1285
+ * before this point renders normally; data at or after it is hidden behind
1286
+ * a hatched "draw here" region until reveal.
1287
+ */
1288
+ from: string | number;
1289
+ /** Prompt text shown in the hatched drawing region. Defaults to "Draw your guess". */
1290
+ prompt?: string;
1291
+ /** Label for the skip-to-reveal button. Defaults to "Show me". */
1292
+ revealLabel?: string;
1293
+ /**
1294
+ * Optional extra comparison line (e.g. "what everyone else guessed"),
1295
+ * supplied by the host app. Rendered in a third, visually distinct style
1296
+ * alongside the reader's guess and the real line after reveal.
1297
+ */
1298
+ comparisonLine?: Array<{ x: string | number; y: number }>;
1299
+ }
1300
+
1301
+ /**
1302
+ * Content composition for an endpoint-label entry: the series name, the
1303
+ * formatted value, or both joined on one line. See
1304
+ * {@link EndpointLabelsConfig.content}.
1305
+ */
1306
+ export type EndpointLabelContent = 'label' | 'value' | 'label value';
1307
+
1041
1308
  /**
1042
1309
  * Configuration for the endpoint labels column rendered at the chart's right edge
1043
1310
  * for multi-series line/area charts. Each entry pairs the series name with its
@@ -1075,6 +1342,20 @@ export interface EndpointLabelsConfig {
1075
1342
  * - `'both'`: labels on both left (leading) and right (trailing) edges.
1076
1343
  */
1077
1344
  ends?: 'end' | 'both';
1345
+ /**
1346
+ * What each entry renders.
1347
+ * - `'label'`: series name only.
1348
+ * - `'value'`: formatted value only.
1349
+ * - `'label value'`: series name and value joined on one line.
1350
+ *
1351
+ * When omitted, entries render the series name with the formatted value on
1352
+ * a second line (the classic endpoint column). Pass an object to set the
1353
+ * leading and trailing columns independently, e.g. the classic slope-chart
1354
+ * look is `{ leading: 'label value', trailing: 'value' }`.
1355
+ */
1356
+ content?:
1357
+ | EndpointLabelContent
1358
+ | { leading?: EndpointLabelContent; trailing?: EndpointLabelContent };
1078
1359
  /** Field to read the displayed value from. Defaults to `encoding.y.field`. */
1079
1360
  valueField?: string;
1080
1361
  /** d3-format string for the value. Defaults to `encoding.y.axis.format`. */
@@ -1250,14 +1531,14 @@ export interface Metric {
1250
1531
 
1251
1532
  /**
1252
1533
  * Encoding for arc marks (pie/donut charts).
1253
- * - `y`: required (quantitative — the slice value)
1534
+ * - `theta`: required (quantitative — the canonical slice value channel)
1254
1535
  * - `color`: required (nominal/ordinal — the category)
1255
- * - `theta`: optional (defaults to `y` channel)
1256
- * - `radius`: optional (donut inner radius)
1536
+ * - `y`: deprecated alias for `theta`, accepted for backward compatibility
1257
1537
  */
1258
1538
  export interface ArcEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1259
- y: EncodingChannel<TData>;
1260
- color: EncodingChannel<TData>;
1539
+ /** Canonical value channel for arc marks (quantitative — the slice value). */
1540
+ theta: EncodingChannel<TData>;
1541
+ color: EncodingChannel<TData> | ConditionalValueDef<TData>;
1261
1542
  }
1262
1543
 
1263
1544
  /**
@@ -1320,6 +1601,67 @@ export interface LollipopEncoding<TData extends DataRow = DataRow> extends Encod
1320
1601
  y: EncodingChannel<TData>;
1321
1602
  }
1322
1603
 
1604
+ /**
1605
+ * Encoding for beeswarm marks (dodged distribution dots).
1606
+ * - Exactly one positional channel is quantitative: the value axis
1607
+ * (x = horizontal swarm, y = vertical swarm).
1608
+ * - The other positional channel is optional (nominal/ordinal): grouped
1609
+ * swarms, one lane per category. The cross axis has no scale; the dodge
1610
+ * layout computes pixel offsets around each lane center.
1611
+ */
1612
+ export interface BeeswarmEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1613
+ x?: EncodingChannel<TData>;
1614
+ y?: EncodingChannel<TData>;
1615
+ }
1616
+
1617
+ /**
1618
+ * Encoding for range marks (dumbbell / arrow / range-bar plots).
1619
+ * - Horizontal (the common editorial form): `y` nominal/ordinal category,
1620
+ * `x` quantitative start, `x2` quantitative end.
1621
+ * - Vertical: `x` nominal/ordinal category, `y` quantitative start,
1622
+ * `y2` quantitative end.
1623
+ */
1624
+ export interface RangeEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1625
+ x: EncodingChannel<TData>;
1626
+ y: EncodingChannel<TData>;
1627
+ }
1628
+
1629
+ /**
1630
+ * Encoding for waffle marks (unit grids for part-to-whole counts).
1631
+ * - `color`: required (nominal/ordinal, the category)
1632
+ * - `theta`: the canonical quantitative share, the same part-to-whole value
1633
+ * channel arc marks use. `y` is a deprecated alias, accepted for backward
1634
+ * compatibility.
1635
+ */
1636
+ export interface WaffleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1637
+ color: EncodingChannel<TData> | ConditionalValueDef<TData>;
1638
+ }
1639
+
1640
+ /**
1641
+ * Encoding for parliament marks (hemicycle seat-dot charts).
1642
+ * - `color`: required (nominal/ordinal, the party)
1643
+ * - `theta`: the canonical quantitative seat count, the same part-to-whole
1644
+ * value channel arc/waffle marks use. `y` is a deprecated alias, accepted
1645
+ * for backward compatibility.
1646
+ */
1647
+ export interface ParliamentEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1648
+ color: EncodingChannel<TData> | ConditionalValueDef<TData>;
1649
+ }
1650
+
1651
+ /**
1652
+ * Encoding for calendar marks (GitHub-style calendar heatmaps).
1653
+ * - `x`: required (temporal, daily dates, one row per day)
1654
+ * - `color`: required (quantitative, the per-day value)
1655
+ *
1656
+ * The calendar computes its own weeks-x-weekdays geometry: no positional
1657
+ * scales or axes. Multi-year data partitions into one stacked band per year,
1658
+ * all bands sharing a single color scale and legend.
1659
+ */
1660
+ export interface CalendarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1661
+ x: EncodingChannel<TData>;
1662
+ color: EncodingChannel<TData>;
1663
+ }
1664
+
1323
1665
  /**
1324
1666
  * Encoding for text marks (data-positioned labels).
1325
1667
  * - `text`: required (the field to render as text)
@@ -1349,6 +1691,33 @@ export interface RectEncoding<TData extends DataRow = DataRow> extends Encoding<
1349
1691
  y: EncodingChannel<TData>;
1350
1692
  }
1351
1693
 
1694
+ // ---------------------------------------------------------------------------
1695
+ // Accessibility configuration
1696
+ // ---------------------------------------------------------------------------
1697
+
1698
+ /**
1699
+ * Author-facing accessibility overrides.
1700
+ *
1701
+ * Everything here is optional; when omitted, openchart auto-generates alt
1702
+ * text, ARIA labels, and a screen-reader data table from the spec and data.
1703
+ */
1704
+ export interface A11yConfig {
1705
+ /**
1706
+ * Alt text announced by screen readers for the chart as a whole. Replaces
1707
+ * the auto-generated description. Also settable via the top-level
1708
+ * `description` field (Vega-Lite convention); this field wins when both
1709
+ * are present.
1710
+ */
1711
+ description?: string;
1712
+ /**
1713
+ * Hide the chart from assistive technology entirely (`aria-hidden`).
1714
+ * Use only when the chart is decorative or the surrounding content
1715
+ * already conveys the same information. Also suppresses the hidden
1716
+ * screen-reader data table and keyboard mark navigation.
1717
+ */
1718
+ hidden?: boolean;
1719
+ }
1720
+
1352
1721
  // ---------------------------------------------------------------------------
1353
1722
  // Shared (non-mark-specific) ChartSpec properties
1354
1723
  // ---------------------------------------------------------------------------
@@ -1360,12 +1729,44 @@ export interface RectEncoding<TData extends DataRow = DataRow> extends Encoding<
1360
1729
  * @internal
1361
1730
  */
1362
1731
  interface BaseChartSpec<TData extends DataRow = DataRow> {
1363
- /** Data array: each element is a row with field values. */
1732
+ /**
1733
+ * Data array: each element is a row with field values.
1734
+ * The Vega-Lite object form `{ values: [...] }` is also accepted at runtime
1735
+ * and unwrapped during the pre-validation sugar expansion. `{ url }` is not
1736
+ * supported (openchart does not fetch data).
1737
+ */
1364
1738
  data: TData[];
1365
1739
  /** Data transforms applied in order before encoding (filter, bin, calculate, timeUnit). */
1366
1740
  transform?: Transform[];
1367
1741
  /** Editorial chrome (title, subtitle, source, etc.). */
1368
1742
  chrome?: Chrome;
1743
+ /**
1744
+ * Top-level title sugar (Vega-Lite aligned). Expanded into `chrome.title`
1745
+ * (and `chrome.subtitle` for the object form). `chrome.title` wins when
1746
+ * both are set.
1747
+ */
1748
+ title?: string | { text: string; subtitle?: string };
1749
+ /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */
1750
+ subtitle?: string;
1751
+ /**
1752
+ * Alt text for the rendered chart (Vega-Lite aligned). Sugar for
1753
+ * `a11y.description`; `a11y.description` wins when both are set.
1754
+ * When absent, alt text is auto-generated from the spec and data.
1755
+ */
1756
+ description?: string;
1757
+ /** Accessibility overrides. See {@link A11yConfig}. */
1758
+ a11y?: A11yConfig;
1759
+ /**
1760
+ * Fixed render width in pixels (Vega-Lite aligned). Overrides the
1761
+ * container-derived width. When both `width` and `height` are set,
1762
+ * `responsive: false` is implied unless `responsive` is set explicitly.
1763
+ * Omit for the default container-driven sizing.
1764
+ */
1765
+ width?: number;
1766
+ /** Fixed render height in pixels (Vega-Lite aligned). See `width`. */
1767
+ height?: number;
1768
+ /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */
1769
+ $schema?: string;
1369
1770
  /**
1370
1771
  * KPI/metric cells rendered as a horizontal row between subtitle and chart
1371
1772
  * area. Each cell shows a label/value pair with optional delta and secondary
@@ -1402,7 +1803,12 @@ interface BaseChartSpec<TData extends DataRow = DataRow> {
1402
1803
  darkMode?: DarkMode;
1403
1804
  /** Whether to show the tryOpenData.ai watermark. Defaults to true. */
1404
1805
  watermark?: boolean;
1405
- /** Series names to hide from rendering. Hidden series remain in the legend but are visually dimmed. */
1806
+ /**
1807
+ * Series names to hide from rendering by default. This is the authored
1808
+ * persistence target for legend visibility. Runtime legend toggles
1809
+ * (legend-toggle edit events) layer on top of this via ephemeral runtime
1810
+ * sets but never modify it.
1811
+ */
1406
1812
  hiddenSeries?: string[];
1407
1813
  /** Per-series visual overrides, keyed by series name (the color field value). */
1408
1814
  seriesStyles?: Record<string, SeriesStyle>;
@@ -1426,6 +1832,22 @@ interface BaseChartSpec<TData extends DataRow = DataRow> {
1426
1832
  * explicitly set.
1427
1833
  */
1428
1834
  crosshair?: boolean;
1835
+ /**
1836
+ * Searchable series highlight ("find your country"). Renders a compact
1837
+ * search input above the chart with typeahead over the color channel's
1838
+ * distinct values. Selecting a value adds it to the highlight set
1839
+ * (multi-select chips, removable); clearing restores the authored
1840
+ * `encoding.color.highlight` baseline. Requires a categorical color
1841
+ * encoding. Mutually exclusive with edit mode (search wins).
1842
+ */
1843
+ seriesSearch?: boolean | SeriesSearchConfig;
1844
+ /**
1845
+ * "You draw it" interactive engagement format: the reader draws their
1846
+ * guess of the trend before the real line reveals. Line marks only,
1847
+ * single-series only. Mutually exclusive with edit mode (validation
1848
+ * warning if both are enabled; youDrawIt is disabled).
1849
+ */
1850
+ youDrawIt?: YouDrawItConfig;
1429
1851
  /**
1430
1852
  * Display mode controlling how much chart chrome is rendered.
1431
1853
  *
@@ -1521,6 +1943,18 @@ export type ChartSpec<TData extends DataRow = DataRow> =
1521
1943
  mark: 'lollipop' | (MarkDef & { type: 'lollipop' });
1522
1944
  encoding: LollipopEncoding<TData>;
1523
1945
  })
1946
+ | (BaseChartSpec<TData> & {
1947
+ mark: 'beeswarm' | (MarkDef & { type: 'beeswarm' });
1948
+ encoding: BeeswarmEncoding<TData>;
1949
+ })
1950
+ | (BaseChartSpec<TData> & {
1951
+ mark: 'range' | (MarkDef & { type: 'range' });
1952
+ encoding: RangeEncoding<TData>;
1953
+ })
1954
+ | (BaseChartSpec<TData> & {
1955
+ mark: 'waffle' | (MarkDef & { type: 'waffle' });
1956
+ encoding: WaffleEncoding<TData>;
1957
+ })
1524
1958
  | (BaseChartSpec<TData> & {
1525
1959
  mark: 'text' | (MarkDef & { type: 'text' });
1526
1960
  encoding: TextEncoding<TData>;
@@ -1536,6 +1970,14 @@ export type ChartSpec<TData extends DataRow = DataRow> =
1536
1970
  | (BaseChartSpec<TData> & {
1537
1971
  mark: 'rule' | (MarkDef & { type: 'rule' });
1538
1972
  encoding: Encoding<TData>;
1973
+ })
1974
+ | (BaseChartSpec<TData> & {
1975
+ mark: 'calendar' | (MarkDef & { type: 'calendar' });
1976
+ encoding: CalendarEncoding<TData>;
1977
+ })
1978
+ | (BaseChartSpec<TData> & {
1979
+ mark: 'parliament' | (MarkDef & { type: 'parliament' });
1980
+ encoding: ParliamentEncoding<TData>;
1539
1981
  });
1540
1982
 
1541
1983
  /**
@@ -1676,7 +2118,26 @@ export interface LayerSpec<TData extends DataRow = DataRow> {
1676
2118
  transform?: Transform[];
1677
2119
  /** Editorial chrome (title, subtitle, source, etc.). */
1678
2120
  chrome?: Chrome;
1679
- /** Annotations on the layered view. */
2121
+ /** Top-level title sugar (VL aligned). Expanded into `chrome.title`; `chrome.title` wins. */
2122
+ title?: string | { text: string; subtitle?: string };
2123
+ /** Top-level subtitle sugar. Expanded into `chrome.subtitle`; `chrome.subtitle` wins. */
2124
+ subtitle?: string;
2125
+ /** Fixed render width in pixels (VL aligned). Overrides the container-derived width. */
2126
+ width?: number;
2127
+ /** Fixed render height in pixels (VL aligned). Overrides the container-derived height. */
2128
+ height?: number;
2129
+ /** Accepted for Vega-Lite compatibility and ignored (with a compile warning). */
2130
+ $schema?: string;
2131
+ /**
2132
+ * Annotations for the layered chart. When present, these are the
2133
+ * annotations rendered and available for editing. When absent, falls back
2134
+ * to the first leaf layer's annotations. Annotations on second and later
2135
+ * leaf layers are not rendered.
2136
+ *
2137
+ * For edit persistence: the `data-annotation-index` stamped on each
2138
+ * rendered annotation maps to this array (or the first leaf's if this is
2139
+ * unset). Place annotations here for reliable index-based edit matching.
2140
+ */
1680
2141
  annotations?: Annotation[];
1681
2142
  /** Label display configuration. `false` disables all labels, `true` uses defaults. */
1682
2143
  labels?: LabelSpec;
@@ -1692,7 +2153,12 @@ export interface LayerSpec<TData extends DataRow = DataRow> {
1692
2153
  watermark?: boolean;
1693
2154
  /** Resolution strategy for shared scales/axes/legends. */
1694
2155
  resolve?: ResolveConfig;
1695
- /** Hidden series names. */
2156
+ /**
2157
+ * Series names to hide from rendering by default. This is the authored
2158
+ * persistence target for legend visibility. Runtime legend toggles
2159
+ * (legend-toggle edit events) layer on top of this via ephemeral runtime
2160
+ * sets but never modify it.
2161
+ */
1696
2162
  hiddenSeries?: string[];
1697
2163
  /** Endpoint labels column, inherited by child layers that don't set their own. */
1698
2164
  endpointLabels?: boolean | EndpointLabelsConfig;
@@ -1774,6 +2240,8 @@ export interface SankeySpec {
1774
2240
  * number (data value 28 renders as "28%"). For currency: "$,.0f" or "$~s".
1775
2241
  * For SI suffixes: "~s" (renders 1000 as "1k"). When not set, values use
1776
2242
  * the default number formatter.
2243
+ * @deprecated Use the encoding channel's `format` field instead. This
2244
+ * property is expanded into the encoding during compilation.
1777
2245
  */
1778
2246
  valueFormat?: string;
1779
2247
  }
@@ -1840,6 +2308,109 @@ export interface TileMapSpec {
1840
2308
  /**
1841
2309
  * d3-format string applied to tile values, legend labels, and tooltips.
1842
2310
  * Examples: ".1f" for one decimal, "$,.0f" for currency, "~s" for SI.
2311
+ * @deprecated Use the encoding channel's `format` field instead. This
2312
+ * property is expanded into the encoding during compilation.
2313
+ */
2314
+ valueFormat?: string;
2315
+ }
2316
+
2317
+ // ---------------------------------------------------------------------------
2318
+ // Map spec (choropleth / symbol maps)
2319
+ // ---------------------------------------------------------------------------
2320
+
2321
+ /** Supported map projections. */
2322
+ export type MapProjection = 'albersUsa' | 'mercator' | 'equalEarth' | 'identity';
2323
+
2324
+ /** Focus target for map camera: feature id(s) to zoom/pan to. */
2325
+ /**
2326
+ * Fit the point layer's cluster. `true` fits every point; `{ field, value }`
2327
+ * fits only the points whose data row has `row[field] === value` (e.g. only the
2328
+ * campuses rated "F"), which lets a story pan between sub-clusters.
2329
+ */
2330
+ export type MapPointsFocus = true | { field: string; value: string | number };
2331
+
2332
+ export type MapFocus =
2333
+ | string
2334
+ | number
2335
+ | Array<string | number>
2336
+ | { features: string | number | Array<string | number>; padding?: number }
2337
+ | { points: MapPointsFocus; padding?: number };
2338
+
2339
+ /** Geo configuration for map specs. */
2340
+ export interface MapGeo {
2341
+ /** TopoJSON topology object. User imports this from us-atlas, world-atlas, or their own source. */
2342
+ features: unknown;
2343
+ /** Field in the TopoJSON feature properties to use as the join key. Defaults to 'id'. */
2344
+ idField?: string;
2345
+ /** Map projection. Defaults to 'albersUsa'. */
2346
+ projection?: MapProjection;
2347
+ /**
2348
+ * Focus the camera. A feature id (or array) fits those features; `{ features, padding }`
2349
+ * adds breathing room; `{ points: true, padding }` fits the points layer's cluster
2350
+ * instead of any feature (use when points occupy a small part of a large feature, so
2351
+ * fitting the feature would leave the cluster small and off-center);
2352
+ * `{ points: { field, value }, padding }` fits only the matching point subset, so a
2353
+ * story can pan between sub-clusters. null clears focus from a prior story step.
2354
+ */
2355
+ focus?: MapFocus | null;
2356
+ }
2357
+
2358
+ /** Encoding channels specific to map visualizations. */
2359
+ export interface MapEncoding {
2360
+ /** Join key field in the data. Required for choropleth; optional in basemap-only mode (points layer with empty data). */
2361
+ key?: EncodingChannel;
2362
+ /** Color encoding (quantitative or nominal). Drives fill color of features. Optional when a points layer is present (basemap-only mode). */
2363
+ color?: EncodingChannel;
2364
+ /** Tooltip encoding (optional). */
2365
+ tooltip?: EncodingChannel | EncodingChannel[];
2366
+ }
2367
+
2368
+ /** Point/symbol layer rendered above choropleth features. */
2369
+ export interface MapPointsLayer {
2370
+ /** Tabular data for the points. Independent of the choropleth data join. */
2371
+ data: DataRow[];
2372
+ /** Field holding longitude (x coordinate). */
2373
+ longitude: EncodingChannel;
2374
+ /** Field holding latitude (y coordinate). */
2375
+ latitude: EncodingChannel;
2376
+ /** Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). No size legend is rendered in v1; explain the scale in chrome.subtitle if needed. */
2377
+ size?: EncodingChannel;
2378
+ /** Optional color encoding (nominal or quantitative). Independent scale from choropleth. */
2379
+ color?: EncodingChannel;
2380
+ /** Tooltip channel(s), same shape as MapEncoding.tooltip. */
2381
+ tooltip?: EncodingChannel | EncodingChannel[];
2382
+ /** Stable id for event callbacks. Data-update transitions for points are not yet supported. */
2383
+ key?: EncodingChannel;
2384
+ /** Fill opacity for point circles. Default 0.65. Range 0-1. */
2385
+ opacity?: number;
2386
+ }
2387
+
2388
+ export interface MapSpec {
2389
+ /** Discriminant: always "map". */
2390
+ type: 'map';
2391
+ /** Geo configuration: TopoJSON features, join key, and projection. */
2392
+ geo: MapGeo;
2393
+ /** Tabular data to join to geo features. */
2394
+ data: DataRow[];
2395
+ /** Encoding channels mapping data fields to visual properties. */
2396
+ encoding: MapEncoding;
2397
+ /** Editorial chrome (title, subtitle, source, byline, footer). */
2398
+ chrome?: Chrome;
2399
+ /** Legend display configuration. */
2400
+ legend?: LegendConfig;
2401
+ /** Theme configuration overrides. */
2402
+ theme?: ThemeConfig;
2403
+ /** Dark mode behavior. Defaults to "off". */
2404
+ darkMode?: DarkMode;
2405
+ /** Whether to show the tryOpenData.ai watermark. Defaults to true. */
2406
+ watermark?: boolean;
2407
+ /** Optional point/symbol layer rendered above choropleth features. */
2408
+ points?: MapPointsLayer;
2409
+ /** Animation configuration for entrance animations. */
2410
+ animation?: AnimationSpec;
2411
+ /**
2412
+ * d3-format string applied to color values in tooltips and legend.
2413
+ * @deprecated Use the encoding channel's `format` field instead.
1843
2414
  */
1844
2415
  valueFormat?: string;
1845
2416
  }
@@ -1877,6 +2448,8 @@ export interface BarListSpec {
1877
2448
  /**
1878
2449
  * d3-format string applied to bar values and tooltips.
1879
2450
  * Examples: ".1f" for one decimal, "$,.0f" for currency, "~s" for SI.
2451
+ * @deprecated Use the encoding channel's `format` field instead. This
2452
+ * property is expanded into the encoding during compilation.
1880
2453
  */
1881
2454
  valueFormat?: string;
1882
2455
  }
@@ -1904,7 +2477,15 @@ export interface BarListEncoding {
1904
2477
  * - GraphSpec: has `type: 'graph'`
1905
2478
  * - SankeySpec: has `type: 'sankey'`
1906
2479
  * - TileMapSpec: has `type: 'tilemap'`
2480
+ * - MapSpec: has `type: 'map'`
1907
2481
  * - BarListSpec: has `type: 'barlist'`
2482
+ *
2483
+ * Election parliament (hemicycle) charts are `ChartSpec` with `mark:
2484
+ * 'parliament'`, not a separate top-level type: seat data (party + seats)
2485
+ * maps naturally onto encoding.color/y like arc and waffle, and riding the
2486
+ * existing chart pipeline gets scales, legend, tooltips, a11y, and dark mode
2487
+ * for free instead of a second bespoke compile path (see
2488
+ * `packages/engine/src/charts/parliament/`).
1908
2489
  */
1909
2490
  export type VizSpec =
1910
2491
  | ChartSpec
@@ -1913,6 +2494,7 @@ export type VizSpec =
1913
2494
  | GraphSpec
1914
2495
  | SankeySpec
1915
2496
  | TileMapSpec
2497
+ | MapSpec
1916
2498
  | BarListSpec;
1917
2499
 
1918
2500
  /**
@@ -1928,6 +2510,10 @@ export type GraphSpecWithoutData = Omit<GraphSpec, 'nodes' | 'edges'>;
1928
2510
  export type SankeySpecWithoutData = Omit<SankeySpec, 'data'>;
1929
2511
  /** TileMap spec without runtime data, for persistence/storage. */
1930
2512
  export type TileMapSpecWithoutData = Omit<TileMapSpec, 'data'>;
2513
+ /** Map spec without runtime data, for persistence/storage. */
2514
+ export type MapSpecWithoutData = Omit<MapSpec, 'data' | 'points'> & {
2515
+ points?: Omit<MapPointsLayer, 'data'>;
2516
+ };
1931
2517
  /** BarList spec without runtime data, for persistence/storage. */
1932
2518
  export type BarListSpecWithoutData = Omit<BarListSpec, 'data'>;
1933
2519
  /** Union of data-stripped spec types for persistence/storage. */
@@ -1937,6 +2523,7 @@ export type StoredVizSpec =
1937
2523
  | GraphSpecWithoutData
1938
2524
  | SankeySpecWithoutData
1939
2525
  | TileMapSpecWithoutData
2526
+ | MapSpecWithoutData
1940
2527
  | BarListSpecWithoutData;
1941
2528
 
1942
2529
  // ---------------------------------------------------------------------------
@@ -2144,9 +2731,10 @@ export interface Condition<TData extends DataRow = DataRow> {
2144
2731
  test: FilterPredicate;
2145
2732
  /**
2146
2733
  * Static value to use when the condition is true.
2147
- * Accepted values: CSS color string, opacity (0-1), size number, or boolean flag.
2734
+ * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,
2735
+ * or a gradient definition for color channels.
2148
2736
  */
2149
- value?: string | number | boolean | null;
2737
+ value?: string | number | boolean | null | GradientDef;
2150
2738
  /**
2151
2739
  * Data field to use when the condition is true.
2152
2740
  * Constrained to column names of `TData` when using `ChartSpec<TData>`.
@@ -2167,18 +2755,19 @@ export interface ConditionalValueDef<TData extends DataRow = DataRow> {
2167
2755
  condition: Condition<TData> | Condition<TData>[];
2168
2756
  /**
2169
2757
  * Default value when no condition matches.
2170
- * Accepted values: CSS color string, opacity (0-1), size number, or boolean flag.
2758
+ * Accepted values: CSS color string, opacity (0-1), size number, boolean flag,
2759
+ * or a gradient definition for color channels.
2171
2760
  */
2172
- value?: string | number | boolean | null;
2761
+ value?: string | number | boolean | null | GradientDef;
2173
2762
  }
2174
2763
 
2175
2764
  /**
2176
2765
  * Check if a channel definition is a regular EncodingChannel (has 'field' at top level).
2177
- * Use this to narrow `EncodingChannel | ConditionalValueDef` in encoding channels
2178
- * that support conditional encoding (color, size, opacity).
2766
+ * Use this to narrow `EncodingChannel | ConditionalValueDef | ValueDef` in encoding
2767
+ * channels that support conditional encoding (color, size, opacity).
2179
2768
  */
2180
2769
  export function isEncodingChannel<TData extends DataRow = DataRow>(
2181
- def: EncodingChannel<TData> | ConditionalValueDef<TData> | undefined,
2770
+ def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,
2182
2771
  ): def is EncodingChannel<TData> {
2183
2772
  if (!def) return false;
2184
2773
  return 'field' in def && !('condition' in def);
@@ -2188,12 +2777,24 @@ export function isEncodingChannel<TData extends DataRow = DataRow>(
2188
2777
  * Check if a channel definition is a ConditionalValueDef.
2189
2778
  */
2190
2779
  export function isConditionalDef<TData extends DataRow = DataRow>(
2191
- def: EncodingChannel<TData> | ConditionalValueDef<TData> | undefined,
2780
+ def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,
2192
2781
  ): def is ConditionalValueDef<TData> {
2193
2782
  if (!def) return false;
2194
2783
  return 'condition' in def;
2195
2784
  }
2196
2785
 
2786
+ /**
2787
+ * Check if a channel definition is a bare ValueDef (constant value, no field
2788
+ * or condition). These are expanded to mark-level properties during the
2789
+ * pre-validation sugar expansion.
2790
+ */
2791
+ export function isValueDef<TData extends DataRow = DataRow>(
2792
+ def: EncodingChannel<TData> | ConditionalValueDef<TData> | ValueDef | undefined,
2793
+ ): def is ValueDef {
2794
+ if (!def) return false;
2795
+ return 'value' in def && !('condition' in def) && !('field' in def);
2796
+ }
2797
+
2197
2798
  // ---------------------------------------------------------------------------
2198
2799
  // Mark type helpers
2199
2800
  // ---------------------------------------------------------------------------
@@ -2211,10 +2812,25 @@ export const MARK_TYPES: ReadonlySet<string> = new Set<MarkType>([
2211
2812
  'tick',
2212
2813
  'rect',
2213
2814
  'lollipop',
2815
+ 'beeswarm',
2816
+ 'range',
2817
+ 'waffle',
2818
+ 'calendar',
2819
+ 'parliament',
2214
2820
  ]);
2215
2821
 
2216
- /** @deprecated Use MARK_TYPES instead. */
2217
- export const CHART_TYPES = MARK_TYPES;
2822
+ /**
2823
+ * Mark types that lay out inside the chart area without positional axes
2824
+ * (no axis or gridline computation; margins shrink to padding).
2825
+ */
2826
+ export function isAxislessMark(markType: MarkType): boolean {
2827
+ return (
2828
+ markType === 'arc' ||
2829
+ markType === 'waffle' ||
2830
+ markType === 'calendar' ||
2831
+ markType === 'parliament'
2832
+ );
2833
+ }
2218
2834
 
2219
2835
  /**
2220
2836
  * Extract the mark type string from a mark field (string or MarkDef).
@@ -2269,6 +2885,11 @@ export function isBarListSpec(spec: VizSpec | Record<string, unknown>): spec is
2269
2885
  return 'type' in spec && (spec as Record<string, unknown>).type === 'barlist';
2270
2886
  }
2271
2887
 
2888
+ /** Check if a spec is a MapSpec. */
2889
+ export function isMapSpec(spec: VizSpec | Record<string, unknown>): spec is MapSpec {
2890
+ return 'type' in spec && (spec as Record<string, unknown>).type === 'map';
2891
+ }
2892
+
2272
2893
  // ---------------------------------------------------------------------------
2273
2894
  // Annotation type guards
2274
2895
  // ---------------------------------------------------------------------------
@@ -2283,9 +2904,14 @@ export function isRangeAnnotation(annotation: Annotation): annotation is RangeAn
2283
2904
  return annotation.type === 'range';
2284
2905
  }
2285
2906
 
2286
- /** Check if an annotation is a RefLineAnnotation. */
2907
+ /**
2908
+ * Check if an annotation is a RefLineAnnotation. Also accepts the deprecated
2909
+ * `'rule'` type at runtime (pre-sugar-expansion input), since `annotation.type`
2910
+ * may not yet be narrowed to the canonical `'refline'` form.
2911
+ */
2287
2912
  export function isRefLineAnnotation(annotation: Annotation): annotation is RefLineAnnotation {
2288
- return annotation.type === 'refline' || annotation.type === 'rule';
2913
+ const type: string = annotation.type;
2914
+ return type === 'refline' || type === 'rule';
2289
2915
  }
2290
2916
 
2291
2917
  // ---------------------------------------------------------------------------
@@ -2305,4 +2931,9 @@ export const MARK_DISPLAY_NAMES: Record<MarkType, string> = {
2305
2931
  tick: 'Tick plot',
2306
2932
  rect: 'Heatmap',
2307
2933
  lollipop: 'Lollipop chart',
2934
+ beeswarm: 'Beeswarm chart',
2935
+ range: 'Range plot',
2936
+ waffle: 'Waffle chart',
2937
+ calendar: 'Calendar heatmap',
2938
+ parliament: 'Parliament chart',
2308
2939
  };