@perspective-dev/viewer-charts 4.5.0 → 4.5.2
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/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/axis/bar-axis.d.ts +9 -1
- package/dist/esm/axis/categorical-axis.d.ts +0 -2
- package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
- package/dist/esm/charts/common/expand-domain.d.ts +20 -0
- package/dist/esm/charts/common/tree-chart.d.ts +7 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
- package/dist/esm/charts/common/tree-interact.d.ts +46 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
- package/dist/esm/charts/series/series-build.d.ts +38 -2
- package/dist/esm/charts/series/series-render.d.ts +1 -4
- package/dist/esm/charts/series/series-type.d.ts +19 -17
- package/dist/esm/charts/series/series.d.ts +16 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
- package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +10 -28
- package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
- package/dist/esm/interaction/zoom-controller.d.ts +31 -20
- package/dist/esm/interaction/zoom-router.d.ts +3 -26
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/plugin.d.ts +15 -3
- package/dist/esm/render/scheduler.d.ts +23 -0
- package/dist/esm/theme/palette.d.ts +0 -5
- package/dist/esm/transport/protocol.d.ts +2 -7
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/dist/esm/worker/renderer.worker.d.ts +2 -4
- package/package.json +45 -45
- package/src/ts/axis/bar-axis.ts +74 -45
- package/src/ts/axis/categorical-axis.ts +0 -2
- package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
- package/src/ts/charts/candlestick/candlestick.ts +10 -29
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
- package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
- package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
- package/src/ts/charts/cartesian/cartesian.ts +43 -4
- package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
- package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
- package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
- package/src/ts/charts/chart-base.ts +20 -6
- package/src/ts/charts/chart.ts +1 -1
- package/src/ts/charts/common/category-axis-resolver.ts +135 -1
- package/src/ts/charts/common/expand-domain.ts +40 -0
- package/src/ts/charts/common/tree-chart.ts +16 -0
- package/src/ts/charts/common/tree-chrome.ts +86 -1
- package/src/ts/charts/common/tree-interact.ts +209 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
- package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
- package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
- package/src/ts/charts/series/series-build.ts +394 -21
- package/src/ts/charts/series/series-render.ts +159 -38
- package/src/ts/charts/series/series-type.ts +37 -17
- package/src/ts/charts/series/series.ts +63 -68
- package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
- package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
- package/src/ts/charts/sunburst/sunburst.ts +1 -15
- package/src/ts/charts/treemap/treemap-interact.ts +22 -189
- package/src/ts/charts/treemap/treemap-render.ts +19 -46
- package/src/ts/charts/treemap/treemap.ts +1 -16
- package/src/ts/config.ts +21 -1
- package/src/ts/interaction/host-sink-message.ts +33 -22
- package/src/ts/interaction/raw-event-forwarder.ts +10 -12
- package/src/ts/interaction/zoom-controller.ts +120 -83
- package/src/ts/interaction/zoom-router.ts +3 -126
- package/src/ts/map/tile-layer.ts +13 -13
- package/src/ts/plugin/plugin.ts +176 -196
- package/src/ts/render/scheduler.ts +125 -42
- package/src/ts/shaders/line-uniform.frag.glsl +2 -1
- package/src/ts/shaders/line-uniform.vert.glsl +19 -0
- package/src/ts/theme/palette.ts +1 -4
- package/src/ts/transport/protocol.ts +3 -8
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/worker/dispatch.ts +0 -1
- package/src/ts/worker/renderer.worker.ts +72 -65
- package/src/ts/worker/session-host.ts +29 -0
package/src/ts/axis/bar-axis.ts
CHANGED
|
@@ -142,6 +142,10 @@ export type BarCategoryAxis =
|
|
|
142
142
|
| { mode: "category"; domain: CategoricalDomain }
|
|
143
143
|
| { mode: "numeric"; domain: AxisDomain; ticks: number[] };
|
|
144
144
|
|
|
145
|
+
export type BarValueAxis =
|
|
146
|
+
| { mode: "category"; domain: CategoricalDomain }
|
|
147
|
+
| { mode: "numeric"; domain: AxisDomain; ticks: number[] };
|
|
148
|
+
|
|
145
149
|
/**
|
|
146
150
|
* Render a numeric date-aware axis along the bottom of the plot. Aliases
|
|
147
151
|
* the bar-axis bottom variant so heatmap can share the implementation.
|
|
@@ -190,13 +194,11 @@ export interface BarAxesFormatters {
|
|
|
190
194
|
export function renderBarAxesChrome(
|
|
191
195
|
canvas: Canvas2D,
|
|
192
196
|
catAxis: BarCategoryAxis,
|
|
193
|
-
|
|
194
|
-
valueTicks: number[],
|
|
197
|
+
valueAxis: BarValueAxis,
|
|
195
198
|
layout: PlotLayout,
|
|
196
199
|
theme: Theme,
|
|
197
200
|
dpr: number,
|
|
198
|
-
|
|
199
|
-
altTicks?: number[],
|
|
201
|
+
altAxis: BarValueAxis | undefined,
|
|
200
202
|
isHorizontal = false,
|
|
201
203
|
formatters: BarAxesFormatters = {},
|
|
202
204
|
): void {
|
|
@@ -212,7 +214,7 @@ export function renderBarAxesChrome(
|
|
|
212
214
|
ctx.moveTo(plot.x, plot.y);
|
|
213
215
|
ctx.lineTo(plot.x, plot.y + plot.height);
|
|
214
216
|
ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
|
|
215
|
-
if (
|
|
217
|
+
if (altAxis) {
|
|
216
218
|
if (isHorizontal) {
|
|
217
219
|
ctx.moveTo(plot.x, plot.y);
|
|
218
220
|
ctx.lineTo(plot.x + plot.width, plot.y);
|
|
@@ -238,31 +240,49 @@ export function renderBarAxesChrome(
|
|
|
238
240
|
);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
layout
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
theme,
|
|
248
|
-
formatters.value,
|
|
249
|
-
);
|
|
250
|
-
if (altDomain && altTicks) {
|
|
251
|
-
const origMin = layout.paddedXMin;
|
|
252
|
-
const origMax = layout.paddedXMax;
|
|
253
|
-
layout.paddedXMin = altDomain.min;
|
|
254
|
-
layout.paddedXMax = altDomain.max;
|
|
243
|
+
if (valueAxis.mode === "category") {
|
|
244
|
+
// Categorical value axis on the bottom: reuse the X
|
|
245
|
+
// categorical painter. Slot indices on the layout's X
|
|
246
|
+
// domain already place each category at its slot pixel.
|
|
247
|
+
renderCategoricalXTicks(ctx, layout, valueAxis.domain, theme);
|
|
248
|
+
} else {
|
|
255
249
|
drawNumericXAxis(
|
|
256
250
|
ctx,
|
|
257
251
|
layout,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"
|
|
252
|
+
valueAxis.domain,
|
|
253
|
+
valueAxis.ticks,
|
|
254
|
+
"bottom",
|
|
261
255
|
theme,
|
|
262
|
-
formatters.
|
|
256
|
+
formatters.value,
|
|
263
257
|
);
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (altAxis) {
|
|
261
|
+
// Alt-axis painter expects the layout's X domain to match
|
|
262
|
+
// the alt domain — temporarily swap in `altDomain.min/max`
|
|
263
|
+
// for the duration of the call. Categorical alt has no
|
|
264
|
+
// top-side painter; render with the bottom-side painter
|
|
265
|
+
// (visual overlap with the primary side — documented
|
|
266
|
+
// limitation; user-pinned categorical alt is rare).
|
|
267
|
+
if (altAxis.mode === "category") {
|
|
268
|
+
renderCategoricalXTicks(ctx, layout, altAxis.domain, theme);
|
|
269
|
+
} else {
|
|
270
|
+
const origMin = layout.paddedXMin;
|
|
271
|
+
const origMax = layout.paddedXMax;
|
|
272
|
+
layout.paddedXMin = altAxis.domain.min;
|
|
273
|
+
layout.paddedXMax = altAxis.domain.max;
|
|
274
|
+
drawNumericXAxis(
|
|
275
|
+
ctx,
|
|
276
|
+
layout,
|
|
277
|
+
altAxis.domain,
|
|
278
|
+
altAxis.ticks,
|
|
279
|
+
"top",
|
|
280
|
+
theme,
|
|
281
|
+
formatters.alt,
|
|
282
|
+
);
|
|
283
|
+
layout.paddedXMin = origMin;
|
|
284
|
+
layout.paddedXMax = origMax;
|
|
285
|
+
}
|
|
266
286
|
}
|
|
267
287
|
} else {
|
|
268
288
|
if (catAxis.mode === "category") {
|
|
@@ -278,31 +298,40 @@ export function renderBarAxesChrome(
|
|
|
278
298
|
);
|
|
279
299
|
}
|
|
280
300
|
|
|
281
|
-
|
|
282
|
-
ctx,
|
|
283
|
-
|
|
284
|
-
valueDomain,
|
|
285
|
-
valueTicks,
|
|
286
|
-
"left",
|
|
287
|
-
theme,
|
|
288
|
-
formatters.value,
|
|
289
|
-
);
|
|
290
|
-
if (altDomain && altTicks) {
|
|
291
|
-
const origMin = layout.paddedYMin;
|
|
292
|
-
const origMax = layout.paddedYMax;
|
|
293
|
-
layout.paddedYMin = altDomain.min;
|
|
294
|
-
layout.paddedYMax = altDomain.max;
|
|
301
|
+
if (valueAxis.mode === "category") {
|
|
302
|
+
renderCategoricalYTicks(ctx, layout, valueAxis.domain, theme);
|
|
303
|
+
} else {
|
|
295
304
|
drawYAxis(
|
|
296
305
|
ctx,
|
|
297
306
|
layout,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"
|
|
307
|
+
valueAxis.domain,
|
|
308
|
+
valueAxis.ticks,
|
|
309
|
+
"left",
|
|
301
310
|
theme,
|
|
302
|
-
formatters.
|
|
311
|
+
formatters.value,
|
|
303
312
|
);
|
|
304
|
-
|
|
305
|
-
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (altAxis) {
|
|
316
|
+
if (altAxis.mode === "category") {
|
|
317
|
+
renderCategoricalYTicks(ctx, layout, altAxis.domain, theme);
|
|
318
|
+
} else {
|
|
319
|
+
const origMin = layout.paddedYMin;
|
|
320
|
+
const origMax = layout.paddedYMax;
|
|
321
|
+
layout.paddedYMin = altAxis.domain.min;
|
|
322
|
+
layout.paddedYMax = altAxis.domain.max;
|
|
323
|
+
drawYAxis(
|
|
324
|
+
ctx,
|
|
325
|
+
layout,
|
|
326
|
+
altAxis.domain,
|
|
327
|
+
altAxis.ticks,
|
|
328
|
+
"right",
|
|
329
|
+
theme,
|
|
330
|
+
formatters.alt,
|
|
331
|
+
);
|
|
332
|
+
layout.paddedYMin = origMin;
|
|
333
|
+
layout.paddedYMax = origMax;
|
|
334
|
+
}
|
|
306
335
|
}
|
|
307
336
|
}
|
|
308
337
|
}
|
|
@@ -56,8 +56,6 @@ function categoryIndexToPixelY(layout: PlotLayout, index: number): number {
|
|
|
56
56
|
return layout.dataToPixel(0, index).py;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export const categoryIndexToPixel = categoryIndexToPixelX;
|
|
60
|
-
|
|
61
59
|
function leafLevelLayout(
|
|
62
60
|
numRows: number,
|
|
63
61
|
longestCharCount: number,
|
|
@@ -125,13 +125,14 @@ export function renderCandlestickFrame(
|
|
|
125
125
|
yMax: chart._yDomain.max,
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
// Auto-fit the price axis to the visible X window. Skipped
|
|
129
|
-
// default zoom (the refit equals `_yDomain` there and would
|
|
130
|
-
// churn baselines)
|
|
128
|
+
// Auto-fit the price axis to the visible X window. Skipped when X
|
|
129
|
+
// is at default zoom (the refit equals `_yDomain` there and would
|
|
130
|
+
// only churn baselines) — Y-axis pan/zoom alone shouldn't trigger
|
|
131
|
+
// an X-window refit.
|
|
131
132
|
if (
|
|
132
133
|
chart._autoFitValue &&
|
|
133
134
|
chart._zoomController &&
|
|
134
|
-
!chart._zoomController.
|
|
135
|
+
!chart._zoomController.isXDefault()
|
|
135
136
|
) {
|
|
136
137
|
const fit = computeVisibleCandleExtent(chart, vis.xMin, vis.xMax);
|
|
137
138
|
if (fit.hasFit) {
|
|
@@ -271,13 +272,15 @@ export function renderCandlestickChromeOverlay(chart: CandlestickChart): void {
|
|
|
271
272
|
renderBarAxesChrome(
|
|
272
273
|
chart._chromeCanvas,
|
|
273
274
|
catAxis,
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
{
|
|
276
|
+
mode: "numeric",
|
|
277
|
+
domain: chart._lastYDomain,
|
|
278
|
+
ticks: chart._lastYTicks,
|
|
279
|
+
},
|
|
276
280
|
chart._lastLayout,
|
|
277
281
|
theme,
|
|
278
282
|
chart._glManager?.dpr ?? 1,
|
|
279
283
|
undefined,
|
|
280
|
-
undefined,
|
|
281
284
|
false,
|
|
282
285
|
{
|
|
283
286
|
value: chart.getColumnFormatter(valueColumn, "tick"),
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./candlestick-interact";
|
|
34
34
|
import { BodyWickGlyph } from "./glyphs/draw-candlesticks";
|
|
35
35
|
import { OHLCGlyph } from "./glyphs/draw-ohlc";
|
|
36
|
+
import { expandDomainInPlace } from "../common/expand-domain";
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Per-frame memo of the auto-fit Y extent for a {@link CandlestickChart},
|
|
@@ -266,38 +267,18 @@ export class CandlestickChart extends CategoricalYChart {
|
|
|
266
267
|
scratchCandles: this._candles,
|
|
267
268
|
});
|
|
268
269
|
// `domain_mode: "expand"` post-build union — mirrors the series
|
|
269
|
-
// pipeline.
|
|
270
|
+
// pipeline. `expandDomainInPlace` mutates `result.*` so the
|
|
270
271
|
// assignments below pick up the grown extent automatically.
|
|
271
272
|
if (this._pluginConfig.domain_mode === "expand") {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
);
|
|
277
|
-
result.yDomain.max = Math.max(
|
|
278
|
-
this._expandedYDomain.max,
|
|
279
|
-
result.yDomain.max,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
this._expandedYDomain = { ...result.yDomain };
|
|
284
|
-
|
|
273
|
+
this._expandedYDomain = expandDomainInPlace(
|
|
274
|
+
this._expandedYDomain,
|
|
275
|
+
result.yDomain,
|
|
276
|
+
);
|
|
285
277
|
if (result.numericCategoryDomain) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
);
|
|
291
|
-
result.numericCategoryDomain.max = Math.max(
|
|
292
|
-
this._expandedCategoryDomain.max,
|
|
293
|
-
result.numericCategoryDomain.max,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
this._expandedCategoryDomain = {
|
|
298
|
-
min: result.numericCategoryDomain.min,
|
|
299
|
-
max: result.numericCategoryDomain.max,
|
|
300
|
-
};
|
|
278
|
+
this._expandedCategoryDomain = expandDomainInPlace(
|
|
279
|
+
this._expandedCategoryDomain,
|
|
280
|
+
result.numericCategoryDomain,
|
|
281
|
+
);
|
|
301
282
|
}
|
|
302
283
|
} else {
|
|
303
284
|
this._expandedYDomain = null;
|
|
@@ -46,9 +46,23 @@ interface WickCache {
|
|
|
46
46
|
u_color: WebGLUniformLocation | null;
|
|
47
47
|
u_resolution: WebGLUniformLocation | null;
|
|
48
48
|
u_line_width: WebGLUniformLocation | null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `line-uniform` was extended to support the Y-Line `interpolate`
|
|
52
|
+
* feature with a per-segment alpha multiplier driven by
|
|
53
|
+
* `a_real_start * a_real_end` and `u_interp_alpha`. Wicks are
|
|
54
|
+
* always "real" data — every segment renders fully — so we hold
|
|
55
|
+
* these locations to neutralize them at draw time (uniform = 1.0,
|
|
56
|
+
* constant attribute values = 1.0). Without that, an unset uniform
|
|
57
|
+
* defaults to 0 and the fragment alpha collapses to 0, rendering
|
|
58
|
+
* the wicks invisible.
|
|
59
|
+
*/
|
|
60
|
+
u_interp_alpha: WebGLUniformLocation | null;
|
|
49
61
|
a_corner: number;
|
|
50
62
|
a_start: number;
|
|
51
63
|
a_end: number;
|
|
64
|
+
a_real_start: number;
|
|
65
|
+
a_real_end: number;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
interface ProgramCache {
|
|
@@ -124,8 +138,14 @@ export class BodyWickGlyph {
|
|
|
124
138
|
"line-uniform",
|
|
125
139
|
lineVert,
|
|
126
140
|
lineFrag,
|
|
127
|
-
[
|
|
128
|
-
|
|
141
|
+
[
|
|
142
|
+
"u_projection",
|
|
143
|
+
"u_color",
|
|
144
|
+
"u_resolution",
|
|
145
|
+
"u_line_width",
|
|
146
|
+
"u_interp_alpha",
|
|
147
|
+
],
|
|
148
|
+
["a_corner", "a_start", "a_end", "a_real_start", "a_real_end"],
|
|
129
149
|
);
|
|
130
150
|
const wick: WickCache = {
|
|
131
151
|
...wickPartial,
|
|
@@ -375,6 +395,20 @@ function drawWicks(
|
|
|
375
395
|
gl.uniform2f(cache.u_resolution, gl.canvas.width, gl.canvas.height);
|
|
376
396
|
gl.uniform1f(cache.u_line_width, chart._pluginConfig.wick_width_px * dpr);
|
|
377
397
|
|
|
398
|
+
// `line-uniform` was extended for the Y-Line interpolate feature
|
|
399
|
+
// with a per-segment alpha multiplier; neutralize it here.
|
|
400
|
+
// Constant attribute values (used when the array is disabled) and
|
|
401
|
+
// uniform are stable for every draw, so set once after
|
|
402
|
+
// `useProgram`. Disabling the arrays first guards against a prior
|
|
403
|
+
// Y-Line draw that left them enabled at the same attribute index
|
|
404
|
+
// (locations are shared because both programs link from the same
|
|
405
|
+
// source).
|
|
406
|
+
gl.disableVertexAttribArray(cache.a_real_start);
|
|
407
|
+
gl.disableVertexAttribArray(cache.a_real_end);
|
|
408
|
+
gl.vertexAttrib1f(cache.a_real_start, 1.0);
|
|
409
|
+
gl.vertexAttrib1f(cache.a_real_end, 1.0);
|
|
410
|
+
gl.uniform1f(cache.u_interp_alpha, 1.0);
|
|
411
|
+
|
|
378
412
|
const instancing = getInstancing(glManager);
|
|
379
413
|
const { setDivisor } = instancing;
|
|
380
414
|
|
|
@@ -30,9 +30,23 @@ interface OHLCCache {
|
|
|
30
30
|
u_color: WebGLUniformLocation | null;
|
|
31
31
|
u_resolution: WebGLUniformLocation | null;
|
|
32
32
|
u_line_width: WebGLUniformLocation | null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `line-uniform` was extended to support the Y-Line `interpolate`
|
|
36
|
+
* feature with a per-segment alpha multiplier driven by
|
|
37
|
+
* `a_real_start * a_real_end` and `u_interp_alpha`. The OHLC glyph
|
|
38
|
+
* doesn't need that — every segment is "real" — so we hold these
|
|
39
|
+
* locations to neutralize them at draw time (uniform = 1.0,
|
|
40
|
+
* constant attribute values = 1.0). Without that, an unset uniform
|
|
41
|
+
* defaults to 0 and the fragment alpha collapses to 0, rendering
|
|
42
|
+
* the entire OHLC glyph invisible.
|
|
43
|
+
*/
|
|
44
|
+
u_interp_alpha: WebGLUniformLocation | null;
|
|
33
45
|
a_corner: number;
|
|
34
46
|
a_start: number;
|
|
35
47
|
a_end: number;
|
|
48
|
+
a_real_start: number;
|
|
49
|
+
a_real_end: number;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/**
|
|
@@ -76,8 +90,14 @@ export class OHLCGlyph {
|
|
|
76
90
|
"line-uniform",
|
|
77
91
|
lineVert,
|
|
78
92
|
lineFrag,
|
|
79
|
-
[
|
|
80
|
-
|
|
93
|
+
[
|
|
94
|
+
"u_projection",
|
|
95
|
+
"u_color",
|
|
96
|
+
"u_resolution",
|
|
97
|
+
"u_line_width",
|
|
98
|
+
"u_interp_alpha",
|
|
99
|
+
],
|
|
100
|
+
["a_corner", "a_start", "a_end", "a_real_start", "a_real_end"],
|
|
81
101
|
);
|
|
82
102
|
this._program = {
|
|
83
103
|
...partial,
|
|
@@ -229,6 +249,20 @@ export class OHLCGlyph {
|
|
|
229
249
|
chart._pluginConfig.ohlc_line_width_px * dpr,
|
|
230
250
|
);
|
|
231
251
|
|
|
252
|
+
// `line-uniform` was extended for the Y-Line interpolate
|
|
253
|
+
// feature with a per-segment alpha multiplier; neutralize it
|
|
254
|
+
// here. Constant attribute values (used when the array is
|
|
255
|
+
// disabled) and uniform are stable for every draw, so set
|
|
256
|
+
// once after `useProgram`. Disabling the arrays first guards
|
|
257
|
+
// against a prior Y-Line draw that left them enabled at the
|
|
258
|
+
// same attribute index (locations are shared because both
|
|
259
|
+
// programs link from the same source).
|
|
260
|
+
gl.disableVertexAttribArray(cache.a_real_start);
|
|
261
|
+
gl.disableVertexAttribArray(cache.a_real_end);
|
|
262
|
+
gl.vertexAttrib1f(cache.a_real_start, 1.0);
|
|
263
|
+
gl.vertexAttrib1f(cache.a_real_end, 1.0);
|
|
264
|
+
gl.uniform1f(cache.u_interp_alpha, 1.0);
|
|
265
|
+
|
|
232
266
|
const instancing = getInstancing(glManager);
|
|
233
267
|
const { setDivisor } = instancing;
|
|
234
268
|
|
|
@@ -16,6 +16,49 @@ import type { WebGLContextManager } from "../../webgl/context-manager";
|
|
|
16
16
|
import type { CartesianChart, SplitGroup } from "./cartesian";
|
|
17
17
|
import { LabelInterner } from "./label-interner";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a row's string value into a slot index in `dictionary`,
|
|
21
|
+
* inserting on first encounter. Invalid / missing values land in a
|
|
22
|
+
* lazily-added `"(null)"` slot — no reserved slot 0 when the data has
|
|
23
|
+
* no nulls. Shared by the X and Y categorical paths in
|
|
24
|
+
* `processCartesianChunk`.
|
|
25
|
+
*/
|
|
26
|
+
function lookupCategorySlot(
|
|
27
|
+
col: ColumnData | null,
|
|
28
|
+
rowIdx: number,
|
|
29
|
+
dictionary: string[],
|
|
30
|
+
seen: Map<string, number>,
|
|
31
|
+
): number {
|
|
32
|
+
let label: string;
|
|
33
|
+
if (!col) {
|
|
34
|
+
label = "(null)";
|
|
35
|
+
} else {
|
|
36
|
+
const valid = col.valid;
|
|
37
|
+
const isValid = valid
|
|
38
|
+
? !!((valid[rowIdx >> 3] >> (rowIdx & 7)) & 1)
|
|
39
|
+
: true;
|
|
40
|
+
if (!isValid) {
|
|
41
|
+
label = "(null)";
|
|
42
|
+
} else if (col.indices && col.dictionary) {
|
|
43
|
+
label = col.dictionary[col.indices[rowIdx]] ?? "(null)";
|
|
44
|
+
} else if (col.values) {
|
|
45
|
+
const v = col.values[rowIdx];
|
|
46
|
+
label = v == null ? "(null)" : String(v);
|
|
47
|
+
} else {
|
|
48
|
+
label = "(null)";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let slot = seen.get(label);
|
|
53
|
+
if (slot === undefined) {
|
|
54
|
+
slot = dictionary.length;
|
|
55
|
+
dictionary.push(label);
|
|
56
|
+
seen.set(label, slot);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return slot;
|
|
60
|
+
}
|
|
61
|
+
|
|
19
62
|
/**
|
|
20
63
|
* Resolve per-split-prefix column-name tuples. `colorBase`/`sizeBase`
|
|
21
64
|
* are optional (empty string when the corresponding slot is unset).
|
|
@@ -122,6 +165,41 @@ export function initCartesianPipeline(
|
|
|
122
165
|
chart._yLabel = yBase;
|
|
123
166
|
chart._xIsRowIndex = !xBase;
|
|
124
167
|
|
|
168
|
+
// Post-aggregation `string` columns on X / Y switch the axis to
|
|
169
|
+
// categorical: per-row slot indices are written into `_xData` /
|
|
170
|
+
// `_yData` instead of raw values, and the chrome overlay paints a
|
|
171
|
+
// categorical axis. Reset the per-frame dictionary state here at
|
|
172
|
+
// chunk 0 so slot 0 is always the first non-null label encountered
|
|
173
|
+
// in arrival order (matching the perspective view's sort).
|
|
174
|
+
chart._xIsString = !!xBase && chart._columnTypes[xBase] === "string";
|
|
175
|
+
chart._yIsString = !!yBase && chart._columnTypes[yBase] === "string";
|
|
176
|
+
chart._xCategoryDictionary = [];
|
|
177
|
+
chart._yCategoryDictionary = [];
|
|
178
|
+
chart._xCategorySeen = new Map();
|
|
179
|
+
chart._yCategorySeen = new Map();
|
|
180
|
+
chart._xCategoryDomain = null;
|
|
181
|
+
chart._yCategoryDomain = null;
|
|
182
|
+
|
|
183
|
+
// Categorical axes use 0-based slot indices, so the rebase origin
|
|
184
|
+
// is fixed at 0. Skipping the NaN-init guard below prevents the
|
|
185
|
+
// first-seen-slot from being adopted as the origin (which would
|
|
186
|
+
// shift every other slot's pixel position).
|
|
187
|
+
if (chart._xIsString) {
|
|
188
|
+
chart._xOrigin = 0;
|
|
189
|
+
chart._xMin = 0;
|
|
190
|
+
chart._xMax = 0;
|
|
191
|
+
chart._expandedXMin = Infinity;
|
|
192
|
+
chart._expandedXMax = -Infinity;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (chart._yIsString) {
|
|
196
|
+
chart._yOrigin = 0;
|
|
197
|
+
chart._yMin = 0;
|
|
198
|
+
chart._yMax = 0;
|
|
199
|
+
chart._expandedYMin = Infinity;
|
|
200
|
+
chart._expandedYMax = -Infinity;
|
|
201
|
+
}
|
|
202
|
+
|
|
125
203
|
// Capture the per-series row budget BEFORE any split expansion. When
|
|
126
204
|
// split_by is active we grow `totalCapacity` to fit `numSplits`
|
|
127
205
|
// parallel slot ranges; reading `totalCapacity` again after that
|
|
@@ -245,9 +323,16 @@ export function processCartesianChunk(
|
|
|
245
323
|
// carries the user's selected color column. The color-resolution
|
|
246
324
|
// logic in the inner loop reads uniformly from `ser.colorCol`
|
|
247
325
|
// across both modes.
|
|
326
|
+
//
|
|
327
|
+
// `xColData` / `yColData` carry the full `ColumnData` so the
|
|
328
|
+
// categorical path can read `indices` + `dictionary` for slot
|
|
329
|
+
// lookup; `xCol` / `yCol` keep the numeric fast path zero-cost
|
|
330
|
+
// (and stay `null` on string columns where `values` is unset).
|
|
248
331
|
type SeriesSrc = {
|
|
249
332
|
xCol: Float32Array | Float64Array | Int32Array | null;
|
|
250
|
-
yCol: Float32Array | Float64Array | Int32Array;
|
|
333
|
+
yCol: Float32Array | Float64Array | Int32Array | null;
|
|
334
|
+
xColData: ColumnData | null;
|
|
335
|
+
yColData: ColumnData | null;
|
|
251
336
|
xValid: Uint8Array | undefined;
|
|
252
337
|
yValid: Uint8Array | undefined;
|
|
253
338
|
colorCol: ColumnData | null;
|
|
@@ -260,7 +345,11 @@ export function processCartesianChunk(
|
|
|
260
345
|
for (const sg of chart._splitGroups) {
|
|
261
346
|
const xc = sg.xColName ? columns.get(sg.xColName) : null;
|
|
262
347
|
const yc = columns.get(sg.yColName);
|
|
263
|
-
if (!yc
|
|
348
|
+
if (!yc) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!chart._yIsString && !yc.values) {
|
|
264
353
|
continue;
|
|
265
354
|
}
|
|
266
355
|
|
|
@@ -273,7 +362,9 @@ export function processCartesianChunk(
|
|
|
273
362
|
: null;
|
|
274
363
|
series.push({
|
|
275
364
|
xCol: xc?.values ?? null,
|
|
276
|
-
yCol: yc.values,
|
|
365
|
+
yCol: yc.values ?? null,
|
|
366
|
+
xColData: xc ?? null,
|
|
367
|
+
yColData: yc,
|
|
277
368
|
xValid: xc?.valid,
|
|
278
369
|
yValid: yc.valid,
|
|
279
370
|
colorCol: cc,
|
|
@@ -284,7 +375,11 @@ export function processCartesianChunk(
|
|
|
284
375
|
} else {
|
|
285
376
|
const xc = chart._xName ? columns.get(chart._xName) : null;
|
|
286
377
|
const yc = chart._yName ? columns.get(chart._yName) : null;
|
|
287
|
-
if (!yc
|
|
378
|
+
if (!yc) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!chart._yIsString && !yc.values) {
|
|
288
383
|
return;
|
|
289
384
|
}
|
|
290
385
|
|
|
@@ -296,7 +391,9 @@ export function processCartesianChunk(
|
|
|
296
391
|
: null;
|
|
297
392
|
series.push({
|
|
298
393
|
xCol: xc?.values ?? null,
|
|
299
|
-
yCol: yc.values,
|
|
394
|
+
yCol: yc.values ?? null,
|
|
395
|
+
xColData: xc ?? null,
|
|
396
|
+
yColData: yc,
|
|
300
397
|
xValid: xc?.valid,
|
|
301
398
|
yValid: yc?.valid,
|
|
302
399
|
colorCol: cc,
|
|
@@ -386,11 +483,20 @@ export function processCartesianChunk(
|
|
|
386
483
|
let writeIdx = 0;
|
|
387
484
|
for (let j = 0; j < sourceLength && writeIdx < maxWrite; j++) {
|
|
388
485
|
const i = j;
|
|
389
|
-
|
|
486
|
+
// Numeric axes filter out null/invalid rows entirely;
|
|
487
|
+
// categorical axes route them into a `"(null)"` slot
|
|
488
|
+
// instead, so the validity / NaN guards only apply on
|
|
489
|
+
// the numeric branch.
|
|
490
|
+
if (
|
|
491
|
+
!chart._yIsString &&
|
|
492
|
+
ser.yValid &&
|
|
493
|
+
!((ser.yValid[i >> 3] >> (i & 7)) & 1)
|
|
494
|
+
) {
|
|
390
495
|
continue;
|
|
391
496
|
}
|
|
392
497
|
|
|
393
498
|
if (
|
|
499
|
+
!chart._xIsString &&
|
|
394
500
|
ser.xCol &&
|
|
395
501
|
ser.xValid &&
|
|
396
502
|
!((ser.xValid[i >> 3] >> (i & 7)) & 1)
|
|
@@ -402,12 +508,40 @@ export function processCartesianChunk(
|
|
|
402
508
|
colorValid !== undefined &&
|
|
403
509
|
!((colorValid[i >> 3] >> (i & 7)) & 1);
|
|
404
510
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
511
|
+
let rawY: number;
|
|
512
|
+
if (chart._yIsString) {
|
|
513
|
+
rawY = lookupCategorySlot(
|
|
514
|
+
ser.yColData,
|
|
515
|
+
i,
|
|
516
|
+
chart._yCategoryDictionary,
|
|
517
|
+
chart._yCategorySeen,
|
|
518
|
+
);
|
|
519
|
+
} else if (ser.yCol) {
|
|
520
|
+
rawY = ser.yCol[i] as number;
|
|
521
|
+
if (isNaN(rawY)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
408
525
|
continue;
|
|
409
526
|
}
|
|
410
527
|
|
|
528
|
+
let rawX: number;
|
|
529
|
+
if (chart._xIsString) {
|
|
530
|
+
rawX = lookupCategorySlot(
|
|
531
|
+
ser.xColData,
|
|
532
|
+
i,
|
|
533
|
+
chart._xCategoryDictionary,
|
|
534
|
+
chart._xCategorySeen,
|
|
535
|
+
);
|
|
536
|
+
} else if (ser.xCol) {
|
|
537
|
+
rawX = ser.xCol[i] as number;
|
|
538
|
+
if (isNaN(rawX)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
rawX = startRow + i;
|
|
543
|
+
}
|
|
544
|
+
|
|
411
545
|
// Project raw (x, y) → data-space (x, y). Default is
|
|
412
546
|
// identity for cartesian charts; map subclasses override
|
|
413
547
|
// to apply Mercator. Second NaN guard catches projection
|