@perspective-dev/viewer-charts 5.3.0 → 5.4.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 (92) 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/cartesian/cartesian.d.ts +6 -0
  7. package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
  8. package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
  9. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
  10. package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
  11. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
  12. package/dist/esm/charts/chart-base.d.ts +2 -0
  13. package/dist/esm/charts/chart.d.ts +77 -6
  14. package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
  15. package/dist/esm/charts/common/tree-interact.d.ts +1 -1
  16. package/dist/esm/charts/series/series-build.d.ts +7 -0
  17. package/dist/esm/charts/series/series-interact.d.ts +1 -1
  18. package/dist/esm/charts/series/series.d.ts +6 -0
  19. package/dist/esm/index.d.ts +2 -0
  20. package/dist/esm/interaction/axis-indicators.d.ts +11 -0
  21. package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
  22. package/dist/esm/interaction/host-sink-message.d.ts +3 -2
  23. package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
  24. package/dist/esm/interaction/legend-controller.d.ts +246 -0
  25. package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
  26. package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
  27. package/dist/esm/layout/facet-grid.d.ts +6 -0
  28. package/dist/esm/layout/ticks.d.ts +1 -0
  29. package/dist/esm/map/tile-source.d.ts +77 -8
  30. package/dist/esm/perspective-viewer-charts.js +1 -2
  31. package/dist/esm/perspective-viewer-charts.js.map +4 -4
  32. package/dist/esm/plugin/charts.d.ts +2 -0
  33. package/dist/esm/plugin/format-defaults.d.ts +9 -0
  34. package/dist/esm/plugin/plugin.d.ts +20 -0
  35. package/dist/esm/theme/gradient.d.ts +1 -0
  36. package/dist/esm/transport/protocol.d.ts +45 -3
  37. package/dist/esm/transport/renderer-transport.d.ts +3 -0
  38. package/dist/esm/worker/renderer.worker.d.ts +19 -1
  39. package/package.json +6 -6
  40. package/src/css/perspective-viewer-charts.css +0 -1
  41. package/src/ts/axis/bar-axis.ts +2 -7
  42. package/src/ts/axis/legend.ts +351 -24
  43. package/src/ts/axis/map-ticks.ts +94 -0
  44. package/src/ts/axis/numeric-axis.ts +2 -9
  45. package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
  46. package/src/ts/charts/candlestick/candlestick-render.ts +73 -10
  47. package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
  48. package/src/ts/charts/cartesian/cartesian-render.ts +369 -75
  49. package/src/ts/charts/cartesian/cartesian.ts +9 -0
  50. package/src/ts/charts/cartesian/glyph.ts +6 -2
  51. package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
  52. package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
  53. package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
  54. package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
  55. package/src/ts/charts/chart-base.ts +34 -32
  56. package/src/ts/charts/chart.ts +106 -7
  57. package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
  58. package/src/ts/charts/common/tree-chrome.ts +102 -23
  59. package/src/ts/charts/common/tree-interact.ts +18 -17
  60. package/src/ts/charts/heatmap/heatmap-interact.ts +65 -7
  61. package/src/ts/charts/heatmap/heatmap-render.ts +100 -24
  62. package/src/ts/charts/map/map.ts +3 -5
  63. package/src/ts/charts/series/series-build.ts +14 -2
  64. package/src/ts/charts/series/series-interact.ts +12 -11
  65. package/src/ts/charts/series/series-render.ts +305 -132
  66. package/src/ts/charts/series/series.ts +10 -0
  67. package/src/ts/charts/sunburst/sunburst-render.ts +7 -1
  68. package/src/ts/charts/treemap/treemap-render.ts +7 -1
  69. package/src/ts/data/view-reader.ts +1 -4
  70. package/src/ts/index.ts +2 -0
  71. package/src/ts/interaction/axis-indicators.ts +137 -0
  72. package/src/ts/interaction/host-sink-dom.ts +66 -2
  73. package/src/ts/interaction/host-sink-message.ts +8 -2
  74. package/src/ts/interaction/lazy-tooltip.ts +5 -5
  75. package/src/ts/interaction/legend-controller.ts +1084 -0
  76. package/src/ts/interaction/tooltip-controller.ts +43 -131
  77. package/src/ts/interaction/tooltip-grid.ts +187 -0
  78. package/src/ts/layout/facet-grid.ts +8 -1
  79. package/src/ts/layout/ticks.ts +12 -0
  80. package/src/ts/map/tile-loader.ts +16 -1
  81. package/src/ts/map/tile-source.ts +199 -53
  82. package/src/ts/map/tile-sources.json +18 -0
  83. package/src/ts/plugin/charts.ts +40 -6
  84. package/src/ts/plugin/format-defaults.ts +37 -0
  85. package/src/ts/plugin/plugin.ts +236 -67
  86. package/src/ts/shaders/line.vert.glsl +5 -2
  87. package/src/ts/shaders/scatter.vert.glsl +0 -7
  88. package/src/ts/theme/gradient.ts +24 -0
  89. package/src/ts/transport/protocol.ts +46 -2
  90. package/src/ts/transport/renderer-transport.ts +83 -4
  91. package/src/ts/worker/dispatch.ts +6 -2
  92. package/src/ts/worker/renderer.worker.ts +78 -2
@@ -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,52 +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";
47
52
  import { applyColumnColorOverrides } from "../theme/overrides";
48
53
  import { requestRender as scheduleRender } from "../render/scheduler";
49
54
 
50
- // TODO I don't know if this is the behavior we want. On the plus side, this
51
- // ad-hoc formatter scales well to small and large data ranges, making a good
52
- // guess at the right format without user input. On the minus side, this
53
- // behavior is inconsistent with datagrid and the rest of the app, and the ad-hoc
54
- // surprising behavior when overriding one field in `number_format` and suddenly
55
- // the entire formatter is replaced.
56
- const REGRESSION_BEHAVIOR = true;
57
-
58
55
  /**
59
- * Locale-aware fallback formatter applied to numeric tooltip / legend
60
- * values when the column has no `number_format` configured. Two
61
- * fractional digits matches the legacy datagrid default and gives
62
- * tooltips a stable display width.
56
+ * Fallback formatter applied to numeric tooltip / legend values when the
57
+ * column has no `number_format` configured.
63
58
  */
64
59
  const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
65
60
  v: number,
66
61
  ) => string) => {
67
- if (REGRESSION_BEHAVIOR) {
68
- return formatTickValue;
69
- } else {
70
- const intl = createNumberFormatter("float");
71
- return (v) => intl.format(v);
72
- }
62
+ const intl = createNumberFormatter(
63
+ "float",
64
+ undefined,
65
+ CHART_NUMBER_DEFAULTS,
66
+ );
67
+ return (v) => intl.format(v);
73
68
  })();
74
69
 
75
70
  /**
76
- * Locale-aware fallback formatter for datetime tooltip / legend values
77
- * when the column has no `date_format` configured. Uses the locale
78
- * default (no `dateStyle` / `timeStyle`) to match what most users
79
- * 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.
80
73
  */
81
74
  const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
82
75
  v: number,
83
76
  ) => string) => {
84
- if (REGRESSION_BEHAVIOR) {
85
- return formatDateTickValue;
86
- } else {
87
- const intl = createDatetimeFormatter();
88
- return (v) => intl.format(v);
89
- }
77
+ const intl = createDatetimeFormatter(undefined, CHART_DATETIME_DEFAULTS);
78
+ return (v) => intl.format(v);
90
79
  })();
91
80
 
92
81
  /**
@@ -238,6 +227,7 @@ export abstract class AbstractChart implements ChartImplementation {
238
227
  */
239
228
  _pluginConfig: PluginConfig = { ...DEFAULT_PLUGIN_CONFIG };
240
229
 
230
+ _legend = new LegendController();
241
231
  _tooltip = new TooltipController();
242
232
 
243
233
  /**
@@ -495,7 +485,11 @@ export abstract class AbstractChart implements ChartImplementation {
495
485
  return undefined;
496
486
  }
497
487
 
498
- const intl = createNumberFormatter(type, numberFormat);
488
+ const intl = createNumberFormatter(
489
+ type,
490
+ numberFormat,
491
+ CHART_NUMBER_DEFAULTS,
492
+ );
499
493
  return (v) => intl.format(v);
500
494
  }
501
495
 
@@ -505,7 +499,10 @@ export abstract class AbstractChart implements ChartImplementation {
505
499
  return undefined;
506
500
  }
507
501
 
508
- const intl = createDatetimeFormatter(dateFormat);
502
+ const intl = createDatetimeFormatter(
503
+ dateFormat,
504
+ CHART_DATETIME_DEFAULTS,
505
+ );
509
506
  return (v) => intl.format(v);
510
507
  }
511
508
 
@@ -515,7 +512,7 @@ export abstract class AbstractChart implements ChartImplementation {
515
512
  return undefined;
516
513
  }
517
514
 
518
- const intl = createDateFormatter(dateFormat);
515
+ const intl = createDateFormatter(dateFormat, CHART_DATE_DEFAULTS);
519
516
  return (v) => intl.format(v);
520
517
  }
521
518
 
@@ -608,6 +605,11 @@ export abstract class AbstractChart implements ChartImplementation {
608
605
  * inputs in `uploadAndRender`; they take effect on next data load.
609
606
  */
610
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);
611
613
  this._pluginConfig = { ...cfg };
612
614
  this._facetConfig = {
613
615
  ...this._facetConfig,
@@ -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";
@@ -198,6 +199,8 @@ export interface ChartImplementation {
198
199
  */
199
200
  deselect?(): void;
200
201
 
202
+ repaintChrome?(): void;
203
+
201
204
  destroy(): void;
202
205
  }
203
206
 
@@ -431,13 +434,15 @@ export interface PluginConfig {
431
434
  gradient_color_mode: "mean" | "density" | "extreme" | "signed";
432
435
 
433
436
  /**
434
- * Map basemap tile provider. Applies only to map plugin tags
435
- * (`map-scatter`, `map-line`, `map-density`). Cartesian charts
436
- * ignore the field. Surfaced as an enum on the settings panel so
437
- * users can switch light/dark/voyager without writing custom
438
- * 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.
439
444
  */
440
- map_tile_provider: "carto-positron" | "carto-dark-matter" | "carto-voyager";
445
+ map_tile_provider: string;
441
446
 
442
447
  /**
443
448
  * Map basemap alpha (0..1). Pre-multiplied into the tile fragment
@@ -446,8 +451,91 @@ export interface PluginConfig {
446
451
  * shows the tiles at full opacity.
447
452
  */
448
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;
449
529
  }
450
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
+
451
539
  export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
452
540
  auto_alt_y_axis: false,
453
541
  facet_mode: "grid",
@@ -465,6 +553,17 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
465
553
  gradient_intensity: 0.6,
466
554
  gradient_heat_max: 4.0,
467
555
  gradient_color_mode: "mean",
468
- map_tile_provider: "carto-positron",
556
+ map_tile_provider: TILE_SOURCES.list()[0].id,
469
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,
470
569
  };
@@ -12,6 +12,13 @@
12
12
 
13
13
  import type { Context2D } from "../canvas-types";
14
14
  import type { Theme } from "../../theme/theme";
15
+ import {
16
+ measureTooltipGrid,
17
+ paintTooltipGrid,
18
+ DEFAULT_TOOLTIP_STYLE,
19
+ type TooltipContent,
20
+ type TooltipStyle,
21
+ } from "../../interaction/tooltip-grid";
15
22
 
16
23
  /**
17
24
  * Draw a freestanding tooltip box anchored near (cx, cy), measuring
@@ -22,32 +29,21 @@ import type { Theme } from "../../theme/theme";
22
29
  export function drawTooltipBox(
23
30
  ctx: Context2D,
24
31
  theme: Theme,
25
- lines: string[],
32
+ grid: TooltipContent,
26
33
  cx: number,
27
34
  cy: number,
28
35
  cssWidth: number,
29
36
  cssHeight: number,
30
37
  fontFamily: string,
38
+ style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
31
39
  ): void {
32
- if (lines.length === 0) {
40
+ if (grid.length === 0) {
33
41
  return;
34
42
  }
35
43
 
36
- const { tooltipBg, tooltipText, tooltipBorder } = theme;
37
-
38
44
  ctx.font = `11px ${fontFamily}`;
39
- const lineHeight = 16;
40
- const padding = 8;
41
- let maxWidth = 0;
42
- for (const line of lines) {
43
- const w = ctx.measureText(line).width;
44
- if (w > maxWidth) {
45
- maxWidth = w;
46
- }
47
- }
48
-
49
- const boxW = maxWidth + padding * 2;
50
- const boxH = lines.length * lineHeight + padding * 2 - 4;
45
+ const measured = measureTooltipGrid(ctx, grid, style.maxColumnPx);
46
+ const { boxW, boxH } = measured;
51
47
 
52
48
  let tx = cx + 12;
53
49
  let ty = cy - boxH - 8;
@@ -67,18 +63,5 @@ export function drawTooltipBox(
67
63
  ty = cssHeight - boxH - 4;
68
64
  }
69
65
 
70
- ctx.fillStyle = tooltipBg;
71
- ctx.strokeStyle = tooltipBorder;
72
- ctx.lineWidth = 1;
73
- ctx.beginPath();
74
- ctx.roundRect(tx, ty, boxW, boxH, 4);
75
- ctx.fill();
76
- ctx.stroke();
77
-
78
- ctx.fillStyle = tooltipText;
79
- ctx.textAlign = "left";
80
- ctx.textBaseline = "top";
81
- for (let i = 0; i < lines.length; i++) {
82
- ctx.fillText(lines[i], tx + padding, ty + padding + i * lineHeight);
83
- }
66
+ paintTooltipGrid(ctx, measured, tx, ty, theme, style.opacity);
84
67
  }