1010// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
1111// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1212
13+ import type { View } from "@perspective-dev/client" ;
1314import type { ColumnDataMap } from "../data/view-reader" ;
15+ import { LazyRowFetcher } from "../data/lazy-row" ;
1416import type { WebGLContextManager } from "../webgl/context-manager" ;
15- import type {
16- ZoomConfig ,
17+ import {
1718 ZoomController ,
19+ type ZoomConfig ,
1820} from "../interaction/zoom-controller" ;
19- import type { ChartImplementation } from "./chart" ;
21+ import {
22+ DEFAULT_FACET_CONFIG ,
23+ type ChartImplementation ,
24+ type FacetConfig ,
25+ } from "./chart" ;
2026import { TooltipController } from "../interaction/tooltip-controller" ;
2127
2228/**
@@ -76,6 +82,15 @@ export abstract class AbstractChart implements ChartImplementation {
7682 _gridlineCanvas : HTMLCanvasElement | null = null ;
7783 _chromeCanvas : HTMLCanvasElement | null = null ;
7884 _zoomController : ZoomController | null = null ;
85+ /**
86+ * Per-facet zoom controllers. Populated when `zoom_mode ===
87+ * "independent"` and the chart enters faceted mode; each facet's
88+ * render path reads its own viewport from the matching entry.
89+ *
90+ * Shared-zoom mode leaves this empty; `_zoomController` is the
91+ * single domain used for every facet.
92+ */
93+ _facetZoomControllers : ZoomController [ ] = [ ] ;
7994 _glCanvas : HTMLCanvasElement | null = null ;
8095
8196 _columnSlots : ( string | null ) [ ] = [ ] ;
@@ -84,9 +99,22 @@ export abstract class AbstractChart implements ChartImplementation {
8499 _columnTypes : Record < string , string > = { } ;
85100 _columnsConfig : Record < string , any > = { } ;
86101 _defaultChartType : string | undefined = undefined ;
102+ _facetConfig : FacetConfig = { ...DEFAULT_FACET_CONFIG } ;
87103
88104 _tooltip = new TooltipController ( ) ;
89105
106+ /**
107+ * On-demand single-row fetcher used by lazy tooltip column
108+ * lookups. Reset on every `setView` call; subclasses read
109+ * `_lazyRows.fetchRow(rowIdx)` from their hover/pin paths and
110+ * compare a captured serial against the current hovered/pinned
111+ * state at resolution time, so stale fetches never paint.
112+ *
113+ * Can be `null` on chart types that don't surface the View
114+ * (unit-tested charts) or before the first `draw`.
115+ */
116+ _lazyRows : LazyRowFetcher | null = null ;
117+
90118 private _renderScheduled = false ;
91119 private _renderRAFId = 0 ;
92120
@@ -105,6 +133,47 @@ export abstract class AbstractChart implements ChartImplementation {
105133 zc . configure ( this . getZoomConfig ( ) ) ;
106134 }
107135
136+ /**
137+ * Resolve the zoom controller that owns facet `idx`. In shared-zoom
138+ * mode (default) this is always the chart's single `_zoomController`.
139+ * In independent-zoom mode the router provisions one controller per
140+ * facet; this returns the matching entry, allocating on demand so
141+ * the render path never has to check `zoom_mode` itself.
142+ */
143+ getZoomControllerForFacet ( idx : number ) : ZoomController | null {
144+ if ( this . _facetConfig . zoom_mode === "shared" ) {
145+ return this . _zoomController ;
146+ }
147+ if ( ! this . _zoomController ) return null ;
148+ let zc = this . _facetZoomControllers [ idx ] ;
149+ if ( ! zc ) {
150+ zc = new ZoomController ( ) ;
151+ zc . configure ( this . getZoomConfig ( ) ) ;
152+ this . _facetZoomControllers [ idx ] = zc ;
153+ }
154+ return zc ;
155+ }
156+
157+ /**
158+ * Seed base domain on every zoom controller owned by this chart.
159+ * Build paths call this once per load with the accumulated data
160+ * extents; independent-zoom facets share the same base so visual
161+ * zoom levels stay comparable across facets.
162+ */
163+ setZoomBaseDomain (
164+ xMin : number ,
165+ xMax : number ,
166+ yMin : number ,
167+ yMax : number ,
168+ ) : void {
169+ if ( this . _zoomController ) {
170+ this . _zoomController . setBaseDomain ( xMin , xMax , yMin , yMax ) ;
171+ }
172+ for ( const zc of this . _facetZoomControllers ) {
173+ if ( zc ) zc . setBaseDomain ( xMin , xMax , yMin , yMax ) ;
174+ }
175+ }
176+
108177 /**
109178 * Zoom-controller config for this chart type. Subclasses override to
110179 * pin an axis (e.g. bar charts pin the categorical axis). Default:
@@ -135,6 +204,28 @@ export abstract class AbstractChart implements ChartImplementation {
135204 this . _defaultChartType = chartType ;
136205 }
137206
207+ setFacetConfig ( cfg : FacetConfig ) : void {
208+ this . _facetConfig = { ...cfg } ;
209+ }
210+
211+ /**
212+ * Install a new view for lazy row fetches. Disposes any prior
213+ * fetcher and dismisses the pinned tooltip — the prior pinned
214+ * row index has no guaranteed correspondence in the new view
215+ * (pivot / filter / sort changes can all reshuffle rows).
216+ *
217+ * TODO: future work will keep pinned tooltips visible with their
218+ * last-resolved lines until the user explicitly dismisses them,
219+ * so a mid-session view update doesn't blow away focused context.
220+ */
221+ setView ( view : View ) : void {
222+ if ( this . _lazyRows ) {
223+ this . _lazyRows . dispose ( ) ;
224+ }
225+ this . _lazyRows = new LazyRowFetcher ( view ) ;
226+ this . _tooltip . dismissPinned ( ) ;
227+ }
228+
138229 // ── Render batching ────────────────────────────────────────────────────
139230
140231 /** Schedule one `_fullRender` on the next animation frame (idempotent). */
@@ -163,6 +254,10 @@ export abstract class AbstractChart implements ChartImplementation {
163254 this . _tooltip . detach ( ) ;
164255 this . _tooltip . dismissPinned ( ) ;
165256 this . _cancelScheduledRender ( ) ;
257+ if ( this . _lazyRows ) {
258+ this . _lazyRows . dispose ( ) ;
259+ this . _lazyRows = null ;
260+ }
166261 this . destroyInternal ( ) ;
167262 }
168263
0 commit comments