@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
@@ -14,6 +14,7 @@ import type { Client, View, ViewConfig } from "@perspective-dev/client";
14
14
  import type { FacetConfig, PluginConfig } from "../charts/chart";
15
15
  import type {
16
16
  ControlMsg,
17
+ ErrorMsg,
17
18
  InitMsg,
18
19
  InteractionEvent,
19
20
  LoadAndRenderMsg,
@@ -26,6 +27,7 @@ import {
26
27
  } from "../event-detail";
27
28
  import { snapshotThemeVars } from "../theme/theme-snapshot";
28
29
  import { snapshotFontFaces } from "../utils/font-snapshot";
30
+ import { TILE_SOURCES } from "../map/tile-source";
29
31
  import { DomHostSink } from "../interaction/host-sink-dom";
30
32
  import { RUNTIME_MODE } from "../config";
31
33
 
@@ -72,6 +74,24 @@ async function getSharedWorker(): Promise<Worker> {
72
74
  const env = e.data as WorkerEnvelope;
73
75
  HOST_LISTENERS.get(env.sessionId)?.(env.msg);
74
76
  });
77
+
78
+ let poisoned = false;
79
+ w.addEventListener("error", (event: ErrorEvent) => {
80
+ if (poisoned) {
81
+ return;
82
+ }
83
+
84
+ poisoned = true;
85
+ SHARED_WORKER = null;
86
+ const detail = event.message || `failed to load ${url}`;
87
+ const message = `perspective-viewer-charts renderer worker error: ${detail}`;
88
+ console.error(message, event.error ?? "");
89
+ const errorMsg: ErrorMsg = { kind: "error", message };
90
+ for (const listener of HOST_LISTENERS.values()) {
91
+ listener(errorMsg);
92
+ }
93
+ });
94
+
75
95
  return w;
76
96
  })();
77
97
 
@@ -156,6 +176,10 @@ export class RendererTransport {
156
176
  private _pendingCounter = 0;
157
177
  private _onZoomChanged: ((isDefault: boolean) => void) | null = null;
158
178
 
179
+ private _onPluginConfigDelta:
180
+ | ((fields: Record<string, string | number | boolean>) => void)
181
+ | null = null;
182
+
159
183
  /**
160
184
  * Cached zoom-default flag pushed by the renderer after each zoom
161
185
  * mutation. Surfaced sync via `allZoomsDefault()`; updates between
@@ -235,6 +259,9 @@ export class RendererTransport {
235
259
  maxCells: number;
236
260
  precompileShaders?: boolean;
237
261
  onZoomChanged?: (isDefault: boolean) => void;
262
+ onPluginConfigDelta?: (
263
+ fields: Record<string, string | number | boolean>,
264
+ ) => void;
238
265
  }) {
239
266
  this._client = opts.client;
240
267
  this._view = opts.view;
@@ -246,6 +273,7 @@ export class RendererTransport {
246
273
  this._maxCells = opts.maxCells;
247
274
  this._precompileShaders = opts.precompileShaders ?? false;
248
275
  this._onZoomChanged = opts.onZoomChanged ?? null;
276
+ this._onPluginConfigDelta = opts.onPluginConfigDelta ?? null;
249
277
  this._ready = new Promise((resolve, reject) => {
250
278
  this._resolveReady = resolve;
251
279
  this._rejectReady = reject;
@@ -351,6 +379,9 @@ export class RendererTransport {
351
379
  pluginConfig: opts.pluginConfig,
352
380
  columnsConfig: opts.columnsConfig,
353
381
  defaultChartType: opts.defaultChartType,
382
+ tileSource: TILE_SOURCES.specFor(
383
+ opts.pluginConfig.map_tile_provider,
384
+ ),
354
385
  themeVars,
355
386
  fontFaces,
356
387
  cssWidth: rect.width,
@@ -488,7 +519,11 @@ export class RendererTransport {
488
519
  }
489
520
 
490
521
  setPluginConfig(cfg: PluginConfig): void {
491
- this._post({ kind: "setPluginConfig", cfg });
522
+ this._post({
523
+ kind: "setPluginConfig",
524
+ cfg,
525
+ tileSource: TILE_SOURCES.specFor(cfg.map_tile_provider),
526
+ });
492
527
  }
493
528
 
494
529
  setBufferMaxCapacity(n: number): void {
@@ -807,7 +842,12 @@ export class RendererTransport {
807
842
  this._resolvePending(msg.requestId, "saveZoom", msg.state);
808
843
  break;
809
844
  case "pinTooltip":
810
- this._ensureHostSink()?.pin(msg.lines, msg.pos, msg.bounds);
845
+ this._ensureHostSink()?.pin(
846
+ msg.grid,
847
+ msg.pos,
848
+ msg.bounds,
849
+ msg.style,
850
+ );
811
851
  break;
812
852
  case "dismissTooltip":
813
853
  this._hostSink?.dismiss();
@@ -815,6 +855,9 @@ export class RendererTransport {
815
855
  case "setCursor":
816
856
  this._ensureHostSink()?.setCursor(msg.cursor);
817
857
  break;
858
+ case "pluginConfigDelta":
859
+ this._onPluginConfigDelta?.(msg.fields);
860
+ break;
818
861
  case "userClick":
819
862
  this._dispatchOnViewer(
820
863
  new CustomEvent<PerspectiveClickDetail>(
@@ -878,13 +921,35 @@ export class RendererTransport {
878
921
  this._rejectReady(new Error(msg.message));
879
922
  break;
880
923
  case "loadAndRenderAck":
881
- this._resolvePending(msg.msgId, "loadAndRender", undefined);
924
+ if (msg.error !== undefined) {
925
+ this._rejectPending(
926
+ msg.msgId,
927
+ "loadAndRender",
928
+ new Error(msg.error),
929
+ );
930
+ } else {
931
+ this._resolvePending(msg.msgId, "loadAndRender", undefined);
932
+ }
933
+
882
934
  break;
883
935
  case "resizeAck":
884
936
  this._resolvePending(msg.msgId, "resize", undefined);
885
937
  break;
886
938
  case "snapshotPngReply":
887
- this._resolvePending(msg.requestId, "snapshotPng", msg.blob);
939
+ if (msg.error !== undefined) {
940
+ this._rejectPending(
941
+ msg.requestId,
942
+ "snapshotPng",
943
+ new Error(msg.error),
944
+ );
945
+ } else {
946
+ this._resolvePending(
947
+ msg.requestId,
948
+ "snapshotPng",
949
+ msg.blob,
950
+ );
951
+ }
952
+
888
953
  break;
889
954
  }
890
955
  }
@@ -910,6 +975,20 @@ export class RendererTransport {
910
975
  entry.resolve(value);
911
976
  }
912
977
 
978
+ private _rejectPending(
979
+ id: number,
980
+ kind: PendingRenderType,
981
+ error: Error,
982
+ ): void {
983
+ const entry = this._pending.get(id);
984
+ if (!entry || entry.kind !== kind) {
985
+ return;
986
+ }
987
+
988
+ this._pending.delete(id);
989
+ entry.reject(error);
990
+ }
991
+
913
992
  /**
914
993
  * Blit-mode handler: draw a renderer-emitted frame into the
915
994
  * visible 2D-context display canvas, then close the bitmap so its
@@ -28,7 +28,7 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
28
28
  r.chartImpl.setColumnsConfig?.(msg.cfg);
29
29
  break;
30
30
  case "setPluginConfig":
31
- r.chartImpl.setPluginConfig?.(msg.cfg);
31
+ r.setPluginConfig(msg.cfg, msg.tileSource);
32
32
  r.redraw();
33
33
  break;
34
34
  case "setBufferMaxCapacity":
@@ -93,7 +93,11 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
93
93
  r.post({ kind: "snapshotPngReply", requestId, blob });
94
94
  })
95
95
  .catch((err) => {
96
- r.post({ kind: "error", message: String(err) });
96
+ r.post({
97
+ kind: "snapshotPngReply",
98
+ requestId,
99
+ error: String(err),
100
+ });
97
101
  });
98
102
  break;
99
103
  }
@@ -18,7 +18,7 @@ import type * as wasm_module_type from "@perspective-dev/viewer/dist/wasm/perspe
18
18
  import { WebGLContextManager } from "../webgl/context-manager";
19
19
  import { ContextPool } from "../webgl/context-pool";
20
20
  import { RENDER_CONTEXT_POOL_SIZE } from "../config";
21
- import { ChartImplementation } from "../charts/chart";
21
+ import { ChartImplementation, type PluginConfig } from "../charts/chart";
22
22
  import { ZoomController } from "../interaction/zoom-controller";
23
23
  import {
24
24
  applyPan,
@@ -36,6 +36,7 @@ import type {
36
36
  WorkerMsg,
37
37
  } from "../transport/protocol";
38
38
  import { viewToColumnDataMap } from "../data/view-reader";
39
+ import { TILE_SOURCES, type TileSourceSpec } from "../map/tile-source";
39
40
  import { loadFontDeduped } from "./font-loader";
40
41
  import { dispatch } from "./dispatch";
41
42
  import { installSessionHost } from "./session-host";
@@ -166,6 +167,13 @@ export class WorkerRenderer {
166
167
 
167
168
  this.chartImpl = new ImplClass();
168
169
 
170
+ // Registry write must precede the chart impl's first
171
+ // `setPluginConfig` — `pluginConfig.map_tile_provider` may
172
+ // name this runtime-registered source.
173
+ if (msg.tileSource) {
174
+ TILE_SOURCES.register(msg.tileSource);
175
+ }
176
+
169
177
  // Three surfaces, by mode:
170
178
  // - direct: the host's transferred `.webgl-canvas` (1:1 with a
171
179
  // context, permanently).
@@ -253,6 +261,20 @@ export class WorkerRenderer {
253
261
  this.chartImpl.setView?.(this.view);
254
262
  }
255
263
 
264
+ /**
265
+ * Registry write precedes the config apply — `cfg` may name the
266
+ * spec riding alongside it (see `SetPluginConfigMsg.tileSource`),
267
+ * and a replaced template must be registered first so the map
268
+ * chart's rebind sees the new content-derived cache id.
269
+ */
270
+ setPluginConfig(cfg: PluginConfig, tileSource?: TileSourceSpec): void {
271
+ if (tileSource) {
272
+ TILE_SOURCES.register(tileSource);
273
+ }
274
+
275
+ this.chartImpl.setPluginConfig?.(cfg);
276
+ }
277
+
256
278
  /**
257
279
  * Full data-fetch + render pipeline. Owns every `Client`/`Table`/
258
280
  * `View` await on the render path:
@@ -283,6 +305,7 @@ export class WorkerRenderer {
283
305
  */
284
306
  async loadAndRender(msg: LoadAndRenderMsg): Promise<void> {
285
307
  const myGen = ++this._renderGen;
308
+ let error: string | undefined;
286
309
  try {
287
310
  const [numRows, schema, exprSchema, tableSchema] =
288
311
  await Promise.all([
@@ -352,9 +375,10 @@ export class WorkerRenderer {
352
375
  } catch (err) {
353
376
  if ((err + "").indexOf("View not found") === -1) {
354
377
  console.error("loadAndRender failed", err);
378
+ error = String(err);
355
379
  }
356
380
  } finally {
357
- this.post({ kind: "loadAndRenderAck", msgId: msg.msgId });
381
+ this.post({ kind: "loadAndRenderAck", msgId: msg.msgId, error });
358
382
  }
359
383
  }
360
384
 
@@ -523,7 +547,59 @@ export class WorkerRenderer {
523
547
  return { controller: this.zoomController, layout };
524
548
  }
525
549
 
550
+ /**
551
+ * Legend-first interaction routing. The chart's `LegendController`
552
+ * sees every forwarded event BEFORE the zoom / tooltip paths, so
553
+ * legend gestures (scroll, width drag, floating move / resize)
554
+ * structurally preempt plot pan / zoom / hover — a wheel over the
555
+ * legend can never zoom the plot under it, and a floating-panel
556
+ * drag can never start a pan.
557
+ */
558
+ private _legendInteraction(event: InteractionEvent): boolean {
559
+ const chart = this.chartImpl as any;
560
+ const legend = chart?._legend;
561
+ const cfg = chart?._pluginConfig;
562
+ if (!legend || !cfg) {
563
+ return false;
564
+ }
565
+
566
+ return legend.handleEvent(event, {
567
+ cfg,
568
+ cssWidth: this.cssWidth,
569
+ cssHeight: this.cssHeight,
570
+ legendRects: chart._legendRects ?? [],
571
+ repaint: (relayout: boolean) => {
572
+ if (!relayout && typeof chart.repaintChrome === "function") {
573
+ chart.repaintChrome();
574
+ } else {
575
+ this.chartImpl.requestRender(this.glManager);
576
+ }
577
+ },
578
+ postDelta: (fields: Record<string, string | number | boolean>) =>
579
+ this.post({ kind: "pluginConfigDelta", fields }),
580
+ setCursor: (cursor: string) => chart._hostSink?.setCursor?.(cursor),
581
+ dispatchLeave: () => this._tooltip()?.dispatchLeave(),
582
+ });
583
+ }
584
+
526
585
  onInteraction(event: InteractionEvent): void {
586
+ try {
587
+ this._routeInteraction(event);
588
+ } catch (err) {
589
+ this._dragTarget = null;
590
+ (this.chartImpl as any)?._legend?.cancelGesture?.();
591
+ console.error("interaction dispatch failed", err);
592
+ }
593
+ }
594
+
595
+ private _routeInteraction(event: InteractionEvent): void {
596
+ const plotDragging =
597
+ this._dragTarget !== null &&
598
+ (event.type === "pointermove" || event.type === "pointerup");
599
+ if (!plotDragging && this._legendInteraction(event)) {
600
+ return;
601
+ }
602
+
527
603
  switch (event.type) {
528
604
  case "wheel": {
529
605
  const target = this._resolveTarget(event.mx, event.my);