@perspective-dev/viewer-charts 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +4 -4
  3. package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
  4. package/dist/esm/charts/cartesian/cartesian.d.ts +1 -0
  5. package/dist/esm/charts/chart-base.d.ts +22 -9
  6. package/dist/esm/charts/chart.d.ts +4 -0
  7. package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
  8. package/dist/esm/charts/common/tree-chart.d.ts +1 -0
  9. package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
  10. package/dist/esm/charts/series/series-type.d.ts +18 -0
  11. package/dist/esm/charts/series/series.d.ts +4 -1
  12. package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
  13. package/dist/esm/perspective-viewer-charts.js +2 -2
  14. package/dist/esm/perspective-viewer-charts.js.map +4 -4
  15. package/dist/esm/plugin/charts.d.ts +24 -0
  16. package/dist/esm/plugin/plugin.d.ts +8 -1
  17. package/dist/esm/theme/gradient.d.ts +31 -0
  18. package/dist/esm/theme/overrides.d.ts +19 -0
  19. package/dist/esm/transport/protocol.d.ts +13 -2
  20. package/dist/esm/utils/css.d.ts +4 -0
  21. package/dist/esm/worker/renderer.worker.d.ts +27 -0
  22. package/package.json +1 -1
  23. package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
  24. package/src/ts/charts/candlestick/candlestick-render.ts +8 -0
  25. package/src/ts/charts/candlestick/candlestick.ts +4 -3
  26. package/src/ts/charts/cartesian/cartesian-render.ts +5 -0
  27. package/src/ts/charts/cartesian/cartesian.ts +4 -0
  28. package/src/ts/charts/chart-base.ts +42 -10
  29. package/src/ts/charts/chart.ts +5 -0
  30. package/src/ts/charts/common/category-axis-resolver.ts +44 -5
  31. package/src/ts/charts/common/tree-chart.ts +4 -0
  32. package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
  33. package/src/ts/charts/heatmap/heatmap-interact.ts +10 -1
  34. package/src/ts/charts/heatmap/heatmap-render.ts +5 -0
  35. package/src/ts/charts/heatmap/heatmap.ts +8 -3
  36. package/src/ts/charts/series/series-build.ts +1 -0
  37. package/src/ts/charts/series/series-interact.ts +2 -13
  38. package/src/ts/charts/series/series-render.ts +5 -0
  39. package/src/ts/charts/series/series-type.ts +65 -0
  40. package/src/ts/charts/series/series.ts +21 -8
  41. package/src/ts/charts/sunburst/sunburst-render.ts +9 -0
  42. package/src/ts/charts/treemap/treemap-render.ts +5 -0
  43. package/src/ts/layout/ticks.ts +36 -12
  44. package/src/ts/plugin/charts.ts +69 -3
  45. package/src/ts/plugin/plugin.ts +99 -10
  46. package/src/ts/theme/gradient.ts +241 -1
  47. package/src/ts/theme/overrides.ts +76 -0
  48. package/src/ts/transport/protocol.ts +14 -2
  49. package/src/ts/transport/renderer-transport.ts +24 -10
  50. package/src/ts/utils/css.ts +12 -0
  51. package/src/ts/worker/renderer.worker.ts +113 -5
@@ -10,6 +10,8 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ import { parseCssColorList, parseCssColorStrict } from "../../theme/gradient";
14
+
13
15
  export type ChartType = "bar" | "line" | "scatter" | "area";
14
16
 
15
17
  /**
@@ -45,6 +47,18 @@ export interface ColumnChartConfig {
45
47
  * `"skip"`). Default `"skip"`. No effect on bar / scatter.
46
48
  */
47
49
  interpolate?: InterpolateMode;
50
+
51
+ /**
52
+ * Single series-color override (`#rrggbb`) — the no-`split_by`
53
+ * shape.
54
+ */
55
+ color?: string;
56
+
57
+ /**
58
+ * Discrete color palette cycled over this column's split-generated
59
+ * series — the `split_by` shape.
60
+ */
61
+ palette?: string;
48
62
  }
49
63
 
50
64
  /**
@@ -98,6 +112,57 @@ export function resolveAltAxis(
98
112
  return cfg?.[aggName]?.alt_axis === true;
99
113
  }
100
114
 
115
+ /**
116
+ * Resolve a per-series color override from `columns_config`, or `null`
117
+ * when the theme palette applies.
118
+ */
119
+ export function resolveSeriesColorOverride(
120
+ series: { aggName: string; splitIdx: number },
121
+ facetActive: boolean,
122
+ cfg: Record<string, ColumnChartConfig> | undefined,
123
+ ): [number, number, number] | null {
124
+ const entry = cfg?.[series.aggName];
125
+ if (!entry) {
126
+ return null;
127
+ }
128
+
129
+ const palette =
130
+ typeof entry.palette === "string"
131
+ ? parsePaletteCached(entry.palette)
132
+ : null;
133
+ if (palette && palette.length > 0) {
134
+ const idx = facetActive ? 0 : series.splitIdx % palette.length;
135
+ return palette[idx];
136
+ }
137
+
138
+ if (typeof entry.color === "string") {
139
+ return parseCssColorStrict(entry.color) ?? null;
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ const PALETTE_PARSE_CACHE = new Map<
146
+ string,
147
+ Array<[number, number, number]> | null
148
+ >();
149
+
150
+ function parsePaletteCached(
151
+ src: string,
152
+ ): Array<[number, number, number]> | null {
153
+ let parsed = PALETTE_PARSE_CACHE.get(src);
154
+ if (parsed === undefined) {
155
+ parsed = parseCssColorList(src);
156
+ if (PALETTE_PARSE_CACHE.size > 256) {
157
+ PALETTE_PARSE_CACHE.clear();
158
+ }
159
+
160
+ PALETTE_PARSE_CACHE.set(src, parsed);
161
+ }
162
+
163
+ return parsed;
164
+ }
165
+
101
166
  /**
102
167
  * Resolve the interpolation mode for this aggregate.
103
168
  */
@@ -41,6 +41,7 @@ import {
41
41
  showBarPinnedTooltipForSample,
42
42
  } from "./series-interact";
43
43
  import { resolvePalette } from "../../theme/palette";
44
+ import { resolveSeriesColorOverride } from "./series-type";
44
45
  import { LineGlyph } from "./glyphs/draw-lines";
45
46
  import { ScatterGlyph } from "./glyphs/draw-scatter";
46
47
  import { AreaGlyph } from "./glyphs/draw-areas";
@@ -250,13 +251,16 @@ export class SeriesChart extends CategoricalYChart {
250
251
  /**
251
252
  * Cached palette + identity-keys for short-circuiting per-frame
252
253
  * resolution. Inputs (`seriesPalette` ref, `gradientStops` ref,
253
- * `series.length`) only change on data load or `restyle()`.
254
+ * `series.length`, `_columnsConfig` ref — per-series color
255
+ * overrides) only change on data load, `restyle()`, or
256
+ * `setColumnsConfig`.
254
257
  */
255
258
  _paletteCache: [number, number, number][] | null = null;
256
259
  _paletteCacheKey: {
257
260
  seriesPalette: [number, number, number][] | null;
258
261
  gradientStops: unknown;
259
262
  seriesLength: number;
263
+ columnsConfig: unknown;
260
264
  } | null = null;
261
265
 
262
266
  /**
@@ -514,9 +518,10 @@ export class SeriesChart extends CategoricalYChart {
514
518
  return;
515
519
  }
516
520
 
517
- const groupByValues: (string | null)[] = this._rowPaths.map(
518
- (level) => level.labels[b.catIdx] ?? null,
519
- );
521
+ const groupByValues: (string | number | null)[] =
522
+ this._categoryAxisMode === "numeric" && this._categoryPositions
523
+ ? [this._categoryPositions[b.catIdx] ?? null]
524
+ : this._rowPaths.map((level) => level.labels[b.catIdx] ?? null);
520
525
  const splitKey = this._splitPrefixes[b.splitIdx] ?? "";
521
526
  const splitByValues =
522
527
  this._splitBy.length > 0 && splitKey !== ""
@@ -903,6 +908,7 @@ export function ensurePalette(chart: SeriesChart): boolean {
903
908
  const theme = chart._resolveTheme();
904
909
  const seriesPalette = theme.seriesPalette;
905
910
  const gradientStops = theme.gradientStops;
911
+ const columnsConfig = chart._columnsConfig;
906
912
  const paletteCount = chart._facetActive
907
913
  ? Math.max(1, chart._aggregates.length)
908
914
  : chart._series.length;
@@ -913,7 +919,8 @@ export function ensurePalette(chart: SeriesChart): boolean {
913
919
  key &&
914
920
  key.seriesPalette === seriesPalette &&
915
921
  key.gradientStops === gradientStops &&
916
- key.seriesLength === paletteCount
922
+ key.seriesLength === paletteCount &&
923
+ key.columnsConfig === columnsConfig
917
924
  ) {
918
925
  return false;
919
926
  }
@@ -928,12 +935,18 @@ export function ensurePalette(chart: SeriesChart): boolean {
928
935
  seriesPalette,
929
936
  gradientStops,
930
937
  seriesLength: paletteCount,
938
+ columnsConfig,
931
939
  };
932
940
 
933
941
  for (let i = 0; i < chart._series.length; i++) {
934
- chart._series[i].color = chart._facetActive
935
- ? palette[chart._series[i].aggIdx]
936
- : palette[i];
942
+ const series = chart._series[i];
943
+ const themed = chart._facetActive ? palette[series.aggIdx] : palette[i];
944
+ series.color =
945
+ resolveSeriesColorOverride(
946
+ series,
947
+ chart._facetActive,
948
+ columnsConfig,
949
+ ) ?? themed;
937
950
  }
938
951
 
939
952
  return true;
@@ -501,7 +501,16 @@ function drawArcs(
501
501
 
502
502
  // Chrome overlay (Canvas2D)
503
503
 
504
+ /**
505
+ * Render the chrome overlay (labels, center text, hover highlight +
506
+ * tooltip).
507
+ */
504
508
  export function renderSunburstChromeOverlay(chart: SunburstChart): void {
509
+ paintSunburstChromeOverlay(chart);
510
+ chart.presentOverlay();
511
+ }
512
+
513
+ function paintSunburstChromeOverlay(chart: SunburstChart): void {
505
514
  if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
506
515
  return;
507
516
  }
@@ -467,6 +467,11 @@ function emitRect(
467
467
  * tooltip + highlight on top.
468
468
  */
469
469
  export function renderTreemapChromeOverlay(chart: TreemapChart): void {
470
+ paintTreemapChromeOverlay(chart);
471
+ chart.presentOverlay();
472
+ }
473
+
474
+ function paintTreemapChromeOverlay(chart: TreemapChart): void {
470
475
  if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
471
476
  return;
472
477
  }
@@ -111,6 +111,30 @@ export function formatTickValue(val: number): string {
111
111
  return val.toFixed(2);
112
112
  }
113
113
 
114
+ /**
115
+ * Cached `Intl.DateTimeFormat` per option shape. `Date.prototype.
116
+ * toLocale*` constructs a fresh `DateTimeFormat` (plus its ICU
117
+ * backing) on EVERY call — ~30µs each — which turned per-row label
118
+ * synthesis over large pivots into a multi-second stall. A cached
119
+ * formatter's `format()` is ~1µs. Keyed by precision tier; the
120
+ * default locale is fixed for the lifetime of the worker, so entries
121
+ * never invalidate.
122
+ */
123
+ const DATE_FORMAT_CACHE = new Map<string, Intl.DateTimeFormat>();
124
+
125
+ function cachedDateFormat(
126
+ key: string,
127
+ options: Intl.DateTimeFormatOptions,
128
+ ): Intl.DateTimeFormat {
129
+ let fmt = DATE_FORMAT_CACHE.get(key);
130
+ if (!fmt) {
131
+ fmt = new Intl.DateTimeFormat(undefined, options);
132
+ DATE_FORMAT_CACHE.set(key, fmt);
133
+ }
134
+
135
+ return fmt;
136
+ }
137
+
114
138
  /**
115
139
  * Format a timestamp (ms since epoch) as a human-readable date/time label.
116
140
  * Adapts precision based on the tick spacing.
@@ -129,49 +153,49 @@ export function formatDateTickValue(val: number, stepMs?: number): string {
129
153
 
130
154
  if (stepMs >= DAY * 28) {
131
155
  // Monthly or longer — show year-month
132
- return d.toLocaleDateString(undefined, {
156
+ return cachedDateFormat("ym", {
133
157
  year: "numeric",
134
158
  month: "short",
135
- });
159
+ }).format(d);
136
160
  }
137
161
 
138
162
  if (stepMs >= DAY) {
139
163
  // Daily — show month and day
140
- return d.toLocaleDateString(undefined, {
164
+ return cachedDateFormat("md", {
141
165
  month: "short",
142
166
  day: "numeric",
143
- });
167
+ }).format(d);
144
168
  }
145
169
 
146
170
  if (stepMs >= HOUR) {
147
171
  // Hourly
148
- return d.toLocaleString(undefined, {
172
+ return cachedDateFormat("mdh", {
149
173
  month: "short",
150
174
  day: "numeric",
151
175
  hour: "numeric",
152
- });
176
+ }).format(d);
153
177
  }
154
178
 
155
179
  if (stepMs >= MINUTE) {
156
180
  // Minutes
157
- return d.toLocaleTimeString(undefined, {
181
+ return cachedDateFormat("hm", {
158
182
  hour: "numeric",
159
183
  minute: "2-digit",
160
- });
184
+ }).format(d);
161
185
  }
162
186
 
163
187
  // Sub-minute
164
- return d.toLocaleTimeString(undefined, {
188
+ return cachedDateFormat("hms", {
165
189
  hour: "numeric",
166
190
  minute: "2-digit",
167
191
  second: "2-digit",
168
- });
192
+ }).format(d);
169
193
  }
170
194
 
171
195
  // Default: show date only
172
- return d.toLocaleDateString(undefined, {
196
+ return cachedDateFormat("ymd", {
173
197
  year: "numeric",
174
198
  month: "short",
175
199
  day: "numeric",
176
- });
200
+ }).format(d);
177
201
  }
@@ -44,6 +44,33 @@ export interface ChartTypeConfig {
44
44
  */
45
45
  applicable_plugin_fields: readonly PluginConfigField[];
46
46
 
47
+ /**
48
+ * What `group_by` DRAWS in this chart, e.g. `"X Axis"` for the
49
+ * Y-series charts. Omitted where the field has no visual role of
50
+ * its own and is a plain aggregation key — the X/Y and map charts,
51
+ * whose axes both come from `columns`.
52
+ *
53
+ * The `group_by` counterpart of `initial.names`: one declaration
54
+ * says what every view field draws, so the settings UI's labels and
55
+ * the agent's `list_plugins` contract read the same source instead
56
+ * of restating the mapping.
57
+ */
58
+ group_by_role?: string;
59
+
60
+ /** What `split_by` DRAWS in this chart. See {@link group_by_role}. */
61
+ split_by_role?: string;
62
+
63
+ /**
64
+ * Set on the charts that CONNECT their points in row order, so the
65
+ * view's row order shows up in the drawing: without a `sort` the
66
+ * line follows the table's natural order, which reads as a tangle
67
+ * unless the rows already arrive ordered along the X axis. The point
68
+ * charts (scatter, density) are unaffected, and a pre-ordered table
69
+ * needs no `sort` — which is why this is declared per chart rather
70
+ * than inferred from an empty `sort`.
71
+ */
72
+ connects_row_order?: boolean;
73
+
47
74
  /**
48
75
  * Per-chart-type overrides for `DEFAULT_PLUGIN_CONFIG`. Used when a
49
76
  * field's sensible default differs by chart family — currently
@@ -187,6 +214,9 @@ function make(
187
214
  | "max_columns"
188
215
  | "default_chart_type"
189
216
  | "plugin_field_defaults"
217
+ | "group_by_role"
218
+ | "split_by_role"
219
+ | "connects_row_order"
190
220
  >
191
221
  >,
192
222
  ): ChartTypeConfig {
@@ -205,30 +235,52 @@ function make(
205
235
  ...(overrides?.plugin_field_defaults
206
236
  ? { plugin_field_defaults: overrides.plugin_field_defaults }
207
237
  : {}),
238
+ ...(overrides?.group_by_role
239
+ ? { group_by_role: overrides.group_by_role }
240
+ : {}),
241
+ ...(overrides?.split_by_role
242
+ ? { split_by_role: overrides.split_by_role }
243
+ : {}),
244
+ ...(overrides?.connects_row_order
245
+ ? { connects_row_order: overrides.connects_row_order }
246
+ : {}),
208
247
  };
209
248
  }
210
249
 
211
250
  const FIN_NAMES = ["Open", "Close", "High", "Low", "Tooltip"];
212
251
  const HIER_NAMES = ["Size", "Color", "Tooltip"];
213
252
 
253
+ const SERIES_Y_ROLES = { group_by_role: "X Axis", split_by_role: "Series" };
254
+ const SERIES_X_ROLES = { group_by_role: "Y Axis", split_by_role: "Series" };
255
+ const CART_ROLES = {};
256
+ const HIER_ROLES = { group_by_role: "Hierarchy", split_by_role: "Facets" };
257
+ const HEATMAP_ROLES = { group_by_role: "X Axis", split_by_role: "Y Axis" };
258
+ const FIN_ROLES = { group_by_role: "X Axis", split_by_role: "Series" };
259
+ const MAP_ROLES = {};
260
+
214
261
  const CHARTS: ChartTypeConfig[] = [
215
262
  make("X Bar", "x-bar", SERIES, SELECT, 1, X_AXIS, SERIES_FIELDS, {
263
+ ...SERIES_X_ROLES,
216
264
  default_chart_type: "bar",
217
265
  plugin_field_defaults: ZERO_ANCHORED_DEFAULTS,
218
266
  }),
219
267
  make("Y Bar", "y-bar", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
268
+ ...SERIES_Y_ROLES,
220
269
  default_chart_type: "bar",
221
270
  plugin_field_defaults: ZERO_ANCHORED_DEFAULTS,
222
271
  }),
223
272
  make("Y Line", "y-line", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
273
+ ...SERIES_Y_ROLES,
224
274
  default_chart_type: "line",
225
275
  plugin_field_defaults: SERIES_DEFAULTS,
226
276
  }),
227
277
  make("Y Scatter", "y-scatter", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
278
+ ...SERIES_Y_ROLES,
228
279
  default_chart_type: "scatter",
229
280
  plugin_field_defaults: SERIES_DEFAULTS,
230
281
  }),
231
282
  make("Y Area", "y-area", SERIES, SELECT, 1, Y_AXIS, SERIES_FIELDS, {
283
+ ...SERIES_Y_ROLES,
232
284
  default_chart_type: "area",
233
285
  plugin_field_defaults: ZERO_ANCHORED_DEFAULTS,
234
286
  }),
@@ -240,6 +292,7 @@ const CHARTS: ChartTypeConfig[] = [
240
292
  2,
241
293
  ["X Axis", "Y Axis", "Color", "Size", "Label", "Tooltip"],
242
294
  CARTESIAN_FIELDS,
295
+ { ...CART_ROLES },
243
296
  ),
244
297
  make(
245
298
  "X/Y Line",
@@ -249,6 +302,7 @@ const CHARTS: ChartTypeConfig[] = [
249
302
  2,
250
303
  ["X Axis", "Y Axis", "Tooltip"],
251
304
  CARTESIAN_FIELDS,
305
+ { ...CART_ROLES, connects_row_order: true },
252
306
  ),
253
307
  make(
254
308
  "Density",
@@ -258,15 +312,24 @@ const CHARTS: ChartTypeConfig[] = [
258
312
  2,
259
313
  ["X Axis", "Y Axis", "Color", "Tooltip"],
260
314
  DENSITY_FIELDS,
315
+ { ...CART_ROLES },
261
316
  ),
262
- make("Treemap", "treemap", HIER, TOGGLE, 1, HIER_NAMES, NO_FIELDS),
263
- make("Sunburst", "sunburst", HIER, TOGGLE, 1, HIER_NAMES, NO_FIELDS),
264
- make("Heatmap", "heatmap", HIER, SELECT, 1, ["Color"], HEATMAP_FIELDS),
317
+ make("Treemap", "treemap", HIER, TOGGLE, 1, HIER_NAMES, NO_FIELDS, {
318
+ ...HIER_ROLES,
319
+ }),
320
+ make("Sunburst", "sunburst", HIER, TOGGLE, 1, HIER_NAMES, NO_FIELDS, {
321
+ ...HIER_ROLES,
322
+ }),
323
+ make("Heatmap", "heatmap", HIER, SELECT, 1, ["Color"], HEATMAP_FIELDS, {
324
+ ...HEATMAP_ROLES,
325
+ }),
265
326
  make("Candlestick", "candlestick", FIN, TOGGLE, 1, FIN_NAMES, FIN_FIELDS, {
327
+ ...FIN_ROLES,
266
328
  default_chart_type: "candlestick",
267
329
  plugin_field_defaults: SERIES_DEFAULTS,
268
330
  }),
269
331
  make("OHLC", "ohlc", FIN, TOGGLE, 1, FIN_NAMES, FIN_FIELDS, {
332
+ ...FIN_ROLES,
270
333
  default_chart_type: "ohlc",
271
334
  plugin_field_defaults: SERIES_DEFAULTS,
272
335
  }),
@@ -278,6 +341,7 @@ const CHARTS: ChartTypeConfig[] = [
278
341
  2,
279
342
  ["Longitude", "Latitude", "Color", "Size", "Label", "Tooltip"],
280
343
  MAP_SCATTER_FIELDS,
344
+ { ...MAP_ROLES },
281
345
  ),
282
346
  make(
283
347
  "Map Line",
@@ -287,6 +351,7 @@ const CHARTS: ChartTypeConfig[] = [
287
351
  2,
288
352
  ["Longitude", "Latitude", "Tooltip"],
289
353
  MAP_LINE_FIELDS,
354
+ { ...MAP_ROLES, connects_row_order: true },
290
355
  ),
291
356
  make(
292
357
  "Map Density",
@@ -296,6 +361,7 @@ const CHARTS: ChartTypeConfig[] = [
296
361
  2,
297
362
  ["Longitude", "Latitude", "Color", "Tooltip"],
298
363
  MAP_DENSITY_FIELDS,
364
+ { ...MAP_ROLES },
299
365
  ),
300
366
  ];
301
367
 
@@ -10,6 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ import { colorsToCss, stopsToCss } from "../theme/gradient";
13
14
  import type { View } from "@perspective-dev/client";
14
15
  import type {
15
16
  HTMLPerspectiveViewerElement,
@@ -27,6 +28,10 @@ import {
27
28
  import { RawEventForwarder } from "../interaction/raw-event-forwarder";
28
29
  import { RendererTransport } from "../transport/renderer-transport";
29
30
  import { RENDER_BLIT_MODE } from "../config";
31
+ import { snapshotThemeVars } from "../theme/theme-snapshot";
32
+ import { resolveThemeFromVars, type Theme } from "../theme/theme";
33
+ import { resolvePalette } from "../theme/palette";
34
+ import { vec3ToHexColor } from "../utils/css";
30
35
 
31
36
  /**
32
37
  * Facet-rendering defaults shared by every chart. Per-chart overrides
@@ -442,26 +447,86 @@ export class HTMLPerspectiveViewerWebGLPluginElement
442
447
  max_cells: this._chartType.max_cells,
443
448
  max_columns: this._chartType.max_columns,
444
449
  group_rollup_modes: ["flat"],
450
+ split_rollup_modes: ["flat"],
451
+ group_by_role: this._chartType.group_by_role,
452
+ split_by_role: this._chartType.split_by_role,
453
+ connects_row_order: !!this._chartType.connects_row_order,
445
454
  priority: 0,
446
455
  can_render_column_styles:
447
456
  !!this._chartType.default_chart_type ||
448
- this._chartType.category === "Cartesian Charts",
457
+ this._chartType.category === "Cartesian Charts" ||
458
+ this._chartType.category === "Hierarchical Charts" ||
459
+ this._chartType.category === "Map Charts",
449
460
  };
450
461
  }
451
462
 
452
463
  column_config_schema(
453
464
  column_type: string,
454
- _group: string | undefined,
455
- _column_name: string,
465
+ group: string | undefined,
466
+ column_name: string,
456
467
  current_value: Record<string, unknown> | null,
457
- _viewer_config?: { group_by?: string[]; group_rollup_mode?: string },
468
+ viewer_config?: {
469
+ columns?: (string | null)[];
470
+ group_by?: string[];
471
+ split_by?: string[];
472
+ group_rollup_mode?: string;
473
+ },
458
474
  ) {
459
475
  const fields: Array<Record<string, unknown> & { kind: string }> = [];
460
476
 
477
+ if (group === "Color") {
478
+ const numeric_gradient =
479
+ this._chartType.category === "Hierarchical Charts"
480
+ ? column_type === "integer" ||
481
+ column_type === "float" ||
482
+ column_type === "date" ||
483
+ column_type === "datetime"
484
+ : column_type !== "string";
485
+ if (numeric_gradient) {
486
+ fields.push({
487
+ kind: "GradientStops",
488
+ key: "gradient",
489
+ default: this._themeGradientStopsSpec(),
490
+ });
491
+ } else {
492
+ fields.push({
493
+ kind: "Palette",
494
+ key: "palette",
495
+ default: this._themeSeriesPaletteHex(),
496
+ });
497
+ }
498
+ }
499
+
461
500
  // Y-series plugins expose the per-column chart_type picker; non-Y
462
501
  // plugins leave `default_chart_type` unset.
463
502
  const def = this._chartType.default_chart_type;
464
503
  if (def && (column_type === "integer" || column_type === "float")) {
504
+ const is_series_glyph =
505
+ def === "bar" ||
506
+ def === "line" ||
507
+ def === "scatter" ||
508
+ def === "area";
509
+
510
+ if (is_series_glyph) {
511
+ const has_split = (viewer_config?.split_by?.length ?? 0) > 0;
512
+ if (has_split) {
513
+ fields.push({
514
+ kind: "Palette",
515
+ key: "palette",
516
+ default: this._themeSeriesPaletteHex(),
517
+ });
518
+ } else {
519
+ const slot = (viewer_config?.columns ?? [])
520
+ .filter((c): c is string => !!c)
521
+ .indexOf(column_name);
522
+ fields.push({
523
+ kind: "Color",
524
+ key: "color",
525
+ default: this._themeSeriesColorHex(Math.max(0, slot)),
526
+ });
527
+ }
528
+ }
529
+
465
530
  fields.push({
466
531
  kind: "Enum",
467
532
  key: "chart_type",
@@ -489,12 +554,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
489
554
  });
490
555
  }
491
556
 
492
- const is_series_glyph =
493
- def === "bar" ||
494
- def === "line" ||
495
- def === "scatter" ||
496
- def === "area";
497
-
498
557
  if (is_series_glyph) {
499
558
  fields.push({
500
559
  kind: "Bool",
@@ -553,6 +612,36 @@ export class HTMLPerspectiveViewerWebGLPluginElement
553
612
  return { fields };
554
613
  }
555
614
 
615
+ private _resolvedTheme(): Theme {
616
+ return resolveThemeFromVars(snapshotThemeVars(this));
617
+ }
618
+
619
+ private static readonly GRADIENT_PALETTE_FALLBACK_COUNT = 6;
620
+
621
+ private _themeSeriesPaletteHex(): string {
622
+ const theme = this._resolvedTheme();
623
+ const count =
624
+ theme.seriesPalette.length ||
625
+ HTMLPerspectiveViewerWebGLPluginElement.GRADIENT_PALETTE_FALLBACK_COUNT;
626
+ return colorsToCss(
627
+ resolvePalette(theme.seriesPalette, theme.gradientStops, count),
628
+ );
629
+ }
630
+
631
+ private _themeSeriesColorHex(idx: number): string {
632
+ const theme = this._resolvedTheme();
633
+ const count = Math.max(theme.seriesPalette.length, idx + 1);
634
+ return vec3ToHexColor(
635
+ resolvePalette(theme.seriesPalette, theme.gradientStops, count)[
636
+ idx
637
+ ],
638
+ );
639
+ }
640
+
641
+ private _themeGradientStopsSpec(): string {
642
+ return stopsToCss(this._resolvedTheme().gradientStops);
643
+ }
644
+
556
645
  async draw(view: View): Promise<void> {
557
646
  // `draw` always indicates a view-level change (pivots, columns,
558
647
  // filters, sorts, schema, …) — invalidate the `domain_mode: