plotnine 0.15.0.dev1__py3-none-any.whl → 0.15.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -361,9 +361,9 @@ class LayoutItems:
361
361
  ]
362
362
  return self.calc.max_width(artists)
363
363
 
364
- def axis_ticks_x_max_height(self, location: AxesLocation) -> float:
364
+ def axis_ticks_x_max_height_at(self, location: AxesLocation) -> float:
365
365
  """
366
- Return maximum height[inches] of x ticks
366
+ Return maximum height[figure space] of x ticks
367
367
  """
368
368
  heights = [
369
369
  self.calc.tight_height(tick.tick1line)
@@ -372,22 +372,31 @@ class LayoutItems:
372
372
  ]
373
373
  return max(heights) if len(heights) else 0
374
374
 
375
- def axis_text_x_max_height(self, location: AxesLocation) -> float:
375
+ def axis_text_x_max_height(self, ax: Axes) -> float:
376
376
  """
377
- Return maximum height[inches] of x tick labels
377
+ Return maximum height[figure space] of x tick labels
378
378
  """
379
379
  heights = [
380
380
  self.calc.tight_height(label) + pad
381
- for ax in self._filter_axes(location)
382
381
  for label, pad in zip(
383
382
  self.axis_text_x(ax), self.axis_text_x_margin(ax)
384
383
  )
385
384
  ]
386
385
  return max(heights) if len(heights) else 0
387
386
 
388
- def axis_ticks_y_max_width(self, location: AxesLocation) -> float:
387
+ def axis_text_x_max_height_at(self, location: AxesLocation) -> float:
388
+ """
389
+ Return maximum height[figure space] of x tick labels
390
+ """
391
+ heights = [
392
+ self.axis_text_x_max_height(ax)
393
+ for ax in self._filter_axes(location)
394
+ ]
395
+ return max(heights) if len(heights) else 0
396
+
397
+ def axis_ticks_y_max_width_at(self, location: AxesLocation) -> float:
389
398
  """
390
- Return maximum width[inches] of y ticks
399
+ Return maximum width[figure space] of y ticks
391
400
  """
392
401
  widths = [
393
402
  self.calc.tight_width(tick.tick1line)
@@ -396,22 +405,31 @@ class LayoutItems:
396
405
  ]
397
406
  return max(widths) if len(widths) else 0
398
407
 
399
- def axis_text_y_max_width(self, location: AxesLocation) -> float:
408
+ def axis_text_y_max_width(self, ax: Axes) -> float:
400
409
  """
401
- Return maximum width[inches] of y tick labels
410
+ Return maximum width[figure space] of y tick labels
402
411
  """
403
412
  widths = [
404
413
  self.calc.tight_width(label) + pad
405
- for ax in self._filter_axes(location)
406
414
  for label, pad in zip(
407
415
  self.axis_text_y(ax), self.axis_text_y_margin(ax)
408
416
  )
409
417
  ]
410
418
  return max(widths) if len(widths) else 0
411
419
 
420
+ def axis_text_y_max_width_at(self, location: AxesLocation) -> float:
421
+ """
422
+ Return maximum width[figure space] of y tick labels
423
+ """
424
+ widths = [
425
+ self.axis_text_y_max_width(ax)
426
+ for ax in self._filter_axes(location)
427
+ ]
428
+ return max(widths) if len(widths) else 0
429
+
412
430
  def axis_text_y_top_protrusion(self, location: AxesLocation) -> float:
413
431
  """
414
- Return maximum height[inches] above the axes of y tick labels
432
+ Return maximum height[figure space] above the axes of y tick labels
415
433
  """
416
434
  extras = []
417
435
  for ax in self._filter_axes(location):
@@ -424,7 +442,7 @@ class LayoutItems:
424
442
 
425
443
  def axis_text_y_bottom_protrusion(self, location: AxesLocation) -> float:
426
444
  """
427
- Return maximum height[inches] below the axes of y tick labels
445
+ Return maximum height[figure space] below the axes of y tick labels
428
446
  """
429
447
  extras = []
430
448
  for ax in self._filter_axes(location):
@@ -438,7 +456,7 @@ class LayoutItems:
438
456
 
439
457
  def axis_text_x_left_protrusion(self, location: AxesLocation) -> float:
440
458
  """
441
- Return maximum width[inches] of x tick labels to the left of the axes
459
+ Return maximum width[figure space] left of the axes of x tick labels
442
460
  """
443
461
  extras = []
444
462
  for ax in self._filter_axes(location):
@@ -452,7 +470,7 @@ class LayoutItems:
452
470
 
453
471
  def axis_text_x_right_protrusion(self, location: AxesLocation) -> float:
454
472
  """
455
- Return maximum width[inches] of x tick labels to the right of the axes
473
+ Return maximum width[figure space] right of the axes of y tick labels
456
474
  """
457
475
  extras = []
458
476
  for ax in self._filter_axes(location):
@@ -470,6 +488,7 @@ class LayoutItems:
470
488
  theme = self.plot.theme
471
489
  plot_title_position = theme.getp("plot_title_position", "panel")
472
490
  plot_caption_position = theme.getp("plot_caption_position", "panel")
491
+ justify = TextJustifier(spaces)
473
492
 
474
493
  if self.plot_tag:
475
494
  set_plot_tag_position(self.plot_tag, spaces)
@@ -477,37 +496,124 @@ class LayoutItems:
477
496
  if self.plot_title:
478
497
  ha = theme.getp(("plot_title", "ha"))
479
498
  self.plot_title.set_y(spaces.t.y2("plot_title"))
480
- horizontally_align_text(
481
- self.plot_title, ha, spaces, plot_title_position
499
+ justify.horizontally_about(
500
+ self.plot_title, ha, plot_title_position
482
501
  )
483
502
 
484
503
  if self.plot_subtitle:
485
504
  ha = theme.getp(("plot_subtitle", "ha"))
486
505
  self.plot_subtitle.set_y(spaces.t.y2("plot_subtitle"))
487
- horizontally_align_text(
488
- self.plot_subtitle, ha, spaces, plot_title_position
506
+ justify.horizontally_about(
507
+ self.plot_subtitle, ha, plot_title_position
489
508
  )
490
509
 
491
510
  if self.plot_caption:
492
511
  ha = theme.getp(("plot_caption", "ha"), "right")
493
512
  self.plot_caption.set_y(spaces.b.y1("plot_caption"))
494
- horizontally_align_text(
495
- self.plot_caption, ha, spaces, plot_caption_position
513
+ justify.horizontally_about(
514
+ self.plot_caption, ha, plot_caption_position
496
515
  )
497
516
 
498
517
  if self.axis_title_x:
499
518
  ha = theme.getp(("axis_title_x", "ha"), "center")
500
519
  self.axis_title_x.set_y(spaces.b.y1("axis_title_x"))
501
- horizontally_align_text(self.axis_title_x, ha, spaces)
520
+ justify.horizontally_about(self.axis_title_x, ha, "panel")
502
521
 
503
522
  if self.axis_title_y:
504
523
  va = theme.getp(("axis_title_y", "va"), "center")
505
524
  self.axis_title_y.set_x(spaces.l.x1("axis_title_y"))
506
- vertically_align_text(self.axis_title_y, va, spaces)
525
+ justify.vertically_about(self.axis_title_y, va, "panel")
507
526
 
508
527
  if self.legends:
509
528
  set_legends_position(self.legends, spaces)
510
529
 
530
+ self._adjust_axis_text_x(justify)
531
+ self._adjust_axis_text_y(justify)
532
+
533
+ def _adjust_axis_text_x(self, justify: TextJustifier):
534
+ """
535
+ Adjust x-axis text, justifying vertically as necessary
536
+ """
537
+
538
+ def to_vertical_axis_dimensions(value: float, ax: Axes) -> float:
539
+ """
540
+ Convert value in figure dimensions to axis dimensions
541
+ """
542
+ _, H = self.plot.figure.bbox.size
543
+ h = ax.get_window_extent().height
544
+ return value * H / h
545
+
546
+ if self._is_blank("axis_text_x"):
547
+ return
548
+
549
+ va = self.plot.theme.getp(("axis_text_x", "va"), "top")
550
+
551
+ for ax in self.plot.axs:
552
+ texts = list(self.axis_text_x(ax))
553
+ axis_text_row_height = to_vertical_axis_dimensions(
554
+ self.axis_text_x_max_height(ax), ax
555
+ )
556
+ for text in texts:
557
+ height = to_vertical_axis_dimensions(
558
+ self.calc.tight_height(text), ax
559
+ )
560
+ justify.vertically(
561
+ text, va, -axis_text_row_height, 0, height=height
562
+ )
563
+
564
+ def _adjust_axis_text_y(self, justify: TextJustifier):
565
+ """
566
+ Adjust x-axis text, justifying horizontally as necessary
567
+ """
568
+
569
+ def to_horizontal_axis_dimensions(value: float, ax: Axes) -> float:
570
+ """
571
+ Convert value in figure dimensions to axis dimensions
572
+
573
+ Matplotlib expects x position of y-axis text is in transAxes,
574
+ but all our layout measurements are in transFigure.
575
+
576
+ ---------------------
577
+ | |
578
+ | ----------- |
579
+ | X | | |
580
+ | X | | |
581
+ | X | | |
582
+ | X | | |
583
+ | X | | |
584
+ | X | | |
585
+ | 0-----------1 |
586
+ | axes |
587
+ | |
588
+ 0---------------------1
589
+ figure
590
+
591
+ We do not set the transform to transFigure because, then we need
592
+ to calculate the position in transFigure; accounting for all the
593
+ space wherever the panel may be.
594
+ """
595
+ W, _ = self.plot.figure.bbox.size
596
+ w = ax.get_window_extent().width
597
+ return value * W / w
598
+
599
+ if self._is_blank("axis_text_y"):
600
+ return
601
+
602
+ ha = self.plot.theme.getp(("axis_text_y", "ha"), "right")
603
+
604
+ for ax in self.plot.axs:
605
+ texts = list(self.axis_text_y(ax))
606
+ axis_text_col_width = to_horizontal_axis_dimensions(
607
+ self.axis_text_y_max_width(ax), ax
608
+ )
609
+ for text in texts:
610
+ width = to_horizontal_axis_dimensions(
611
+ self.calc.tight_width(text), ax
612
+ )
613
+ justify.horizontally(
614
+ text, ha, -axis_text_col_width, 0, width=width
615
+ )
616
+
511
617
 
512
618
  def _text_is_visible(text: Text) -> bool:
513
619
  """
@@ -516,54 +622,47 @@ def _text_is_visible(text: Text) -> bool:
516
622
  return text.get_visible() and text._text # type: ignore
517
623
 
518
624
 
519
- def horizontally_align_text(
520
- text: Text,
521
- ha: str | float,
522
- spaces: LayoutSpaces,
523
- how: Literal["panel", "plot"] = "panel",
524
- ):
625
+ @dataclass
626
+ class TextJustifier:
525
627
  """
526
- Horizontal justification
628
+ Justify Text
527
629
 
528
- Reinterpret horizontal alignment to be justification about the panels or
529
- the plot (depending on the how parameter)
630
+ The justification methods reinterpret alignment values to be justification
631
+ about a span.
530
632
  """
531
- if isinstance(ha, str):
532
- lookup = {
533
- "left": 0.0,
534
- "center": 0.5,
535
- "right": 1.0,
536
- }
537
- rel = lookup[ha]
538
- else:
539
- rel = ha
540
-
541
- if how == "panel":
542
- left = spaces.l.left
543
- right = spaces.r.right
544
- else:
545
- left = spaces.l.plot_left
546
- right = spaces.r.plot_right
547
633
 
548
- width = spaces.items.calc.width(text)
549
- x = rel_position(rel, width, left, right)
550
- text.set_x(x)
551
- text.set_horizontalalignment("left")
634
+ spaces: LayoutSpaces
552
635
 
553
-
554
- def vertically_align_text(
555
- text: Text,
556
- va: str | float,
557
- spaces: LayoutSpaces,
558
- how: Literal["panel", "plot"] = "panel",
559
- ):
560
- """
561
- Vertical justification
562
-
563
- Reinterpret vertical alignment to be justification about the panels or
564
- the plot (depending on the how parameter).
565
- """
566
- if isinstance(va, str):
636
+ def horizontally(
637
+ self,
638
+ text: Text,
639
+ ha: str | float,
640
+ left: float,
641
+ right: float,
642
+ width: float | None = None,
643
+ ):
644
+ """
645
+ Horizontally Justify text between left and right
646
+ """
647
+ lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
648
+ rel = lookup.get(ha, ha) # pyright: ignore[reportCallIssue, reportArgumentType]
649
+ if width is None:
650
+ width = self.spaces.items.calc.width(text)
651
+ x = rel_position(rel, width, left, right)
652
+ text.set_x(x)
653
+ text.set_horizontalalignment("left")
654
+
655
+ def vertically(
656
+ self,
657
+ text: Text,
658
+ va: str | float,
659
+ bottom: float,
660
+ top: float,
661
+ height: float | None = None,
662
+ ):
663
+ """
664
+ Vertically Justify text between bottom and top
665
+ """
567
666
  lookup = {
568
667
  "top": 1.0,
569
668
  "center": 0.5,
@@ -571,21 +670,63 @@ def vertically_align_text(
571
670
  "center_baseline": 0.5,
572
671
  "bottom": 0.0,
573
672
  }
574
- rel = lookup[va]
575
- else:
576
- rel = va
673
+ rel = lookup.get(va, va) # pyright: ignore[reportCallIssue, reportArgumentType]
577
674
 
578
- if how == "panel":
579
- top = spaces.t.top
580
- bottom = spaces.b.bottom
581
- else:
582
- top = spaces.t.plot_top
583
- bottom = spaces.b.plot_bottom
675
+ if height is None:
676
+ height = self.spaces.items.calc.height(text)
677
+ y = rel_position(rel, height, bottom, top)
678
+ text.set_y(y)
679
+ text.set_verticalalignment("bottom")
584
680
 
585
- height = spaces.items.calc.height(text)
586
- y = rel_position(rel, height, bottom, top)
587
- text.set_y(y)
588
- text.set_verticalalignment("bottom")
681
+ def horizontally_across_panel(self, text: Text, ha: str | float):
682
+ """
683
+ Horizontally Justify text accross the panel(s) width
684
+ """
685
+ self.horizontally(text, ha, self.spaces.l.left, self.spaces.r.right)
686
+
687
+ def horizontally_across_plot(self, text: Text, ha: str | float):
688
+ """
689
+ Horizontally Justify text across the plot's width
690
+ """
691
+ self.horizontally(
692
+ text, ha, self.spaces.l.plot_left, self.spaces.r.plot_right
693
+ )
694
+
695
+ def vertically_along_panel(self, text: Text, va: str | float):
696
+ """
697
+ Horizontally Justify text along the panel(s) height
698
+ """
699
+ self.vertically(text, va, self.spaces.b.bottom, self.spaces.t.top)
700
+
701
+ def vertically_along_plot(self, text: Text, va: str | float):
702
+ """
703
+ Vertically Justify text along the plot's height
704
+ """
705
+ self.vertically(
706
+ text, va, self.spaces.b.plot_bottom, self.spaces.t.plot_top
707
+ )
708
+
709
+ def horizontally_about(
710
+ self, text: Text, ratio: float, how: Literal["panel", "plot"]
711
+ ):
712
+ """
713
+ Horizontally Justify text across the panel or plot
714
+ """
715
+ if how == "panel":
716
+ self.horizontally_across_panel(text, ratio)
717
+ else:
718
+ self.horizontally_across_plot(text, ratio)
719
+
720
+ def vertically_about(
721
+ self, text: Text, ratio: float, how: Literal["panel", "plot"]
722
+ ):
723
+ """
724
+ Vertically Justify text along the panel or plot
725
+ """
726
+ if how == "panel":
727
+ self.vertically_along_panel(text, ratio)
728
+ else:
729
+ self.vertically_along_plot(text, ratio)
589
730
 
590
731
 
591
732
  def set_legends_position(legends: legend_artists, spaces: LayoutSpaces):
@@ -255,8 +255,8 @@ class left_spaces(_side_spaces):
255
255
  self.axis_title_y_margin_right = m.r
256
256
 
257
257
  # Account for the space consumed by the axis
258
- self.axis_text_y = items.axis_text_y_max_width("first_col")
259
- self.axis_ticks_y = items.axis_ticks_y_max_width("first_col")
258
+ self.axis_text_y = items.axis_text_y_max_width_at("first_col")
259
+ self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col")
260
260
 
261
261
  # Adjust plot_margin to make room for ylabels that protude well
262
262
  # beyond the axes
@@ -616,8 +616,8 @@ class bottom_spaces(_side_spaces):
616
616
  self.axis_title_x_margin_top = m.t * F
617
617
 
618
618
  # Account for the space consumed by the axis
619
- self.axis_ticks_x = items.axis_ticks_x_max_height("last_row")
620
- self.axis_text_x = items.axis_text_x_max_height("last_row")
619
+ self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row")
620
+ self.axis_text_x = items.axis_text_x_max_height_at("last_row")
621
621
 
622
622
  # Adjust plot_margin to make room for ylabels that protude well
623
623
  # beyond the axes
@@ -917,13 +917,13 @@ class LayoutSpaces:
917
917
  self.sh += self.t.strip_text_x_height_top * (1 + strip_align_x)
918
918
 
919
919
  if facet.free["x"]:
920
- self.sh += self.items.axis_text_x_max_height(
920
+ self.sh += self.items.axis_text_x_max_height_at(
921
921
  "all"
922
- ) + self.items.axis_ticks_x_max_height("all")
922
+ ) + self.items.axis_ticks_x_max_height_at("all")
923
923
  if facet.free["y"]:
924
- self.sw += self.items.axis_text_y_max_width(
924
+ self.sw += self.items.axis_text_y_max_width_at(
925
925
  "all"
926
- ) + self.items.axis_ticks_y_max_width("all")
926
+ ) + self.items.axis_ticks_y_max_width_at("all")
927
927
 
928
928
  # width and height of axes as fraction of figure width & height
929
929
  self.w = ((self.r.right - self.l.left) - self.sw * (ncol - 1)) / ncol
plotnine/_mpl/patches.py CHANGED
@@ -49,7 +49,7 @@ class StripTextPatch(FancyBboxPatch):
49
49
  return
50
50
 
51
51
  text = self.text
52
- posx, posy = text.get_transform().transform((text._x, text._y))
52
+ posx, posy = text.get_transform().transform(text.get_position())
53
53
  x, y, w, h = _get_textbox(text, renderer)
54
54
 
55
55
  self.set_bounds(0.0, 0.0, w, h)
plotnine/_mpl/text.py CHANGED
@@ -1,16 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from matplotlib.text import Text
6
6
 
7
7
  from .patches import StripTextPatch
8
- from .utils import bbox_in_axes_space
8
+ from .utils import bbox_in_axes_space, rel_position
9
9
 
10
- if typing.TYPE_CHECKING:
10
+ if TYPE_CHECKING:
11
11
  from matplotlib.backend_bases import RendererBase
12
12
 
13
13
  from plotnine.iapi import strip_draw_info
14
+ from plotnine.typing import HorizontalJustification, VerticalJustification
14
15
 
15
16
 
16
17
  class StripText(Text):
@@ -23,8 +24,6 @@ class StripText(Text):
23
24
 
24
25
  def __init__(self, info: strip_draw_info):
25
26
  kwargs = {
26
- "ha": info.ha,
27
- "va": info.va,
28
27
  "rotation": info.rotation,
29
28
  "transform": info.ax.transAxes,
30
29
  "clip_on": False,
@@ -40,38 +39,74 @@ class StripText(Text):
40
39
  self.draw_info = info
41
40
  self.patch = StripTextPatch(self)
42
41
 
42
+ # TODO: Move these _justify methods to the layout manager
43
+ # We need to first make sure that the patch has the final size during
44
+ # layout computation. Right now, the final size is calculated during
45
+ # draw (in these justify methods)
46
+ def _justify_horizontally(self, renderer):
47
+ """
48
+ Justify the text along the strip_background
49
+ """
50
+ info = self.draw_info
51
+ lookup: dict[HorizontalJustification, float] = {
52
+ "left": 0.0,
53
+ "center": 0.5,
54
+ "right": 1.0,
55
+ }
56
+ rel = lookup.get(info.ha, 0.5) if isinstance(info.ha, str) else info.ha
57
+ patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
58
+ text_bbox = bbox_in_axes_space(self, info.ax, renderer)
59
+ l, b, w, h = info.x, info.y, info.box_width, patch_bbox.height
60
+ b = b + patch_bbox.height * info.strip_align
61
+ x = rel_position(rel, text_bbox.width, patch_bbox.x0, patch_bbox.x1)
62
+ y = b + h / 2
63
+ self.set_horizontalalignment("left")
64
+ self.patch.set_bounds(l, b, w, h)
65
+ self.set_position((x, y))
66
+
67
+ def _justify_vertically(self, renderer):
68
+ """
69
+ Justify the text along the strip_background
70
+ """
71
+ # Note that the strip text & background and horizontal but
72
+ # rotated to appear vertical. So we really are still justifying
73
+ # horizontally.
74
+ info = self.draw_info
75
+ lookup: dict[VerticalJustification, float] = {
76
+ "bottom": 0.0,
77
+ "center": 0.5,
78
+ "top": 1.0,
79
+ }
80
+ rel = lookup.get(info.va, 0.5) if isinstance(info.va, str) else info.va
81
+ patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
82
+ text_bbox = bbox_in_axes_space(self, info.ax, renderer)
83
+ l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
84
+ l = l + patch_bbox.width * info.strip_align
85
+ x = l + w / 2
86
+ y = rel_position(rel, text_bbox.height, patch_bbox.y0, patch_bbox.y1)
87
+ self.set_horizontalalignment("right") # 90CW right means bottom
88
+ self.patch.set_bounds(l, b, w, h)
89
+ self.set_position((x, y))
90
+
43
91
  def draw(self, renderer: RendererBase):
44
92
  if not self.get_visible():
45
93
  return
46
94
 
47
- info = self.draw_info
48
- # "fill up" spatch to contain the text
95
+ # expand strip_text patch to contain the text
49
96
  self.patch.update_position_size(renderer)
50
97
 
51
- # Get bbox of spatch in transAxes space
52
- patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
53
-
54
98
  # Align patch across the edge of the panel
55
- if info.position == "top":
56
- l, b, w, h = info.x, info.y, info.box_width, patch_bbox.height
57
- b = b + patch_bbox.height * info.strip_align
99
+ if self.draw_info.position == "top":
100
+ self._justify_horizontally(renderer)
58
101
  else: # "right"
59
- l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
60
- l = l + patch_bbox.width * info.strip_align
102
+ self._justify_vertically(renderer)
61
103
 
62
- self.patch.set_bounds(l, b, w, h)
63
- self.patch.set_transform(info.ax.transAxes)
104
+ self.patch.set_transform(self.draw_info.ax.transAxes)
64
105
  self.patch.set_mutation_scale(0)
65
106
 
66
107
  # Put text in center of patch
67
- self._x = l + w / 2
68
- self._y = b + h / 2
69
-
70
- # "anchor" aligns before rotation so the right-strip get properly
71
- # centered text
72
108
  self.set_rotation_mode("anchor")
73
- self.set_horizontalalignment("center") # right-strip
74
- self.set_verticalalignment("center_baseline") # top-strip
109
+ self.set_verticalalignment("center_baseline")
75
110
 
76
111
  # Draw spatch
77
112
  self.patch.draw(renderer)
@@ -299,7 +299,7 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
299
299
  return _id_var(df[df.columns[0]], drop)
300
300
 
301
301
  # Calculate individual ids
302
- ids = df.apply(_id_var, axis=0)
302
+ ids = df.apply(_id_var, axis=0, drop=drop)
303
303
  ids = ids.reindex(columns=list(reversed(ids.columns)))
304
304
 
305
305
  # Calculate dimensions
plotnine/facets/strips.py CHANGED
@@ -63,11 +63,13 @@ class strip:
63
63
  """
64
64
  theme = self.theme
65
65
  position = self.position
66
+
66
67
  if position == "top":
67
68
  # The x & y values are just starting locations
68
69
  # The final location is determined by the layout manager.
69
70
  y = 1
70
- ha, va = "center", "bottom"
71
+ ha = theme.getp(("strip_text_x", "ha"), "center")
72
+ va = theme.getp(("strip_text_x", "va"), "bottom")
71
73
  rotation = theme.getp(("strip_text_x", "rotation"))
72
74
  box_width = 1
73
75
  box_height = 0 # Determined by the text size
@@ -88,7 +90,8 @@ class strip:
88
90
  # The x & y values are just starting locations
89
91
  # The final location is determined by the layout manager.
90
92
  x = 1
91
- ha, va = "left", "center"
93
+ ha = theme.getp(("strip_text_y", "ha"), "left")
94
+ va = theme.getp(("strip_text_y", "va"), "center")
92
95
  rotation = theme.getp(("strip_text_y", "rotation"))
93
96
  box_width = 0 # Determine by the text height
94
97
  # TODO: Allow two unique paddings for either side.
plotnine/guides/guide.py CHANGED
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  from typing_extensions import Self
19
19
 
20
20
  from plotnine import aes, guides
21
- from plotnine.layer import Layers
21
+ from plotnine.layer import Layers, layer
22
22
  from plotnine.scales.scale import scale
23
23
  from plotnine.typing import (
24
24
  LegendPosition,
@@ -79,7 +79,7 @@ class guide(ABC, metaclass=Register):
79
79
  self.elements = cast("GuideElements", None)
80
80
  self.guides_elements: GuidesElements
81
81
 
82
- def legend_aesthetics(self, layer):
82
+ def legend_aesthetics(self, layer: layer):
83
83
  """
84
84
  Return the aesthetics that contribute to the legend
85
85
 
@@ -171,21 +171,20 @@ class guide_legend(guide):
171
171
  # Modify aesthetics
172
172
 
173
173
  # When doing after_scale evaluations, we only consider those
174
- # for the aesthetics of this legend. The reduces the spurious
175
- # warnings where an evaluation of another aesthetic failed yet
176
- # it is not needed.
174
+ # for the aesthetics that are valid for this layer/geom.
177
175
  aes_modifiers = {
178
- ae: expr
179
- for ae, expr in l.mapping._scaled.items()
180
- if ae in matched_set
176
+ ae: l.mapping._scaled[ae]
177
+ for ae in l.geom.aesthetics() & l.mapping._scaled.keys()
181
178
  }
182
179
 
183
180
  try:
184
181
  data = l.use_defaults(data, aes_modifiers)
185
182
  except PlotnineError:
186
183
  warn(
187
- "Failed to apply `after_scale` modifications "
188
- "to the legend.",
184
+ "Failed to apply `after_scale` modifications to the "
185
+ "legend. This probably should not happen. Help us "
186
+ "discover why, please open and issue at "
187
+ "https://github.com/has2k1/plotnine/issues",
189
188
  PlotnineWarning,
190
189
  )
191
190
  data = l.use_defaults(data, {})
plotnine/iapi.py CHANGED
@@ -22,8 +22,10 @@ if TYPE_CHECKING:
22
22
  from plotnine.typing import (
23
23
  CoordRange,
24
24
  FloatArrayLike,
25
+ HorizontalJustification,
25
26
  ScaledAestheticsName,
26
27
  StripPosition,
28
+ VerticalJustification,
27
29
  )
28
30
 
29
31
  from ._mpl.offsetbox import FlexibleAnchoredOffsetbox
@@ -231,8 +233,8 @@ class strip_draw_info:
231
233
 
232
234
  x: float
233
235
  y: float
234
- ha: str
235
- va: str
236
+ ha: HorizontalJustification | float
237
+ va: VerticalJustification | float
236
238
  box_width: float
237
239
  box_height: float
238
240
  strip_text_margin: float
plotnine/mapping/aes.py CHANGED
@@ -6,6 +6,7 @@ from collections.abc import Iterable, Sequence
6
6
  from contextlib import suppress
7
7
  from copy import deepcopy
8
8
  from dataclasses import fields
9
+ from functools import cached_property
9
10
  from typing import Any, Dict
10
11
 
11
12
  import pandas as pd
@@ -237,7 +238,7 @@ class aes(Dict[str, Any]):
237
238
  kwargs[name] = after_stat(_after_stat)
238
239
  return kwargs
239
240
 
240
- @property
241
+ @cached_property
241
242
  def _starting(self) -> dict[str, Any]:
242
243
  """
243
244
  Return the subset of aesthetics mapped from the layer data
@@ -254,7 +255,7 @@ class aes(Dict[str, Any]):
254
255
 
255
256
  return d
256
257
 
257
- @property
258
+ @cached_property
258
259
  def _calculated(self) -> dict[str, Any]:
259
260
  """
260
261
  Return only the aesthetics mapped to calculated statistics
@@ -269,7 +270,7 @@ class aes(Dict[str, Any]):
269
270
 
270
271
  return d
271
272
 
272
- @property
273
+ @cached_property
273
274
  def _scaled(self) -> dict[str, Any]:
274
275
  """
275
276
  Return only the aesthetics mapped to after scaling
@@ -283,11 +283,20 @@ class Compose:
283
283
  )
284
284
  return figure
285
285
 
286
- def save(
287
- self, filename: str | Path | BytesIO, save_format: str | None = None
288
- ):
286
+ def save(self, filename: str | Path | BytesIO, format: str | None = None):
287
+ """
288
+ Save a Compose object as an image file
289
+
290
+ Parameters
291
+ ----------
292
+ filename :
293
+ File name to write the plot to. If not specified, a name
294
+ format :
295
+ Image format to use, automatically extract from
296
+ file name extension.
297
+ """
289
298
  figure = self.draw()
290
- figure.savefig(filename, format=save_format)
299
+ figure.savefig(filename, format=format)
291
300
 
292
301
 
293
302
  @dataclass
@@ -164,6 +164,7 @@ class element_text(element_base):
164
164
  "size",
165
165
  "style",
166
166
  "va",
167
+ "ma",
167
168
  "weight",
168
169
  "rotation_mode",
169
170
  )
@@ -21,7 +21,7 @@ class ThemeTargets:
21
21
  """
22
22
  Artists that will be themed
23
23
 
24
- This includes only artist that cannot be accessed easily from
24
+ This includes only artist that cannot be easily accessed from
25
25
  the figure or the axes.
26
26
  """
27
27
 
@@ -43,6 +43,7 @@ class theme_gray(theme):
43
43
  family=base_family,
44
44
  style="normal",
45
45
  color="black",
46
+ ma="center",
46
47
  size=base_size,
47
48
  linespacing=0.9,
48
49
  rotation=0,
@@ -811,7 +811,7 @@ class strip_text_x(MixinSequenceOfValues):
811
811
  theme_element : element_text
812
812
  """
813
813
 
814
- _omit = ["margin"]
814
+ _omit = ["margin", "ha"]
815
815
 
816
816
  def apply_figure(self, figure: Figure, targets: ThemeTargets):
817
817
  super().apply_figure(figure, targets)
@@ -834,7 +834,7 @@ class strip_text_y(MixinSequenceOfValues):
834
834
  theme_element : element_text
835
835
  """
836
836
 
837
- _omit = ["margin"]
837
+ _omit = ["margin", "va"]
838
838
 
839
839
  def apply_figure(self, figure: Figure, targets: ThemeTargets):
840
840
  super().apply_figure(figure, targets)
@@ -890,7 +890,7 @@ class axis_text_x(MixinSequenceOfValues):
890
890
  creates a margin of 5 points.
891
891
  """
892
892
 
893
- _omit = ["margin"]
893
+ _omit = ["margin", "va"]
894
894
 
895
895
  def apply_ax(self, ax: Axes):
896
896
  super().apply_ax(ax)
@@ -923,7 +923,7 @@ class axis_text_y(MixinSequenceOfValues):
923
923
  creates a margin of 5 points.
924
924
  """
925
925
 
926
- _omit = ["margin"]
926
+ _omit = ["margin", "ha"]
927
927
 
928
928
  def apply_ax(self, ax: Axes):
929
929
  super().apply_ax(ax)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotnine
3
- Version: 0.15.0.dev1
3
+ Version: 0.15.0.dev2
4
4
  Summary: A Grammar of Graphics for Python
5
5
  Author-email: Hassan Kibirige <has2k1@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -3,7 +3,7 @@ plotnine/animation.py,sha256=izJZ4Gy0cBHEBc8ehofsWSWOzZW8UEroy1Uvw86Igb0,7521
3
3
  plotnine/doctools.py,sha256=OoPKtbFNkhQ6vKk8NfivC_xKPSvwBWvNo5-ZHS8tRAk,14540
4
4
  plotnine/exceptions.py,sha256=SgTxBHkV65HjGI3aFy2q1_lHP9HAdiuxVLN3U-PJWSQ,1616
5
5
  plotnine/ggplot.py,sha256=oMeAwsYDVhpsFV1VhxYes-0bgAPmlAE8py_eI92Xlbo,24822
6
- plotnine/iapi.py,sha256=APg-oid_IHPPrumSwLLe-O4B4sa-DTzX3wIHZdfwVQE,8302
6
+ plotnine/iapi.py,sha256=GZfXYS6JKyc8J2DB-QuSdtG_WGjBeR2vZP6ERYPUlrg,8420
7
7
  plotnine/labels.py,sha256=Ja1F0ZN5YxNjaWiAckc-jniMqXdnZxHCsqK5uudpx1s,2827
8
8
  plotnine/layer.py,sha256=r46mX4Sh_M2sHSpLcnf9UL-zc4ANuXvK8HRCQeWcya4,16749
9
9
  plotnine/options.py,sha256=j3zXv4wc3J4nOI_TqJ5s_abuifodt_UN8MR8M4i8UVA,3108
@@ -14,17 +14,17 @@ plotnine/watermark.py,sha256=_SLjhiQquB0Vd_b5t_7-mUCvrbiN86tUYbBqU76JcSM,1506
14
14
  plotnine/_mpl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  plotnine/_mpl/gridspec.py,sha256=OvlXchYXam1c1n8iNPKu4fJr6rqmH7OWebhJX5qqOmY,7903
16
16
  plotnine/_mpl/offsetbox.py,sha256=E7810lS9W7Gexwz5mkSMz7PQXXHkYX-WHKlPHqapmTg,3216
17
- plotnine/_mpl/patches.py,sha256=2-2DwHWNHlLeSfEn1gadLRkcLVPFwan_qayXbhhriRM,2567
18
- plotnine/_mpl/text.py,sha256=nIHQw20afSb0xbitY58m3YlVFT4wGhOuZr_KvowAElI,2183
17
+ plotnine/_mpl/patches.py,sha256=DLIFn4BFqrv0FXtbtloNWE5kaJRq85Q_8vz21sMJxuU,2568
18
+ plotnine/_mpl/text.py,sha256=0rkIi69Yd3qX9yg4f3wfaDY8tky1HZXkZFA0575Di_c,3822
19
19
  plotnine/_mpl/ticker.py,sha256=RY_7AdTggc7QBq9_t0KBJXg36oxKfB-Vtc9FzLnaGnQ,393
20
20
  plotnine/_mpl/transforms.py,sha256=DNaOlNq76xlT696sN8ot1bmYyp4mmrjXQHk3kTi4HIg,76
21
21
  plotnine/_mpl/utils.py,sha256=c9wkxxUEweDg6O0hdgZonNOFncwxkqFye8_bL7td7I4,4131
22
22
  plotnine/_mpl/layout_manager/__init__.py,sha256=IXpPF5Oycc45uFpK4MJ6kcQCe1u5VUfnHLNZGcnrJCg,157
23
23
  plotnine/_mpl/layout_manager/_engine.py,sha256=ESUvbLAlZApFbBi6w7gZA7S4guS0Rmrj-us-gzYP2ZM,2809
24
- plotnine/_mpl/layout_manager/_layout_items.py,sha256=AoB-dq7DToGaTI9Th__wnIGqdt55LRlBHC4qsaDl1dE,23213
24
+ plotnine/_mpl/layout_manager/_layout_items.py,sha256=9wkXgCa66yCCYtpF_e0n0ORGuGGwDmEmPnNrkv-QbEg,28360
25
25
  plotnine/_mpl/layout_manager/_layout_tree.py,sha256=Cm2-udh5DQ8cxIZwefRkRp1ddgtJp8kvMYKeWBasHVU,18061
26
- plotnine/_mpl/layout_manager/_spaces.py,sha256=RddnszIvOflkTw9g_1JhDi45DPkNl01VSfsQYskKMW0,31842
27
- plotnine/_utils/__init__.py,sha256=Fvf-0Rx0DAOiQkgZ8SJ5O728qt3BrKQ5Wc2tyB96pbY,32462
26
+ plotnine/_mpl/layout_manager/_spaces.py,sha256=8ydStHv0O1b6JBdtBXbHkSrGzklMgutMUfgwB0lH_Dk,31866
27
+ plotnine/_utils/__init__.py,sha256=9RVYfwi9NmDGGzprUlE40yv9tJhW_VDAZkUKTTuvN7g,32473
28
28
  plotnine/_utils/context.py,sha256=HPQy_uyNXdS0s9URD7ZePyuc5hFU2XrRBLDTqRDLJzY,1708
29
29
  plotnine/_utils/dev.py,sha256=0qgRbMhcd4dfuLuYxx0skocKAtfwHF02ntyILRBogbg,1629
30
30
  plotnine/_utils/ipython.py,sha256=5Obr73xJ-4dzJEdBrFA8z9TXuxY7pIjKmzdTzWwnxNk,1884
@@ -61,7 +61,7 @@ plotnine/facets/facet_null.py,sha256=J--RdOvFP7AE0jK79-LNRksvCJExQTpS-tU5i0pGqs8
61
61
  plotnine/facets/facet_wrap.py,sha256=NOoNdkaSb1y_ho6UygGkDih7zlQYXcEgJYK0Lqu6e98,8805
62
62
  plotnine/facets/labelling.py,sha256=JEuwERTK0IfmxTWHbl2nsGgxZ6xi0n2TTWT4_OSfQcQ,8833
63
63
  plotnine/facets/layout.py,sha256=TIkMChA0wJWLKN31PH0czS6CN4pw3o--PF49LakJ2h4,8967
64
- plotnine/facets/strips.py,sha256=Ue2AYdyK79EduAkazlfctB2uZ5kLVsWl8TOHid6SWXg,5985
64
+ plotnine/facets/strips.py,sha256=7K_SLVLhlb2-E-JNCZWOamFbC-yKLJ0yuqbwNoElLXk,6154
65
65
  plotnine/geoms/__init__.py,sha256=HEfhNmmNH4xm4rpXnFRXY4eLkJha3XPM72IIwVjv5Lc,2697
66
66
  plotnine/geoms/annotate.py,sha256=5sCHIMR_GjM2ujNY73i4iBczOkGi-vq1Nr1Yc3cBgZo,4022
67
67
  plotnine/geoms/annotation_logticks.py,sha256=2_ILqE2cgX2acFoGZPDnbTJ0V7b3-ALVjno71FQuuxE,8953
@@ -111,17 +111,17 @@ plotnine/geoms/geom_tile.py,sha256=3x9BSxaSr-ys6N5R2wY8B9fNiyV9vMdoXbjIDqHy_ng,1
111
111
  plotnine/geoms/geom_violin.py,sha256=-lXdHQjrPAQXvFEviGi9iJqU_8L6_YrYYXwg62-8N58,6545
112
112
  plotnine/geoms/geom_vline.py,sha256=qKUd4IosH1VrwHbqNbs29kZyIW5lQRa_LZLRcLbPg38,3377
113
113
  plotnine/guides/__init__.py,sha256=ulI-mDhtq3jAQEAqPv8clrn3UHGFOu3xRuO7jXlq-LY,201
114
- plotnine/guides/guide.py,sha256=xWTMrQPbgxIPOR73uLLQNcItggIwK_465ycmBRvCcmY,8215
114
+ plotnine/guides/guide.py,sha256=L1O26atzSjiQR-YW9w3XEcH0BDFiqthdY6wA6DAOapo,8229
115
115
  plotnine/guides/guide_axis.py,sha256=zG_5Ot1kTuHOeuQspL5V1A1-7c7X8cNeMDoF01Ghh2w,296
116
116
  plotnine/guides/guide_colorbar.py,sha256=zHM6QRrzACC2l1Vq6p04Cu0QAGuTBfp3AhPyyMbYbgc,16290
117
- plotnine/guides/guide_legend.py,sha256=v8utsFzfGXU9XeDF6Nm9b5wxLNgWfShWVYc2t_1gKO8,14185
117
+ plotnine/guides/guide_legend.py,sha256=0CSyRxlHu0RSJjnMWvnStnq-EtjgdmHv54QgPWC3ufE,14236
118
118
  plotnine/guides/guides.py,sha256=UI3AhTOQCsXur_L-Jr3VwAQcX4dBeZMohSbA2wg1YeA,15478
119
119
  plotnine/mapping/__init__.py,sha256=DLu9E0kwwuHxzTUenoVjCNTTdkWMwIDtkExLleBq1MI,205
120
120
  plotnine/mapping/_env.py,sha256=ZzcSv54PLOD8b8Ny2h6xteGoO8bJdbj9dM6Mlg5h0V8,6094
121
- plotnine/mapping/aes.py,sha256=C7pFfB7bdN-DY9WK_bcLgrImxKUOiaKFrELLpYLutsQ,16133
121
+ plotnine/mapping/aes.py,sha256=feWP04esVXjrpHlIsSRgoGvFtJjQ4noy-2gIp5MdUww,16192
122
122
  plotnine/mapping/evaluation.py,sha256=91xVP2KxCM-ur_KOFyki7Jm1t8GWPA1TecJbjZ9yOaQ,7699
123
123
  plotnine/plot_composition/__init__.py,sha256=ZJYpfVF158cQZ1zREXy6wHNJ4FbSmqWxIkHWZwX3QT8,148
124
- plotnine/plot_composition/_compose.py,sha256=BCfin11JoqrFcY-Yo5eysShHqvmnFY-cxxWExPyUNvI,11193
124
+ plotnine/plot_composition/_compose.py,sha256=uQfXxDbEayqyjX6yMOCJiePqAK7I2OU5YnBhphkYFY4,11477
125
125
  plotnine/plot_composition/_plotspec.py,sha256=0F7q7PjDMDqcallpnBdX3N2iSRjdBTyjSvMFf83uvPU,1015
126
126
  plotnine/plot_composition/_spacer.py,sha256=vaC4F5tHhvL7T7Ns9zxUbytqwB6MLNhm5jtiKG0vAiU,798
127
127
  plotnine/positions/__init__.py,sha256=DQZE6duMUNRQifpa6SBrOKxZpGDk4NqQSGZLr9Yc9GI,595
@@ -184,13 +184,13 @@ plotnine/stats/stat_unique.py,sha256=7SXu_gb6h6YgLa1L-LM8hHe0jMZfcus4c_mEAN9zcWo
184
184
  plotnine/stats/stat_ydensity.py,sha256=cuGZwpBdotGhOLYaK_W_JvJSjCht9W1eJpbzW69ZhOY,5686
185
185
  plotnine/themes/__init__.py,sha256=tEKIF4gYmOF2Z1-_UDdK8zh41dLl-61HUGIhOQvki6I,916
186
186
  plotnine/themes/seaborn_rcmod.py,sha256=Pi-UX5LyH9teSuteYpqPOEnfLgKUz01LnKDyDA7Aois,15502
187
- plotnine/themes/targets.py,sha256=LwuNEgpatus5ATmjl_Cze7HLa_VR2ST8M74o2S68yN0,1686
187
+ plotnine/themes/targets.py,sha256=MjBRWWRgsLXXM_PJffPsV4DttQJB_m11jdD837BteuU,1686
188
188
  plotnine/themes/theme.py,sha256=sMEDXgbqMNRCnkS3AXIqE_KXdXrXVjwf3r1Kr0WnE5E,16244
189
189
  plotnine/themes/theme_538.py,sha256=6ZeIzzJ1VTgCMBYw4uprSjkS8GtcFvJmOIGjbm7h3Ok,1141
190
190
  plotnine/themes/theme_bw.py,sha256=KDOqt5C6SYaxr05ngZoTmvGvgZVeCDFLWOpChcO-Hek,1077
191
191
  plotnine/themes/theme_classic.py,sha256=B6QkU6blGnEY5iaiPtu4VsvFzC0peWSAhlKiC2SJSkM,923
192
192
  plotnine/themes/theme_dark.py,sha256=RbdMfK9GW3q5XFIVgSe_OH-ozohSgjMoxMYqw40QZJ4,1335
193
- plotnine/themes/theme_gray.py,sha256=9XfrH4xJ8q7kWNMPrjQi66Q48RYHZ8UMrTHOFsnUJL8,5271
193
+ plotnine/themes/theme_gray.py,sha256=9NVn5jOh_ypMbtnkICQk_v6GsL2gUzjal1xQKMHmwFk,5300
194
194
  plotnine/themes/theme_light.py,sha256=-vFWjP_vBMnuThNxkhVJvT9rKtr0tXWcrHDKdI_G3Ic,1429
195
195
  plotnine/themes/theme_linedraw.py,sha256=woMr18xoEJmsD8ZCiUpdyn-CE-P1AGFHRNapwCGWUjg,1350
196
196
  plotnine/themes/theme_matplotlib.py,sha256=Dqjr4J68lPxaCxp6Xq9VthOOS5CZLit-pL3zMV24ZaA,4260
@@ -199,16 +199,16 @@ plotnine/themes/theme_seaborn.py,sha256=l4lz5qD5CqhqBDgfQ61Mye3TX3f5aCHoHHx1Q04j
199
199
  plotnine/themes/theme_tufte.py,sha256=qUOrZhQyfJgc0fmy8Es7tT7aYqUSzCjvkP7-dBilwHE,1926
200
200
  plotnine/themes/theme_void.py,sha256=bU2REV9dI4x2kDIlPO6Oaq3E5di2cE8VDH-hRrnWEMc,3367
201
201
  plotnine/themes/theme_xkcd.py,sha256=q3i1W97kBwpCRbR_Y609JxcfJA2cEX5e5iAS7flbF6I,2257
202
- plotnine/themes/themeable.py,sha256=VZhDDADPPTOLvXHS6ODSsCJ6qSHlxjtlPLXXrFaHiGw,69155
202
+ plotnine/themes/themeable.py,sha256=nWgJ-0u4TnXH2rJa-qScUT-Hrefc2O7lPfYnmTTnttg,69179
203
203
  plotnine/themes/elements/__init__.py,sha256=Z9xHdhyWPNR2uF_P80aBEXYWp1DU-T2KGOuM7VimpbM,295
204
204
  plotnine/themes/elements/element_base.py,sha256=D7cfEglzsSuhW91KpZVAZ2MAHWZp64r9Aajoh8uMGZ4,832
205
205
  plotnine/themes/elements/element_blank.py,sha256=4r7-6HeR1494oWNIGQh0ASrFQ4SLvYa6aQHA85eH-Ds,187
206
206
  plotnine/themes/elements/element_line.py,sha256=xF6xW-iA66YEP_fN7ooqaYry8_8qZT-eT5wvKwXg3to,1838
207
207
  plotnine/themes/elements/element_rect.py,sha256=w5cLH-Sr4cTRXVdkRiu8kBqFt3TXHhIb1MUITfi89gE,1767
208
- plotnine/themes/elements/element_text.py,sha256=ekqK5Qh9ZNPkuEvVyVsBkPzjaABgU40vkLuXjrTgCb4,6237
208
+ plotnine/themes/elements/element_text.py,sha256=8yhwBa9s9JKCtBcqcBNybbCGK6ieDnZv4SHiC4Sy2qc,6255
209
209
  plotnine/themes/elements/margin.py,sha256=EsT46lqky7APHxMUDiNiTieNo_SIbHF-Sjhmf9zo4WY,2880
210
- plotnine-0.15.0.dev1.dist-info/licenses/LICENSE,sha256=GY4tQiUd17Tq3wWR42Zs9MRTFOTf6ahIXhZTcwAdOeU,1082
211
- plotnine-0.15.0.dev1.dist-info/METADATA,sha256=smCDQI2oJQbztKcwSwbWJePOXLjTyMcjkLc1EZCFtbQ,9289
212
- plotnine-0.15.0.dev1.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
213
- plotnine-0.15.0.dev1.dist-info/top_level.txt,sha256=t340Mbko1ZbmvYPkQ81dIiPHcaQdTUszYz-bWUpr8ys,9
214
- plotnine-0.15.0.dev1.dist-info/RECORD,,
210
+ plotnine-0.15.0.dev2.dist-info/licenses/LICENSE,sha256=GY4tQiUd17Tq3wWR42Zs9MRTFOTf6ahIXhZTcwAdOeU,1082
211
+ plotnine-0.15.0.dev2.dist-info/METADATA,sha256=FYs_q0rKbRXD28ywez0hpKxIKULY21v9RCMw2dEgClI,9289
212
+ plotnine-0.15.0.dev2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
213
+ plotnine-0.15.0.dev2.dist-info/top_level.txt,sha256=t340Mbko1ZbmvYPkQ81dIiPHcaQdTUszYz-bWUpr8ys,9
214
+ plotnine-0.15.0.dev2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5