Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Geographic Projections & Features

UXarray logo

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

ConceptsImportanceNotes
CartopyNecessary
GeoViewsHelpfull

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.

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.

Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
---------------------------------------------------------------------------
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:

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’.

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).