@perspective-dev/viewer-charts 5.2.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/candlestick/candlestick-render.d.ts +3 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +7 -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 +24 -9
- package/dist/esm/charts/chart.d.ts +81 -6
- package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
- package/dist/esm/charts/common/tree-chart.d.ts +1 -0
- package/dist/esm/charts/common/tree-interact.d.ts +1 -1
- package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
- 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-type.d.ts +18 -0
- package/dist/esm/charts/series/series.d.ts +10 -1
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -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 +28 -1
- package/dist/esm/theme/gradient.d.ts +32 -0
- package/dist/esm/theme/overrides.d.ts +19 -0
- package/dist/esm/transport/protocol.d.ts +58 -5
- package/dist/esm/transport/renderer-transport.d.ts +3 -0
- package/dist/esm/utils/css.d.ts +4 -0
- package/dist/esm/worker/renderer.worker.d.ts +46 -1
- package/package.json +1 -1
- 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-build.ts +1 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
- package/src/ts/charts/candlestick/candlestick-render.ts +81 -10
- package/src/ts/charts/candlestick/candlestick.ts +4 -3
- package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
- package/src/ts/charts/cartesian/cartesian-render.ts +374 -75
- package/src/ts/charts/cartesian/cartesian.ts +13 -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 +76 -42
- package/src/ts/charts/chart.ts +111 -7
- package/src/ts/charts/common/category-axis-resolver.ts +44 -5
- package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
- package/src/ts/charts/common/tree-chart.ts +4 -0
- 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-build.ts +7 -1
- package/src/ts/charts/heatmap/heatmap-interact.ts +75 -8
- package/src/ts/charts/heatmap/heatmap-render.ts +105 -24
- package/src/ts/charts/heatmap/heatmap.ts +8 -3
- package/src/ts/charts/map/map.ts +3 -5
- package/src/ts/charts/series/series-build.ts +15 -2
- package/src/ts/charts/series/series-interact.ts +14 -24
- package/src/ts/charts/series/series-render.ts +310 -132
- package/src/ts/charts/series/series-type.ts +65 -0
- package/src/ts/charts/series/series.ts +31 -8
- package/src/ts/charts/sunburst/sunburst-render.ts +16 -1
- package/src/ts/charts/treemap/treemap-render.ts +12 -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 +48 -12
- 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 +330 -76
- package/src/ts/shaders/line.vert.glsl +5 -2
- package/src/ts/shaders/scatter.vert.glsl +0 -7
- package/src/ts/theme/gradient.ts +265 -1
- package/src/ts/theme/overrides.ts +76 -0
- package/src/ts/transport/protocol.ts +60 -4
- package/src/ts/transport/renderer-transport.ts +107 -14
- package/src/ts/utils/css.ts +12 -0
- package/src/ts/worker/dispatch.ts +6 -2
- package/src/ts/worker/renderer.worker.ts +191 -7
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./series-build";
|
|
31
31
|
import {
|
|
32
32
|
renderBarFrame,
|
|
33
|
+
renderBarChromeOverlay,
|
|
33
34
|
uploadBarInstances,
|
|
34
35
|
invalidateGlyphBuffers,
|
|
35
36
|
rebuildGlyphBuffers,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
showBarPinnedTooltipForSample,
|
|
42
43
|
} from "./series-interact";
|
|
43
44
|
import { resolvePalette } from "../../theme/palette";
|
|
45
|
+
import { resolveSeriesColorOverride } from "./series-type";
|
|
44
46
|
import { LineGlyph } from "./glyphs/draw-lines";
|
|
45
47
|
import { ScatterGlyph } from "./glyphs/draw-scatter";
|
|
46
48
|
import { AreaGlyph } from "./glyphs/draw-areas";
|
|
@@ -125,6 +127,15 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
125
127
|
return { lockAxis: this._isHorizontal ? "x" : "y" };
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Chrome-only repaint for legend scroll / floating-legend drag —
|
|
132
|
+
* the same lightweight path hover updates use. No GL pass; the
|
|
133
|
+
* composite re-presents over the retained plot bitmap.
|
|
134
|
+
*/
|
|
135
|
+
repaintChrome(): void {
|
|
136
|
+
renderBarChromeOverlay(this);
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
_locations: CachedLocations | null = null;
|
|
129
140
|
|
|
130
141
|
// Series-specific categorical-axis bookkeeping. `_rowPaths`,
|
|
@@ -250,13 +261,16 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
250
261
|
/**
|
|
251
262
|
* Cached palette + identity-keys for short-circuiting per-frame
|
|
252
263
|
* resolution. Inputs (`seriesPalette` ref, `gradientStops` ref,
|
|
253
|
-
* `series.length`
|
|
264
|
+
* `series.length`, `_columnsConfig` ref — per-series color
|
|
265
|
+
* overrides) only change on data load, `restyle()`, or
|
|
266
|
+
* `setColumnsConfig`.
|
|
254
267
|
*/
|
|
255
268
|
_paletteCache: [number, number, number][] | null = null;
|
|
256
269
|
_paletteCacheKey: {
|
|
257
270
|
seriesPalette: [number, number, number][] | null;
|
|
258
271
|
gradientStops: unknown;
|
|
259
272
|
seriesLength: number;
|
|
273
|
+
columnsConfig: unknown;
|
|
260
274
|
} | null = null;
|
|
261
275
|
|
|
262
276
|
/**
|
|
@@ -514,9 +528,10 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
514
528
|
return;
|
|
515
529
|
}
|
|
516
530
|
|
|
517
|
-
const groupByValues: (string | null)[] =
|
|
518
|
-
|
|
519
|
-
|
|
531
|
+
const groupByValues: (string | number | null)[] =
|
|
532
|
+
this._categoryAxisMode === "numeric" && this._categoryPositions
|
|
533
|
+
? [this._categoryPositions[b.catIdx] ?? null]
|
|
534
|
+
: this._rowPaths.map((level) => level.labels[b.catIdx] ?? null);
|
|
520
535
|
const splitKey = this._splitPrefixes[b.splitIdx] ?? "";
|
|
521
536
|
const splitByValues =
|
|
522
537
|
this._splitBy.length > 0 && splitKey !== ""
|
|
@@ -903,6 +918,7 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
903
918
|
const theme = chart._resolveTheme();
|
|
904
919
|
const seriesPalette = theme.seriesPalette;
|
|
905
920
|
const gradientStops = theme.gradientStops;
|
|
921
|
+
const columnsConfig = chart._columnsConfig;
|
|
906
922
|
const paletteCount = chart._facetActive
|
|
907
923
|
? Math.max(1, chart._aggregates.length)
|
|
908
924
|
: chart._series.length;
|
|
@@ -913,7 +929,8 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
913
929
|
key &&
|
|
914
930
|
key.seriesPalette === seriesPalette &&
|
|
915
931
|
key.gradientStops === gradientStops &&
|
|
916
|
-
key.seriesLength === paletteCount
|
|
932
|
+
key.seriesLength === paletteCount &&
|
|
933
|
+
key.columnsConfig === columnsConfig
|
|
917
934
|
) {
|
|
918
935
|
return false;
|
|
919
936
|
}
|
|
@@ -928,12 +945,18 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
928
945
|
seriesPalette,
|
|
929
946
|
gradientStops,
|
|
930
947
|
seriesLength: paletteCount,
|
|
948
|
+
columnsConfig,
|
|
931
949
|
};
|
|
932
950
|
|
|
933
951
|
for (let i = 0; i < chart._series.length; i++) {
|
|
934
|
-
chart._series[i]
|
|
935
|
-
|
|
936
|
-
|
|
952
|
+
const series = chart._series[i];
|
|
953
|
+
const themed = chart._facetActive ? palette[series.aggIdx] : palette[i];
|
|
954
|
+
series.color =
|
|
955
|
+
resolveSeriesColorOverride(
|
|
956
|
+
series,
|
|
957
|
+
chart._facetActive,
|
|
958
|
+
columnsConfig,
|
|
959
|
+
) ?? themed;
|
|
937
960
|
}
|
|
938
961
|
|
|
939
962
|
return true;
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
INNER_RING_PX,
|
|
29
29
|
} from "./sunburst-layout";
|
|
30
30
|
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
31
|
+
import { legendTreeGutter } from "../../interaction/legend-controller";
|
|
31
32
|
import { withChromeCache } from "../common/chrome-cache";
|
|
32
33
|
import {
|
|
33
34
|
renderBreadcrumbs as renderTreeBreadcrumbs,
|
|
@@ -106,7 +107,12 @@ export function renderSunburstFrame(
|
|
|
106
107
|
chart._colorMin < chart._colorMax;
|
|
107
108
|
const breadcrumbH =
|
|
108
109
|
!hasSplits && chart._breadcrumbIds.length > 1 ? BREADCRUMB_H : 0;
|
|
109
|
-
const legendW =
|
|
110
|
+
const legendW = legendTreeGutter(
|
|
111
|
+
chart._pluginConfig,
|
|
112
|
+
hasLegend,
|
|
113
|
+
LEGEND_W,
|
|
114
|
+
chart._colorMode === "series" ? chart._uniqueColorLabels.size : 0,
|
|
115
|
+
);
|
|
110
116
|
|
|
111
117
|
if (hasSplits) {
|
|
112
118
|
layoutFacetedSunburst(chart, cssWidth, cssHeight, legendW);
|
|
@@ -501,7 +507,16 @@ function drawArcs(
|
|
|
501
507
|
|
|
502
508
|
// Chrome overlay (Canvas2D)
|
|
503
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Render the chrome overlay (labels, center text, hover highlight +
|
|
512
|
+
* tooltip).
|
|
513
|
+
*/
|
|
504
514
|
export function renderSunburstChromeOverlay(chart: SunburstChart): void {
|
|
515
|
+
paintSunburstChromeOverlay(chart);
|
|
516
|
+
chart.presentOverlay();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function paintSunburstChromeOverlay(chart: SunburstChart): void {
|
|
505
520
|
if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
|
|
506
521
|
return;
|
|
507
522
|
}
|
|
@@ -23,6 +23,7 @@ import { Theme } from "../../theme/theme";
|
|
|
23
23
|
import { resolvePalette, type Vec3 } from "../../theme/palette";
|
|
24
24
|
import { type GradientStop } from "../../theme/gradient";
|
|
25
25
|
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
26
|
+
import { legendTreeGutter } from "../../interaction/legend-controller";
|
|
26
27
|
import { leafColor, leafRGBA, luminance } from "../common/leaf-color";
|
|
27
28
|
import treemapVert from "../../shaders/treemap.vert.glsl";
|
|
28
29
|
import treemapFrag from "../../shaders/treemap.frag.glsl";
|
|
@@ -70,7 +71,12 @@ export function renderTreemapFrame(
|
|
|
70
71
|
? chart._uniqueColorLabels.size > 1
|
|
71
72
|
: chart._colorMode === "numeric" &&
|
|
72
73
|
chart._colorMin < chart._colorMax;
|
|
73
|
-
const legendW =
|
|
74
|
+
const legendW = legendTreeGutter(
|
|
75
|
+
chart._pluginConfig,
|
|
76
|
+
hasLegend,
|
|
77
|
+
90,
|
|
78
|
+
chart._colorMode === "series" ? chart._uniqueColorLabels.size : 0,
|
|
79
|
+
);
|
|
74
80
|
|
|
75
81
|
// Scratch buffer for the ordered-layout child ids. Worst case:
|
|
76
82
|
// active children at every level = store.count. Reuse the chart's
|
|
@@ -467,6 +473,11 @@ function emitRect(
|
|
|
467
473
|
* tooltip + highlight on top.
|
|
468
474
|
*/
|
|
469
475
|
export function renderTreemapChromeOverlay(chart: TreemapChart): void {
|
|
476
|
+
paintTreemapChromeOverlay(chart);
|
|
477
|
+
chart.presentOverlay();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function paintTreemapChromeOverlay(chart: TreemapChart): void {
|
|
470
481
|
if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
|
|
471
482
|
return;
|
|
472
483
|
}
|
|
@@ -95,12 +95,9 @@ export async function viewToColumnDataMap(
|
|
|
95
95
|
} else if (vals instanceof Float64Array) {
|
|
96
96
|
// Datetime/Date columns are emitted as Float64 to keep
|
|
97
97
|
// millisecond precision; numeric Float64 also lands here
|
|
98
|
-
// when `float32` mode is off.
|
|
99
|
-
// chart's CPU mirrors and extents will rebase to f32 at
|
|
100
|
-
// upload time.
|
|
98
|
+
// when `float32` mode is off.
|
|
101
99
|
result.set(name, { type: "float64", values: vals, valid });
|
|
102
100
|
} else {
|
|
103
|
-
// Fallback: treat as float32
|
|
104
101
|
// TODO: Instance check if this needs a copy?
|
|
105
102
|
result.set(name, {
|
|
106
103
|
type: "float32",
|
package/src/ts/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { HTMLPerspectiveViewerWebGLPluginElement } from "./plugin/plugin";
|
|
|
15
15
|
|
|
16
16
|
export type { PerspectiveClickDetail } from "./event-detail";
|
|
17
17
|
export { PerspectiveSelectDetail } from "./event-detail";
|
|
18
|
+
export { registerTileSource } from "./plugin/plugin";
|
|
19
|
+
export { tileSources, type TileSourceSpec } from "./map/tile-source";
|
|
18
20
|
|
|
19
21
|
export function register(...plugin_names: string[]) {
|
|
20
22
|
const plugins = new Set(
|
|
@@ -0,0 +1,137 @@
|
|
|
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 { Canvas2D, Context2D } from "../charts/canvas-types";
|
|
14
|
+
import type { PlotLayout } from "../layout/plot-layout";
|
|
15
|
+
import type { Theme } from "../theme/theme";
|
|
16
|
+
|
|
17
|
+
export type AxisSide = "bottom" | "left" | "right";
|
|
18
|
+
|
|
19
|
+
export interface AxisIndicator {
|
|
20
|
+
side: AxisSide;
|
|
21
|
+
|
|
22
|
+
px: number;
|
|
23
|
+
py: number;
|
|
24
|
+
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BADGE_FONT_PX = 11;
|
|
29
|
+
const BADGE_PAD_H = 4;
|
|
30
|
+
const BADGE_PAD_V = 2;
|
|
31
|
+
const BADGE_GAP = 4;
|
|
32
|
+
|
|
33
|
+
export function renderAxisHoverIndicators(
|
|
34
|
+
canvas: Canvas2D | null,
|
|
35
|
+
layout: PlotLayout,
|
|
36
|
+
theme: Theme,
|
|
37
|
+
dpr: number,
|
|
38
|
+
indicators: AxisIndicator[],
|
|
39
|
+
opacity: number = 1.0,
|
|
40
|
+
): void {
|
|
41
|
+
if (!canvas || indicators.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ctx = canvas.getContext("2d") as Context2D | null;
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const plot = layout.plotRect;
|
|
51
|
+
ctx.save();
|
|
52
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
53
|
+
ctx.scale(dpr, dpr);
|
|
54
|
+
ctx.font = `${BADGE_FONT_PX}px ${theme.fontFamily}`;
|
|
55
|
+
|
|
56
|
+
for (const ind of indicators) {
|
|
57
|
+
if (
|
|
58
|
+
ind.px < plot.x ||
|
|
59
|
+
ind.px > plot.x + plot.width ||
|
|
60
|
+
ind.py < plot.y ||
|
|
61
|
+
ind.py > plot.y + plot.height
|
|
62
|
+
) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
drawTraceLine(ctx, ind, plot, theme);
|
|
67
|
+
drawBadge(ctx, ind, layout, theme, opacity);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ctx.restore();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function drawTraceLine(
|
|
74
|
+
ctx: Context2D,
|
|
75
|
+
ind: AxisIndicator,
|
|
76
|
+
plot: { x: number; y: number; width: number; height: number },
|
|
77
|
+
theme: Theme,
|
|
78
|
+
): void {
|
|
79
|
+
ctx.strokeStyle = theme.tickColor;
|
|
80
|
+
ctx.lineWidth = 1;
|
|
81
|
+
ctx.setLineDash([4, 4]);
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.moveTo(ind.px, ind.py);
|
|
84
|
+
if (ind.side === "bottom") {
|
|
85
|
+
ctx.lineTo(ind.px, plot.y + plot.height);
|
|
86
|
+
} else if (ind.side === "left") {
|
|
87
|
+
ctx.lineTo(plot.x, ind.py);
|
|
88
|
+
} else {
|
|
89
|
+
ctx.lineTo(plot.x + plot.width, ind.py);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.stroke();
|
|
93
|
+
ctx.setLineDash([]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function drawBadge(
|
|
97
|
+
ctx: Context2D,
|
|
98
|
+
ind: AxisIndicator,
|
|
99
|
+
layout: PlotLayout,
|
|
100
|
+
theme: Theme,
|
|
101
|
+
opacity: number,
|
|
102
|
+
): void {
|
|
103
|
+
const plot = layout.plotRect;
|
|
104
|
+
const boxW = ctx.measureText(ind.text).width + BADGE_PAD_H * 2;
|
|
105
|
+
const boxH = BADGE_FONT_PX + BADGE_PAD_V * 2 + 2;
|
|
106
|
+
|
|
107
|
+
let bx: number;
|
|
108
|
+
let by: number;
|
|
109
|
+
if (ind.side === "bottom") {
|
|
110
|
+
bx = ind.px - boxW / 2;
|
|
111
|
+
by = plot.y + plot.height + BADGE_GAP;
|
|
112
|
+
} else if (ind.side === "left") {
|
|
113
|
+
bx = plot.x - BADGE_GAP - boxW;
|
|
114
|
+
by = ind.py - boxH / 2;
|
|
115
|
+
} else {
|
|
116
|
+
bx = plot.x + plot.width + BADGE_GAP;
|
|
117
|
+
by = ind.py - boxH / 2;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
bx = Math.min(Math.max(bx, 0), Math.max(0, layout.cssWidth - boxW));
|
|
121
|
+
by = Math.min(Math.max(by, 0), Math.max(0, layout.cssHeight - boxH));
|
|
122
|
+
|
|
123
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
124
|
+
ctx.fillStyle = theme.tooltipBg;
|
|
125
|
+
ctx.strokeStyle = theme.tooltipBorder;
|
|
126
|
+
ctx.lineWidth = 1;
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.rect(bx, by, boxW, boxH);
|
|
129
|
+
ctx.fill();
|
|
130
|
+
ctx.stroke();
|
|
131
|
+
ctx.globalAlpha = 1.0;
|
|
132
|
+
|
|
133
|
+
ctx.fillStyle = theme.tooltipText;
|
|
134
|
+
ctx.textAlign = "left";
|
|
135
|
+
ctx.textBaseline = "middle";
|
|
136
|
+
ctx.fillText(ind.text, bx + BADGE_PAD_H, by + boxH / 2 + 1);
|
|
137
|
+
}
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
import type { CssBounds, HostSink } from "./tooltip-controller";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_TOOLTIP_STYLE,
|
|
16
|
+
NUMERIC_RE,
|
|
17
|
+
TOOLTIP_COL_GAP,
|
|
18
|
+
type TooltipContent,
|
|
19
|
+
type TooltipStyle,
|
|
20
|
+
} from "./tooltip-grid";
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* Host-side `HostSink` that materializes pinned tooltips as a `<div>`
|
|
@@ -29,16 +36,17 @@ export class DomHostSink implements HostSink {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
pin(
|
|
32
|
-
|
|
39
|
+
grid: TooltipContent,
|
|
33
40
|
pos: { px: number; py: number },
|
|
34
41
|
bounds: CssBounds,
|
|
42
|
+
style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
|
|
35
43
|
): void {
|
|
36
44
|
this.dismiss();
|
|
37
45
|
const div = document.createElement("div");
|
|
38
46
|
|
|
39
47
|
div.className = "webgl-tooltip";
|
|
40
48
|
div.style.maxHeight = `${Math.round(bounds.cssHeight * 0.6)}px`;
|
|
41
|
-
div
|
|
49
|
+
renderTooltipGridDom(div, grid, style);
|
|
42
50
|
|
|
43
51
|
if (getComputedStyle(this._parent).position === "static") {
|
|
44
52
|
this._parent.style.position = "relative";
|
|
@@ -83,3 +91,59 @@ export class DomHostSink implements HostSink {
|
|
|
83
91
|
this._glCanvas.style.cursor = cursor;
|
|
84
92
|
}
|
|
85
93
|
}
|
|
94
|
+
|
|
95
|
+
function renderTooltipGridDom(
|
|
96
|
+
root: HTMLDivElement,
|
|
97
|
+
grid: TooltipContent,
|
|
98
|
+
style: TooltipStyle,
|
|
99
|
+
): void {
|
|
100
|
+
const ncols = grid.reduce((n, row) => Math.max(n, row.length), 0);
|
|
101
|
+
root.style.display = "grid";
|
|
102
|
+
root.style.gridTemplateColumns = `repeat(${Math.max(1, ncols)}, minmax(0, max-content))`;
|
|
103
|
+
root.style.columnGap = `${TOOLTIP_COL_GAP}px`;
|
|
104
|
+
if (style.opacity < 1) {
|
|
105
|
+
const pct = Math.max(0, Math.round(style.opacity * 100));
|
|
106
|
+
root.style.background = `color-mix(in srgb, var(--psp-charts--tooltip--background) ${pct}%, transparent)`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rightAlign: boolean[] = [];
|
|
110
|
+
for (const row of grid) {
|
|
111
|
+
if (row.length <= 1) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (let j = 1; j < row.length; j++) {
|
|
116
|
+
if (row[j] === "") {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const numeric = NUMERIC_RE.test(row[j]);
|
|
121
|
+
rightAlign[j] =
|
|
122
|
+
rightAlign[j] === undefined
|
|
123
|
+
? numeric
|
|
124
|
+
: rightAlign[j] && numeric;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const row of grid) {
|
|
129
|
+
const span = row.length <= 1;
|
|
130
|
+
for (let j = 0; j < Math.max(1, row.length); j++) {
|
|
131
|
+
const cell = document.createElement("div");
|
|
132
|
+
cell.textContent = row[j] ?? "";
|
|
133
|
+
cell.style.maxWidth = `${style.maxColumnPx * (span ? 2 : 1)}px`;
|
|
134
|
+
cell.style.overflow = "hidden";
|
|
135
|
+
cell.style.textOverflow = "ellipsis";
|
|
136
|
+
cell.style.whiteSpace = "nowrap";
|
|
137
|
+
if (span) {
|
|
138
|
+
cell.style.gridColumn = "1 / -1";
|
|
139
|
+
} else if (rightAlign[j]) {
|
|
140
|
+
cell.style.textAlign = "right";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
root.appendChild(cell);
|
|
144
|
+
if (span) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -23,6 +23,11 @@ import type {
|
|
|
23
23
|
UserClickPayload,
|
|
24
24
|
UserSelectPayload,
|
|
25
25
|
} from "./tooltip-controller";
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_TOOLTIP_STYLE,
|
|
28
|
+
type TooltipContent,
|
|
29
|
+
type TooltipStyle,
|
|
30
|
+
} from "./tooltip-grid";
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* The subset of `WorkerMsg`s that flow chart → host through a
|
|
@@ -52,11 +57,12 @@ export class MessageHostSink implements HostSink {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
pin(
|
|
55
|
-
|
|
60
|
+
grid: TooltipContent,
|
|
56
61
|
pos: { px: number; py: number },
|
|
57
62
|
bounds: CssBounds,
|
|
63
|
+
style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
|
|
58
64
|
): void {
|
|
59
|
-
this._send({ kind: "pinTooltip",
|
|
65
|
+
this._send({ kind: "pinTooltip", grid, style, pos, bounds });
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
dismiss(): void {
|
|
@@ -32,7 +32,7 @@ export class LazyTooltip<Target> {
|
|
|
32
32
|
/**
|
|
33
33
|
* Cached lines for the latest committed hover, or `null`.
|
|
34
34
|
*/
|
|
35
|
-
|
|
35
|
+
grid: string[][] | null = null;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Identity of the entity `lines` describe. `null` when cleared.
|
|
@@ -50,7 +50,7 @@ export class LazyTooltip<Target> {
|
|
|
50
50
|
* the write.
|
|
51
51
|
*/
|
|
52
52
|
beginHover(target: Target): number {
|
|
53
|
-
this.
|
|
53
|
+
this.grid = null;
|
|
54
54
|
this.hoveredTarget = target;
|
|
55
55
|
return ++this._hoverSerial;
|
|
56
56
|
}
|
|
@@ -60,12 +60,12 @@ export class LazyTooltip<Target> {
|
|
|
60
60
|
* when the write happened (caller should repaint), false when the
|
|
61
61
|
* serial was stale.
|
|
62
62
|
*/
|
|
63
|
-
commitHover(serial: number,
|
|
63
|
+
commitHover(serial: number, grid: string[][]): boolean {
|
|
64
64
|
if (serial !== this._hoverSerial) {
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
this.
|
|
68
|
+
this.grid = grid;
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -73,7 +73,7 @@ export class LazyTooltip<Target> {
|
|
|
73
73
|
* Clear hover state (mouse left, view changed, etc.).
|
|
74
74
|
*/
|
|
75
75
|
clearHover(): void {
|
|
76
|
-
this.
|
|
76
|
+
this.grid = null;
|
|
77
77
|
this.hoveredTarget = null;
|
|
78
78
|
this._hoverSerial++;
|
|
79
79
|
}
|