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.
- matplotlib_map_utils/core/inset_map.py +64 -40
- matplotlib_map_utils/core/north_arrow.py +24 -9
- matplotlib_map_utils/core/scale_bar.py +261 -197
- matplotlib_map_utils/defaults/scale_bar.py +5 -0
- matplotlib_map_utils/validation/functions.py +3 -6
- matplotlib_map_utils/validation/inset_map.py +3 -0
- matplotlib_map_utils/validation/north_arrow.py +1 -0
- matplotlib_map_utils/validation/scale_bar.py +7 -4
- {matplotlib_map_utils-3.0.1.dist-info → matplotlib_map_utils-3.1.0.dist-info}/METADATA +53 -8
- {matplotlib_map_utils-3.0.1.dist-info → matplotlib_map_utils-3.1.0.dist-info}/RECORD +13 -13
- {matplotlib_map_utils-3.0.1.dist-info → matplotlib_map_utils-3.1.0.dist-info}/WHEEL +0 -0
- {matplotlib_map_utils-3.0.1.dist-info → matplotlib_map_utils-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {matplotlib_map_utils-3.0.1.dist-info → matplotlib_map_utils-3.1.0.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
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
|
-
#
|
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
|
-
#
|
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 =
|
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
|
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
|
-
|
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
|
-
|
662
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
679
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
768
|
+
units_user = sbt.units_standard.get(bar_unit)
|
705
769
|
except:
|
706
|
-
warnings.warn(f"
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
if
|
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
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
major_div
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
787
|
-
|
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
|
-
#
|
799
|
-
|
800
|
-
|
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
|
-
|
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
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
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
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
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
|
-
|
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
|
-
|
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
|
923
|
-
s["label"] = _format_numeric(
|
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):
|