@@ -15,6 +15,11 @@ import { getTopLevelBoundingClientRect } from "cx/util";
1515// - bind `yMin`/`yMax` -> the selector zooms the y-axis
1616// - bind both -> rectangular (x + y) zoom
1717//
18+ // An optional `onZoom(range, instance)` callback fires after a zoom is applied
19+ // (and on double-click reset). `range` is `{ x, y, reset? }`, where `x`/`y` are
20+ // `[min, max]` for the axes that changed or `null` otherwise; `reset` is `true`
21+ // for a double-click reset.
22+ //
1823// It relies on the axis calculators (available to any chart child via
1924// `context.axes[...]`) to convert pixel <-> value:
2025// - trackValue(px, offset, constrain) : pixel -> value
@@ -171,21 +176,27 @@ class RangeSelector extends BoundedObject {
171176 window . removeEventListener ( "mousemove" , move , true ) ;
172177 window . removeEventListener ( "mouseup" , up , true ) ;
173178
179+ let range = { x : null , y : null } ;
180+
174181 instance . store . batch ( ( ) => {
175182 instance . set ( "x1" , null ) ;
176183 instance . set ( "x2" , null ) ;
177184 instance . set ( "y1" , null ) ;
178185 instance . set ( "y2" , null ) ;
179186
180187 if ( useX && ax && Math . abs ( lastPxX - startPxX ) >= 3 ) {
181- instance . set ( "xMin" , Math . min ( startX , lastX ) ) ;
182- instance . set ( "xMax" , Math . max ( startX , lastX ) ) ;
188+ range . x = [ Math . min ( startX , lastX ) , Math . max ( startX , lastX ) ] ;
189+ instance . set ( "xMin" , range . x [ 0 ] ) ;
190+ instance . set ( "xMax" , range . x [ 1 ] ) ;
183191 }
184192 if ( useY && ay && Math . abs ( lastPxY - startPxY ) >= 3 ) {
185- instance . set ( "yMin" , Math . min ( startY , lastY ) ) ;
186- instance . set ( "yMax" , Math . max ( startY , lastY ) ) ;
193+ range . y = [ Math . min ( startY , lastY ) , Math . max ( startY , lastY ) ] ;
194+ instance . set ( "yMin" , range . y [ 0 ] ) ;
195+ instance . set ( "yMax" , range . y [ 1 ] ) ;
187196 }
188197 } ) ;
198+
199+ if ( ( range . x || range . y ) && this . onZoom ) instance . invoke ( "onZoom" , range , instance ) ;
189200 } ;
190201
191202 window . addEventListener ( "mousemove" , move , true ) ;
@@ -203,6 +214,10 @@ class RangeSelector extends BoundedObject {
203214 instance . set ( "yMax" , null ) ;
204215 }
205216 } ) ;
217+
218+ // reset is signalled with null ranges for the axes this selector controls
219+ if ( this . onZoom )
220+ instance . invoke ( "onZoom" , { x : null , y : null , reset : true } , instance ) ;
206221 }
207222}
208223
@@ -226,10 +241,19 @@ class PageController extends Controller {
226241 }
227242}
228243
244+ function describeZoom ( range , label ) {
245+ if ( range . reset ) return `${ label } : reset` ;
246+ let d = ( ms ) => new Date ( ms ) . toLocaleDateString ( ) ;
247+ let parts = [ ] ;
248+ if ( range . x ) parts . push ( `x ${ d ( range . x [ 0 ] ) } → ${ d ( range . x [ 1 ] ) } ` ) ;
249+ if ( range . y ) parts . push ( `y ${ range . y [ 0 ] . toFixed ( 1 ) } → ${ range . y [ 1 ] . toFixed ( 1 ) } ` ) ;
250+ return `${ label } : ${ parts . join ( ", " ) } ` ;
251+ }
252+
229253// A single chart bound to the shared x-range ($page.range) and its own y-range.
230254// `zoomY` decides whether the y-axis bindings are wired to the RangeSelector;
231255// when they are omitted, the selector zooms the x-axis only.
232- const ZoomChart = ( { lineStyle, yField, yRange, sel, zoomY } ) => (
256+ const ZoomChart = ( { label , lineStyle, yField, yRange, sel, zoomY } ) => (
233257 < cx >
234258 < Svg style = "width:800px; height:260px;" >
235259 < Chart
@@ -260,6 +284,7 @@ const ZoomChart = ({ lineStyle, yField, yRange, sel, zoomY }) => (
260284 y2 = { zoomY ? { bind : `${ sel } .y2` } : undefined }
261285 yMin = { zoomY ? { bind : `${ yRange } .from` } : undefined }
262286 yMax = { zoomY ? { bind : `${ yRange } .to` } : undefined }
287+ onZoom = { ( range , inst ) => inst . store . set ( "$page.status" , describeZoom ( range , label ) ) }
263288 />
264289 </ Chart >
265290 </ Svg >
@@ -282,13 +307,15 @@ export default (
282307 store . set ( "$page.range" , null ) ;
283308 store . set ( "$page.y1" , null ) ;
284309 store . set ( "$page.y2" , null ) ;
310+ store . set ( "$page.status" , null ) ;
285311 } }
286312 >
287313 Reset zoom
288314 </ Button >
315+ < div style = "color:#555; font-family:monospace" text-tpl = "onZoom → {$page.status:s;—}" />
289316 </ FlexRow >
290- < ZoomChart lineStyle = "stroke: #555; stroke-width: 1.5" yField = "value" yRange = "$page.y1" sel = "$page.sel1" />
291- < ZoomChart lineStyle = "stroke: #888; stroke-width: 1.5" yField = "value2" yRange = "$page.y2" sel = "$page.sel2" zoomY />
317+ < ZoomChart label = "Top" lineStyle = "stroke: #555; stroke-width: 1.5" yField = "value" yRange = "$page.y1" sel = "$page.sel1" />
318+ < ZoomChart label = "Bottom" lineStyle = "stroke: #888; stroke-width: 1.5" yField = "value2" yRange = "$page.y2" sel = "$page.sel2" zoomY />
292319 </ div >
293320 </ cx >
294321) ;
0 commit comments