Unstructured Mesh Visualization Guide#
This guide explains what unstructured meshes are, their components,
how they differ from regular grids, and how to visualize them with
Cleopatra's MeshGlyph class.
What Is an Unstructured Mesh?#
A regular (structured) grid divides space into a uniform array of
rectangles. Every cell has the same shape and size, and cell positions
are defined implicitly by row and column indices. This is what
ArrayGlyph visualizes.
An unstructured mesh divides space into arbitrary polygons -- typically triangles, quadrilaterals, or a mix of both. Cell shapes and sizes vary freely across the domain, which allows:
- Higher resolution where it matters (around structures, along coastlines, in steep gradients) without wasting cells in uniform regions.
- Flexible geometry that conforms to irregular boundaries like river banks, coastlines, or building footprints.
- Mixed elements combining triangles and quads in the same mesh.
Unstructured meshes are widely used in computational fluid dynamics (CFD), finite element analysis (FEA), and hydrodynamic modeling (Delft3D FM, MIKE FM, SCHISM, ADCIRC).
Structured Grid Unstructured Mesh
+---+---+---+---+ +-------+
| | | | | / \ / \
+---+---+---+---+ / \ / \
| | | | | +-----+-----+
+---+---+---+---+ \ / \ . / \
| | | | | \ / . .\/ . \
+---+---+---+---+ +-----+-----+
Mesh Components#
An unstructured mesh is defined by three types of topological elements:
Nodes (Vertices)#
The fundamental building blocks. Each node is a point in 2D space
with an (x, y) coordinate.
In MeshGlyph, nodes are passed as two separate 1D arrays
(node_x and node_y), each of length n_nodes.
Faces (Cells / Elements)#
Polygonal regions bounded by edges. Each face is defined by an ordered list of node indices that form its vertices.
# Two triangular faces referencing 4 nodes
face_node_connectivity = np.array([
[0, 1, 2], # triangle: nodes 0, 1, 2
[1, 3, 2], # triangle: nodes 1, 3, 2
])
This is the face-node connectivity array, shaped
(n_faces, max_nodes_per_face). For mixed meshes where faces have
different numbers of vertices, shorter rows are padded with a
fill value (typically -1):
# Mixed mesh: 1 quad + 1 triangle
face_node_connectivity = np.array([
[0, 1, 4, 3], # quad: 4 nodes
[1, 2, 5, -1], # triangle: 3 nodes + padding
])
Faces are where simulation results are most commonly defined -- water depth, velocity magnitude, pollutant concentration, etc.
Edges#
Line segments connecting two nodes. Each edge is shared by at most two faces. Edges are defined by a 2-column array of node index pairs:
edge_node_connectivity = np.array([
[0, 1], # edge between node 0 and node 1
[1, 2],
[2, 0],
[1, 3],
[3, 2],
])
Edges are optional in MeshGlyph. If not provided, they are
derived automatically from the face-node connectivity (with
deduplication). Providing them explicitly is faster for large meshes
since it avoids the derivation step.
Data Locations#
Simulation data can be defined at different mesh locations:
Face-Centered Data#
One value per face. This is the most common output from finite volume models. Each face gets a single flat color when plotted.
MeshGlyph uses matplotlib's tripcolor for face-centered rendering.
Internally, polygonal faces are decomposed into triangles via fan
triangulation (a quad becomes 2 triangles, a pentagon becomes 3,
etc.), and each sub-triangle receives its parent face's value.
Node-Centered Data#
One value per node. Used when data is defined at vertices (e.g., finite element solutions). Produces smooth interpolated contours.
MeshGlyph uses matplotlib's tricontourf for node-centered
rendering, which interpolates between node values to produce smooth
filled contour plots.
The UGRID Convention#
UGRID is a standard for storing unstructured mesh data in NetCDF files. It defines how mesh topology (nodes, edges, faces, connectivity arrays) and data variables are organized.
A typical UGRID file contains:
| Variable | Shape | Description |
|---|---|---|
mesh2d_node_x |
(n_nodes,) |
Node x-coordinates |
mesh2d_node_y |
(n_nodes,) |
Node y-coordinates |
mesh2d_face_nodes |
(n_faces, max_nodes) |
Face-node connectivity |
mesh2d_edge_nodes |
(n_edges, 2) |
Edge-node connectivity |
mesh2d_waterdepth |
(n_faces,) |
Face-centered data |
mesh2d_s1 |
(n_nodes,) |
Node-centered data |
MeshGlyph maps directly to these arrays:
import netCDF4 as nc
ds = nc.Dataset("output.nc")
mg = MeshGlyph(
node_x=ds["mesh2d_node_x"][:],
node_y=ds["mesh2d_node_y"][:],
face_node_connectivity=ds["mesh2d_face_nodes"][:],
fill_value=ds["mesh2d_face_nodes"]._FillValue,
edge_node_connectivity=ds["mesh2d_edge_nodes"][:],
)
fig, ax = mg.plot(ds["mesh2d_waterdepth"][:], title="Water Depth")
Fan Triangulation#
Matplotlib can only render triangles natively. When a mesh contains
quads, pentagons, or other polygons, MeshGlyph decomposes them into
triangles using fan triangulation:
Fan triangulation of a quad:
3 ---- 2 3 ---- 2 3 ---- 2
| | | / | | \ |
| | --> | / | or | \ |
| | | / | | \ |
0 ---- 1 0 ---- 1 0 ---- 1
Original Fan from Fan from
quad node 0 node 3
MeshGlyph fans from the first vertex of each face. A face with
N valid nodes produces N - 2 triangles. All sub-triangles inherit
the same data value from their parent face.
For pure-triangle meshes (the most common case), this step is skipped entirely -- the face-node connectivity is already the triangle array.
Visualization Modes#
Face Plot (location="face")#
Each face is colored by its data value using flat shading. Good for showing discrete per-cell results from finite volume models.
Node Contour Plot (location="node")#
Smooth filled contours interpolated between node values. Good for showing continuous fields from finite element models.
Wireframe Outline#
Renders mesh edges only, without data. Useful for inspecting mesh quality, resolution distribution, or overlaying on a data plot.
Since MeshGlyph stores fig/ax as instance state, you can overlay
a wireframe on a data plot by calling both methods in sequence:
mg = MeshGlyph(node_x, node_y, faces)
mg.plot(data, cmap="Blues")
mg.plot_outline(color="white", linewidth=0.1)
Animation#
For time-varying data on a fixed mesh (e.g., a hydrodynamic
simulation), animate() creates a frame-by-frame animation:
# frames: shape (n_timesteps, n_faces)
mg.animate(frames, time=time_labels, cmap="plasma")
mg.save_animation("simulation.gif", fps=5)
Color Scales#
MeshGlyph supports all 5 color scale types inherited from the
Glyph base class:
| Scale | Use case | Key parameters |
|---|---|---|
linear |
Default. Uniform mapping from data to color. | vmin, vmax |
power |
Emphasize low or high values. | gamma (< 1 emphasizes low) |
sym-lognorm |
Data spanning many orders of magnitude with a zero crossing. | line_threshold, line_scale |
boundary-norm |
Discrete color bins at specific thresholds. | bounds (list of boundaries) |
midpoint |
Split the colormap at a meaningful value (e.g., zero for difference plots). | midpoint |
# Example: midpoint scale for a difference plot
mg.plot(
difference_data,
color_scale="midpoint",
midpoint=0.0,
cmap="RdBu_r",
cbar_label="Change [m]",
)
Comparison: ArrayGlyph vs MeshGlyph#
| Aspect | ArrayGlyph | MeshGlyph |
|---|---|---|
| Data type | Regular 2D/3D numpy arrays | Unstructured mesh (nodes + connectivity) |
| Rendering | imshow / matshow |
tripcolor / tricontourf |
| Cell shape | Uniform rectangles | Arbitrary polygons (triangles, quads, mixed) |
| Resolution | Uniform everywhere | Variable -- dense where needed |
| Wireframe | N/A | plot_outline() |
| Face vs Node | N/A (always grid cells) | location="face" or location="node" |
| Color scales | All 5 types | All 5 types |
| Animation | animate() over 3D array slices |
animate() over list of data arrays |
| RGB support | Yes | No |
| Shared base | Glyph |
Glyph |