Skip to content

Commit 5d9f46c

Browse files
authored
fix: PNG backend: markers at data range boundary clipped by plot border (fixes #1683) (#1818)
## Summary Increase the data-range margin expansion from 2% to 5% so markers placed at the exact data-range boundary are pushed far enough inside the plot area to avoid visual overlap with the axes frame. ## Changes - `src/figures/fortplot_figure_rendering_pipeline.f90`: bump `PDF_DATA_RANGE_MARGIN` from `0.02_wp` to `0.05_wp` - `test/test_marker_boundary_clipping.f90`: new regression test placing markers at all four data-range corners and verifying each has sufficient visible pixel coverage ## Root Cause The `expand_data_range` subroutine inflates the coordinate system so boundary data points map inside the plot area rather than onto the axes frame. A 2 % inflation on a typical ~400 px plot height gives only ~8 px of clearance, which is barely enough for a 5–7 px marker radius. At that distance the 1 px axes frame line overlaps the outer edge of the marker, making it look "half-clipped". A 5 % inflation gives ~20 px clearance, which comfortably accommodates all built-in marker sizes. ## Verification ### Test passes after fix ``` $ fpm test --target test_marker_boundary_clipping PASS: all four corner markers fully visible corner pixel counts: 699 521 463 408 ``` ### Full test suite green ``` $ fpm test 2>&1 | grep -c "STOP 1\|<ERROR>" 0 ``` ### Affected examples regenerate cleanly - `make example ARGS="scatter_demo"` → scatter_basic.png, scatter_multi.png, scatter_gaussian.png - `make example ARGS="disconnected_lines"` → disconnected_lines.png, disconnected_lines.pdf Artifact file sizes are consistent with pre-change renders (no regression in output volume).
1 parent 03cb580 commit 5d9f46c

2 files changed

Lines changed: 116 additions & 4 deletions

File tree

src/figures/fortplot_figure_rendering_pipeline.f90

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ module fortplot_figure_rendering_pipeline
4747
public :: render_figure_axes_labels_only, render_title_only
4848
public :: render_polar_axes
4949

50-
real(wp), parameter :: PDF_DATA_RANGE_MARGIN = 0.02_wp
50+
real(wp), parameter :: DATA_RANGE_MARGIN = 0.05_wp
5151

5252
contains
5353

@@ -85,8 +85,8 @@ subroutine setup_coordinate_system(backend, x_min_transformed, x_max_transformed
8585
end subroutine setup_coordinate_system
8686

8787
subroutine expand_data_range(data_min, data_max, expanded_min, expanded_max)
88-
!! Expand a data range by PDF_DATA_RANGE_MARGIN on each side,
89-
!! keeping the range center fixed. Prevents data at exact boundaries
88+
!! Expand a data range by DATA_RANGE_MARGIN (5%) on each side,
89+
!! keeping the range center fixed. Prevents markers at exact boundaries
9090
!! from being clipped by the plot frame stroke.
9191
real(wp), intent(in) :: data_min, data_max
9292
real(wp), intent(out) :: expanded_min, expanded_max
@@ -100,7 +100,7 @@ subroutine expand_data_range(data_min, data_max, expanded_min, expanded_max)
100100

101101
center = 0.5_wp*(data_min + data_max)
102102
half_range = 0.5_wp*(data_max - data_min)
103-
half_range = half_range*(1.0_wp + PDF_DATA_RANGE_MARGIN)
103+
half_range = half_range*(1.0_wp + DATA_RANGE_MARGIN)
104104

105105
expanded_min = center - half_range
106106
expanded_max = center + half_range
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
program test_marker_boundary_clipping
2+
!! Regression test for issue #1683: markers at data range boundary
3+
!! should not be clipped by the plot border.
4+
!!
5+
!! The fix increases DATA_RANGE_MARGIN from 2% to 5% so that
6+
!! markers at the exact data boundary are pushed far enough inside
7+
!! the plot area to avoid overlap with the 1-pixel axes frame.
8+
use, intrinsic :: iso_fortran_env, only: wp => real64, error_unit
9+
use fortplot_figure_core, only: figure_t
10+
implicit none
11+
12+
integer, parameter :: w = 400
13+
integer, parameter :: h = 300
14+
real(wp) :: rgb(w, h, 3)
15+
integer :: corner_pixels(4)
16+
integer :: i
17+
18+
! Render four markers, one at each data-range corner
19+
call render_four_corners(rgb)
20+
21+
! Count dark pixels in each corner region of the plot area.
22+
! The plot area is roughly central; corners are offset from image edges.
23+
! With sufficient margin, each corner marker should have a full set
24+
! of pixels. With insufficient margin, the marker near the axes
25+
! frame would have fewer visible pixels (clipped by the frame line).
26+
call count_corner_pixels(rgb, corner_pixels)
27+
28+
do i = 1, 4
29+
if (corner_pixels(i) < 200) then
30+
write (error_unit, *) "FAIL: corner ", i, &
31+
" has only ", corner_pixels(i), &
32+
" marker pixels (expected >= 200)"
33+
write (error_unit, *) "INFO: marker at data boundary may be clipped by plot border"
34+
stop 1
35+
end if
36+
end do
37+
38+
print *, "PASS: all four corner markers fully visible"
39+
print *, " corner pixel counts:", corner_pixels
40+
41+
contains
42+
43+
subroutine render_four_corners(rgb)
44+
real(wp), intent(out) :: rgb(w, h, 3)
45+
type(figure_t) :: fig
46+
real(wp) :: x(4), y(4)
47+
48+
! Markers at all four corners of the data range [0,1] x [0,1]
49+
x = [0.0_wp, 1.0_wp, 0.0_wp, 1.0_wp]
50+
y = [0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp]
51+
52+
call fig%initialize(width=w, height=h, backend='png')
53+
call fig%set_xlim(0.0_wp, 1.0_wp)
54+
call fig%set_ylim(0.0_wp, 1.0_wp)
55+
56+
call fig%scatter(x, y, marker='o', facecolor=[0.0_wp, 0.0_wp, 0.0_wp], &
57+
edgecolor=[0.0_wp, 0.0_wp, 0.0_wp])
58+
59+
call fig%extract_rgb_data_for_animation(rgb)
60+
end subroutine render_four_corners
61+
62+
subroutine count_corner_pixels(rgb, counts)
63+
real(wp), intent(in) :: rgb(w, h, 3)
64+
integer, intent(out) :: counts(4)
65+
integer :: i, j
66+
real(wp) :: lum
67+
68+
! Define search regions for each corner marker.
69+
! These are approximate regions inside the plot area where the
70+
! corner markers should appear after margin expansion.
71+
!
72+
! Corner 1: bottom-left (data 0,0) -> image bottom-left of plot area
73+
! Corner 2: bottom-right (data 1,0) -> image bottom-right of plot area
74+
! Corner 3: top-left (data 0,1) -> image top-left of plot area
75+
! Corner 4: top-right (data 1,1) -> image top-right of plot area
76+
77+
counts = 0
78+
79+
! Corner 1: bottom-left region
80+
do j = 3*h/4, h - 10
81+
do i = 10, w/4
82+
lum = (rgb(i, j, 1) + rgb(i, j, 2) + rgb(i, j, 3)) / 3.0_wp
83+
if (lum < 0.5_wp) counts(1) = counts(1) + 1
84+
end do
85+
end do
86+
87+
! Corner 2: bottom-right region
88+
do j = 3*h/4, h - 10
89+
do i = 3*w/4, w - 10
90+
lum = (rgb(i, j, 1) + rgb(i, j, 2) + rgb(i, j, 3)) / 3.0_wp
91+
if (lum < 0.5_wp) counts(2) = counts(2) + 1
92+
end do
93+
end do
94+
95+
! Corner 3: top-left region
96+
do j = 10, h/4
97+
do i = 10, w/4
98+
lum = (rgb(i, j, 1) + rgb(i, j, 2) + rgb(i, j, 3)) / 3.0_wp
99+
if (lum < 0.5_wp) counts(3) = counts(3) + 1
100+
end do
101+
end do
102+
103+
! Corner 4: top-right region
104+
do j = 10, h/4
105+
do i = 3*w/4, w - 10
106+
lum = (rgb(i, j, 1) + rgb(i, j, 2) + rgb(i, j, 3)) / 3.0_wp
107+
if (lum < 0.5_wp) counts(4) = counts(4) + 1
108+
end do
109+
end do
110+
end subroutine count_corner_pixels
111+
112+
end program test_marker_boundary_clipping

0 commit comments

Comments
 (0)