@@ -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" ])
531555def 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