Skip to content

Contour maps optimization - #1849

Draft
cmdupuis3 wants to merge 5 commits into
mainfrom
cmd/contour-maps-opt-2
Draft

cmdupuis3 wants to merge 5 commits into
mainfrom
cmd/contour-maps-opt-2

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #XXX

Overview

Second round of contour map optimization. This PR uses algorithms from cartopy, so we should be sure about the licensing situation before merging.

PR Checklist

General

  • An issue is created and linked
  • Added appropriate labels (if your uxarray repo permissions allow it)
  • Filled out Overview and Expected Usage (if applicable) sections

Testing & Benchmarking

  • There is adequate test coverage of changes from this PR (add new tests if needed)
  • If this PR could affect performance, ran ASV benchmarks and confirmed they show expected behavior (add a new benchmark if necessary)

Documentation and Examples

  • Docstrings updated with any function changes, and included in all new functions
  • User (public) functions added to docs/api.rst; internal (private) function names start with an underscore (_)

AI Disclosure

AI Usage: Claude Opus 5.5

  • I have tested and take responsibility for all AI-generated content in my PR.

cmdupuis3 and others added 5 commits October 23, 2026 18:05
With a projection, almost all the time of a contour plot went into
GeoViews projecting the lines one at a time through Cartopy while
rendering: ~13 of the ~17 s for a noisy field on a 786k-face mesh.
GeoViews does this for any element that has a crs, even one already in
the plot's projection, and a plain HoloViews element would lose
GeoViews' map extents and options such as global_extent.

_project_lines replaces _drop_lines_outside_projection. It projects all
points and segment midpoints at once and applies Cartopy's own test,
from cartopy/trace.pyx, to every segment: the projected midpoint within
the projection's threshold of the segment, every point on the map, and
the segment covered by the map when the map is not convex. Lines that
pass are exactly what Cartopy gives; only the others, such as those
that cross the edge of the map or a seam of the projection, are
projected by Cartopy. The result matched Cartopy line by line, with no
difference in any coordinate, for 10 projections on four datasets and
both methods.

The lines are a subclass of gv.Contours with crs=projection, plotted
by GeoViews' own GeoContourPlot with its projection step turned off.
It is named Contours to share the options of contour lines, created on
first use since GeoViews is optional, and found by pickle through a
module __getattr__.

Noisy field, 786k faces, contour() + render:
  Robinson, Bokeh       17.1 s -> 2.3 s
  Robinson, Matplotlib  17.1 s -> 2.3 s
  Orthographic, Bokeh   16.1 s -> 5.7 s

GeoViews now frames a plot of the lines alone on the projected lines,
rather than on the projection of their longitude-latitude bounding box,
so the extent of some maps (Lambert conformal, orthographic) changes
slightly; the lines themselves are the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test that decides whether a projected segment can be kept as it is
was adapted from straightAndDomain() in lib/cartopy/trace.pyx of Cartopy
0.25.0: the position along the segment and the separation of the
projected midpoint from it, the tolerance that narrows towards the ends
of the segment, and the requirement that the map cover the segment.

Cartopy is released under the BSD 3-Clause License, which requires that
redistributed source keep its copyright notice, conditions and
disclaimer. The notice is reproduced verbatim above _project_lines,
where it travels with the source into every distribution, and the
docstring and comments name where the test comes from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an orthographic map about half the lines of a field are on the far
side of the globe, and none of their points can be projected. They
were still given to Cartopy one at a time, only to come back empty.

For a segment whose ends both cannot be projected, Cartopy's bisection
in lib/cartopy/trace.pyx accepts the end at once and _project_segment()
adds nothing, without looking at the points in between. A line with no
point that can be projected is therefore never on the map, and is now
dropped from the points already projected. The result still matches
Cartopy line by line for 10 projections on four datasets and both
methods, and projecting the lines of a 197k-face noisy field on an
orthographic map takes 0.85 s instead of 1.9 s.

The rule is credited to Cartopy next to the segment test, under the
same BSD 3-Clause notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tour

The "interpolated" method handed its triangulation to Matplotlib's
tricontour, which spent ~2.2 s of its ~2.6 s on a 786k-face mesh
setting up the neighbours of every triangle before contouring.

_crossed_edges, compiled with numba (cache=True, nogil=True), finds the
two edges where a level crosses each triangle that it crosses. Each
crossing point is known by its edge, which the triangles on either side
share, so the pieces are joined into lines by the existing
_join_segments kernel, and the points are interpolated along their
edges all at once. The triangulation is unchanged.

The lines are the same segments as tricontour's, to within 1e-9, on
eight datasets including node-centered data and faces with repeated
nodes; tricontour also left a few slivers ~1e-14 long on the
antimeridian, which are gone. A closed loop may start at a different
point, and a loop that crosses the antimeridian is then cut there
instead, which draws the same. Contouring a noisy field on the 786k-face
mesh takes 0.68 s instead of 3.4 s, of which 0.3 s is the
triangulation.

The splitting of lines at the antimeridian, shared with the edges
method, moves to _split_lines_at_antimeridian.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
contour-maps frames the lines by the area of the grid, set as their
extents, so that they never widen a shaded plot they are overlaid on.
Since the lines are projected before GeoViews sees them, HoloViews
framed the lines and their labels by the projected line data instead,
which reach closer to the antimeridian than the shaded faces: a Robinson
map of a field whose lines reach the edge of the map was 34 km wider on
each side than the shaded plot.

The projected area of the grid is now also the default xlim and ylim of
the lines, which HoloViews keeps to; options passed to .plot.contour()
still win. The frame then equals the shaded plot's own on Robinson,
Robinson centred on 180, orthographic, Lambert conformal and
PlateCarree maps, alone and overlaid, with the frame sizes and label
positions of contour-maps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cmdupuis3 cmdupuis3 self-assigned this Oct 23, 2026
@cmdupuis3 cmdupuis3 added scalability Related to scalability & performance efforts visualization Plotting or other visualizations labels Oct 23, 2026
@cmdupuis3
cmdupuis3 requested a review from dylannelson October 23, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scalability Related to scalability & performance efforts visualization Plotting or other visualizations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant