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.
- 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 +265 -190
- 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.0.dist-info → matplotlib_map_utils-3.1.0.dist-info}/METADATA +59 -8
- matplotlib_map_utils-3.1.0.dist-info/RECORD +24 -0
- {matplotlib_map_utils-3.0.0.dist-info → matplotlib_map_utils-3.1.0.dist-info}/WHEEL +1 -1
- matplotlib_map_utils-3.0.0.dist-info/RECORD +0 -24
- {matplotlib_map_utils-3.0.0.dist-info → matplotlib_map_utils-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {matplotlib_map_utils-3.0.0.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,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 =
|
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
|
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
|
-
|
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
|
-
|
651
|
-
# Finding the max length and optimal divisions of the scale bar
|
697
|
+
return bar_vertical
|
652
698
|
|
653
|
-
|
654
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
668
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
768
|
+
units_user = sbt.units_standard.get(bar_unit)
|
694
769
|
except:
|
695
|
-
warnings.warn(f"
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
if
|
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
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
major_div
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
776
|
-
|
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
|
-
#
|
788
|
-
|
789
|
-
|
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
|
-
|
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
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
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
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
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
|
-
|
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
|
-
|
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
|
912
|
-
s["label"] = _format_numeric(
|
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.
|
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
|