Skip to content

Pointer events in the pixel-centre convention; bold and outlined text markers - #73

Merged
CSSFrancis merged 1 commit into
mainfrom
fix/issues-66-72
Sep 14, 2026
Merged

CSSFrancis merged 1 commit into
mainfrom
fix/issues-66-72

Conversation

@CSSFrancis

@CSSFrancis CSSFrancis commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes #72. Closes #66.

#72 — pointer coordinates are half a pixel off

A real click in the lower right of pixel (5, 3): main reports (5.80, 3.80), which rounds to pixel (6, 4); this PR reports (5.30, 3.30), which rounds to (5, 3)

The same physical click (cyan cross) recorded from the main renderer and from this branch. The red square is the pixel round(event.img_x), round(event.img_y) picks.

_imgToCanvas2d puts image coordinate i at the centre of pixel i, the same convention marker offsets, widget positions and Python's display_to_data use. _canvasToImg2d skipped the matching -0.5, so the two weren't inverses. Every 2-D pointer event read half a pixel right and down of anything drawn at the same spot.

  • _canvasToImg2d now subtracts the 0.5. The round trip is exact at every zoom and pan (it was off by 0.5 before).
  • Two small helpers keep the convention in one place: _imgPix2d (floor(v + 0.5)) gives the pixel index and _inImgAxis2d checks bounds. The readout, the value probe, the brush bounds and _inImage2d all use them. _pixelValue2d adds the 0.5 back itself, because it addresses texels and the detail tile by pixel edge.
  • xdata/ydata for pointer_down, double_click, pointer_settled, key_down and the readout now come from _imgToAxisVal2d:
    • imshow (one axis value per pixel centre): pixel i reads exactly x_axis[i]. Past the first or last pixel centre the value extends linearly instead of clamping.
    • pcolormesh (cell edges): uses (i + 0.5) / n, which gives exactly the values it reported before, so mesh xdata doesn't move.
  • The cursor-anchored wheel zoom adds the 0.5 back, so zooming still keeps the image point under the cursor fixed.
  • Side effect: brush strokes now land under the cursor instead of half a pixel up-left of it.

This is a behaviour change, so it has an api_change fragment. Handlers should use round(event.img_x) to get a pixel index. int() no longer does that, and docs/events.rst used it, so the docs are updated. The bundled examples already used round().

Not changed: the imshow tick gutters still stretch x_axis[0]x_axis[-1] from the image's left edge to its right edge, not from the first pixel centre to the last. A tick label can therefore sit up to half a pixel from the xdata of a click at the same spot. That mapping was already inconsistent with marker placement. Fixing it moves tick positions, so it is a separate stacked PR: #74. That PR also turned up a letterbox bug in the gutters and the missing origin='lower' y ticks.

#66 — bold and outlined text markers

add_texts (Plot2D and Plot1D) and add_text gain three options:

  • fontweight: "normal" (default), "bold", or a CSS numeric weight in [1, 1000]. The name matches fontsize, following matplotlib.
  • outline_color: a halo colour, None by default. The halo is drawn with strokeText under fillText, with round joins.
  • outline_width: default 3.0. Roughly half of it shows outside the glyphs.

The options are validated on creation and on MarkerGroup.set, so a bad value is refused before it reaches the group. The 1-D and 2-D renderers share _markerTextStyle / _drawMarkerText. Plot2D's add_texts also gets the docstring it was missing.

plot.add_texts([[40, 12]], ["2 Å"], fontsize=16, fontweight="bold",
               color="#ffffff", outline_color="#000000")

Three white labels over high-contrast rings: plain (the only option before), bold, and bold with a black outline

Tests

  • test_plot2d/test_pointer_pixel_centre.py: round trip of the extracted JS helpers across zoom and pan, the imshow and mesh axis mapping, and real Playwright clicks placed via data_to_display. The three click tests fail on main and pass here.
  • test_markers/test_text_style.py: wire format, validation, live set, and screenshot checks on 1-D and 2-D panels. Bold puts down more ink; the outline paints its own colour while the fill stays on top.
  • FIGURE_ESM.md anchors are updated to the current line numbers. They were uniformly one line off on main.
  • Full suite run locally (Windows, Chromium): 2438 passed, 58 skipped.

https://claude.ai/code/session_01EUrvzeXdzNjPKBTk5jCtp1

… markers

GH #72: _canvasToImg2d was not the inverse of _imgToCanvas2d. The forward
map puts image coordinate i at the centre of pixel i -- the convention
marker offsets, widget positions and Python's display_to_data share -- but
the inverse skipped the matching -0.5, so every 2-D pointer event read half
a pixel right and down of anything drawn at the same spot and
round(event.img_x) named pixel i+1 over the right half of pixel i. It now
subtracts the 0.5; _imgPix2d / _inImgAxis2d carry the index and bounds rule
to the readout, value probe and brush, and _pixelValue2d adds the 0.5 back
where texels and the detail tile are addressed by edge. The wheel-zoom
anchor compensates so the point under the cursor still stays put, and brush
strokes now land under the cursor.

xdata/ydata go through _imgToAxisVal2d: imshow axes hold one value per
pixel centre, so pixel i reads exactly x_axis[i] (continued linearly over
the outer half of the edge pixels rather than clamped); pcolormesh axes are
cell edges and keep the (i + 0.5) / n mapping, i.e. the values they already
reported.

GH #66: add_texts / add_text take fontweight ('normal' | 'bold' | CSS
number) and outline_color / outline_width, a halo stroked under the fill so
a label stays legible on light and dark ground. Validated on creation and
on MarkerGroup.set; the 1-D and 2-D renderers share _markerTextStyle /
_drawMarkerText.

FIGURE_ESM.md anchors are set to the current line numbers (they were one
line off throughout on main).

Claude-Session: https://claude.ai/code/session_01EUrvzeXdzNjPKBTk5jCtp1
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.16%. Comparing base (ce451b6) to head (6c93609).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #73      +/-   ##
==========================================
+ Coverage   91.12%   91.16%   +0.03%     
==========================================
  Files          41       41              
  Lines        4847     4865      +18     
==========================================
+ Hits         4417     4435      +18     
  Misses        430      430              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis
CSSFrancis merged commit ce22710 into main Sep 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_canvasToImg2d is not the inverse of _imgToCanvas2d (+0.5 px in pointer-event coordinates) add_texts: a weight (and outline) option for text markers

2 participants