Geographic Projections & Features¶
In this tutorial, you’ll learn about:¶
Selecting appropriate projections for your specific visualization needs
Adding geographic features to provide context for your data
Customizing projection parameters to optimize your visualizations
Related Documentation
Prerequisites¶
Time to learn: 5 minutes
Introduction¶
When visualizing geospatial data on unstructured grids, choosing the right map projection is crucial for accurately representing your data. Geographic projections transform the three-dimensional surface of the Earth onto a two-dimensional plane, each offering different trade-offs between preserving area, distance, direction, or shape.
UXarray leverages Cartopy’s robust projection capabilities to provide flexible geographic visualization options. Through integration with hvPlot and GeoViews, UXarray allows you to easily switch between different projections and add geographic features like coastlines and borders to enhance your visualizations.
We’ll use practical examples to demonstrate these capabilities, focusing on common use cases in climate science, oceanography, and other geophysical applications where unstructured grids are prevalent.
import cartopy.crs as ccrs
import uxarray as uxgrid_path = "../../meshfiles/oQU480.grid.nc"
data_path = "../../meshfiles/oQU480.data.nc"
uxds = ux.open_dataset(grid_path, data_path)Projections¶
The geographic projection used for visualization can be specified through the projection parameter in UXarray’s plotting methods. UXarray utilizes Cartopy’s Coordinate Reference System (CRS) objects to define these projections.
The simplest way to set a projection is to create a Cartopy CRS object and pass it to the plotting method.
projection = ccrs.Orthographic()
projectionuxds["bottomDepth"].plot.polygons(projection=projection)---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/dimension.py:1493, in Dimensioned._repr_mimebundle_(self, include, exclude)
1486 def _repr_mimebundle_(self, include=None, exclude=None):
1487 """Resolves the class hierarchy for the class rendering the
1488 object using any display hooks registered on Store.display
1489 hooks. The output of all registered display_hooks is then
1490 combined and returned.
1491
1492 """
-> 1493 return Store.render(self)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/options.py:1519, in Store.render(cls, obj)
1517 data, metadata = {}, {}
1518 for hook in hooks:
-> 1519 ret = hook(obj)
1520 if ret is None:
1521 continue
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/ipython/display_hooks.py:330, in pprint_display(obj)
328 if not ip.display_formatter.formatters["text/plain"].pprint:
329 return None
--> 330 return display(obj, raw_output=True)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/ipython/display_hooks.py:298, in display(obj, raw_output, **kwargs)
296 elif isinstance(obj, (CompositeOverlay, ViewableElement)):
297 with option_state(obj):
--> 298 output = element_display(obj)
299 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
300 with option_state(obj):
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/ipython/display_hooks.py:193, in display_hook.<locals>.wrapped(element)
191 try:
192 max_frames = OutputSettings.options["max_frames"]
--> 193 mimebundle = fn(element, max_frames=max_frames)
194 if mimebundle is None:
195 return {}, {}
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/ipython/display_hooks.py:231, in element_display(element, max_frames)
228 if type(element) not in Store.registry[backend]:
229 return None
--> 231 return render(element)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/ipython/display_hooks.py:62, in render(obj, **kwargs)
59 if renderer.fig == "pdf":
60 renderer = renderer.instance(fig="png")
---> 62 return renderer.components(obj, **kwargs)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/renderer.py:459, in Renderer.components(self, obj, fmt, comm, **kwargs)
457 embed = not (dynamic or streams or self.widget_mode == "live") or config.embed
458 if embed or config.comms == "default":
--> 459 return self._render_panel(plot, embed, comm)
460 return self._render_ipywidget(plot)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/renderer.py:466, in Renderer._render_panel(self, plot, embed, comm)
464 doc = Document()
465 with config.set(embed=embed):
--> 466 model = plot.layout._render_model(doc, comm)
467 if embed:
468 return render_model(model, comm)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/viewable.py:803, in Viewable._render_model(self, doc, comm)
801 if comm is None:
802 comm = state._comm_manager.get_server_comm()
--> 803 model = self.get_root(doc, comm)
805 if self._design and self._design.theme.bokeh_theme:
806 doc.theme = self._design.theme.bokeh_theme
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/layout/base.py:386, in Panel.get_root(self, doc, comm, preprocess)
382 def get_root(
383 self, doc: Document | None = None, comm: Comm | None = None,
384 preprocess: bool = True
385 ) -> Model:
--> 386 root = super().get_root(doc, comm, preprocess)
387 # ALERT: Find a better way to handle this
388 if hasattr(root, 'styles') and 'overflow-x' in root.styles:
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/viewable.py:733, in Renderable.get_root(self, doc, comm, preprocess)
731 wrapper = self._design._wrapper(self)
732 if wrapper is self:
--> 733 root = self._get_model(doc, comm=comm)
734 if preprocess:
735 self._preprocess(root)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/layout/base.py:370, in Panel._get_model(self, doc, root, parent, comm)
368 root = root or model
369 self._models[root.ref['id']] = (model, parent)
--> 370 objects, _ = self._get_objects(model, [], doc, root, comm)
371 props = self._get_properties(doc)
372 props[self._property_mapping['objects']] = objects
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/layout/base.py:352, in Panel._get_objects(self, model, old_objects, doc, root, comm)
350 else:
351 try:
--> 352 child = pane._get_model(doc, root, model, comm)
353 except RerenderError as e:
354 if e.layout is not None and e.layout is not self:
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/pane/holoviews.py:494, in HoloViews._get_model(self, doc, root, parent, comm)
492 plot = self.object
493 else:
--> 494 plot = self._render(doc, comm, root)
496 plot.pane = self
497 backend = plot.renderer.backend
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/panel/pane/holoviews.py:588, in HoloViews._render(self, doc, comm, root)
585 if comm:
586 kwargs['comm'] = comm
--> 588 return renderer.get_plot(self.object, **kwargs)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/bokeh/renderer.py:77, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
70 @bothmethod
71 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
72 """Given a HoloViews Viewable return a corresponding plot instance.
73 Allows supplying a document attach the plot to, useful when
74 combining the bokeh model with another plot.
75
76 """
---> 77 plot = super().get_plot(obj, doc, renderer, **kwargs)
78 if plot.document is None:
79 plot.document = Document() if self_or_cls.notebook_context else curdoc()
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/renderer.py:300, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
296 defaults = [kd.default for kd in plot.dimensions]
297 init_key = tuple(
298 v if d is None else d for v, d in zip(plot.keys[0], defaults, strict=None)
299 )
--> 300 plot.update(init_key)
301 else:
302 plot = obj
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/plot.py:1086, in DimensionedPlot.update(self, key)
1084 def update(self, key):
1085 if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn:
-> 1086 return self.initialize_plot()
1087 item = self.__getitem__(key)
1088 self.traverse(lambda x: setattr(x, "_updated", True))
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/geoviews/plotting/bokeh/plot.py:121, in GeoPlot.initialize_plot(self, ranges, plot, plots, source)
119 def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
120 opts = {} if isinstance(self, HvOverlayPlot) else {'source': source}
--> 121 fig = super().initialize_plot(ranges, plot, plots, **opts)
122 style_element = self.current_frame.last if self.batched else self.current_frame
123 el_ranges = match_spec(style_element, self.current_ranges) if self.current_ranges else {}
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/bokeh/element.py:2525, in ElementPlot.initialize_plot(self, ranges, plot, plots, source)
2523 # Initialize plot, source and glyph
2524 if plot is None:
-> 2525 plot = self._init_plot(key, style_element, ranges=ranges, plots=plots)
2526 self._populate_axis_handles(plot)
2527 else:
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/bokeh/element.py:1150, in ElementPlot._init_plot(self, key, element, plots, ranges)
1146 subplots = list(self.subplots.values()) if self.subplots else []
1148 axis_specs = {"x": {}, "y": {}}
1149 axis_specs["x"]["x"] = (
-> 1150 *self._axis_props(plots, subplots, element, ranges, pos=0),
1151 self.xaxis,
1152 {},
1153 )
1154 if self.multi_y and subplots:
1155 if not BOKEH_GE_3_2_0:
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/plotting/bokeh/element.py:936, in ElementPlot._axis_props(self, plots, subplots, element, ranges, pos, dim, range_tags_extras, extra_range_name)
934 else:
935 try:
--> 936 l, b, r, t = self.get_extents(range_el, ranges, dimension=dim)
937 except TypeError:
938 # Backward compatibility for e.g. GeoViews=<1.10.1 since dimension
939 # is a newly added keyword argument in HoloViews 1.17
940 l, b, r, t = self.get_extents(range_el, ranges)
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/geoviews/plotting/plot.py:70, in ProjectionPlot.get_extents(self, element, ranges, range_type, **kwargs)
68 extents = None
69 else:
---> 70 extents = project_extents(extents, element.crs, proj)
71 return (np.nan,)*4 if not extents else extents
File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/geoviews/util.py:72, in project_extents(extents, src_proj, dest_proj, tol)
67 def project_extents(extents, src_proj, dest_proj, tol=1e-6):
68 x1, y1, x2, y2 = extents
70 if (isinstance(src_proj, ccrs.PlateCarree) and
71 not isinstance(dest_proj, ccrs.PlateCarree) and
---> 72 src_proj.proj4_params['lon_0'] != 0):
73 xoffset = src_proj.proj4_params['lon_0']
74 x1 = x1 - xoffset
KeyError: 'lon_0':Image [Longitude,Latitude] (Longitude_Latitude bottomDepth)When visualizing data concentrated in specific geographic regions, adjusting the projection’s center point can significantly improve the visualization’s clarity and accuracy. Many projections, including the Orthographic projection, provide central_longitude and central_latitude parameters for this purpose.
This capability is particularly valuable when working with data near the antimeridian (180°/ longitude). For instance, data centered around the Pacific Ocean often crosses this boundary, which can cause visualization artifacts or split features across the edges of the plot. By setting central_longitude=180, you can shift the projection’s center to properly display these regions:
projection = ccrs.Orthographic(central_longitude=-180)
projectionuxds["bottomDepth"].plot.polygons(projection=projection)Features¶
Geographic features are specified through the features parameter in your plotting command. You can add multiple features by providing them as a list.
Available features include ‘borders’, ‘coastline’, ‘lakes’, ‘land’, ‘ocean’, ‘rivers’ and ‘states’.
uxds["bottomDepth"].plot.polygons(
projection=projection, features=["borders", "coastline"]
)The resolution of geographic features can be controlled by specifying a scale in the features parameter. This is done by passing a dictionary that maps each feature to its desired scale. There are three resolution levels: ‘10m’ (highest detail), ‘50m’ (medium detail), and ‘110m’ (lowest detail).
uxds["bottomDepth"].plot.polygons(
projection=projection, features={"borders": "10m", "coastline": "10m"}
)