Skip to content

fix: align raster frame with matplotlib pixel rows#2018

Merged
krystophny merged 1 commit into
mainfrom
parity/p3-raster-frame-offset
Jun 14, 2026
Merged

fix: align raster frame with matplotlib pixel rows#2018
krystophny merged 1 commit into
mainfrom
parity/p3-raster-frame-offset

Conversation

@krystophny

Copy link
Copy Markdown
Collaborator

Summary

The raster axes frame rendered one display pixel inward of matplotlib. The
plot-area layout already computes matplotlib-exact edges, but the pixel blend
converted device coordinate N to 1-based array element N, shifting every drawn
feature one display pixel toward the origin.

blend_pixel now maps device coordinate N to display pixel N (1-based array
element N+1). It is the single sub-pixel write chokepoint, so spines, ticks,
the data curve, and markers all shift together: their mutual registration is
preserved while the frame lands on matplotlib's exact rows and columns.

Before -> after vs matplotlib (default 640x480 figure, simple_plot)

Spine positions (0-indexed display rows/cols), measured against the matplotlib
reference render:

spine matplotlib before after
top (row) 58 57 58
bottom (row) 427 426 427
left (col) 80 79 80
right (col) 576 575 576

Footprint now matches matplotlib pixel-for-pixel: full black on the spine
pixel with symmetric light-gray antialiasing on both neighbours (e.g. top
spine col 300: row 57 gray, row 58 black, row 59 gray).

multi_line (800x600) spines likewise align exactly: top/bottom rows 72/534,
left/right cols 100/720 -- identical to matplotlib.

Major tick marks register with the corrected frame (interior ticks match
matplotlib columns exactly, e.g. 174, 246, 461, 533). The data curve moves the
same direction, improving curve-to-frame registration as well.

Recalibrated tests

Two tests hard-coded the old inward-biased pixel positions and are updated to
the matplotlib-correct values (not weakened):

  • test_dashdot_rendering: scanned display row 24 for a line drawn at device
    y=25. The line now renders on display row 25, so all three scan loops move to
    that row. Without the move the scan hit an empty row and the dash-dot vs
    dotted comparison saw two blank rows as identical.
  • test_suptitle: counted the subplot top spine inside a 1-based top band whose
    end (int(0.12*h)) excluded the now-correctly-registered spine by one pixel.
    Band end extended to int(0.12*h) + 1 to cover the matplotlib 12% region in
    1-based array coordinates.

Verification

Commands run:

  • fpm build -- compiled successfully.
  • fpm run --example basic_plots -- regenerated simple_plot/multi_line PNG+PDF.
  • make verify-artifacts -- "Artifact verification passed." (ylabel-left
    stripe gates mean ~1.0 vs 0.91 threshold; quiver geometry gate green).
  • make test -- "ALL TESTS PASSED (fpm test)".
  • make test-ci -- "CI essential test suite completed successfully".

Failing-before / passing-after (full make test):

  • before: test_dashdot_rendering -> "FAIL: dash-dot and dotted patterns are
    identical"; test_suptitle -> "FAIL: 1x3 suptitle is not clearly above the
    subplot grid".
  • after: PASS: patterns differ; dotted dark=169 dash-dot dark=281,
    All dash-dot rendering tests passed.; PASS: 1x3 suptitle renders in the top band, All suptitle tests PASSED!.

PDF backend is unchanged (vector path does not use the raster blend); pdftotext
on simple_plot.pdf still yields the title, axis labels, and tick labels
("Simple Sine Wave ... sin(x) ... 0 2 4 6 8 x 10 12").

The raster pixel blend converted device coordinate N to the 1-based array
element N, placing every feature one display pixel inward of matplotlib's
position. The plot-area layout already produces matplotlib-exact edges
(top spine y=58, bottom y=427, left x=80, right x=576 for the default
640x480 figure), but the bias rendered them at 57/426/79/575.

Map device coordinate N to display pixel N (1-based array element N+1) in
blend_pixel, the single sub-pixel write chokepoint. This shifts spines,
ticks, the data curve, and markers together, so their mutual registration
is preserved while the whole frame lands on matplotlib's rows and columns.

Recalibrate two tests that hard-coded the old inward-biased scan rows:
- test_dashdot_rendering scanned display row 24 for a line drawn at device
  y=25; the line now renders on row 25, so the scan row moves with it.
- test_suptitle counted the subplot top spine in a 1-based top band whose
  end excluded the now-correctly-registered spine by one pixel.
@krystophny krystophny merged commit 85de05c into main Jun 14, 2026
6 checks passed
@krystophny krystophny deleted the parity/p3-raster-frame-offset branch June 14, 2026 07:15
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.

1 participant