@perspective-dev/viewer-charts 4.3.0 → 4.5.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 (72) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  3. package/dist/esm/axis/bar-axis.d.ts +9 -1
  4. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  5. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  6. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  7. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  8. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  9. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  10. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  11. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  12. package/dist/esm/charts/series/series-build.d.ts +38 -2
  13. package/dist/esm/charts/series/series-render.d.ts +1 -4
  14. package/dist/esm/charts/series/series-type.d.ts +19 -17
  15. package/dist/esm/charts/series/series.d.ts +16 -0
  16. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  17. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  18. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  19. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  20. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  21. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  22. package/dist/esm/perspective-viewer-charts.js +2 -2
  23. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  24. package/dist/esm/plugin/plugin.d.ts +0 -1
  25. package/dist/esm/theme/palette.d.ts +0 -5
  26. package/dist/esm/transport/protocol.d.ts +2 -7
  27. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  28. package/package.json +1 -1
  29. package/src/ts/axis/bar-axis.ts +74 -45
  30. package/src/ts/axis/categorical-axis.ts +0 -2
  31. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  32. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  33. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  34. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  35. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  36. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  37. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  38. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  39. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  40. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  41. package/src/ts/charts/chart-base.ts +20 -6
  42. package/src/ts/charts/chart.ts +1 -1
  43. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  44. package/src/ts/charts/common/expand-domain.ts +40 -0
  45. package/src/ts/charts/common/tree-chart.ts +16 -0
  46. package/src/ts/charts/common/tree-chrome.ts +86 -1
  47. package/src/ts/charts/common/tree-interact.ts +209 -0
  48. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  49. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  50. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  51. package/src/ts/charts/series/series-build.ts +394 -21
  52. package/src/ts/charts/series/series-render.ts +159 -38
  53. package/src/ts/charts/series/series-type.ts +37 -17
  54. package/src/ts/charts/series/series.ts +63 -68
  55. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  56. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  57. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  58. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  59. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  60. package/src/ts/charts/treemap/treemap.ts +1 -16
  61. package/src/ts/interaction/host-sink-message.ts +33 -22
  62. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  63. package/src/ts/interaction/zoom-controller.ts +120 -83
  64. package/src/ts/interaction/zoom-router.ts +3 -126
  65. package/src/ts/map/tile-layer.ts +13 -13
  66. package/src/ts/plugin/plugin.ts +100 -184
  67. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  68. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  69. package/src/ts/theme/palette.ts +1 -4
  70. package/src/ts/transport/protocol.ts +3 -8
  71. package/src/ts/worker/dispatch.ts +0 -1
  72. package/src/ts/worker/renderer.worker.ts +10 -46
@@ -24,134 +24,11 @@ export interface ZoomTarget {
24
24
  controller: ZoomController;
25
25
  layout: PlotLayout;
26
26
  }
27
- export type ZoomTargetResolver = (mx: number, my: number) => ZoomTarget | null;
28
27
 
29
28
  /**
30
- * One set of wheel / pointer listeners on the GL canvas that dispatches
31
- * zoom + pan events to a {@link ZoomController} resolved from the
32
- * cursor position. Replaces `ZoomController.attach` so multiple
33
- * controllers (one per facet) can coexist on a single canvas.
34
- */
35
- export class ZoomRouter {
36
- private _element: HTMLElement | null = null;
37
- private _resolve: ZoomTargetResolver | null = null;
38
- private _onUpdate: (() => void) | null = null;
39
-
40
- private _pointerDown = false;
41
- private _pointerTarget: ZoomTarget | null = null;
42
- private _lastPointerX = 0;
43
- private _lastPointerY = 0;
44
-
45
- private _onWheel: ((e: WheelEvent) => void) | null = null;
46
- private _onPointerDown: ((e: PointerEvent) => void) | null = null;
47
- private _onPointerMove: ((e: PointerEvent) => void) | null = null;
48
- private _onPointerUp: ((e: PointerEvent) => void) | null = null;
49
-
50
- attach(
51
- element: HTMLElement,
52
- resolve: ZoomTargetResolver,
53
- onUpdate: () => void,
54
- ): void {
55
- this.detach();
56
- this._element = element;
57
- this._resolve = resolve;
58
- this._onUpdate = onUpdate;
59
-
60
- this._onWheel = (e: WheelEvent) => {
61
- const rect = element.getBoundingClientRect();
62
- const mouseX = e.clientX - rect.left;
63
- const mouseY = e.clientY - rect.top;
64
- const target = resolve(mouseX, mouseY);
65
- if (!target) {
66
- return;
67
- }
68
-
69
- e.preventDefault();
70
- applyWheel(target, mouseX, mouseY, e.deltaY);
71
- onUpdate();
72
- };
73
-
74
- this._onPointerDown = (e: PointerEvent) => {
75
- const rect = element.getBoundingClientRect();
76
- const mouseX = e.clientX - rect.left;
77
- const mouseY = e.clientY - rect.top;
78
- const target = resolve(mouseX, mouseY);
79
- if (!target) {
80
- return;
81
- }
82
-
83
- this._pointerDown = true;
84
- this._pointerTarget = target;
85
- this._lastPointerX = e.clientX;
86
- this._lastPointerY = e.clientY;
87
- element.setPointerCapture(e.pointerId);
88
- };
89
-
90
- this._onPointerMove = (e: PointerEvent) => {
91
- if (!this._pointerDown || !this._pointerTarget) {
92
- return;
93
- }
94
-
95
- const dx = e.clientX - this._lastPointerX;
96
- const dy = e.clientY - this._lastPointerY;
97
- this._lastPointerX = e.clientX;
98
- this._lastPointerY = e.clientY;
99
- applyPan(this._pointerTarget, dx, dy);
100
- onUpdate();
101
- };
102
-
103
- this._onPointerUp = () => {
104
- this._pointerDown = false;
105
- this._pointerTarget = null;
106
- };
107
-
108
- element.addEventListener("wheel", this._onWheel, { passive: false });
109
- element.addEventListener("pointerdown", this._onPointerDown);
110
- element.addEventListener("pointermove", this._onPointerMove);
111
- element.addEventListener("pointerup", this._onPointerUp);
112
- }
113
-
114
- detach(): void {
115
- if (this._element) {
116
- if (this._onWheel) {
117
- this._element.removeEventListener("wheel", this._onWheel);
118
- }
119
-
120
- if (this._onPointerDown) {
121
- this._element.removeEventListener(
122
- "pointerdown",
123
- this._onPointerDown,
124
- );
125
- }
126
-
127
- if (this._onPointerMove) {
128
- this._element.removeEventListener(
129
- "pointermove",
130
- this._onPointerMove,
131
- );
132
- }
133
-
134
- if (this._onPointerUp) {
135
- this._element.removeEventListener(
136
- "pointerup",
137
- this._onPointerUp,
138
- );
139
- }
140
- }
141
-
142
- this._element = null;
143
- this._resolve = null;
144
- this._onUpdate = null;
145
- this._pointerDown = false;
146
- this._pointerTarget = null;
147
- }
148
- }
149
-
150
- /**
151
- * Apply a wheel-zoom delta to the resolved target. Exported for
152
- * re-use inside the worker, where the renderer dispatches interaction
153
- * events forwarded from the host's `RawEventForwarder` instead of
154
- * receiving DOM events directly.
29
+ * Apply a wheel-zoom delta to the resolved target. The worker
30
+ * dispatches interaction events forwarded from the host's
31
+ * `RawEventForwarder` and calls this directly.
155
32
  */
156
33
  export function applyWheel(
157
34
  target: ZoomTarget,
@@ -18,6 +18,7 @@ import { TileLoader, tileKey } from "./tile-loader";
18
18
  import type { TileSource } from "./tile-source";
19
19
  import tileVert from "../shaders/tile.vert.glsl";
20
20
  import tileFrag from "../shaders/tile.frag.glsl";
21
+ import { compileProgram } from "../webgl/program-cache";
21
22
 
22
23
  type GL = WebGL2RenderingContext | WebGLRenderingContext;
23
24
 
@@ -344,24 +345,23 @@ export class TileLayer {
344
345
  }
345
346
 
346
347
  const gl = glManager.gl;
347
- const program = glManager.shaders.getOrCreate(
348
+ this._program = compileProgram<TileProgramCache>(
349
+ glManager,
348
350
  "map-tile",
349
351
  tileVert,
350
352
  tileFrag,
353
+ [
354
+ "u_projection",
355
+ "u_extent_min",
356
+ "u_extent_max",
357
+ "u_uv_min",
358
+ "u_uv_max",
359
+ "u_tile",
360
+ "u_alpha",
361
+ ],
362
+ ["a_corner"],
351
363
  );
352
364
 
353
- this._program = {
354
- program,
355
- u_projection: gl.getUniformLocation(program, "u_projection"),
356
- u_extent_min: gl.getUniformLocation(program, "u_extent_min"),
357
- u_extent_max: gl.getUniformLocation(program, "u_extent_max"),
358
- u_uv_min: gl.getUniformLocation(program, "u_uv_min"),
359
- u_uv_max: gl.getUniformLocation(program, "u_uv_max"),
360
- u_tile: gl.getUniformLocation(program, "u_tile"),
361
- u_alpha: gl.getUniformLocation(program, "u_alpha"),
362
- a_corner: gl.getAttribLocation(program, "a_corner"),
363
- };
364
-
365
365
  const buf = gl.createBuffer();
366
366
  if (!buf) {
367
367
  return;
@@ -38,173 +38,85 @@ import { RENDER_BLIT_MODE } from "../config";
38
38
  const FACET_CONFIG_DEFAULTS: FacetConfig = { ...DEFAULT_FACET_CONFIG };
39
39
 
40
40
  /**
41
- * Build a UI control spec for one plugin-config field. Mirrors the
42
- * shape `column_config_schema` already returns (datagrid). Numeric
43
- * fields get a `Number` control with min/max clamps; fractions get a
44
- * 0..1 range; enums + booleans pass through their variant list.
41
+ * Static UI-control spec per `plugin_config` field. Mirrors the shape
42
+ * `column_config_schema` already returns (datagrid). The runtime default
43
+ * is sourced separately from the chart-type-effective defaults at
44
+ * `fieldSpec` call time so per-chart overrides like
45
+ * `include_zero=true` for Y Bar / Y Area / X Bar surface in the UI.
45
46
  */
47
+ type FieldSpec =
48
+ | { kind: "Bool" }
49
+ | {
50
+ kind: "Enum";
51
+ variants: ReadonlyArray<{ value: string; label: string }>;
52
+ }
53
+ | { kind: "Number"; min: number; max: number; step?: number };
54
+
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" },
77
+ ],
78
+ },
79
+ series_zoom_mode: {
80
+ kind: "Enum",
81
+ variants: [
82
+ { value: "dynamic", label: "Dynamic" },
83
+ { value: "fixed", label: "Fixed" },
84
+ ],
85
+ },
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" },
102
+ ],
103
+ },
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" },
110
+ ],
111
+ },
112
+ map_tile_alpha: { kind: "Number", min: 0, max: 1, step: 0.05 },
113
+ };
114
+
46
115
  function fieldSpec(
47
116
  key: PluginConfigField,
48
117
  defaults: PluginConfig,
49
118
  ): Record<string, unknown> & { kind: string } {
50
- switch (key) {
51
- case "auto_alt_y_axis":
52
- return { kind: "Bool", key, default: defaults.auto_alt_y_axis };
53
- case "include_zero":
54
- return { kind: "Bool", key, default: defaults.include_zero };
55
- case "domain_mode":
56
- return {
57
- kind: "Enum",
58
- key,
59
- default: defaults.domain_mode,
60
- variants: [
61
- { value: "fit", label: "Fit" },
62
- { value: "expand", label: "Expand" },
63
- ],
64
- };
65
- case "facet_mode":
66
- return {
67
- kind: "Enum",
68
- key,
69
- default: DEFAULT_PLUGIN_CONFIG.facet_mode,
70
- variants: [
71
- { value: "grid", label: "Grid" },
72
- { value: "overlay", label: "Overlay" },
73
- ],
74
- };
75
- case "facet_zoom_mode":
76
- return {
77
- kind: "Enum",
78
- key,
79
- default: DEFAULT_PLUGIN_CONFIG.facet_zoom_mode,
80
- variants: [
81
- { value: "shared", label: "Shared" },
82
- { value: "independent", label: "Independent" },
83
- ],
84
- };
85
- case "series_zoom_mode":
86
- return {
87
- kind: "Enum",
88
- key,
89
- default: DEFAULT_PLUGIN_CONFIG.series_zoom_mode,
90
- variants: [
91
- { value: "dynamic", label: "Dynamic" },
92
- { value: "fixed", label: "Fixed" },
93
- ],
94
- };
95
- case "line_width_px":
96
- return {
97
- kind: "Number",
98
- key,
99
- default: DEFAULT_PLUGIN_CONFIG.line_width_px,
100
- min: 0.5,
101
- step: 0.5,
102
- max: 16,
103
- };
104
- case "point_size_px":
105
- return {
106
- kind: "Number",
107
- key,
108
- default: DEFAULT_PLUGIN_CONFIG.point_size_px,
109
- min: 1,
110
- max: 32,
111
- };
112
- case "band_inner_frac":
113
- return {
114
- kind: "Number",
115
- key,
116
- default: DEFAULT_PLUGIN_CONFIG.band_inner_frac,
117
- min: 0.1,
118
- max: 1,
119
- step: 0.01,
120
- };
121
- case "bar_inner_pad":
122
- return {
123
- kind: "Number",
124
- key,
125
- default: DEFAULT_PLUGIN_CONFIG.bar_inner_pad,
126
- min: 0,
127
- max: 0.9,
128
- step: 0.01,
129
- };
130
- case "wick_width_px":
131
- return {
132
- kind: "Number",
133
- key,
134
- default: DEFAULT_PLUGIN_CONFIG.wick_width_px,
135
- min: 0.5,
136
- step: 0.5,
137
- max: 8,
138
- };
139
- case "ohlc_line_width_px":
140
- return {
141
- kind: "Number",
142
- key,
143
- default: DEFAULT_PLUGIN_CONFIG.ohlc_line_width_px,
144
- min: 0.5,
145
- step: 0.5,
146
- max: 8,
147
- };
148
- case "gradient_radius_px":
149
- return {
150
- kind: "Number",
151
- key,
152
- default: DEFAULT_PLUGIN_CONFIG.gradient_radius_px,
153
- min: 2,
154
- step: 1,
155
- max: 256,
156
- };
157
- case "gradient_intensity":
158
- return {
159
- kind: "Number",
160
- key,
161
- default: DEFAULT_PLUGIN_CONFIG.gradient_intensity,
162
- min: 0.05,
163
- step: 0.05,
164
- max: 4,
165
- };
166
- case "gradient_heat_max":
167
- return {
168
- kind: "Number",
169
- key,
170
- default: DEFAULT_PLUGIN_CONFIG.gradient_heat_max,
171
- min: 0.1,
172
- step: 0.1,
173
- max: 64,
174
- };
175
- case "gradient_color_mode":
176
- return {
177
- kind: "Enum",
178
- key,
179
- default: DEFAULT_PLUGIN_CONFIG.gradient_color_mode,
180
- variants: [
181
- { value: "mean", label: "Mean (density-weighted)" },
182
- { value: "density", label: "Density only" },
183
- { value: "extreme", label: "Extremes" },
184
- { value: "signed", label: "Signed sum" },
185
- ],
186
- };
187
- case "map_tile_provider":
188
- return {
189
- kind: "Enum",
190
- key,
191
- default: DEFAULT_PLUGIN_CONFIG.map_tile_provider,
192
- variants: [
193
- { value: "carto-positron", label: "Light (Positron)" },
194
- { value: "carto-dark-matter", label: "Dark Matter" },
195
- { value: "carto-voyager", label: "Voyager" },
196
- ],
197
- };
198
- case "map_tile_alpha":
199
- return {
200
- kind: "Number",
201
- key,
202
- default: DEFAULT_PLUGIN_CONFIG.map_tile_alpha,
203
- min: 0,
204
- max: 1,
205
- step: 0.05,
206
- };
207
- }
119
+ return { ...FIELD_SCHEMAS[key], key, default: defaults[key] };
208
120
  }
209
121
 
210
122
  const GLOBAL_STYLES = (() => {
@@ -519,6 +431,32 @@ export class HTMLPerspectiveViewerWebGLPluginElement
519
431
  default: false,
520
432
  });
521
433
  }
434
+
435
+ // Line / area glyphs can bridge interior nulls by linear
436
+ // interpolation. Bar / scatter ignore the flag.
437
+ const supports_interpolate =
438
+ effective_chart_type === "line" ||
439
+ effective_chart_type === "area";
440
+
441
+ if (supports_interpolate) {
442
+ const variants =
443
+ effective_chart_type === "area"
444
+ ? [
445
+ { value: "skip", label: "Skip" },
446
+ { value: "solid", label: "Solid" },
447
+ ]
448
+ : [
449
+ { value: "skip", label: "Skip" },
450
+ { value: "solid", label: "Solid" },
451
+ { value: "transparent", label: "Transparent" },
452
+ ];
453
+ fields.push({
454
+ kind: "Enum",
455
+ key: "interpolate",
456
+ default: "solid",
457
+ variants,
458
+ });
459
+ }
522
460
  }
523
461
 
524
462
  // Per-column formatter widgets. Surfaced for every chart type so
@@ -599,28 +537,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
599
537
  return 5;
600
538
  }
601
539
 
602
- save() {
603
- const state: any = {};
604
- const zoom = this._renderer?.saveZoom();
605
- if (zoom) {
606
- state.zoom = zoom;
607
- }
608
-
609
- // Only emit the keys this chart actually consumes.
610
- const cfg: Partial<PluginConfig> = {};
611
- for (const key of this._chartType.applicable_plugin_fields) {
612
- // `key` is `PluginConfigField` = `keyof PluginConfig`, so this
613
- // indexed assignment is type-safe without a cast.
614
- (cfg[key] as PluginConfig[typeof key]) = this._pluginConfig[key];
615
- }
616
-
617
- if (Object.keys(cfg).length > 0) {
618
- state.plugin_config = cfg;
619
- }
620
-
621
- return state;
622
- }
623
-
624
540
  async render(view: View): Promise<Blob> {
625
541
  await this._ensureRenderer(view);
626
542
  await this.draw(view);
@@ -16,11 +16,12 @@ uniform vec4 u_color;
16
16
  uniform float u_line_width;
17
17
 
18
18
  varying float v_edge_dist;
19
+ varying float v_seg_alpha;
19
20
 
20
21
  void main() {
21
22
  float dist = abs(v_edge_dist);
22
23
  float coreEdge = u_line_width / (u_line_width + 1.5);
23
24
  float alpha = 1.0 - smoothstep(coreEdge, 1.0, dist);
24
25
 
25
- gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
26
+ gl_FragColor = vec4(u_color.rgb, u_color.a * alpha * v_seg_alpha);
26
27
  }
@@ -22,11 +22,27 @@ attribute vec2 a_end;
22
22
  // 0 = start+left, 1 = start+right, 2 = end+left, 3 = end+right
23
23
  attribute float a_corner;
24
24
 
25
+ // Per-segment "is endpoint a real source-data cell" flags. Both vertices
26
+ // of a segment's quad see the same instance values, so `v_seg_alpha`
27
+ // below is constant across the quad — no gradient fade. Read from the
28
+ // per-cell real-flag buffer with offset 0 / 1 (same overlap trick as
29
+ // the bar-line glyph's segment-position attributes).
30
+ attribute float a_real_start;
31
+ attribute float a_real_end;
32
+
25
33
  uniform mat4 u_projection;
26
34
  uniform vec2 u_resolution;
27
35
  uniform float u_line_width;
28
36
 
37
+ // Alpha multiplier applied to any segment whose endpoints are not both
38
+ // real. Set per draw based on the series' interpolate mode:
39
+ // 0.0 = skip (gaps at synthesized cells)
40
+ // 1.0 = solid (no visible difference; default for non-line)
41
+ // 0.5 = transparent (50% opacity on segments touching synthesized cells)
42
+ uniform float u_interp_alpha;
43
+
29
44
  varying float v_edge_dist;
45
+ varying float v_seg_alpha;
30
46
 
31
47
  void main() {
32
48
  vec4 clipStart = u_projection * vec4(a_start, 0.0, 1.0);
@@ -51,4 +67,7 @@ void main() {
51
67
 
52
68
  gl_Position = clipPos + vec4(clipOffset, 0.0, 0.0);
53
69
  v_edge_dist = side;
70
+
71
+ float bothReal = a_real_start * a_real_end;
72
+ v_seg_alpha = mix(u_interp_alpha, 1.0, step(0.5, bothReal));
54
73
  }
@@ -18,10 +18,7 @@ export type Vec3 = [number, number, number];
18
18
  * Build a series palette of length `count` by sampling the theme gradient
19
19
  * at evenly-spaced offsets. For count == 1 returns the 50% stop.
20
20
  */
21
- export function interpolatePalette(
22
- stops: GradientStop[],
23
- count: number,
24
- ): Vec3[] {
21
+ function interpolatePalette(stops: GradientStop[], count: number): Vec3[] {
25
22
  if (count <= 0) {
26
23
  return [];
27
24
  }
@@ -12,8 +12,11 @@
12
12
 
13
13
  import type { FacetConfig, PluginConfig } from "../charts/chart";
14
14
  import type { PerspectiveClickDetail } from "../event-detail";
15
+ import type { ThemeSnapshot } from "../theme/theme";
15
16
  import type { ViewConfig } from "@perspective-dev/client";
16
17
 
18
+ export type { ThemeSnapshot };
19
+
17
20
  /**
18
21
  * Worker-mode control-channel messages. Distinct from the perspective
19
22
  * `ProxySession` channel that the worker's `Client` uses to talk to the
@@ -222,14 +225,6 @@ export interface FontFaceDescriptor {
222
225
  display?: string;
223
226
  }
224
227
 
225
- /**
226
- * Theme values resolved on the host via `getComputedStyle` and shipped
227
- * to the renderer scope, which has no DOM. Decoded by the chart via
228
- * `theme/theme.ts::resolveThemeFromVars`. Plain map for
229
- * structured-clone.
230
- */
231
- export type ThemeSnapshot = Record<string, string>;
232
-
233
228
  export interface SetViewByNameMsg {
234
229
  kind: "setViewByName";
235
230
  name: string;
@@ -41,7 +41,6 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
41
41
  r.redraw();
42
42
  break;
43
43
  case "resize":
44
- console.log("resize");
45
44
  r.resize(msg.cssWidth, msg.cssHeight, msg.dpr);
46
45
  r.redraw();
47
46
  break;
@@ -177,41 +177,11 @@ export class WorkerRenderer {
177
177
  this.chartImpl.setView?.(view);
178
178
  this.glManager.bufferPool.maxCapacity = msg.bufferMaxCapacity;
179
179
  this.glManager.resize(msg.cssWidth, msg.cssHeight, msg.dpr);
180
- const hostSink = new MessageHostSink((envelope) => {
181
- switch (envelope.kind) {
182
- case "pin":
183
- this.post({
184
- kind: "pinTooltip",
185
- lines: envelope.payload.lines,
186
- pos: envelope.payload.pos,
187
- bounds: envelope.payload.bounds,
188
- });
189
- break;
190
- case "dismiss":
191
- this.post({ kind: "dismissTooltip" });
192
- break;
193
- case "setCursor":
194
- this.post({ kind: "setCursor", cursor: envelope.cursor });
195
- break;
196
- case "userClick":
197
- this.post({
198
- kind: "userClick",
199
- detail: envelope.payload as any,
200
- });
201
- break;
202
- case "userSelect":
203
- this.post({
204
- kind: "userSelect",
205
- selected: envelope.payload.selected,
206
- row: envelope.payload.row,
207
- column_names: envelope.payload.column_names,
208
- insertConfig: envelope.payload.insertConfig as any,
209
- });
210
- break;
211
- }
212
- });
213
-
214
- this.chartImpl.attachTooltip?.(hostSink);
180
+ // `MessageHostSink` emits `WorkerMsg`s directly no translation
181
+ // needed here.
182
+ this.chartImpl.attachTooltip?.(
183
+ new MessageHostSink((msg) => this.post(msg)),
184
+ );
215
185
  }
216
186
 
217
187
  setViewByName(name: string): void {
@@ -309,13 +279,9 @@ export class WorkerRenderer {
309
279
  }
310
280
  }
311
281
  } catch (err) {
312
- // Any unexpected throw proxy hiccup, chart-impl mutation
313
- // failure, RAF chain rejection — must not leak past the
314
- // outer fire-and-forget caller (`dispatch` does not await
315
- // this method). Surface to the worker console; the host's
316
- // pending promise still gets resolved via the `finally`
317
- // ack below so `draw()` can't deadlock on a renderer error.
318
- console.error("loadAndRender failed", err);
282
+ if ((err + "").indexOf("View not found") === -1) {
283
+ console.error("loadAndRender failed", err);
284
+ }
319
285
  } finally {
320
286
  this.post({ kind: "loadAndRenderAck", msgId: msg.msgId });
321
287
  }
@@ -417,10 +383,8 @@ export class WorkerRenderer {
417
383
 
418
384
  /**
419
385
  * Hit-test the cursor against the chart's facet grid (in faceted
420
- * mode) or its current layout (single-plot). Mirrors the resolver
421
- * `_setupZoomRouter` builds on the host for in-process mode — the
422
- * worker owns the facet grid and controllers, so the resolution
423
- * runs here.
386
+ * mode) or its current layout (single-plot). The worker owns the
387
+ * facet grid and controllers, so the resolution runs here.
424
388
  */
425
389
  private _resolveTarget(mx: number, my: number): ZoomTarget | null {
426
390
  const chart = this.chartImpl as any;