@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
|
@@ -67,12 +67,16 @@ export interface Glyph {
|
|
|
67
67
|
buildTooltipLines(
|
|
68
68
|
chart: CartesianChart,
|
|
69
69
|
flatIdx: number,
|
|
70
|
-
): Promise<string[]>;
|
|
70
|
+
): Promise<string[][]>;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Hover-overlay options (crosshair, highlight radius).
|
|
74
74
|
*/
|
|
75
|
-
tooltipOptions(): {
|
|
75
|
+
tooltipOptions(): {
|
|
76
|
+
crosshair: boolean;
|
|
77
|
+
highlightRadius: number;
|
|
78
|
+
axisIndicators: boolean;
|
|
79
|
+
};
|
|
76
80
|
|
|
77
81
|
/**
|
|
78
82
|
* Release GL resources created by `ensureProgram`.
|
|
@@ -309,12 +309,12 @@ export class DensityGlyph implements Glyph {
|
|
|
309
309
|
buildTooltipLines(
|
|
310
310
|
chart: CartesianChart,
|
|
311
311
|
flatIdx: number,
|
|
312
|
-
): Promise<string[]> {
|
|
312
|
+
): Promise<string[][]> {
|
|
313
313
|
return buildPointRowTooltipLines(chart, flatIdx);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
tooltipOptions() {
|
|
317
|
-
return { crosshair: true, highlightRadius: 0 };
|
|
317
|
+
return { crosshair: true, highlightRadius: 0, axisIndicators: true };
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
destroy(chart: CartesianChart): void {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getInstancing,
|
|
20
20
|
} from "../../../webgl/instanced-attrs";
|
|
21
21
|
import { compileProgram } from "../../../webgl/program-cache";
|
|
22
|
+
import { colorRangePivot } from "../../../theme/gradient";
|
|
22
23
|
import { formatTickValue, formatDateTickValue } from "../../../layout/ticks";
|
|
23
24
|
import lineVert from "../../../shaders/line.vert.glsl";
|
|
24
25
|
import lineFrag from "../../../shaders/line.frag.glsl";
|
|
@@ -124,17 +125,17 @@ export class LineGlyph implements Glyph {
|
|
|
124
125
|
async buildTooltipLines(
|
|
125
126
|
chart: CartesianChart,
|
|
126
127
|
flatIdx: number,
|
|
127
|
-
): Promise<string[]> {
|
|
128
|
-
const
|
|
128
|
+
): Promise<string[][]> {
|
|
129
|
+
const grid: string[][] = [];
|
|
129
130
|
if (!chart._xData || !chart._yData) {
|
|
130
|
-
return
|
|
131
|
+
return grid;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
if (chart._splitGroups.length > 0 && chart._seriesCapacity > 0) {
|
|
134
135
|
const seriesIdx = Math.floor(flatIdx / chart._seriesCapacity);
|
|
135
136
|
const sg = chart._splitGroups[seriesIdx];
|
|
136
137
|
if (sg) {
|
|
137
|
-
|
|
138
|
+
grid.push([sg.prefix]);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -146,20 +147,20 @@ export class LineGlyph implements Glyph {
|
|
|
146
147
|
const xFormatted = xIsDate
|
|
147
148
|
? formatDateTickValue(xVal)
|
|
148
149
|
: formatTickValue(xVal);
|
|
149
|
-
|
|
150
|
+
grid.push([chart._xLabel || "Row", xFormatted]);
|
|
150
151
|
|
|
151
152
|
const yType = chart._columnTypes[chart._yLabel] || "";
|
|
152
153
|
const yIsDate = yType === "date" || yType === "datetime";
|
|
153
154
|
const yFormatted = yIsDate
|
|
154
155
|
? formatDateTickValue(yVal)
|
|
155
156
|
: formatTickValue(yVal);
|
|
156
|
-
|
|
157
|
+
grid.push([chart._yLabel, yFormatted]);
|
|
157
158
|
|
|
158
|
-
return
|
|
159
|
+
return grid;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
tooltipOptions() {
|
|
162
|
-
return { crosshair: true, highlightRadius: 5 };
|
|
163
|
+
return { crosshair: true, highlightRadius: 5, axisIndicators: true };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
destroy(chart: CartesianChart): void {
|
|
@@ -195,10 +196,13 @@ function bindLineState(
|
|
|
195
196
|
gl.uniformMatrix4fv(cache.u_projection, false, projection);
|
|
196
197
|
gl.uniform2f(cache.u_resolution, gl.canvas.width, gl.canvas.height);
|
|
197
198
|
gl.uniform1f(cache.u_line_width, chart._pluginConfig.line_width_px * dpr);
|
|
198
|
-
if (chart._colorMin
|
|
199
|
-
gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
|
|
200
|
-
} else {
|
|
199
|
+
if (chart._colorMin >= chart._colorMax) {
|
|
201
200
|
gl.uniform2f(cache.u_color_range, 0.0, 0.0);
|
|
201
|
+
} else if (chart._colorName && !chart._colorIsString) {
|
|
202
|
+
const [lo, hi] = colorRangePivot(chart._colorMin, chart._colorMax);
|
|
203
|
+
gl.uniform2f(cache.u_color_range, lo, hi);
|
|
204
|
+
} else {
|
|
205
|
+
gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
bindGradientTexture(
|
|
@@ -14,6 +14,7 @@ import type { WebGLContextManager } from "../../../webgl/context-manager";
|
|
|
14
14
|
import type { CartesianChart } from "../cartesian";
|
|
15
15
|
import type { Glyph } from "../glyph";
|
|
16
16
|
import { bindGradientTexture } from "../../../webgl/gradient-texture";
|
|
17
|
+
import { colorRangePivot } from "../../../theme/gradient";
|
|
17
18
|
import { compileProgram } from "../../../webgl/program-cache";
|
|
18
19
|
import { buildPointRowTooltipLines } from "../tooltip-lines";
|
|
19
20
|
import scatterVert from "../../../shaders/scatter.vert.glsl";
|
|
@@ -130,12 +131,12 @@ export class PointGlyph implements Glyph {
|
|
|
130
131
|
buildTooltipLines(
|
|
131
132
|
chart: CartesianChart,
|
|
132
133
|
flatIdx: number,
|
|
133
|
-
): Promise<string[]> {
|
|
134
|
+
): Promise<string[][]> {
|
|
134
135
|
return buildPointRowTooltipLines(chart, flatIdx);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
tooltipOptions() {
|
|
138
|
-
return { crosshair: true, highlightRadius: 6 };
|
|
139
|
+
return { crosshair: true, highlightRadius: 6, axisIndicators: true };
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
destroy(_chart: CartesianChart): void {
|
|
@@ -155,10 +156,13 @@ function setUniforms(
|
|
|
155
156
|
gl.uniformMatrix4fv(cache.u_projection, false, projection);
|
|
156
157
|
gl.uniform1f(cache.u_point_size, chart._pluginConfig.point_size_px * dpr);
|
|
157
158
|
|
|
158
|
-
if (chart._colorMin
|
|
159
|
-
gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
|
|
160
|
-
} else {
|
|
159
|
+
if (chart._colorMin >= chart._colorMax) {
|
|
161
160
|
gl.uniform2f(cache.u_color_range, 0.0, 0.0);
|
|
161
|
+
} else if (chart._colorName && !chart._colorIsString) {
|
|
162
|
+
const [lo, hi] = colorRangePivot(chart._colorMin, chart._colorMax);
|
|
163
|
+
gl.uniform2f(cache.u_color_range, lo, hi);
|
|
164
|
+
} else {
|
|
165
|
+
gl.uniform2f(cache.u_color_range, chart._colorMin, chart._colorMax);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
if (chart._sizeMin < chart._sizeMax) {
|
|
@@ -24,22 +24,22 @@ import type { CartesianChart } from "./cartesian";
|
|
|
24
24
|
export async function buildPointRowTooltipLines(
|
|
25
25
|
chart: CartesianChart,
|
|
26
26
|
flatIdx: number,
|
|
27
|
-
): Promise<string[]> {
|
|
28
|
-
const
|
|
27
|
+
): Promise<string[][]> {
|
|
28
|
+
const grid: string[][] = [];
|
|
29
29
|
if (!chart._rowIndexData || !chart._lazyRows) {
|
|
30
|
-
return
|
|
30
|
+
return grid;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const rowIdx = chart._rowIndexData[flatIdx];
|
|
34
34
|
if (rowIdx < 0) {
|
|
35
|
-
return
|
|
35
|
+
return grid;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (chart._splitGroups.length > 0 && chart._seriesCapacity > 0) {
|
|
39
39
|
const seriesIdx = Math.floor(flatIdx / chart._seriesCapacity);
|
|
40
40
|
const sg = chart._splitGroups[seriesIdx];
|
|
41
41
|
if (sg?.prefix) {
|
|
42
|
-
|
|
42
|
+
grid.push([sg.prefix]);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -70,11 +70,11 @@ export async function buildPointRowTooltipLines(
|
|
|
70
70
|
|
|
71
71
|
if (typeof value === "number") {
|
|
72
72
|
const formatted = chart.getColumnFormatter(colName, "value")(value);
|
|
73
|
-
|
|
73
|
+
grid.push([displayName, formatted]);
|
|
74
74
|
} else {
|
|
75
|
-
|
|
75
|
+
grid.push([displayName, String(value)]);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
return
|
|
79
|
+
return grid;
|
|
80
80
|
}
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
} from "@perspective-dev/viewer/src/ts/column-format.js";
|
|
22
22
|
import type { ColumnDataMap } from "../data/view-reader";
|
|
23
23
|
import { LazyRowFetcher } from "../data/lazy-row";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
CHART_DATE_DEFAULTS,
|
|
26
|
+
CHART_DATETIME_DEFAULTS,
|
|
27
|
+
CHART_NUMBER_DEFAULTS,
|
|
28
|
+
} from "../plugin/format-defaults";
|
|
25
29
|
import type { WebGLContextManager } from "../webgl/context-manager";
|
|
26
30
|
import {
|
|
27
31
|
ZoomController,
|
|
@@ -41,51 +45,37 @@ import {
|
|
|
41
45
|
type UserClickPayload,
|
|
42
46
|
type UserSelectPayload,
|
|
43
47
|
} from "../interaction/tooltip-controller";
|
|
48
|
+
import { LegendController } from "../interaction/legend-controller";
|
|
44
49
|
import type { PerspectiveClickDetail } from "../event-detail";
|
|
45
50
|
import type { ViewConfig } from "@perspective-dev/client";
|
|
46
51
|
import { resolveThemeFromVars, type Theme } from "../theme/theme";
|
|
52
|
+
import { applyColumnColorOverrides } from "../theme/overrides";
|
|
47
53
|
import { requestRender as scheduleRender } from "../render/scheduler";
|
|
48
54
|
|
|
49
|
-
// TODO I don't know if this is the behavior we want. On the plus side, this
|
|
50
|
-
// ad-hoc formatter scales well to small and large data ranges, making a good
|
|
51
|
-
// guess at the right format without user input. On the minus side, this
|
|
52
|
-
// behavior is inconsistent with datagrid and the rest of the app, and the ad-hoc
|
|
53
|
-
// surprising behavior when overriding one field in `number_format` and suddenly
|
|
54
|
-
// the entire formatter is replaced.
|
|
55
|
-
const REGRESSION_BEHAVIOR = true;
|
|
56
|
-
|
|
57
55
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* fractional digits matches the legacy datagrid default and gives
|
|
61
|
-
* tooltips a stable display width.
|
|
56
|
+
* Fallback formatter applied to numeric tooltip / legend values when the
|
|
57
|
+
* column has no `number_format` configured.
|
|
62
58
|
*/
|
|
63
59
|
const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
|
|
64
60
|
v: number,
|
|
65
61
|
) => string) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
const intl = createNumberFormatter(
|
|
63
|
+
"float",
|
|
64
|
+
undefined,
|
|
65
|
+
CHART_NUMBER_DEFAULTS,
|
|
66
|
+
);
|
|
67
|
+
return (v) => intl.format(v);
|
|
72
68
|
})();
|
|
73
69
|
|
|
74
70
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* default (no `dateStyle` / `timeStyle`) to match what most users
|
|
78
|
-
* expect from an `Intl.DateTimeFormat()` constructed with no options.
|
|
71
|
+
* Fallback formatter for datetime tooltip / legend values when the
|
|
72
|
+
* column has no `date_format` configured.
|
|
79
73
|
*/
|
|
80
74
|
const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
|
|
81
75
|
v: number,
|
|
82
76
|
) => string) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} else {
|
|
86
|
-
const intl = createDatetimeFormatter();
|
|
87
|
-
return (v) => intl.format(v);
|
|
88
|
-
}
|
|
77
|
+
const intl = createDatetimeFormatter(undefined, CHART_DATETIME_DEFAULTS);
|
|
78
|
+
return (v) => intl.format(v);
|
|
89
79
|
})();
|
|
90
80
|
|
|
91
81
|
/**
|
|
@@ -141,6 +131,7 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
141
131
|
_glManager: WebGLContextManager | null = null;
|
|
142
132
|
_gridlineCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
|
143
133
|
_chromeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
|
134
|
+
_overlayPresenter: (() => void) | null = null;
|
|
144
135
|
|
|
145
136
|
/**
|
|
146
137
|
* 2D-canvas draw closures collected during `_fullRender` (the GL
|
|
@@ -236,6 +227,7 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
236
227
|
*/
|
|
237
228
|
_pluginConfig: PluginConfig = { ...DEFAULT_PLUGIN_CONFIG };
|
|
238
229
|
|
|
230
|
+
_legend = new LegendController();
|
|
239
231
|
_tooltip = new TooltipController();
|
|
240
232
|
|
|
241
233
|
/**
|
|
@@ -259,12 +251,16 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
259
251
|
|
|
260
252
|
/**
|
|
261
253
|
* Cached resolved theme — populated on first `_resolveTheme()` call,
|
|
262
|
-
* cleared by `invalidateTheme()` (driven from `plugin.restyle()`)
|
|
263
|
-
*
|
|
264
|
-
*
|
|
254
|
+
* cleared by `invalidateTheme()` (driven from `plugin.restyle()`)
|
|
255
|
+
* and by `setColumnsConfig` (per-column color overrides patch the
|
|
256
|
+
* resolved theme). `getComputedStyle` / `getPropertyValue` reads
|
|
257
|
+
* cost ~100µs each; zoom/hover dispatch redraws at 60Hz so we
|
|
258
|
+
* resolve once and reuse.
|
|
265
259
|
*/
|
|
266
260
|
_theme: Theme | null = null;
|
|
267
261
|
|
|
262
|
+
_themeColorColumn: string | null = null;
|
|
263
|
+
|
|
268
264
|
/**
|
|
269
265
|
* On-demand single-row fetcher used by lazy tooltip column
|
|
270
266
|
* lookups. Reset on every `setView` call; subclasses read
|
|
@@ -287,6 +283,18 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
287
283
|
this._chromeCanvas = canvas;
|
|
288
284
|
}
|
|
289
285
|
|
|
286
|
+
setOverlayPresenter(cb: () => void): void {
|
|
287
|
+
this._overlayPresenter = cb;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Present a chrome-overlay repaint that ran outside the scheduler's
|
|
292
|
+
* frame chain.
|
|
293
|
+
*/
|
|
294
|
+
presentOverlay(): void {
|
|
295
|
+
this._overlayPresenter?.();
|
|
296
|
+
}
|
|
297
|
+
|
|
290
298
|
setTheme(vars: Record<string, string>): void {
|
|
291
299
|
this._themeVars = vars;
|
|
292
300
|
this._theme = null;
|
|
@@ -434,6 +442,8 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
434
442
|
setColumnsConfig(cfg: Record<string, any>): void {
|
|
435
443
|
this._columnsConfig = cfg ?? {};
|
|
436
444
|
this._rebuildColumnFormatters();
|
|
445
|
+
|
|
446
|
+
this._theme = null;
|
|
437
447
|
}
|
|
438
448
|
|
|
439
449
|
/**
|
|
@@ -475,7 +485,11 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
475
485
|
return undefined;
|
|
476
486
|
}
|
|
477
487
|
|
|
478
|
-
const intl = createNumberFormatter(
|
|
488
|
+
const intl = createNumberFormatter(
|
|
489
|
+
type,
|
|
490
|
+
numberFormat,
|
|
491
|
+
CHART_NUMBER_DEFAULTS,
|
|
492
|
+
);
|
|
479
493
|
return (v) => intl.format(v);
|
|
480
494
|
}
|
|
481
495
|
|
|
@@ -485,7 +499,10 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
485
499
|
return undefined;
|
|
486
500
|
}
|
|
487
501
|
|
|
488
|
-
const intl = createDatetimeFormatter(
|
|
502
|
+
const intl = createDatetimeFormatter(
|
|
503
|
+
dateFormat,
|
|
504
|
+
CHART_DATETIME_DEFAULTS,
|
|
505
|
+
);
|
|
489
506
|
return (v) => intl.format(v);
|
|
490
507
|
}
|
|
491
508
|
|
|
@@ -495,7 +512,7 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
495
512
|
return undefined;
|
|
496
513
|
}
|
|
497
514
|
|
|
498
|
-
const intl = createDateFormatter(dateFormat);
|
|
515
|
+
const intl = createDateFormatter(dateFormat, CHART_DATE_DEFAULTS);
|
|
499
516
|
return (v) => intl.format(v);
|
|
500
517
|
}
|
|
501
518
|
|
|
@@ -588,6 +605,11 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
588
605
|
* inputs in `uploadAndRender`; they take effect on next data load.
|
|
589
606
|
*/
|
|
590
607
|
setPluginConfig(cfg: PluginConfig): void {
|
|
608
|
+
// Persistence echo vs. real change: a restore whose legend
|
|
609
|
+
// fields equal the current values (the round-trip of a
|
|
610
|
+
// completed drag) must not disturb legend scroll or an
|
|
611
|
+
// in-flight gesture; different values win and cancel any drag.
|
|
612
|
+
this._legend.reconcileConfig(this._pluginConfig, cfg);
|
|
591
613
|
this._pluginConfig = { ...cfg };
|
|
592
614
|
this._facetConfig = {
|
|
593
615
|
...this._facetConfig,
|
|
@@ -599,16 +621,28 @@ export abstract class AbstractChart implements ChartImplementation {
|
|
|
599
621
|
cfg.series_zoom_mode === "dynamic";
|
|
600
622
|
}
|
|
601
623
|
|
|
624
|
+
protected colorScaleColumn(): string | null {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
602
628
|
/**
|
|
603
|
-
* Lazily decode the host-supplied theme vars
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
629
|
+
* Lazily decode the host-supplied theme vars, then patch in any
|
|
630
|
+
* per-column color-scale override for `colorScaleColumn()`.
|
|
631
|
+
* Subsequent calls hit the cache until `invalidateTheme()` /
|
|
632
|
+
* `setColumnsConfig` clears it or the color column changes.
|
|
633
|
+
* Render-path callers should always read theme values through this
|
|
634
|
+
* method so the parsed `Theme` (gradient stops, palette, etc.)
|
|
635
|
+
* amortizes across an entire frame.
|
|
608
636
|
*/
|
|
609
637
|
_resolveTheme(): Theme {
|
|
610
|
-
|
|
611
|
-
|
|
638
|
+
const colorColumn = this.colorScaleColumn();
|
|
639
|
+
if (!this._theme || this._themeColorColumn !== colorColumn) {
|
|
640
|
+
this._themeColorColumn = colorColumn;
|
|
641
|
+
this._theme = applyColumnColorOverrides(
|
|
642
|
+
resolveThemeFromVars(this._themeVars),
|
|
643
|
+
this._columnsConfig,
|
|
644
|
+
colorColumn,
|
|
645
|
+
);
|
|
612
646
|
}
|
|
613
647
|
|
|
614
648
|
return this._theme;
|
package/src/ts/charts/chart.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
import type { View } from "@perspective-dev/client";
|
|
14
|
+
import { TILE_SOURCES } from "../map/tile-source";
|
|
14
15
|
import type { ColumnDataMap } from "../data/view-reader";
|
|
15
16
|
import type { WebGLContextManager } from "../webgl/context-manager";
|
|
16
17
|
import type { ZoomController } from "../interaction/zoom-controller";
|
|
@@ -84,6 +85,11 @@ export interface ChartImplementation {
|
|
|
84
85
|
*/
|
|
85
86
|
setChromeCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void;
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Install the renderer's overlay-present hook.
|
|
90
|
+
*/
|
|
91
|
+
setOverlayPresenter?(cb: () => void): void;
|
|
92
|
+
|
|
87
93
|
/**
|
|
88
94
|
* Hand the chart a pre-computed CSS-variable map produced on the
|
|
89
95
|
* main thread via `snapshotThemeVars(el)`, which it can decode into
|
|
@@ -193,6 +199,8 @@ export interface ChartImplementation {
|
|
|
193
199
|
*/
|
|
194
200
|
deselect?(): void;
|
|
195
201
|
|
|
202
|
+
repaintChrome?(): void;
|
|
203
|
+
|
|
196
204
|
destroy(): void;
|
|
197
205
|
}
|
|
198
206
|
|
|
@@ -426,13 +434,15 @@ export interface PluginConfig {
|
|
|
426
434
|
gradient_color_mode: "mean" | "density" | "extreme" | "signed";
|
|
427
435
|
|
|
428
436
|
/**
|
|
429
|
-
* Map basemap tile provider
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
437
|
+
* Map basemap tile provider — a `TileSourceSpec` id from the
|
|
438
|
+
* tile-source registry ([map/tile-sources.json] entries plus any
|
|
439
|
+
* runtime `registerTileSource` additions). Applies only to map
|
|
440
|
+
* plugin tags (`map-scatter`, `map-line`, `map-density`); other
|
|
441
|
+
* charts ignore the field. The default is the JSON's FIRST entry
|
|
442
|
+
* (reordering the file changes the default), and unknown ids fall
|
|
443
|
+
* back to that same entry rather than blanking the map.
|
|
434
444
|
*/
|
|
435
|
-
map_tile_provider:
|
|
445
|
+
map_tile_provider: string;
|
|
436
446
|
|
|
437
447
|
/**
|
|
438
448
|
* Map basemap alpha (0..1). Pre-multiplied into the tile fragment
|
|
@@ -441,8 +451,91 @@ export interface PluginConfig {
|
|
|
441
451
|
* shows the tiles at full opacity.
|
|
442
452
|
*/
|
|
443
453
|
map_tile_alpha: number;
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Map plugins only. `true` (default): standard numeric axes in the
|
|
457
|
+
* usual cartesian gutters, with tick labels in degrees
|
|
458
|
+
* longitude/latitude (`122.4°W`) rather than Mercator meters.
|
|
459
|
+
* `false`: no axes, and the plot is full-bleed — the basemap fills
|
|
460
|
+
* the entire canvas, minus only the sidebar legend gutter when a
|
|
461
|
+
* legend is actually shown. Gridlines are never drawn in map mode
|
|
462
|
+
* — the gridline canvas composites BELOW the GL layer, so opaque
|
|
463
|
+
* basemap tiles would hide them.
|
|
464
|
+
*/
|
|
465
|
+
numeric_axes: boolean;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Legend presentation mode. `"auto"` (default) resolves per frame
|
|
469
|
+
* to `"floating"` when every entry fits the default floating panel
|
|
470
|
+
* without scrolling (≤ 7 entries; continuous gradient legends
|
|
471
|
+
* always qualify) and to `"sidebar"` otherwise — see
|
|
472
|
+
* `resolveLegendMode`. Treemap overrides the default to
|
|
473
|
+
* `"sidebar"` (a floating panel over edge-to-edge tiles always
|
|
474
|
+
* occludes data).
|
|
475
|
+
*/
|
|
476
|
+
legend_mode: "auto" | "sidebar" | "none" | "floating";
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Floating-panel sizing regime. `"auto"` (default) sizes the panel
|
|
480
|
+
* to its CONTENT every frame — the height hugs the entry rows
|
|
481
|
+
* exactly, and the width hugs the widest entry label (measured on
|
|
482
|
+
* the chrome canvas). `"fixed"` uses the saved `legend_width_px` /
|
|
483
|
+
* `legend_height_px` verbatim.
|
|
484
|
+
*
|
|
485
|
+
* Applies to `legend_mode: "floating"` ONLY; the sidebar gutter is
|
|
486
|
+
* always `legend_width_px` (its height is the plot's). Dragging a
|
|
487
|
+
* resize handle switches an auto panel to `"fixed"` — otherwise
|
|
488
|
+
* the gesture would be undone by the next paint — and
|
|
489
|
+
* double-clicking a resize handle switches it back.
|
|
490
|
+
*/
|
|
491
|
+
legend_size_mode: LegendSizeMode;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Legend width in CSS pixels. `0` (default) = automatic — each
|
|
495
|
+
* chart family keeps its historical gutter width (80–96px). In
|
|
496
|
+
* `"sidebar"` mode this is the full right-gutter width; in
|
|
497
|
+
* `"floating"` mode it is the panel width. Clamped at paint time
|
|
498
|
+
* to at most half the canvas width so a saved wide legend cannot
|
|
499
|
+
* crush a small panel. Ignored by a floating panel in
|
|
500
|
+
* `legend_size_mode: "auto"`.
|
|
501
|
+
*/
|
|
502
|
+
legend_width_px: number;
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Floating-legend panel height in CSS pixels. Ignored in
|
|
506
|
+
* `"sidebar"` mode (the legend spans the plot height) and by a
|
|
507
|
+
* floating panel in `legend_size_mode: "auto"`. Clamped at paint
|
|
508
|
+
* time to the canvas height.
|
|
509
|
+
*/
|
|
510
|
+
legend_height_px: number;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Canvas corner that `legend_x` / `legend_y` are measured FROM.
|
|
514
|
+
* Floating mode only. The panel keeps its distance to this corner
|
|
515
|
+
* across panel resizes — anchor `"bottom-right"` with small
|
|
516
|
+
* offsets stays glued to the bottom-right.
|
|
517
|
+
*/
|
|
518
|
+
legend_anchor: LegendAnchor;
|
|
519
|
+
|
|
520
|
+
legend_x: number;
|
|
521
|
+
legend_y: number;
|
|
522
|
+
legend_opacity: number;
|
|
523
|
+
|
|
524
|
+
/** Per-column width cap for tooltip grid cells, in CSS pixels. */
|
|
525
|
+
tooltip_max_column_px: number;
|
|
526
|
+
|
|
527
|
+
/** Tooltip background/border alpha (0..1). */
|
|
528
|
+
tooltip_opacity: number;
|
|
444
529
|
}
|
|
445
530
|
|
|
531
|
+
export type LegendAnchor =
|
|
532
|
+
| "top-left"
|
|
533
|
+
| "top-right"
|
|
534
|
+
| "bottom-left"
|
|
535
|
+
| "bottom-right";
|
|
536
|
+
|
|
537
|
+
export type LegendSizeMode = "auto" | "fixed";
|
|
538
|
+
|
|
446
539
|
export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
|
|
447
540
|
auto_alt_y_axis: false,
|
|
448
541
|
facet_mode: "grid",
|
|
@@ -460,6 +553,17 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
|
|
|
460
553
|
gradient_intensity: 0.6,
|
|
461
554
|
gradient_heat_max: 4.0,
|
|
462
555
|
gradient_color_mode: "mean",
|
|
463
|
-
map_tile_provider:
|
|
556
|
+
map_tile_provider: TILE_SOURCES.list()[0].id,
|
|
464
557
|
map_tile_alpha: 1.0,
|
|
558
|
+
numeric_axes: true,
|
|
559
|
+
legend_mode: "auto",
|
|
560
|
+
legend_size_mode: "auto",
|
|
561
|
+
legend_width_px: 0,
|
|
562
|
+
legend_height_px: 160,
|
|
563
|
+
legend_anchor: "top-right",
|
|
564
|
+
legend_x: 0,
|
|
565
|
+
legend_y: 0,
|
|
566
|
+
legend_opacity: 0.8,
|
|
567
|
+
tooltip_max_column_px: 160,
|
|
568
|
+
tooltip_opacity: 0.8,
|
|
465
569
|
};
|
|
@@ -203,15 +203,27 @@ export function synthesizeStringLevel(
|
|
|
203
203
|
const seen = new Map<string, number>();
|
|
204
204
|
seen.set("", 0);
|
|
205
205
|
|
|
206
|
+
const valueIdx = new Map<number, number>();
|
|
207
|
+
|
|
206
208
|
for (let r = 0; r < numRows; r++) {
|
|
207
209
|
const isValid = valid ? !!((valid[r >> 3] >> (r & 7)) & 1) : true;
|
|
210
|
+
if (!isValid) {
|
|
211
|
+
indices[r] = 0;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
208
215
|
const v = values[r] as number;
|
|
209
|
-
|
|
210
|
-
let dictIdx = seen.get(label);
|
|
216
|
+
let dictIdx = valueIdx.get(v);
|
|
211
217
|
if (dictIdx === undefined) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
const label = formatLevelValue(v, true, levelType);
|
|
219
|
+
dictIdx = seen.get(label);
|
|
220
|
+
if (dictIdx === undefined) {
|
|
221
|
+
dictIdx = dictionary.length;
|
|
222
|
+
dictionary.push(label);
|
|
223
|
+
seen.set(label, dictIdx);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
valueIdx.set(v, dictIdx);
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
indices[r] = dictIdx;
|
|
@@ -240,7 +252,34 @@ export function resolveCategoryAxis(
|
|
|
240
252
|
numRows: number,
|
|
241
253
|
groupByLen: number,
|
|
242
254
|
levelTypes: string[] = [],
|
|
255
|
+
axisMode?: AxisMode,
|
|
243
256
|
): CategoryAxisResult {
|
|
257
|
+
if (axisMode?.mode === "numeric") {
|
|
258
|
+
const rp = columns.get("__ROW_PATH_0__");
|
|
259
|
+
if (rp && rp.type !== "string" && rp.values) {
|
|
260
|
+
let rowOffset = 0;
|
|
261
|
+
const values = rp.values;
|
|
262
|
+
const valid = rp.valid;
|
|
263
|
+
while (rowOffset < numRows) {
|
|
264
|
+
const isValid = valid
|
|
265
|
+
? !!((valid[rowOffset >> 3] >> (rowOffset & 7)) & 1)
|
|
266
|
+
: true;
|
|
267
|
+
const v = values[rowOffset] as number;
|
|
268
|
+
if (isValid && v != null && !Number.isNaN(v)) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
rowOffset++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
rowPaths: [],
|
|
277
|
+
numCategories: Math.max(0, numRows - rowOffset),
|
|
278
|
+
rowOffset,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
244
283
|
type RawLevel = { indices: Int32Array; dictionary: string[] };
|
|
245
284
|
const rawRowPaths: RawLevel[] = [];
|
|
246
285
|
for (let n = 0; ; n++) {
|