Skip to content

Commit 5801d42

Browse files
committed
WIP
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent 49d0219 commit 5801d42

51 files changed

Lines changed: 1071 additions & 835 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/viewer-charts/src/css/perspective-viewer-charts.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,18 @@
140140
.zoom-reset:hover {
141141
opacity: 1;
142142
}
143+
144+
.webgl-tooltip {
145+
position: absolute;
146+
pointer-events: auto;
147+
font: 11px var(--psp-webgl--font-family);
148+
background: var(--psp-webgl--tooltip--background);
149+
color: var(--psp-webgl--tooltip--color);
150+
border: 1px solid var(--psp-webgl--tooltip--border-color);
151+
border-radius: 3px;
152+
padding: 3px;
153+
overflow-y: auto;
154+
white-space: pre;
155+
z-index: 10;
156+
line-height: 16px;
157+
}

packages/viewer-charts/src/ts/charts/bar/bar-interact.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import type { BarChart } from "./bar";
1414
import type { BarRecord } from "./bar-build";
15-
import { resolveTheme } from "../../theme/theme";
1615
import { formatTickValue } from "../../layout/ticks";
1716
import {
1817
renderBarFrame,
@@ -357,6 +356,7 @@ export function buildBarTooltipLines(chart: BarChart, b: BarRecord): string[] {
357356
lines.push(`Base: ${formatTickValue(b.y0)}`);
358357
lines.push(`Top: ${formatTickValue(b.y1)}`);
359358
}
359+
360360
return lines;
361361
}
362362

@@ -368,7 +368,7 @@ export function formatBarCategoryPath(chart: BarChart, catIdx: number): string {
368368
if (chart._rowPaths.length === 0) return "";
369369
const parts: string[] = [];
370370
for (const rp of chart._rowPaths) {
371-
const s = rp.dictionary[rp.indices[catIdx]];
371+
const s = rp.labels[catIdx];
372372
if (s != null && s !== "") parts.push(s);
373373
}
374374
return parts.join(" / ");
@@ -409,13 +409,9 @@ function pinTooltip(chart: BarChart, b: BarRecord): void {
409409
const lines = buildBarTooltipLines(chart, b);
410410
if (lines.length === 0) return;
411411

412-
const themeEl = chart._gridlineCanvas || chart._chromeCanvas;
413-
if (!themeEl) return;
414-
const theme = resolveTheme(themeEl);
415-
416412
const parent = chart._glCanvas?.parentElement;
417413
if (!parent) return;
418-
chart._tooltip.showPinned(parent, lines, pos, layout, theme);
414+
chart._tooltip.showPinned(parent, lines, pos, layout);
419415

420416
chart._hoveredBarIdx = -1;
421417
chart._hoveredSample = null;

packages/viewer-charts/src/ts/charts/candlestick/candlestick-interact.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import type { CandlestickChart } from "./candlestick";
1414
import type { CandleRecord } from "./candlestick-build";
15-
import { resolveTheme } from "../../theme/theme";
1615
import { formatTickValue } from "../../layout/ticks";
1716
import {
1817
renderCandlestickChromeOverlay,
@@ -86,10 +85,6 @@ export function showCandlestickPinnedTooltip(
8685
const candle = chart._candles[idx];
8786
if (!candle || !chart._lastLayout) return;
8887

89-
const themeEl = chart._gridlineCanvas || chart._chromeCanvas;
90-
if (!themeEl) return;
91-
const theme = resolveTheme(themeEl);
92-
9388
const lines = buildCandlestickTooltipLines(chart, candle);
9489
if (lines.length === 0) return;
9590

@@ -104,13 +99,7 @@ export function showCandlestickPinnedTooltip(
10499
const cssWidth = (chart._glCanvas?.width || 100) / dpr;
105100
const cssHeight = (chart._glCanvas?.height || 100) / dpr;
106101

107-
chart._tooltip.showPinned(
108-
parent,
109-
lines,
110-
pos,
111-
{ cssWidth, cssHeight },
112-
theme,
113-
);
102+
chart._tooltip.showPinned(parent, lines, pos, { cssWidth, cssHeight });
114103

115104
chart._hoveredIdx = -1;
116105
if (chart._glManager) renderCandlestickFrame(chart, chart._glManager);
@@ -131,7 +120,7 @@ export function buildCandlestickTooltipLines(
131120
if (chart._rowPaths.length > 0) {
132121
const parts: string[] = [];
133122
for (const rp of chart._rowPaths) {
134-
const s = rp.dictionary[rp.indices[candle.catIdx]] ?? "";
123+
const s = rp.labels[candle.catIdx] ?? "";
135124
if (s) parts.push(s);
136125
}
137126
if (parts.length > 0) lines.push(parts.join(" › "));

packages/viewer-charts/src/ts/charts/common/category-axis.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212

1313
import type { ColumnDataMap } from "../../data/view-reader";
1414
import type { CategoricalLevel } from "../../chrome/categorical-axis";
15+
import { buildGroupRuns } from "../../chrome/categorical-axis-core";
1516

1617
export interface CategoryAxisResult {
1718
/**
18-
* Zero-copy views over the `__ROW_PATH_N__` dictionaries, sliced to
19-
* skip leading empty rows (the "Total" aggregate header that the
20-
* view produces when `group_by` is non-empty). Empty when `groupBy`
21-
* is empty.
19+
* Fully materialized hierarchical levels — labels and group runs are
20+
* pre-resolved from the view's `__ROW_PATH_N__` dictionaries so the
21+
* chart can retain them past the `with_typed_arrays` callback scope.
22+
* Empty when `groupBy` is empty.
2223
*/
2324
rowPaths: CategoricalLevel[];
2425
/** Rows that actually contribute a category (post-offset). */
@@ -30,8 +31,9 @@ export interface CategoryAxisResult {
3031
/**
3132
* Resolve the category axis for a categorical-X chart (bar, candlestick,
3233
* ohlc, …). Walks the `__ROW_PATH_N__` hierarchy columns, skips the
33-
* rollup rows at the top ("Total" parent aggregates), and returns zero-
34-
* copy dictionary views plus the trimmed category count.
34+
* rollup rows at the top ("Total" parent aggregates), and returns fully
35+
* JS-owned level structures (precomputed labels + runs) plus the
36+
* trimmed category count.
3537
*
3638
* When `groupByLen === 0`, there are no row-path columns and the
3739
* category axis falls back to the raw row index — callers infer that
@@ -42,7 +44,8 @@ export function resolveCategoryAxis(
4244
numRows: number,
4345
groupByLen: number,
4446
): CategoryAxisResult {
45-
const rawRowPaths: CategoricalLevel[] = [];
47+
type RawLevel = { indices: Int32Array; dictionary: string[] };
48+
const rawRowPaths: RawLevel[] = [];
4649
for (let n = 0; ; n++) {
4750
const rp = columns.get(`__ROW_PATH_${n}__`);
4851
if (!rp || rp.type !== "string" || !rp.indices || !rp.dictionary) break;
@@ -66,15 +69,34 @@ export function resolveCategoryAxis(
6669
}
6770
const numCategories = Math.max(0, numRows - rowOffset);
6871

72+
const L = rawRowPaths.length;
6973
const rowPaths: CategoricalLevel[] =
70-
groupByLen > 0 && rawRowPaths.length > 0
71-
? rawRowPaths.map((rp) => ({
72-
indices:
73-
rowOffset === 0
74-
? rp.indices
75-
: rp.indices.subarray(rowOffset),
76-
dictionary: rp.dictionary,
77-
}))
74+
groupByLen > 0 && L > 0
75+
? rawRowPaths.map((rp, levelIdx) => {
76+
const labels = new Array<string>(numCategories);
77+
let maxLabelChars = 0;
78+
for (let r = 0; r < numCategories; r++) {
79+
const s = rp.dictionary[rp.indices[r + rowOffset]] ?? "";
80+
labels[r] = s;
81+
if (s.length > maxLabelChars) maxLabelChars = s.length;
82+
}
83+
// Only outer levels need the run-length encoding for
84+
// bracket rendering; leaves render per-row.
85+
const runs =
86+
levelIdx === L - 1
87+
? []
88+
: buildGroupRuns(
89+
rp.indices,
90+
rp.dictionary,
91+
rowOffset,
92+
rowOffset + numCategories,
93+
).map((run) => ({
94+
startIdx: run.startIdx - rowOffset,
95+
endIdx: run.endIdx - rowOffset,
96+
label: run.label,
97+
}));
98+
return { labels, runs, maxLabelChars };
99+
})
78100
: [];
79101

80102
return { rowPaths, numCategories, rowOffset };

packages/viewer-charts/src/ts/charts/common/node-store.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,29 @@ export class NodeStore {
8989
}
9090

9191
reset(): void {
92+
// Zero the layout typed arrays for slots that were occupied
93+
// before. `squarify` / `partitionSunburst` overwrite visited
94+
// nodes before any read, but early-bail branches (node area
95+
// below `MIN_VISIBLE_AREA`) skip the recursion and leave
96+
// descendants untouched. Re-using those slots in the next
97+
// render with stale `x0/y0/x1/y1/a0/a1/r0/r1` leaks the
98+
// previous render's rectangles / arcs through `collectVisible`
99+
// into the new scene — the "leftover hoverable cells" bug.
100+
//
101+
// Fill only up to the prior `count` (the tree's logical size)
102+
// — capacity can be much larger after growth and filling it
103+
// whole is O(capacity) unnecessary work.
104+
if (this.count > 0) {
105+
this.x0.fill(0, 0, this.count);
106+
this.y0.fill(0, 0, this.count);
107+
this.x1.fill(0, 0, this.count);
108+
this.y1.fill(0, 0, this.count);
109+
this.a0.fill(0, 0, this.count);
110+
this.a1.fill(0, 0, this.count);
111+
this.r0.fill(0, 0, this.count);
112+
this.r1.fill(0, 0, this.count);
113+
}
92114
this.count = 0;
93-
// Typed-array content doesn't need clearing — valid slots are
94-
// always written before read. `name` / `colorLabel` get stale
95-
// entries but `allocate()` clears them per-slot as reused.
96115
}
97116

98117
/**

packages/viewer-charts/src/ts/charts/common/tree-data.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ function insertRow(
122122
// render can dim negative leaves (matches the area
123123
// chart's `theme.areaOpacity`).
124124
chart._nodeStore.size[childId] = Math.abs(sizeValue);
125-
chart._nodeStore.sizeSign[childId] =
126-
sizeValue < 0 ? -1 : 1;
125+
chart._nodeStore.sizeSign[childId] = sizeValue < 0 ? -1 : 1;
127126
chart._nodeStore.leafRowIdx[childId] = rowIdx;
128127
}
129128
if (!isNaN(colorValue)) {

packages/viewer-charts/src/ts/charts/continuous/continuous-interact.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212

1313
import type { ContinuousChart } from "./continuous-chart";
14-
import { resolveTheme } from "../../theme/theme";
1514
import { renderContinuousChromeOverlay } from "./continuous-render";
1615

1716
const TOOLTIP_RADIUS_PX = 24;
@@ -145,7 +144,10 @@ function resolveHoverTarget(
145144
chart: ContinuousChart,
146145
mx: number,
147146
my: number,
148-
): { layout: import("../../layout/plot-layout").PlotLayout | null; facetIdx: number } {
147+
): {
148+
layout: import("../../layout/plot-layout").PlotLayout | null;
149+
facetIdx: number;
150+
} {
149151
if (chart._facetGrid) {
150152
const cells = chart._facetGrid.cells;
151153
for (let i = 0; i < cells.length; i++) {
@@ -265,9 +267,6 @@ export function showContinuousPinnedTooltip(
265267
chart._xData[pointIdx],
266268
chart._yData[pointIdx],
267269
);
268-
const themeEl = chart._gridlineCanvas || chart._chromeCanvas;
269-
if (!themeEl) return;
270-
const theme = resolveTheme(themeEl);
271270

272271
const parent = chart._glCanvas?.parentElement;
273272
if (!parent) return;
@@ -279,7 +278,7 @@ export function showContinuousPinnedTooltip(
279278
if (serial !== chart._pinnedTooltipSerial) return;
280279
if (chart._pinnedIndex !== pointIdx) return;
281280
if (lines.length === 0) return;
282-
chart._tooltip.showPinned(parent, lines, pos, layout, theme);
281+
chart._tooltip.showPinned(parent, lines, pos, layout);
283282
});
284283

285284
chart._hoveredIndex = -1;

packages/viewer-charts/src/ts/charts/continuous/continuous-render.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,7 @@ function renderFacetedChromeOverlay(chart: ContinuousChart): void {
618618
// which `buildFacetGrid` populates when `hasLegend` was set.
619619
if (chart._lastHasColorCol && grid.legendRect) {
620620
const stops = chart._lastGradientStops ?? theme.gradientStops;
621-
if (
622-
chart._colorIsString &&
623-
chart._uniqueColorLabels.size > 0
624-
) {
621+
if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
625622
const seriesPalette =
626623
chart._lastSeriesPalette ?? readSeriesPalette(canvas);
627624
const palette = resolvePalette(

packages/viewer-charts/src/ts/charts/continuous/glyphs/points.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ export class PointGlyph implements Glyph {
147147
// the tooltip shows only relevant facet values.
148148
const prefixFilter =
149149
chart._splitGroups.length > 0 && chart._seriesCapacity > 0
150-
? (chart._splitGroups[Math.floor(flatIdx / chart._seriesCapacity)]
151-
?.prefix ?? null)
150+
? (chart._splitGroups[
151+
Math.floor(flatIdx / chart._seriesCapacity)
152+
]?.prefix ?? null)
152153
: null;
153154

154155
for (const [colName, value] of row) {

packages/viewer-charts/src/ts/charts/heatmap/heatmap-build.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import type { ColumnDataMap } from "../../data/view-reader";
1414
import type { CategoricalLevel } from "../../chrome/categorical-axis";
15+
import { buildGroupRuns } from "../../chrome/categorical-axis-core";
1516

1617
export interface HeatmapCell {
1718
xIdx: number;
@@ -70,7 +71,8 @@ export function buildHeatmapPipeline(
7071
};
7172

7273
// Resolve group_by row-paths + grand-total offset (same as bar pipeline).
73-
const rawRowPaths: CategoricalLevel[] = [];
74+
type RawLevel = { indices: Int32Array; dictionary: string[] };
75+
const rawRowPaths: RawLevel[] = [];
7476
for (let n = 0; ; n++) {
7577
const rp = columns.get(`__ROW_PATH_${n}__`);
7678
if (!rp || rp.type !== "string" || !rp.indices || !rp.dictionary) break;
@@ -94,15 +96,32 @@ export function buildHeatmapPipeline(
9496
}
9597
const numX = Math.max(0, numRows - rowOffset);
9698

99+
const L = rawRowPaths.length;
97100
const xLevels: CategoricalLevel[] =
98-
groupBy.length > 0 && rawRowPaths.length > 0
99-
? rawRowPaths.map((rp) => ({
100-
indices:
101-
rowOffset === 0
102-
? rp.indices
103-
: rp.indices.subarray(rowOffset),
104-
dictionary: rp.dictionary,
105-
}))
101+
groupBy.length > 0 && L > 0
102+
? rawRowPaths.map((rp, levelIdx) => {
103+
const labels = new Array<string>(numX);
104+
let maxLabelChars = 0;
105+
for (let r = 0; r < numX; r++) {
106+
const s = rp.dictionary[rp.indices[r + rowOffset]] ?? "";
107+
labels[r] = s;
108+
if (s.length > maxLabelChars) maxLabelChars = s.length;
109+
}
110+
const runs =
111+
levelIdx === L - 1
112+
? []
113+
: buildGroupRuns(
114+
rp.indices,
115+
rp.dictionary,
116+
rowOffset,
117+
rowOffset + numX,
118+
).map((run) => ({
119+
startIdx: run.startIdx - rowOffset,
120+
endIdx: run.endIdx - rowOffset,
121+
label: run.label,
122+
}));
123+
return { labels, runs, maxLabelChars };
124+
})
106125
: [];
107126

108127
// Enumerate Y columns in arrow iteration order, skipping metadata.
@@ -224,6 +243,8 @@ function buildYLevelsFromNames(names: string[]): CategoricalLevel[] {
224243
const dictionary: string[] = [];
225244
const dictIndex = new Map<string, number>();
226245
const indices = new Int32Array(names.length);
246+
const labels = new Array<string>(names.length);
247+
let maxLabelChars = 0;
227248
for (let i = 0; i < names.length; i++) {
228249
const seg = segments[i][d] ?? "";
229250
let idx = dictIndex.get(seg);
@@ -233,8 +254,14 @@ function buildYLevelsFromNames(names: string[]): CategoricalLevel[] {
233254
dictIndex.set(seg, idx);
234255
}
235256
indices[i] = idx;
257+
labels[i] = seg;
258+
if (seg.length > maxLabelChars) maxLabelChars = seg.length;
236259
}
237-
levels.push({ indices, dictionary });
260+
const isLeaf = d === maxDepth - 1;
261+
const runs = isLeaf
262+
? []
263+
: buildGroupRuns(indices, dictionary, 0, names.length);
264+
levels.push({ labels, runs, maxLabelChars });
238265
}
239266
return levels;
240267
}

0 commit comments

Comments
 (0)