Skip to content

Commit 4f339d8

Browse files
authored
Merge pull request #74 from CSSFrancis/fix/imshow-centre-ticks
Tick gutters label the pixels they sit next to
2 parents ce22710 + f28049c commit 4f339d8

7 files changed

Lines changed: 361 additions & 94 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ grep -nE '^\s*(function|const|let) [A-Za-z_]' anyplotlib/figure_esm.js
115115
```
116116

117117
and reconcile against the two numbered tables (the section map near the top and
118-
the 2-D function table). Both were last verified at 12,386 lines.
118+
the 2-D function table). Both were last verified at 12,407 lines.
119119

120120
Changelog entries: add a fragment file to `upcoming_changes/` (e.g.
121121
`123.new_feature.rst`) — towncrier assembles `CHANGELOG.rst` at release time.

‎anyplotlib/FIGURE_ESM.md‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,18 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
6363
| `drawScaleBar2d` / `drawColorbar2d` | 3076 / 3356 |
6464
| **Floating keys**: `_keyEnsure` / `_keyRect` / `drawKeys` | 3175 / 3198 / 3211 |
6565
| `_drawAxes2d` (ticks, labels, title) | 3468 |
66-
| `drawOverlay2d` / `drawMarkers2d` | 3621 / 3785 |
66+
| `drawOverlay2d` / `drawMarkers2d` | 3623 / 3787 |
6767
| **Image layers**: `_layerBytes` / `_layerBitmap` / `_drawLayers2d` | 2701 / 2725 / 2786 |
6868
| Binary-bytes splice: `_spliceBinaryBytes` / `_registerBinaryPixelListeners` | 849 / 880 |
69-
| **Hover readout**: `_pixelValue2d` / `_readoutInfo2d` | 4629 / 4714 |
70-
| `_notifyReadout` / `_updateStatus2d` / `_armValueProbe` | 4753 / 4768 / 4789 |
71-
| **3D drawing**: `draw3d` | 5765 |
72-
| Event emission `_emitEvent` | 6602 |
73-
| 3D event handlers `_attachEvents3d` | 6659 |
74-
| **1D drawing**: `draw1d` | 6883 |
75-
| `_drawLine` (1D series + markers) | 7036 |
76-
| `drawOverlay1d` / `drawMarkers1d` | 7329 / 7413 |
77-
| Marker hit-test `_markerHitTest2d` | 7680 |
69+
| **Hover readout**: `_pixelValue2d` / `_readoutInfo2d` | 4631 / 4716 |
70+
| `_notifyReadout` / `_updateStatus2d` / `_armValueProbe` | 4755 / 4770 / 4791 |
71+
| **3D drawing**: `draw3d` | 5767 |
72+
| Event emission `_emitEvent` | 6604 |
73+
| 3D event handlers `_attachEvents3d` | 6661 |
74+
| **1D drawing**: `draw1d` | 6885 |
75+
| `_drawLine` (1D series + markers) | 7038 |
76+
| `drawOverlay1d` / `drawMarkers1d` | 7331 / 7415 |
77+
| Marker hit-test `_markerHitTest2d` | 7682 |
7878

7979
> **`raster` marker (1D/PlotXY)** — `drawMarkers1d` has a `type==='raster'`
8080
> branch that blits a single RGBA image across data-coord `extent` (the fast
@@ -83,22 +83,22 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
8383
> redraws never re-transmit them; the decoded `OffscreenCanvas` is cached on
8484
> the marker set (`ms._rasterBmp`/`_rasterKey`). The shared `clip_path` block
8585
> clips it to a curved sector.
86-
| Panel event dispatch `_attachPanelEvents` | 7937 |
87-
| 2D events `_attachEvents2d` | 8004 |
88-
| 1D events `_attachEvents1d` | 8390 |
89-
| 2D widget drag `_ovHitTest2d` / `_doDrag2d` | 8665 / 8944 |
90-
| **Brush strokes**: `_brushLiveBegin` / `_brushCommit` / `_brushErase` / `_brushPaintAt` | 8857 / 8871 / 8900 / 8935 |
91-
| 1D widget drag `_canvasXToFrac1d` … / snapping `_snapVal` | 9069 / 9142 |
92-
| Shared-axis propagation `_getShareGroups` | 9213 |
93-
| Figure resize `_applyFigResizeDOM` | 9277 |
94-
| **Bar chart**: `_barGeom` / `drawBar` / `_attachEventsBar` | 9471 / 9534 / 9910 |
95-
| Generic redraw `_redrawPanel` | 10100 |
96-
| **PNG export**: `_compositeCanvas` / `exportCanvas` / `exportPNG` | 10263 / 10459 / 10518 |
97-
| Native-resolution render `_withNativeSize` | 10239 |
98-
| **Export UI**: `_toast` / `_downloadCanvas` / `_openMenu` | 10552 / 10661 / 10840 |
99-
| Export registry `registerExportAction` | 10717 |
100-
| **Embedding API**: `createLocalModel` / `mount` | 11231 / 11287 |
101-
| **Navigated embed**: `decodeBlocks` / `mountNavigated` | 11542 / 11929 |
86+
| Panel event dispatch `_attachPanelEvents` | 7939 |
87+
| 2D events `_attachEvents2d` | 8025 |
88+
| 1D events `_attachEvents1d` | 8411 |
89+
| 2D widget drag `_ovHitTest2d` / `_doDrag2d` | 8686 / 8965 |
90+
| **Brush strokes**: `_brushLiveBegin` / `_brushCommit` / `_brushErase` / `_brushPaintAt` | 8878 / 8892 / 8921 / 8956 |
91+
| 1D widget drag `_canvasXToFrac1d` … / snapping `_snapVal` | 9090 / 9163 |
92+
| Shared-axis propagation `_getShareGroups` | 9234 |
93+
| Figure resize `_applyFigResizeDOM` | 9298 |
94+
| **Bar chart**: `_barGeom` / `drawBar` / `_attachEventsBar` | 9492 / 9555 / 9931 |
95+
| Generic redraw `_redrawPanel` | 10121 |
96+
| **PNG export**: `_compositeCanvas` / `exportCanvas` / `exportPNG` | 10284 / 10480 / 10539 |
97+
| Native-resolution render `_withNativeSize` | 10260 |
98+
| **Export UI**: `_toast` / `_downloadCanvas` / `_openMenu` | 10573 / 10682 / 10861 |
99+
| Export registry `registerExportAction` | 10738 |
100+
| **Embedding API**: `createLocalModel` / `mount` | 11252 / 11308 |
101+
| **Navigated embed**: `decodeBlocks` / `mountNavigated` | 11563 / 11950 |
102102

103103
> **`brush` widget (2-D)** — the one widget whose drag is *modal*, and the one
104104
> that must NOT write the model per tick. `_ovHitTest2d` takes an extra `mods`
@@ -287,8 +287,8 @@ st.colorbar_label_size (label font sizes; optional)
287287
| `drawScaleBar2d(p)` | 3076 | Physical scale bar |
288288
| `_rawBand(st)` / `_displayFrac(st, val)` / `_buildLut32(st)` | 2596 / 2606 / 2614 | The quantisation band the u8 bytes were encoded over, the ONE rule mapping a value through the display window and scale mode to a colormap fraction (shared by the LUT and the colorbar strip, so the strip's colours are the image's), then the 256-entry LUT built from both. `_rawBand` mirrors Python `_tile_quant_clim`: a DEGENERATE band (`raw_max <= raw_min`) is UNSET and falls back to `display_min/max`. Both render paths and the colorbar go through it — honouring a `(0, 0)` band paints solid black |
289289
| `drawColorbar2d(p)` | 3356 | Gradient strip coloured through the display window (`_displayFrac` — saturated beyond it, like the image) + min/max marks (band-relative, via `_rawBand`) + the two values written beside them (`fmtRange`; kept inside the strip by the glyphs' measured extent, held apart on a tiny range, the maximum alone at its own place on a strip too short for both; omitted when the layout dropped the gutter, `p._cbTickW == 0`) + rotated label centred right after the values' measured width (never past the reserved gutter) |
290-
| `_drawAxes2d(p)` | 3468 | Ticks (edge labels nudged inward both axes), axis labels + title via `_drawTex` |
291-
| `drawOverlay2d(p)` / `drawMarkers2d(p)` | 3621 / 3785 | Widgets / marker groups |
290+
| `_drawAxes2d(p)` | 3468 | Ticks placed through `_imgToCanvas2d` (pixel-centre axes, letterbox-aware; edge labels nudged inward both axes), axis labels + title via `_drawTex` |
291+
| `drawOverlay2d(p)` / `drawMarkers2d(p)` | 3623 / 3787 | Widgets / marker groups |
292292

293293
Zoom model: at `zoom=1` the whole image fills the fit-rect; at `zoom=Z>1` a
294294
`1/Z` region fills it. `_imgToCanvas2d` / `_canvasToImg2d` must stay exact
@@ -301,6 +301,14 @@ coordinate to `xdata`/`ydata` through `_imgToAxisVal2d` (imshow axes are
301301
per-pixel centres, pcolormesh axes cell edges). Code that addresses texels or
302302
the detail region by pixel EDGE (`_pixelValue2d`) adds the `0.5` back itself.
303303

304+
The tick gutters (`_drawAxes2d`) use the same chain in reverse: tick value →
305+
`_axisValToImg2d` (imshow; pcolormesh goes through `_axisValToFrac` × n − ½) →
306+
`_imgToCanvas2d`, so ticks honour the letterboxed fit rect, zoom < 1, pan
307+
clamping and descending (`origin='lower'`) axes for free. The visible value
308+
range is the gutter's two ends through `_canvasToImg2d`, clamped to the outer
309+
pixel edges. Python's `Plot2D._axis_extent_frac` (behind `set_view` /
310+
`set_xlim`) mirrors `_axisValToImg2d` — keep the two in step.
311+
304312
`texts` markers (1-D and 2-D) set their font and optional outline through
305313
`_markerTextStyle(ctx, ms)` (`fontweight`, `outline_color`, `outline_width`)
306314
and draw via `_drawMarkerText`, which strokes the halo under the fill.
@@ -652,13 +660,13 @@ exportCanvas(same opts) → {canvas, width, height} // synchronous, throws
652660

653661
| Function | Line | Purpose |
654662
|----------|------|---------|
655-
| `_cssScale` | 10135 | inverse of `_applyScale`'s `transform:scale()` |
656-
| `_panelBox` | 10146 | the element whose rect bounds one panel |
657-
| `_neutralizeView` / `_restoreView` | 10155 / 10180 | transient whole-extent view |
658-
| `_nativeGeom` / `_nativeGuard` | 10195 / 10214 | native size + why-not message |
659-
| `_withNativeSize` | 10239 | resize → redraw → run → restore |
660-
| `_compositeCanvas` | 10263 | the compositor (`_drawEl` / `_drawPanel` …) |
661-
| `exportCanvas` / `exportPNG` | 10459 / 10518 | orchestrator / data-URL wrapper |
663+
| `_cssScale` | 10156 | inverse of `_applyScale`'s `transform:scale()` |
664+
| `_panelBox` | 10167 | the element whose rect bounds one panel |
665+
| `_neutralizeView` / `_restoreView` | 10176 / 10201 | transient whole-extent view |
666+
| `_nativeGeom` / `_nativeGuard` | 10216 / 10235 | native size + why-not message |
667+
| `_withNativeSize` | 10260 | resize → redraw → run → restore |
668+
| `_compositeCanvas` | 10284 | the compositor (`_drawEl` / `_drawPanel` …) |
669+
| `exportCanvas` / `exportPNG` | 10480 / 10539 | orchestrator / data-URL wrapper |
662670

663671
**The whole pipeline is ONE synchronous task** — theme swap, view reset, native
664672
resize, composite, restore — so the browser never paints an intermediate state
@@ -754,13 +762,13 @@ leaders that cross into the panel included. Pinned by
754762

755763
| Function | Line | Purpose |
756764
|----------|------|---------|
757-
| `_toast` | 10552 | transient bottom-centre message |
758-
| `_copyCanvas` | 10587 | clipboard write + feature detection |
759-
| `_showPngPreview` | 10611 | framed-document download fallback |
760-
| `_downloadCanvas` | 10661 | `<a download>` or the preview |
761-
| `registerExportAction` | 10717 | downstream extension point |
762-
| `_menuRows` / `_openMenu` | 10771 / 10840 | menu model / DOM |
763-
| `_panelAtPoint` | 10952 | hit test (insets first — they sit on top) |
765+
| `_toast` | 10573 | transient bottom-centre message |
766+
| `_copyCanvas` | 10608 | clipboard write + feature detection |
767+
| `_showPngPreview` | 10632 | framed-document download fallback |
768+
| `_downloadCanvas` | 10682 | `<a download>` or the preview |
769+
| `registerExportAction` | 10738 | downstream extension point |
770+
| `_menuRows` / `_openMenu` | 10792 / 10861 | menu model / DOM |
771+
| `_panelAtPoint` | 10973 | hit test (insets first — they sit on top) |
764772

765773
- **An `exportBtn` badge (⤓, beside the help badge) opens the same menu on an
766774
ordinary left click.** It is a `role="button"` with `tabIndex=0` and
@@ -844,16 +852,16 @@ bindings, let it dispatch", rather than a hand-written program per result kind.
844852

845853
| Function | Line | Purpose |
846854
|----------|------|---------|
847-
| `decodeBlocks` | 11542 | one base64 `fetch` → one ArrayBuffer → a typed-array view per manifest entry |
848-
| `dense` | 11568 | `at` / `gather` / `reduce` over a block whose leading axes are the nav axes |
849-
| `ragged` | 11633 | the same three, over a row-pointer block (`offsets` + one array per column) |
850-
| `maskFromWidget` | 11716 | rectangle / circle / annulus widget dict → `Uint8Array` (carries `width`/`height`) |
851-
| `rasterDisks` | 11756 | splat `{x, y, intensity}` rows as filled disks — the base image of a vectors panel |
852-
| `robustLevels` / `toU8` | 11785 / 11826 | the percentile window and the 8-bit code map, one implementation |
853-
| `panelAxis` | 11904 | a 1-D panel's decoded x axis (`_1dXArr`, else `x_axis_b64`) |
854-
| `installTouchShim` / `reportEmbedHeight` | 11841 / 11860 | page chrome: touch → mouse, `postMessage({aplEmbedHeight})` |
855-
| `encodeBase64` / `typedArrayBytes` | 11880 / 11888 | a 3-D cloud's geometry channel is base64, not the binary side table |
856-
| `mountNavigated` | 11929 | mount + bind + dispatch; resolves to the mount handle plus `dispatch`/`index`/`blocks` |
855+
| `decodeBlocks` | 11563 | one base64 `fetch` → one ArrayBuffer → a typed-array view per manifest entry |
856+
| `dense` | 11589 | `at` / `gather` / `reduce` over a block whose leading axes are the nav axes |
857+
| `ragged` | 11654 | the same three, over a row-pointer block (`offsets` + one array per column) |
858+
| `maskFromWidget` | 11737 | rectangle / circle / annulus widget dict → `Uint8Array` (carries `width`/`height`) |
859+
| `rasterDisks` | 11777 | splat `{x, y, intensity}` rows as filled disks — the base image of a vectors panel |
860+
| `robustLevels` / `toU8` | 11806 / 11847 | the percentile window and the 8-bit code map, one implementation |
861+
| `panelAxis` | 11925 | a 1-D panel's decoded x axis (`_1dXArr`, else `x_axis_b64`) |
862+
| `installTouchShim` / `reportEmbedHeight` | 11862 / 11881 | page chrome: touch → mouse, `postMessage({aplEmbedHeight})` |
863+
| `encodeBase64` / `typedArrayBytes` | 11901 / 11909 | a 3-D cloud's geometry channel is base64, not the binary side table |
864+
| `mountNavigated` | 11950 | mount + bind + dispatch; resolves to the mount handle plus `dispatch`/`index`/`blocks` |
857865

858866
`mountNavigated(el, page, opts)` is **async** — the blob decode is a `fetch` of
859867
a `data:` URL — so a host `await`s it. `page` is `{state, blocks, bindings,

‎anyplotlib/figure_esm.js‎

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3472,7 +3472,6 @@ function render({ model, el, onResize, onReadout }) {
34723472
const imgH = p.imgH||Math.max(1, ph - PAD_T - PAD_B);
34733473
const xArr=st.x_axis||[], yArr=st.y_axis||[];
34743474
const TICK=6;
3475-
const zoom=st.zoom, cx=st.center_x, cy=st.center_y;
34763475
const units=st.units||'px';
34773476
const hasPhysAxis = (st.is_mesh || st.has_axes) && xArr.length>=2 && yArr.length>=2;
34783477
if(st.axis_visible===false){
@@ -3485,16 +3484,22 @@ function render({ model, el, onResize, onReadout }) {
34853484
const hasX=hasPhysAxis&&st.axis_visible!==false&&st.x_ticks_visible!==false&&p.xCtx&&p.xAxisCanvas&&p.xAxisCanvas.style.display!=='none';
34863485
const hasY=hasPhysAxis&&st.axis_visible!==false&&st.y_ticks_visible!==false&&p.yCtx&&p.yAxisCanvas&&p.yAxisCanvas.style.display!=='none';
34873486

3488-
function _visFrac(z,c){
3489-
if(z>=1.0){const h=0.5/z;const cc=Math.max(h,Math.min(1-h,c));return[cc-h,cc+h];}
3490-
return[0,1];
3491-
}
3492-
function _fracToPx(frac, z, center, span) {
3493-
if(z>=1.0){
3494-
const h=0.5/z, cc=Math.max(h,Math.min(1-h,center));
3495-
return (frac-(cc-h))/(2*h)*span;
3496-
}
3497-
return (span-span*z)/2+frac*span*z;
3487+
// Ticks go through the SAME transform markers, widgets and pointer events
3488+
// use — axis value → centre-convention image coordinate → _imgToCanvas2d —
3489+
// so a tick sits on the pixels it labels at any zoom and pan, and on the
3490+
// letterboxed fit rect rather than the whole gutter. (They used to take the
3491+
// value's fraction of the axis array as a fraction of the gutter: imshow's
3492+
// first and last pixel CENTRES landed on the gutter's ends, stretching every
3493+
// label outward, and a pillarboxed image's ticks ignored its offset.)
3494+
const iw=st.image_width||1, ih=st.image_height||1;
3495+
const _valToImg=(arr,v,n)=>st.is_mesh ? _axisValToFrac(arr,v)*n-0.5 : _axisValToImg2d(arr,v,n);
3496+
// [lo, hi] axis values over the visible stretch of the image along axis `ax`
3497+
// (0 = x, 1 = y): the gutter's two ends, clamped to the outer pixel edges.
3498+
function _visRange(arr, n, ax){
3499+
const a=_canvasToImg2d(0,0,st,imgW,imgH)[ax], b=_canvasToImg2d(imgW,imgH,st,imgW,imgH)[ax];
3500+
const va=_imgToAxisVal2d(st,arr,Math.max(-0.5,Math.min(a,b)),n);
3501+
const vb=_imgToAxisVal2d(st,arr,Math.min(n-0.5,Math.max(a,b)),n);
3502+
return [Math.min(va,vb), Math.max(va,vb)];
34983503
}
34993504

35003505
// ── X axis canvas: imgW × PAD_B, origin at top-left ─────────────────
@@ -3505,8 +3510,7 @@ function render({ model, el, onResize, onReadout }) {
35053510
p.xCtx.fillStyle=theme.axisBg; p.xCtx.fillRect(0,0,aw,ah);
35063511
p.xCtx.strokeStyle=theme.axisStroke; p.xCtx.lineWidth=1;
35073512
p.xCtx.beginPath(); p.xCtx.moveTo(0,0); p.xCtx.lineTo(aw,0); p.xCtx.stroke();
3508-
const [xF0,xF1]=_visFrac(zoom,cx);
3509-
const xVMin=_axisFracToVal(xArr,xF0), xVMax=_axisFracToVal(xArr,xF1);
3513+
const [xVMin,xVMax]=_visRange(xArr,iw,0);
35103514
const step=findNice((xVMax-xVMin)/Math.max(3,Math.floor(imgW/60)));
35113515
p.xCtx.strokeStyle=theme.tickStroke;
35123516
p.xCtx.fillStyle=theme.tickText; p.xCtx.font=(st.tick_size||10)+'px sans-serif';
@@ -3521,12 +3525,12 @@ function render({ model, el, onResize, onReadout }) {
35213525
let lastPx=-Infinity;
35223526
for(let ti=0;ti<xTicks.length;ti++){
35233527
const v=xTicks[ti];
3524-
const frac=_axisValToFrac(xArr,v);
3525-
const px2=_fracToPx(frac,zoom,cx,imgW);
3528+
const px2=_imgToCanvas2d(_valToImg(xArr,v,iw),0,st,imgW,imgH)[0];
35263529
if(px2<0||px2>imgW) continue;
35273530
p.xCtx.beginPath(); p.xCtx.moveTo(px2,0); p.xCtx.lineTo(px2,TICK); p.xCtx.stroke();
3528-
// Skip label if too close to the previous one
3529-
if(px2-lastPx>=minLabelGap){
3531+
// Skip label if too close to the previous one (either direction: a
3532+
// descending axis walks the gutter right to left)
3533+
if(Math.abs(px2-lastPx)>=minLabelGap){
35303534
const txt=fmtVal(v);
35313535
// Nudge edge labels inward so they are never clipped by the canvas
35323536
const hw=p.xCtx.measureText(txt).width/2;
@@ -3549,8 +3553,7 @@ function render({ model, el, onResize, onReadout }) {
35493553
p.yCtx.fillStyle=theme.axisBg; p.yCtx.fillRect(0,0,aw,ah);
35503554
p.yCtx.strokeStyle=theme.axisStroke; p.yCtx.lineWidth=1;
35513555
p.yCtx.beginPath(); p.yCtx.moveTo(aw,0); p.yCtx.lineTo(aw,ah); p.yCtx.stroke();
3552-
const [yF0,yF1]=_visFrac(zoom,cy);
3553-
const yVMin=_axisFracToVal(yArr,yF0), yVMax=_axisFracToVal(yArr,yF1);
3556+
const [yVMin,yVMax]=_visRange(yArr,ih,1);
35543557
const step=findNice((yVMax-yVMin)/Math.max(3,Math.floor(imgH/60)));
35553558
p.yCtx.strokeStyle=theme.tickStroke;
35563559
p.yCtx.fillStyle=theme.tickText; p.yCtx.font=(st.tick_size||10)+'px sans-serif';
@@ -3561,11 +3564,10 @@ function render({ model, el, onResize, onReadout }) {
35613564
let lastPy=-Infinity;
35623565
for(let ti=0;ti<yTicks.length;ti++){
35633566
const v=yTicks[ti];
3564-
const frac=_axisValToFrac(yArr,v);
3565-
const py2=_fracToPx(frac,zoom,cy,imgH);
3567+
const py2=_imgToCanvas2d(0,_valToImg(yArr,v,ih),st,imgW,imgH)[1];
35663568
if(py2<0||py2>imgH) continue;
35673569
p.yCtx.beginPath(); p.yCtx.moveTo(aw,py2); p.yCtx.lineTo(aw-TICK,py2); p.yCtx.stroke();
3568-
if(py2-lastPy>=minLabelGapY){
3570+
if(Math.abs(py2-lastPy)>=minLabelGapY){
35693571
// Nudge edge labels inward so digits are never cut by the canvas
35703572
const vh=(st.tick_size||10)*0.5+1;
35713573
p.yCtx.fillText(fmtVal(v), aw-TICK-2, Math.min(Math.max(py2,vh), ah-vh));
@@ -8001,6 +8003,25 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
80018003
return arr[lo] + (pos - lo) * (arr[lo + 1] - arr[lo]);
80028004
}
80038005

8006+
// The inverse for imshow's centre arrays: an axis value → the centre-convention
8007+
// image coordinate it sits at, continued linearly past either end (the tick
8008+
// gutters place labels in the outer half of the edge pixels through this).
8009+
// `arr` is monotonic, ascending or descending (origin='lower' reverses y).
8010+
function _axisValToImg2d(arr, v, n) {
8011+
const m = arr.length - 1;
8012+
if (m < 1) return 0;
8013+
const asc = arr[m] >= arr[0];
8014+
// The last segment start at or before v — segment 0 below the range, m-1 above.
8015+
let lo = 0, hi = m - 1;
8016+
while (lo < hi) {
8017+
const mid = (lo + hi + 1) >> 1;
8018+
if (asc ? arr[mid] <= v : arr[mid] >= v) lo = mid; else hi = mid - 1;
8019+
}
8020+
const d = arr[lo + 1] - arr[lo];
8021+
const pos = lo + (d ? (v - arr[lo]) / d : 0);
8022+
return n > 1 ? pos * (n - 1) / m : 0;
8023+
}
8024+
80048025
function _attachEvents2d(p) {
80058026
const { overlayCanvas } = p;
80068027
let localOnly = false;

0 commit comments

Comments
 (0)