matplotlib-map-utils 3.0.0__py3-none-any.whl → 3.1.0__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.
@@ -47,6 +47,7 @@ class ScaleBar(matplotlib.artist.Artist):
47
47
  labels: None | bool | sbt._TYPE_LABELS=None,
48
48
  text: None | bool | sbt._TYPE_TEXT=None,
49
49
  aob: None | bool | sbt._TYPE_AOB=None,
50
+ zorder: int=99,
50
51
  ):
51
52
  # Starting up the object with the base properties of a matplotlib Artist
52
53
  matplotlib.artist.Artist.__init__(self)
@@ -62,8 +63,10 @@ class ScaleBar(matplotlib.artist.Artist):
62
63
  # Location is stored as just a string
63
64
  location = sbf._validate(sbt._VALIDATE_PRIMARY, "location", location)
64
65
  self._location = location
65
-
66
66
 
67
+ zorder = sbf._validate(sbt._VALIDATE_PRIMARY, "zorder", zorder)
68
+ self._zorder = zorder
69
+
67
70
  # Shared elements for both ticked and boxed bars
68
71
  # This validation is dependent on the type of bar we are constructing
69
72
  # So we modify the validation dictionary to remove the keys that are not relevant (throwing a warning if they exist in the input)
@@ -84,16 +87,8 @@ class ScaleBar(matplotlib.artist.Artist):
84
87
  text = sbf._validate_dict(text, _DEFAULT_TEXT, sbt._VALIDATE_TEXT, return_clean=True, to_validate="input")
85
88
  self._text = text
86
89
 
87
- # pack = sbf._validate_dict(pack, _DEFAULT_PACK, sbt._VALIDATE_PACK, return_clean=True, to_validate="input")
88
- # self._pack = pack
89
-
90
90
  aob = sbf._validate_dict(aob, _DEFAULT_AOB, sbt._VALIDATE_AOB, return_clean=True, to_validate="input")
91
91
  self._aob = aob
92
-
93
- # We do set the zorder for our objects individually,
94
- # but we ALSO set it for the entire artist, here
95
- # Thank you to matplotlib-scalebar for this tip
96
- zorder = 99
97
92
 
98
93
  ## INTERNAL PROPERTIES ##
99
94
  # This allows for easy-updating of properties
@@ -188,6 +183,16 @@ class ScaleBar(matplotlib.artist.Artist):
188
183
  val = sbf._validate_dict(val, self._aob, sbt._VALIDATE_AOB, return_clean=True, parse_false=False)
189
184
  self._aob = val
190
185
 
186
+ # zorder
187
+ @property
188
+ def zorder(self):
189
+ return self._zorder
190
+
191
+ @zorder.setter
192
+ def zorder(self, val: int):
193
+ val = sbf._validate(sbt._VALIDATE_PRIMARY, "zorder", val)
194
+ self._zorder = val
195
+
191
196
  ## COPY FUNCTION ##
192
197
  # This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
193
198
  # Instead, you can use add_artist() like normal, but with add_artist(na.copy())
@@ -203,10 +208,12 @@ class ScaleBar(matplotlib.artist.Artist):
203
208
  # Can re-use the drawing function we already established, but return the object instead
204
209
  sb_artist = scale_bar(ax=self.axes, style=self._style, location=self._location, draw=False,
205
210
  bar=self._bar, units=self._units,
206
- labels=self._labels, text=self._text, aob=self._aob)
211
+ labels=self._labels, text=self._text, aob=self._aob,
212
+ zorder=self._zorder)
207
213
  # This handles the actual drawing
208
214
  sb_artist.axes = self.axes
209
215
  sb_artist.set_figure(self.axes.get_figure())
216
+ sb_artist.set_zorder(self._zorder)
210
217
  sb_artist.draw(renderer)
211
218
 
212
219
  ## SIZE FUNCTION ##
@@ -242,12 +249,13 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
242
249
  labels: None | bool | sbt._TYPE_LABELS=None,
243
250
  text: None | bool | sbt._TYPE_TEXT=None,
244
251
  aob: None | bool | sbt._TYPE_AOB=None,
245
- return_aob: bool=True
246
- ):
252
+ zorder: int=99,
253
+ return_aob: bool=True,):
247
254
 
248
255
  ##### VALIDATION #####
249
256
  _style = sbf._validate(sbt._VALIDATE_PRIMARY, "style", style)
250
257
  _location = sbf._validate(sbt._VALIDATE_PRIMARY, "location", location)
258
+ _zorder = sbf._validate(sbt._VALIDATE_PRIMARY, "zorder", zorder)
251
259
 
252
260
  # This works the same as it does with the ScaleBar object(s)
253
261
  # If a dictionary is passed to any of the elements, first validate that it is "correct"
@@ -279,6 +287,11 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
279
287
 
280
288
  ##### CONFIG #####
281
289
 
290
+ # First, ensuring matplotlib knows the correct dimensions for everything
291
+ # as we need it to be accurate to calculate out the plots!
292
+ if draw:
293
+ ax.get_figure().draw_without_rendering()
294
+
282
295
  # Getting the config for the bar (length, text, divs, etc.)
283
296
  bar_max, bar_length, units_label, major_div, minor_div = _config_bar(ax, _bar)
284
297
 
@@ -452,7 +465,6 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
452
465
 
453
466
  # AOB will contain the final artist
454
467
  aob_box = matplotlib.offsetbox.AnchoredOffsetbox(loc="center", child=major_pack, frameon=False, pad=0, borderpad=0)
455
-
456
468
  # Function that will handle invisibly rendering our object, returning an image
457
469
  img_scale_bar = _render_as_image(fig_temp, ax_temp, aob_box, _bar["rotation"])
458
470
 
@@ -482,6 +494,8 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
482
494
  if _aob["alpha"]:
483
495
  aob_img.patch.set_alpha(_aob["alpha"])
484
496
  aob_img.patch.set_visible(True)
497
+
498
+ aob_img.set_zorder(_zorder)
485
499
 
486
500
  # Finally, adding to the axis
487
501
  if draw == True:
@@ -501,12 +515,14 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
501
515
  labels: None | bool | sbt._TYPE_LABELS=None,
502
516
  text: None | bool | sbt._TYPE_TEXT=None,
503
517
  aob: None | bool | sbt._TYPE_AOB=None,
518
+ zorder: int=99,
504
519
  pad=0, sep=0,
505
520
  return_aob: bool=True
506
521
  ):
507
522
 
508
523
  _style = sbf._validate(sbt._VALIDATE_PRIMARY, "style", style)
509
524
  _location = sbf._validate(sbt._VALIDATE_PRIMARY, "location", location)
525
+ _zorder = sbf._validate(sbt._VALIDATE_PRIMARY, "zorder", zorder)
510
526
 
511
527
  ##### CONCATENATION #####
512
528
  # NOTE: Probably a better way to do this, will investigate
@@ -581,7 +597,7 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
581
597
 
582
598
  ##### PACKING #####
583
599
  # First need to know if we pack vertically or horizontally
584
- bar_vertical = _calc_vert(_bar["rotation"])
600
+ bar_vertical = _config_bar_vert(_bar["rotation"])
585
601
  packer = matplotlib.offsetbox.VPacker if bar_vertical == False else matplotlib.offsetbox.HPacker
586
602
  if bar["reverse"] == True:
587
603
  align = "right" if bar_vertical == False else "top"
@@ -618,6 +634,8 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
618
634
  if _aob["alpha"]:
619
635
  aob_pack.patch.set_alpha(_aob["alpha"])
620
636
  aob_pack.patch.set_visible(True)
637
+
638
+ aob_pack.set_zorder(_zorder)
621
639
 
622
640
  # Finally, adding to the axis
623
641
  if draw == True:
@@ -633,214 +651,286 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
633
651
  def _del_keys(dict, to_remove):
634
652
  return {key: val for key, val in dict.items() if key not in to_remove}
635
653
 
636
- # This function handles the config steps (width, divs, etc)
654
+ # This function handles the configuration steps
655
+ # (i.e. calculating the length of the bar, its divisions, etc.)
637
656
  # that are shared across all the different scale bars
638
657
  def _config_bar(ax, bar):
639
658
 
640
- ## PLOT INFO ##
641
- # Literally just getting the figure for the passed axis
642
-
643
- fig = ax.get_figure()
644
-
645
659
  ## ROTATION ##
646
660
  # Calculating if the rotation is vertical or horizontal
661
+ bar_vertical = _config_bar_vert(bar["rotation"])
662
+
663
+ ## DIM ##
664
+ # Finding the size of the axis in inches
665
+ # The size of the axis in the units requested by the user
666
+ # and determining the appropriate units label to use
667
+ ax_inches, ax_units, units_label = _config_bar_dim(ax, bar_vertical, bar["projection"], bar["unit"])
668
+
669
+ ## LENGTH ##
670
+ # Finding the size of the bar in inches
671
+ # and the size of the bar in the units requested by the user
672
+ # and the optimal major and minor bar divisions
673
+ bar_max, bar_length, bar_major_div, bar_minor_div = _config_bar_length(bar["max"], bar["length"], bar["major_mult"], bar["major_div"], ax_inches, ax_units, bar["projection"])
674
+
675
+ ## DIVISIONS ##
676
+ # We've done most of the work here - just need to see if we want a minor div or not
677
+ # If one is provided, override what we calculated above
678
+ if bar["minor_div"] is not None:
679
+ bar_minor_div = bar["minor_div"]
680
+
681
+ # Returning everything
682
+ return bar_max, bar_length, units_label, bar_major_div, bar_minor_div
647
683
 
648
- bar_vertical = _calc_vert(bar["rotation"])
684
+ # A small function for calculating the number of 90 degree rotations that are being applied
685
+ # So we know if the bar is in a vertical or a horizontal rotation
686
+ def _config_bar_vert(degrees):
687
+ # Figuring out how many quarter turns the rotation value is approximately
688
+ quarters = int(round(degrees/90,0))
689
+
690
+ # EVEN quarter turns (0, 180, 360, -180) are considered horizontal
691
+ # ODD quarter turns (90, 270, -90, -270) are considered vertical
692
+ if quarters % 2 == 0:
693
+ bar_vertical = False
694
+ else:
695
+ bar_vertical = True
649
696
 
650
- ## BAR DIMENSIONS ##
651
- # Finding the max length and optimal divisions of the scale bar
697
+ return bar_vertical
652
698
 
653
- # Finding the dimensions of the axis and the limits
654
- # get_tightbbox() returns values in pixel coordinates
699
+ # This function determines the dimensions of the relevant axis, in the user's desired units
700
+ def _config_bar_dim(ax, bar_vertical, bar_projection, bar_unit):
701
+
702
+ # Literally just getting the figure for the passed axis
703
+ fig = ax.get_figure()
704
+
705
+ # First, finding the dimensions of the axis and the limits
706
+ # get_window_extent() returns values in pixel coordinates
655
707
  # so dividing by dpi gets us the inches of the axis
656
708
  # Vertical scale bars are oriented against the y-axis (height)
657
709
  if bar_vertical==True:
658
- ax_dim = ax.patch.get_tightbbox().height / fig.dpi
710
+ ax_inches = ax.patch.get_window_extent().height / fig.dpi
659
711
  min_lim, max_lim = ax.get_ylim()
660
712
  # Horizontal scale bars are oriented against the x-axis (width)
661
713
  else:
662
- ax_dim = ax.patch.get_tightbbox().width / fig.dpi
714
+ ax_inches = ax.patch.get_window_extent().width / fig.dpi
663
715
  min_lim, max_lim = ax.get_xlim()
664
716
  # This calculates the range from max to min on the axis of interest
665
717
  ax_range = abs(max_lim - min_lim)
666
718
 
667
- ## UNITS ##
668
- # Now, calculating the proportion of the dimension axis that we need
719
+
720
+ # If the user is using one of the custom overrides, we can stop here, basically
721
+ if bar_projection in ["px","pixel","pixels","pt","point","points","dx","custom","axis"]:
722
+ # Enforcing an empty units label here
723
+ units_label = ""
724
+ if bar_unit is not None:
725
+ warnings.warn(f"When bar['projection'] is set to '{bar_projection}', bar['unit'] will be ignored; you can set a custom label for the bar via the units argument, see documentation for details.")
726
+ # If we're in pixels, just need to re-multiply ax_inches back out by fig.dpi
727
+ if bar_projection in ["px","pixel","pixels"]:
728
+ ax_units = ax_inches * fig.dpi
729
+ # If we're in points, need to multiply by 72
730
+ elif bar_projection in ["pt","point","points"]:
731
+ ax_units = ax_inches * 72
732
+ # If we're using the axis, then we can just use that directly!
733
+ elif bar_projection in ["dx","custom","axis"]:
734
+ ax_units = ax_range
669
735
 
670
- # Capturing the unit from the projection
671
- # (We use bar_vertical to index; 0 is for east-west axis, 1 is for north-south)
672
- units_proj = pyproj.CRS(bar["projection"]).axis_info[bar_vertical].unit_name
673
- # If the provided units are in degrees, we will convert to meters first
674
- # This will recalculate the ax_range
675
- if units_proj=="degree":
676
- warnings.warn(f"Provided CRS {bar['projection']} uses degrees. An attempt will be made at conversion, but there will be accuracy issues: it is recommended that you use a projected CRS instead.")
677
- ylim = ax.get_ylim()
678
- xlim = ax.get_xlim()
679
- # Using https://github.com/seangrogan/great_circle_calculator/blob/master/great_circle_calculator/great_circle_calculator.py
680
- # If the bar is vertical, we use the midpoint of the longitude (x-axis) and the max and min of the latitude (y-axis)
681
- if bar_vertical==True:
682
- ax_range = distance_between_points(((xlim[0]+xlim[1])/2, ylim[0]), ((xlim[0]+xlim[1])/2, ylim[1]))
683
- # Otherwise, the opposite
684
- else:
685
- ax_range = distance_between_points((xlim[0], (ylim[0]+ylim[1])/2), (xlim[1], (ylim[0]+ylim[1])/2))
686
- # Setting units_proj to meters now
687
- units_proj = "m"
688
-
689
- # If a projected CRS is provided instead...
736
+ # Otherwise, we have more work to do!
690
737
  else:
691
- # Standardizing the projection unit
738
+ # Capturing the unit from the projection
739
+ # (We use bar_vertical to index; 0 is for east-west axis, 1 is for north-south)
740
+ units_proj = pyproj.CRS(bar_projection).axis_info[bar_vertical].unit_name
741
+ # If the provided units are in degrees, we will convert ax_range to meters first
742
+ if units_proj=="degree":
743
+ warnings.warn(f"Provided CRS {bar_projection} uses degrees. An attempt will be made at conversion, but there will be accuracy issues: it is recommended that you use a projected CRS instead.")
744
+ ylim = ax.get_ylim()
745
+ xlim = ax.get_xlim()
746
+ # Using https://github.com/seangrogan/great_circle_calculator/blob/master/great_circle_calculator/great_circle_calculator.py
747
+ # If the bar is vertical, we use the midpoint of the longitude (x-axis) and the max and min of the latitude (y-axis)
748
+ if bar_vertical==True:
749
+ ax_range = distance_between_points(((xlim[0]+xlim[1])/2, ylim[0]), ((xlim[0]+xlim[1])/2, ylim[1]))
750
+ # Otherwise, the opposite
751
+ else:
752
+ ax_range = distance_between_points((xlim[0], (ylim[0]+ylim[1])/2), (xlim[1], (ylim[0]+ylim[1])/2))
753
+ # Setting units_proj to meters now
754
+ units_proj = "m"
755
+
756
+ # If a projected CRS is provided instead...
757
+ else:
758
+ # Standardizing the projection unit
759
+ try:
760
+ units_proj = sbt.units_standard[units_proj]
761
+ except:
762
+ warnings.warn(f"Units for specified projection ({units_proj}) are considered invalid; please use a different projection that conforms to an expected unit value (such as US survey feet or metres)")
763
+ return None
764
+
765
+ # Standardizing the units specified by the user
766
+ # This means we will also handle conversion if necessary
692
767
  try:
693
- units_proj = sbt.units_standard[units_proj]
768
+ units_user = sbt.units_standard.get(bar_unit)
694
769
  except:
695
- warnings.warn(f"Units for specified projection ({units_proj}) are considered invalid; please use a different projection that conforms to an expected unit value (such as US survey feet or metres)")
696
- return None
697
-
698
- # Standardizing the units specified by the user
699
- # This means we will also handle conversion if necessary
700
- try:
701
- units_user = sbt.units_standard.get(bar["unit"])
702
- except:
703
- warnings.warn(f"Desired output units selected by user ({bar['unit']}) are considered invalid; please use one of the units specified in the units_standard dictionary in defaults.py")
704
- units_user = None
705
-
706
- # Converting
707
-
708
- # First, the case where the user doesn't provide any units
709
- # In this instance, we just use the units from the projection
710
- if units_user is None:
711
- units_label = units_proj
712
- # If necessary, scaling "small" units to "large" units
713
- # Meters to km
714
- if units_proj == "m" and ax_range > (1000*5):
715
- ax_range = ax_range / 1000
716
- units_label = "km"
717
- # Feet to mi
718
- elif units_proj == "ft" and ax_range > (5280*5):
719
- ax_range = ax_range / 5280
720
- units_label = "mi"
721
-
722
- # Otherwise, if the user supplied a unit of some sort, then handle conversion
723
- else:
724
- units_label = units_user
725
- # We only need to do so if the units are different, however!
726
- if units_user != units_proj:
727
- # This works by finding the ratios between the two units, using meters as the base
728
- ax_range = ax_range * (sbt.convert_dict[units_proj] / sbt.convert_dict[units_user])
729
-
730
- ## BAR LENGTH AND MAX VALUE ##
731
- # bar_max is the length of the bar in UNITS, not INCHES
732
- # If it is not provided, the optimal value is calculated
733
- if bar["max"] is None:
734
- # If no bar length is provided, set to ~25% of the limit
735
- if bar["length"] is None:
736
- bar_max = 0.25 * ax_range
737
- # If the value is less than 1, set to that proportion of the limit
738
- elif bar["length"] < 1:
739
- bar_max = bar["length"] * ax_range
740
- # Otherwise, assume the value is already in inches, and calculate the fraction relative to the axis
741
- # Then find the proportion of the limit
770
+ warnings.warn(f"Desired output units selected by user ({bar_unit}) are considered invalid; please use one of the units specified in the units_standard dictionary in defaults.py")
771
+ units_user = None
772
+
773
+ # Converting
774
+
775
+ # First, the case where the user doesn't provide any units
776
+ # In this instance, we just use the units from the projection
777
+ if units_user is None:
778
+ units_label = units_proj
779
+ # If necessary, scaling "small" units to "large" units
780
+ # Meters to km
781
+ if units_proj == "m" and ax_range > (1000*5):
782
+ ax_units = ax_range / 1000
783
+ units_label = "km"
784
+ # Feet to mi
785
+ elif units_proj == "ft" and ax_range > (5280*5):
786
+ ax_units = ax_range / 5280
787
+ units_label = "mi"
788
+
789
+ # Otherwise, if the user supplied a unit of some sort, then handle conversion
742
790
  else:
743
- if bar["length"] < ax_dim:
744
- bar_max = (bar["length"] / ax_dim) * ax_range
745
- else:
746
- warnings.warn(f"Provided bar length ({bar['length']}) is greater than the axis length ({ax_dim}); setting bar length to default (25% of axis length).")
747
- bar_max = 0.25 * ax_range
748
- # If bar["max"] is provided, don't need to go through all of this effort
749
- else:
750
- if bar["length"] is not None:
751
- warnings.warn("Both bar['max'] and bar['length'] were set, so the value for bar['length'] will be ignored. Please reference the documentation to understand why both may not be set at the same time.")
752
- bar_max = bar["max"]
753
-
754
-
755
- ## BAR DIVISIONS ##
756
- # If both a max bar value and the # of breaks is provided, will not need to auto calculate
757
- if bar["max"] is not None and bar["major_div"] is not None:
758
- bar_max = bar["max"]
759
- bar_length = (bar_max / ax_range) * ax_dim
760
- major_div = bar["major_div"]
761
- # If we don't want minor divs, 1 is the default value to auto-hide it
762
- if bar.get("minor_type","none") == "none":
763
- minor_div = 1
764
- # Else, if the minor div is not provided, will generate a default
765
- elif bar["minor_div"] is None:
766
- # If major div is divisible by 2, then 2 is a good minor div
767
- if major_div % 2 == 0:
768
- minor_div = 2
769
- # Otherwise, will basically auto-hide the minor div
791
+ units_label = units_user
792
+ # We only need to do so if the units are different, however!
793
+ if units_user != units_proj:
794
+ # This works by finding the ratios between the two units, using meters as the base
795
+ ax_units = ax_range * (sbt.convert_dict[units_proj] / sbt.convert_dict[units_user])
796
+
797
+ return ax_inches, ax_units, units_label
798
+
799
+ # This function calculates the optimal length of the entire bar in inches and the user's units
800
+ def _config_bar_length(bar_max, bar_length, bar_major_mult, bar_major_div, ax_inches, ax_units, bar_projection):
801
+
802
+ # First checking that the user is providing valid values
803
+ if bar_length is not None and bar_max is not None:
804
+ warnings.warn(f"Both bar['max'] and bar['length'] are provided; bar['length'] will be ignored. Please reference the documentation to understand why both may not be set at the same time.")
805
+ elif (bar_max is not None or bar_length is not None) and bar_major_mult is not None:
806
+ warnings.warn(f"Either bar['max'] or bar['length'] are provided, along with bar['major_mult']; bar['major_mult'] will be ignored. Please reference the documentation to understand why both may not be set at the same time.")
807
+ elif bar_length is not None and bar_major_div is not None:
808
+ warnings.warn(f"Both bar['length'] and bar['major_div'] are provided; bar['major_div'] will be ignored, and instead an optimal value will be calculated. Please reference the documentation to understand why both may not be set at the same time.")
809
+ elif bar_length is not None and bar_length > ax_inches:
810
+ warnings.warn(f"Provided bar length ({bar_length}) is greater than the axis length ({ax_inches} inches); setting bar length to default (25% of axis length).")
811
+ bar_length = 0.25
812
+ elif bar_projection in ["px","pixel","pixels","pt","point","points","dx","custom","axis"] and bar_length is not None:
813
+ warnings.warn(f"Providing a bar length is incompatible with a bar['projection'] value of ${bar_projection}, please use either bar['max'] or bar['major_mult'] instead.")
814
+ elif bar_major_mult is not None and bar_major_div is None:
815
+ warnings.warn(f"bar['major_div'] must be supplied alongside bar['major_mult']. Reverting to default behavior (bar['length'] = 0.25).")
816
+ bar_length = 0.25
817
+
818
+ # Then, seeing if we need to use the default behavior (if everything is none)
819
+ if bar_max is None and bar_length is None and bar_major_mult is None and bar_major_div is None:
820
+ bar_length = 0.25
821
+
822
+ # Deriving the length of the bar
823
+ # First, with bar_max, if provided - it is the length of the bar in UNITS
824
+ if bar_max is not None:
825
+ # Only need to update the bar_length here, then
826
+ bar_length = ax_inches * (bar_max / ax_units)
827
+
828
+ # If we don't have a bar_major_div, set one
829
+ if bar_major_div is None:
830
+ if bar_max % 3 == 0:
831
+ bar_major_div = 3
832
+ elif bar_max % 2 == 0:
833
+ bar_major_div = 2
770
834
  else:
771
- minor_div = 1
835
+ bar_major_div = 1
836
+ # Same for minor
837
+ if bar_major_div % 2 == 0:
838
+ bar_minor_div = 2
772
839
  else:
773
- minor_div = bar["minor_div"]
840
+ bar_minor_div = 1
841
+
842
+ # Otherwise, if bar_length is provided - it is the length of the bar in INCHES
843
+ elif bar_length is not None:
844
+ # Converting, if bar length is expressed as a fraction of the axis
845
+ if bar_length < 1:
846
+ bar_length = ax_inches * bar_length
847
+
848
+ # Calculating bar_max based on this length
849
+ bar_max = ax_units * (bar_length / ax_inches)
774
850
 
775
- # If none, or only one, is provided, need to auto calculate optimal values
776
- else:
777
- # First, if a max bar value IS provided, but not the # of breaks, provide a warning that the value might be changed
778
- if bar["max"] is not None or bar["major_div"] is not None:
779
- warnings.warn(f"As one of bar['max'] and bar['major_div'] were not set, the values will be calculated automatically. This may result in different values from your input.")
780
- # Finding the magnitude of the max of the bar
851
+ # We will also optimize it, so that it rounds to a "nice" number
852
+ # Scaling the max down to just the important digits
781
853
  for units_mag in range(0,23):
782
854
  if bar_max / (10 ** (units_mag+1)) > 1.5:
783
855
  units_mag += 1
784
856
  else:
785
857
  break
786
858
 
787
- # Calculating the RMS for each preferred max number we have
788
- major_breaks = list(sbt.preferred_divs.keys())
789
- major_rms = [math.sqrt((m - (bar_max/(10**units_mag)))**2) for m in major_breaks]
790
-
859
+ # Getting a list of "optimal" numbers we want to aim for
860
+ preferred_maxes = list(sbt.preferred_divs.keys())
861
+ # Finding the RMS/distance between each perferred max and our actual max
862
+ max_rms = [math.sqrt((m - (bar_max/(10**units_mag)))**2) for m in preferred_maxes]
791
863
  # Sorting for the "best" number
792
864
  # Sorted() works on the first item in the tuple contained in the list
793
- sorted_breaks = [(m,r) for r,m in sorted(zip(major_rms, major_breaks))]
865
+ # Note that we reverse m,r and r,m here, just to keep things confusing
866
+ sorted_breaks = [(m,r) for r,m in sorted(zip(max_rms, preferred_maxes))]
794
867
 
795
868
  # Saving the values
796
869
  bar_max_best = sorted_breaks[0][0]
797
870
  bar_max = bar_max_best * 10**units_mag
798
- bar_length = (bar_max / ax_range) * ax_dim
799
- major_div = sbt.preferred_divs[bar_max_best][0]
800
- if bar.get("minor_type","none") == "none":
801
- minor_div = 1
871
+
872
+ # Going back and re-calculating the length (it'll be slightly more/less now)
873
+ bar_length = ax_inches * (bar_max / ax_units)
874
+
875
+ # And finally picking out the major div from this
876
+ bar_major_div, bar_minor_div = sbt.preferred_divs[bar_max_best]
877
+
878
+ # Otherwise, if BOTH bar_major_mult and bar_major_div are provided, using that
879
+ # bar_major_mult is expressed in UNITS
880
+ elif bar_major_div is not None and bar_major_mult is not None:
881
+ bar_max = bar_major_mult * bar_major_div
882
+ bar_length = ax_inches * (bar_max / ax_units)
883
+ # And we set bar_minor_div based on major_div
884
+ if bar_major_div % 2 == 0:
885
+ bar_minor_div = 2
802
886
  else:
803
- minor_div = sbt.preferred_divs[bar_max_best][1]
804
-
805
- # Doing a quick check of the calculated value, to see if it is "too long"
806
- if (bar_length / ax_dim > 0.9) or (bar_max > ax_range * 0.9):
807
- warnings.warn(f"The auto-calculated dimensions of the bar are too large for the axis. This usually happens when the height or width of your map is ~1 to 2 miles or kilometres (depending on your selected unit). This will result in a bar close to or longer than your axis, extending beyond your frame. Consider either manually specifying a 'max' and 'major_div' value less than 2, or switching your units to feet/metres as necessary.")
887
+ bar_minor_div = 1
888
+
889
+ # Generic catch to throw a warning of any other situation
890
+ else:
891
+ warnings.warn("Error in calculating bar length, please re-check your values for bar['max'], bar['length'], bar['major_mult'], and/or bar['major_div']. If error persists when values appear correct, please file an issue on GitHub with a code sample.")
892
+ return None
808
893
 
809
- return bar_max, bar_length, units_label, major_div, minor_div
894
+ # Another final check just to make sure our final calculations aren't too wonky
895
+ if ((bar_length / ax_inches) > 0.9) or (bar_max > (ax_units * 0.9)):
896
+ warnings.warn(f"The auto-calculated dimensions of the bar are too large for the axis. This usually happens when the height or width of your map is ~1 to 2 miles or kilometres (depending on your selected unit). This will result in a bar close to or longer than your axis, extending beyond your frame. Consider either manually specifying a 'max' and 'major_div' value less than 2, or switching your units to feet/metres as necessary.")
897
+
898
+ return bar_max, bar_length, bar_major_div, bar_minor_div
810
899
 
811
900
  # This function handles the creation of the segments and their labels
812
901
  # It is a doozy - needs to handle all the different inputs for minor_type and label_type
813
902
  # The output of this function will be a list of dictionaries
814
903
  # With each element in the list representing a segment with four keys:
815
- # width (for the segment, in points), length(for the label, in points), value (numeric value in units), type (major or minor or spacer), and label (either the value (rounded if needed) or None if no label is required)
904
+ # width (for the segment, in points), length (for the label, in points), value (numeric value in units), type (major or minor or spacer), and label (either the value (rounded if needed) or None if no label is required)
816
905
  def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_style, labels, format_str, format_int):
817
906
  segments = []
818
907
  ## SEGMENT WIDTHS ##
819
908
  # Starting with the minor boxes, if any
820
- if minor_div > 1:
821
- # If minor_type is first, we only need to append minor boxes for the first set of major divisions
822
- if minor_type == "first":
823
- # Minor
824
- segments += [{"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/major_div/minor_div)), "type":"minor"} for d in range(0,minor_div)]
825
- # The edge between minor and major needs to have the width of a major div, but the lenght if a minor one
826
- segments += [{"width":(major_width), "length":(major_width/minor_div), "value":(bar_max/major_div), "type":"major"}]
827
- # After this we need to add a spacer! Otherwise our major divisions are offset
828
- # I figured out the ((minor_div-1)/2) part by trial and error, but it seems to work well enough for now
829
- segments += [{"width":(major_width/minor_div*((minor_div-1)/2)), "length":(major_width/minor_div*((minor_div-1)/2)), "value":-1, "type":"spacer", "label":None}]
830
- # All the major divs (if any) after this are normal
831
- if major_div > 1:
832
- segments += [{"width":(major_width),"length":(major_width), "value":(d*(bar_max/major_div)), "type":"major"} for d in range(2,major_div+1)]
833
- # If minor_type is all, we append minor boxes for every major division, and no major boxes at all
834
- else:
835
- # Here, we have to do another correction for the minor divs that fall on what would be a major division
836
- segments += [{"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"major"}
837
- if ((d*(bar_max/(major_div*minor_div))) % (bar_max/major_div) == 0) else
838
- {"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"minor"}
839
- for d in range(0,(minor_div*major_div)+1)]
840
909
  # If you don't have minor divs, you only make boxes for the major divs, and you start at the zeroeth position
841
- else:
910
+ if minor_div <= 1 or minor_type == "none":
842
911
  segments += [{"width":(major_width), "length":(major_width), "value":(d*(bar_max/major_div)), "type":"major"} for d in range(0,major_div+1)]
843
-
912
+ else:
913
+ if minor_div > 1:
914
+ # If minor_type is first, we only need to append minor boxes for the first set of major divisions
915
+ if minor_type == "first":
916
+ # Minor
917
+ segments += [{"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/major_div/minor_div)), "type":"minor"} for d in range(0,minor_div)]
918
+ # The edge between minor and major needs to have the width of a major div, but the lenght if a minor one
919
+ segments += [{"width":(major_width), "length":(major_width/minor_div), "value":(bar_max/major_div), "type":"major"}]
920
+ # After this we need to add a spacer! Otherwise our major divisions are offset
921
+ # I figured out the ((minor_div-1)/2) part by trial and error, but it seems to work well enough for now
922
+ segments += [{"width":(major_width/minor_div*((minor_div-1)/2)), "length":(major_width/minor_div*((minor_div-1)/2)), "value":-1, "type":"spacer", "label":None}]
923
+ # All the major divs (if any) after this are normal
924
+ if major_div > 1:
925
+ segments += [{"width":(major_width),"length":(major_width), "value":(d*(bar_max/major_div)), "type":"major"} for d in range(2,major_div+1)]
926
+ # If minor_type is all, we append minor boxes for every major division, and no major boxes at all
927
+ else:
928
+ # Here, we have to do another correction for the minor divs that fall on what would be a major division
929
+ segments += [{"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"major"}
930
+ if ((d*(bar_max/(major_div*minor_div))) % (bar_max/major_div) == 0) else
931
+ {"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"minor"}
932
+ for d in range(0,(minor_div*major_div)+1)]
933
+
844
934
  # For all segments, we make sure that the first and last types are set to major
845
935
  segments[0]["type"] = "major"
846
936
  segments[-1]["type"] = "major"
@@ -905,12 +995,12 @@ def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_st
905
995
  warnings.warn(f"Fewer labels were provided ({len(labels)}) than needed ({num_labels}). The last {num_labels-len(labels)} will be set to None.")
906
996
  labels = _expand_list(labels, num_labels, "nfill")
907
997
  # Keeping track of how many labels we have applied
998
+ # So we can map the labels list to the list of segments with labels
908
999
  i = 0
909
1000
  for s in segments:
910
1001
  if s["label"] is not None:
911
- if labels[i] == True or type(labels[i])==int or type(labels[i])==float:
912
- s["label"] = _format_numeric(s["label"], format_str, format_int)
913
- pass
1002
+ if labels[i] == True or isinstance(labels[i], (int, float)):
1003
+ s["label"] = _format_numeric(labels[i], format_str, format_int)
914
1004
  elif labels[i] == False or labels[i] is None:
915
1005
  s["label"] = None
916
1006
  else:
@@ -927,21 +1017,6 @@ def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_st
927
1017
  # Returning everything at the end
928
1018
  return segments
929
1019
 
930
- # A small function for calculating the number of 90 degree rotations that are being applied
931
- # So we know if the bar is in a vertical or a horizontal rotation
932
- def _calc_vert(degrees):
933
- # Figuring out how many quarter turns the rotation value is approximately
934
- quarters = int(round(degrees/90,0))
935
-
936
- # EVEN quarter turns (0, 180, 360, -180) are considered horizontal
937
- # ODD quarter turns (90, 270, -90, -270) are considered vertical
938
- if quarters % 2 == 0:
939
- bar_vertical = False
940
- else:
941
- bar_vertical = True
942
-
943
- return bar_vertical
944
-
945
1020
  # A small function for expanding a list a potentially uneven number of times
946
1021
  # Ex. ['black','white'] -> ['black','white','black','white''black']
947
1022
  def _expand_list(seq: list, length: int, how="cycle", convert=True):
@@ -1011,7 +1086,7 @@ def _temp_figure(ax, axis=False, visible=False):
1011
1086
  # Getting the figure of the provided axis
1012
1087
  fig = ax.get_figure()
1013
1088
  # Getting the dimensions of the axis
1014
- ax_bbox = ax.patch.get_tightbbox()
1089
+ ax_bbox = ax.patch.get_window_extent()
1015
1090
  # Converting to inches and rounding up
1016
1091
  ax_dim = math.ceil(max(ax_bbox.height, ax_bbox.width) / fig.dpi)
1017
1092
  # Creating a new temporary figure