55from dataclasses import dataclass , field
66from functools import cached_property
77from itertools import islice
8- from types import SimpleNamespace as NS
98from typing import TYPE_CHECKING , cast
109from warnings import warn
1110
1211import numpy as np
1312import pandas as pd
1413
14+ from plotnine .iapi import guide_text
15+
1516from .._utils import remove_missing
1617from ..exceptions import PlotnineError , PlotnineWarning
1718from ..mapping .aes import rename_aesthetics
1819from .guide import GuideElements , guide
1920
2021if TYPE_CHECKING :
21- from typing import Any , Optional
22+ from typing import Any , Optional , Sequence
2223
2324 from matplotlib .artist import Artist
2425 from matplotlib .offsetbox import PackerBase
@@ -209,7 +210,7 @@ def _calculate_rows_and_cols(
209210 self , elements : GuideElementsLegend
210211 ) -> tuple [int , int ]:
211212 nrow , ncol = self .nrow , self .ncol
212- nbreak = len ( self .key )
213+ nbreak = self .num_breaks
213214
214215 if nrow and ncol :
215216 if nrow * ncol < nbreak :
@@ -248,7 +249,7 @@ def draw(self):
248249
249250 obverse = slice (0 , None )
250251 reverse = slice (None , None , - 1 )
251- nbreak = len ( self .key )
252+ nbreak = self .num_breaks
252253 targets = self .theme .targets
253254 keys_order = reverse if self .reverse else obverse
254255 elements = self .elements
@@ -259,8 +260,12 @@ def draw(self):
259260 targets .legend_title = title_box ._text # type: ignore
260261
261262 # labels
262- props = {"ha" : elements .text .ha , "va" : elements .text .va }
263- labels = [TextArea (s , textprops = props ) for s in self .key ["label" ]]
263+ has = elements .text .has
264+ vas = elements .text .vas
265+ labels = [
266+ TextArea (s , textprops = {"ha" : ha , "va" : va })
267+ for s , ha , va in zip (self .key ["label" ], has , vas )
268+ ]
264269 _texts = [l ._text for l in labels ] # type: ignore
265270 targets .legend_text_legend = _texts
266271
@@ -287,18 +292,28 @@ def draw(self):
287292 "bottom" : (VPacker , reverse ),
288293 "top" : (VPacker , obverse ),
289294 }
290- packer , slc = lookup [ elements . text_position ]
295+
291296 if self .elements .text .is_blank :
292297 key_boxes = [d for d in drawings ][keys_order ]
293298 else :
299+ packers , slices = [], []
300+ for side in elements .text_positions :
301+ tup = lookup [side ]
302+ packers .append (tup [0 ])
303+ slices .append (tup [1 ])
304+
305+ seps = elements .text .margins
306+ aligns = elements .text .aligns
294307 key_boxes = [
295308 packer (
296309 children = [l , d ][slc ],
297- sep = elements . text . margin ,
298- align = elements . text . align ,
310+ sep = sep ,
311+ align = align ,
299312 pad = 0 ,
300313 )
301- for d , l in zip (drawings , labels )
314+ for d , l , packer , slc , sep , align in zip (
315+ drawings , labels , packers , slices , seps , aligns
316+ )
302317 ][keys_order ]
303318
304319 # Put the entries together in rows or columns
@@ -326,7 +341,7 @@ def draw(self):
326341 break
327342
328343 chunk_boxes : list [Artist ] = [
329- packer_dim1 (children = chunk , align = "left " , sep = sep1 , pad = 0 )
344+ packer_dim1 (children = chunk , align = "right " , sep = sep1 , pad = 0 )
330345 for chunk in chunks
331346 ]
332347
@@ -364,26 +379,38 @@ def text(self):
364379 ha = self .theme .getp (("legend_text_legend" , "ha" ), "center" )
365380 va = self .theme .getp (("legend_text_legend" , "va" ), "center" )
366381 is_blank = self .theme .T .is_blank ("legend_text_legend" )
382+ n = self .guide .num_breaks
367383
368384 # The original ha & va values are used by the HPacker/VPacker
369385 # to align the TextArea with the DrawingArea.
370386 # We set ha & va to values that combine best with the aligning
371387 # for the text area.
372- align = va if self .text_position in {"left" , "right" } else ha
373- return NS (
374- margin = self ._text_margin ,
375- align = align ,
388+ has = (ha ,) * n if isinstance (ha , str ) else ha
389+ vas = (va ,) * n if isinstance (va , str ) else va
390+ aligns = [
391+ va if side in ("right" , "left" ) else ha
392+ for side , ha , va in zip (self .text_positions , has , vas )
393+ ]
394+ return guide_text (
395+ margins = self ._text_margin ,
396+ aligns = aligns , # pyright: ignore[reportArgumentType]
376397 fontsize = size ,
377- ha = "center" ,
378- va = "baseline" ,
398+ has = ( "center" ,) * n ,
399+ vas = ( "baseline" ,) * n ,
379400 is_blank = is_blank ,
380401 )
381402
382403 @cached_property
383- def text_position (self ) -> Side :
384- if not (pos := self .theme .getp ("legend_text_position" )):
385- pos = "right"
386- return pos
404+ def text_positions (self ) -> Sequence [Side ]:
405+ if not (position := self .theme .getp ("legend_text_position" )):
406+ return ("right" ,) * self .guide .num_breaks
407+
408+ position = cast ("Side | Sequence[Side]" , position )
409+
410+ if isinstance (position , str ):
411+ position = (position ,) * self .guide .num_breaks
412+
413+ return position
387414
388415 @cached_property
389416 def key_spacing_x (self ) -> float :
0 commit comments