@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.
- package/dist/cdn/perspective-viewer-charts.js +1 -2
- package/dist/cdn/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/axis/legend.d.ts +53 -5
- package/dist/esm/axis/map-ticks.d.ts +15 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +2 -6
- package/dist/esm/charts/cartesian/cartesian.d.ts +6 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
- package/dist/esm/charts/chart-base.d.ts +2 -0
- package/dist/esm/charts/chart.d.ts +77 -6
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
- package/dist/esm/charts/common/tree-interact.d.ts +1 -1
- package/dist/esm/charts/series/series-build.d.ts +7 -0
- package/dist/esm/charts/series/series-interact.d.ts +1 -1
- package/dist/esm/charts/series/series.d.ts +6 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/interaction/axis-indicators.d.ts +11 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
- package/dist/esm/interaction/host-sink-message.d.ts +3 -2
- package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
- package/dist/esm/interaction/legend-controller.d.ts +246 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
- package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
- package/dist/esm/layout/facet-grid.d.ts +6 -0
- package/dist/esm/layout/ticks.d.ts +1 -0
- package/dist/esm/map/tile-source.d.ts +77 -8
- package/dist/esm/perspective-viewer-charts.js +1 -2
- package/dist/esm/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/plugin/charts.d.ts +2 -0
- package/dist/esm/plugin/format-defaults.d.ts +9 -0
- package/dist/esm/plugin/plugin.d.ts +20 -0
- package/dist/esm/theme/gradient.d.ts +1 -0
- package/dist/esm/transport/protocol.d.ts +45 -3
- package/dist/esm/transport/renderer-transport.d.ts +3 -0
- package/dist/esm/worker/renderer.worker.d.ts +19 -1
- package/package.json +6 -6
- package/src/css/perspective-viewer-charts.css +0 -1
- package/src/ts/axis/bar-axis.ts +2 -7
- package/src/ts/axis/legend.ts +351 -24
- package/src/ts/axis/map-ticks.ts +94 -0
- package/src/ts/axis/numeric-axis.ts +2 -9
- package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
- package/src/ts/charts/candlestick/candlestick-render.ts +73 -10
- package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
- package/src/ts/charts/cartesian/cartesian-render.ts +369 -75
- package/src/ts/charts/cartesian/cartesian.ts +9 -0
- package/src/ts/charts/cartesian/glyph.ts +6 -2
- package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
- package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
- package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
- package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
- package/src/ts/charts/chart-base.ts +34 -32
- package/src/ts/charts/chart.ts +106 -7
- package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
- package/src/ts/charts/common/tree-chrome.ts +102 -23
- package/src/ts/charts/common/tree-interact.ts +18 -17
- package/src/ts/charts/heatmap/heatmap-interact.ts +65 -7
- package/src/ts/charts/heatmap/heatmap-render.ts +100 -24
- package/src/ts/charts/map/map.ts +3 -5
- package/src/ts/charts/series/series-build.ts +14 -2
- package/src/ts/charts/series/series-interact.ts +12 -11
- package/src/ts/charts/series/series-render.ts +305 -132
- package/src/ts/charts/series/series.ts +10 -0
- package/src/ts/charts/sunburst/sunburst-render.ts +7 -1
- package/src/ts/charts/treemap/treemap-render.ts +7 -1
- package/src/ts/data/view-reader.ts +1 -4
- package/src/ts/index.ts +2 -0
- package/src/ts/interaction/axis-indicators.ts +137 -0
- package/src/ts/interaction/host-sink-dom.ts +66 -2
- package/src/ts/interaction/host-sink-message.ts +8 -2
- package/src/ts/interaction/lazy-tooltip.ts +5 -5
- package/src/ts/interaction/legend-controller.ts +1084 -0
- package/src/ts/interaction/tooltip-controller.ts +43 -131
- package/src/ts/interaction/tooltip-grid.ts +187 -0
- package/src/ts/layout/facet-grid.ts +8 -1
- package/src/ts/layout/ticks.ts +12 -0
- package/src/ts/map/tile-loader.ts +16 -1
- package/src/ts/map/tile-source.ts +199 -53
- package/src/ts/map/tile-sources.json +18 -0
- package/src/ts/plugin/charts.ts +40 -6
- package/src/ts/plugin/format-defaults.ts +37 -0
- package/src/ts/plugin/plugin.ts +236 -67
- package/src/ts/shaders/line.vert.glsl +5 -2
- package/src/ts/shaders/scatter.vert.glsl +0 -7
- package/src/ts/theme/gradient.ts +24 -0
- package/src/ts/transport/protocol.ts +46 -2
- package/src/ts/transport/renderer-transport.ts +83 -4
- package/src/ts/worker/dispatch.ts +6 -2
- package/src/ts/worker/renderer.worker.ts +78 -2
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
import type { Canvas2D, Context2D } from "../charts/canvas-types";
|
|
14
14
|
import type { PlotLayout } from "../layout/plot-layout";
|
|
15
15
|
import type { Theme } from "../theme/theme";
|
|
16
|
+
import {
|
|
17
|
+
measureTooltipGrid,
|
|
18
|
+
paintTooltipGrid,
|
|
19
|
+
DEFAULT_TOOLTIP_STYLE,
|
|
20
|
+
type TooltipContent,
|
|
21
|
+
type TooltipStyle,
|
|
22
|
+
} from "./tooltip-grid";
|
|
16
23
|
|
|
17
|
-
/**
|
|
18
|
-
* Minimal positioning input — PlotLayout satisfies this.
|
|
19
|
-
*/
|
|
20
24
|
export interface CssBounds {
|
|
21
25
|
cssWidth: number;
|
|
22
26
|
cssHeight: number;
|
|
@@ -25,7 +29,7 @@ export interface CssBounds {
|
|
|
25
29
|
export interface TooltipCallbacks {
|
|
26
30
|
/**
|
|
27
31
|
* RAF-throttled mouse position in CSS pixels, relative to the GL
|
|
28
|
-
* canvas
|
|
32
|
+
* canvas.
|
|
29
33
|
*/
|
|
30
34
|
onHover(mx: number, my: number): void;
|
|
31
35
|
|
|
@@ -35,8 +39,7 @@ export interface TooltipCallbacks {
|
|
|
35
39
|
onLeave(): void;
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* Fires on click with mouse position.
|
|
39
|
-
* (skipping the default pin/dismiss flow — used for legend clicks).
|
|
42
|
+
* Fires on click with mouse position.
|
|
40
43
|
*/
|
|
41
44
|
onClickPre?(mx: number, my: number): boolean;
|
|
42
45
|
|
|
@@ -46,93 +49,45 @@ export interface TooltipCallbacks {
|
|
|
46
49
|
onPin?(mx: number, my: number): void;
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
|
-
* Fires on dblclick (treemap drill-up gesture).
|
|
50
|
-
* that don't bind a handler simply ignore the event.
|
|
52
|
+
* Fires on dblclick (treemap drill-up gesture).
|
|
51
53
|
*/
|
|
52
54
|
onDblClick?(mx: number, my: number): void;
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
|
-
* Fires when an active pin is dismissed by a click
|
|
56
|
-
* already-pinned target (the "click again to unpin" gesture in
|
|
57
|
-
* `dispatchClick`). Chart impls hook this to emit a
|
|
58
|
-
* `perspective-global-filter` with `selected: false`. Does *not*
|
|
59
|
-
* fire on the implicit dismiss inside `pin()` that replaces an
|
|
60
|
-
* existing pin — that path is followed by a fresh `onPin` which
|
|
61
|
-
* emits its own `selected: true`.
|
|
57
|
+
* Fires when an active pin is dismissed by a click.
|
|
62
58
|
*/
|
|
63
59
|
onUnpin?(): void;
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
export interface RenderTooltipOptions {
|
|
67
|
-
/**
|
|
68
|
-
* Draw a dashed crosshair at `pos`. Used by scatter/line.
|
|
69
|
-
*/
|
|
70
63
|
crosshair?: boolean;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Draw a ring of `radius` CSS pixels at `pos`. Used to highlight a
|
|
74
|
-
* hovered point. Omit for bars (where the bar itself highlights).
|
|
75
|
-
*/
|
|
76
64
|
highlightRadius?: number;
|
|
65
|
+
maxColumnPx?: number;
|
|
66
|
+
opacity?: number;
|
|
77
67
|
}
|
|
78
68
|
|
|
79
69
|
/**
|
|
80
|
-
* Side-channel from the chart back to the host's DOM.
|
|
81
|
-
* into a sink rather than touching the DOM itself; the host
|
|
82
|
-
* materializes the actual visual (`<div>` for pinned tooltip, cursor
|
|
83
|
-
* mutation on the GL canvas).
|
|
84
|
-
*
|
|
85
|
-
* - `MessageHostSink` (in worker/) — forwards calls over a
|
|
86
|
-
* `postMessage`-shaped channel back to
|
|
87
|
-
* the host.
|
|
88
|
-
* - `DomHostSink` (in host transport) — receives the matching
|
|
89
|
-
* envelopes and applies them to the DOM.
|
|
90
|
-
*
|
|
91
|
-
* The controller's `_pinned` flag is the source of truth for whether
|
|
92
|
-
* hover updates are gated; the sink only owns the visual artifact.
|
|
70
|
+
* Side-channel from the chart back to the host's DOM.
|
|
93
71
|
*/
|
|
94
72
|
export interface HostSink {
|
|
95
73
|
pin(
|
|
96
|
-
|
|
74
|
+
grid: TooltipContent,
|
|
97
75
|
pos: { px: number; py: number },
|
|
98
76
|
bounds: CssBounds,
|
|
77
|
+
style?: TooltipStyle,
|
|
99
78
|
): void;
|
|
100
79
|
dismiss(): void;
|
|
101
80
|
setCursor(cursor: string): void;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Forward a `perspective-click` to the host. Optional — only the
|
|
105
|
-
* worker-bound `MessageHostSink` implements it; `DomHostSink` (the
|
|
106
|
-
* host-side consumer of pin/dismiss) never sees user-event calls,
|
|
107
|
-
* so omits the implementation.
|
|
108
|
-
*/
|
|
109
81
|
emitUserClick?(detail: UserClickPayload): void;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Forward a `perspective-global-filter` to the host with the
|
|
113
|
-
* `selected: true` / `selected: false` semantics. The host owns the
|
|
114
|
-
* `removeConfigs` history (mirrors datagrid's
|
|
115
|
-
* `model._last_insert_configs`); the sink only ships the new state.
|
|
116
|
-
*/
|
|
117
82
|
emitUserSelect?(payload: UserSelectPayload): void;
|
|
118
83
|
}
|
|
119
84
|
|
|
120
|
-
/**
|
|
121
|
-
* Plain-object payload for `HostSink.emitUserClick`. Matches
|
|
122
|
-
* `PerspectiveClickDetail` byte-for-byte; defined locally to avoid a
|
|
123
|
-
* cycle through `event-detail.ts`.
|
|
124
|
-
*/
|
|
125
85
|
export interface UserClickPayload {
|
|
126
86
|
row: Record<string, unknown>;
|
|
127
87
|
column_names: string[];
|
|
128
88
|
config: { filter?: unknown[] };
|
|
129
89
|
}
|
|
130
90
|
|
|
131
|
-
/**
|
|
132
|
-
* Plain-object payload for `HostSink.emitUserSelect`. The host
|
|
133
|
-
* transport reconstructs a `PerspectiveSelectDetail` class instance
|
|
134
|
-
* from this plus its cached `_lastInsertConfig`.
|
|
135
|
-
*/
|
|
136
91
|
export interface UserSelectPayload {
|
|
137
92
|
selected: boolean;
|
|
138
93
|
row: Record<string, unknown>;
|
|
@@ -142,13 +97,7 @@ export interface UserSelectPayload {
|
|
|
142
97
|
|
|
143
98
|
/**
|
|
144
99
|
* Owns the hover/click/dblclick state machine and the pinned-tooltip
|
|
145
|
-
* lifecycle.
|
|
146
|
-
* `dispatchHover` / `dispatchLeave` / `dispatchClick` /
|
|
147
|
-
* `dispatchDblClick` — the host's `RawEventForwarder` captures DOM
|
|
148
|
-
* events on the GL canvas and posts them as `InteractionEvent`s.
|
|
149
|
-
*
|
|
150
|
-
* Pinning + cursor changes go through a {@link HostSink} so the actual
|
|
151
|
-
* DOM mutations happen host-side regardless of where the chart runs.
|
|
100
|
+
* lifecycle.
|
|
152
101
|
*/
|
|
153
102
|
export class TooltipController {
|
|
154
103
|
private _callbacks: TooltipCallbacks | null = null;
|
|
@@ -162,10 +111,7 @@ export class TooltipController {
|
|
|
162
111
|
}
|
|
163
112
|
|
|
164
113
|
/**
|
|
165
|
-
* Replace the active host sink.
|
|
166
|
-
* prior sink so we never leak a pinned artifact across resets —
|
|
167
|
-
* though in practice each chart instance uses one sink for its
|
|
168
|
-
* lifetime.
|
|
114
|
+
* Replace the active host sink.
|
|
169
115
|
*/
|
|
170
116
|
setHost(sink: HostSink): void {
|
|
171
117
|
if (this._pinned) {
|
|
@@ -177,18 +123,14 @@ export class TooltipController {
|
|
|
177
123
|
}
|
|
178
124
|
|
|
179
125
|
/**
|
|
180
|
-
* Forward a cursor change to the host.
|
|
181
|
-
* installed (chart constructed without a transport).
|
|
126
|
+
* Forward a cursor change to the host.
|
|
182
127
|
*/
|
|
183
128
|
setCursor(cursor: string): void {
|
|
184
129
|
this._host?.setCursor(cursor);
|
|
185
130
|
}
|
|
186
131
|
|
|
187
132
|
/**
|
|
188
|
-
* Install the chart's tooltip callbacks.
|
|
189
|
-
* controller via `dispatchHover` / `dispatchLeave` /
|
|
190
|
-
* `dispatchClick` / `dispatchDblClick`; this controller never
|
|
191
|
-
* touches the DOM directly.
|
|
133
|
+
* Install the chart's tooltip callbacks.
|
|
192
134
|
*/
|
|
193
135
|
attach(callbacks: TooltipCallbacks): void {
|
|
194
136
|
this.detach();
|
|
@@ -211,13 +153,7 @@ export class TooltipController {
|
|
|
211
153
|
|
|
212
154
|
/**
|
|
213
155
|
* Schedule an `onHover` callback for the given canvas-relative
|
|
214
|
-
* coords.
|
|
215
|
-
* pointer streams don't backlog the chart's hit-test path.
|
|
216
|
-
*
|
|
217
|
-
* Workers ship with `requestAnimationFrame` (DedicatedWorkerGlobalScope
|
|
218
|
-
* exposes it for OffscreenCanvas painting), so the same coalescer
|
|
219
|
-
* works in both modes. We fall back to setTimeout if RAF is missing
|
|
220
|
-
* (e.g. node tests without a polyfill).
|
|
156
|
+
* coords.
|
|
221
157
|
*/
|
|
222
158
|
dispatchHover(mx: number, my: number): void {
|
|
223
159
|
if (this._pinned || !this._callbacks) {
|
|
@@ -273,20 +209,19 @@ export class TooltipController {
|
|
|
273
209
|
}
|
|
274
210
|
|
|
275
211
|
/**
|
|
276
|
-
* Pin a tooltip (or replace an active one).
|
|
277
|
-
* configured sink and flips the controller's pinned flag so hover
|
|
278
|
-
* dispatch is suppressed until dismissal.
|
|
212
|
+
* Pin a tooltip (or replace an active one).
|
|
279
213
|
*/
|
|
280
214
|
pin(
|
|
281
|
-
|
|
215
|
+
grid: TooltipContent,
|
|
282
216
|
pos: { px: number; py: number },
|
|
283
217
|
bounds: CssBounds,
|
|
218
|
+
style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
|
|
284
219
|
): void {
|
|
285
|
-
if (
|
|
220
|
+
if (grid.length === 0) {
|
|
286
221
|
return;
|
|
287
222
|
}
|
|
288
223
|
|
|
289
|
-
this._host?.pin(
|
|
224
|
+
this._host?.pin(grid, pos, bounds, style);
|
|
290
225
|
this._pinned = true;
|
|
291
226
|
}
|
|
292
227
|
|
|
@@ -298,17 +233,12 @@ export class TooltipController {
|
|
|
298
233
|
|
|
299
234
|
/**
|
|
300
235
|
* Paint a canvas tooltip (crosshair, highlight ring, box + text) onto
|
|
301
|
-
* `canvas`.
|
|
302
|
-
* identity transform on entry and restores prior state on exit, so it
|
|
303
|
-
* composes cleanly with other chrome painters that may have already
|
|
304
|
-
* called `initCanvas` on the same canvas — re-applying `scale(dpr,dpr)`
|
|
305
|
-
* blind would double-scale in that case, misplacing the tooltip
|
|
306
|
-
* proportionally to its distance from the origin.
|
|
236
|
+
* `canvas`.
|
|
307
237
|
*/
|
|
308
238
|
export function renderCanvasTooltip(
|
|
309
239
|
canvas: Canvas2D | null,
|
|
310
240
|
pos: { px: number; py: number },
|
|
311
|
-
|
|
241
|
+
grid: TooltipContent,
|
|
312
242
|
layout: PlotLayout,
|
|
313
243
|
theme: Theme,
|
|
314
244
|
dpr: number,
|
|
@@ -327,18 +257,12 @@ export function renderCanvasTooltip(
|
|
|
327
257
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
328
258
|
ctx.scale(dpr, dpr);
|
|
329
259
|
ctx.font = `11px ${theme.fontFamily}`;
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
maxWidth = w;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const boxW = maxWidth + padding * 2;
|
|
341
|
-
const boxH = lines.length * lineHeight + padding * 2 - 4;
|
|
260
|
+
const measured = measureTooltipGrid(
|
|
261
|
+
ctx,
|
|
262
|
+
grid,
|
|
263
|
+
options.maxColumnPx ?? DEFAULT_TOOLTIP_STYLE.maxColumnPx,
|
|
264
|
+
);
|
|
265
|
+
const { boxW, boxH } = measured;
|
|
342
266
|
let tx = pos.px + 12;
|
|
343
267
|
let ty = pos.py - boxH - 8;
|
|
344
268
|
if (tx + boxW > layout.cssWidth) {
|
|
@@ -353,8 +277,6 @@ export function renderCanvasTooltip(
|
|
|
353
277
|
ty = layout.cssHeight - boxH - 4;
|
|
354
278
|
}
|
|
355
279
|
|
|
356
|
-
const hasLines = lines.length > 0;
|
|
357
|
-
|
|
358
280
|
// Crosshair
|
|
359
281
|
if (options.crosshair) {
|
|
360
282
|
ctx.strokeStyle = theme.tickColor;
|
|
@@ -382,25 +304,15 @@ export function renderCanvasTooltip(
|
|
|
382
304
|
ctx.globalAlpha = 1.0;
|
|
383
305
|
}
|
|
384
306
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
ctx.roundRect(tx, ty, boxW, boxH, 4);
|
|
395
|
-
ctx.fill();
|
|
396
|
-
ctx.stroke();
|
|
397
|
-
|
|
398
|
-
ctx.fillStyle = theme.tooltipText;
|
|
399
|
-
ctx.textAlign = "left";
|
|
400
|
-
ctx.textBaseline = "top";
|
|
401
|
-
for (let i = 0; i < lines.length; i++) {
|
|
402
|
-
ctx.fillText(lines[i], tx + padding, ty + padding + i * lineHeight);
|
|
403
|
-
}
|
|
307
|
+
if (grid.length > 0) {
|
|
308
|
+
paintTooltipGrid(
|
|
309
|
+
ctx,
|
|
310
|
+
measured,
|
|
311
|
+
tx,
|
|
312
|
+
ty,
|
|
313
|
+
theme,
|
|
314
|
+
options.opacity ?? DEFAULT_TOOLTIP_STYLE.opacity,
|
|
315
|
+
);
|
|
404
316
|
}
|
|
405
317
|
|
|
406
318
|
ctx.restore();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { Context2D } from "../charts/canvas-types";
|
|
14
|
+
import type { Theme } from "../theme/theme";
|
|
15
|
+
|
|
16
|
+
export type TooltipRow = string[];
|
|
17
|
+
|
|
18
|
+
export type TooltipContent = TooltipRow[];
|
|
19
|
+
|
|
20
|
+
export interface TooltipStyle {
|
|
21
|
+
opacity: number;
|
|
22
|
+
maxColumnPx: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_TOOLTIP_STYLE: TooltipStyle = {
|
|
26
|
+
opacity: 1.0,
|
|
27
|
+
maxColumnPx: 160,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function tooltipStyleOf(cfg: {
|
|
31
|
+
tooltip_opacity: number;
|
|
32
|
+
tooltip_max_column_px: number;
|
|
33
|
+
}): TooltipStyle {
|
|
34
|
+
return {
|
|
35
|
+
opacity: cfg.tooltip_opacity,
|
|
36
|
+
maxColumnPx: cfg.tooltip_max_column_px,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const NUMERIC_RE =
|
|
41
|
+
/^[-+−]?[$€£¥]?\s?\d[\d., \s]*(?:[eE][-+]?\d+|[KMB%])?$/;
|
|
42
|
+
|
|
43
|
+
export const TOOLTIP_ROW_HEIGHT = 16;
|
|
44
|
+
export const TOOLTIP_PADDING = 8;
|
|
45
|
+
export const TOOLTIP_COL_GAP = 8;
|
|
46
|
+
|
|
47
|
+
export interface MeasuredTooltipGrid {
|
|
48
|
+
rows: string[][];
|
|
49
|
+
|
|
50
|
+
colWidths: number[];
|
|
51
|
+
|
|
52
|
+
colRightAlign: boolean[];
|
|
53
|
+
|
|
54
|
+
boxW: number;
|
|
55
|
+
boxH: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Measure a tooltip grid under the current `ctx.font`.
|
|
60
|
+
*/
|
|
61
|
+
export function measureTooltipGrid(
|
|
62
|
+
ctx: Context2D,
|
|
63
|
+
grid: TooltipContent,
|
|
64
|
+
maxColumnPx: number,
|
|
65
|
+
): MeasuredTooltipGrid {
|
|
66
|
+
const spanCap = maxColumnPx * 2 + TOOLTIP_COL_GAP;
|
|
67
|
+
const colWidths: number[] = [];
|
|
68
|
+
const colNumeric: boolean[] = [];
|
|
69
|
+
const rows: string[][] = [];
|
|
70
|
+
let spanWidth = 0;
|
|
71
|
+
|
|
72
|
+
for (const row of grid) {
|
|
73
|
+
if (row.length <= 1) {
|
|
74
|
+
const text = ellipsize(ctx, row[0] ?? "", spanCap);
|
|
75
|
+
rows.push([text]);
|
|
76
|
+
spanWidth = Math.max(spanWidth, ctx.measureText(text).width);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cells: string[] = [];
|
|
81
|
+
for (let j = 0; j < row.length; j++) {
|
|
82
|
+
const original = row[j] ?? "";
|
|
83
|
+
const text = ellipsize(ctx, original, maxColumnPx);
|
|
84
|
+
cells.push(text);
|
|
85
|
+
colWidths[j] = Math.max(
|
|
86
|
+
colWidths[j] ?? 0,
|
|
87
|
+
ctx.measureText(text).width,
|
|
88
|
+
);
|
|
89
|
+
if (j >= 1 && original !== "") {
|
|
90
|
+
const numeric = NUMERIC_RE.test(original);
|
|
91
|
+
colNumeric[j] =
|
|
92
|
+
colNumeric[j] === undefined
|
|
93
|
+
? numeric
|
|
94
|
+
: colNumeric[j] && numeric;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
rows.push(cells);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let columnsWidth = 0;
|
|
102
|
+
for (let j = 0; j < colWidths.length; j++) {
|
|
103
|
+
columnsWidth += colWidths[j] + (j > 0 ? TOOLTIP_COL_GAP : 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const innerW = Math.max(spanWidth, columnsWidth);
|
|
107
|
+
return {
|
|
108
|
+
rows,
|
|
109
|
+
colWidths,
|
|
110
|
+
colRightAlign: colWidths.map(
|
|
111
|
+
(_, j) => (colNumeric[j] ?? false) && j >= 1,
|
|
112
|
+
),
|
|
113
|
+
boxW: innerW + TOOLTIP_PADDING * 2,
|
|
114
|
+
boxH: grid.length * TOOLTIP_ROW_HEIGHT + TOOLTIP_PADDING * 2 - 4,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Paint a measured tooltip grid at `(tx, ty)`.
|
|
120
|
+
*/
|
|
121
|
+
export function paintTooltipGrid(
|
|
122
|
+
ctx: Context2D,
|
|
123
|
+
measured: MeasuredTooltipGrid,
|
|
124
|
+
tx: number,
|
|
125
|
+
ty: number,
|
|
126
|
+
theme: Theme,
|
|
127
|
+
opacity: number,
|
|
128
|
+
): void {
|
|
129
|
+
const { rows, colWidths, colRightAlign, boxW, boxH } = measured;
|
|
130
|
+
|
|
131
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
132
|
+
ctx.fillStyle = theme.tooltipBg;
|
|
133
|
+
ctx.strokeStyle = theme.tooltipBorder;
|
|
134
|
+
ctx.lineWidth = 1;
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.rect(tx, ty, boxW, boxH);
|
|
137
|
+
ctx.fill();
|
|
138
|
+
ctx.stroke();
|
|
139
|
+
ctx.globalAlpha = 1.0;
|
|
140
|
+
|
|
141
|
+
ctx.fillStyle = theme.tooltipText;
|
|
142
|
+
ctx.textBaseline = "top";
|
|
143
|
+
for (let i = 0; i < rows.length; i++) {
|
|
144
|
+
const row = rows[i];
|
|
145
|
+
const rowY = ty + TOOLTIP_PADDING + i * TOOLTIP_ROW_HEIGHT;
|
|
146
|
+
if (row.length <= 1) {
|
|
147
|
+
ctx.textAlign = "left";
|
|
148
|
+
ctx.fillText(row[0], tx + TOOLTIP_PADDING, rowY);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let colX = tx + TOOLTIP_PADDING;
|
|
153
|
+
for (let j = 0; j < row.length; j++) {
|
|
154
|
+
const width = colWidths[j] ?? 0;
|
|
155
|
+
if (colRightAlign[j]) {
|
|
156
|
+
ctx.textAlign = "right";
|
|
157
|
+
ctx.fillText(row[j], colX + width, rowY);
|
|
158
|
+
} else {
|
|
159
|
+
ctx.textAlign = "left";
|
|
160
|
+
ctx.fillText(row[j], colX, rowY);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
colX += width + TOOLTIP_COL_GAP;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
ctx.textAlign = "left";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function ellipsize(ctx: Context2D, text: string, maxW: number): string {
|
|
171
|
+
if (ctx.measureText(text).width <= maxW) {
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let lo = 0;
|
|
176
|
+
let hi = text.length;
|
|
177
|
+
while (lo < hi) {
|
|
178
|
+
const mid = (lo + hi + 1) >> 1;
|
|
179
|
+
if (ctx.measureText(text.slice(0, mid) + "…").width <= maxW) {
|
|
180
|
+
lo = mid;
|
|
181
|
+
} else {
|
|
182
|
+
hi = mid - 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return text.slice(0, lo) + "…";
|
|
187
|
+
}
|
|
@@ -50,6 +50,13 @@ export interface FacetGridOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
hasLegend?: boolean;
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Width of the legend gutter when `hasLegend` is set. Defaults to
|
|
55
|
+
* the historical `LEGEND_GUTTER` (96). Callers resolve this from
|
|
56
|
+
* `plugin_config.legend_width_px` via `legendSidebarWidth`.
|
|
57
|
+
*/
|
|
58
|
+
legendWidth?: number;
|
|
59
|
+
|
|
53
60
|
/** Axis-label allowance (consumed only when the corresponding axis
|
|
54
61
|
* mode produces a gutter — outer band or per-cell). */
|
|
55
62
|
hasXLabel?: boolean;
|
|
@@ -220,7 +227,7 @@ export function buildFacetGrid(
|
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
const titleBand = opts.titleBand ?? TITLE_BAND_DEFAULT;
|
|
223
|
-
const legendW = opts.hasLegend ? LEGEND_GUTTER : 0;
|
|
230
|
+
const legendW = opts.hasLegend ? (opts.legendWidth ?? LEGEND_GUTTER) : 0;
|
|
224
231
|
|
|
225
232
|
const xMode: AxisMode = opts.xAxis ?? "cell";
|
|
226
233
|
const yMode: AxisMode = opts.yAxis ?? "cell";
|
package/src/ts/layout/ticks.ts
CHANGED
|
@@ -69,6 +69,18 @@ export function computeNiceTicks(
|
|
|
69
69
|
return ticks;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export function stepTickFormatter(
|
|
73
|
+
isDate: boolean | undefined,
|
|
74
|
+
ticks: number[] | null | undefined,
|
|
75
|
+
): (v: number) => string {
|
|
76
|
+
if (!isDate) {
|
|
77
|
+
return formatTickValue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const step = ticks && ticks.length > 1 ? ticks[1] - ticks[0] : 0;
|
|
81
|
+
return (v: number) => formatDateTickValue(v, step);
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
/**
|
|
73
85
|
* Format a numeric tick value for display.
|
|
74
86
|
* Uses K/M/B suffixes for large numbers, fixed decimals for small.
|
|
@@ -119,7 +119,22 @@ export class TileLoader {
|
|
|
119
119
|
url: string,
|
|
120
120
|
signal: AbortSignal,
|
|
121
121
|
): Promise<ImageBitmap | null> {
|
|
122
|
-
|
|
122
|
+
// Explicit same-origin `referrer`: in worker mode this code runs
|
|
123
|
+
// in a blob-URL worker, whose client URL is unreferable — its
|
|
124
|
+
// fetches carry NO `Referer` under ANY document Referrer-Policy
|
|
125
|
+
// (verified empirically, Chromium 139). Tile providers gate on
|
|
126
|
+
// it: OSM serves "403r — referer is required" blocked TILES
|
|
127
|
+
// (HTTP 200, error drawn into the image; see the OSM wiki's
|
|
128
|
+
// Blocked_tiles page) to referer-less requests. An explicit
|
|
129
|
+
// same-origin referrer serializes to the page origin under the
|
|
130
|
+
// default policy, which is exactly the identification those
|
|
131
|
+
// providers ask for. Opaque/non-http origins (`"null"`) are
|
|
132
|
+
// skipped — the option would throw.
|
|
133
|
+
const referrer = self.origin?.startsWith("http")
|
|
134
|
+
? `${self.origin}/`
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
const resp = await fetch(url, { signal, referrer });
|
|
123
138
|
if (!resp.ok) {
|
|
124
139
|
return null;
|
|
125
140
|
}
|