@opendata-ai/openchart-engine 6.25.4 → 6.27.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/compile.ts CHANGED
@@ -77,6 +77,7 @@ import { computeLegend } from './legend/compute';
77
77
  import { legendGap } from './legend/wrap';
78
78
  import { compileSankey as compileSankeyImpl } from './sankey/compile-sankey';
79
79
  import { compileTableLayout } from './tables/compile-table';
80
+ import { compileTileMap as compileTileMapImpl } from './tilemap/compile-tilemap';
80
81
  import { computeTooltipDescriptors } from './tooltips/compute';
81
82
  import { runTransforms } from './transforms';
82
83
 
@@ -198,7 +199,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
198
199
 
199
200
  // Resolve watermark: explicit spec value wins, then options fallback, then default true.
200
201
  const rawWatermark = (expandedSpec as Record<string, unknown>).watermark;
201
- const watermark = rawWatermark !== undefined ? chartSpec.watermark : (options.watermark ?? true);
202
+ let watermark = rawWatermark !== undefined ? chartSpec.watermark : (options.watermark ?? true);
202
203
 
203
204
  // Run data transforms (filter, bin, calculate, timeUnit) before any other data processing.
204
205
  // Transforms are defined on the expanded spec (which includes any auto-generated
@@ -222,10 +223,49 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
222
223
  | Partial<
223
224
  Record<
224
225
  string,
225
- { chrome?: unknown; labels?: unknown; legend?: unknown; annotations?: unknown }
226
+ {
227
+ chrome?: unknown;
228
+ labels?: unknown;
229
+ legend?: unknown;
230
+ annotations?: unknown;
231
+ animation?: unknown;
232
+ display?: unknown;
233
+ encoding?: unknown;
234
+ watermark?: unknown;
235
+ crosshair?: unknown;
236
+ }
226
237
  >
227
238
  >
228
239
  | undefined;
240
+
241
+ // Build userExplicit descriptor BEFORE applying any overrides so we capture
242
+ // the union of "user wrote this at top-level" and "user wrote this in the
243
+ // active breakpoint override." Sparkline display mode reads this to decide
244
+ // whether to suppress chrome/axes/legend/etc. by default vs. respecting an
245
+ // explicit user opt-in. Precedence: explicit at any level wins.
246
+ const rawEncoding = rawSpec.encoding as
247
+ | { x?: { axis?: unknown }; y?: { axis?: unknown } }
248
+ | undefined;
249
+ const bpForExplicit = overrides?.[breakpoint];
250
+ const bpEncoding = bpForExplicit?.encoding as
251
+ | { x?: { axis?: unknown }; y?: { axis?: unknown } }
252
+ | undefined;
253
+ // chrome: {} (empty object) is not "explicit" — it's an idiom users write to
254
+ // silence defaults. Require at least one chrome key set to count as opt-in.
255
+ const hasChromeKeys = (v: unknown): boolean =>
256
+ !!v && typeof v === 'object' && Object.keys(v as Record<string, unknown>).length > 0;
257
+ const userExplicit = {
258
+ chrome: hasChromeKeys(rawSpec.chrome) || hasChromeKeys(bpForExplicit?.chrome),
259
+ legend: rawSpec.legend !== undefined || bpForExplicit?.legend !== undefined,
260
+ xAxis: rawEncoding?.x?.axis !== undefined || bpEncoding?.x?.axis !== undefined,
261
+ yAxis: rawEncoding?.y?.axis !== undefined || bpEncoding?.y?.axis !== undefined,
262
+ labels: rawSpec.labels !== undefined || bpForExplicit?.labels !== undefined,
263
+ animation: rawSpec.animation !== undefined || bpForExplicit?.animation !== undefined,
264
+ watermark: rawSpec.watermark !== undefined || bpForExplicit?.watermark !== undefined,
265
+ crosshair: rawSpec.crosshair !== undefined || bpForExplicit?.crosshair !== undefined,
266
+ };
267
+ chartSpec = { ...chartSpec, userExplicit };
268
+
229
269
  if (overrides?.[breakpoint]) {
230
270
  const bp = overrides[breakpoint]!;
231
271
  if (bp.chrome) {
@@ -273,14 +313,138 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
273
313
  // responsive strategy so they render inline instead of being stripped.
274
314
  strategy = { ...strategy, annotationPosition: 'inline' };
275
315
  }
316
+ // New override branches for sparkline mode and related fields:
317
+ if (bp.display !== undefined) {
318
+ chartSpec = {
319
+ ...chartSpec,
320
+ display: bp.display as NormalizedChartSpec['display'],
321
+ };
322
+ }
323
+ if (bp.encoding !== undefined) {
324
+ // Merge encoding so a breakpoint can flip on/off encoding.x.axis or
325
+ // encoding.y.axis (used by sparkline display mode to opt back in to
326
+ // axes at a specific breakpoint). Channels merge per-key, and `axis`
327
+ // and `scale` deep-merge one level so a breakpoint can set
328
+ // `axis: { title: 'foo' }` without dropping the base spec's
329
+ // `axis.tickCount` / `axis.format`.
330
+ const bpEnc = bp.encoding as Record<string, Record<string, unknown> | undefined>;
331
+ const mergedEncoding = { ...chartSpec.encoding } as Record<
332
+ string,
333
+ Record<string, unknown> | undefined
334
+ >;
335
+ const NESTED_CHANNEL_KEYS = ['axis', 'scale'];
336
+ for (const channel of Object.keys(bpEnc)) {
337
+ const baseCh = mergedEncoding[channel];
338
+ const bpCh = bpEnc[channel];
339
+ if (bpCh && baseCh) {
340
+ const merged: Record<string, unknown> = { ...baseCh, ...bpCh };
341
+ for (const key of NESTED_CHANNEL_KEYS) {
342
+ const baseNested = baseCh[key];
343
+ const bpNested = bpCh[key];
344
+ if (
345
+ baseNested &&
346
+ bpNested &&
347
+ typeof baseNested === 'object' &&
348
+ typeof bpNested === 'object' &&
349
+ !Array.isArray(baseNested) &&
350
+ !Array.isArray(bpNested)
351
+ ) {
352
+ merged[key] = { ...baseNested, ...bpNested };
353
+ }
354
+ }
355
+ mergedEncoding[channel] = merged;
356
+ } else if (bpCh) {
357
+ mergedEncoding[channel] = bpCh;
358
+ }
359
+ }
360
+ chartSpec = {
361
+ ...chartSpec,
362
+ encoding: mergedEncoding as unknown as NormalizedChartSpec['encoding'],
363
+ };
364
+ }
365
+ if (typeof bp.watermark === 'boolean') {
366
+ // Update the resolved watermark value used downstream. ChartSpec carries
367
+ // this in its normalized shape; the local `watermark` variable controls
368
+ // chrome computation and rendering.
369
+ watermark = bp.watermark;
370
+ chartSpec = { ...chartSpec, watermark };
371
+ }
372
+ }
373
+
374
+ // Sparkline mode: default labels off. Mark renderers draw value labels per
375
+ // labels.density (default 'auto'), which fills tiny sparklines with text and
376
+ // is never what you want. Explicit user labels at any level wins via
377
+ // userExplicit.labels.
378
+ if (chartSpec.display === 'sparkline' && !chartSpec.userExplicit.labels) {
379
+ chartSpec = {
380
+ ...chartSpec,
381
+ labels: { ...chartSpec.labels, density: 'none' },
382
+ };
276
383
  }
277
384
 
278
385
  // Resolve animation spec. Breakpoint override wins over base spec (matching
279
386
  // chrome, labels, legend, and annotation override precedence).
280
- const rawAnimationSpec = ((overrides?.[breakpoint] as Record<string, unknown> | undefined)
387
+ // Precedence rule for sparkline mode: an explicit user animation at ANY
388
+ // level (top-level OR breakpoint) always wins, regardless of display mode.
389
+ // resolveAnimation handles the explicit-user value; the sparkline default-off
390
+ // behavior is applied below when no explicit value exists.
391
+ let rawAnimationSpec = ((overrides?.[breakpoint] as Record<string, unknown> | undefined)
281
392
  ?.animation ?? rawSpec.animation) as AnimationSpec | undefined;
393
+ if (rawAnimationSpec === undefined && chartSpec.display === 'sparkline') {
394
+ // Sparkline mode: animation defaults to false. User-explicit (top OR bp)
395
+ // already short-circuits this branch via userExplicit.animation.
396
+ rawAnimationSpec = false;
397
+ }
398
+ // Sparkline mode: when animation is on but the user didn't specify duration,
399
+ // bump to 1100ms so the line/area reveal feels paced rather than mechanical.
400
+ // The CSS override pairs this with an expo-out easing curve. AnimationConfig
401
+ // nests duration under `enter`, so we set it there.
402
+ if (
403
+ chartSpec.display === 'sparkline' &&
404
+ rawAnimationSpec !== false &&
405
+ rawAnimationSpec !== undefined
406
+ ) {
407
+ const SPARK_DURATION = 1100;
408
+ if (rawAnimationSpec === true) {
409
+ rawAnimationSpec = { enter: { duration: SPARK_DURATION } } as AnimationSpec;
410
+ } else if (typeof rawAnimationSpec === 'object') {
411
+ const cfg = rawAnimationSpec as { enter?: unknown; annotationDelay?: number };
412
+ const enter = cfg.enter;
413
+ if (enter === undefined || enter === true) {
414
+ rawAnimationSpec = {
415
+ ...cfg,
416
+ enter: { duration: SPARK_DURATION },
417
+ } as AnimationSpec;
418
+ } else if (
419
+ typeof enter === 'object' &&
420
+ enter !== null &&
421
+ (enter as { duration?: number }).duration === undefined
422
+ ) {
423
+ rawAnimationSpec = {
424
+ ...cfg,
425
+ enter: { ...(enter as object), duration: SPARK_DURATION },
426
+ } as AnimationSpec;
427
+ }
428
+ }
429
+ }
282
430
  const resolvedAnimation = resolveAnimation(rawAnimationSpec);
283
431
 
432
+ // Crosshair: explicit user value at any level wins. In sparkline mode the
433
+ // default is off, otherwise default is off too (crosshair is opt-in). The
434
+ // value is plumbed through ChartLayout so the renderer doesn't need to
435
+ // re-inspect the raw spec.
436
+ const rawCrosshair = (bpForExplicit?.crosshair ?? rawSpec.crosshair) as boolean | undefined;
437
+ const crosshair =
438
+ chartSpec.display === 'sparkline' && !chartSpec.userExplicit.crosshair
439
+ ? false
440
+ : rawCrosshair === true;
441
+
442
+ // Watermark default-off in sparkline mode unless user-explicit.
443
+ if (chartSpec.display === 'sparkline' && !chartSpec.userExplicit.watermark) {
444
+ watermark = false;
445
+ chartSpec = { ...chartSpec, watermark: false };
446
+ }
447
+
284
448
  // Resolve theme: merge spec-level theme with options-level overrides
285
449
  const mergedThemeConfig = options.theme
286
450
  ? { ...chartSpec.theme, ...options.theme }
@@ -310,7 +474,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
310
474
  // the reserved margin. This way computeLegend positions the legend outside
311
475
  // the data area (in the margin) instead of overlapping data marks.
312
476
  const legendArea: Rect = { ...chartArea };
313
- if (legendLayout.entries.length > 0) {
477
+ if ('entries' in legendLayout && legendLayout.entries.length > 0) {
314
478
  const gap = legendGap(options.width);
315
479
  switch (legendLayout.position) {
316
480
  case 'top':
@@ -364,10 +528,19 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
364
528
  // Arc charts (pie/donut) don't use axes or gridlines
365
529
  const isRadial = chartSpec.markType === 'arc';
366
530
 
367
- // Compute axes (skip for radial charts)
531
+ // Compute axes (skip for radial charts).
532
+ // Sparkline mode skips axes by default unless the user explicitly opted into
533
+ // an axis on a specific channel.
534
+ const skipX = chartSpec.display === 'sparkline' && !chartSpec.userExplicit.xAxis;
535
+ const skipY = chartSpec.display === 'sparkline' && !chartSpec.userExplicit.yAxis;
368
536
  const axes = isRadial
369
537
  ? { x: undefined, y: undefined }
370
- : computeAxes(scales, chartArea, strategy, theme, options.measureText);
538
+ : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
539
+ data: renderSpec.data,
540
+ encoding: renderSpec.encoding as Encoding,
541
+ skipX,
542
+ skipY,
543
+ });
371
544
 
372
545
  // INVARIANT 2 — computeGridlines mutates `axes` in place. Downstream consumers read
373
546
  // axes.y.gridlines off the same object. Do not introduce a copy-on-write.
@@ -460,6 +633,8 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
460
633
  },
461
634
  animation: resolvedAnimation,
462
635
  watermark,
636
+ display: chartSpec.display,
637
+ crosshair,
463
638
  measureText: options.measureText,
464
639
  };
465
640
  }
@@ -504,7 +679,8 @@ export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLay
504
679
 
505
680
  const allMarks: Mark[] = [];
506
681
  const seenLabels = new Set<string>();
507
- const mergedLegendEntries = [...primaryLayout.legend.entries];
682
+ const pLegend = primaryLayout.legend;
683
+ const mergedLegendEntries = 'entries' in pLegend ? [...pLegend.entries] : [];
508
684
  for (const entry of mergedLegendEntries) {
509
685
  seenLabels.add(entry.label);
510
686
  }
@@ -522,10 +698,13 @@ export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLay
522
698
 
523
699
  allMarks.push(...leafLayout.marks);
524
700
 
525
- for (const entry of leafLayout.legend.entries) {
526
- if (!seenLabels.has(entry.label)) {
527
- seenLabels.add(entry.label);
528
- mergedLegendEntries.push(entry);
701
+ const leafLeg = leafLayout.legend;
702
+ if ('entries' in leafLeg) {
703
+ for (const entry of leafLeg.entries) {
704
+ if (!seenLabels.has(entry.label)) {
705
+ seenLabels.add(entry.label);
706
+ mergedLegendEntries.push(entry);
707
+ }
529
708
  }
530
709
  }
531
710
  }
@@ -535,8 +714,8 @@ export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLay
535
714
  marks: allMarks,
536
715
  legend: {
537
716
  ...primaryLayout.legend,
538
- entries: mergedLegendEntries,
539
- },
717
+ ...('entries' in pLegend ? { entries: mergedLegendEntries } : {}),
718
+ } as typeof primaryLayout.legend,
540
719
  };
541
720
  }
542
721
 
@@ -812,9 +991,12 @@ function compileLayerIndependent(
812
991
 
813
992
  // Merge legend entries with deduplication
814
993
  const seenLabels = new Set<string>();
815
- const mergedLegendEntries = [...layout0.legend.entries];
994
+ const l0Legend = layout0.legend;
995
+ const l1Legend = layout1.legend;
996
+ const mergedLegendEntries = 'entries' in l0Legend ? [...l0Legend.entries] : [];
816
997
  for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
817
- for (const entry of layout1.legend.entries) {
998
+ const l1Entries = 'entries' in l1Legend ? l1Legend.entries : [];
999
+ for (const entry of l1Entries) {
818
1000
  if (!seenLabels.has(entry.label)) {
819
1001
  seenLabels.add(entry.label);
820
1002
  mergedLegendEntries.push(entry);
@@ -856,8 +1038,8 @@ function compileLayerIndependent(
856
1038
  marks,
857
1039
  legend: {
858
1040
  ...layout0.legend,
859
- entries: mergedLegendEntries,
860
- },
1041
+ ...('entries' in l0Legend ? { entries: mergedLegendEntries } : {}),
1042
+ } as typeof layout0.legend,
861
1043
  tooltipDescriptors: mergedTooltips,
862
1044
  };
863
1045
  }
@@ -1111,3 +1293,26 @@ export function compileSankey(
1111
1293
  ): import('@opendata-ai/openchart-core').SankeyLayout {
1112
1294
  return compileSankeyImpl(spec, options);
1113
1295
  }
1296
+
1297
+ // ---------------------------------------------------------------------------
1298
+ // TileMap compilation
1299
+ // ---------------------------------------------------------------------------
1300
+
1301
+ /**
1302
+ * Compile a tilemap spec into a TileMapLayout.
1303
+ *
1304
+ * Takes a raw tilemap spec, validates, normalizes, resolves theme and chrome,
1305
+ * computes tile positions, builds tile marks with colors and labels, and
1306
+ * returns a TileMapLayout ready for rendering.
1307
+ *
1308
+ * @param spec - Raw tilemap spec (validated and normalized internally).
1309
+ * @param options - Compile options (width, height, theme, darkMode).
1310
+ * @returns TileMapLayout with computed positions and visual properties.
1311
+ * @throws Error if spec is invalid or not a tilemap type.
1312
+ */
1313
+ export function compileTileMap(
1314
+ spec: unknown,
1315
+ options: CompileOptions,
1316
+ ): import('@opendata-ai/openchart-core').TileMapLayout {
1317
+ return compileTileMapImpl(spec, options);
1318
+ }
@@ -21,6 +21,7 @@ import type {
21
21
  LayerSpec,
22
22
  SankeySpec,
23
23
  TableSpec,
24
+ TileMapSpec,
24
25
  VizSpec,
25
26
  } from '@opendata-ai/openchart-core';
26
27
  import {
@@ -29,10 +30,13 @@ import {
29
30
  isLayerSpec,
30
31
  isSankeySpec,
31
32
  isTableSpec,
33
+ isTileMapSpec,
32
34
  resolveMarkDef,
33
35
  resolveMarkType,
34
36
  } from '@opendata-ai/openchart-core';
35
37
  import type { NormalizedSankeySpec } from '../sankey/types';
38
+ import { STATE_CODE_SET } from '../tilemap/layout';
39
+ import type { NormalizedTileMapSpec } from '../tilemap/types';
36
40
  import type {
37
41
  NormalizedChartSpec,
38
42
  NormalizedChrome,
@@ -213,6 +217,19 @@ function normalizeChartSpec(spec: ChartSpec, warnings: string[]): NormalizedChar
213
217
  const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
214
218
  const markType = resolveMarkType(spec.mark);
215
219
  const markDef = resolveMarkDef(spec.mark);
220
+ const display = spec.display ?? 'full';
221
+
222
+ if (
223
+ display === 'sparkline' &&
224
+ markType !== 'line' &&
225
+ markType !== 'area' &&
226
+ markType !== 'bar' &&
227
+ markType !== 'point'
228
+ ) {
229
+ warnings.push(
230
+ `[openchart] display: 'sparkline' works best with mark: 'line' | 'area' | 'bar' | 'point'. Got mark: '${markType}' — rendering may degrade.`,
231
+ );
232
+ }
216
233
 
217
234
  return {
218
235
  markType,
@@ -229,6 +246,19 @@ function normalizeChartSpec(spec: ChartSpec, warnings: string[]): NormalizedChar
229
246
  hiddenSeries: spec.hiddenSeries ?? [],
230
247
  seriesStyles: spec.seriesStyles ?? {},
231
248
  watermark: spec.watermark ?? true,
249
+ display,
250
+ // Default empty userExplicit; compileChart overwrites this with the real
251
+ // descriptor built from the raw expanded spec before normalize runs.
252
+ userExplicit: {
253
+ chrome: false,
254
+ legend: false,
255
+ xAxis: false,
256
+ yAxis: false,
257
+ labels: false,
258
+ animation: false,
259
+ watermark: false,
260
+ crosshair: false,
261
+ },
232
262
  };
233
263
  }
234
264
 
@@ -303,6 +333,55 @@ function normalizeGraphSpec(spec: GraphSpec, _warnings: string[]): NormalizedGra
303
333
  };
304
334
  }
305
335
 
336
+ function normalizeTileMapSpec(spec: TileMapSpec, warnings: string[]): NormalizedTileMapSpec {
337
+ // Convert record data to array if needed
338
+ let data: Record<string, unknown>[] = Array.isArray(spec.data) ? spec.data : [];
339
+
340
+ if (!Array.isArray(spec.data)) {
341
+ // Convert record map to array of rows
342
+ data = Object.entries(spec.data).map(([state, value]) => ({ state, value }));
343
+ }
344
+
345
+ // Auto-generate encoding if not provided
346
+ let encoding = spec.encoding;
347
+ if (!encoding) {
348
+ encoding = {
349
+ state: { field: 'state', type: 'nominal' },
350
+ value: { field: 'value', type: 'quantitative' },
351
+ };
352
+ }
353
+
354
+ // Count matched states and warn if low match ratio
355
+ let matchedCount = 0;
356
+ for (const row of data) {
357
+ const stateCode = String(row[encoding.state.field]);
358
+ if (STATE_CODE_SET.has(stateCode)) {
359
+ matchedCount++;
360
+ }
361
+ }
362
+
363
+ const matchRatio = data.length > 0 ? matchedCount / data.length : 0;
364
+ if (matchRatio < 0.5 && data.length > 0) {
365
+ warnings.push(
366
+ `TileMap data: only ${matchedCount} of ${data.length} rows have valid US state codes (expected ≥50%)`,
367
+ );
368
+ }
369
+
370
+ return {
371
+ type: 'tilemap',
372
+ data,
373
+ encoding,
374
+ palette: spec.palette ?? 'blue',
375
+ chrome: normalizeChrome(spec.chrome),
376
+ legend: spec.legend,
377
+ theme: spec.theme ?? {},
378
+ darkMode: spec.darkMode ?? 'off',
379
+ watermark: spec.watermark ?? true,
380
+ animation: spec.animation,
381
+ valueFormat: spec.valueFormat,
382
+ };
383
+ }
384
+
306
385
  // ---------------------------------------------------------------------------
307
386
  // Public API
308
387
  // ---------------------------------------------------------------------------
@@ -337,9 +416,12 @@ export function normalizeSpec(spec: VizSpec, warnings: string[] = []): Normalize
337
416
  if (isSankeySpec(spec)) {
338
417
  return normalizeSankeySpec(spec, warnings);
339
418
  }
419
+ if (isTileMapSpec(spec)) {
420
+ return normalizeTileMapSpec(spec, warnings);
421
+ }
340
422
  // Should never happen after validation
341
423
  throw new Error(
342
- `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', or type: 'sankey'.`,
424
+ `Unknown spec shape. Expected mark (chart), layer, type: 'table', type: 'graph', type: 'sankey', or type: 'tilemap'.`,
343
425
  );
344
426
  }
345
427
 
@@ -15,6 +15,7 @@ import type {
15
15
  ColumnConfig,
16
16
  DarkMode,
17
17
  DataRow,
18
+ Display,
18
19
  Encoding,
19
20
  FieldType,
20
21
  GraphEncoding,
@@ -29,6 +30,7 @@ import type {
29
30
  ThemeConfig,
30
31
  } from '@opendata-ai/openchart-core';
31
32
  import type { NormalizedSankeySpec } from '../sankey/types';
33
+ import type { NormalizedTileMapSpec } from '../tilemap/types';
32
34
 
33
35
  // ---------------------------------------------------------------------------
34
36
  // NormalizedChrome: all fields are ChromeText objects (not plain strings)
@@ -60,6 +62,35 @@ export interface NormalizedEncodingChannel {
60
62
  // NormalizedSpec types
61
63
  // ---------------------------------------------------------------------------
62
64
 
65
+ /**
66
+ * Tracks which top-level fields the user explicitly set in their input spec.
67
+ *
68
+ * Built from the raw expandedSpec (post-breakpoint-merge, pre-normalize) so
69
+ * that "user wrote chrome.title" vs "user wrote nothing" is distinguishable
70
+ * after normalization fills in defaults.
71
+ *
72
+ * Used by sparkline display mode to decide whether to suppress chrome/axes/
73
+ * legend/etc. by default vs. respecting an explicit user opt-in.
74
+ */
75
+ export interface UserExplicit {
76
+ /** True if user wrote `chrome` (any non-empty chrome). */
77
+ chrome: boolean;
78
+ /** True if user wrote `legend`. */
79
+ legend: boolean;
80
+ /** True if user wrote `encoding.x.axis`. */
81
+ xAxis: boolean;
82
+ /** True if user wrote `encoding.y.axis`. */
83
+ yAxis: boolean;
84
+ /** True if user wrote `labels`. */
85
+ labels: boolean;
86
+ /** True if user wrote `animation`. */
87
+ animation: boolean;
88
+ /** True if user wrote `watermark`. */
89
+ watermark: boolean;
90
+ /** True if user wrote `crosshair`. */
91
+ crosshair: boolean;
92
+ }
93
+
63
94
  /** A ChartSpec with all optional fields filled with sensible defaults. */
64
95
  export interface NormalizedChartSpec {
65
96
  /** Resolved mark type string (extracted from spec.mark). */
@@ -84,6 +115,14 @@ export interface NormalizedChartSpec {
84
115
  hiddenSeries: string[];
85
116
  /** Per-series visual style overrides. */
86
117
  seriesStyles: Record<string, import('@opendata-ai/openchart-core').SeriesStyle>;
118
+ /** Display mode controlling chrome/axes/legend stripping. Defaults to `'full'`. */
119
+ display: Display;
120
+ /**
121
+ * Which top-level fields the user explicitly set. Populated by compileChart
122
+ * from the raw expanded spec before normalization. NormalizeChartSpec runs
123
+ * with a default-empty descriptor; compileChart overwrites it post-normalize.
124
+ */
125
+ userExplicit: UserExplicit;
87
126
  }
88
127
 
89
128
  /** A TableSpec with all optional fields filled with sensible defaults. */
@@ -124,7 +163,8 @@ export type NormalizedSpec =
124
163
  | NormalizedChartSpec
125
164
  | NormalizedTableSpec
126
165
  | NormalizedGraphSpec
127
- | NormalizedSankeySpec;
166
+ | NormalizedSankeySpec
167
+ | NormalizedTileMapSpec;
128
168
 
129
169
  // ---------------------------------------------------------------------------
130
170
  // Validation types