ArrayGlyph¶
- This script demonstrates the usage of the ArrayGlyph class from the Cleopatra package. The ArrayGlyph class provides functionality for visualizing arrays and creating animations.
import os
import matplotlib.pyplot as plt
import numpy as np
from cleopatra.config import Config
Config.set_matplotlib_backend()
from cleopatra.glyphs.base.animation import embed_gif
from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph, ColorBar, PointOverlay, RgbBands
# Set the random seed for reproducibility
np.random.seed(42)
from cleopatra.styling.scaling import ColorScaling
from cleopatra.styling.params import CellValues, DataStyle
1. Creating and Visualizing a Simple Array¶
Let's start by creating a simple 2D array and visualizing it with ArrayGlyph.
# Create a simple 2D array
simple_array = np.zeros((10, 10))
for i in range(10):
for j in range(10):
simple_array[i, j] = i * j # Create a multiplication table pattern
# Initialize the ArrayGlyph with the array
array_glyph = ArrayGlyph(simple_array)
# Plot the array with default settings
fig, ax = array_glyph.plot()
# Display information about the array
print(f"Array shape: {simple_array.shape}")
print(f"Min value: {simple_array.min()}")
print(f"Max value: {simple_array.max()}")
Array shape: (10, 10) Min value: 0.0 Max value: 81.0
2. Customizing the Array Visualization¶
The ArrayGlyph class provides many options for customizing the visualization.
Create a more complex array
complex_array = np.random.rand(20, 20) # Random values between 0 and 1
complex_array[5:15, 5:15] = (
complex_array[5:15, 5:15] * 2
) # Increase values in the center
- Initialize the ArrayGlyph with the array
array_glyph_custom = ArrayGlyph(complex_array)
# Plot with custom settings
fig, ax = array_glyph_custom.plot(
cmap="viridis", # Use the viridis colormap
title="Custom Array Plot", # Add a title
colorbar=ColorBar(ticks_spacing=0.5), # Set tick spacing
figsize=(10, 8), # Set figure size
)
3. Using Different Color Scales¶
- The ArrayGlyph class supports different color scales for visualizing arrays.
- Create an array with positive and negative values
mixed_array = np.random.randn(15, 15) # Random values from a normal distribution
- Initialize the ArrayGlyph with the array
array_glyph_scales = ArrayGlyph(mixed_array)
3.1 Linear Scale (default)¶
fig1, ax1 = array_glyph_scales.plot(cmap="RdBu_r", title="Linear Scale", color=ColorScaling.linear())
3.2 Power Scale¶
array_glyph_scales = ArrayGlyph(mixed_array)
fig2, ax2 = array_glyph_scales.plot(cmap="RdBu_r", title="Power Scale", color=ColorScaling.power(gamma=0.5))
3.3 Log Scale¶
# For log scale, we need positive values
positive_array = np.abs(mixed_array) + 0.1 # Make all values positive and non-zero
array_glyph_positive = ArrayGlyph(positive_array)
fig3, ax3 = array_glyph_positive.plot(cmap="viridis", title="Log Scale", color=ColorScaling.sym_log())
3.4 Midpoint Scale¶
array_glyph_scales = ArrayGlyph(mixed_array)
fig4, ax4 = array_glyph_scales.plot(# Set the midpoint at 0
cmap="RdBu_r", title="Midpoint Scale", color=ColorScaling.midpoint(at=0))
4. Displaying Cell Values¶
- The ArrayGlyph class can display the values of each cell in the array.
- Create a small array for better visibility of cell values
# Create a small array for better visibility of cell values
small_array = np.random.randint(0, 100, (5, 5))
# Initialize the ArrayGlyph with the array
array_glyph_values = ArrayGlyph(small_array)
# Plot with cell values displayed
fig, ax = array_glyph_values.plot(
cmap="viridis", # Use the viridis colormap
title="Array with Cell Values", figsize=(8, 6),
cells=CellValues(show=True, size=12))
5. Displaying Points on the Array¶
- The ArrayGlyph class can display points on top of the array.
# Create an array
point_array = np.random.rand(15, 15)
# Create some points to display on the array
# Points are specified as [value, row, column] coordinates
points = np.array(
[
[1, 3, 3], # Top-left region
[2, 3, 11], # Top-right region
[3, 11, 3], # Bottom-left region
[4, 11, 11], # Bottom-right region
]
)
# Style the points via a PointOverlay: locations plus marker/label styling
point_overlay = PointOverlay(
points,
color="red", # Set point color
size=100, # Set point size
label_color="blue", # Set point-value label color
label_size=16, # Set point-value label size
)
# Initialize the ArrayGlyph with the array
array_glyph_points = ArrayGlyph(point_array)
# Plot with points
fig, ax = array_glyph_points.plot(
points=point_overlay, # Points to display
cmap="viridis", # Use the viridis colormap
title="Array with Points",
figsize=(10, 8),
)
6. Creating an Animation¶
- The ArrayGlyph class can create animations of arrays changing over time.
# Create a 3D array for the animation
n_frames = 10
x = np.linspace(0, 10, 20)
y = np.linspace(0, 10, 20)
X, Y = np.meshgrid(x, y)
# Create a 3D array with shape (n_frames, 20, 20)
animation_array = np.zeros((n_frames, 20, 20))
for i in range(n_frames):
# Create a wave pattern that changes with time
animation_array[i] = np.sin(X + i * 0.5) * np.cos(Y + i * 0.5)
# Initialize the ArrayGlyph with the 3D array
array_glyph_animation = ArrayGlyph(animation_array)
# Create the animation
anim = array_glyph_animation.animate(
time=list(range(n_frames)), # Time points
points=None, # No points to display
interval=200, # Interval between frames (ms)
cmap="viridis", # Use the viridis colormap
title="Array Animation",
figsize=(10, 8),
)
# Display the animation inline as a looping GIF. `embed_gif` renders it to a GIF
# wrapped in an IPython Image, which every notebook viewer plays (JupyterLab,
# the IDE, GitHub, the docs site) -- more portable than `anim.to_jshtml()`,
# whose embedded JavaScript some IDE notebook renderers do not run (the
# animation then shows as a single frozen frame). Close the animation's figure
# first so the inline backend does not also emit a stray static frame beside it.
plt.close(array_glyph_animation.fig)
embed_gif(anim, fps=5)
<IPython.core.display.Image object>
- Note: To save the animation, you would use:
array_glyph_animation.save_animation("animation.gif", fps=5)
os.listdir(".")
['rgb_animation.ipynb', 'reference_map.ipynb', 'array_glyph_examples.ipynb', 'animation.gif']
7. Preparing Arrays with Different Methods¶
- The ArrayGlyph class provides methods for preparing arrays for visualization.
7.1 RGB percentile stretch¶
RGB images are composited with an RgbBands object passed as rgb_bands=: the band indices to pull from a
band-first array, plus a stretch. A percentile stretch enhances contrast by clipping the histogram tails
(here the 2nd/98th percentiles).
# RGB compositing is configured with an RgbBands object (band indices + stretch).
# Build a synthetic band-first (3, H, W) image.
rgb_stack = np.random.default_rng(0).integers(0, 10000, size=(3, 40, 40)).astype(float)
# Percentile stretch: clip the 2nd/98th percentile tails and rescale to [0, 1].
array_glyph_percentile = ArrayGlyph(
rgb_stack, rgb_bands=RgbBands([0, 1, 2], percentile=2)
)
fig, ax = array_glyph_percentile.plot(
title="RGB with percentile stretch", figsize=(8, 6)
)
/home/runner/work/cleopatra/cleopatra/.venv/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py:4786: UserWarning: Warning: 'partition' will ignore the 'mask' of the MaskedArray. arr.partition(
7.2 RGB surface-reflectance normalisation (with per-band cutoff)¶
surface_reflectance scales raw satellite counts into [0, 1] (e.g. 10000 for Sentinel-2). An optional
cutoff then clips each band to a fraction of that range and rescales it to [0, 1] for extra contrast, one
value per band.
# Surface-reflectance normalisation with an optional per-band cutoff.
array_glyph_cutoff = ArrayGlyph(
rgb_stack,
rgb_bands=RgbBands([0, 1, 2], surface_reflectance=10000, cutoff=[0.3, 0.3, 0.3]),
)
fig, ax = array_glyph_cutoff.plot(
title="RGB with surface-reflectance + cutoff", figsize=(8, 6)
)
7.3 Preparing an Array with Excluded Values¶
# Create an array with some specific values to exclude
exclude_array = np.random.rand(15, 15)
exclude_array[5:10, 5:10] = -9999 # Set some values to -9999 (to be excluded)
# Initialize the ArrayGlyph with excluded values
array_glyph_exclude = ArrayGlyph(
exclude_array,
exclude_value=[-9999], # Exclude values equal to -9999
)
# Plot the array
fig, ax = array_glyph_exclude.plot(title="Array with Excluded Values", figsize=(8, 6))
8. Creating a Custom Colorbar¶
- The ArrayGlyph class allows customization of the colorbar.
# Create an array
colorbar_array = np.random.default_rng(42).random((15, 15))
# Initialize the ArrayGlyph with the array
array_glyph_colorbar = ArrayGlyph(colorbar_array)
# Plot with a custom colorbar
fig, ax = array_glyph_colorbar.plot(
colorbar=ColorBar(label="Values", length=0.8, orientation="horizontal"),
cmap="plasma", # Use the plasma colormap
title="Array with Custom Colorbar",
figsize=(10, 8),
)
9. Relief shading (hillshade) for DEMs¶
A DEM with a wide elevation range cannot be read from colour alone: a flat plain and a high plateau land on
different colours, but both look featureless. The hillshade option blends terrain illumination into the
colour-mapped surface, so it reads by form (slopes, ridges, valleys) independent of the elevation range.
Pass it via data_style=DataStyle(hillshade=True) for defaults, or a dict to tune vert_exag (relief
contrast), azimuth/altitude (light position), blend_mode, or multidirectional.
# a wide-range synthetic DEM: a broad gentle plain plus a steep ridge and fine texture
yy, xx = np.mgrid[0:140, 0:180]
dem = (
10
+ 0.3 * yy
+ 900 * np.exp(-(((xx - 130) / 20) ** 2 + ((yy - 70) / 45) ** 2))
+ 30 * np.sin(xx / 7) * np.cos(yy / 7)
)
dem.min(), dem.max()
(np.float64(-19.99994603115228), np.float64(939.1411834292655))
Colour only: the terrain colormap shows the big ridge, but the plain and the fine texture are almost flat.
glyph = ArrayGlyph(dem, cmap='terrain')
glyph.plot(title='terrain colormap only')
(<Figure size 682.022x490.333 with 2 Axes>,
<Axes: title={'center': 'terrain colormap only'}>)
With hillshade: the same colormap, now relief-shaded, so every slope and ridge is legible -- and the
colorbar still reflects the elevation scale.
glyph = ArrayGlyph(dem, cmap='terrain')
glyph.plot(data_style=DataStyle(hillshade={'vert_exag': 8}), title='+ hillshade')
(<Figure size 682.022x490.333 with 2 Axes>,
<Axes: title={'center': '+ hillshade'}>)
The relief is the part colour alone could not provide. hillshade is a shared capability, not an ArrayGlyph
special case: it works the same way on any glyph whose data is a continuous surface -- MeshGlyph (a
triangulated terrain, plot(z, location='node', data_style=DataStyle(hillshade=True))) and KDEGlyph
(relief-shading a density surface). It composes with the topography data-style preset and the diverging
center=0 sea-level hinge.
10. Data-style presets via data_style=DataStyle(style=...)¶
Instead of choosing a colormap and scale by hand, pass a named preset from
cleopatra.styling.colors.DATA_STYLES via data_style=DataStyle(style="..."). The preset owns the colormap,
the norm (linear / log / symlog / diverging), the transparent nodata, any value-linked opacity, and — for
categorical presets — a discrete legend. The same data_style= option works on MeshGlyph and KDEGlyph
too, and composes with hillshade.
# A DEM styled by the 'topography' preset, and the same DEM relief-shaded (style + hillshade compose)
topo = ArrayGlyph(dem)
topo.plot(data_style=DataStyle(style='topography'), title="style='topography'")
topo_shaded = ArrayGlyph(dem)
topo_shaded.plot(data_style=DataStyle(style='topography', hillshade={'vert_exag': 8}), title="style='topography' + hillshade")
(<Figure size 598x490.333 with 1 Axes>,
<Axes: title={'center': "style='topography' + hillshade"}>)
Hydrology rasters have dedicated presets: flow_accumulation (symlog Blues, low cells fade) and the categorical flow_direction_d8 (the eight D8 class codes with a discrete legend).
import numpy as np
# a synthetic flow-accumulation field and a D8 direction raster
accum = np.abs(np.random.default_rng(0).normal(size=(80, 100))).cumsum(axis=1) * 80
d8 = (
np.random.default_rng(1)
.choice([1, 2, 4, 8, 16, 32, 64, 128], size=(40, 40))
.astype(float)
)
accum_glyph = ArrayGlyph(accum)
accum_glyph.plot(data_style=DataStyle(style='flow_accumulation'), title="style='flow_accumulation'")
d8_glyph = ArrayGlyph(d8)
d8_glyph.plot(data_style=DataStyle(style='flow_direction_d8'), title="style='flow_direction_d8'")
(<Figure size 466x490.333 with 1 Axes>,
<Axes: title={'center': "style='flow_direction_d8'"}>)
Restyling an existing glyph: apply_style() and .style¶
You can apply -- or change -- a preset on a glyph that already exists, without rebuilding it. apply_style(name) re-renders the glyph in place by preset name, glyph.style reads the applied preset back, and the style is sticky (a later plain plot() keeps it) and clearable (plot(data_style=DataStyle(style=None))). apply_style takes ownership of the glyph's own axes -- don't use it on an axes shared with other content.
glyph = ArrayGlyph(accum)
glyph.plot(title='default colouring')
print('style before:', glyph.style)
glyph.apply_style('flow_accumulation') # restyle in place, by name
print('style after apply_style:', glyph.style)
glyph.plot(title='still flow_accumulation (sticky)') # a plain plot keeps it
glyph.plot(title='cleared back to default', data_style=DataStyle(style=None))
print('style after plot(style=None):', glyph.style)
style before: None style after apply_style: flow_accumulation style after plot(style=None): None
Summary¶
In this notebook, we've explored the ArrayGlyph class from the Cleopatra package.
We've seen how to:
- Create and visualize simple arrays
- Customize array visualizations
- Use different color scales
- Display cell values
- Display points on arrays
- Create animations
- Prepare arrays with different methods
- Create custom colorbars
The ArrayGlyph class provides powerful tools for visualizing and animating arrays.