Skip to content
Binary file added doc/bar_style_geography.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions doc/bar_style_geography.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import matplotlib.pyplot as plt
from matplotlib_scalebar.scalebar import ScaleBar
from osmnx import features_from_place
from geopandas import GeoSeries
from shapely import Point

fig, ax = plt.subplots()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
ax.set_facecolor("ivory")
ax.set_xlim(58.12, 58.20)
ax.set_ylim(54.90, 54.95)

place = "Ust-Katav"
water_gdf = features_from_place(place, tags={"natural": "water"})
forest_gdf = features_from_place(place, {"natural": ["wood", "tree", "tree_row"]})
landuse_gdf = features_from_place(place, tags={"landuse": True})
highway_gdf = features_from_place(place, tags={"highway": ["secondary", "tertiary"]})

forest_gdf.plot(ax=ax, facecolor="palegreen")
landuse_gdf.plot(ax=ax, facecolor="lightgray", edgecolor="gray")
water_gdf.plot(ax=ax, facecolor="lightskyblue", edgecolor="deepskyblue")
highway_gdf.plot(ax=ax, color="palegoldenrod", linewidth=3)

points = GeoSeries([Point(58.12, 54.90), Point(59.12, 54.90)], crs=4326).to_crs(32619)

scalebar = ScaleBar(
dx=points[0].distance(points[1]),
bar_style="geography",
length_fraction=1,
width_fraction=0.03,
)
ax.add_artist(scalebar)
fig.savefig("bar_style_geography.png", dpi=60, bbox_inches="tight")
123 changes: 106 additions & 17 deletions matplotlib_scalebar/scalebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- scalebar.box_alpha
- scalebar.scale_loc
- scalebar.label_loc
- scalebar.bar_style

See the class documentation (:class:`.Scalebar`) for a description of the
parameters.
Expand All @@ -40,6 +41,7 @@
# Standard library modules.
import bisect
import warnings
import math
import dataclasses

# Third party modules.
Expand All @@ -61,6 +63,7 @@
AnchoredOffsetbox,
)
from matplotlib.patches import Rectangle
from matplotlib.colors import to_rgba

# Local modules.
from matplotlib_scalebar.dimension import (
Expand Down Expand Up @@ -89,6 +92,9 @@
_VALID_ROTATIONS = ["horizontal", "horizontal-only", "vertical", "vertical-only"]
_validate_rotation = ValidateInStrings("rotation", _VALID_ROTATIONS, ignorecase=True)

_VALID_SCALE_STYLES = ["simple", "geography"]
_validate_bar_style = ValidateInStrings("bar_style", _VALID_SCALE_STYLES, ignorecase=True)


def _validate_legend_loc(loc):
rc = matplotlib.RcParams()
Expand All @@ -111,6 +117,7 @@ def _validate_legend_loc(loc):
"scalebar.scale_loc": ["bottom", _validate_scale_loc],
"scalebar.label_loc": ["top", _validate_label_loc],
"scalebar.rotation": ["horizontal", _validate_rotation],
"scalebar.bar_style": ["simple", _validate_bar_style],
}
)

Expand Down Expand Up @@ -190,6 +197,7 @@ def __init__(
box_alpha=None,
scale_loc=None,
label_loc=None,
bar_style=None,
font_properties=None,
label_formatter=None,
scale_formatter=None,
Expand Down Expand Up @@ -291,6 +299,10 @@ def __init__(
If ``none`` the label is not shown.
:type label_loc: :class:`str`

:arg bar_style: style of box
(default: rcParams['scalebar.bar_style'] or ``simple``)
:type bar_style: :class:`str`

:arg font_properties: font properties of the label text, specified
either as dict or `fontconfig <http://www.fontconfig.org/>`_
pattern (XML).
Expand Down Expand Up @@ -370,6 +382,7 @@ def __init__(
self.box_alpha = box_alpha
self.scale_loc = scale_loc
self.label_loc = label_loc
self.bar_style = bar_style
self.scale_formatter = scale_formatter
self.font_properties = font_properties
self.fixed_value = fixed_value
Expand Down Expand Up @@ -403,6 +416,76 @@ def _calculate_exact_length(self, value, units):
newvalue = self.dimension.convert(value, units, self.units)
return newvalue / self.dx

def _draw_simple_rect(self, rotation, length_px, width_px, color):
if rotation == "horizontal":
rec = Rectangle(
(0, 0),
length_px,
width_px,
fill=True,
facecolor=color,
edgecolor="none",
)
else:
rec = Rectangle(
(0, 0),
width_px,
length_px,
fill=True,
facecolor=color,
edgecolor="none",
)
return [rec]

def _draw_geography_rect(
self,
rotation,
length_px,
width_px,
color,
value,
):
def calc_best_step(length):
capacity = int(math.floor(math.log10(length)))
step = math.pow(10, capacity)
if step * 2 > length:
step = step / 2
return step

step = calc_best_step(value)
step_px = step * length_px / value

current_color = color
second_color = "white"

filled_px = 0
rectangles = []
while filled_px < length_px:
if filled_px + step_px <= length_px:
rec_step_px = step_px
else:
rec_step_px = length_px - filled_px
if rotation == "horizontal":
rec_xy = (filled_px, 0)
rec_width = rec_step_px
rec_height = width_px
else:
rec_xy = (0, filled_px)
rec_width = width_px
rec_height = rec_step_px
rectangle = Rectangle(
rec_xy,
rec_width,
rec_height,
fill=True,
facecolor=current_color,
edgecolor=color
)
rectangles.append(rectangle)
current_color, second_color = second_color, current_color
filled_px += rec_step_px
return rectangles

def draw(self, renderer, *args, **kwargs):
self._info = None

Expand Down Expand Up @@ -446,6 +529,7 @@ def _get_value(attr, default):
box_alpha = _get_value("box_alpha", 1.0)
scale_loc = _get_value("scale_loc", "bottom").lower()
label_loc = _get_value("label_loc", "top").lower()
bar_style = _get_value("bar_style", "simple")
font_properties = self.font_properties
fixed_value = self.fixed_value
fixed_units = self.fixed_units or self.units
Expand Down Expand Up @@ -488,28 +572,20 @@ def _get_value(attr, default):

width_px = abs(ylim[1] - ylim[0]) * width_fraction

# Create scale bar
if rotation == "horizontal":
scale_rect = Rectangle(
(0, 0),
if not bar_style or bar_style == "simple":
scale_rects = self._draw_simple_rect(rotation, length_px, width_px, color)
elif bar_style == "geography":
scale_rects = self._draw_geography_rect(
rotation,
length_px,
width_px,
fill=True,
facecolor=color,
edgecolor="none",
)
else:
scale_rect = Rectangle(
(0, 0),
width_px,
length_px,
fill=True,
facecolor=color,
edgecolor="none",
color,
value,
)

scale_bar_box = AuxTransformBox(ax.transData)
scale_bar_box.add_artist(scale_rect)
for rect in scale_rects:
scale_bar_box.add_artist(rect)

# Create scale text
if scale_loc != "none":
Expand Down Expand Up @@ -796,6 +872,19 @@ def set_scale_formatter(self, scale_formatter):

scale_formatter = property(get_scale_formatter, set_scale_formatter)

def get_bar_style(self):
return self._bar_style

def set_bar_style(self, bar_style):
if bar_style is not None and bar_style not in _VALID_SCALE_STYLES:
raise ValueError(
f"Unknown bar_style: {bar_style}. "
f"Valid bar styles: {', '.join(_VALID_SCALE_STYLES)}"
)
self._bar_style = bar_style

bar_style = property(get_bar_style, set_bar_style)

def get_label_formatter(self):
warnings.warn(
"The get_label_formatter method is deprecated. "
Expand Down
11 changes: 11 additions & 0 deletions tests/test_scalebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def test_mpl_rcParams_update():
"scalebar.scale_loc": "bottom",
"scalebar.label_loc": "top",
"scalebar.rotation": "horizontal",
"scalebar.bar_style": "simple",
}
matplotlib.rcParams.update(params)

Expand Down Expand Up @@ -371,6 +372,16 @@ def test_bbox_transform(scalebar):
assert scalebar.bbox_transform == scalebar.axes.transAxes


@pytest.mark.parametrize("bar_style", ["simple", "geography"])
def test_bar_style(scalebar, bar_style):
assert scalebar.get_bar_style() is None
assert scalebar.bar_style is None

scalebar.set_bar_style(bar_style)
assert scalebar.get_bar_style() == bar_style
assert scalebar.bar_style == bar_style


def test_info():
fig = plt.figure()
ax = fig.add_subplot(111)
Expand Down