Skip to content

Commit 1c3e605

Browse files
authored
Add tests for scatter PDF optimization with colored markers
1 parent 1f3b3c3 commit 1c3e605

1 file changed

Lines changed: 115 additions & 6 deletions

File tree

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,9 @@ def test_scatter_offaxis_colored_pdf_size():
507507
size_offaxis_colored = buf1.tell()
508508
plt.close(fig1)
509509

510-
# Test 2: Empty scatter (baseline - smallest possible)
510+
# Test 2: Empty scatter (baseline - accounts for scatter call overhead)
511511
fig2, ax2 = plt.subplots()
512+
ax2.scatter([], []) # Empty scatter to match the axes structure
512513
ax2.set_xlim(20, 30)
513514
ax2.set_ylim(20, 30)
514515

@@ -517,15 +518,38 @@ def test_scatter_offaxis_colored_pdf_size():
517518
size_empty = buf2.tell()
518519
plt.close(fig2)
519520

520-
# The off-axis colored scatter should be close to empty size
521-
# Allow up to 50KB overhead for axes/metadata, but should be much smaller
522-
# than if all 1000 markers were written (which would add ~200-400KB)
523-
assert size_offaxis_colored < size_empty + 50_000, (
521+
# Test 3: Scatter with visible markers (should be much larger)
522+
fig3, ax3 = plt.subplots()
523+
ax3.scatter(x + 20, y + 20, c=c) # Shift points to be visible
524+
ax3.set_xlim(20, 30)
525+
ax3.set_ylim(20, 30)
526+
527+
buf3 = io.BytesIO()
528+
fig3.savefig(buf3, format='pdf')
529+
size_visible = buf3.tell()
530+
plt.close(fig3)
531+
532+
# The off-axis colored scatter should be close to empty size.
533+
# Since the axes are identical, the difference should be minimal
534+
# (just the scatter collection setup, no actual marker data).
535+
# Use a tight tolerance since axes output is identical.
536+
assert size_offaxis_colored < size_empty + 5_000, (
524537
f"Off-axis colored scatter PDF ({size_offaxis_colored} bytes) is too large. "
525-
f"Expected close to empty figure size ({size_empty} bytes). "
538+
f"Expected close to empty scatter size ({size_empty} bytes). "
526539
f"Markers may not be properly skipped."
527540
)
528541

542+
# The visible scatter should be significantly larger than both empty and
543+
# off-axis, demonstrating the optimization is working.
544+
assert size_visible > size_empty + 15_000, (
545+
f"Visible scatter PDF ({size_visible} bytes) should be much larger "
546+
f"than empty ({size_empty} bytes) to validate the test."
547+
)
548+
assert size_visible > size_offaxis_colored + 15_000, (
549+
f"Visible scatter PDF ({size_visible} bytes) should be much larger "
550+
f"than off-axis ({size_offaxis_colored} bytes) to validate optimization."
551+
)
552+
529553

530554
@check_figures_equal(extensions=["pdf"])
531555
def test_scatter_offaxis_colored_visual(fig_test, fig_ref):
@@ -587,3 +611,88 @@ def test_scatter_mixed_onoff_axis(fig_test, fig_ref):
587611
ax_ref.scatter(x_on, y_on, c=c[:n_points], s=50)
588612
ax_ref.set_xlim(0, 10)
589613
ax_ref.set_ylim(0, 10)
614+
615+
616+
@check_figures_equal(extensions=["pdf"])
617+
def test_scatter_large_markers_partial_clip(fig_test, fig_ref):
618+
"""
619+
Test that large markers are rendered when partially visible.
620+
621+
Addresses reviewer concern: markers with centers outside the canvas but
622+
with edges extending into the visible area should still be rendered.
623+
"""
624+
# Create markers just outside the visible area
625+
# Canvas is 0-10, markers at x=-0.5 and x=10.5
626+
x = np.array([-0.5, 10.5, 5]) # left edge, right edge, center
627+
y = np.array([5, 5, -0.5]) # center, center, bottom edge
628+
c = np.array([0.2, 0.5, 0.8])
629+
630+
# Test figure: large markers (s=500 ≈ 11 points radius)
631+
# Centers are outside, but marker edges extend into visible area
632+
ax_test = fig_test.subplots()
633+
ax_test.scatter(x, y, c=c, s=500)
634+
ax_test.set_xlim(0, 10)
635+
ax_test.set_ylim(0, 10)
636+
637+
# Reference figure: same plot (should render identically)
638+
ax_ref = fig_ref.subplots()
639+
ax_ref.scatter(x, y, c=c, s=500)
640+
ax_ref.set_xlim(0, 10)
641+
ax_ref.set_ylim(0, 10)
642+
643+
644+
@check_figures_equal(extensions=["pdf"])
645+
def test_scatter_logscale(fig_test, fig_ref):
646+
"""
647+
Test scatter optimization with logarithmic scales.
648+
649+
Ensures bounds checking works correctly in log-transformed coordinates.
650+
"""
651+
rng = np.random.default_rng(19680801)
652+
653+
# Create points across several orders of magnitude
654+
n_points = 50
655+
x = 10 ** (rng.random(n_points) * 4) # 1 to 10000
656+
y = 10 ** (rng.random(n_points) * 4)
657+
c = rng.random(n_points)
658+
659+
# Test figure: log scale with points mostly outside view
660+
ax_test = fig_test.subplots()
661+
ax_test.scatter(x, y, c=c, s=50)
662+
ax_test.set_xscale('log')
663+
ax_test.set_yscale('log')
664+
ax_test.set_xlim(100, 1000) # Only show middle range
665+
ax_test.set_ylim(100, 1000)
666+
667+
# Reference figure: should render identically
668+
ax_ref = fig_ref.subplots()
669+
ax_ref.scatter(x, y, c=c, s=50)
670+
ax_ref.set_xscale('log')
671+
ax_ref.set_yscale('log')
672+
ax_ref.set_xlim(100, 1000)
673+
ax_ref.set_ylim(100, 1000)
674+
675+
676+
@check_figures_equal(extensions=["pdf"])
677+
def test_scatter_polar(fig_test, fig_ref):
678+
"""
679+
Test scatter optimization with polar coordinates.
680+
681+
Ensures bounds checking works correctly in polar projections.
682+
"""
683+
rng = np.random.default_rng(19680801)
684+
685+
n_points = 50
686+
theta = rng.random(n_points) * 2 * np.pi
687+
r = rng.random(n_points) * 3
688+
c = rng.random(n_points)
689+
690+
# Test figure: polar projection
691+
ax_test = fig_test.subplots(subplot_kw={'projection': 'polar'})
692+
ax_test.scatter(theta, r, c=c, s=50)
693+
ax_test.set_ylim(0, 2) # Limit radial range
694+
695+
# Reference figure: should render identically
696+
ax_ref = fig_ref.subplots(subplot_kw={'projection': 'polar'})
697+
ax_ref.scatter(theta, r, c=c, s=50)
698+
ax_ref.set_ylim(0, 2)

0 commit comments

Comments
 (0)