matplotlib-map-utils 3.0.1__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,14 +287,12 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
279
287
 
280
288
  ##### CONFIG #####
281
289
 
282
- # Getting the config for the bar (length, text, divs, etc.)
290
+ # First, ensuring matplotlib knows the correct dimensions for everything
291
+ # as we need it to be accurate to calculate out the plots!
283
292
  if draw:
284
293
  ax.get_figure().draw_without_rendering()
285
294
 
286
- # window = ax.get_window_extent()
287
- # tight = ax.patch.get_tightbbox()
288
- # print(f"{draw} Window: width {round(window.width,2)} ({round(ax.get_figure().dpi_scale_trans.inverted().transform([window.width,0])[0],2)}), height {round(window.height,2)} ({round(ax.get_figure().dpi_scale_trans.inverted().transform([0,window.height])[1],2)})")
289
- # print(f"{draw} Tight: width {round(tight.width,2)} ({round(ax.get_figure().dpi_scale_trans.inverted().transform([tight.width,0])[0],2)}), height {round(tight.height,2)} ({round(ax.get_figure().dpi_scale_trans.inverted().transform([0,tight.height])[1],2)})")
295
+ # Getting the config for the bar (length, text, divs, etc.)
290
296
  bar_max, bar_length, units_label, major_div, minor_div = _config_bar(ax, _bar)
291
297
 
292
298
  # Getting the config for the segments (width, label, etc.)
@@ -488,6 +494,8 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
488
494
  if _aob["alpha"]:
489
495
  aob_img.patch.set_alpha(_aob["alpha"])
490
496
  aob_img.patch.set_visible(True)
497
+
498
+ aob_img.set_zorder(_zorder)
491
499
 
492
500
  # Finally, adding to the axis
493
501
  if draw == True:
@@ -507,12 +515,14 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
507
515
  labels: None | bool | sbt._TYPE_LABELS=None,
508
516
  text: None | bool | sbt._TYPE_TEXT=None,
509
517
  aob: None | bool | sbt._TYPE_AOB=None,
518
+ zorder: int=99,
510
519
  pad=0, sep=0,
511
520
  return_aob: bool=True
512
521
  ):
513
522
 
514
523
  _style = sbf._validate(sbt._VALIDATE_PRIMARY, "style", style)
515
524
  _location = sbf._validate(sbt._VALIDATE_PRIMARY, "location", location)
525
+ _zorder = sbf._validate(sbt._VALIDATE_PRIMARY, "zorder", zorder)
516
526
 
517
527
  ##### CONCATENATION #####
518
528
  # NOTE: Probably a better way to do this, will investigate
@@ -587,7 +597,7 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
587
597
 
588
598
  ##### PACKING #####
589
599
  # First need to know if we pack vertically or horizontally
590
- bar_vertical = _calc_vert(_bar["rotation"])
600
+ bar_vertical = _config_bar_vert(_bar["rotation"])
591
601
  packer = matplotlib.offsetbox.VPacker if bar_vertical == False else matplotlib.offsetbox.HPacker
592
602
  if bar["reverse"] == True:
593
603
  align = "right" if bar_vertical == False else "top"
@@ -624,6 +634,8 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
624
634
  if _aob["alpha"]:
625
635
  aob_pack.patch.set_alpha(_aob["alpha"])
626
636
  aob_pack.patch.set_visible(True)
637
+
638
+ aob_pack.set_zorder(_zorder)
627
639
 
628
640
  # Finally, adding to the axis
629
641
  if draw == True:
@@ -639,219 +651,286 @@ def dual_bars(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
639
651
  def _del_keys(dict, to_remove):
640
652
  return {key: val for key, val in dict.items() if key not in to_remove}
641
653
 
642
- # 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.)
643
656
  # that are shared across all the different scale bars
644
657
  def _config_bar(ax, bar):
645
658
 
646
- ## PLOT INFO ##
647
- # Literally just getting the figure for the passed axis
648
-
649
- fig = ax.get_figure()
650
- # fig.draw_without_rendering()
651
- # Sets the canvas for the figure to AGG (Anti-Grain Geometry)
652
- # canvas = FigureCanvasAgg(fig)
653
- # Draws the figure onto the canvas
654
- # canvas.draw()
655
-
656
659
  ## ROTATION ##
657
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
658
683
 
659
- 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
660
696
 
661
- ## BAR DIMENSIONS ##
662
- # Finding the max length and optimal divisions of the scale bar
697
+ return bar_vertical
698
+
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()
663
704
 
664
- # Finding the dimensions of the axis and the limits
705
+ # First, finding the dimensions of the axis and the limits
665
706
  # get_window_extent() returns values in pixel coordinates
666
707
  # so dividing by dpi gets us the inches of the axis
667
708
  # Vertical scale bars are oriented against the y-axis (height)
668
709
  if bar_vertical==True:
669
- ax_dim = ax.patch.get_window_extent().height / fig.dpi
710
+ ax_inches = ax.patch.get_window_extent().height / fig.dpi
670
711
  min_lim, max_lim = ax.get_ylim()
671
712
  # Horizontal scale bars are oriented against the x-axis (width)
672
713
  else:
673
- ax_dim = ax.patch.get_window_extent().width / fig.dpi
714
+ ax_inches = ax.patch.get_window_extent().width / fig.dpi
674
715
  min_lim, max_lim = ax.get_xlim()
675
716
  # This calculates the range from max to min on the axis of interest
676
717
  ax_range = abs(max_lim - min_lim)
677
718
 
678
- ## UNITS ##
679
- # 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
680
735
 
681
- # Capturing the unit from the projection
682
- # (We use bar_vertical to index; 0 is for east-west axis, 1 is for north-south)
683
- units_proj = pyproj.CRS(bar["projection"]).axis_info[bar_vertical].unit_name
684
- # If the provided units are in degrees, we will convert to meters first
685
- # This will recalculate the ax_range
686
- if units_proj=="degree":
687
- 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.")
688
- ylim = ax.get_ylim()
689
- xlim = ax.get_xlim()
690
- # Using https://github.com/seangrogan/great_circle_calculator/blob/master/great_circle_calculator/great_circle_calculator.py
691
- # If the bar is vertical, we use the midpoint of the longitude (x-axis) and the max and min of the latitude (y-axis)
692
- if bar_vertical==True:
693
- ax_range = distance_between_points(((xlim[0]+xlim[1])/2, ylim[0]), ((xlim[0]+xlim[1])/2, ylim[1]))
694
- # Otherwise, the opposite
695
- else:
696
- ax_range = distance_between_points((xlim[0], (ylim[0]+ylim[1])/2), (xlim[1], (ylim[0]+ylim[1])/2))
697
- # Setting units_proj to meters now
698
- units_proj = "m"
699
-
700
- # If a projected CRS is provided instead...
736
+ # Otherwise, we have more work to do!
701
737
  else:
702
- # 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
703
767
  try:
704
- units_proj = sbt.units_standard[units_proj]
768
+ units_user = sbt.units_standard.get(bar_unit)
705
769
  except:
706
- 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)")
707
- return None
708
-
709
- # Standardizing the units specified by the user
710
- # This means we will also handle conversion if necessary
711
- try:
712
- units_user = sbt.units_standard.get(bar["unit"])
713
- except:
714
- 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")
715
- units_user = None
716
-
717
- # Converting
718
-
719
- # First, the case where the user doesn't provide any units
720
- # In this instance, we just use the units from the projection
721
- if units_user is None:
722
- units_label = units_proj
723
- # If necessary, scaling "small" units to "large" units
724
- # Meters to km
725
- if units_proj == "m" and ax_range > (1000*5):
726
- ax_range = ax_range / 1000
727
- units_label = "km"
728
- # Feet to mi
729
- elif units_proj == "ft" and ax_range > (5280*5):
730
- ax_range = ax_range / 5280
731
- units_label = "mi"
732
-
733
- # Otherwise, if the user supplied a unit of some sort, then handle conversion
734
- else:
735
- units_label = units_user
736
- # We only need to do so if the units are different, however!
737
- if units_user != units_proj:
738
- # This works by finding the ratios between the two units, using meters as the base
739
- ax_range = ax_range * (sbt.convert_dict[units_proj] / sbt.convert_dict[units_user])
740
-
741
- ## BAR LENGTH AND MAX VALUE ##
742
- # bar_max is the length of the bar in UNITS, not INCHES
743
- # If it is not provided, the optimal value is calculated
744
- if bar["max"] is None:
745
- # If no bar length is provided, set to ~25% of the limit
746
- if bar["length"] is None:
747
- bar_max = 0.25 * ax_range
748
- # If the value is less than 1, set to that proportion of the limit
749
- elif bar["length"] < 1:
750
- bar_max = bar["length"] * ax_range
751
- # Otherwise, assume the value is already in inches, and calculate the fraction relative to the axis
752
- # 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
753
790
  else:
754
- if bar["length"] < ax_dim:
755
- bar_max = (bar["length"] / ax_dim) * ax_range
756
- else:
757
- 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).")
758
- bar_max = 0.25 * ax_range
759
- # If bar["max"] is provided, don't need to go through all of this effort
760
- else:
761
- if bar["length"] is not None:
762
- 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.")
763
- bar_max = bar["max"]
764
-
765
-
766
- ## BAR DIVISIONS ##
767
- # If both a max bar value and the # of breaks is provided, will not need to auto calculate
768
- if bar["max"] is not None and bar["major_div"] is not None:
769
- bar_max = bar["max"]
770
- bar_length = (bar_max / ax_range) * ax_dim
771
- major_div = bar["major_div"]
772
- # If we don't want minor divs, 1 is the default value to auto-hide it
773
- if bar.get("minor_type","none") == "none":
774
- minor_div = 1
775
- # Else, if the minor div is not provided, will generate a default
776
- elif bar["minor_div"] is None:
777
- # If major div is divisible by 2, then 2 is a good minor div
778
- if major_div % 2 == 0:
779
- minor_div = 2
780
- # 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
781
834
  else:
782
- minor_div = 1
835
+ bar_major_div = 1
836
+ # Same for minor
837
+ if bar_major_div % 2 == 0:
838
+ bar_minor_div = 2
783
839
  else:
784
- 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)
785
850
 
786
- # If none, or only one, is provided, need to auto calculate optimal values
787
- else:
788
- # First, if a max bar value IS provided, but not the # of breaks, provide a warning that the value might be changed
789
- if bar["max"] is not None or bar["major_div"] is not None:
790
- 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.")
791
- # 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
792
853
  for units_mag in range(0,23):
793
854
  if bar_max / (10 ** (units_mag+1)) > 1.5:
794
855
  units_mag += 1
795
856
  else:
796
857
  break
797
858
 
798
- # Calculating the RMS for each preferred max number we have
799
- major_breaks = list(sbt.preferred_divs.keys())
800
- major_rms = [math.sqrt((m - (bar_max/(10**units_mag)))**2) for m in major_breaks]
801
-
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]
802
863
  # Sorting for the "best" number
803
864
  # Sorted() works on the first item in the tuple contained in the list
804
- 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))]
805
867
 
806
868
  # Saving the values
807
869
  bar_max_best = sorted_breaks[0][0]
808
870
  bar_max = bar_max_best * 10**units_mag
809
- bar_length = (bar_max / ax_range) * ax_dim
810
- major_div = sbt.preferred_divs[bar_max_best][0]
811
- if bar.get("minor_type","none") == "none":
812
- 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
813
886
  else:
814
- minor_div = sbt.preferred_divs[bar_max_best][1]
815
-
816
- # Doing a quick check of the calculated value, to see if it is "too long"
817
- if (bar_length / ax_dim > 0.9) or (bar_max > ax_range * 0.9):
818
- 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
819
893
 
820
- 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
821
899
 
822
900
  # This function handles the creation of the segments and their labels
823
901
  # It is a doozy - needs to handle all the different inputs for minor_type and label_type
824
902
  # The output of this function will be a list of dictionaries
825
903
  # With each element in the list representing a segment with four keys:
826
- # 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)
827
905
  def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_style, labels, format_str, format_int):
828
906
  segments = []
829
907
  ## SEGMENT WIDTHS ##
830
908
  # Starting with the minor boxes, if any
831
- if minor_div > 1:
832
- # If minor_type is first, we only need to append minor boxes for the first set of major divisions
833
- if minor_type == "first":
834
- # Minor
835
- 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)]
836
- # The edge between minor and major needs to have the width of a major div, but the lenght if a minor one
837
- segments += [{"width":(major_width), "length":(major_width/minor_div), "value":(bar_max/major_div), "type":"major"}]
838
- # After this we need to add a spacer! Otherwise our major divisions are offset
839
- # I figured out the ((minor_div-1)/2) part by trial and error, but it seems to work well enough for now
840
- 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}]
841
- # All the major divs (if any) after this are normal
842
- if major_div > 1:
843
- segments += [{"width":(major_width),"length":(major_width), "value":(d*(bar_max/major_div)), "type":"major"} for d in range(2,major_div+1)]
844
- # If minor_type is all, we append minor boxes for every major division, and no major boxes at all
845
- else:
846
- # Here, we have to do another correction for the minor divs that fall on what would be a major division
847
- segments += [{"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"major"}
848
- if ((d*(bar_max/(major_div*minor_div))) % (bar_max/major_div) == 0) else
849
- {"width":(major_width/minor_div), "length":(major_width/minor_div), "value":(d*(bar_max/(major_div*minor_div))), "type":"minor"}
850
- for d in range(0,(minor_div*major_div)+1)]
851
909
  # If you don't have minor divs, you only make boxes for the major divs, and you start at the zeroeth position
852
- else:
910
+ if minor_div <= 1 or minor_type == "none":
853
911
  segments += [{"width":(major_width), "length":(major_width), "value":(d*(bar_max/major_div)), "type":"major"} for d in range(0,major_div+1)]
854
-
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
+
855
934
  # For all segments, we make sure that the first and last types are set to major
856
935
  segments[0]["type"] = "major"
857
936
  segments[-1]["type"] = "major"
@@ -916,12 +995,12 @@ def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_st
916
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.")
917
996
  labels = _expand_list(labels, num_labels, "nfill")
918
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
919
999
  i = 0
920
1000
  for s in segments:
921
1001
  if s["label"] is not None:
922
- if labels[i] == True or type(labels[i])==int or type(labels[i])==float:
923
- s["label"] = _format_numeric(s["label"], format_str, format_int)
924
- pass
1002
+ if labels[i] == True or isinstance(labels[i], (int, float)):
1003
+ s["label"] = _format_numeric(labels[i], format_str, format_int)
925
1004
  elif labels[i] == False or labels[i] is None:
926
1005
  s["label"] = None
927
1006
  else:
@@ -938,21 +1017,6 @@ def _config_seg(bar_max, major_width, major_div, minor_div, minor_type, label_st
938
1017
  # Returning everything at the end
939
1018
  return segments
940
1019
 
941
- # A small function for calculating the number of 90 degree rotations that are being applied
942
- # So we know if the bar is in a vertical or a horizontal rotation
943
- def _calc_vert(degrees):
944
- # Figuring out how many quarter turns the rotation value is approximately
945
- quarters = int(round(degrees/90,0))
946
-
947
- # EVEN quarter turns (0, 180, 360, -180) are considered horizontal
948
- # ODD quarter turns (90, 270, -90, -270) are considered vertical
949
- if quarters % 2 == 0:
950
- bar_vertical = False
951
- else:
952
- bar_vertical = True
953
-
954
- return bar_vertical
955
-
956
1020
  # A small function for expanding a list a potentially uneven number of times
957
1021
  # Ex. ['black','white'] -> ['black','white','black','white''black']
958
1022
  def _expand_list(seq: list, length: int, how="cycle", convert=True):