@@ -395,6 +395,7 @@ def _fit_path_to_handlebox(
395395 width : float ,
396396 height : float ,
397397 pad : float = 0.08 ,
398+ preserve_aspect : bool = True ,
398399) -> mpath .Path :
399400 """
400401 Normalize an arbitrary path into the legend-handle box.
@@ -411,11 +412,15 @@ def _fit_path_to_handlebox(
411412 py = max (height * pad , 0.0 )
412413 span_x = max (width - 2 * px , 1e-12 )
413414 span_y = max (height - 2 * py , 1e-12 )
414- scale = min (span_x / dx , span_y / dy )
415415 cx = - xdescent + width * 0.5
416416 cy = - ydescent + height * 0.5
417- verts [finite , 0 ] = (verts [finite , 0 ] - (xmin + xmax ) * 0.5 ) * scale + cx
418- verts [finite , 1 ] = (verts [finite , 1 ] - (ymin + ymax ) * 0.5 ) * scale + cy
417+ if preserve_aspect :
418+ scale_x = scale_y = min (span_x / dx , span_y / dy )
419+ else :
420+ scale_x = span_x / dx
421+ scale_y = span_y / dy
422+ verts [finite , 0 ] = (verts [finite , 0 ] - (xmin + xmax ) * 0.5 ) * scale_x + cx
423+ verts [finite , 1 ] = (verts [finite , 1 ] - (ymin + ymax ) * 0.5 ) * scale_y + cy
419424 return mpath .Path (
420425 verts , None if path .codes is None else np .array (path .codes , copy = True )
421426 )
@@ -494,6 +499,89 @@ def _patch_joinstyle(value: Any, default: str = _DEFAULT_GEO_JOINSTYLE) -> str:
494499 return default
495500
496501
502+ def _patch_color (
503+ orig_handle : Any ,
504+ prop : str ,
505+ default : Any = None ,
506+ ) -> Any :
507+ """
508+ Resolve a patch color, preferring the artist's original color spec.
509+
510+ Collection-like artists often report post-alpha RGBA arrays from
511+ `get_facecolor()` / `get_edgecolor()`. If we then also copy `alpha`, the
512+ legend proxy ends up visually double-dimmed. Prefer the original color
513+ attributes when available so patch proxies can apply alpha once.
514+ """
515+ original = getattr (orig_handle , f"_original_{ prop } " , None )
516+ if original is not None :
517+ value = _first_scalar (original , default = None )
518+ if value is not None :
519+ return value
520+ getter = getattr (orig_handle , f"get_{ prop } " , None )
521+ if not callable (getter ):
522+ return default
523+ try :
524+ value = getter ()
525+ except Exception :
526+ return default
527+ return _first_scalar (value , default = default )
528+
529+
530+ _PATCH_STYLE_PROP_SPECS = {
531+ "facecolor" : {"default" : "none" , "transform" : None },
532+ "edgecolor" : {"default" : "none" , "transform" : None },
533+ "linewidth" : {"default" : 0.0 , "transform" : _first_scalar },
534+ "linestyle" : {"default" : None , "transform" : _first_scalar },
535+ "hatch" : {"default" : None , "transform" : None },
536+ "hatch_linewidth" : {"default" : None , "transform" : None },
537+ "fill" : {"default" : None , "transform" : None },
538+ "alpha" : {"default" : None , "transform" : None },
539+ "capstyle" : {"default" : None , "transform" : None },
540+ }
541+
542+
543+ def _copy_patch_style (
544+ legend_handle : mpatches .Patch ,
545+ orig_handle : Any ,
546+ * ,
547+ joinstyle_default : str = _DEFAULT_GEO_JOINSTYLE ,
548+ ) -> None :
549+ """
550+ Copy common patch-style properties from source artist to legend proxy.
551+
552+ Matplotlib does not provide a reliable generic style-transfer API for
553+ cross-family artists here. In particular, `Artist.update_from()` is not
554+ safe for `Collection -> Patch` copies like `FeatureArtist -> PathPatch`,
555+ and `properties()` still leaves us to normalize collection-valued fields.
556+ So this helper intentionally copies the shared patch-style surface only.
557+ """
558+ for prop , spec in _PATCH_STYLE_PROP_SPECS .items ():
559+ setter = getattr (legend_handle , f"set_{ prop } " , None )
560+ if not callable (setter ):
561+ continue
562+ default = spec ["default" ]
563+ if prop in ("facecolor" , "edgecolor" ):
564+ value = _patch_color (orig_handle , prop , default = default )
565+ else :
566+ getter = getattr (orig_handle , f"get_{ prop } " , None )
567+ if not callable (getter ):
568+ continue
569+ try :
570+ value = getter ()
571+ except Exception :
572+ continue
573+ transform = spec ["transform" ]
574+ if transform is not None :
575+ value = transform (value , default = default )
576+ elif value is None :
577+ value = default
578+ if value is not None :
579+ setter (value )
580+ legend_handle .set_joinstyle (
581+ _patch_joinstyle (orig_handle , default = joinstyle_default )
582+ )
583+
584+
497585def _feature_legend_patch (
498586 legend ,
499587 orig_handle ,
@@ -515,6 +603,7 @@ def _feature_legend_patch(
515603 ydescent = ydescent ,
516604 width = width ,
517605 height = height ,
606+ preserve_aspect = False ,
518607 )
519608 return mpatches .PathPatch (path , joinstyle = _DEFAULT_GEO_JOINSTYLE )
520609
@@ -544,6 +633,7 @@ def _shapely_geometry_patch(
544633 ydescent = ydescent ,
545634 width = width ,
546635 height = height ,
636+ preserve_aspect = False ,
547637 )
548638 return mpatches .PathPatch (path , joinstyle = _DEFAULT_GEO_JOINSTYLE )
549639
@@ -566,6 +656,7 @@ def _geometry_entry_patch(
566656 ydescent = ydescent ,
567657 width = width ,
568658 height = height ,
659+ preserve_aspect = True ,
569660 )
570661 return mpatches .PathPatch (path , joinstyle = _DEFAULT_GEO_JOINSTYLE )
571662
@@ -579,36 +670,7 @@ def __init__(self):
579670 super ().__init__ (patch_func = _feature_legend_patch )
580671
581672 def update_prop (self , legend_handle , orig_handle , legend ):
582- facecolor = _first_scalar (
583- (
584- orig_handle .get_facecolor ()
585- if hasattr (orig_handle , "get_facecolor" )
586- else None
587- ),
588- default = "none" ,
589- )
590- edgecolor = _first_scalar (
591- (
592- orig_handle .get_edgecolor ()
593- if hasattr (orig_handle , "get_edgecolor" )
594- else None
595- ),
596- default = "none" ,
597- )
598- linewidth = _first_scalar (
599- (
600- orig_handle .get_linewidth ()
601- if hasattr (orig_handle , "get_linewidth" )
602- else None
603- ),
604- default = 0.0 ,
605- )
606- legend_handle .set_facecolor (facecolor )
607- legend_handle .set_edgecolor (edgecolor )
608- legend_handle .set_linewidth (linewidth )
609- legend_handle .set_joinstyle (_patch_joinstyle (orig_handle ))
610- if hasattr (orig_handle , "get_alpha" ):
611- legend_handle .set_alpha (orig_handle .get_alpha ())
673+ _copy_patch_style (legend_handle , orig_handle )
612674 legend ._set_artist_props (legend_handle )
613675 legend_handle .set_clip_box (None )
614676 legend_handle .set_clip_path (None )
@@ -1638,6 +1700,14 @@ def geolegend(
16381700 ):
16391701 """
16401702 Build geometry legend entries and optionally draw a legend.
1703+
1704+ Notes
1705+ -----
1706+ Geometry legend entries use normalized patch proxies inside the legend
1707+ handle box rather than reusing the original map artist directly. This
1708+ preserves the general geometry shape and copied patch styling, but very
1709+ small or high-aspect-ratio handles can still make hatches difficult to
1710+ read at legend scale.
16411711 """
16421712 facecolor = _not_none (facecolor , rc ["legend.geo.facecolor" ])
16431713 edgecolor = _not_none (edgecolor , rc ["legend.geo.edgecolor" ])
0 commit comments