@perspective-dev/viewer-charts 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,15 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
25
25
  * resolved values in the `InitMsg`.
26
26
  */
27
27
  private _pluginConfigStore;
28
+ /**
29
+ * Per-column config (`alt_axis`, `chart_type`, formats, …), held on
30
+ * the element for the same reason as `_pluginConfigStore`: the host
31
+ * calls `restore()` BEFORE the first draw builds the renderer, so
32
+ * forwarding only to a live renderer silently drops the initial
33
+ * `columns_config` — `_buildRenderer` ships it in the `InitMsg`
34
+ * instead.
35
+ */
36
+ private _columnsConfig;
28
37
  private get _pluginConfig();
29
38
  private set _pluginConfig(value);
30
39
  private _effectiveDefaults;
@@ -104,6 +104,14 @@ export interface InitMsg {
104
104
  * a `setPluginConfig` control msg.
105
105
  */
106
106
  pluginConfig: PluginConfig;
107
+ /**
108
+ * Initial per-column config (`alt_axis`, `chart_type`, formats, …).
109
+ * Seeds the chart impl before the first `loadAndRender` — the host
110
+ * calls `plugin.restore` before the renderer exists, so without
111
+ * this the initial `columns_config` never reaches the worker.
112
+ * Later changes arrive as `setColumnsConfig` control msgs.
113
+ */
114
+ columnsConfig?: Record<string, any>;
107
115
  defaultChartType?: string;
108
116
  /**
109
117
  * Pre-resolved CSS-variable theme snapshot from the host.
@@ -133,6 +133,7 @@ export declare class RendererTransport {
133
133
  chrome: HTMLCanvasElement;
134
134
  facetConfig: FacetConfig;
135
135
  pluginConfig: PluginConfig;
136
+ columnsConfig?: Record<string, any>;
136
137
  defaultChartType?: string;
137
138
  renderBlitMode: "blit" | "direct";
138
139
  }): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perspective-dev/viewer-charts",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Perspective.js WebGL Plugin",
5
5
  "unpkg": "./dist/cdn/perspective-viewer-charts.js",
6
6
  "jsdelivr": "./dist/cdn/perspective-viewer-charts.js",
@@ -188,7 +188,9 @@ export class AreaGlyph {
188
188
  /**
189
189
  * Bind persistent strip buffers and dispatch one TRIANGLE_STRIP per
190
190
  * series-run. Skips hidden series. `splitFilter` (faceted frames)
191
- * draws only the series whose `splitIdx` matches.
191
+ * draws only the series whose `splitIdx` matches; `aggRange` (mixed
192
+ * glyph-run frames) only those whose `aggIdx` lies in the inclusive
193
+ * run span.
192
194
  */
193
195
  draw(
194
196
  chart: SeriesChart,
@@ -198,6 +200,7 @@ export class AreaGlyph {
198
200
  projRight: Float32Array,
199
201
  opacity: number,
200
202
  splitFilter?: number,
203
+ aggRange?: { start: number; end: number },
201
204
  ): void {
202
205
  const buf = this._buffers;
203
206
  const cache = this._program;
@@ -221,6 +224,14 @@ export class AreaGlyph {
221
224
  continue;
222
225
  }
223
226
 
227
+ const aggIdx = chart._series[s.seriesId].aggIdx;
228
+ if (
229
+ aggRange !== undefined &&
230
+ (aggIdx < aggRange.start || aggIdx > aggRange.end)
231
+ ) {
232
+ continue;
233
+ }
234
+
224
235
  gl.uniformMatrix4fv(
225
236
  cache.u_projection,
226
237
  false,
@@ -272,7 +272,9 @@ export class LineGlyph {
272
272
  * per series. Skips hidden series via `_hiddenSeries`. Gap /
273
273
  * transparency rendering is governed by `u_interp_alpha`, set per
274
274
  * series. `splitFilter` (faceted frames) draws only the series
275
- * whose `splitIdx` matches — one call per facet.
275
+ * whose `splitIdx` matches — one call per facet. `aggRange` (mixed
276
+ * glyph-run frames) draws only the series whose `aggIdx` lies in
277
+ * the inclusive run span.
276
278
  */
277
279
  draw(
278
280
  chart: SeriesChart,
@@ -281,6 +283,7 @@ export class LineGlyph {
281
283
  projLeft: Float32Array,
282
284
  projRight: Float32Array,
283
285
  splitFilter?: number,
286
+ aggRange?: { start: number; end: number },
284
287
  ): void {
285
288
  const buf = this._buffers;
286
289
  const cache = this._program;
@@ -319,6 +322,14 @@ export class LineGlyph {
319
322
  continue;
320
323
  }
321
324
 
325
+ const aggIdx = chart._series[s.seriesId].aggIdx;
326
+ if (
327
+ aggRange !== undefined &&
328
+ (aggIdx < aggRange.start || aggIdx > aggRange.end)
329
+ ) {
330
+ continue;
331
+ }
332
+
322
333
  gl.uniformMatrix4fv(
323
334
  cache.u_projection,
324
335
  false,
@@ -268,9 +268,9 @@ export class ScatterGlyph {
268
268
  /**
269
269
  * Bind the persistent left/right buffers and issue up to two draw
270
270
  * calls. No per-frame allocations or buffer uploads. `splitFilter`
271
- * (faceted frames) instead draws each matching series' contiguous
272
- * bucket sub-range — one `drawArrays(POINTS, first, count)` per
273
- * series of the facet.
271
+ * (faceted frames) and/or `aggRange` (mixed glyph-run frames)
272
+ * instead draw each matching series' contiguous bucket sub-range —
273
+ * one `drawArrays(POINTS, first, count)` per matching series.
274
274
  */
275
275
  draw(
276
276
  chart: SeriesChart,
@@ -279,6 +279,7 @@ export class ScatterGlyph {
279
279
  projLeft: Float32Array,
280
280
  projRight: Float32Array,
281
281
  splitFilter?: number,
282
+ aggRange?: { start: number; end: number },
282
283
  ): void {
283
284
  const buf = this._buffers;
284
285
  const cache = this._program;
@@ -297,9 +298,20 @@ export class ScatterGlyph {
297
298
  chart._pluginConfig.point_size_px * dpr,
298
299
  );
299
300
 
300
- if (splitFilter !== undefined) {
301
+ if (splitFilter !== undefined || aggRange !== undefined) {
301
302
  for (const r of buf.seriesRanges) {
302
- if (chart._series[r.seriesId].splitIdx !== splitFilter) {
303
+ if (
304
+ splitFilter !== undefined &&
305
+ chart._series[r.seriesId].splitIdx !== splitFilter
306
+ ) {
307
+ continue;
308
+ }
309
+
310
+ const aggIdx = chart._series[r.seriesId].aggIdx;
311
+ if (
312
+ aggRange !== undefined &&
313
+ (aggIdx < aggRange.start || aggIdx > aggRange.end)
314
+ ) {
303
315
  continue;
304
316
  }
305
317
 
@@ -47,8 +47,9 @@ export function getHoveredBar(chart: SeriesChart): SeriesChartRecord | null {
47
47
  }
48
48
 
49
49
  /**
50
- * Handle mouse-move across all glyph types. Tests (in reverse paint order
51
- * so top glyphs win): scatter points → line points → bars → areas.
50
+ * Handle mouse-move across all glyph types. Tests glyph runs in reverse
51
+ * paint order so top glyphs win — paint order is the `columns`
52
+ * declaration order (see `_glyphRuns` / `drawGlyphRuns`).
52
53
  * Updates `_hoveredBarIdx` or `_hoveredSample` and re-renders on change.
53
54
  *
54
55
  * Faceted frames first resolve the cell under the cursor: that cell's
@@ -153,96 +154,131 @@ export function handleBarHover(
153
154
  let nextBarIdx = -1;
154
155
  let nextSample: SeriesChartRecord | null = null;
155
156
 
156
- // 1. Scatter (top).
157
- nextSample = hitTestPoints(
158
- chart,
159
- "scatter",
160
- dataX,
161
- dataYLeft,
162
- dataYRight,
163
- pxPerDataX,
164
- pxPerDataYLeft,
165
- pxPerDataYRight,
166
- splitFilter,
167
- );
157
+ // Test topmost-first: the REVERSE of the paint order, which is the
158
+ // `columns` declaration order (`_glyphRuns`, ascending `aggIdx` —
159
+ // see `drawGlyphRuns`). A single-run chart passes no `aggRange`,
160
+ // reducing to the legacy whole-type scan.
161
+ const runs = chart._glyphRuns;
162
+ const single = runs.length <= 1;
163
+ for (let r = runs.length - 1; r >= 0; r--) {
164
+ const run = runs[r];
165
+ const aggRange = single
166
+ ? undefined
167
+ : { start: run.aggStart, end: run.aggEnd };
168
+ switch (run.chartType) {
169
+ case "scatter":
170
+ case "line":
171
+ nextSample = hitTestPoints(
172
+ chart,
173
+ run.chartType,
174
+ dataX,
175
+ dataYLeft,
176
+ dataYRight,
177
+ pxPerDataX,
178
+ pxPerDataYLeft,
179
+ pxPerDataYRight,
180
+ splitFilter,
181
+ aggRange,
182
+ );
183
+ break;
184
+ case "bar":
185
+ nextBarIdx = hitTestBars(
186
+ chart,
187
+ dataX,
188
+ dataYLeft,
189
+ dataYRight,
190
+ splitFilter,
191
+ aggRange,
192
+ );
193
+ break;
194
+ case "area": {
195
+ const areaHit = hitTestAreas(
196
+ chart,
197
+ dataX,
198
+ dataYLeft,
199
+ dataYRight,
200
+ splitFilter,
201
+ aggRange,
202
+ );
203
+ if (areaHit) {
204
+ if (areaHit.idx >= 0) {
205
+ nextBarIdx = areaHit.idx;
206
+ } else {
207
+ nextSample = areaHit.bar;
208
+ }
209
+ }
168
210
 
169
- // 2. Line points (still above bars; treat as point hits).
170
- if (!nextSample) {
171
- nextSample = hitTestPoints(
172
- chart,
173
- "line",
174
- dataX,
175
- dataYLeft,
176
- dataYRight,
177
- pxPerDataX,
178
- pxPerDataYLeft,
179
- pxPerDataYRight,
180
- splitFilter,
181
- );
211
+ break;
212
+ }
213
+ }
214
+
215
+ if (nextBarIdx >= 0 || nextSample) {
216
+ break;
217
+ }
182
218
  }
183
219
 
184
- // 3. Bars (rect intersect).
185
- if (!nextSample) {
186
- const bars = chart._bars;
187
- const ct = bars.chartType;
188
- const sid = bars.seriesId;
189
- const xC = bars.xCenter;
190
- const hw = bars.halfWidth;
191
- const by0 = bars.y0;
192
- const by1 = bars.y1;
193
- const ax = bars.axis;
194
- const hidden = chart._hiddenSeries;
195
- const P = chart._splitPrefixes.length;
196
- for (let i = 0; i < bars.count; i++) {
197
- if (ct[i] !== BAR_TYPE_BAR) {
198
- continue;
199
- }
220
+ applyHover(chart, nextBarIdx, nextSample);
221
+ }
200
222
 
201
- if (hidden.has(sid[i])) {
202
- continue;
203
- }
223
+ /**
224
+ * Rect-intersect hit-test over the bar-typed `_bars` records. Returns
225
+ * the record index, or `-1`.
226
+ */
227
+ function hitTestBars(
228
+ chart: SeriesChart,
229
+ dataX: number,
230
+ dataYLeft: number,
231
+ dataYRight: number,
232
+ splitFilter?: number,
233
+ aggRange?: { start: number; end: number },
234
+ ): number {
235
+ const bars = chart._bars;
236
+ const ct = bars.chartType;
237
+ const sid = bars.seriesId;
238
+ const xC = bars.xCenter;
239
+ const hw = bars.halfWidth;
240
+ const by0 = bars.y0;
241
+ const by1 = bars.y1;
242
+ const ax = bars.axis;
243
+ const hidden = chart._hiddenSeries;
244
+ const P = Math.max(1, chart._splitPrefixes.length);
245
+ for (let i = 0; i < bars.count; i++) {
246
+ if (ct[i] !== BAR_TYPE_BAR) {
247
+ continue;
248
+ }
204
249
 
205
- if (splitFilter !== undefined && sid[i] % P !== splitFilter) {
206
- continue;
207
- }
250
+ if (hidden.has(sid[i])) {
251
+ continue;
252
+ }
208
253
 
209
- const xc = xC[i];
210
- const halfW = hw[i];
211
- if (dataX < xc - halfW || dataX > xc + halfW) {
254
+ if (splitFilter !== undefined && sid[i] % P !== splitFilter) {
255
+ continue;
256
+ }
257
+
258
+ if (aggRange !== undefined) {
259
+ const aggIdx = Math.floor(sid[i] / P);
260
+ if (aggIdx < aggRange.start || aggIdx > aggRange.end) {
212
261
  continue;
213
262
  }
263
+ }
214
264
 
215
- const dy = ax[i] === 0 ? dataYLeft : dataYRight;
216
- const y0 = by0[i];
217
- const y1 = by1[i];
218
- const lo = y0 < y1 ? y0 : y1;
219
- const hi = y0 < y1 ? y1 : y0;
220
- if (dy >= lo && dy <= hi) {
221
- nextBarIdx = i;
222
- break;
223
- }
265
+ const xc = xC[i];
266
+ const halfW = hw[i];
267
+ if (dataX < xc - halfW || dataX > xc + halfW) {
268
+ continue;
224
269
  }
225
- }
226
270
 
227
- // 4. Areas (strip hit — stacked records via `_bars`, unstacked via samples).
228
- if (nextBarIdx < 0 && !nextSample) {
229
- const areaHit = hitTestAreas(
230
- chart,
231
- dataX,
232
- dataYLeft,
233
- dataYRight,
234
- splitFilter,
235
- );
236
- if (areaHit) {
237
- if (areaHit.idx >= 0) {
238
- nextBarIdx = areaHit.idx;
239
- } else {
240
- nextSample = areaHit.bar;
241
- }
271
+ const dy = ax[i] === 0 ? dataYLeft : dataYRight;
272
+ const y0 = by0[i];
273
+ const y1 = by1[i];
274
+ const lo = y0 < y1 ? y0 : y1;
275
+ const hi = y0 < y1 ? y1 : y0;
276
+ if (dy >= lo && dy <= hi) {
277
+ return i;
242
278
  }
243
279
  }
244
280
 
245
- applyHover(chart, nextBarIdx, nextSample);
281
+ return -1;
246
282
  }
247
283
 
248
284
  function hitTestPoints(
@@ -255,6 +291,7 @@ function hitTestPoints(
255
291
  pxPerDataYLeft: number,
256
292
  pxPerDataYRight: number,
257
293
  splitFilter?: number,
294
+ aggRange?: { start: number; end: number },
258
295
  ): SeriesChartRecord | null {
259
296
  const N = chart._numCategories;
260
297
  const S = chart._series.length;
@@ -283,6 +320,13 @@ function hitTestPoints(
283
320
  continue;
284
321
  }
285
322
 
323
+ if (
324
+ aggRange !== undefined &&
325
+ (s.aggIdx < aggRange.start || s.aggIdx > aggRange.end)
326
+ ) {
327
+ continue;
328
+ }
329
+
286
330
  const dataY = s.axis === 1 ? dataYRight : dataYLeft;
287
331
  const pyPerData = s.axis === 1 ? pxPerDataYRight : pxPerDataYLeft;
288
332
 
@@ -340,6 +384,7 @@ function hitTestAreas(
340
384
  dataYLeft: number,
341
385
  dataYRight: number,
342
386
  splitFilter?: number,
387
+ aggRange?: { start: number; end: number },
343
388
  ): { idx: number; bar: SeriesChartRecord | null } | null {
344
389
  // Closest category to the mouse; an area covers every [cat - 0.5, cat + 0.5]
345
390
  // slot, so use `round(dataX)` as the candidate index.
@@ -382,6 +427,13 @@ function hitTestAreas(
382
427
  continue;
383
428
  }
384
429
 
430
+ if (aggRange !== undefined) {
431
+ const aggIdx = Math.floor(sid[i] / Math.max(1, P));
432
+ if (aggIdx < aggRange.start || aggIdx > aggRange.end) {
433
+ continue;
434
+ }
435
+ }
436
+
385
437
  const dy = ax[i] === 0 ? dataYLeft : dataYRight;
386
438
  const y0 = by0[i];
387
439
  const y1 = by1[i];
@@ -406,6 +458,13 @@ function hitTestAreas(
406
458
  continue;
407
459
  }
408
460
 
461
+ if (
462
+ aggRange !== undefined &&
463
+ (s.aggIdx < aggRange.start || s.aggIdx > aggRange.end)
464
+ ) {
465
+ continue;
466
+ }
467
+
409
468
  const idx = cat * S + s.seriesId;
410
469
  if (!((valid[idx >> 3] >> (idx & 7)) & 1)) {
411
470
  continue;