@perspective-dev/viewer-charts 5.2.0 → 5.3.1

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 (109) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +1 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +4 -4
  3. package/dist/esm/axis/legend.d.ts +53 -5
  4. package/dist/esm/axis/map-ticks.d.ts +15 -0
  5. package/dist/esm/charts/candlestick/candlestick-interact.d.ts +2 -6
  6. package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
  7. package/dist/esm/charts/cartesian/cartesian.d.ts +7 -0
  8. package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
  9. package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
  10. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
  11. package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
  12. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
  13. package/dist/esm/charts/chart-base.d.ts +24 -9
  14. package/dist/esm/charts/chart.d.ts +81 -6
  15. package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
  16. package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
  17. package/dist/esm/charts/common/tree-chart.d.ts +1 -0
  18. package/dist/esm/charts/common/tree-interact.d.ts +1 -1
  19. package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
  20. package/dist/esm/charts/series/series-build.d.ts +7 -0
  21. package/dist/esm/charts/series/series-interact.d.ts +1 -1
  22. package/dist/esm/charts/series/series-type.d.ts +18 -0
  23. package/dist/esm/charts/series/series.d.ts +10 -1
  24. package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
  25. package/dist/esm/index.d.ts +2 -0
  26. package/dist/esm/interaction/axis-indicators.d.ts +11 -0
  27. package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
  28. package/dist/esm/interaction/host-sink-message.d.ts +3 -2
  29. package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
  30. package/dist/esm/interaction/legend-controller.d.ts +246 -0
  31. package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
  32. package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
  33. package/dist/esm/layout/facet-grid.d.ts +6 -0
  34. package/dist/esm/layout/ticks.d.ts +1 -0
  35. package/dist/esm/map/tile-source.d.ts +77 -8
  36. package/dist/esm/perspective-viewer-charts.js +1 -2
  37. package/dist/esm/perspective-viewer-charts.js.map +4 -4
  38. package/dist/esm/plugin/charts.d.ts +2 -0
  39. package/dist/esm/plugin/format-defaults.d.ts +9 -0
  40. package/dist/esm/plugin/plugin.d.ts +28 -1
  41. package/dist/esm/theme/gradient.d.ts +32 -0
  42. package/dist/esm/theme/overrides.d.ts +19 -0
  43. package/dist/esm/transport/protocol.d.ts +58 -5
  44. package/dist/esm/transport/renderer-transport.d.ts +3 -0
  45. package/dist/esm/utils/css.d.ts +4 -0
  46. package/dist/esm/worker/renderer.worker.d.ts +46 -1
  47. package/package.json +1 -1
  48. package/src/css/perspective-viewer-charts.css +0 -1
  49. package/src/ts/axis/bar-axis.ts +2 -7
  50. package/src/ts/axis/legend.ts +351 -24
  51. package/src/ts/axis/map-ticks.ts +94 -0
  52. package/src/ts/axis/numeric-axis.ts +2 -9
  53. package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
  54. package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
  55. package/src/ts/charts/candlestick/candlestick-render.ts +81 -10
  56. package/src/ts/charts/candlestick/candlestick.ts +4 -3
  57. package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
  58. package/src/ts/charts/cartesian/cartesian-render.ts +374 -75
  59. package/src/ts/charts/cartesian/cartesian.ts +13 -0
  60. package/src/ts/charts/cartesian/glyph.ts +6 -2
  61. package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
  62. package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
  63. package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
  64. package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
  65. package/src/ts/charts/chart-base.ts +76 -42
  66. package/src/ts/charts/chart.ts +111 -7
  67. package/src/ts/charts/common/category-axis-resolver.ts +44 -5
  68. package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
  69. package/src/ts/charts/common/tree-chart.ts +4 -0
  70. package/src/ts/charts/common/tree-chrome.ts +102 -23
  71. package/src/ts/charts/common/tree-interact.ts +18 -17
  72. package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
  73. package/src/ts/charts/heatmap/heatmap-interact.ts +75 -8
  74. package/src/ts/charts/heatmap/heatmap-render.ts +105 -24
  75. package/src/ts/charts/heatmap/heatmap.ts +8 -3
  76. package/src/ts/charts/map/map.ts +3 -5
  77. package/src/ts/charts/series/series-build.ts +15 -2
  78. package/src/ts/charts/series/series-interact.ts +14 -24
  79. package/src/ts/charts/series/series-render.ts +310 -132
  80. package/src/ts/charts/series/series-type.ts +65 -0
  81. package/src/ts/charts/series/series.ts +31 -8
  82. package/src/ts/charts/sunburst/sunburst-render.ts +16 -1
  83. package/src/ts/charts/treemap/treemap-render.ts +12 -1
  84. package/src/ts/data/view-reader.ts +1 -4
  85. package/src/ts/index.ts +2 -0
  86. package/src/ts/interaction/axis-indicators.ts +137 -0
  87. package/src/ts/interaction/host-sink-dom.ts +66 -2
  88. package/src/ts/interaction/host-sink-message.ts +8 -2
  89. package/src/ts/interaction/lazy-tooltip.ts +5 -5
  90. package/src/ts/interaction/legend-controller.ts +1084 -0
  91. package/src/ts/interaction/tooltip-controller.ts +43 -131
  92. package/src/ts/interaction/tooltip-grid.ts +187 -0
  93. package/src/ts/layout/facet-grid.ts +8 -1
  94. package/src/ts/layout/ticks.ts +48 -12
  95. package/src/ts/map/tile-loader.ts +16 -1
  96. package/src/ts/map/tile-source.ts +199 -53
  97. package/src/ts/map/tile-sources.json +18 -0
  98. package/src/ts/plugin/charts.ts +40 -6
  99. package/src/ts/plugin/format-defaults.ts +37 -0
  100. package/src/ts/plugin/plugin.ts +330 -76
  101. package/src/ts/shaders/line.vert.glsl +5 -2
  102. package/src/ts/shaders/scatter.vert.glsl +0 -7
  103. package/src/ts/theme/gradient.ts +265 -1
  104. package/src/ts/theme/overrides.ts +76 -0
  105. package/src/ts/transport/protocol.ts +60 -4
  106. package/src/ts/transport/renderer-transport.ts +107 -14
  107. package/src/ts/utils/css.ts +12 -0
  108. package/src/ts/worker/dispatch.ts +6 -2
  109. package/src/ts/worker/renderer.worker.ts +191 -7
@@ -10,13 +10,19 @@
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,
16
17
  IPerspectiveViewerPlugin,
17
18
  PluginStaticConfig,
18
19
  } from "@perspective-dev/viewer";
19
- import { ChartTypeConfig, PluginConfigField } from "./charts";
20
+ import {
21
+ ChartTypeConfig,
22
+ LEGEND_FIELDS,
23
+ TOOLTIP_FIELDS,
24
+ PluginConfigField,
25
+ } from "./charts";
20
26
  import style from "../../css/perspective-viewer-charts.css";
21
27
  import {
22
28
  DEFAULT_FACET_CONFIG,
@@ -24,9 +30,19 @@ import {
24
30
  type FacetConfig,
25
31
  type PluginConfig,
26
32
  } from "../charts/chart";
33
+ import {
34
+ CHART_DATE_DEFAULTS,
35
+ CHART_DATETIME_DEFAULTS,
36
+ CHART_NUMBER_DEFAULTS,
37
+ } from "./format-defaults";
27
38
  import { RawEventForwarder } from "../interaction/raw-event-forwarder";
28
39
  import { RendererTransport } from "../transport/renderer-transport";
40
+ import { TILE_SOURCES, type TileSourceSpec } from "../map/tile-source";
29
41
  import { RENDER_BLIT_MODE } from "../config";
42
+ import { snapshotThemeVars } from "../theme/theme-snapshot";
43
+ import { resolveThemeFromVars, type Theme } from "../theme/theme";
44
+ import { resolvePalette } from "../theme/palette";
45
+ import { vec3ToHexColor } from "../utils/css";
30
46
 
31
47
  /**
32
48
  * Facet-rendering defaults shared by every chart. Per-chart overrides
@@ -37,6 +53,26 @@ import { RENDER_BLIT_MODE } from "../config";
37
53
  */
38
54
  const FACET_CONFIG_DEFAULTS: FacetConfig = { ...DEFAULT_FACET_CONFIG };
39
55
 
56
+ /**
57
+ * Register a raster XYZ tile provider at runtime. The provider joins
58
+ * the bundled [map/tile-sources.json] entries in the settings panel's
59
+ * `map_tile_provider` enum and resolves on any map chart from its next
60
+ * config forward onward — the resolved spec rides every outgoing
61
+ * `setPluginConfig` / `init` message alongside the config that names
62
+ * it, so no separate registry-sync operation exists. Re-registering an
63
+ * id replaces it (a changed template drops cached tiles via the
64
+ * content-derived cache identity); a chart currently displaying that
65
+ * id picks the replacement up on its next `restore()`. Throws
66
+ * `TypeError` on a malformed spec. Returns the normalized spec.
67
+ *
68
+ * A key-gated provider is registered with the key embedded in its
69
+ * `template` — keys never enter `plugin_config`, so they never appear
70
+ * in `save()` output.
71
+ */
72
+ export function registerTileSource(spec: unknown): TileSourceSpec {
73
+ return TILE_SOURCES.register(spec);
74
+ }
75
+
40
76
  /**
41
77
  * Static UI-control spec per `plugin_config` field. Mirrors the shape
42
78
  * `column_config_schema` already returns (datagrid). The runtime default
@@ -52,72 +88,157 @@ type FieldSpec =
52
88
  }
53
89
  | { kind: "Number"; min: number; max: number; step?: number };
54
90
 
55
- const FIELD_SCHEMAS: Record<PluginConfigField, FieldSpec> = {
56
- auto_alt_y_axis: { kind: "Bool" },
57
- include_zero: { kind: "Bool" },
58
- domain_mode: {
59
- kind: "Enum",
60
- variants: [
61
- { value: "fit", label: "Fit" },
62
- { value: "expand", label: "Expand" },
63
- ],
64
- },
65
- facet_mode: {
66
- kind: "Enum",
67
- variants: [
68
- { value: "grid", label: "Grid" },
69
- { value: "overlay", label: "Overlay" },
70
- ],
71
- },
72
- facet_zoom_mode: {
73
- kind: "Enum",
74
- variants: [
75
- { value: "shared", label: "Shared" },
76
- { value: "independent", label: "Independent" },
91
+ /**
92
+ * A `FieldSpec` entry may be a thunk when its contents depend on
93
+ * runtime state — `map_tile_provider`'s variants come from the tile-
94
+ * source registry, which grows via `registerTileSource`, so the enum
95
+ * must be resolved per `plugin_config_schema()` call rather than at
96
+ * module init.
97
+ */
98
+ const FIELD_SCHEMAS: Record<PluginConfigField, FieldSpec | (() => FieldSpec)> =
99
+ {
100
+ auto_alt_y_axis: { kind: "Bool" },
101
+ include_zero: { kind: "Bool" },
102
+ domain_mode: {
103
+ kind: "Enum",
104
+ variants: [
105
+ { value: "fit", label: "Fit" },
106
+ { value: "expand", label: "Expand" },
107
+ ],
108
+ },
109
+ facet_mode: {
110
+ kind: "Enum",
111
+ variants: [
112
+ { value: "grid", label: "Grid" },
113
+ { value: "overlay", label: "Overlay" },
114
+ ],
115
+ },
116
+ facet_zoom_mode: {
117
+ kind: "Enum",
118
+ variants: [
119
+ { value: "shared", label: "Shared" },
120
+ { value: "independent", label: "Independent" },
121
+ ],
122
+ },
123
+ series_zoom_mode: {
124
+ kind: "Enum",
125
+ variants: [
126
+ { value: "dynamic", label: "Dynamic" },
127
+ { value: "fixed", label: "Fixed" },
128
+ ],
129
+ },
130
+ line_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 16 },
131
+ point_size_px: { kind: "Number", min: 1, max: 32 },
132
+ band_inner_frac: { kind: "Number", min: 0.1, max: 1, step: 0.01 },
133
+ bar_inner_pad: { kind: "Number", min: 0, max: 0.9, step: 0.01 },
134
+ wick_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 8 },
135
+ ohlc_line_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 8 },
136
+ gradient_radius_px: { kind: "Number", min: 2, step: 1, max: 256 },
137
+ gradient_intensity: { kind: "Number", min: 0.05, step: 0.05, max: 4 },
138
+ gradient_heat_max: { kind: "Number", min: 0.1, step: 0.1, max: 64 },
139
+ gradient_color_mode: {
140
+ kind: "Enum",
141
+ variants: [
142
+ { value: "mean", label: "Mean (density-weighted)" },
143
+ { value: "density", label: "Density only" },
144
+ { value: "extreme", label: "Extremes" },
145
+ { value: "signed", label: "Signed sum" },
146
+ ],
147
+ },
148
+ map_tile_provider: () => ({
149
+ kind: "Enum",
150
+ variants: TILE_SOURCES.list().map((s) => ({
151
+ value: s.id,
152
+ label: s.label,
153
+ })),
154
+ }),
155
+ map_tile_alpha: { kind: "Number", min: 0, max: 1, step: 0.05 },
156
+ numeric_axes: { kind: "Bool" },
157
+ legend_mode: {
158
+ kind: "Enum",
159
+ variants: [
160
+ { value: "auto", label: "Auto" },
161
+ { value: "sidebar", label: "Sidebar" },
162
+ { value: "none", label: "None" },
163
+ { value: "floating", label: "Floating" },
164
+ ],
165
+ },
166
+ legend_size_mode: {
167
+ kind: "Enum",
168
+ variants: [
169
+ { value: "auto", label: "Auto" },
170
+ { value: "fixed", label: "Fixed" },
171
+ ],
172
+ },
173
+ // 0 = auto (the chart family's historical gutter width).
174
+ legend_width_px: { kind: "Number", min: 0, max: 512, step: 1 },
175
+ legend_height_px: { kind: "Number", min: 48, max: 1024, step: 1 },
176
+ legend_anchor: {
177
+ kind: "Enum",
178
+ variants: [
179
+ { value: "top-right", label: "Top Right" },
180
+ { value: "top-left", label: "Top Left" },
181
+ { value: "bottom-right", label: "Bottom Right" },
182
+ { value: "bottom-left", label: "Bottom Left" },
183
+ ],
184
+ },
185
+ legend_x: { kind: "Number", min: 0, max: 1, step: 0.01 },
186
+ legend_y: { kind: "Number", min: 0, max: 1, step: 0.01 },
187
+ legend_opacity: { kind: "Number", min: 0, max: 1, step: 0.05 },
188
+ tooltip_max_column_px: { kind: "Number", min: 40, max: 512, step: 1 },
189
+ tooltip_opacity: { kind: "Number", min: 0, max: 1, step: 0.05 },
190
+ };
191
+
192
+ function fieldSpec(
193
+ key: PluginConfigField,
194
+ defaults: PluginConfig,
195
+ ): Record<string, unknown> & { kind: string } {
196
+ const entry = FIELD_SCHEMAS[key];
197
+ const spec = typeof entry === "function" ? entry() : entry;
198
+ return { ...spec, key, default: defaults[key] };
199
+ }
200
+
201
+ const PLUGIN_FIELD_GROUPS: ReadonlyArray<{
202
+ key: string;
203
+ fields: readonly PluginConfigField[];
204
+ }> = [
205
+ {
206
+ key: "axes",
207
+ fields: [
208
+ "auto_alt_y_axis",
209
+ "include_zero",
210
+ "domain_mode",
211
+ "numeric_axes",
77
212
  ],
78
213
  },
79
- series_zoom_mode: {
80
- kind: "Enum",
81
- variants: [
82
- { value: "dynamic", label: "Dynamic" },
83
- { value: "fixed", label: "Fixed" },
84
- ],
214
+ {
215
+ key: "facets",
216
+ fields: ["facet_mode", "facet_zoom_mode", "series_zoom_mode"],
85
217
  },
86
- line_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 16 },
87
- point_size_px: { kind: "Number", min: 1, max: 32 },
88
- band_inner_frac: { kind: "Number", min: 0.1, max: 1, step: 0.01 },
89
- bar_inner_pad: { kind: "Number", min: 0, max: 0.9, step: 0.01 },
90
- wick_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 8 },
91
- ohlc_line_width_px: { kind: "Number", min: 0.5, step: 0.5, max: 8 },
92
- gradient_radius_px: { kind: "Number", min: 2, step: 1, max: 256 },
93
- gradient_intensity: { kind: "Number", min: 0.05, step: 0.05, max: 4 },
94
- gradient_heat_max: { kind: "Number", min: 0.1, step: 0.1, max: 64 },
95
- gradient_color_mode: {
96
- kind: "Enum",
97
- variants: [
98
- { value: "mean", label: "Mean (density-weighted)" },
99
- { value: "density", label: "Density only" },
100
- { value: "extreme", label: "Extremes" },
101
- { value: "signed", label: "Signed sum" },
218
+ {
219
+ key: "glyph",
220
+ fields: [
221
+ "line_width_px",
222
+ "point_size_px",
223
+ "band_inner_frac",
224
+ "bar_inner_pad",
225
+ "wick_width_px",
226
+ "ohlc_line_width_px",
102
227
  ],
103
228
  },
104
- map_tile_provider: {
105
- kind: "Enum",
106
- variants: [
107
- { value: "carto-positron", label: "Light (Positron)" },
108
- { value: "carto-dark-matter", label: "Dark Matter" },
109
- { value: "carto-voyager", label: "Voyager" },
229
+ {
230
+ key: "density",
231
+ fields: [
232
+ "gradient_color_mode",
233
+ "gradient_radius_px",
234
+ "gradient_intensity",
235
+ "gradient_heat_max",
110
236
  ],
111
237
  },
112
- map_tile_alpha: { kind: "Number", min: 0, max: 1, step: 0.05 },
113
- };
114
-
115
- function fieldSpec(
116
- key: PluginConfigField,
117
- defaults: PluginConfig,
118
- ): Record<string, unknown> & { kind: string } {
119
- return { ...FIELD_SCHEMAS[key], key, default: defaults[key] };
120
- }
238
+ { key: "basemap", fields: ["map_tile_provider", "map_tile_alpha"] },
239
+ { key: "legend", fields: LEGEND_FIELDS },
240
+ { key: "tooltip", fields: TOOLTIP_FIELDS },
241
+ ];
121
242
 
122
243
  const GLOBAL_STYLES = (() => {
123
244
  const sheet = new CSSStyleSheet();
@@ -394,6 +515,19 @@ export class HTMLPerspectiveViewerWebGLPluginElement
394
515
  zoomControls.classList.toggle("visible", !isDefault);
395
516
  }
396
517
  },
518
+ onPluginConfigDelta: (fields) => {
519
+ this._pluginConfig = { ...this._pluginConfig, ...fields };
520
+ const host = this
521
+ .parentElement as HTMLPerspectiveViewerElement | null;
522
+ (
523
+ host?.restore(
524
+ { plugin_config: fields },
525
+ panel ? { panel } : undefined,
526
+ ) as Promise<void> | undefined
527
+ )?.catch((e: unknown) => {
528
+ console.error("legend config persistence failed", e);
529
+ });
530
+ },
397
531
  });
398
532
 
399
533
  await transport.init({
@@ -432,6 +566,14 @@ export class HTMLPerspectiveViewerWebGLPluginElement
432
566
  BLIT_MODE = mode;
433
567
  }
434
568
 
569
+ static registerTileSource(spec: unknown): TileSourceSpec {
570
+ return registerTileSource(spec);
571
+ }
572
+
573
+ static tileSources(): readonly TileSourceSpec[] {
574
+ return TILE_SOURCES.list();
575
+ }
576
+
435
577
  get_static_config(): PluginStaticConfig {
436
578
  return {
437
579
  name: this._chartType.name,
@@ -449,23 +591,79 @@ export class HTMLPerspectiveViewerWebGLPluginElement
449
591
  priority: 0,
450
592
  can_render_column_styles:
451
593
  !!this._chartType.default_chart_type ||
452
- this._chartType.category === "Cartesian Charts",
594
+ this._chartType.category === "Cartesian Charts" ||
595
+ this._chartType.category === "Hierarchical Charts" ||
596
+ this._chartType.category === "Map Charts",
453
597
  };
454
598
  }
455
599
 
456
600
  column_config_schema(
457
601
  column_type: string,
458
- _group: string | undefined,
459
- _column_name: string,
602
+ group: string | undefined,
603
+ column_name: string,
460
604
  current_value: Record<string, unknown> | null,
461
- _viewer_config?: { group_by?: string[]; group_rollup_mode?: string },
605
+ viewer_config?: {
606
+ columns?: (string | null)[];
607
+ group_by?: string[];
608
+ split_by?: string[];
609
+ group_rollup_mode?: string;
610
+ },
462
611
  ) {
463
612
  const fields: Array<Record<string, unknown> & { kind: string }> = [];
464
613
 
614
+ if (group === "Color") {
615
+ const numeric_gradient =
616
+ this._chartType.category === "Hierarchical Charts"
617
+ ? column_type === "integer" ||
618
+ column_type === "float" ||
619
+ column_type === "date" ||
620
+ column_type === "datetime"
621
+ : column_type !== "string";
622
+ if (numeric_gradient) {
623
+ fields.push({
624
+ kind: "GradientStops",
625
+ key: "gradient",
626
+ default: this._themeGradientStopsSpec(),
627
+ });
628
+ } else {
629
+ fields.push({
630
+ kind: "Palette",
631
+ key: "palette",
632
+ default: this._themeSeriesPaletteHex(),
633
+ });
634
+ }
635
+ }
636
+
465
637
  // Y-series plugins expose the per-column chart_type picker; non-Y
466
638
  // plugins leave `default_chart_type` unset.
467
639
  const def = this._chartType.default_chart_type;
468
640
  if (def && (column_type === "integer" || column_type === "float")) {
641
+ const is_series_glyph =
642
+ def === "bar" ||
643
+ def === "line" ||
644
+ def === "scatter" ||
645
+ def === "area";
646
+
647
+ if (is_series_glyph) {
648
+ const has_split = (viewer_config?.split_by?.length ?? 0) > 0;
649
+ if (has_split) {
650
+ fields.push({
651
+ kind: "Palette",
652
+ key: "palette",
653
+ default: this._themeSeriesPaletteHex(),
654
+ });
655
+ } else {
656
+ const slot = (viewer_config?.columns ?? [])
657
+ .filter((c): c is string => !!c)
658
+ .indexOf(column_name);
659
+ fields.push({
660
+ kind: "Color",
661
+ key: "color",
662
+ default: this._themeSeriesColorHex(Math.max(0, slot)),
663
+ });
664
+ }
665
+ }
666
+
469
667
  fields.push({
470
668
  kind: "Enum",
471
669
  key: "chart_type",
@@ -493,12 +691,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
493
691
  });
494
692
  }
495
693
 
496
- const is_series_glyph =
497
- def === "bar" ||
498
- def === "line" ||
499
- def === "scatter" ||
500
- def === "area";
501
-
502
694
  if (is_series_glyph) {
503
695
  fields.push({
504
696
  kind: "Bool",
@@ -537,9 +729,20 @@ export class HTMLPerspectiveViewerWebGLPluginElement
537
729
  // Per-column formatter widgets. Surfaced for every chart type so
538
730
  // axes / tooltips / legends honor the user's format choice.
539
731
  if (column_type === "integer" || column_type === "float") {
540
- fields.push({ kind: "NumberFormat" });
541
- } else if (column_type === "date" || column_type === "datetime") {
542
- fields.push({ kind: "DatetimeFormat" });
732
+ fields.push({
733
+ kind: "NumberFormat",
734
+ default: CHART_NUMBER_DEFAULTS,
735
+ });
736
+ } else if (column_type === "datetime") {
737
+ fields.push({
738
+ kind: "DatetimeFormat",
739
+ default: CHART_DATETIME_DEFAULTS,
740
+ });
741
+ } else if (column_type === "date") {
742
+ fields.push({
743
+ kind: "DatetimeFormat",
744
+ default: CHART_DATE_DEFAULTS,
745
+ });
543
746
  }
544
747
 
545
748
  return { fields };
@@ -550,11 +753,62 @@ export class HTMLPerspectiveViewerWebGLPluginElement
550
753
  group_rollup_mode?: string;
551
754
  }) {
552
755
  const defaults = this._effectiveDefaults();
553
- const fields = this._chartType.applicable_plugin_fields.map((key) =>
554
- fieldSpec(key, defaults),
756
+ const applicable = this._chartType.applicable_plugin_fields;
757
+ const present = new Set(applicable);
758
+
759
+ const grouped = new Set<PluginConfigField>();
760
+ const groups: Array<Record<string, unknown> & { kind: string }> = [];
761
+ for (const group of PLUGIN_FIELD_GROUPS) {
762
+ const members = group.fields.filter((k) => present.has(k));
763
+ if (members.length >= 1) {
764
+ for (const k of members) {
765
+ grouped.add(k);
766
+ }
767
+
768
+ groups.push({
769
+ kind: "Group",
770
+ key: group.key,
771
+ fields: members.map((k) => fieldSpec(k, defaults)),
772
+ });
773
+ }
774
+ }
775
+
776
+ const fields: Array<Record<string, unknown> & { kind: string }> =
777
+ applicable
778
+ .filter((k) => !grouped.has(k))
779
+ .map((k) => fieldSpec(k, defaults));
780
+
781
+ return { fields: [...fields, ...groups] };
782
+ }
783
+
784
+ private _resolvedTheme(): Theme {
785
+ return resolveThemeFromVars(snapshotThemeVars(this));
786
+ }
787
+
788
+ private static readonly GRADIENT_PALETTE_FALLBACK_COUNT = 6;
789
+
790
+ private _themeSeriesPaletteHex(): string {
791
+ const theme = this._resolvedTheme();
792
+ const count =
793
+ theme.seriesPalette.length ||
794
+ HTMLPerspectiveViewerWebGLPluginElement.GRADIENT_PALETTE_FALLBACK_COUNT;
795
+ return colorsToCss(
796
+ resolvePalette(theme.seriesPalette, theme.gradientStops, count),
555
797
  );
798
+ }
556
799
 
557
- return { fields };
800
+ private _themeSeriesColorHex(idx: number): string {
801
+ const theme = this._resolvedTheme();
802
+ const count = Math.max(theme.seriesPalette.length, idx + 1);
803
+ return vec3ToHexColor(
804
+ resolvePalette(theme.seriesPalette, theme.gradientStops, count)[
805
+ idx
806
+ ],
807
+ );
808
+ }
809
+
810
+ private _themeGradientStopsSpec(): string {
811
+ return stopsToCss(this._resolvedTheme().gradientStops);
558
812
  }
559
813
 
560
814
  async draw(view: View): Promise<void> {
@@ -21,8 +21,11 @@
21
21
  // `a_color_start` / `a_color_end` carry the segment endpoints' raw
22
22
  // color values (numeric data value for gradient, dictionary index for
23
23
  // categorical). The gradient LUT is sampled using the same mapping the
24
- // scatter shader uses — `(v - cmin) / (cmax - cmin)` with sign-aware
25
- // handling for zero-crossing domains. The two endpoints' colors are
24
+ // scatter shader uses — `u_color_range` arrives PRE-SHAPED by the host:
25
+ // the symmetric sign-pivot range (`colorRangePivot`) for numeric
26
+ // columns, so linear normalization matches the CPU `colorValueToT`
27
+ // used by the legend / heatmap / tree charts; raw `[0, N-1]` extents
28
+ // for categorical index domains. The two endpoints' colors are
26
29
  // averaged so the segment reads as a single chord in gradient space.
27
30
  attribute vec2 a_start;
28
31
  attribute vec2 a_end;
@@ -47,13 +47,6 @@ void main() {
47
47
 
48
48
  v_point_size = gl_PointSize;
49
49
 
50
- // Color-t mapping. Linear across `[cmin, cmax]` for single-sign
51
- // domains (which includes categorical `[0, N-1]` split / string
52
- // indices, so the colors match `interpolatePalette`'s even sampling
53
- // used by the legend). When the domain actually crosses zero we
54
- // switch to sign-aware so the value 0 always lands at the 50% stop
55
- // of the diverging gradient — matching heatmap and the Canvas2D
56
- // tooltip paths.
57
50
  float cmin = u_color_range.x;
58
51
  float cmax = u_color_range.y;
59
52
  if(cmax <= cmin) {