{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Adding Scale Bars and North Arrows to a Matplotlib Plot\n", "Scale bars and north arrows are common elements added to maps to indicate the scale and orientation of the map, respectively. \n", "\n", "Two packages exist for easily adding these elements to the default `matplotlib` plots generated by GeoPandas' `plot()` function: [matplotlib-scalebar](https://pypi.org/project/matplotlib-scalebar/) and [matplotlib-map-utils](https://github.com/moss-xyz/matplotlib-map-utils). The use of each is described below." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using `matplotlib-scalebar`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from geodatasets import get_path\n", "from matplotlib_scalebar.scalebar import ScaleBar\n", "\n", "import geopandas as gpd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a ScaleBar object\n", "The only required parameter for creating a ScaleBar object is `dx`. This is equal to a size of one pixel in real world. Value of this parameter depends on units of your CRS.\n", "\n", "#### Projected coordinate system (meters)\n", "The easiest way to add a scale bar is using a projected coordinate system with meters as units. Just set `dx = 1`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nybb = gpd.read_file(get_path(\"nybb\"))\n", "nybb = nybb.to_crs(32619) # Convert the dataset to a coordinate\n", "# system which uses meters\n", "\n", "ax = nybb.plot()\n", "ax.add_artist(ScaleBar(1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Geographic coordinate system (degrees)\n", "With a geographic coordinate system with degrees as units, `dx` should be equal to a distance in meters of two points with the same latitude (Y coordinate) which are one full degree of longitude (X) apart. You can calculate this distance by online calculator [(e.g. the Great Circle calculator)](http://edwilliams.org/gccalc.htm) or in geopandas.\\\n", "\\\n", "Firstly, we will create a GeoSeries with two points that have roughly the coordinates of NYC. They are located on the same latitude but one degree of longitude from each other. Their initial coordinates are specified in a geographic coordinate system (geographic WGS 84). They are then converted to a projected system for the calculation:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from shapely.geometry.point import Point\n", "\n", "points = gpd.GeoSeries(\n", " [Point(-73.5, 40.5), Point(-74.5, 40.5)], crs=4326\n", ") # Geographic WGS 84 - degrees\n", "points = points.to_crs(32619) # Projected WGS 84 - meters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After the conversion, we can calculate the distance between the points. The result slightly differs from the Great Circle Calculator but the difference is insignificant (84,921 and 84,767 meters):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "distance_meters = points[0].distance(points[1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we are able to use geographic coordinate system in our plot. We set value of `dx` parameter to a distance we just calculated:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nybb = gpd.read_file(get_path(\"nybb\"))\n", "nybb = nybb.to_crs(4326) # Using geographic WGS 84\n", "\n", "ax = nybb.plot()\n", "ax.add_artist(ScaleBar(distance_meters))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using other units \n", "The default unit for `dx` is m (meter). You can change this unit by the `units` and `dimension` parameters. There is a list of some possible `units` for various values of `dimension` below:\n", "\n", "| dimension | units |\n", "| ----- |:-----:|\n", "| si-length | km, m, cm, um|\n", "| imperial-length |in, ft, yd, mi|\n", "|si-length-reciprocal|1/m, 1/cm|\n", "|angle|deg|\n", "\n", "In the following example, we will leave the dataset in its initial CRS which uses feet as units. The plot shows scale of 2 leagues (approximately 11 kilometers):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nybb = gpd.read_file(get_path(\"nybb\"))\n", "\n", "ax = nybb.plot()\n", "ax.add_artist(ScaleBar(1, dimension=\"imperial-length\", units=\"ft\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Customization of the scale bar" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nybb = gpd.read_file(get_path(\"nybb\")).to_crs(32619)\n", "ax = nybb.plot()\n", "\n", "# Position and layout\n", "scale1 = ScaleBar(\n", " dx=1,\n", " label=\"Scale 1\",\n", " location=\"upper left\", # in relation to the whole plot\n", " label_loc=\"left\",\n", " scale_loc=\"bottom\", # in relation to the line\n", ")\n", "\n", "# Color\n", "scale2 = ScaleBar(\n", " dx=1,\n", " label=\"Scale 2\",\n", " location=\"center\",\n", " color=\"#b32400\",\n", " box_color=\"yellow\",\n", " box_alpha=0.8, # Slightly transparent box\n", ")\n", "\n", "# Font and text formatting\n", "scale3 = ScaleBar(\n", " dx=1,\n", " label=\"Scale 3\",\n", " font_properties={\n", " \"family\": \"serif\",\n", " \"size\": \"large\",\n", " }, # For more information, see the cell below\n", " scale_formatter=lambda value, unit: f\"> {value} {unit} <\",\n", ")\n", "\n", "ax.add_artist(scale1)\n", "ax.add_artist(scale2)\n", "ax.add_artist(scale3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Note:* Font is specified by six properties: `family`, `style`, `variant`, `stretch`, `weight`, `size` (and `math_fontfamily`). See [more](https://matplotlib.org/stable/api/font_manager_api.html#matplotlib.font_manager.FontProperties).\\\n", "\\\n", "For more information about matplotlib-scalebar library, see the [PyPI](https://pypi.org/project/matplotlib-scalebar/) or [GitHub](https://github.com/ppinard/matplotlib-scalebar) page." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using `matplotlib-map-utils`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using this package, north arrows and scale bars can be made with either **functions** or **classes**; for the purposes of this tutorial, only the *functions* will be used, though the *classes* work in much the same way. Tutorials for customizing each object are available within the [docs](https://github.com/moss-xyz/matplotlib-map-utils/tree/main/matplotlib_map_utils/docs) directory of the repository." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from geodatasets import get_path\n", "from matplotlib_map_utils.core.north_arrow import NorthArrow, north_arrow\n", "from matplotlib_map_utils.core.scale_bar import ScaleBar, scale_bar\n", "\n", "import geopandas as gpd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Set-Up" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nybb = gpd.read_file(get_path(\"nybb\"))\n", "# By default, the data is projected in feet\n", "nybb.crs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you are working with a common plot size, both `NorthArrow` and `ScaleBar` have a function called `set_size()` that bulk-updates a variety of settings so that the relevant object looks \"better\" at that size.\n", "\n", "GeoPandas' `plot()` function will create figures of `6.4\"x4.8\"` with the data used in this tutorial, which corresponds to a size of `small`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "NorthArrow.set_size(\"small\")\n", "ScaleBar.set_size(\"small\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### North Arrows\n", "\n", "The `north_arrow()` function takes in the following arguments:\n", "\n", "* `ax`: the axis on which to plot the north arrow\n", "\n", "* `location`: a string indicating the location of the north arrow on the plot (see `loc` under [matplotlib.pyplot.legend](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html) e.g., \"upper left\", \"lower right\", etc.)\n", "\n", "* `scale`: the desired height of the north arrow, in *inches*\n", "\n", "* `rotation`: a dictionary containing either a value for `degrees` (if rotation will be set manually), or arguments for `crs`, `reference`, and `coords` (if rotation will be calculated based on the provided CRS)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making a basic arrow using the minimum amount of arguments\n", "ax = nybb.plot()\n", "north_arrow(\n", " ax, location=\"upper left\", rotation={\"crs\": nybb.crs, \"reference\": \"center\"}\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Optional additional arguments can be passed to `base`, `fancy`, `shadow`, `label`, `pack`, and `aob` to change the styling of the arrow; see the documentation under [docs/howto_north_arrow.ipynb](https://github.com/moss-xyz/matplotlib-map-utils/blob/main/matplotlib_map_utils/docs/howto_north_arrow.ipynb) in the GitHub repository for details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making a more customized arrow\n", "ax = nybb.plot()\n", "north_arrow(\n", " ax,\n", " location=\"upper left\",\n", " scale=0.4,\n", " rotation={\"crs\": nybb.crs, \"reference\": \"center\"},\n", " base={\"edgecolor\": \"blue\", \"linewidth\": 2},\n", " fancy=False,\n", " shadow=False, # this turns off the component\n", " label={\"position\": \"top\", \"text\": \"North\", \"fontsize\": 8},\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Scale Bars\n", "\n", "The `scale_bar()` function takes in the following arguments:\n", "\n", "* `ax`: the axis on which to plot the scale bar\n", "\n", "* `location`: a string indicating the location of the scale bar on the plot (see `loc` under [matplotlib.pyplot.legend](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html) e.g., \"upper left\", \"lower right\", etc.)\n", "\n", "* `style`: the appearance of the arrow: can be either `ticks` or `boxes` (default)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making a basic scale bar using the minimum amount of arguments\n", "# Note that the data is auto-converted to miles\n", "ax = nybb.plot()\n", "scale_bar(ax, location=\"upper left\", style=\"boxes\", bar={\"projection\": nybb.crs})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making the same scale bar but in the other style (ticks)\n", "ax = nybb.plot()\n", "scale_bar(ax, location=\"upper left\", style=\"ticks\", bar={\"projection\": nybb.crs})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The scale bar can handle converting between common unit types, as shown in the table below. \n", "\n", "| Unit Type | Conversion Factor | Accepted Inputs |\n", "|----------------|-------------------|--------------------------------------------------------|\n", "| Meters | 1 | `m`,`meter`,`metre`,`meters`,`meters` |\n", "| Kilometers | 1000 | `km`,`kilometer`,`kilometre`,`kilometers`,`kilometers` |\n", "| Feet | 0.3048 | `ft`,`ftUS`,`foot`,`feet`,`US survey foot` |\n", "| Yards | 0.9144 | `yd`,`yard`,`yards` |\n", "| Miles | 1609.34 | `mi`,`mile`,`miles` |\n", "| Nautical Miles | 1852 | `nmi`,`nautical`,`nautical mile`,`nautical miles` |\n", "\n", "The scale bar can also handle *unprojected* data (degrees) - it will convert it to metres using *great circle distance*, and then convert it into the units selected by the user. This will happen automatically when `projection` is set to a CRS of `4326` or similar." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making a scale bar in kilometers instead, by changing bar[\"units\"]\n", "# Note that the data did not have to be reprojected to do this\n", "ax = nybb.plot()\n", "scale_bar(\n", " ax, location=\"upper left\", style=\"boxes\", bar={\"projection\": nybb.crs, \"unit\": \"km\"}\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Optional additional arguments can be passed to `bar`, `labels`, `units`, `text`, and `aob` to change the styling of the bar; see the documentation under [docs/howto_scale_bar.ipynb](https://github.com/moss-xyz/matplotlib-map-utils/blob/main/matplotlib_map_utils/docs/howto_scale_bar.ipynb) in the GitHub repository for details.\n", "\n", "*Note that control of the length of the bar, as well as the number of major and minor divisions, is handled within the `bar` style dictionary.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Making a more formatted scale bar (ticks)\n", "ax = nybb.plot()\n", "scale_bar(\n", " ax,\n", " location=\"upper left\",\n", " style=\"ticks\",\n", " bar={\n", " \"projection\": nybb.crs,\n", " \"max\": 12,\n", " \"major_div\": 2,\n", " \"minor_div\": 3,\n", " \"minor_type\": \"first\",\n", " \"tick_loc\": \"middle\",\n", " \"tickcolors\": \"blue\",\n", " \"basecolors\": \"blue\",\n", " \"tickwidth\": 1.5,\n", " },\n", " labels={\"loc\": \"above\", \"style\": \"major\"},\n", " units={\"loc\": \"bar\", \"fontsize\": 8},\n", " text={\"fontfamily\": \"monospace\"},\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.12.3" } }, "nbformat": 4, "nbformat_minor": 2 }