Furniture — Scale Bar & North Arrow#
The cleopatra.styling.furniture module draws the two remaining pieces of standard chart
furniture cleopatra did not have: a scale bar and a north arrow. Both are free
functions that decorate an existing matplotlib.axes.Axes and return the frameless inset axes
they drew on, exactly like stamp_mark — so they read alike and anchor identically (they share
stamp_mark's corner-placement plumbing) and use the same box / label_location /
label_size vocabulary as ColorBar.
The key design point is the package boundary: these are plain matplotlib artistry and know
nothing about geography. A scale bar is length axes data units wide with a caller-supplied
label string; a north arrow is rotated by a caller-supplied rotation in degrees. The
ellipsoidal questions — how long is 100 km in axis units at this latitude in this projection,
what is the grid convergence here — belong to whoever owns the CRS (the consumer), never to
this generic layer. So a micrograph with a µm bar, a floor plan, an engineering section and a map
all use the identical artist. There is no pyproj import here and no CRS logic of any kind.
Both draw the bar / arrow on a frameless inset axes in axes-fraction coordinates, so the
furniture stays anchored in its corner across a dpi or limits change rather than drifting like a
data-coordinate Rectangle, and sits at a high zorder above the data.
Scale bar#
import matplotlib.pyplot as plt
import numpy as np
from cleopatra.styling.furniture import add_scale_bar, ScaleBar
fig, ax = plt.subplots()
ax.imshow(np.random.default_rng(0).random((100, 100)), extent=[0, 500_000, 0, 500_000])
# the consumer computes the ground distance in axis units; cleopatra draws it
add_scale_bar(ax, 100_000, ScaleBar(label="100 km", location="lower left", segments=4, box=True))
length is in the axes' x data units; all the presentation options are grouped into a
ScaleBar object (mirroring FacetLayout / ColorBar), so the call stays small. label is the
caption you supply (defaulting to f"{length:g}"); segments sets the number of alternating
blocks (1 draws a plain bar); ticks=True (default) numbers the block boundaries 0 .. length,
a sequence numbers those data positions, and False draws no numbers. location is one of the
four corners; pad, height and the two colours are axes-fraction / matplotlib values;
label_location ("bottom" / "top" / None for the interior side) picks the caption side;
box adds a backing panel (True, a colour, or a dict of Rectangle kwargs).
North arrow#
from cleopatra.styling.furniture import add_north_arrow, NorthArrow
# rotation (grid convergence) is the caller's to supply — cleopatra never derives it
add_north_arrow(ax, 0.0, NorthArrow(location="upper right", style="arrow"))
rotation is degrees clockwise from up (e.g. the grid convergence at the map centre) and stays a
direct argument (like length on the scale bar); the arrow and its "N" label rotate together.
The presentation options are grouped into a NorthArrow object: style is "arrow" (a single
filled arrow), "needle" (a two-tone compass needle) or "rose" (a four-point compass star);
size, location, pad, label, the colours and box mirror ScaleBar.
GeoMixin sugar#
The six geographic glyphs (ArrayGlyph, MeshGlyph, VectorGlyph, FlowGlyph, PolygonGlyph,
ScatterGlyph) expose thin methods next to add_tiles / add_features / add_labels, so you
can decorate a glyph without importing the free functions or repeating the axes:
glyph.plot()
glyph.add_scale_bar(100_000, ScaleBar(label="100 km", location="lower left"))
glyph.add_north_arrow(grid_convergence_deg, NorthArrow(style="needle"))
The free functions stay the API; the methods only supply the glyph's axes.
cleopatra.styling.furniture.ScaleBar
dataclass
#
Presentation options for add_scale_bar (everything but the axes/length).
Grouped into one object -- mirroring FacetLayout / ColorBar -- so the
scale-bar call stays small: add_scale_bar(ax, length, ScaleBar(...)).
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str | None
|
The caption under (or over) the bar, e.g. |
location |
str
|
Which corner to anchor to -- one of |
pad |
float | tuple[float, float]
|
The gap between the bar and the axes edges, as an axes fraction --
a scalar for both axes or an |
height |
float
|
The bar thickness as an axes fraction. |
segments |
int
|
The number of alternating blocks. |
ticks |
bool | Sequence[float]
|
|
color |
str
|
The fill of the even blocks and the colour of the block outline, the ticks and the text. |
edge_color |
str
|
The fill of the odd blocks (the alternating light blocks). |
label_location |
str | None
|
|
label_size |
float | None
|
Font size (points) for the tick numbers and caption.
|
box |
bool | str | dict | None
|
A backing panel behind the bar, using |
zorder |
float | None
|
The draw order for the furniture. |
Source code in src/cleopatra/styling/furniture.py
cleopatra.styling.furniture.add_scale_bar(ax, length, spec=None)
#
Draw a segmented scale bar on ax, sized in the axes' own data units.
The bar is length data units wide (the caller computes that number --
cleopatra owns no geodesy), rendered as spec.segments alternating blocks
on a frameless inset axes anchored in one corner. Tick numbers at the block
boundaries and the caption are drawn on the parent axes in axes-fraction
coordinates, so the whole assembly stays put across a dpi or limits change
instead of drifting like a data-coordinate Rectangle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
Axes
|
The axes to decorate. Its current x-limits set the data-to-figure scale, so call this after the data is plotted and the limits are final. |
required |
length
|
float
|
The bar length in the axes' x data units. Must be finite
and |
required |
spec
|
ScaleBar | None
|
The presentation options as a |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Axes |
Axes
|
The frameless inset axes the bar was drawn on, so the caller can |
Axes
|
adjust it further. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
- A four-block "100 km" bar in the lower-left corner:
>>> import matplotlib >>> matplotlib.use("Agg") >>> import matplotlib.pyplot as plt >>> from cleopatra.styling.furniture import add_scale_bar, ScaleBar >>> fig, ax = plt.subplots() >>> ax.set_xlim(0, 500_000) (0.0, 500000.0) >>> bar = add_scale_bar( ... ax, 100_000, ScaleBar(label="100 km", location="lower left", segments=4) ... ) >>> len(bar.patches) # four alternating blocks 4 >>> plt.close(fig)
Source code in src/cleopatra/styling/furniture.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
cleopatra.styling.furniture.NorthArrow
dataclass
#
Presentation options for add_north_arrow (everything but the axes/rotation).
Grouped into one object -- mirroring ScaleBar / FacetLayout / ColorBar --
so the call stays small: add_north_arrow(ax, rotation, NorthArrow(...)).
Attributes:
| Name | Type | Description |
|---|---|---|
location |
str
|
Which corner to anchor to -- one of |
pad |
float | tuple[float, float]
|
The gap to the axes edges, as an axes fraction -- a scalar or an
|
size |
float
|
The arrow height as an axes fraction. |
style |
str
|
|
label |
str | None
|
The label at the arrow tip. Defaults to |
color |
str
|
The primary fill and the outline / label colour. |
edge_color |
str
|
The secondary (alternating) flank fill of a needle / rose. |
label_size |
float | None
|
Font size (points) for the label. |
box |
bool | str | dict | None
|
A backing panel, using |
zorder |
float | None
|
The draw order. |
Source code in src/cleopatra/styling/furniture.py
cleopatra.styling.furniture.add_north_arrow(ax, rotation=0.0, spec=None)
#
Draw a north arrow on ax, rotated by a caller-supplied angle.
The arrow is drawn undistorted on a frameless inset axes anchored in one
corner and rotated rotation degrees clockwise from straight up (the caller
supplies the grid convergence -- cleopatra owns no CRS). The "N" label
rotates with it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
Axes
|
The axes to decorate. |
required |
rotation
|
float
|
Degrees clockwise from up to rotate the arrow (e.g. the grid
convergence at the map centre). Must be finite. Defaults to |
0.0
|
spec
|
NorthArrow | None
|
The presentation options as a |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Axes |
Axes
|
The frameless inset axes the arrow was drawn on. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
- A plain north arrow in the upper-right corner:
Source code in src/cleopatra/styling/furniture.py
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 | |