{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Visualizing Spatial Information\n", "\n", "Function `geom_livemap()` enables a researcher to visualize geospatial information on interactive map.\n", "\n", "When building interactive geospatial visualizations with *Lets-Plot* the visualisation workflow remains the \n", "same as it is when building a regular `ggplot2` plot.\n", "\n", "However, `geom_livemap()` creates an interactive base-map super-layer and certain limitations do apply \n", "comparing to a regular `ggplot2` geom-layer:\n", "\n", "* `geom_livemap()` must be added as a 1-st layer in plot;\n", "* Maximum one `geom_livemap()` layer is alloed per plot;\n", "* Not any type of *geometry* can be combined with interactive map layer in one plot;\n", "* Internet connection to *map tiles provider* is required.\n", "\n", "The following `ggplot2` geometry can be used with interactive maps:\n", "\n", "* `geom_point`\n", "* `geom_rect`\n", "* `geom_path`\n", "* `geom_polygon`\n", "* `geom_segment`\n", "* `geom_text`\n", "* `geom_tile`\n", "* `geom_vline`, `geon_hline`\n", "* `geom_bin2d`\n", "* `geom_contour`, `geom_contourf`\n", "* `geom_density2d`, `geom_density2df`\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:46.651117Z", "iopub.status.busy": "2024-04-26T11:57:46.651117Z", "iopub.status.idle": "2024-04-26T11:57:47.616355Z", "shell.execute_reply": "2024-04-26T11:57:47.616355Z" } }, "outputs": [], "source": [ "from lets_plot import *" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.616355Z", "iopub.status.busy": "2024-04-26T11:57:47.616355Z", "iopub.status.idle": "2024-04-26T11:57:47.632487Z", "shell.execute_reply": "2024-04-26T11:57:47.631979Z" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "LetsPlot.setup_html()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The First Map" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.648085Z", "iopub.status.busy": "2024-04-26T11:57:47.648085Z", "iopub.status.idle": "2024-04-26T11:57:47.775316Z", "shell.execute_reply": "2024-04-26T11:57:47.773995Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = ggplot() + ggsize(700, 400)\n", "\n", "# Add an empty base-map layer.\n", "p + geom_livemap()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Initial location and zoom level\n", "\n", "The default initial location and zoom level shown on the figure above \n", "can be adjusted by using the `location` and `zoom` parameters." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.775316Z", "iopub.status.busy": "2024-04-26T11:57:47.775316Z", "iopub.status.idle": "2024-04-26T11:57:47.789938Z", "shell.execute_reply": "2024-04-26T11:57:47.789938Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# In Lets-Plot coordinates are always encoded as: `longitude` - first and `latitude` - second\n", "p + geom_livemap(location=[29.737809, 60.003151], zoom=11)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding 'geometries'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_hline(), geom_vline()\n", "\n", "Map's initial `location` and `zoom` parameters are not used very often because map \n", "automatically chooses its initial viewport so that all the added *geometries* are visible.\n", "\n", "But the `horizontal` and `vertical` lines are exception because they do not have determined bounds. In this case its often necessary to set the desired location and zoom value manually." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.789938Z", "iopub.status.busy": "2024-04-26T11:57:47.789938Z", "iopub.status.idle": "2024-04-26T11:57:47.805731Z", "shell.execute_reply": "2024-04-26T11:57:47.805731Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lons = [-73 + x for x in range(4)]\n", "lats = [41.5 + 0.5 * y for y in range(4)]\n", "\n", "p + geom_livemap(location=[-71.94375, 42.572511], zoom=7)\\\n", " + geom_hline(aes(yintercept=lats), color='red', linetype=2)\\\n", " + geom_vline(aes(xintercept=lons), color='dark_green', linetype=4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_path()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.805731Z", "iopub.status.busy": "2024-04-26T11:57:47.805731Z", "iopub.status.idle": "2024-04-26T11:57:47.821374Z", "shell.execute_reply": "2024-04-26T11:57:47.821374Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "path = {\n", " 'lon': [11.620273, -71.075466, -121.898220, -96.761501], \n", " 'lat': [48.169211, 42.383651, 37.362240, 32.797371]\n", "}\n", "p + geom_livemap()\\\n", " + geom_path(data=path, mapping=aes(x='lon', y='lat'), size=1, color='magenta')\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.821374Z", "iopub.status.busy": "2024-04-26T11:57:47.821374Z", "iopub.status.idle": "2024-04-26T11:57:47.837447Z", "shell.execute_reply": "2024-04-26T11:57:47.837447Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p + geom_livemap()\\\n", " + geom_path(data=path, mapping=aes(x='lon', y='lat'), size=1, color='magenta')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_point()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.837447Z", "iopub.status.busy": "2024-04-26T11:57:47.837447Z", "iopub.status.idle": "2024-04-26T11:57:47.853119Z", "shell.execute_reply": "2024-04-26T11:57:47.853119Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p + geom_livemap()\\\n", " + geom_path(data=path, mapping=aes(x='lon', y='lat'), size=1, color='magenta')\\\n", " + geom_point(data=path, mapping=aes(x='lon', y='lat'), shape=21, size=7, fill='yellow')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_segment()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.853119Z", "iopub.status.busy": "2024-04-26T11:57:47.853119Z", "iopub.status.idle": "2024-04-26T11:57:47.868743Z", "shell.execute_reply": "2024-04-26T11:57:47.868743Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data = dict(\n", " x0 = [-97.8, 22.6], y0 = [41.2, -1.3],\n", " x1 = [ 19.3, -64.5], y1 = [18.7, -15.9],\n", " Direction = ['east', 'west']\n", ")\n", "p + geom_livemap()\\\n", " + geom_segment(data=data, mapping=aes(x='x0', y='y0', xend='x1', yend='y1', color='Direction'), size=1, linetype=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_text()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.870100Z", "iopub.status.busy": "2024-04-26T11:57:47.870100Z", "iopub.status.idle": "2024-04-26T11:57:47.884632Z", "shell.execute_reply": "2024-04-26T11:57:47.884632Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data = {\n", " 'label': [\"== 0 ==>\", \"== 60 ==>\", \"== 120 ==>\", \"== 180 ==>\", \"== -60 ==>\", \"== -120 ==>\"],\n", " 'angle': [0, 60, 120, 180, -60, -120],\n", "}\n", "\n", "p + geom_livemap() + geom_point(x=0, y=0, size=140, color='white', alpha=0.6)\\\n", " + geom_text(aes(label = 'label', angle='angle'), data=data,\n", " x=0, y=0, \n", " hjust = \"left\", vjust = \"center\",\n", " size=10, family='monospace', color='red')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_polygon()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.884632Z", "iopub.status.busy": "2024-04-26T11:57:47.884632Z", "iopub.status.idle": "2024-04-26T11:57:47.900446Z", "shell.execute_reply": "2024-04-26T11:57:47.900446Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "UK = {\n", " 'lon': [-2.598046, -2.378320, -2.466210, -3.169335, -1.938867, \n", " -0.576562, -0.312890, 0.873632, 0.082617, -2.598046], \n", " 'lat': [51.030349, 51.797754, 53.945750, 54.561879, 55.193929, \n", " 53.816229, 52.924809, 52.525588, 51.113188, 51.030349]\n", "}\n", "Germany = {\n", " 'lon': [7.685156, 9.926367, 13.661718, 14.101171, 11.464453, \n", " 12.870703, 8.564062, 8.651953, 6.806250, 6.938085, 7.685156], \n", " 'lat': [53.294124, 54.049078, 53.608160, 51.305902, 50.221916, \n", " 48.679365, 48.007575, 49.485266, 50.024691, 51.552493, 53.294124]\n", "}\n", "France = {\n", " 'lon': [-2.246484, 2.367773, 7.245703, 5.268164, 6.586523, \n", " 2.895117, 2.279882, -0.532617, -0.356835, -2.246484], \n", " 'lat': [48.095702, 50.586036, 48.795295, 46.365136, 44.169607, \n", " 43.663114, 43.088157, 43.631315, 46.516550, 48.095702]\n", "}\n", "polygons = dict(\n", " lon = UK['lon'] + Germany['lon'] + France['lon'],\n", " lat = UK['lat'] + Germany['lat'] + France['lat'],\n", " country = ['UK' for _ in UK['lon']] + ['Germany' for _ in Germany['lon']] + ['France' for _ in France['lon']]\n", ")\n", "\n", "p + geom_livemap()\\\n", " + geom_polygon(aes('lon', 'lat'),\n", " data=polygons, \n", " fill='green', \n", " alpha=0.3)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_rect()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.900446Z", "iopub.status.busy": "2024-04-26T11:57:47.900446Z", "iopub.status.idle": "2024-04-26T11:57:47.917220Z", "shell.execute_reply": "2024-04-26T11:57:47.915934Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bounds = dict(\n", " lon0 = [min(UK['lon']), min(Germany['lon']), min(France['lon'])],\n", " lon1 = [max(UK['lon']), max(Germany['lon']), max(France['lon'])],\n", " lat0 = [min(UK['lat']), min(Germany['lat']), min(France['lat'])],\n", " lat1 = [max(UK['lat']), max(Germany['lat']), max(France['lat'])],\n", " country = ['UK', 'Germany', 'France']\n", ")\n", "\n", "p + geom_livemap()\\\n", " + geom_rect(aes(xmin='lon0', ymin='lat0', xmax='lon1', ymax='lat1', fill='country'),\n", " data=bounds, \n", " alpha = 0.3) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data on Map\n", "\n", "In the examples above we've seen how values in input data series can be mapped to aesthetic values on map.\n", "For the instance, values in the `country` series were mapped to the `fill` color in \"geom_rect()\" example.\n", "\n", "Lets plot another data - the countries population:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.917220Z", "iopub.status.busy": "2024-04-26T11:57:47.917220Z", "iopub.status.idle": "2024-04-26T11:57:47.932907Z", "shell.execute_reply": "2024-04-26T11:57:47.931895Z" } }, "outputs": [], "source": [ "pop_UK = 66.65e6\n", "pop_Germany = 83.02e6\n", "pop_France = 66.99e6" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Proportional symbols map" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.932907Z", "iopub.status.busy": "2024-04-26T11:57:47.932907Z", "iopub.status.idle": "2024-04-26T11:57:47.948101Z", "shell.execute_reply": "2024-04-26T11:57:47.948101Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create data frame containing countries name, centroid and population.\n", "centroids = dict(\n", " lon = [(max(xs) - min(xs)) / 2 + min(xs) for xs in [UK['lon'], Germany['lon'], France['lon']]],\n", " lat = [(max(ys) - min(ys)) / 2 + min(ys)for ys in [UK['lat'], Germany['lat'], France['lat']]],\n", " country = ['UK', 'Germany', 'France'],\n", " population = [pop_UK, pop_Germany, pop_France] \n", ")\n", "\n", "p + geom_livemap() + scale_size(range=[10, 50], guide='none')\\\n", " + geom_point(aes('lon', 'lat', color='country', size='population'),\n", " data=centroids, \n", " alpha = 0.8) \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Choropleth map" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.948101Z", "iopub.status.busy": "2024-04-26T11:57:47.948101Z", "iopub.status.idle": "2024-04-26T11:57:47.963991Z", "shell.execute_reply": "2024-04-26T11:57:47.963991Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create data frame containing countries name, boundary polygon and population.\n", "polygons_and_pop = polygons.copy()\n", "polygons_and_pop['population'] = [pop_UK for _ in UK['lon']]\\\n", " + [pop_Germany for _ in Germany['lon']]\\\n", " + [pop_France for _ in France['lon']]\n", "\n", "# Note that in this case it's necessary to define `group` aesthetic.\n", "# Otherwise `geom_polygon` will not be able to split the data into tree \"data points\".\n", "p + geom_livemap()\\\n", " + geom_polygon(aes('lon', 'lat', fill='population', group='country'), \n", " data=polygons_and_pop, \n", " alpha=0.3)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Choropleth map (geopandas and `map_join`)\n", "\n", "The dataframe format used in the previouse `Choropleth` example is not the most \n", "convenient way to represent data with geospatial boundaries because each\n", "real data-point is duplicated for each vertex in the corresponding polygon.\n", "\n", "More convenient would be to store the data-points and their geospatial boundaries in \n", "a separate data structures.\n", "\n", "In this example let's use `geopandas.GeoDataFrame` to store the boundaries." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.963991Z", "iopub.status.busy": "2024-04-26T11:57:47.963991Z", "iopub.status.idle": "2024-04-26T11:57:47.979527Z", "shell.execute_reply": "2024-04-26T11:57:47.979527Z" } }, "outputs": [], "source": [ "# Data-points.\n", "population = dict(\n", " country = [\"UK\", \"Germany\", \"France\"],\n", " population = [pop_UK, pop_Germany, pop_France]\n", ")" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:47.979527Z", "iopub.status.busy": "2024-04-26T11:57:47.979527Z", "iopub.status.idle": "2024-04-26T11:57:48.010774Z", "shell.execute_reply": "2024-04-26T11:57:48.010774Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from geopandas import GeoDataFrame, points_from_xy\n", "from shapely.geometry import Polygon, LinearRing\n", "\n", "# GeoDataFrame with boundaries.\n", "polygons_gdf = GeoDataFrame(\n", " data=dict(\n", " name = ['UK', 'Germany','France']\n", " ),\n", " geometry=[\n", " Polygon(tuple(zip(UK['lon'], UK['lat']))),\n", " Polygon(tuple(zip(Germany['lon'], Germany['lat']))),\n", " Polygon(tuple(zip(France['lon'], France['lat'])))\n", " ]\n", ")\n", "\n", "# Use `map` parameter to pass `boundaries` and \n", "# `map_join` parameter to tell `geom_polygon` how to merge \"data\" and \"map\".\n", "p + geom_livemap()\\\n", " + geom_polygon(aes(fill='population'), \n", " data=population, \n", " map=polygons_gdf, \n", " map_join=[['country'],['name']],\n", " alpha=0.3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Geometries with built-in statistical transformation\n", "\n", "The way these geometries are added to an interactive map is the same as it is done on regular plots." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_bin2d()" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:48.010774Z", "iopub.status.busy": "2024-04-26T11:57:48.010774Z", "iopub.status.idle": "2024-04-26T11:57:48.043399Z", "shell.execute_reply": "2024-04-26T11:57:48.042664Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "np.random.seed(32)\n", "x, y = np.random.multivariate_normal(mean=[0,0], cov=[[1,0],[0,1]], size=50).T\n", "p + geom_livemap() + scale_fill_gradient('white', 'darkgreen')\\\n", " + geom_bin2d(aes(x * 5, y * 5, fill='..density..'), bins=[7, 7], alpha=0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geom_density2df()" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "execution": { "iopub.execute_input": "2024-04-26T11:57:48.046035Z", "iopub.status.busy": "2024-04-26T11:57:48.046035Z", "iopub.status.idle": "2024-04-26T11:57:48.279804Z", "shell.execute_reply": "2024-04-26T11:57:48.279804Z" } }, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p + geom_livemap() + scale_fill_gradient('white', 'darkgreen')\\\n", " + geom_density2df(aes(x * 5, y * 5, fill='..level..'), alpha=0.5)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.13" } }, "nbformat": 4, "nbformat_minor": 4 }