@opendata-ai/openchart-core 6.28.5 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types/spec.ts CHANGED
@@ -147,10 +147,13 @@ export interface MarkDef {
147
147
  * Show point markers on line/area marks.
148
148
  * - true: filled circles at each data point
149
149
  * - 'transparent': invisible hover targets (legacy behavior)
150
- * - 'endpoints': show only first and last point per series
150
+ * - 'endpoints': show only first and last point per series (hollow dots)
151
+ * - 'last' / 'first': show only the last (or first) point — filled dot,
152
+ * no white halo. Useful for highlighting the latest reading on a line
153
+ * chart and the default for sparkline mode.
151
154
  * - false: no point marks (default; uses voronoi overlay for tooltips)
152
155
  */
153
- point?: boolean | 'transparent' | 'endpoints';
156
+ point?: boolean | 'transparent' | 'endpoints' | 'last' | 'first';
154
157
  /**
155
158
  * Curve interpolation for line/area marks.
156
159
  * Maps to d3-shape curve factories.
@@ -245,6 +248,14 @@ export interface AxisConfig {
245
248
  labelColor?: string;
246
249
  /** Secondary data field to display alongside each tick label. Renders in lighter weight/color. Only effective on categorical y-axis labels (horizontal bar charts). */
247
250
  labelField?: string;
251
+ /**
252
+ * Where tick labels render relative to the chart area. Editorial line/area
253
+ * y-axes default to `'inline'`: labels sit above their gridlines at the
254
+ * chart's left edge, with no left gutter, axis line, or tick marks. Other
255
+ * axis types default to `'gutter'` (the classic placement outside the chart
256
+ * area).
257
+ */
258
+ tickPosition?: 'inline' | 'gutter';
248
259
  }
249
260
 
250
261
  /** Scale configuration for an encoding channel. */
@@ -301,10 +312,21 @@ export type ScaleType =
301
312
  * Follows the Vega-Lite encoding model: field identifies the column,
302
313
  * type determines how the engine interprets values, aggregate applies
303
314
  * a transformation before encoding.
315
+ *
316
+ * @template TData - The shape of a data row. When `ChartSpec<TData>` is used
317
+ * with a typed data array, `field` is constrained to `keyof TData & string`,
318
+ * giving IDE autocomplete and compile-time typo detection. Defaults to `DataRow`
319
+ * which degrades to plain `string` (no constraint), keeping untyped specs working.
304
320
  */
305
- export interface EncodingChannel {
306
- /** Data field name (column in the data array). */
307
- field: string;
321
+ export interface EncodingChannel<TData extends DataRow = DataRow> {
322
+ /**
323
+ * Data field name (column in the data array).
324
+ *
325
+ * When using `ChartSpec<TData>` with a typed data array, this is constrained
326
+ * to the actual column names of `TData`. Typos fail at compile time and IDEs
327
+ * autocomplete your column names.
328
+ */
329
+ field: keyof TData & string;
308
330
  /**
309
331
  * How to interpret the field values.
310
332
  * - quantitative: continuous numbers (scale: linear)
@@ -321,18 +343,33 @@ export interface EncodingChannel {
321
343
  scale?: ScaleConfig;
322
344
  /**
323
345
  * Stacking behavior for quantitative channels (Vega-Lite aligned).
324
- * - undefined | true | 'zero': stack from zero baseline (default)
346
+ *
347
+ * Vega-Lite accepts `'zero' | 'normalize' | 'center' | null`. OpenChart adds
348
+ * `true` (sugar for `'zero'`) and `false` (sugar for `null`) so callers can
349
+ * write `stack: true` / `stack: false` without learning the string forms.
350
+ * The string forms are still the canonical input — the boolean shorthand is
351
+ * normalized to the matching string before reaching layout.
352
+ *
353
+ * - undefined: chart-type default (see below)
354
+ * - true | 'zero': stack from zero baseline
325
355
  * - 'normalize': stack and normalize to fraction of total (0-1 per category)
326
356
  * - 'center': center stacks around zero (streamgraph style)
327
- * - null | false: no stacking -- renders grouped (side-by-side) bars instead
357
+ * - null | false: no stacking -- renders overlap (area) or grouped/dodged (bar)
328
358
  *
329
- * Use `stack: null` to get grouped/dodged bars. This is the idiomatic way to
330
- * compare values across categories side-by-side rather than stacked on top of
331
- * each other. Without it, multi-series bar data stacks by default.
359
+ * **Defaults differ by chart type:**
360
+ * - **Bar**: defaults to stacked. Use `stack: null` for grouped (side-by-side) bars.
361
+ * - **Area**: defaults to overlap (v6 breaking change). Use `stack: 'zero'` (or `true`)
362
+ * to opt into stacked areas. Each overlapping series renders as a translucent
363
+ * gradient band anchored at the y-domain baseline.
364
+ * - **Line**: stacking is not applied (lines always overlap).
332
365
  *
333
366
  * @example
334
367
  * // Side-by-side grouped bars (comparing 2018 vs 2022 wages by firm size):
335
368
  * "x": { "field": "pay", "type": "quantitative", "stack": null }
369
+ *
370
+ * @example
371
+ * // Stacked area (opt-in; default is overlap):
372
+ * "y": { "field": "value", "type": "quantitative", "stack": "zero" }
336
373
  */
337
374
  stack?: boolean | 'zero' | 'normalize' | 'center' | null;
338
375
  /**
@@ -374,44 +411,97 @@ export interface EncodingChannel {
374
411
 
375
412
  /**
376
413
  * Encoding object mapping visual channels to data fields.
377
- * Which channels are required depends on the mark type.
378
- * See MARK_ENCODING_RULES in encoding.ts for per-type requirements.
414
+ * Which channels are required depends on the mark type — see the per-mark
415
+ * encoding interfaces (ArcEncoding, LineEncoding, etc.) and MARK_ENCODING_RULES
416
+ * in encoding.ts for the full requirements table.
417
+ *
418
+ * @template TData - Propagated from ChartSpec<TData>. Constrains `field` in
419
+ * every channel to `keyof TData & string` when a typed data row is provided.
379
420
  */
380
- export interface Encoding {
381
- /** Horizontal position channel. */
382
- x?: EncodingChannel;
383
- /** Vertical position channel. */
384
- y?: EncodingChannel;
385
- /** Color channel (series differentiation or heatmap). Accepts conditional definitions. */
386
- color?: EncodingChannel | ConditionalValueDef;
387
- /** Size channel (bubble charts, dot plots). Accepts conditional definitions. */
388
- size?: EncodingChannel | ConditionalValueDef;
389
- /** Detail channel (group without encoding to a visual property). */
390
- detail?: EncodingChannel;
391
- /** Secondary x position (for ranges, error bars). */
392
- x2?: EncodingChannel;
393
- /** Secondary y position (for ranges, error bars). */
394
- y2?: EncodingChannel;
395
- /** Data-driven opacity (0-1). Accepts conditional definitions. */
396
- opacity?: EncodingChannel | ConditionalValueDef;
397
- /** Point shape encoding (circle, square, diamond, triangle-up, etc.). */
398
- shape?: EncodingChannel;
399
- /** Data-driven stroke dash patterns. */
400
- strokeDash?: EncodingChannel;
401
- /** Rotation angle encoding. */
402
- angle?: EncodingChannel;
403
- /** Text content for text marks. */
404
- text?: EncodingChannel;
405
- /** Tooltip field(s). Can be a single channel or array. */
406
- tooltip?: EncodingChannel | EncodingChannel[];
407
- /** Hyperlink encoding. */
408
- href?: EncodingChannel;
409
- /** Stacking/drawing order. */
410
- order?: EncodingChannel;
411
- /** Angular position for arc marks. */
412
- theta?: EncodingChannel;
413
- /** Radial position for arc marks. */
414
- radius?: EncodingChannel;
421
+ export interface Encoding<TData extends DataRow = DataRow> {
422
+ /**
423
+ * Horizontal position channel. Required for: bar, line, area, point, tick, rect, lollipop.
424
+ * Maps a field to the x-axis. Use `type: 'temporal'` for dates, `'nominal'` for categories,
425
+ * `'quantitative'` for numbers.
426
+ */
427
+ x?: EncodingChannel<TData>;
428
+ /**
429
+ * Vertical position channel. Required for: bar, line, area, point, tick, rect, arc, lollipop.
430
+ * For arc marks, this is the value field (slice size). For all others it's the y-axis position.
431
+ */
432
+ y?: EncodingChannel<TData>;
433
+ /**
434
+ * Color channel. Required for arc marks (determines pie/donut slice coloring).
435
+ * Optional for all other marks -- used for series differentiation on multi-series charts,
436
+ * or heatmap intensity. Accepts a conditional definition to apply colors based on data predicates.
437
+ */
438
+ color?: EncodingChannel<TData> | ConditionalValueDef<TData>;
439
+ /**
440
+ * Size channel. Used by point/bubble charts to scale dot area by a quantitative field.
441
+ * Accepts a conditional definition to vary size based on data predicates.
442
+ */
443
+ size?: EncodingChannel<TData> | ConditionalValueDef<TData>;
444
+ /**
445
+ * Detail channel. Groups data into multiple series without mapping to a visual property.
446
+ * Useful when you want separate lines per category but don't need the color to differ.
447
+ */
448
+ detail?: EncodingChannel<TData>;
449
+ /**
450
+ * Secondary x position. Used with `x` to define a horizontal span (rect marks, error bars).
451
+ * Both `x` and `x2` must be quantitative.
452
+ */
453
+ x2?: EncodingChannel<TData>;
454
+ /**
455
+ * Secondary y position. Used with `y` to define a vertical span (rect marks, error bands).
456
+ * Both `y` and `y2` must be quantitative.
457
+ */
458
+ y2?: EncodingChannel<TData>;
459
+ /**
460
+ * Data-driven opacity (0-1 range). Accepts a conditional definition to vary opacity
461
+ * based on data predicates (e.g., highlight selected points).
462
+ */
463
+ opacity?: EncodingChannel<TData> | ConditionalValueDef<TData>;
464
+ /**
465
+ * Point shape encoding. Valid values: 'circle', 'square', 'diamond', 'triangle-up',
466
+ * 'triangle-down', 'cross'. Used on point/scatter marks to differentiate series by shape.
467
+ */
468
+ shape?: EncodingChannel<TData>;
469
+ /**
470
+ * Stroke dash pattern encoding. Maps a nominal field to different dash patterns
471
+ * on line marks. Useful when color alone doesn't distinguish series well.
472
+ */
473
+ strokeDash?: EncodingChannel<TData>;
474
+ /** Rotation angle encoding for point marks. Maps a quantitative field to 0-360 degrees. */
475
+ angle?: EncodingChannel<TData>;
476
+ /**
477
+ * Text content for `text` marks. Required when mark is `'text'`.
478
+ * Not meaningful for other mark types.
479
+ */
480
+ text?: EncodingChannel<TData>;
481
+ /**
482
+ * Tooltip field(s). Shown on hover. Can be a single channel or an array for
483
+ * multi-field tooltips. Independent of the x/y/color encoding.
484
+ */
485
+ tooltip?: EncodingChannel<TData> | EncodingChannel<TData>[];
486
+ /** Hyperlink encoding. Maps a field containing URLs to clickable marks. */
487
+ href?: EncodingChannel<TData>;
488
+ /**
489
+ * Drawing order. Controls z-order and stacking sort order for bar/area marks.
490
+ * Lower values are drawn first (behind higher values).
491
+ */
492
+ order?: EncodingChannel<TData>;
493
+ /**
494
+ * Angular position for arc marks (pie/donut).
495
+ * Optional -- defaults to the `y` channel value when omitted.
496
+ * Not used by any other mark type.
497
+ */
498
+ theta?: EncodingChannel<TData>;
499
+ /**
500
+ * Radial distance from center for arc marks.
501
+ * Optional -- only meaningful on donut charts (controls inner radius boundary).
502
+ * Not used by any other mark type.
503
+ */
504
+ radius?: EncodingChannel<TData>;
415
505
  }
416
506
 
417
507
  // ---------------------------------------------------------------------------
@@ -492,11 +582,18 @@ export interface ChromeText {
492
582
  }
493
583
 
494
584
  /**
495
- * Editorial chrome elements: title, subtitle, source attribution, byline, footer.
585
+ * Editorial chrome elements: eyebrow, title, subtitle, source attribution, byline, footer.
496
586
  * These are first-class structural elements, not string-only afterthoughts.
497
587
  * Each element can be a simple string or a ChromeText object with style overrides.
498
588
  */
499
589
  export interface Chrome {
590
+ /**
591
+ * Editorial kicker/category label rendered above the title. Typically
592
+ * uppercase, tracked, and tinted with the accent color (e.g.
593
+ * "Equities · Single Ticker"). Term follows IBM Carbon and Atlassian
594
+ * Design System conventions; not part of Vega-Lite's title model.
595
+ */
596
+ eyebrow?: string | ChromeText;
500
597
  /** Main title displayed above the visualization. */
501
598
  title?: string | ChromeText;
502
599
  /** Subtitle displayed below the title, typically providing context. */
@@ -507,6 +604,12 @@ export interface Chrome {
507
604
  byline?: string | ChromeText;
508
605
  /** Footer text, displayed at the very bottom. */
509
606
  footer?: string | ChromeText;
607
+ /**
608
+ * Right-anchored brand block on the footer row, paired with a small accent
609
+ * dot to its left. Visually balances the source/byline left-anchored text.
610
+ * When set, suppresses the default `tryOpenData.ai` watermark for this chart.
611
+ */
612
+ brand?: string | ChromeText;
510
613
  }
511
614
 
512
615
  // ---------------------------------------------------------------------------
@@ -524,6 +627,18 @@ export interface AnnotationOffset {
524
627
  /** Anchor direction for annotation label placement relative to the data point. */
525
628
  export type AnnotationAnchor = 'top' | 'bottom' | 'left' | 'right' | 'auto';
526
629
 
630
+ /** Style overrides for the dot marker drawn at the connector's data-point endpoint. */
631
+ export interface AnnotationDot {
632
+ /** Circle radius in pixels. Default 5. */
633
+ radius?: number;
634
+ /** Fill color. Defaults to theme background for an "open ring" look. */
635
+ fill?: string;
636
+ /** Stroke color. Defaults to theme text color. */
637
+ stroke?: string;
638
+ /** Stroke width in pixels. Default 2. */
639
+ strokeWidth?: number;
640
+ }
641
+
527
642
  /** Base properties shared by all annotation types. */
528
643
  interface AnnotationBase {
529
644
  /** Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching. */
@@ -554,6 +669,18 @@ export interface TextAnnotation extends AnnotationBase {
554
669
  y: string | number;
555
670
  /** The annotation text. Required for text annotations. */
556
671
  text: string;
672
+ /**
673
+ * Optional muted second-tone text rendered below the primary `text`.
674
+ * Used for supporting context (e.g. methodology, source). Newlines in
675
+ * `text` still produce multi-line primary; subtitle is a separate block.
676
+ */
677
+ subtitle?: string;
678
+ /**
679
+ * Optional dot marker drawn at the connector's data-point endpoint.
680
+ * `true` enables the default open-ring style. Pass an object to override
681
+ * radius, fill, stroke, or strokeWidth.
682
+ */
683
+ dot?: boolean | AnnotationDot;
557
684
  /** Font size override. */
558
685
  fontSize?: number;
559
686
  /** Font weight override. */
@@ -565,10 +692,12 @@ export interface TextAnnotation extends AnnotationBase {
565
692
  /**
566
693
  * Connector from label to anchor point.
567
694
  * - `true` (default): straight line
695
+ * - `'straight'`: straight line (alias of `true`)
568
696
  * - `'curve'`: curved arrow with arrowhead
697
+ * - `'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
569
698
  * - `false`: no connector
570
699
  */
571
- connector?: boolean | 'curve';
700
+ connector?: boolean | 'straight' | 'curve' | 'drop-line';
572
701
  /** Per-endpoint offsets for the connector line. Allows fine-tuning where the connector starts and ends. */
573
702
  connectorOffset?: {
574
703
  /** Offset for the label-end of the connector. */
@@ -745,6 +874,60 @@ export interface LegendConfig {
745
874
  exclude?: string[];
746
875
  }
747
876
 
877
+ /**
878
+ * Configuration for the endpoint labels column rendered at the chart's right edge
879
+ * for multi-series line/area charts. Each entry pairs the series name with its
880
+ * last formatted value, optionally anchored to the line by an open-circle marker.
881
+ *
882
+ * The column is independent of the traditional `legend` and the legacy
883
+ * end-of-line labels. Together with `legend.show`, the three suppression toggles
884
+ * follow this truth table for ≥2-series line/area charts:
885
+ *
886
+ * | `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels |
887
+ * |--|--|--|--|--|
888
+ * | unset | unset | hidden (auto-suppressed) | shown (default) | hidden |
889
+ * | true | unset | shown | shown | hidden |
890
+ * | unset | false | shown (auto-suppress revoked) | hidden | hidden |
891
+ * | false | false | hidden | hidden | shown (last-resort) |
892
+ * | true | false | shown | hidden | hidden |
893
+ * | false | true | hidden | shown | hidden |
894
+ * | true | true | shown | shown | hidden |
895
+ *
896
+ * Single-series charts: column is hidden by default (nothing to identify).
897
+ *
898
+ * The implementation is in `packages/engine/src/legend/suppression.ts` —
899
+ * `resolveSuppression` is the single source of truth. The table above is
900
+ * a user-facing mirror; if the two ever diverge, the engine wins. Tests
901
+ * in `legend/__tests__/suppression.test.ts` enforce every cell.
902
+ */
903
+ export interface EndpointLabelsConfig {
904
+ /** Explicit on/off. When undefined, the chart auto-decides based on series count. */
905
+ show?: boolean;
906
+ /** Field to read the displayed value from. Defaults to `encoding.y.field`. */
907
+ valueField?: string;
908
+ /** d3-format string for the value. Defaults to `encoding.y.axis.format`. */
909
+ format?: string;
910
+ /** Max wrap width in pixels for long series names. Default 96. */
911
+ width?: number;
912
+ /** Render an open-circle marker on the line at the right edge. Default true. */
913
+ showMarker?: boolean;
914
+ /** Override the marker style. */
915
+ markerStyle?: {
916
+ fill?: string;
917
+ stroke?: string;
918
+ strokeWidth?: number;
919
+ radius?: number;
920
+ };
921
+ /**
922
+ * Render a thin leader line from the swatch row back to the line endpoint
923
+ * when collision-sweep displaces a label off its data point. Default false:
924
+ * the marker on the line plus the swatch in the column already pair label
925
+ * to data; the connector tends to add visual noise for small displacements.
926
+ * Opt in when labels collide hard and you need the explicit tie-back.
927
+ */
928
+ showLeader?: boolean;
929
+ }
930
+
748
931
  // ---------------------------------------------------------------------------
749
932
  // Spec types (the top-level discriminated union)
750
933
  // ---------------------------------------------------------------------------
@@ -858,33 +1041,174 @@ export interface ChartSpecOverride {
858
1041
  }
859
1042
 
860
1043
  /**
861
- * Chart specification: the primary input for standard chart types.
1044
+ * A KPI/metric cell rendered above the chart in a horizontal row.
862
1045
  *
863
- * Uses the Vega-Lite `mark` property instead of `type` to specify
864
- * the visualization mark. The mark can be a string shorthand or an
865
- * object with additional properties (interpolation, point markers, etc.).
1046
+ * Used for editorial dashboards (e.g. "CLOSE $186.10 +1.4%") where the chart
1047
+ * is paired with summary statistics. Cells lay out evenly across the chart
1048
+ * width. Hidden in sparkline mode; auto-stripped when the container can't
1049
+ * fit the laid-out values.
866
1050
  */
867
- export interface ChartSpec {
868
- /**
869
- * The mark type to render.
870
- * String shorthand: `mark: 'bar'`
871
- * Object with properties: `mark: { type: 'line', interpolate: 'step' }`
872
- */
873
- mark: MarkType | MarkDef;
1051
+ export interface Metric {
1052
+ /** Uppercase eyebrow label, e.g. "CLOSE". */
1053
+ label: string;
1054
+ /** Primary numeric value, e.g. "$186.10". */
1055
+ value: string;
1056
+ /** Optional change indicator, e.g. "+1.4%". Rendered next to the value. */
1057
+ delta?: string;
1058
+ /** Tone for the delta. 'up' = positive (green), 'down' = negative (red). Default 'up'. */
1059
+ deltaTone?: 'up' | 'down';
1060
+ /** Optional secondary value (e.g. multiplier "10.3×"). Rendered after the delta. */
1061
+ secondary?: string;
1062
+ }
1063
+
1064
+ // ---------------------------------------------------------------------------
1065
+ // Per-mark encoding interfaces (Task 2)
1066
+ // Required channels derived directly from MARK_ENCODING_RULES in encoding.ts.
1067
+ // TypeScript types must stay in sync with those runtime rules.
1068
+ // ---------------------------------------------------------------------------
1069
+
1070
+ /**
1071
+ * Encoding for arc marks (pie/donut charts).
1072
+ * - `y`: required (quantitative — the slice value)
1073
+ * - `color`: required (nominal/ordinal — the category)
1074
+ * - `theta`: optional (defaults to `y` channel)
1075
+ * - `radius`: optional (donut inner radius)
1076
+ */
1077
+ export interface ArcEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1078
+ y: EncodingChannel<TData>;
1079
+ color: EncodingChannel<TData>;
1080
+ }
1081
+
1082
+ /**
1083
+ * Encoding for line marks.
1084
+ * - `x`: required (temporal or ordinal — the time/category axis)
1085
+ * - `y`: required (quantitative — the value axis)
1086
+ */
1087
+ export interface LineEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1088
+ x: EncodingChannel<TData>;
1089
+ y: EncodingChannel<TData>;
1090
+ }
1091
+
1092
+ /**
1093
+ * Encoding for bar marks (vertical columns and horizontal bars).
1094
+ * - `x`: required
1095
+ * - `y`: required
1096
+ */
1097
+ export interface BarEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1098
+ x: EncodingChannel<TData>;
1099
+ y: EncodingChannel<TData>;
1100
+ }
1101
+
1102
+ /**
1103
+ * Encoding for area marks.
1104
+ * - `x`: required (temporal or ordinal)
1105
+ * - `y`: required (quantitative)
1106
+ */
1107
+ export interface AreaEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1108
+ x: EncodingChannel<TData>;
1109
+ y: EncodingChannel<TData>;
1110
+ }
1111
+
1112
+ /**
1113
+ * Encoding for point marks (scatter plots).
1114
+ * - `x`: required
1115
+ * - `y`: required
1116
+ */
1117
+ export interface PointEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1118
+ x: EncodingChannel<TData>;
1119
+ y: EncodingChannel<TData>;
1120
+ }
1121
+
1122
+ /**
1123
+ * Encoding for circle marks (dot plots).
1124
+ * - `x`: required (quantitative)
1125
+ * - `y`: required (nominal/ordinal — the category axis)
1126
+ */
1127
+ export interface CircleEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1128
+ x: EncodingChannel<TData>;
1129
+ y: EncodingChannel<TData>;
1130
+ }
1131
+
1132
+ /**
1133
+ * Encoding for lollipop marks.
1134
+ * - `x`: required (quantitative)
1135
+ * - `y`: required (nominal/ordinal)
1136
+ */
1137
+ export interface LollipopEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1138
+ x: EncodingChannel<TData>;
1139
+ y: EncodingChannel<TData>;
1140
+ }
1141
+
1142
+ /**
1143
+ * Encoding for text marks (data-positioned labels).
1144
+ * - `text`: required (the field to render as text)
1145
+ * - `x`, `y`: optional positioning
1146
+ */
1147
+ export interface TextEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1148
+ text: EncodingChannel<TData>;
1149
+ }
1150
+
1151
+ /**
1152
+ * Encoding for tick marks (strip/rug plots).
1153
+ * - `x`: required
1154
+ * - `y`: required
1155
+ */
1156
+ export interface TickEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1157
+ x: EncodingChannel<TData>;
1158
+ y: EncodingChannel<TData>;
1159
+ }
1160
+
1161
+ /**
1162
+ * Encoding for rect marks (heatmaps, 2D binned plots).
1163
+ * - `x`: required
1164
+ * - `y`: required
1165
+ */
1166
+ export interface RectEncoding<TData extends DataRow = DataRow> extends Encoding<TData> {
1167
+ x: EncodingChannel<TData>;
1168
+ y: EncodingChannel<TData>;
1169
+ }
1170
+
1171
+ // ---------------------------------------------------------------------------
1172
+ // Shared (non-mark-specific) ChartSpec properties
1173
+ // ---------------------------------------------------------------------------
1174
+
1175
+ /**
1176
+ * Properties shared across all ChartSpec mark variants.
1177
+ * Extracted to avoid repeating them in every union member.
1178
+ *
1179
+ * @internal
1180
+ */
1181
+ interface BaseChartSpec<TData extends DataRow = DataRow> {
874
1182
  /** Data array: each element is a row with field values. */
875
- data: DataRow[];
1183
+ data: TData[];
876
1184
  /** Data transforms applied in order before encoding (filter, bin, calculate, timeUnit). */
877
1185
  transform?: Transform[];
878
- /** Encoding mapping data fields to visual channels. */
879
- encoding: Encoding;
880
1186
  /** Editorial chrome (title, subtitle, source, etc.). */
881
1187
  chrome?: Chrome;
1188
+ /**
1189
+ * KPI/metric cells rendered as a horizontal row between subtitle and chart
1190
+ * area. Each cell shows a label/value pair with optional delta and secondary
1191
+ * value. Hidden in sparkline mode and auto-stripped when the container is
1192
+ * too narrow or short, or when value text would overflow its cell.
1193
+ */
1194
+ metrics?: Metric[];
882
1195
  /** Data annotations (text callouts, highlighted ranges, reference lines). */
883
1196
  annotations?: Annotation[];
884
1197
  /** Label display configuration. `false` disables all labels, `true` uses defaults. */
885
1198
  labels?: LabelSpec;
886
1199
  /** Legend display configuration (position override). */
887
1200
  legend?: LegendConfig;
1201
+ /**
1202
+ * Right-side endpoint labels column for multi-series line/area charts.
1203
+ *
1204
+ * - `true` or `EndpointLabelsConfig`: render the column.
1205
+ * - `false`: hide the column.
1206
+ * - omitted: auto-enable for multi-series line/area charts, hide otherwise.
1207
+ *
1208
+ * See {@link EndpointLabelsConfig} for the full suppression truth table that
1209
+ * relates this flag to `legend.show` and the legacy end-of-line labels.
1210
+ */
1211
+ endpointLabels?: boolean | EndpointLabelsConfig;
888
1212
  /** Whether the chart adapts to container width. Defaults to true. */
889
1213
  responsive?: boolean;
890
1214
  /** Theme configuration overrides. */
@@ -941,6 +1265,87 @@ export interface ChartSpec {
941
1265
  zIndex?: number;
942
1266
  }
943
1267
 
1268
+ /**
1269
+ * Chart specification: the primary input for standard chart types.
1270
+ *
1271
+ * Uses the Vega-Lite `mark` property instead of `type` to specify
1272
+ * the visualization mark. The mark can be a string shorthand or an
1273
+ * object with additional properties (interpolation, point markers, etc.).
1274
+ *
1275
+ * This is a discriminated union — the `mark` value determines which encoding
1276
+ * channels are required. TypeScript enforces required channels at compile time,
1277
+ * matching the runtime rules in `MARK_ENCODING_RULES`.
1278
+ *
1279
+ * @template TData - The shape of a single data row. When provided, `encoding.*.field`
1280
+ * values are constrained to `keyof TData` and IDEs autocomplete your column names.
1281
+ * Defaults to `DataRow` (no constraint) — existing untyped specs work unchanged.
1282
+ *
1283
+ * @example
1284
+ * // Typed: field autocomplete + typo detection
1285
+ * type SalesRow = { date: string; revenue: number; region: string };
1286
+ * const spec: ChartSpec<SalesRow> = {
1287
+ * mark: 'line',
1288
+ * data: rows,
1289
+ * encoding: {
1290
+ * x: { field: 'date', type: 'temporal' }, // autocompletes
1291
+ * y: { field: 'revenue', type: 'quantitative' }, // typos fail at compile time
1292
+ * }
1293
+ * };
1294
+ *
1295
+ * @example
1296
+ * // Untyped: works exactly as before (no migration needed)
1297
+ * const spec: ChartSpec = {
1298
+ * mark: 'bar',
1299
+ * data: myData,
1300
+ * encoding: { x: { field: 'category', type: 'nominal' }, y: { field: 'value', type: 'quantitative' } }
1301
+ * };
1302
+ */
1303
+ export type ChartSpec<TData extends DataRow = DataRow> =
1304
+ | (BaseChartSpec<TData> & {
1305
+ mark: 'arc' | (MarkDef & { type: 'arc' });
1306
+ encoding: ArcEncoding<TData>;
1307
+ })
1308
+ | (BaseChartSpec<TData> & {
1309
+ mark: 'line' | (MarkDef & { type: 'line' });
1310
+ encoding: LineEncoding<TData>;
1311
+ })
1312
+ | (BaseChartSpec<TData> & {
1313
+ mark: 'bar' | (MarkDef & { type: 'bar' });
1314
+ encoding: BarEncoding<TData>;
1315
+ })
1316
+ | (BaseChartSpec<TData> & {
1317
+ mark: 'area' | (MarkDef & { type: 'area' });
1318
+ encoding: AreaEncoding<TData>;
1319
+ })
1320
+ | (BaseChartSpec<TData> & {
1321
+ mark: 'point' | (MarkDef & { type: 'point' });
1322
+ encoding: PointEncoding<TData>;
1323
+ })
1324
+ | (BaseChartSpec<TData> & {
1325
+ mark: 'circle' | (MarkDef & { type: 'circle' });
1326
+ encoding: CircleEncoding<TData>;
1327
+ })
1328
+ | (BaseChartSpec<TData> & {
1329
+ mark: 'lollipop' | (MarkDef & { type: 'lollipop' });
1330
+ encoding: LollipopEncoding<TData>;
1331
+ })
1332
+ | (BaseChartSpec<TData> & {
1333
+ mark: 'text' | (MarkDef & { type: 'text' });
1334
+ encoding: TextEncoding<TData>;
1335
+ })
1336
+ | (BaseChartSpec<TData> & {
1337
+ mark: 'tick' | (MarkDef & { type: 'tick' });
1338
+ encoding: TickEncoding<TData>;
1339
+ })
1340
+ | (BaseChartSpec<TData> & {
1341
+ mark: 'rect' | (MarkDef & { type: 'rect' });
1342
+ encoding: RectEncoding<TData>;
1343
+ })
1344
+ | (BaseChartSpec<TData> & {
1345
+ mark: 'rule' | (MarkDef & { type: 'rule' });
1346
+ encoding: Encoding<TData>;
1347
+ });
1348
+
944
1349
  /**
945
1350
  * Table specification: input for data table visualizations.
946
1351
  *
@@ -1062,14 +1467,19 @@ export interface ResolveConfig {
1062
1467
  * Each element in `layer` is either a ChartSpec or another LayerSpec (nested).
1063
1468
  * Shared data, encoding, and transforms at the LayerSpec level are inherited
1064
1469
  * by children that don't define their own.
1470
+ *
1471
+ * @template TData - The shape of a single data row. When all layers share the
1472
+ * same data shape, pass it here to get field autocomplete across the layer.
1473
+ * For layers with different data shapes per child, omit TData (defaults to
1474
+ * `DataRow`) — children can be independently typed.
1065
1475
  */
1066
- export interface LayerSpec {
1476
+ export interface LayerSpec<TData extends DataRow = DataRow> {
1067
1477
  /** Array of child layers (ChartSpec or nested LayerSpec). */
1068
- layer: (ChartSpec | LayerSpec)[];
1478
+ layer: (ChartSpec<TData> | LayerSpec<TData>)[];
1069
1479
  /** Shared data inherited by children without their own data. */
1070
- data?: DataRow[];
1480
+ data?: TData[];
1071
1481
  /** Shared encoding inherited by children (overridden per-channel by child). */
1072
- encoding?: Encoding;
1482
+ encoding?: Encoding<TData>;
1073
1483
  /** Shared transforms. Parent transforms run before child transforms. */
1074
1484
  transform?: Transform[];
1075
1485
  /** Editorial chrome (title, subtitle, source, etc.). */
@@ -1297,8 +1707,11 @@ export type VizSpec =
1297
1707
  | TileMapSpec
1298
1708
  | BarListSpec;
1299
1709
 
1300
- /** Chart spec without runtime data, for persistence/storage. */
1301
- export type ChartSpecWithoutData = Omit<ChartSpec, 'data'>;
1710
+ /**
1711
+ * Chart spec without runtime data, for persistence/storage.
1712
+ * Generic: `ChartSpecWithoutData<MyRow>` constrains encoding field names.
1713
+ */
1714
+ export type ChartSpecWithoutData<TData extends DataRow = DataRow> = Omit<ChartSpec<TData>, 'data'>;
1302
1715
  /** Table spec without runtime data and columns, for persistence/storage. Columns can be auto-generated via dataTable(). */
1303
1716
  export type TableSpecWithoutData = Omit<TableSpec, 'data' | 'columns'>;
1304
1717
  /** Graph spec without runtime data, for persistence/storage. */
@@ -1346,7 +1759,12 @@ export interface RelativeTimeRef {
1346
1759
 
1347
1760
  /** A predicate that tests a field value against a condition. */
1348
1761
  export interface FieldPredicate {
1349
- /** Data field to test. */
1762
+ /**
1763
+ * Data field to test.
1764
+ * Note: FieldPredicate is used in transforms (FilterTransform) which operate
1765
+ * on the raw DataRow type — field is typed as string here since transforms
1766
+ * can reference computed/derived fields not present in the original TData.
1767
+ */
1350
1768
  field: string;
1351
1769
  /** Equals comparison. */
1352
1770
  equal?: unknown;
@@ -1509,14 +1927,23 @@ export type Transform =
1509
1927
  /**
1510
1928
  * A single condition with a test predicate and resulting value/field.
1511
1929
  * When the test passes for a datum, the condition's value/field is used.
1930
+ *
1931
+ * @template TData - Propagated from ChartSpec<TData>. Constrains `field` to
1932
+ * `keyof TData & string` when a typed data row is provided.
1512
1933
  */
1513
- export interface Condition {
1934
+ export interface Condition<TData extends DataRow = DataRow> {
1514
1935
  /** Predicate to test against each datum. */
1515
1936
  test: FilterPredicate;
1516
- /** Static value to use when the condition is true. */
1517
- value?: unknown;
1518
- /** Data field to use when the condition is true. */
1519
- field?: string;
1937
+ /**
1938
+ * Static value to use when the condition is true.
1939
+ * Accepted values: CSS color string, opacity (0-1), size number, or boolean flag.
1940
+ */
1941
+ value?: string | number | boolean | null;
1942
+ /**
1943
+ * Data field to use when the condition is true.
1944
+ * Constrained to column names of `TData` when using `ChartSpec<TData>`.
1945
+ */
1946
+ field?: keyof TData & string;
1520
1947
  /** Field type for the conditional field. */
1521
1948
  type?: FieldType;
1522
1949
  }
@@ -1524,12 +1951,17 @@ export interface Condition {
1524
1951
  /**
1525
1952
  * A conditional value definition for an encoding channel.
1526
1953
  * Evaluates conditions in order, falling back to the default value.
1954
+ *
1955
+ * @template TData - Propagated from ChartSpec<TData>.
1527
1956
  */
1528
- export interface ConditionalValueDef {
1957
+ export interface ConditionalValueDef<TData extends DataRow = DataRow> {
1529
1958
  /** One or more conditions to evaluate. */
1530
- condition: Condition | Condition[];
1531
- /** Default value when no condition matches. */
1532
- value?: unknown;
1959
+ condition: Condition<TData> | Condition<TData>[];
1960
+ /**
1961
+ * Default value when no condition matches.
1962
+ * Accepted values: CSS color string, opacity (0-1), size number, or boolean flag.
1963
+ */
1964
+ value?: string | number | boolean | null;
1533
1965
  }
1534
1966
 
1535
1967
  /**
@@ -1537,9 +1969,9 @@ export interface ConditionalValueDef {
1537
1969
  * Use this to narrow `EncodingChannel | ConditionalValueDef` in encoding channels
1538
1970
  * that support conditional encoding (color, size, opacity).
1539
1971
  */
1540
- export function isEncodingChannel(
1541
- def: EncodingChannel | ConditionalValueDef | undefined,
1542
- ): def is EncodingChannel {
1972
+ export function isEncodingChannel<TData extends DataRow = DataRow>(
1973
+ def: EncodingChannel<TData> | ConditionalValueDef<TData> | undefined,
1974
+ ): def is EncodingChannel<TData> {
1543
1975
  if (!def) return false;
1544
1976
  return 'field' in def && !('condition' in def);
1545
1977
  }
@@ -1547,9 +1979,9 @@ export function isEncodingChannel(
1547
1979
  /**
1548
1980
  * Check if a channel definition is a ConditionalValueDef.
1549
1981
  */
1550
- export function isConditionalDef(
1551
- def: EncodingChannel | ConditionalValueDef | undefined,
1552
- ): def is ConditionalValueDef {
1982
+ export function isConditionalDef<TData extends DataRow = DataRow>(
1983
+ def: EncodingChannel<TData> | ConditionalValueDef<TData> | undefined,
1984
+ ): def is ConditionalValueDef<TData> {
1553
1985
  if (!def) return false;
1554
1986
  return 'condition' in def;
1555
1987
  }