Geographic data visualization and analysis with EOmaps: Interactive maps in python!
Tags: python, datascience, visualization, opensource
Published: 2023-07-12 10:29:30

This article is about [EOmaps](https://github.com/raphaelquast/EOmaps): A `python` package that helps you to create beautiful interactive maps with a few lines of code.

It provides many useful tools to **create publication ready maps** and allows you to use the maps for **interactive geo-data analysis**.


The main features are:

- A comprehensive python API to create highly customized maps
- A GUI-widget to interactively compare, analyze and edit maps
- Capabilities to visualize (possibly large) datasets  
- Tools to use the maps for interactive data analysis
- An extensive [documentation](eomaps.readthedocs.io/)

Use it with your favorite IDE, in [Jupyter Notebooks](https://eomaps.readthedocs.io/en/latest/FAQ.html#jupyter-notebooks) or as a [command line tool](https://eomaps.readthedocs.io/en/latest/api_command_line_interface.html)!

The following sections provide a quick overview on the basic capabilities of EOmaps. We will not go into details, but just explore what's possible and create a few example maps!

## Basic concepts - let's create a map!

EOmaps is all about `Maps` objects. 
To start a new map, we create a `Maps` object named `m`.  
This will initialize a new `matplotlib.Figure` and a `cartopy.GeoAxes` for a map.

```python
from eomaps import Maps
m = Maps()
m.add_feature.preset("coastline", "ocean")
```

![A very basic EOmaps map.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ognvpg9l23s9cccug9bp.png)

The `Maps` object `m` can now be used to add features, plot datasets or to add callbacks to interact with the map.

### Map Projections

By default, the map will be in `PlateCarree` projection (e.g. lon/lat). To create a map in a different projection, you can use  *epsg*-codes or any [cartopy projection](https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html).

> To add customized features like coastlines, ocean colouring etc. provided by [NaturalEarth](https://www.naturalearthdata.com/) to the map, use `m.add_feature.<category>.<name>()`.

```python
from eomaps import Maps
m = Maps(crs=Maps.CRS.Mollweide())
m.add_feature.physical.ocean(scale=110, fc="darkslategrey")
m.add_feature.physical.land(scale=110, fc="sienna")
m.add_feature.cultural.admin_0_countries(scale=110, fc="none", ec=(0.9, 0.8, 0.75), lw=0.5)
```

![A map in Mollweide-projection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmfjpxz73imvgkastug0.png)

---

EOmaps provides a lot of additional features to customize the appearance of a map. Since a complete overview on the capabilities would go beyond the scope of this article, make sure to checkout the [documentation](eomaps.readthedocs.io/) to get a more detailed overview!

To give a quick example on the possibilities, here is how you can create the following map that includes **gridlines**, a **scalebar**, a **compass**, a **logo**, some **text** and imagery fetched from a **WebMap service**:

![A map showcasing some of the features available with EOmaps.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt40jhzeplqeevs0yq10.png)

```python
from eomaps import Maps
m = Maps(crs=3035, figsize=(7, 5))

# add some features, a compass, a scalebar and a logo
m.add_feature.physical.ocean(scale=50, fc=".5")
m.add_feature.physical.coastline(scale=50, fc="none", ec="k", lw=1)

m.add_compass(pos=(0.1, 0.1))
m.add_scalebar(pos=(-39, 36), scale=2e5, preset="bw", size_factor=1.8)
m.add_logo()

# add grid lines with bold labels
g = m.add_gridlines(45)
g.add_labels(fontsize=8, fontweight="bold")

# add a watercolor basemap provided by: maps.stamen.com
m.add_wms.OpenStreetMap.add_layer.stamen_watercolor()

# write some info-text below the axes
m.text(0.75, -0.02,
       "data provided by: naturalearthdata.com, maps.stamen.com/watercolor",
       fontsize=6, transform=m.ax.transAxes)
```

## Data Visualization

Now that we've learned how we can create nice basemaps, its time to do some data visualization!

EOmaps can be used to visualize (possibly very large) datasets. It handles re-projection and classification of the data and provides the following methods to represent 1D or 2D data on a map:

- Draw **projected polygons** for each data point: 
  - ellipses/rectangles (radius defined in a given projection)
  - geodesic circles (radius defined in meters)
- Draw a **marker** for each datapoint
- Draw a **Voronoi Diagram** or a **Delaunay Triangulation** of the data
- Draw (possibly large) 2D datasets as a **rectangular raster**
- Use **data shading** for 1D and 2D datasets to visualize very large datasets.

A detailed overview on how to visualize datasets is given in the [Data Visualization]() section of the documentation. 

For now, let's just have a quick look at the code to create a map with a dataset plotted on a globe and an inset-map that highlights a selected area:


![A map showing some data and an [inset-map](https://eomaps.readthedocs.io/en/latest/api_inset_maps.html) to highlight a specific area on the map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6g5dl44vjhb0s0nm0ncn.png)

```python
# create some random data
import numpy as np
x, y = np.linspace(-10, 10, 100), np.linspace(-20, 70, 200)
x, y = np.meshgrid(x, y)
data = np.sqrt(x**4 + y**2 + np.random.randint(0, 2e3, x.shape))
```

```python
from eomaps import Maps

# create a map
m = Maps(Maps.CRS.Orthographic(45, 45))
m.add_feature.preset("coastline", "ocean")
m.add_gridlines(20)

# plot the data
m.set_data(data, x, y, crs=4326)             # assign the data
m.set_classify.NaturalBreaks(k=5)            # classify the data
m.set_shape.raster()                         # set the shape
m.plot_map(cmap="RdYlBu", set_extent=False)  # plot the data

# add a colorbar
cb = m.add_colorbar()
cb.set_labels("values: $\\sqrt{x^4 + y^2 + random}$",
              "hist. count", fontweight="bold")

# create a new inset-map to highlight a specific region
m_inset = m.new_inset_map(xy=(5, 45), radius=15)
m_inset.add_extent_indicator(lw=3)
m_inset.add_indicator_line()

m_inset.add_feature.preset("coastline", "ocean")
m_inset.add_gridlines(([0, 10], [50, 40]), lw=2, labels=True)
m_inset.add_gridlines(2.5, lw=0.3)

# plot the same data as before on the inset-map
m_inset.inherit_data(m)             # inherit the data
m_inset.inherit_classification(m)   # inherit the classification
m_inset.set_shape.ellipses()        # use a different shape
m_inset.plot_map()                  # plot the data

# re-arrange the axes with respect to a given layout
# >>> checkout the LayoutEditor!
m.apply_layout(
    {'figsize': [6.4, 4.8],
     '0_map': [0.025, 0.45, 0.4, 0.5],
     '1_cb': [0.01, 0.05, 0.98, 0.25],
     '1_cb_histogram_size': 0.8,
     '2_inset_map': [0.5, 0.35, 0.42, 0.55]}
    )
```

## Layer management

With EOmaps, a **figure can have as many [layers](https://eomaps.readthedocs.io/en/dev/api_basics.html) as you like**.

This is really useful to:
- Compare multiple datasets with each other
- Quickly check OpenStreetMap or some satellite imagery to get a broader picture of what's going on


The following image explains how you can create and manage multiple plot-layers. In short: 
- Use `m_A = m.new_layer("layer A")` to create a new layer
- Now you can use everything we've learned so far to add features to the new layer by using the associated `Maps` object `m_A`.


![EOmaps layer management](https://eomaps.readthedocs.io/en/dev/_images/intro.png)

## Companion Widget

Now that we know some basics on how to create (multi-layered) maps and visualize datasets, it is time to introduce the [Companion Widget](https://eomaps.readthedocs.io/en/dev/api_companion_widget.html) - A nice little **graphical user interface** that allows you to interact with the figure and explore the capabilities of EOmaps in many ways:

- **Compare layers**: Create / Switch / Overlay
- **Add features**: NaturalEarth, WebMaps, etc.
- **Plot files**: Drag-and-drop GeoTIFF, NetCDF, CSV and shape files
- **Draw shapes**: Draw and export geocoded polygons on the map
- **Add/edit annotations**: Add text indicators to the map
- **Export images**: Customize figure export (png, jpeg, svg...)

<img src=https://eomaps.readthedocs.io/en/dev/_images/companion_widget.gif width=75% alt=EOmaps GUI widget/>

To **activate the widget, press ``w``** on the keyboard while the mouse is on top of an EOmaps map. 

> To get information on the buttons/sliders etc. click the info-button!
> <a href=https://eomaps.readthedocs.io/en/latest/api_companion_widget.html><img src=https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1cz2fwci3gvyqyj658sj.png alt="?"/></a>


![Using the companion widget.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pi6tk9nv7cfjh3tl7mtd.gif)

## Data analysis
Finally, lets explore how we can use the maps as interactive data analysis widgets!

To interact with a map, you can attach **callback functions** that are triggered on one of the following events:

- General events:
  - **click**: You clicked with the mouse anywhere on a map.
  - **move**: You moved the mouse over a map.
  - **keypress**: A key was pressed on the keyboard.

- Events associated with a dataset:
  - **pick**: If you click on a map, EOmaps will identify the closest datapoint(s) and provide basic properties (ID, position, value, ...) as arguments to the callback.

There are many [pre-defined](https://eomaps.readthedocs.io/en/latest/api_callbacks.html#pre-defined-callbacks) callbacks, but you can of course also define [custom callbacks](https://eomaps.readthedocs.io/en/latest/api_callbacks.html#custom-callbacks) and attach them to a map.

As a quick example lets create a map with the following click and pick callbacks:

- **left-click**: show a marker and a basic annotation
- **right-click**: identify the closest datapoint, indicate the point with a black boundary, show a customized annotation and execute a custom callback

![Using Callbacks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qxt3h7ckfznctrwdegv.gif)

```python
from eomaps import Maps
m = Maps(3035)
m.add_title("Circles with 10° radius.")
m.add_feature.preset("coastline", "ocean")
m.add_logo(pad=(0, 0.2), size=0.2)

m.set_data(data=[1, 2, 3], x=[-47, -6, 63], y=[56, 46, 61], crs=4326)
m.set_shape.ellipses(radius=10, radius_crs=4326)
m.plot_map()

# Attach CLICK callbacks on RIGHT click (button 3)
m.cb.click.attach.annotate(button=3, fontsize=8, xytext=(-30,-70))
m.cb.click.attach.mark(button=3, radius=1, radius_crs=4326, fc="r")

# Attach PICK callbacks on LEFT click (the default)
m.cb.pick.attach.mark(ec="k", fc="none", lw=3, n=100)

def my_text(ID, **kwargs):
    return f"Datapoint {ID}"

m.cb.pick.attach.annotate(
    text=my_text, 
    xytext=(0.5, .9), 
    textcoords="axes fraction",
    fontsize=20, 
    horizontalalignment="center", 
    bbox=dict(lw=3),
    arrowprops=dict(
        arrowstyle="fancy", 
        fc="k", 
        connectionstyle="Angle3"
        )
    )

def my_callback(ID, pos, val, **kwargs):
    print(f"\nFound datapoint {ID}:\n"
          f" xy (plot): {pos}\n"
          f" xy (data): ({m.data_specs.x[ID]}, {m.data_specs.y[ID]})\n"
          f" value:     {val}\n")

m.cb.pick.attach(my_callback)
```


## Conclusion

With this, we have completed our quick tour on the basic features of EOmaps... there is much more to [explore](https://eomaps.readthedocs.io/)!

Give it a try! 

**Note:** EOmaps is a free and  [open source](https://github.com/raphaelquast/EOmaps) python module that requires a lot of time and effort to maintain. 
- **Found EOmaps useful? Support the development!**
  - Spread the word!
  - Add a [citation](https://zenodo.org/badge/latestdoi/410829039) to your publication
- **Interested in contributing to EOmaps?** 
  - Awesome! Any contributions are welcome!
  - Checkout the [Contribution Guide](https://eomaps.readthedocs.io/en/dev/contribute.html) on how to get started!