@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
@@ -67,12 +67,16 @@ export interface Glyph {
67
67
  buildTooltipLines(
68
68
  chart: CartesianChart,
69
69
  flatIdx: number,
70
- ): Promise<string[]>;
70
+ ): Promise<string[][]>;
71
71
 
72
72
  /**
73
73
  * Hover-overlay options (crosshair, highlight radius).
74
74
  */
75
- tooltipOptions(): { crosshair: boolean; highlightRadius: number };
75
+ tooltipOptions(): {
76
+ crosshair: boolean;
77
+ highlightRadius: number;
78
+ axisIndicators: boolean;
79
+ };
76
80
 
77
81
  /**
78
82
  * Release GL resources created by `ensureProgram`.
@@ -309,12 +309,12 @@ export class DensityGlyph implements Glyph {
309
309
  buildTooltipLines(
310
310
  chart: CartesianChart,
311
311
  flatIdx: number,
312
- ): Promise<string[]> {
312
+ ): Promise<string[][]> {
313
313
  return buildPointRowTooltipLines(chart, flatIdx);
314
314
  }
315
315
 
316
316
  tooltipOptions() {
317
- return { crosshair: true, highlightRadius: 0 };
317
+ return { crosshair: true, highlightRadius: 0, axisIndicators: true };
318
318
  }
319
319
 
320
320
  destroy(chart: CartesianChart): void {
@@ -19,6 +19,7 @@ import {
19
19
  getInstancing,
20
20
  } from "../../../webgl/instanced-attrs";
21
21
  import { compileProgram } from "../../../webgl/program-cache";
22
+ import { colorRangePivot } from "../../../theme/gradient";
22
23
  import { formatTickValue, formatDateTickValue } from "../../../layout/ticks";
23
24
  import lineVert from "../../../shaders/line.vert.glsl";
24
25
  import lineFrag from "../../../shaders/line.frag.glsl";
@@ -124,17 +125,17 @@ export class LineGlyph implements Glyph {
124
125
  async buildTooltipLines(
125
126
  chart: CartesianChart,
126
127
  flatIdx: number,
127
- ): Promise<string[]> {
128
- const lines: string[] = [];
128
+ ): Promise<string[][]> {
129
+ const grid: string[][] = [];
129
130
  if (!chart._xData || !chart._yData) {
130
- return lines;
131
+ return grid;
131
132
  }
132
133
 
133
134
  if (chart._splitGroups.length > 0 && chart._seriesCapacity > 0) {
134
135
  const seriesIdx = Math.floor(flatIdx / chart._seriesCapacity);
135
136
  const sg = chart._splitGroups[seriesIdx];
136
137
  if (sg) {
137
- lines.push(sg.prefix);
138
+ grid.push([sg.prefix]);
138
139
  }
139
140
  }
140
141
 
@@ -146,20 +147,20 @@ export class LineGlyph implements Glyph {
146
147
  const xFormatted = xIsDate
147
148
  ? formatDateTickValue(xVal)
148
149
  : formatTickValue(xVal);
149
- lines.push(`${chart._xLabel || "Row"}: ${xFormatted}`);
150
+ grid.push([chart._xLabel || "Row", xFormatted]);
150
151
 
151
152
  const yType = chart._columnTypes[chart._yLabel] || "";
152
153
  const yIsDate = yType === "date" || yType === "datetime";
153
154
  const yFormatted = yIsDate
154
155
  ? formatDateTickValue(yVal)
155
156
  : formatTickValue(yVal);
156
- lines.push(`${chart._yLabel}: ${yFormatted}`);
157
+ grid.push([chart._yLabel, yFormatted]);
157
158
 
158
- return lines;
159
+ return grid;
159
160
  }
160
161
 
161
162
  tooltipOptions() {
162
- return { crosshair: true, highlightRadius: 5 };
163
+ return { crosshair: true, highlightRadius: 5, axisIndicators: true };
163
164
  }
164
165
 
165
166
  destroy(chart: CartesianChart): void {
@@ -195,10 +196,13 @@ function bindLineState(
195
196
  gl.uniformMatrix4fv(cache.u_projection, false, projection);
196
197
  gl.uniform2f(cache.u_resolution, gl.canvas.width, gl.canvas.height);
197
198
  gl.uniform1f(cache.u_line_width, chart._pluginConfig.line_width_px * dpr);
198
- if (chart._colorMin < chart._colorMax) {
199
- gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
200
- } else {
199
+ if (chart._colorMin >= chart._colorMax) {
201
200
  gl.uniform2f(cache.u_color_range, 0.0, 0.0);
201
+ } else if (chart._colorName && !chart._colorIsString) {
202
+ const [lo, hi] = colorRangePivot(chart._colorMin, chart._colorMax);
203
+ gl.uniform2f(cache.u_color_range, lo, hi);
204
+ } else {
205
+ gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
202
206
  }
203
207
 
204
208
  bindGradientTexture(
@@ -14,6 +14,7 @@ import type { WebGLContextManager } from "../../../webgl/context-manager";
14
14
  import type { CartesianChart } from "../cartesian";
15
15
  import type { Glyph } from "../glyph";
16
16
  import { bindGradientTexture } from "../../../webgl/gradient-texture";
17
+ import { colorRangePivot } from "../../../theme/gradient";
17
18
  import { compileProgram } from "../../../webgl/program-cache";
18
19
  import { buildPointRowTooltipLines } from "../tooltip-lines";
19
20
  import scatterVert from "../../../shaders/scatter.vert.glsl";
@@ -130,12 +131,12 @@ export class PointGlyph implements Glyph {
130
131
  buildTooltipLines(
131
132
  chart: CartesianChart,
132
133
  flatIdx: number,
133
- ): Promise<string[]> {
134
+ ): Promise<string[][]> {
134
135
  return buildPointRowTooltipLines(chart, flatIdx);
135
136
  }
136
137
 
137
138
  tooltipOptions() {
138
- return { crosshair: true, highlightRadius: 6 };
139
+ return { crosshair: true, highlightRadius: 6, axisIndicators: true };
139
140
  }
140
141
 
141
142
  destroy(_chart: CartesianChart): void {
@@ -155,10 +156,13 @@ function setUniforms(
155
156
  gl.uniformMatrix4fv(cache.u_projection, false, projection);
156
157
  gl.uniform1f(cache.u_point_size, chart._pluginConfig.point_size_px * dpr);
157
158
 
158
- if (chart._colorMin < chart._colorMax) {
159
- gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
160
- } else {
159
+ if (chart._colorMin >= chart._colorMax) {
161
160
  gl.uniform2f(cache.u_color_range, 0.0, 0.0);
161
+ } else if (chart._colorName && !chart._colorIsString) {
162
+ const [lo, hi] = colorRangePivot(chart._colorMin, chart._colorMax);
163
+ gl.uniform2f(cache.u_color_range, lo, hi);
164
+ } else {
165
+ gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
162
166
  }
163
167
 
164
168
  if (chart._sizeMin < chart._sizeMax) {
@@ -24,22 +24,22 @@ import type { CartesianChart } from "./cartesian";
24
24
  export async function buildPointRowTooltipLines(
25
25
  chart: CartesianChart,
26
26
  flatIdx: number,
27
- ): Promise<string[]> {
28
- const lines: string[] = [];
27
+ ): Promise<string[][]> {
28
+ const grid: string[][] = [];
29
29
  if (!chart._rowIndexData || !chart._lazyRows) {
30
- return lines;
30
+ return grid;
31
31
  }
32
32
 
33
33
  const rowIdx = chart._rowIndexData[flatIdx];
34
34
  if (rowIdx < 0) {
35
- return lines;
35
+ return grid;
36
36
  }
37
37
 
38
38
  if (chart._splitGroups.length > 0 && chart._seriesCapacity > 0) {
39
39
  const seriesIdx = Math.floor(flatIdx / chart._seriesCapacity);
40
40
  const sg = chart._splitGroups[seriesIdx];
41
41
  if (sg?.prefix) {
42
- lines.push(sg.prefix);
42
+ grid.push([sg.prefix]);
43
43
  }
44
44
  }
45
45
 
@@ -70,11 +70,11 @@ export async function buildPointRowTooltipLines(
70
70
 
71
71
  if (typeof value === "number") {
72
72
  const formatted = chart.getColumnFormatter(colName, "value")(value);
73
- lines.push(`${displayName}: ${formatted}`);
73
+ grid.push([displayName, formatted]);
74
74
  } else {
75
- lines.push(`${displayName}: ${value}`);
75
+ grid.push([displayName, String(value)]);
76
76
  }
77
77
  }
78
78
 
79
- return lines;
79
+ return grid;
80
80
  }
@@ -21,7 +21,11 @@ import {
21
21
  } from "@perspective-dev/viewer/src/ts/column-format.js";
22
22
  import type { ColumnDataMap } from "../data/view-reader";
23
23
  import { LazyRowFetcher } from "../data/lazy-row";
24
- import { formatTickValue, formatDateTickValue } from "../layout/ticks";
24
+ import {
25
+ CHART_DATE_DEFAULTS,
26
+ CHART_DATETIME_DEFAULTS,
27
+ CHART_NUMBER_DEFAULTS,
28
+ } from "../plugin/format-defaults";
25
29
  import type { WebGLContextManager } from "../webgl/context-manager";
26
30
  import {
27
31
  ZoomController,
@@ -41,51 +45,37 @@ import {
41
45
  type UserClickPayload,
42
46
  type UserSelectPayload,
43
47
  } from "../interaction/tooltip-controller";
48
+ import { LegendController } from "../interaction/legend-controller";
44
49
  import type { PerspectiveClickDetail } from "../event-detail";
45
50
  import type { ViewConfig } from "@perspective-dev/client";
46
51
  import { resolveThemeFromVars, type Theme } from "../theme/theme";
52
+ import { applyColumnColorOverrides } from "../theme/overrides";
47
53
  import { requestRender as scheduleRender } from "../render/scheduler";
48
54
 
49
- // TODO I don't know if this is the behavior we want. On the plus side, this
50
- // ad-hoc formatter scales well to small and large data ranges, making a good
51
- // guess at the right format without user input. On the minus side, this
52
- // behavior is inconsistent with datagrid and the rest of the app, and the ad-hoc
53
- // surprising behavior when overriding one field in `number_format` and suddenly
54
- // the entire formatter is replaced.
55
- const REGRESSION_BEHAVIOR = true;
56
-
57
55
  /**
58
- * Locale-aware fallback formatter applied to numeric tooltip / legend
59
- * values when the column has no `number_format` configured. Two
60
- * fractional digits matches the legacy datagrid default and gives
61
- * tooltips a stable display width.
56
+ * Fallback formatter applied to numeric tooltip / legend values when the
57
+ * column has no `number_format` configured.
62
58
  */
63
59
  const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
64
60
  v: number,
65
61
  ) => string) => {
66
- if (REGRESSION_BEHAVIOR) {
67
- return formatTickValue;
68
- } else {
69
- const intl = createNumberFormatter("float");
70
- return (v) => intl.format(v);
71
- }
62
+ const intl = createNumberFormatter(
63
+ "float",
64
+ undefined,
65
+ CHART_NUMBER_DEFAULTS,
66
+ );
67
+ return (v) => intl.format(v);
72
68
  })();
73
69
 
74
70
  /**
75
- * Locale-aware fallback formatter for datetime tooltip / legend values
76
- * when the column has no `date_format` configured. Uses the locale
77
- * default (no `dateStyle` / `timeStyle`) to match what most users
78
- * expect from an `Intl.DateTimeFormat()` constructed with no options.
71
+ * Fallback formatter for datetime tooltip / legend values when the
72
+ * column has no `date_format` configured.
79
73
  */
80
74
  const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
81
75
  v: number,
82
76
  ) => string) => {
83
- if (REGRESSION_BEHAVIOR) {
84
- return formatDateTickValue;
85
- } else {
86
- const intl = createDatetimeFormatter();
87
- return (v) => intl.format(v);
88
- }
77
+ const intl = createDatetimeFormatter(undefined, CHART_DATETIME_DEFAULTS);
78
+ return (v) => intl.format(v);
89
79
  })();
90
80
 
91
81
  /**
@@ -141,6 +131,7 @@ export abstract class AbstractChart implements ChartImplementation {
141
131
  _glManager: WebGLContextManager | null = null;
142
132
  _gridlineCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
143
133
  _chromeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
134
+ _overlayPresenter: (() => void) | null = null;
144
135
 
145
136
  /**
146
137
  * 2D-canvas draw closures collected during `_fullRender` (the GL
@@ -236,6 +227,7 @@ export abstract class AbstractChart implements ChartImplementation {
236
227
  */
237
228
  _pluginConfig: PluginConfig = { ...DEFAULT_PLUGIN_CONFIG };
238
229
 
230
+ _legend = new LegendController();
239
231
  _tooltip = new TooltipController();
240
232
 
241
233
  /**
@@ -259,12 +251,16 @@ export abstract class AbstractChart implements ChartImplementation {
259
251
 
260
252
  /**
261
253
  * Cached resolved theme — populated on first `_resolveTheme()` call,
262
- * cleared by `invalidateTheme()` (driven from `plugin.restyle()`).
263
- * `getComputedStyle` / `getPropertyValue` reads cost ~100µs each;
264
- * zoom/hover dispatch redraws at 60Hz so we resolve once and reuse.
254
+ * cleared by `invalidateTheme()` (driven from `plugin.restyle()`)
255
+ * and by `setColumnsConfig` (per-column color overrides patch the
256
+ * resolved theme). `getComputedStyle` / `getPropertyValue` reads
257
+ * cost ~100µs each; zoom/hover dispatch redraws at 60Hz so we
258
+ * resolve once and reuse.
265
259
  */
266
260
  _theme: Theme | null = null;
267
261
 
262
+ _themeColorColumn: string | null = null;
263
+
268
264
  /**
269
265
  * On-demand single-row fetcher used by lazy tooltip column
270
266
  * lookups. Reset on every `setView` call; subclasses read
@@ -287,6 +283,18 @@ export abstract class AbstractChart implements ChartImplementation {
287
283
  this._chromeCanvas = canvas;
288
284
  }
289
285
 
286
+ setOverlayPresenter(cb: () => void): void {
287
+ this._overlayPresenter = cb;
288
+ }
289
+
290
+ /**
291
+ * Present a chrome-overlay repaint that ran outside the scheduler's
292
+ * frame chain.
293
+ */
294
+ presentOverlay(): void {
295
+ this._overlayPresenter?.();
296
+ }
297
+
290
298
  setTheme(vars: Record<string, string>): void {
291
299
  this._themeVars = vars;
292
300
  this._theme = null;
@@ -434,6 +442,8 @@ export abstract class AbstractChart implements ChartImplementation {
434
442
  setColumnsConfig(cfg: Record<string, any>): void {
435
443
  this._columnsConfig = cfg ?? {};
436
444
  this._rebuildColumnFormatters();
445
+
446
+ this._theme = null;
437
447
  }
438
448
 
439
449
  /**
@@ -475,7 +485,11 @@ export abstract class AbstractChart implements ChartImplementation {
475
485
  return undefined;
476
486
  }
477
487
 
478
- const intl = createNumberFormatter(type, numberFormat);
488
+ const intl = createNumberFormatter(
489
+ type,
490
+ numberFormat,
491
+ CHART_NUMBER_DEFAULTS,
492
+ );
479
493
  return (v) => intl.format(v);
480
494
  }
481
495
 
@@ -485,7 +499,10 @@ export abstract class AbstractChart implements ChartImplementation {
485
499
  return undefined;
486
500
  }
487
501
 
488
- const intl = createDatetimeFormatter(dateFormat);
502
+ const intl = createDatetimeFormatter(
503
+ dateFormat,
504
+ CHART_DATETIME_DEFAULTS,
505
+ );
489
506
  return (v) => intl.format(v);
490
507
  }
491
508
 
@@ -495,7 +512,7 @@ export abstract class AbstractChart implements ChartImplementation {
495
512
  return undefined;
496
513
  }
497
514
 
498
- const intl = createDateFormatter(dateFormat);
515
+ const intl = createDateFormatter(dateFormat, CHART_DATE_DEFAULTS);
499
516
  return (v) => intl.format(v);
500
517
  }
501
518
 
@@ -588,6 +605,11 @@ export abstract class AbstractChart implements ChartImplementation {
588
605
  * inputs in `uploadAndRender`; they take effect on next data load.
589
606
  */
590
607
  setPluginConfig(cfg: PluginConfig): void {
608
+ // Persistence echo vs. real change: a restore whose legend
609
+ // fields equal the current values (the round-trip of a
610
+ // completed drag) must not disturb legend scroll or an
611
+ // in-flight gesture; different values win and cancel any drag.
612
+ this._legend.reconcileConfig(this._pluginConfig, cfg);
591
613
  this._pluginConfig = { ...cfg };
592
614
  this._facetConfig = {
593
615
  ...this._facetConfig,
@@ -599,16 +621,28 @@ export abstract class AbstractChart implements ChartImplementation {
599
621
  cfg.series_zoom_mode === "dynamic";
600
622
  }
601
623
 
624
+ protected colorScaleColumn(): string | null {
625
+ return null;
626
+ }
627
+
602
628
  /**
603
- * Lazily decode the host-supplied theme vars. Subsequent calls hit
604
- * the cache until `invalidateTheme()` clears it. Render-path
605
- * callers should always read theme values through this method so
606
- * the parsed `Theme` (gradient stops, palette, etc.) amortizes
607
- * across an entire frame.
629
+ * Lazily decode the host-supplied theme vars, then patch in any
630
+ * per-column color-scale override for `colorScaleColumn()`.
631
+ * Subsequent calls hit the cache until `invalidateTheme()` /
632
+ * `setColumnsConfig` clears it or the color column changes.
633
+ * Render-path callers should always read theme values through this
634
+ * method so the parsed `Theme` (gradient stops, palette, etc.)
635
+ * amortizes across an entire frame.
608
636
  */
609
637
  _resolveTheme(): Theme {
610
- if (!this._theme) {
611
- this._theme = resolveThemeFromVars(this._themeVars);
638
+ const colorColumn = this.colorScaleColumn();
639
+ if (!this._theme || this._themeColorColumn !== colorColumn) {
640
+ this._themeColorColumn = colorColumn;
641
+ this._theme = applyColumnColorOverrides(
642
+ resolveThemeFromVars(this._themeVars),
643
+ this._columnsConfig,
644
+ colorColumn,
645
+ );
612
646
  }
613
647
 
614
648
  return this._theme;
@@ -11,6 +11,7 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  import type { View } from "@perspective-dev/client";
14
+ import { TILE_SOURCES } from "../map/tile-source";
14
15
  import type { ColumnDataMap } from "../data/view-reader";
15
16
  import type { WebGLContextManager } from "../webgl/context-manager";
16
17
  import type { ZoomController } from "../interaction/zoom-controller";
@@ -84,6 +85,11 @@ export interface ChartImplementation {
84
85
  */
85
86
  setChromeCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void;
86
87
 
88
+ /**
89
+ * Install the renderer's overlay-present hook.
90
+ */
91
+ setOverlayPresenter?(cb: () => void): void;
92
+
87
93
  /**
88
94
  * Hand the chart a pre-computed CSS-variable map produced on the
89
95
  * main thread via `snapshotThemeVars(el)`, which it can decode into
@@ -193,6 +199,8 @@ export interface ChartImplementation {
193
199
  */
194
200
  deselect?(): void;
195
201
 
202
+ repaintChrome?(): void;
203
+
196
204
  destroy(): void;
197
205
  }
198
206
 
@@ -426,13 +434,15 @@ export interface PluginConfig {
426
434
  gradient_color_mode: "mean" | "density" | "extreme" | "signed";
427
435
 
428
436
  /**
429
- * Map basemap tile provider. Applies only to map plugin tags
430
- * (`map-scatter`, `map-line`, `map-density`). Cartesian charts
431
- * ignore the field. Surfaced as an enum on the settings panel so
432
- * users can switch light/dark/voyager without writing custom
433
- * tile-source code.
437
+ * Map basemap tile provider — a `TileSourceSpec` id from the
438
+ * tile-source registry ([map/tile-sources.json] entries plus any
439
+ * runtime `registerTileSource` additions). Applies only to map
440
+ * plugin tags (`map-scatter`, `map-line`, `map-density`); other
441
+ * charts ignore the field. The default is the JSON's FIRST entry
442
+ * (reordering the file changes the default), and unknown ids fall
443
+ * back to that same entry rather than blanking the map.
434
444
  */
435
- map_tile_provider: "carto-positron" | "carto-dark-matter" | "carto-voyager";
445
+ map_tile_provider: string;
436
446
 
437
447
  /**
438
448
  * Map basemap alpha (0..1). Pre-multiplied into the tile fragment
@@ -441,8 +451,91 @@ export interface PluginConfig {
441
451
  * shows the tiles at full opacity.
442
452
  */
443
453
  map_tile_alpha: number;
454
+
455
+ /**
456
+ * Map plugins only. `true` (default): standard numeric axes in the
457
+ * usual cartesian gutters, with tick labels in degrees
458
+ * longitude/latitude (`122.4°W`) rather than Mercator meters.
459
+ * `false`: no axes, and the plot is full-bleed — the basemap fills
460
+ * the entire canvas, minus only the sidebar legend gutter when a
461
+ * legend is actually shown. Gridlines are never drawn in map mode
462
+ * — the gridline canvas composites BELOW the GL layer, so opaque
463
+ * basemap tiles would hide them.
464
+ */
465
+ numeric_axes: boolean;
466
+
467
+ /**
468
+ * Legend presentation mode. `"auto"` (default) resolves per frame
469
+ * to `"floating"` when every entry fits the default floating panel
470
+ * without scrolling (≤ 7 entries; continuous gradient legends
471
+ * always qualify) and to `"sidebar"` otherwise — see
472
+ * `resolveLegendMode`. Treemap overrides the default to
473
+ * `"sidebar"` (a floating panel over edge-to-edge tiles always
474
+ * occludes data).
475
+ */
476
+ legend_mode: "auto" | "sidebar" | "none" | "floating";
477
+
478
+ /**
479
+ * Floating-panel sizing regime. `"auto"` (default) sizes the panel
480
+ * to its CONTENT every frame — the height hugs the entry rows
481
+ * exactly, and the width hugs the widest entry label (measured on
482
+ * the chrome canvas). `"fixed"` uses the saved `legend_width_px` /
483
+ * `legend_height_px` verbatim.
484
+ *
485
+ * Applies to `legend_mode: "floating"` ONLY; the sidebar gutter is
486
+ * always `legend_width_px` (its height is the plot's). Dragging a
487
+ * resize handle switches an auto panel to `"fixed"` — otherwise
488
+ * the gesture would be undone by the next paint — and
489
+ * double-clicking a resize handle switches it back.
490
+ */
491
+ legend_size_mode: LegendSizeMode;
492
+
493
+ /**
494
+ * Legend width in CSS pixels. `0` (default) = automatic — each
495
+ * chart family keeps its historical gutter width (80–96px). In
496
+ * `"sidebar"` mode this is the full right-gutter width; in
497
+ * `"floating"` mode it is the panel width. Clamped at paint time
498
+ * to at most half the canvas width so a saved wide legend cannot
499
+ * crush a small panel. Ignored by a floating panel in
500
+ * `legend_size_mode: "auto"`.
501
+ */
502
+ legend_width_px: number;
503
+
504
+ /**
505
+ * Floating-legend panel height in CSS pixels. Ignored in
506
+ * `"sidebar"` mode (the legend spans the plot height) and by a
507
+ * floating panel in `legend_size_mode: "auto"`. Clamped at paint
508
+ * time to the canvas height.
509
+ */
510
+ legend_height_px: number;
511
+
512
+ /**
513
+ * Canvas corner that `legend_x` / `legend_y` are measured FROM.
514
+ * Floating mode only. The panel keeps its distance to this corner
515
+ * across panel resizes — anchor `"bottom-right"` with small
516
+ * offsets stays glued to the bottom-right.
517
+ */
518
+ legend_anchor: LegendAnchor;
519
+
520
+ legend_x: number;
521
+ legend_y: number;
522
+ legend_opacity: number;
523
+
524
+ /** Per-column width cap for tooltip grid cells, in CSS pixels. */
525
+ tooltip_max_column_px: number;
526
+
527
+ /** Tooltip background/border alpha (0..1). */
528
+ tooltip_opacity: number;
444
529
  }
445
530
 
531
+ export type LegendAnchor =
532
+ | "top-left"
533
+ | "top-right"
534
+ | "bottom-left"
535
+ | "bottom-right";
536
+
537
+ export type LegendSizeMode = "auto" | "fixed";
538
+
446
539
  export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
447
540
  auto_alt_y_axis: false,
448
541
  facet_mode: "grid",
@@ -460,6 +553,17 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
460
553
  gradient_intensity: 0.6,
461
554
  gradient_heat_max: 4.0,
462
555
  gradient_color_mode: "mean",
463
- map_tile_provider: "carto-positron",
556
+ map_tile_provider: TILE_SOURCES.list()[0].id,
464
557
  map_tile_alpha: 1.0,
558
+ numeric_axes: true,
559
+ legend_mode: "auto",
560
+ legend_size_mode: "auto",
561
+ legend_width_px: 0,
562
+ legend_height_px: 160,
563
+ legend_anchor: "top-right",
564
+ legend_x: 0,
565
+ legend_y: 0,
566
+ legend_opacity: 0.8,
567
+ tooltip_max_column_px: 160,
568
+ tooltip_opacity: 0.8,
465
569
  };
@@ -203,15 +203,27 @@ export function synthesizeStringLevel(
203
203
  const seen = new Map<string, number>();
204
204
  seen.set("", 0);
205
205
 
206
+ const valueIdx = new Map<number, number>();
207
+
206
208
  for (let r = 0; r < numRows; r++) {
207
209
  const isValid = valid ? !!((valid[r >> 3] >> (r & 7)) & 1) : true;
210
+ if (!isValid) {
211
+ indices[r] = 0;
212
+ continue;
213
+ }
214
+
208
215
  const v = values[r] as number;
209
- const label = formatLevelValue(v, isValid, levelType);
210
- let dictIdx = seen.get(label);
216
+ let dictIdx = valueIdx.get(v);
211
217
  if (dictIdx === undefined) {
212
- dictIdx = dictionary.length;
213
- dictionary.push(label);
214
- seen.set(label, dictIdx);
218
+ const label = formatLevelValue(v, true, levelType);
219
+ dictIdx = seen.get(label);
220
+ if (dictIdx === undefined) {
221
+ dictIdx = dictionary.length;
222
+ dictionary.push(label);
223
+ seen.set(label, dictIdx);
224
+ }
225
+
226
+ valueIdx.set(v, dictIdx);
215
227
  }
216
228
 
217
229
  indices[r] = dictIdx;
@@ -240,7 +252,34 @@ export function resolveCategoryAxis(
240
252
  numRows: number,
241
253
  groupByLen: number,
242
254
  levelTypes: string[] = [],
255
+ axisMode?: AxisMode,
243
256
  ): CategoryAxisResult {
257
+ if (axisMode?.mode === "numeric") {
258
+ const rp = columns.get("__ROW_PATH_0__");
259
+ if (rp && rp.type !== "string" && rp.values) {
260
+ let rowOffset = 0;
261
+ const values = rp.values;
262
+ const valid = rp.valid;
263
+ while (rowOffset < numRows) {
264
+ const isValid = valid
265
+ ? !!((valid[rowOffset >> 3] >> (rowOffset & 7)) & 1)
266
+ : true;
267
+ const v = values[rowOffset] as number;
268
+ if (isValid && v != null && !Number.isNaN(v)) {
269
+ break;
270
+ }
271
+
272
+ rowOffset++;
273
+ }
274
+
275
+ return {
276
+ rowPaths: [],
277
+ numCategories: Math.max(0, numRows - rowOffset),
278
+ rowOffset,
279
+ };
280
+ }
281
+ }
282
+
244
283
  type RawLevel = { indices: Int32Array; dictionary: string[] };
245
284
  const rawRowPaths: RawLevel[] = [];
246
285
  for (let n = 0; ; n++) {