diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 57447fd08e..04058ed0fa 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -32,14 +32,23 @@ jobs: shell: bash -l {0} run: | set -e - cp examples/Quickstart.ipynb docs/quickstart.ipynb pushd docs make clean html linkcheck popd - - name: GitHub Pages action + - name: Publish to Github Pages on main + if: ${{ github.ref == 'refs/heads/main' }} + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/_build/html/ + destination_dir: latest + keep_files: false + + - name: Publish to Github Pages on release if: ${{ github.event_name == 'release' }} uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html/ + destination_dir: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 173e59bc1b..b6d9f94ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .pytest_cache build/ dist/ +docs/index.html docs/_build/ docs/quickstart.ipynb examples/results/* diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000000..33a90b4205 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,4 @@ +.bd-content { + flex-grow: 1; + max-width: 100%; +} diff --git a/docs/_static/flask_example.py b/docs/_static/flask_example.py new file mode 100644 index 0000000000..47d4273cf6 --- /dev/null +++ b/docs/_static/flask_example.py @@ -0,0 +1,92 @@ +""" flask_example.py + + Required packages: + - flask + - folium + + Usage: + + Start the flask server by running: + + $ python flask_example.py + + And then head to http://127.0.0.1:5000/ in your browser to see the map displayed + +""" + +from flask import Flask, render_template_string + +import folium + +app = Flask(__name__) + + +@app.route("/") +def fullscreen(): + """Simple example of a fullscreen map.""" + m = folium.Map() + return m.get_root().render() + + +@app.route("/iframe") +def iframe(): + """Embed a map as an iframe on a page.""" + m = folium.Map() + + # set the iframe width and height + m.get_root().width = "800px" + m.get_root().height = "600px" + iframe = m.get_root()._repr_html_() + + return render_template_string( + """ + + + + +

Using an iframe

+ {{ iframe|safe }} + + + """, + iframe=iframe, + ) + + +@app.route("/components") +def components(): + """Extract map components and put those on a page.""" + m = folium.Map( + width=800, + height=600, + ) + + m.get_root().render() + header = m.get_root().header.render() + body_html = m.get_root().html.render() + script = m.get_root().script.render() + + return render_template_string( + """ + + + + {{ header|safe }} + + +

Using components

+ {{ body_html|safe }} + + + + """, + header=header, + body_html=body_html, + script=script, + ) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/docs/_static/switcher.json b/docs/_static/switcher.json new file mode 100644 index 0000000000..225c769631 --- /dev/null +++ b/docs/_static/switcher.json @@ -0,0 +1,46 @@ +[ + { + "version": "v0.14.0 (stable)", + "url": "https://python-visualization.github.io/folium/version-v0.14.0/" + }, + { + "version": "v0.12.1", + "url": "https://python-visualization.github.io/folium/version-v0.12.1/" + }, + { + "version": "v0.12.0", + "url": "https://python-visualization.github.io/folium/version-v0.12.0/" + }, + { + "version": "v0.11.0", + "url": "https://python-visualization.github.io/folium/version-v0.11.0/" + }, + { + "version": "v0.10.1", + "url": "https://python-visualization.github.io/folium/version-v0.10.1/" + }, + { + "version": "v0.10.0", + "url": "https://python-visualization.github.io/folium/version-v0.10.0/" + }, + { + "version": "v0.9.1", + "url": "https://python-visualization.github.io/folium/version-v0.9.1/" + }, + { + "version": "v0.9.0", + "url": "https://python-visualization.github.io/folium/version-v0.9.0/" + }, + { + "version": "v0.8.3", + "url": "https://python-visualization.github.io/folium/version-v0.8.3/" + }, + { + "version": "v0.8.2", + "url": "https://python-visualization.github.io/folium/version-v0.8.2/" + }, + { + "version": "v0.8.1", + "url": "https://python-visualization.github.io/folium/version-v0.8.1/" + } +] diff --git a/docs/_templates/version.html b/docs/_templates/version.html new file mode 100644 index 0000000000..80c739d457 --- /dev/null +++ b/docs/_templates/version.html @@ -0,0 +1,2 @@ + +Folium version {{ version }} diff --git a/docs/_themes/f6/NEWS.txt b/docs/_themes/f6/NEWS.txt deleted file mode 100644 index 65238d9011..0000000000 --- a/docs/_themes/f6/NEWS.txt +++ /dev/null @@ -1,7 +0,0 @@ -News -==== - -1.0 ---- -* Release date: 2012-11-01 -* Initial release diff --git a/docs/_themes/f6/layout.html b/docs/_themes/f6/layout.html deleted file mode 100644 index 3e7fbd9566..0000000000 --- a/docs/_themes/f6/layout.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "basic/layout.html" %} - -{%- block doctype -%} - -{%- endblock -%} - -{%- block extrahead -%} -{%- endblock -%} - -{# put the sidebar before the body #} -{% block sidebar1 %}{{ sidebar() }}{% endblock %} -{% block sidebar2 %}{% endblock %} - -{%- block footer %} - -{%- endblock %} diff --git a/docs/_themes/f6/static/brillant.png b/docs/_themes/f6/static/brillant.png deleted file mode 100644 index 82cf2d4957..0000000000 Binary files a/docs/_themes/f6/static/brillant.png and /dev/null differ diff --git a/docs/_themes/f6/static/f6.css b/docs/_themes/f6/static/f6.css deleted file mode 100644 index 2cc0c4b474..0000000000 --- a/docs/_themes/f6/static/f6.css +++ /dev/null @@ -1,365 +0,0 @@ -/* f6.css - * Modified from sphinxdoc.css of the sphinxdoc theme. -*/ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Source Sans Pro', sans-serif; - font-size: 14px; - line-height: 150%; - text-align: center; - color: #4d4d4c; - padding: 0; - margin: 0px 80px 0px 80px; - min-width: 740px; - border: 1px solid #d6d6d6; - background: url("brilliant.png") repeat; -} - -div.document { - text-align: left; - background-repeat: repeat-x; -} - -div.bodywrapper { - margin: 0 240px 0 0; - border-right: 1px dotted #d6d6d6; -} - -div.body { - background-color: white; - margin: 0; - padding: 0.5em 20px 20px 20px; -} - -div.related { - font-size: 1em; - background-color: #efefef; - color: #4d4d4c; - padding: 5px 0px; - border-bottom: 1px solid #d6d6d6; -} - -div.related ul { - height: 2em; - margin: 2px; -} - -div.related ul li { - margin: 0; - padding: 0; - height: 2em; - float: left; -} - -div.related ul li.right { - float: right; - margin-right: 5px; -} - -div.related ul li a { - margin: 0; - padding: 2px 5px; - line-height: 2em; - text-decoration: none; - color: #4d4d4c; -} - -div.related ul li a:hover { - color: #4271ae; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; -} - -div.sphinxsidebarwrapper { - padding: 0; -} - -div.sphinxsidebar { - margin: 0; - padding: 0.5em 15px 15px 0; - width: 210px; - float: right; - font-size: 0.9em; - text-align: left; -} - -div.sphinxsidebar h3, div.sphinxsidebar h4 { - font-size: 1em; - padding: 0.5em 0 0.5em 0; - text-transform: uppercase; -} - -div.sphinxsidebar h3 a { - color: #4271ae; -} - -div.sphinxsidebar ul { - padding-left: 1.5em; - margin-top: 7px; - padding: 0; - line-height: 150%; - color: #4d4d4c; -} - -div.sphinxsidebar ul li { - color: #8e908c; -} - -div.sphinxsidebar ul ul { - margin-left: 1em; -} - -div.sphinxsidebar input { - border: 1px solid #efefef; - font-family: inherit; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - color: white; - background-color: #4271ae; -} - -div.footer { - color: #4d4d4c; - padding: 3px 8px 3px 0; - clear: both; - font-size: 0.8em; -} - -div.footer a { - color: #4d4d4c; - text-decoration: none; - border-bottom: 1px dotted #4271ae; -} - -div.footer a:hover { - color: #4271ae; - text-decoration: none; - border-bottom: 1px dotted #4271ae; -} - -/* -- body styles ----------------------------------------------------------- */ - -p { - margin: 0.8em 0 0.5em 0; -} - -div.body a, div.sphinxsidebarwrapper a { - color: #4d4d4c; - text-decoration: none; - border-bottom: 1px dotted #4271ae; -} - -div.body a:hover, div.sphinxsidebarwrapper a:hover { - color: #4271ae; - border-bottom: 1px dotted #4271ae; -} - -h1, h2, h3, h4, h5, h6 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 400; - text-shadow: #efefef 0.1em 0.1em 0.1em; -} - -h1 { - margin: 0; - padding: 0.7em 0 0.3em 0; - line-height: 1.2em; - text-shadow: #efefef 0.1em 0.1em 0.1em; -} - -h2 { - margin: 1.3em 0 0.2em 0; - padding: 0 0 10px 0; -} - -h3 { - margin: 1em 0 -0.3em 0; - padding-bottom: 5px; -} - -h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { - display: none; - margin: 0 0 0 0.3em; - padding: 0 0.2em 0 0.2em; - color: #aaa!important; -} - -h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, -h5:hover a.anchor, h6:hover a.anchor { - display: inline; -} - -h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, -h5 a.anchor:hover, h6 a.anchor:hover { - color: #777; - background-color: #eee; -} - -a.headerlink { - color: #4d4d4c!important; - font-size: 1em; - margin-left: 6px; - padding: 0 4px 0 4px; - text-decoration: none!important; -} - -a.headerlink:hover { - background-color: #efefef; - color: white!important; -} - - -cite, code, tt { - font-family: 'Source Code Pro', monospace; - font-size: 0.9em; - letter-spacing: 0.01em; - background-color: #fbfbfb; - font-style: normal; - border: 1px dotted #efefef; - border-radius: 2px; - padding: 0 2px; -} - -hr { - border: 1px solid #d6d6d6; - margin: 2em; -} - -.highlight { - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; -} - -.highlighted { - background-color: #4271ae; - color: white; - padding: 0 0.3em; -} - -pre { - font-family: 'Source Code Pro', monospace; - font-style: normal; - font-size: 0.9em; - letter-spacing: 0.015em; - line-height: 120%; - padding: 0.7em; - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ -} - -pre a { - color: inherit; - text-decoration: underline; -} - -td.linenos pre { - padding: 0.5em 0; -} - -div.quotebar { - background-color: #f8f8f8; - max-width: 250px; - float: right; - padding: 2px 7px; - border: 1px solid #ccc; -} - -div.topic { - background-color: #f8f8f8; -} - -table { - border-collapse: collapse; - margin: 0 -0.5em 0 -0.5em; -} - -table td, table th { - padding: 0.2em 0.5em 0.2em 0.5em; -} - -div.admonition { - font-size: 0.9em; - margin: 1em 0 1em 0; - background-color: #fdfdfd; - padding: 0; - -moz-box-shadow: 0px 8px 6px -8px #d6d6d6; - -webkit-box-shadow: 0px 8px 6px -8px #d6d6d6; - box-shadow: 0px 8px 6px -8px #d6d6d6; -} - -div.admonition p { - margin: 0.5em 1em 0.5em 1em; - padding: 0.2em; -} - -div.admonition pre { - margin: 0.4em 1em 0.4em 1em; -} - -div.admonition p.admonition-title -{ - margin: 0; - padding: 0.2em 0 0.2em 0.6em; - background-color: white; - border-bottom: 1px solid #4271ae; - font-weight: 600; - font-size: 1.2em; - color: #4271ae; - text-transform: uppercase; -} - -div.note p.admonition-title -{ - color: #4271ae; - border-bottom: 1px solid #4271ae; -} - -div.warning p.admonition-title, -div.important p.admonition-title { - color: #f5871f; - border-bottom: 1px solid #f5871f; -} - -div.hint p.admonition-title, -div.tip p.admonition-title { - color: #718c00; - border-bottom: 1px solid #718c00; -} - -div.caution p.admonition-title, -div.attention p.admonition-title, -div.danger p.admonition-title, -div.error p.admonition-title { - color: #c82829; - border-bottom: 1px solid #c82829; -} - -div.admonition ul, div.admonition ol { - margin: 0.1em 0.5em 0.5em 3em; - padding: 0; -} - -div.versioninfo { - margin: 1em 0 0 0; - border: 1px solid #eee; - background-color: #DDEAF0; - padding: 8px; - line-height: 1.3em; - font-size: 0.9em; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #eee; - border-bottom: 1px solid #eee; -} diff --git a/docs/_themes/f6/theme.conf b/docs/_themes/f6/theme.conf deleted file mode 100644 index 5dacf6859f..0000000000 --- a/docs/_themes/f6/theme.conf +++ /dev/null @@ -1,3 +0,0 @@ -[theme] -inherit = basic -stylesheet = f6.css diff --git a/docs/advanced_guide.rst b/docs/advanced_guide.rst new file mode 100644 index 0000000000..bd4a60bf09 --- /dev/null +++ b/docs/advanced_guide.rst @@ -0,0 +1,16 @@ +Advanced guide +============== + +.. toctree:: + :maxdepth: 1 + + advanced_guide/flask + advanced_guide/choropleth with Jenks natural breaks optimization + advanced_guide/subplots + advanced_guide/colormaps + advanced_guide/world_copy + advanced_guide/custom_panes + advanced_guide/geodedetic_image_overlay + advanced_guide/custom_tiles + advanced_guide/piechart_icons + advanced_guide/polygons_from_list_of_points diff --git a/docs/advanced_guide/choropleth with Jenks natural breaks optimization.md b/docs/advanced_guide/choropleth with Jenks natural breaks optimization.md new file mode 100644 index 0000000000..e73b7e52a8 --- /dev/null +++ b/docs/advanced_guide/choropleth with Jenks natural breaks optimization.md @@ -0,0 +1,66 @@ +# Integrating Jenks Natural Break Optimization with choropleth + +Choropleths provide an easy way to visually see data distributions across geography. By default, folium uses the breaks created by numpy.histogram (np.histogram), which generally creates an evenly spaced quantiles. + +This works well enough for evenly distributed data, but for unevenly distributed data, these even quantiles can obscure more than they show. To demonstrate this, I have created maps showing the labor force of each US state. + +The data was taken from the county-level data and aggregated. Since our geographic data does not have areas representing Puerto Rico or the United States as a whole, I removed those entries while keeping Washington, D.C. in our data set. Already, looking at the first five states alphabetically, we can see that Alaska (AK) has a work force roughly 2% the size of California (CA). + +```{code-cell} ipython3 +import folium +import numpy as np +import pandas as pd +import json +import requests +``` + +```{code-cell} ipython3 +geo_json_data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() + +clf = 'Civilian_labor_force_2011' +labor_force = pd.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_labor_force_2011.csv" +) + +labor_force.head() +``` + +Using default breaks, most states are represented as being part of the bottom quantile. This distribution is similar to what we might expect if US states follow a Power Law or a Zipf distribution. + +```{code-cell} ipython3 +m = folium.Map(location=[38, -96], zoom_start=4) + +folium.Choropleth( + geo_data=geo_json_data, + data=labor_force, + columns=['State', clf], + key_on='id', + fill_color='RdBu', +).add_to(m) + +m +``` + +However, when using Jenks natural Breaks Optimization, we now see more granular detail at the bottom of the distribution, where most of our states are located. The upper western states (Idaho, Montana, Wyoming and the Dakotas) are distinguished from their Midwestern and Mountain West neighbors to the south. Gradations in the deep south between Mississippi and Alabama provide more visual information than in the previous map. Overall, this is a richer representation of the data distribution. + +One notable drawback of this representation is the legend. Because the lower bins are smaller, the numerical values overlap, making them unreadable. + +```{code-cell} ipython3 +m = folium.Map(location=[38, -96], zoom_start=4) + +choropleth = folium.Choropleth( + geo_data=geo_json_data, + data=labor_force, + columns=['State', clf], + key_on='id', + fill_color='RdBu', + use_jenks=True, +) +choropleth.add_to(m) + +choropleth.color_scale.width = 800 + +m +``` diff --git a/docs/advanced_guide/colormaps.md b/docs/advanced_guide/colormaps.md new file mode 100644 index 0000000000..a1c6a4567d --- /dev/null +++ b/docs/advanced_guide/colormaps.md @@ -0,0 +1,193 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Using colormaps + +A few examples of how to use `folium.colormap` in choropleths. + +Let's load a GeoJSON file, and try to choropleth it. + +```{code-cell} ipython3 +import pandas +import requests + +geo_json_data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() +unemployment = pandas.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_unemployment_oct_2012.csv" +) + +unemployment_dict = unemployment.set_index("State")["Unemployment"] +``` + +## Self-defined + +You can build a choropleth in using a self-defined function. +It has to output an hexadecimal color string of the form `#RRGGBB` or `#RRGGBBAA`. + +```{code-cell} ipython3 +def my_color_function(feature): + """Maps low values to green and high values to red.""" + if unemployment_dict[feature["id"]] > 6.5: + return "#ff0000" + else: + return "#008000" +``` + +```{code-cell} ipython3 +m = folium.Map([43, -100], tiles="cartodbpositron", zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": my_color_function(feature), + "color": "black", + "weight": 2, + "dashArray": "5, 5", + }, +).add_to(m) + +m +``` + +## StepColormap + +But to help you define your colormap, we've embedded `StepColormap` in `folium.colormap`. + +You can simply define the colors you want, and the `index` (*thresholds*) that correspond. + +```{code-cell} ipython3 +import branca.colormap as cm + +step = cm.StepColormap( + ["green", "yellow", "red"], vmin=3, vmax=10, index=[3, 4, 8, 10], caption="step" +) + +step +``` + +```{code-cell} ipython3 +m = folium.Map([43, -100], tiles="cartodbpositron", zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": step(unemployment_dict[feature["id"]]), + "color": "black", + "weight": 2, + "dashArray": "5, 5", + }, +).add_to(m) + +m +``` + +If you specify no index, colors will be set uniformly. + +```{code-cell} ipython3 +cm.StepColormap(["r", "y", "g", "c", "b", "m"]) +``` + +## LinearColormap + +But sometimes, you would prefer to have a *continuous* set of colors. This can be done by `LinearColormap`. + +```{code-cell} ipython3 +linear = cm.LinearColormap(["green", "yellow", "red"], vmin=3, vmax=10) + +linear +``` + +```{code-cell} ipython3 +m = folium.Map([43, -100], tiles="cartodbpositron", zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": linear(unemployment_dict[feature["id"]]), + "color": "black", + "weight": 2, + "dashArray": "5, 5", + }, +).add_to(m) + +m +``` + +Again, you can set the `index` if you want something irregular. + +```{code-cell} ipython3 +cm.LinearColormap(["red", "orange", "yellow", "green"], index=[0, 0.1, 0.9, 1.0]) +``` + +If you want to transform a linear map into a *step* one, you can use the method `to_step`. + +```{code-cell} ipython3 +linear.to_step(6) +``` + +You can also use more sophisticated rules to create the thresholds. + +```{code-cell} ipython3 +linear.to_step( + n=6, + data=[30.6, 50, 51, 52, 53, 54, 55, 60, 70, 100], + method="quantiles", + round_method="int", +) +``` + +And the opposite is also possible with `to_linear`. + +```{code-cell} ipython3 +step.to_linear() +``` + +## Built-in + +For convenience, we provide a (small) set of built-in linear colormaps, in `folium.colormap.linear`. + +```{code-cell} ipython3 +cm.linear.OrRd_09 +``` + +You can also use them to generate regular `StepColormap`. + +```{code-cell} ipython3 +cm.linear.PuBuGn_09.to_step(12) +``` + +Of course, you may need to scale the colormaps to your bounds. This is doable with `.scale`. + +```{code-cell} ipython3 +cm.linear.YlGnBu_09.scale(3, 12) +``` + +```{code-cell} ipython3 +cm.linear.RdGy_11.to_step(10).scale(5, 100) +``` + +At last, if you want to check them all, simply ask for `linear` in the notebook. + +```{code-cell} ipython3 +cm.linear +``` + +## Draw a `ColorMap` on a map + +By the way, a ColorMap is also a Folium `Element` that you can draw on a map. + +```{code-cell} ipython3 +m = folium.Map(tiles="cartodbpositron") + +colormap = cm.linear.Set1_09.scale(0, 35).to_step(10) +colormap.caption = "A colormap caption" +m.add_child(colormap) + +m +``` diff --git a/docs/advanced_guide/custom_panes.md b/docs/advanced_guide/custom_panes.md new file mode 100644 index 0000000000..39066f548b --- /dev/null +++ b/docs/advanced_guide/custom_panes.md @@ -0,0 +1,56 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Panes and CustomPane + +Panes are used to control the ordering of layers on the map. You can customise +them using the `CustomPane` class. + +For more info on the panes Leaflet has, see https://leafletjs.com/reference.html#map-pane. + +First we'll load geojson data to use in the examples: + +```{code-cell} ipython3 +import requests + +geo_json_data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() + +style_function = lambda x: {"fillOpacity": 0.8} +``` + +## Map without custom pane + +We'll make an example to show how the GeoJson we add hides any labels +underneath. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4, tiles="stamentoner") + +folium.GeoJson(geo_json_data, style_function=style_function).add_to(m) + +m +``` + +## Map with custom pane + +Now we'll create a custom pane and add a tile layer that contains only labels. +The labels will show on top off the geojson. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4, tiles="stamentonerbackground") + +folium.GeoJson(geo_json_data, style_function=style_function).add_to(m) + +folium.map.CustomPane("labels").add_to(m) + +# Final layer associated to custom pane via the appropriate kwarg +folium.TileLayer("stamentonerlabels", pane="labels").add_to(m) + +m +``` diff --git a/docs/advanced_guide/custom_tiles.md b/docs/advanced_guide/custom_tiles.md new file mode 100644 index 0000000000..787e86a417 --- /dev/null +++ b/docs/advanced_guide/custom_tiles.md @@ -0,0 +1,70 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Custom tiles + +## No tiles + +```{code-cell} ipython3 +import requests + +states = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() + +kw = {"location": [48, -102], "zoom_start": 3} +``` + +```{code-cell} ipython3 +m = folium.Map(tiles=None, **kw) + +folium.GeoJson(states).add_to(m) + +m +``` + +## Use image as tiles + +```{code-cell} ipython3 +import branca + +# Create a white image of 4 pixels, and embed it in a url. +white_tile = branca.utilities.image_to_url([[1, 1], [1, 1]]) + +# Create a map using this url for each tile. +m = folium.Map(tiles=white_tile, attr="white tile", **kw) + +folium.GeoJson(states).add_to(m) + +m +``` + +## Create a larger pattern to use as tiles + +```{code-cell} ipython3 +images = [[(-1) ** ((i + j) // 30) for i in range(300)] for j in range(300)] + +tiles = branca.utilities.image_to_url(images) + +m = folium.Map(tiles=tiles, attr="Just because we can", **kw) + +folium.GeoJson(states).add_to(m) + +m +``` + +```{code-cell} ipython3 +images = [[(-1) ** ((i // 30 + j // 30)) for i in range(300)] for j in range(300)] + +tiles = branca.utilities.image_to_url(images) + +m = folium.Map(tiles=tiles, attr="Just because we can", **kw) + +folium.GeoJson(states).add_to(m) + +m +``` diff --git a/docs/flask.rst b/docs/advanced_guide/flask.rst similarity index 88% rename from docs/flask.rst rename to docs/advanced_guide/flask.rst index fe865349df..48af3eef9c 100644 --- a/docs/flask.rst +++ b/docs/advanced_guide/flask.rst @@ -8,4 +8,4 @@ or extract the map components and use those. Below is a script containing examples for all three use cases: -.. literalinclude:: ../examples/flask_example.py +.. literalinclude:: /_static/flask_example.py diff --git a/docs/advanced_guide/geodedetic_image_overlay.md b/docs/advanced_guide/geodedetic_image_overlay.md new file mode 100644 index 0000000000..66b1186247 --- /dev/null +++ b/docs/advanced_guide/geodedetic_image_overlay.md @@ -0,0 +1,107 @@ +# Geodedetic image overlay + +```{code-cell} ipython3 +import numpy as np + + +def sample_data(shape=(73, 145)): + nlats, nlons = shape + lats = np.linspace(-np.pi / 2, np.pi / 2, nlats) + lons = np.linspace(0, 2 * np.pi, nlons) + lons, lats = np.meshgrid(lons, lats) + wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons) + mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2) + + lats = np.rad2deg(lats) + lons = np.rad2deg(lons) + data = wave + mean + + return lons, lats, data + + +lon, lat, data = sample_data(shape=(73, 145)) +lon -= 180 +``` + +```{code-cell} ipython3 +%matplotlib inline + +import matplotlib + +cm = matplotlib.cm.get_cmap("cubehelix") + +normed_data = (data - data.min()) / (data.max() - data.min()) +colored_data = cm(normed_data) +``` + +## Bad + +```{code-cell} ipython3 +import folium + + +m = folium.Map(location=[lat.mean(), lon.mean()], zoom_start=1) + +folium.raster_layers.ImageOverlay( + image=colored_data, + bounds=[[lat.min(), lon.min()], [lat.max(), lon.max()]], + opacity=0.25, +).add_to(m) + +m +``` + +## Good + +```{code-cell} ipython3 +m = folium.Map(location=[lat.mean(), lon.mean()], zoom_start=1) + +folium.raster_layers.ImageOverlay( + image=colored_data, + bounds=[[lat.min(), lon.min()], [lat.max(), lon.max()]], + mercator_project=True, + opacity=0.25, +).add_to(m) + + +m +``` + +## Same as above but with cartopy + +```{code-cell} ipython3 +import cartopy.crs as ccrs +from cartopy.img_transform import warp_array + +source_extent = [lon.min(), lon.max(), lat.min(), lat.max()] + +new_data = warp_array( + colored_data, + target_proj=ccrs.GOOGLE_MERCATOR, + source_proj=ccrs.PlateCarree(), + target_res=data.shape, + source_extent=source_extent, + target_extent=None, + mask_extrapolated=False, +) + + +m = folium.Map(location=[lat.mean(), lon.mean()], zoom_start=1) + +folium.raster_layers.ImageOverlay( + image=new_data[0], + bounds=[[lat.min(), lon.min()], [lat.max(), lon.max()]], + opacity=0.25, +).add_to(m) + +m +``` + +TODO: Try [rasterio](https://github.com/mapbox/rasterio/blob/ca75cf0a842943c1b3da4522e6ea3500215130fd/docs/reproject.rst). Rasterio can warp images and arrays. + + +## Compare to original + +From https://scitools.org.uk/cartopy/docs/latest/gallery/scalar_data/waves.html + +![](https://scitools.org.uk/cartopy/docs/latest/_images/sphx_glr_waves_001.png) diff --git a/docs/advanced_guide/piechart_icons.md b/docs/advanced_guide/piechart_icons.md new file mode 100644 index 0000000000..9bef3de1a1 --- /dev/null +++ b/docs/advanced_guide/piechart_icons.md @@ -0,0 +1,110 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Piechart icons + +In this example we show how you can make mini-charts on several locations. +We'll make little piecharts showing the number of consonants and vowels in +a couple of languages. Those piecharts will be included as icons on the map. + +```{code-cell} ipython3 +import ast +import pandas + +data = pandas.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/consonants_vowels.csv", + # To ensure that tuples are read as tuples + converters={"coordinates": ast.literal_eval}, +) + +data.head() +``` + +## Pie charts + +```{code-cell} ipython3 +import io + +import matplotlib.pyplot as plt + +pie_charts_data = zip(data.consonants, data.vowels) + +fig = plt.figure(figsize=(0.5, 0.5)) +fig.patch.set_alpha(0) +ax = fig.add_subplot(111) +plots = [] +for sizes in pie_charts_data: + ax.pie(sizes, colors=("#e6194b", "#19e6b4")) + buff = io.StringIO() + plt.savefig(buff, format="SVG") + buff.seek(0) + svg = buff.read() + svg = svg.replace("\n", "") + plots.append(svg) + plt.cla() +plt.clf() +plt.close() +``` + +## Legend + +```{code-cell} ipython3 +import branca + +legend_html = """ +{% macro html(this, kwargs) %} +
+

 Consonants

+

 Vowels

+
+
+
+{% endmacro %} +""" + +legend = branca.element.MacroElement() +legend._template = branca.element.Template(legend_html) +``` + +## Map + +```{code-cell} ipython3 +m = folium.Map(location=(0, 0), zoom_start=2) + +for i, coord in enumerate(data.coordinates): + marker = folium.Marker(coord) + icon = folium.DivIcon(html=plots[i]) + marker.add_child(icon) + popup = folium.Popup( + "Consonants: {}
\nVowels: {}".format(data.consonants[i], data.vowels[i]) + ) + marker.add_child(popup) + m.add_child(marker) +m.get_root().add_child(legend) + +m +``` diff --git a/docs/advanced_guide/polygons_from_list_of_points.md b/docs/advanced_guide/polygons_from_list_of_points.md new file mode 100644 index 0000000000..5b3984b51a --- /dev/null +++ b/docs/advanced_guide/polygons_from_list_of_points.md @@ -0,0 +1,270 @@ +# Creating a polygon from a list of points + +For many of those working with geo data it is a common task being asked to create a polygon from a list of points. More specific, to create a polygon that wraps around those points in a meaningful manner. So, there are several sources in the web explaining how to create the shape (see sources at end of document). This example notebook is the application of those solutions to folium maps. + +## Helpers + +```{code-cell} ipython3 +# Imports +import random + +import folium +from scipy.spatial import ConvexHull + + +# Function to create a list of some random points +def randome_points(amount, LON_min, LON_max, LAT_min, LAT_max): + + points = [] + for _ in range(amount): + points.append( + (random.uniform(LON_min, LON_max), random.uniform(LAT_min, LAT_max)) + ) + + return points + + +# Function to draw points in the map +def draw_points(map_object, list_of_points, layer_name, line_color, fill_color, text): + + fg = folium.FeatureGroup(name=layer_name) + + for point in list_of_points: + fg.add_child( + folium.CircleMarker( + point, + radius=1, + color=line_color, + fill_color=fill_color, + popup=(folium.Popup(text)), + ) + ) + + map_object.add_child(fg) +``` + +## Convex hull + +The convex hull is probably the most common approach - its goal is to create the smallest polygon that contains all points from a given list. The scipy.spatial package provides this algorithm (https://docs.scipy.org/doc/scipy-0.19.0/reference/generated/scipy.spatial.ConvexHull.html, accessed 29.12.2018). + +```{code-cell} ipython3 +# Function that takes a map and a list of points (LON,LAT tupels) and +# returns a map with the convex hull polygon from the points as a new layer + + +def create_convexhull_polygon( + map_object, list_of_points, layer_name, line_color, fill_color, weight, text +): + + # Since it is pointless to draw a convex hull polygon around less than 3 points check len of input + if len(list_of_points) < 3: + return + + # Create the convex hull using scipy.spatial + form = [list_of_points[i] for i in ConvexHull(list_of_points).vertices] + + # Create feature group, add the polygon and add the feature group to the map + fg = folium.FeatureGroup(name=layer_name) + fg.add_child( + folium.vector_layers.Polygon( + locations=form, + color=line_color, + fill_color=fill_color, + weight=weight, + popup=(folium.Popup(text)), + ) + ) + map_object.add_child(fg) + + return map_object +``` + +```{code-cell} ipython3 +# Initialize map +my_convexhull_map = folium.Map(location=[48.5, 9.5], zoom_start=8) + +# Create a convex hull polygon that contains some points +list_of_points = randome_points( + amount=10, LON_min=48, LON_max=49, LAT_min=9, LAT_max=10 +) + +create_convexhull_polygon( + my_convexhull_map, + list_of_points, + layer_name="Example convex hull", + line_color="lightblue", + fill_color="lightskyblue", + weight=5, + text="Example convex hull", +) + +draw_points( + my_convexhull_map, + list_of_points, + layer_name="Example points for convex hull", + line_color="royalblue", + fill_color="royalblue", + text="Example point for convex hull", +) + +# Add layer control and show map +folium.LayerControl(collapsed=False).add_to(my_convexhull_map) +my_convexhull_map +``` + +## Envelope + +The envelope is another interesting approach - its goal is to create a box that contains all points from a given list. + +```{code-cell} ipython3 +def create_envelope_polygon( + map_object, list_of_points, layer_name, line_color, fill_color, weight, text +): + + # Since it is pointless to draw a box around less than 2 points check len of input + if len(list_of_points) < 2: + return + + # Find the edges of box + from operator import itemgetter + + list_of_points = sorted(list_of_points, key=itemgetter(0)) + x_min = list_of_points[0] + x_max = list_of_points[len(list_of_points) - 1] + + list_of_points = sorted(list_of_points, key=itemgetter(1)) + y_min = list_of_points[0] + y_max = list_of_points[len(list_of_points) - 1] + + upper_left = (x_min[0], y_max[1]) + upper_right = (x_max[0], y_max[1]) + lower_right = (x_max[0], y_min[1]) + lower_left = (x_min[0], y_min[1]) + + edges = [upper_left, upper_right, lower_right, lower_left] + + # Create feature group, add the polygon and add the feature group to the map + fg = folium.FeatureGroup(name=layer_name) + fg.add_child( + folium.vector_layers.Polygon( + locations=edges, + color=line_color, + fill_color=fill_color, + weight=weight, + popup=(folium.Popup(text)), + ) + ) + map_object.add_child(fg) + + return map_object +``` + +```{code-cell} ipython3 +# Initialize map +my_envelope_map = folium.Map(location=[49.5, 8.5], zoom_start=8) + +# Create an envelope polygon that contains some points +list_of_points = randome_points( + amount=10, LON_min=49.1, LON_max=50, LAT_min=8, LAT_max=9 +) + +create_envelope_polygon( + my_envelope_map, + list_of_points, + layer_name="Example envelope", + line_color="indianred", + fill_color="red", + weight=5, + text="Example envelope", +) + +draw_points( + my_envelope_map, + list_of_points, + layer_name="Example points for envelope", + line_color="darkred", + fill_color="darkred", + text="Example point for envelope", +) + +# Add layer control and show map +folium.LayerControl(collapsed=False).add_to(my_envelope_map) +my_envelope_map +``` + +## Concave hull (alpha shape) +In some cases the convex hull does not yield good results - this is when the shape of the polygon should be concave instead of convex. The solution is a concave hull that is also called alpha shape. Yet, there is no ready to go, off the shelve solution for this but there are great resources (see: https://web.archive.org/web/20191207074940/http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/, accessed 04.01.2019 or https://towardsdatascience.com/the-concave-hull-c649795c0f0f, accessed 29.12.2018). + + +## Putting it together + +Just putting it all together... + +```{code-cell} ipython3 +# Initialize map +my_map_global = folium.Map(location=[48.2460683, 9.26764125], zoom_start=7) + +# Create a convex hull polygon that contains some points +list_of_points = randome_points( + amount=10, LON_min=48, LON_max=49, LAT_min=9, LAT_max=10 +) + +create_convexhull_polygon( + my_map_global, + list_of_points, + layer_name="Example convex hull", + line_color="lightblue", + fill_color="lightskyblue", + weight=5, + text="Example convex hull", +) + +draw_points( + my_map_global, + list_of_points, + layer_name="Example points for convex hull", + line_color="royalblue", + fill_color="royalblue", + text="Example point for convex hull", +) + +# Create an envelope polygon that contains some points +list_of_points = randome_points( + amount=10, LON_min=49.1, LON_max=50, LAT_min=8, LAT_max=9 +) + +create_envelope_polygon( + my_map_global, + list_of_points, + layer_name="Example envelope", + line_color="indianred", + fill_color="red", + weight=5, + text="Example envelope", +) + +draw_points( + my_map_global, + list_of_points, + layer_name="Example points for envelope", + line_color="darkred", + fill_color="darkred", + text="Example point for envelope", +) + +# Add layer control and show map +folium.LayerControl(collapsed=False).add_to(my_map_global) +my_map_global +``` + +## Sources: + +* https://web.archive.org/web/20200222150431/http://blog.yhat.com/posts/interactive-geospatial-analysis.html, accessed 28.12.2018 + +* https://docs.scipy.org/doc/scipy-0.19.0/reference/generated/scipy.spatial.ConvexHull.html, accessed 29.12.2018 + +* https://medium.com/@vworri/simple-geospacial-mapping-with-geopandas-and-the-usual-suspects-77f46d40e807, accessed 29.12.2018 + +* https://towardsdatascience.com/the-concave-hull-c649795c0f0f, accessed 29.12.2018 + +* https://web.archive.org/web/20191207074940/http://blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/, accessed 04.01.2019 diff --git a/docs/advanced_guide/subplots.md b/docs/advanced_guide/subplots.md new file mode 100644 index 0000000000..75b9f283ca --- /dev/null +++ b/docs/advanced_guide/subplots.md @@ -0,0 +1,89 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import branca +``` + +## Subplots + +### Two maps in one figure + +A `Map` automatically creates a `Figure` to put itself in. You can also do this +manually, or in this example, create subplots to have two maps in one figure. + +```{code-cell} ipython3 +fig = branca.element.Figure() + +subplot1 = fig.add_subplot(1, 2, 1) +subplot2 = fig.add_subplot(1, 2, 2) + +subplot1.add_child( + folium.Map([0, 0], tiles="stamenwatercolor", zoom_start=1) +) +subplot2.add_child( + folium.Map([46, 3], tiles="OpenStreetMap", zoom_start=5) +) + +fig +``` + + +### Vega div and a Map + +Here we create a single figure in which we'll embed two maps and two Vega charts. +Note that this also works with `VegaLite`. + +```{code-cell} ipython3 +import json + +import numpy as np +import vincent + + +multi_iter2 = { + "x": np.random.uniform(size=(100,)), + "y": np.random.uniform(size=(100,)), +} +scatter = vincent.Scatter(multi_iter2, iter_idx="x", height=250, width=420) +data = json.loads(scatter.to_json()) + +f = branca.element.Figure() + +# Create two maps. +m = folium.Map( + location=[0, 0], + tiles="stamenwatercolor", + zoom_start=1, + position="absolute", + left="0%", + width="50%", + height="50%", +) + +m2 = folium.Map( + location=[46, 3], + tiles="OpenStreetMap", + zoom_start=4, + position="absolute", + left="50%", + width="50%", + height="50%", + top="50%", +) + +# Create two Vega charts +v = folium.Vega(data, position="absolute", left="50%", width="50%", height="50%") + +v2 = folium.Vega( + data, position="absolute", left="0%", width="50%", height="50%", top="50%" +) + +f.add_child(m) +f.add_child(m2) +f.add_child(v) +f.add_child(v2) + +f +``` diff --git a/docs/advanced_guide/world_copy.md b/docs/advanced_guide/world_copy.md new file mode 100644 index 0000000000..aed0e1376c --- /dev/null +++ b/docs/advanced_guide/world_copy.md @@ -0,0 +1,43 @@ +# Scrolling beyond the world edge + +What happens if you scroll beyond the longitudinal edge of the world? Leaflet has a setting +to determine the behavior, which we'll demonstrate here. + +## Defaults + +```{code-cell} ipython3 +import folium + +m = folium.Map(world_copy_jump=False) + +folium.Marker( + location=[0, 0], popup="I will disappear when moved outside the map domain." +).add_to(m) + +m +``` + +## World copy jump + +```{code-cell} ipython3 +m = folium.Map(world_copy_jump=True) + +folium.Marker( + location=[0, 0], + popup="I will magically reappear when moved outside the map domain.", +).add_to(m) + +m +``` + +## No wrap + +```{code-cell} ipython3 +m = folium.Map( + tiles=folium.TileLayer(no_wrap=True) +) + +folium.Marker(location=[0, 0], popup="The map domain here is not wrapped.").add_to(m) + +m +``` diff --git a/docs/conf.py b/docs/conf.py index 46d87f0f0b..818f31c6fe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,9 +59,10 @@ import folium version = release = folium.__version__ - -# The short X.Y version. -version = ".".join(release.split(".")[:2]) +if "+" in version: + version, remainder = release.split("+") + if not remainder.startswith("0"): + version = version + ".dev+" + remainder.split(".")[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -102,19 +103,26 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "f6" +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -# html_theme_options = {} +html_theme_options = { + "switcher": { + "json_url": "https://python-visualization.github.io/folium/latest/_static/switcher.json", + "version_match": "latest" if ".dev+" in version else version, + }, + "navbar_start": ["navbar-logo", "version-switcher"], + "footer_start": ["version", "copyright", "sphinx-version", "theme-version"], +} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ["_themes"] +# html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -# html_title = None +html_title = "Folium documentation" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None @@ -133,6 +141,10 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_css_files = [ + "custom.css", +] + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' @@ -262,4 +274,19 @@ "https://xyzservices.readthedocs.io/en/latest/", "https://xyzservices.readthedocs.io/en/latest/objects.inv", ), + "branca": ( + "https://python-visualization.github.io/branca/", + "https://python-visualization.github.io/branca/objects.inv", + ), +} + +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, + "no-value": "default_js,default_css", +} + +nbsphinx_custom_formats = { + ".md": ["jupytext.reads", {"fmt": "mystnb"}], } diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000000..43f936aabf --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,204 @@ +Getting started +=============== + +Installation +------------ + +Folium can be installed using + +``` +$ pip install folium +``` + +If you are using the Conda package manager, the equivalent is + +``` +$ conda install folium -c conda-forge +``` + + +### Dependencies + +Folium has the following dependencies, all of which are installed automatically +with the above installation commands: + +- branca +- Jinja2 +- Numpy +- Requests + +Additional packages may be necessary for some functionality. It will say so in +the documentation where that's the case. + + +Creating a map +--------------- + +Here's a basic example of creating a map: + +```{code-cell} ipython3 +import folium + +m = folium.Map(location=(45.5236, -122.6750)) +``` + +If you are in a Jupyter Notebook, you can display it by asking for the object representation: + +```{code-cell} ipython3 +m +``` + +Or you can save it as an HTML file: + +```{code-cell} ipython3 +m.save("index.html") +``` + + +Choosing a tileset +------------------ + +The default tiles are set to `OpenStreetMap`, but a selection of tilesets from Stamen and CartoDB are also built in. + +```{code-cell} ipython3 +folium.Map((45.5236, -122.6750), tiles="cartodb positron") +``` + +You can also pass any tileset as a url template. Choose one from https://leaflet-extras.github.io/leaflet-providers/preview/ +and pass the url and attribution. For example: + +``` +folium.Map(tiles='https://{s}.tiles.example.com/{z}/{x}/{y}.png', attr='My Data Attribution') +``` + +Folium also accepts objects from the [xyzservices package](https://github.com/geopandas/xyzservices). + + +Adding markers +-------------- + +There are various marker types, here we start with a simple `Marker`. You can add a popup and +tooltip. You can also pick colors and icons. + +```{code-cell} ipython3 +m = folium.Map([45.35, -121.6972], zoom_start=12, tiles="Stamen Terrain") + +folium.Marker( + location=[45.3288, -121.6625], + tooltip="Click me!", + popup="Mt. Hood Meadows", + icon=folium.Icon(icon="cloud"), +).add_to(m) + +folium.Marker( + location=[45.3311, -121.7113], + tooltip="Click me!", + popup="Timberline Lodge", + icon=folium.Icon(color="green"), +).add_to(m) + +m +``` + + +Vectors such as lines +--------------------- + +Folium has various vector elements. One example is `PolyLine`, which can show linear elements on a map. +This object can help put emphasis on a trail, a road, or a coastline. + +```{code-cell} ipython3 +m = folium.Map(location=[-71.38, -73.9], zoom_start=11) + +trail_coordinates = [ + (-71.351871840295871, -73.655963711222626), + (-71.374144382613707, -73.719861619751498), + (-71.391042575973145, -73.784922248007007), + (-71.400964450973134, -73.851042243124397), + (-71.402411391077322, -74.050048183880477), +] + +folium.PolyLine(trail_coordinates, tooltip="Coast").add_to(m) + +m +``` + + +Grouping and controlling +------------------------ + +You can group multiple elements such as markers together in a `FeatureGroup`. You can select +which you want to show by adding a `LayerControl` to the map. + +```{code-cell} ipython3 +m = folium.Map((0, 0), zoom_start=7) + +group_1 = folium.FeatureGroup("first group").add_to(m) +folium.Marker((0, 0), icon=folium.Icon("red")).add_to(group_1) +folium.Marker((1, 0), icon=folium.Icon("red")).add_to(group_1) + +group_2 = folium.FeatureGroup("second group").add_to(m) +folium.Marker((0, 1), icon=folium.Icon("green")).add_to(group_2) + +folium.LayerControl().add_to(m) + +m +``` + + +GeoJSON/TopoJSON overlays +------------------------- + +Folium supports both GeoJSON and TopoJSON data in various formats, such as urls, +file paths and dictionaries. + +```{code-cell} ipython3 +import requests + +m = folium.Map(tiles="cartodbpositron") + +geojson_data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/world_countries.json" +).json() + +folium.GeoJson(geojson_data, name="hello world").add_to(m) + +folium.LayerControl().add_to(m) + +m +``` + + +Choropleth maps +--------------- + +Choropleth can be created by binding the data between Pandas DataFrames/Series and Geo/TopoJSON geometries. + +```{code-cell} ipython3 +import pandas + +state_geo = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() +state_data = pandas.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_unemployment_oct_2012.csv" +) + +m = folium.Map(location=[48, -102], zoom_start=3) + +folium.Choropleth( + geo_data=state_geo, + name="choropleth", + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", + fill_color="YlGn", + fill_opacity=0.7, + line_opacity=0.2, + legend_name="Unemployment Rate (%)", +).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` diff --git a/docs/index.rst b/docs/index.rst index 03cf021f06..dcc20f260f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,8 +5,7 @@ Folium .. image:: _static/folium_logo.jpg -Python data, leaflet.js maps -**************************** +**Python data, leaflet.js maps** ``folium`` builds on the data wrangling strengths of the Python ecosystem and the mapping strengths of the ``leaflet.js`` library. @@ -21,18 +20,28 @@ It enables both the binding of data to a map for ``choropleth`` visualizations as well as passing rich vector/raster/HTML visualizations as markers on the map. The library has a number of built-in tilesets from OpenStreetMap, -Mapbox, and Stamen, and supports custom tilesets with Mapbox or Cloudmade API keys. -``folium`` supports both Image, Video, GeoJSON and TopoJSON overlays. +Mapbox, and Stamen, and supports custom tilesets. +``folium`` supports both Image, Video, GeoJSON and TopoJSON overlays and has a +number of vector layers built-in. Contents ======== .. toctree:: - :maxdepth: 3 + :maxdepth: 1 - installing - quickstart.ipynb - flask - modules - plugins + Home + getting_started + user_guide + advanced_guide + reference + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installing.rst b/docs/installing.rst deleted file mode 100644 index 734f9d8637..0000000000 --- a/docs/installing.rst +++ /dev/null @@ -1,24 +0,0 @@ -Installing -========== - -Requirements ------------- -:: - - branca, jinja2 and requests. - -Some functionalities may require extra dependencies -`numpy`, `pandas`, `geopandas`, `altair`, etc. - - -Installation ------------- -:: - -$ pip install folium - -or - -:: - -$ conda install folium -c conda-forge diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index 707c3336a3..0000000000 --- a/docs/modules.rst +++ /dev/null @@ -1,43 +0,0 @@ -:mod:`folium` -------------- - -.. automodule:: folium.folium - :members: - :undoc-members: - :show-inheritance: - - -:mod:`map` ----------- - -.. automodule:: folium.map - :members: - :undoc-members: - :show-inheritance: - - -:mod:`Vector Layers` --------------------- - -.. automodule:: folium.vector_layers - :members: - :undoc-members: - :show-inheritance: - - -:mod:`Raster Layers` --------------------- - -.. automodule:: folium.raster_layers - :members: - :undoc-members: - :show-inheritance: - - -:mod:`Extra Features` ---------------------- - -.. automodule:: folium.features - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/plugins.rst b/docs/plugins.rst deleted file mode 100644 index d3a810c8e7..0000000000 --- a/docs/plugins.rst +++ /dev/null @@ -1,7 +0,0 @@ -:mod:`plugins` --------------- - -.. automodule:: folium.plugins - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000000..0b3c7d9c75 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,36 @@ +API reference +============= + +Map +-------------------- + +.. automodule:: folium.folium + + +UI elements +-------------------- + +.. automodule:: folium.map + + +Raster Layers +-------------------- + +.. automodule:: folium.raster_layers + + +Vector Layers +-------------------- + +.. automodule:: folium.vector_layers + + +Other map features +--------------------- + +.. automodule:: folium.features + + +Plugins +-------------------- +.. automodule:: folium.plugins diff --git a/docs/user_guide.rst b/docs/user_guide.rst new file mode 100644 index 0000000000..fa7e3db099 --- /dev/null +++ b/docs/user_guide.rst @@ -0,0 +1,19 @@ +User guide +========== + +The user guide covers different parts of basic usage of Folium. Each page focuses on a single topic and outlines how it is implemented in Folium, with reproducible examples. + +If you don't know anything about Folium, start with the :doc:`getting_started`. + +Advanced topics can be found in the :doc:`Advanced Guide ` and further specification in the :doc:`API Reference `. + +.. toctree:: + :maxdepth: 2 + + user_guide/map + user_guide/ui_elements + user_guide/raster_layers + user_guide/vector_layers + user_guide/geojson + user_guide/features + user_guide/plugins diff --git a/docs/user_guide/features.rst b/docs/user_guide/features.rst new file mode 100644 index 0000000000..99455a4277 --- /dev/null +++ b/docs/user_guide/features.rst @@ -0,0 +1,8 @@ +Features +-------------------------- + +.. toctree:: + :maxdepth: 1 + + features/fit_overlays + features/click_related_classes diff --git a/docs/user_guide/features/click_related_classes.md b/docs/user_guide/features/click_related_classes.md new file mode 100644 index 0000000000..9996a7a784 --- /dev/null +++ b/docs/user_guide/features/click_related_classes.md @@ -0,0 +1,65 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +### Click-related classes + +#### ClickForMarker + +`ClickForMarker` lets you create markers on each click. + +```{code-cell} ipython3 +folium.Map().add_child( + folium.ClickForMarker() +) +``` + +*Click on the map to see the effects* + +You can customize the popup by providing a string, an IFrame object or an Html object. You can include the latitude and longitude of the marker by using `${lat}` and `${lng}`. + +```{code-cell} ipython3 +folium.Map().add_child( + folium.ClickForMarker("Lat: ${lat}
Lon: ${lng}") +) +``` + +*Click on the map to see the effects* + + +#### LatLngPopup + +`LatLngPopup` lets you create a simple popup at each click. + +```{code-cell} ipython3 +folium.Map().add_child( + folium.LatLngPopup() +) +``` + +*Click on the map to see the effects* + ++++ + +#### ClickForLatLng + +`ClickForLatLng` lets you copy coordinates to your browser clipboard. + +```{code-cell} ipython3 +folium.Map().add_child( + folium.ClickForLatLng(format_str='"[" + lat + "," + lng + "]"', alert=True) +) +``` + +*Click on the map to see the effects* + +If you want to collect back the information in python, you may (install and) import the [clipboard](https://github.com/terryyin/clipboard) library: + +``` +>>> import clipboard +>>> clipboard.paste() +[-43.580391,-123.824467] +``` diff --git a/docs/user_guide/features/fit_overlays.md b/docs/user_guide/features/fit_overlays.md new file mode 100644 index 0000000000..e4ba74dca1 --- /dev/null +++ b/docs/user_guide/features/fit_overlays.md @@ -0,0 +1,40 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## FitOverlays + +When you add this class to your map, the map will pan and zoom to fit the enabled overlays. + +By default, the map won't necessarily show all elements that were added. You may have to pan or zoom out to find them. + +If we add the `FitOverlays` class, it will automatically pan and zoom to show the enabled overlays. +In this example we show only the first marker by default. If you enable the second marker, the view changes to include it. + +```{code-cell} ipython3 +m = folium.Map((52, 0), tiles='cartodbpositron', zoom_start=8) + +fg1 = folium.FeatureGroup().add_to(m) +folium.Marker((52, 5)).add_to(fg1) + +fg2 = folium.FeatureGroup(show=False).add_to(m) +folium.Marker((52, 5.1)).add_to(fg2) + +folium.FitOverlays().add_to(m) + +folium.LayerControl().add_to(m) + +m +``` + +`FitOverlays` has a couple options: + +- `padding` adds pixels around the bounds. +- `max_zoom` can be used to prevent zooming in too far. +- `fly` enables a smoother, longer animation, so you can see how the view changes. +- `fit_on_map_load` can be used to disable the fitting that happens when the map loads. + +Note that `padding` and `max_zoom` can achieve the same effect. diff --git a/docs/user_guide/geojson.rst b/docs/user_guide/geojson.rst new file mode 100644 index 0000000000..704dc2e4b1 --- /dev/null +++ b/docs/user_guide/geojson.rst @@ -0,0 +1,12 @@ +GeoJSON and choropleth +-------------------------- + +.. toctree:: + :maxdepth: 2 + + geojson/geojson + geojson/choropleth + geojson/geojson_marker + geojson/geojson_popup_and_tooltip + geojson/geopandas_and_geo_interface + geojson/smoothing diff --git a/docs/user_guide/geojson/choropleth.md b/docs/user_guide/geojson/choropleth.md new file mode 100644 index 0000000000..540cd2f259 --- /dev/null +++ b/docs/user_guide/geojson/choropleth.md @@ -0,0 +1,132 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## Using `Choropleth` + +Now if you want to get faster, you can use the `Choropleth` class. Have a look at it's docstring, it has several styling options. + +Just like the `GeoJson` class you can provide it a filename, a dict, or a geopandas object. + +```{code-cell} ipython3 +import requests + +m = folium.Map([43, -100], zoom_start=4) + +us_states = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() + +folium.Choropleth( + geo_data=us_states, + fill_opacity=0.3, + line_weight=2, +).add_to(m) + +m +``` + +Then, in playing with keyword arguments, you can get a choropleth in a few lines: + +```{code-cell} ipython3 +import pandas + +state_data = pandas.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_unemployment_oct_2012.csv" +) + +m = folium.Map([43, -100], zoom_start=4) + +folium.Choropleth( + geo_data=us_states, + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", +).add_to(m) + +m +``` + +You can force the color scale to a given number of bins (or directly list the bins you would like), by providing the `bins` argument. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.Choropleth( + geo_data=us_states, + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", + fill_color="YlGn", + bins=[3, 4, 9, 11], +).add_to(m) + +m +``` + +You can also enable the highlight function, to enable highlight functionality when you hover over each area. + +```{code-cell} ipython3 +m = folium.Map(location=[48, -102], zoom_start=3) +folium.Choropleth( + geo_data=us_states, + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", + fill_color="YlGn", + fill_opacity=0.7, + line_opacity=0.2, + legend_name="Unemployment Rate (%)", + highlight=True, +).add_to(m) + +m +``` + +You can customize the way missing and `nan` values are displayed on your map using the two parameters `nan_fill_color` and `nan_fill_opacity`. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +messed_up_data = state_data.drop(0) +messed_up_data.loc[4, "Unemployment"] = float("nan") + +folium.Choropleth( + geo_data=us_states, + data=messed_up_data, + columns=["State", "Unemployment"], + nan_fill_color="purple", + nan_fill_opacity=0.4, + key_on="feature.id", + fill_color="YlGn", +).add_to(m) + +m +``` + +Internally Choropleth uses the `GeoJson` or `TopoJson` class, depending on your settings, and the `StepColormap` class. Both objects are attributes of your `Choropleth` object called `geojson` and `color_scale`. You can make changes to them, but for regular things you won't have to. For example setting a name for in the layer controls or disabling showing the layer on opening the map is possible in `Choropleth` itself. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +choropleth = folium.Choropleth( + geo_data=us_states, + data=state_data, + columns=["State", "Unemployment"], + key_on="feature.id", + fill_color="YlGn", + name="Unenployment", + show=False, +).add_to(m) + +# The underlying GeoJson and StepColormap objects are reachable +print(type(choropleth.geojson)) +print(type(choropleth.color_scale)) + +folium.LayerControl(collapsed=False).add_to(m) + +m +``` diff --git a/docs/user_guide/geojson/geojson.md b/docs/user_guide/geojson/geojson.md new file mode 100644 index 0000000000..5a5207f3b8 --- /dev/null +++ b/docs/user_guide/geojson/geojson.md @@ -0,0 +1,238 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## Using `GeoJson` + +### Loading data + +Let us load a GeoJSON file representing the US states. + +```{code-cell} ipython3 +import requests + +geo_json_data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() +``` + +It is a classical GeoJSON `FeatureCollection` (see https://en.wikipedia.org/wiki/GeoJSON) of the form : + + { + "type": "FeatureCollection", + "features": [ + { + "properties": {"name": "Alabama"}, + "id": "AL", + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[-87.359296, 35.00118], ...]] + } + }, + { + "properties": {"name": "Alaska"}, + "id": "AK", + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [[[[-131.602021, 55.117982], ... ]]] + } + }, + ... + ] + } + +A first way of drawing it on a map, is simply to use `folium.GeoJson` : + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson(geo_json_data).add_to(m) + +m +``` + +Note that you can avoid loading the file on yourself, +by providing a (local) file path or a url. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +url = "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" + +folium.GeoJson(url).add_to(m) + +m +``` + +You can pass a geopandas object. + +```{code-cell} ipython3 +import geopandas + +gdf = geopandas.read_file(url) + +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson( + gdf, +).add_to(m) + +m +``` + +### Click on zoom + +You can enable an option that if you click on a part of the geometry the map will zoom in to that. + +Try it on the map below: + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson(geo_json_data, zoom_on_click=True).add_to(m) + +m +``` + +### Styling + +Now this is cool and simple, but we may be willing to choose the style of the data. + +You can provide a function of the form `lambda feature: {}` that sets the style of each feature. + +For possible options, see: + +* For `Point` and `MultiPoint`, see https://leafletjs.com/reference.html#marker +* For other features, see https://leafletjs.com/reference.html#path and https://leafletjs.com/reference.html#polyline + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": "#ffff00", + "color": "black", + "weight": 2, + "dashArray": "5, 5", + }, +).add_to(m) + +m +``` + +What's cool in providing a function, is that you can specify a style depending on the feature. For example, if you want to visualize in green all states whose name contains the letter 'E', just do: + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": "green" + if "e" in feature["properties"]["name"].lower() + else "#ffff00", + "color": "black", + "weight": 2, + "dashArray": "5, 5", + }, +).add_to(m) + +m +``` + +Wow, this looks almost like a choropleth. To do one, we just need to compute a color for each state. + +Let's imagine we want to draw a choropleth of unemployment in the US. + +First, we may load the data: + +```{code-cell} ipython3 +import pandas + +unemployment = pandas.read_csv( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_unemployment_oct_2012.csv" +) + +unemployment.head(5) +``` + +Now we need to create a function that maps one value to a RGB color (of the form `#RRGGBB`). +For this, we'll use colormap tools from `folium.colormap`. + +```{code-cell} ipython3 +from branca.colormap import linear + +colormap = linear.YlGn_09.scale( + unemployment.Unemployment.min(), unemployment.Unemployment.max() +) + +print(colormap(5.0)) + +colormap +``` + +We need also to convert the table into a dictionary, in order to map a feature to it's unemployment value. + +```{code-cell} ipython3 +unemployment_dict = unemployment.set_index("State")["Unemployment"] + +unemployment_dict["AL"] +``` + +Now we can do the choropleth. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson( + geo_json_data, + name="unemployment", + style_function=lambda feature: { + "fillColor": colormap(unemployment_dict[feature["id"]]), + "color": "black", + "weight": 1, + "dashArray": "5, 5", + "fillOpacity": 0.9, + }, +).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` + +Of course, if you can create and/or use a dictionary providing directly the good color. Thus, the finishing seems faster: + +```{code-cell} ipython3 +color_dict = {key: colormap(unemployment_dict[key]) for key in unemployment_dict.keys()} +``` + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +folium.GeoJson( + geo_json_data, + style_function=lambda feature: { + "fillColor": color_dict[feature["id"]], + "color": "black", + "weight": 1, + "dashArray": "5, 5", + "fillOpacity": 0.9, + }, +).add_to(m) +``` + +Note that adding a color legend may be a good idea. + +```{code-cell} ipython3 +colormap.caption = "Unemployment color scale" +colormap.add_to(m) + +m +``` diff --git a/docs/user_guide/geojson/geojson_marker.md b/docs/user_guide/geojson/geojson_marker.md new file mode 100644 index 0000000000..5f9505d458 --- /dev/null +++ b/docs/user_guide/geojson/geojson_marker.md @@ -0,0 +1,115 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# GeoJSON point features with markers + + +```{code-cell} ipython3 +import geopandas + +gdf = geopandas.read_file( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson" +) + +gdf.head() +``` + +```{code-cell} ipython3 +gdf['href'] = '' + gdf.url + "" +gdf['service_level'] = gdf.notes.str.split(', ').apply(lambda x: len([v for v in x if "all" in v])) +gdf['lines_served'] = gdf.line.str.split('-').apply(lambda x: len(x)) +``` + +```{code-cell} ipython3 +service_levels = gdf.service_level.unique().tolist() +service_levels +``` + +```{code-cell} ipython3 +colors = ["orange", "yellow", "green", "blue"] +``` + +## Use a Circle as a Marker + +```{code-cell} ipython3 +m = folium.Map(location=[40.75, -73.95], zoom_start=13) + +folium.GeoJson( + gdf, + name="Subway Stations", + marker=folium.Circle(radius=4, fill_color="orange", fill_opacity=0.4, color="black", weight=1), + tooltip=folium.GeoJsonTooltip(fields=["name", "line", "notes"]), + popup=folium.GeoJsonPopup(fields=["name", "line", "href", "notes"]), + style_function=lambda x: { + "fillColor": colors[x['properties']['service_level']], + "radius": (x['properties']['lines_served'])*30, + }, + highlight_function=lambda x: {"fillOpacity": 0.8}, + zoom_on_click=True, +).add_to(m) + +m +``` + +## Or use a DivIcon + +```{code-cell} ipython3 +m = folium.Map(location=[40.75, -73.95], zoom_start=13) + + +def style_function(feature): + props = feature.get('properties') + markup = f""" + +
+
+
+ {props.get('name')} +
+
+ """ + return {"html": markup} + + +folium.GeoJson( + gdf, + name="Subway Stations", + marker=folium.Marker(icon=folium.DivIcon()), + tooltip=folium.GeoJsonTooltip(fields=["name", "line", "notes"]), + popup=folium.GeoJsonPopup(fields=["name", "line", "href", "notes"]), + style_function=style_function, + zoom_on_click=True, +).add_to(m) + +m +``` + +## Use a Marker + +```{code-cell} ipython3 +m = folium.Map(location=[40.75, -73.95], zoom_start=13) + +marker_colors = ["red", "orange", "green", "blue"] + +folium.GeoJson( + gdf, + name="Subway Stations", + zoom_on_click=True, + marker=folium.Marker(icon=folium.Icon(icon='star')), + tooltip=folium.GeoJsonTooltip(fields=["name", "line", "notes"]), + popup=folium.GeoJsonPopup(fields=["name", "line", "href", "notes"]), + style_function=lambda x: { + 'markerColor': marker_colors[x['properties']['service_level']], + }, +).add_to(m) + +m +``` diff --git a/docs/user_guide/geojson/geojson_popup_and_tooltip.md b/docs/user_guide/geojson/geojson_popup_and_tooltip.md new file mode 100644 index 0000000000..68cb31f0a1 --- /dev/null +++ b/docs/user_guide/geojson/geojson_popup_and_tooltip.md @@ -0,0 +1,138 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# GeoJSON popup and tooltip + +```{code-cell} ipython3 +import pandas as pd + + +income = pd.read_csv( + "https://raw.githubusercontent.com/pri-data/50-states/master/data/income-counties-states-national.csv", + dtype={"fips": str}, +) +income["income-2015"] = pd.to_numeric(income["income-2015"], errors="coerce") +``` + +```{code-cell} ipython3 +income.head() +``` + +```{code-cell} ipython3 +import geopandas +import requests + +data = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() +states = geopandas.GeoDataFrame.from_features(data, crs="EPSG:4326") + +states.head() +``` + +```{code-cell} ipython3 +import requests + +response = requests.get( + "https://gist.githubusercontent.com/tvpmb/4734703/raw/" + "b54d03154c339ed3047c66fefcece4727dfc931a/US%2520State%2520List" +) +abbrs = pd.read_json(response.text) + +abbrs.head(3) +``` + +```{code-cell} ipython3 +statesmerge = states.merge(abbrs, how="left", left_on="name", right_on="name") +statesmerge["geometry"] = statesmerge.geometry.simplify(0.05) + +statesmerge.head() +``` + +```{code-cell} ipython3 +income.groupby("state")["income-2015"].median().head() +``` + +```{code-cell} ipython3 +statesmerge["medianincome"] = statesmerge.merge( + income.groupby("state")["income-2015"].median(), + how="left", + left_on="alpha-2", + right_on="state", +)["income-2015"] +statesmerge["change"] = statesmerge.merge( + income.groupby("state")["change"].median(), + how="left", + left_on="alpha-2", + right_on="state", +)["change"] +``` + +```{code-cell} ipython3 +statesmerge.head() +``` + +```{code-cell} ipython3 +statesmerge["medianincome"].quantile(0.25) +``` + +```{code-cell} ipython3 +import branca + + +colormap = branca.colormap.LinearColormap( + vmin=statesmerge["change"].quantile(0.0), + vmax=statesmerge["change"].quantile(1), + colors=["red", "orange", "lightblue", "green", "darkgreen"], + caption="State Level Median County Household Income (%)", +) +``` + +```{code-cell} ipython3 +m = folium.Map(location=[35.3, -97.6], zoom_start=4) + +popup = folium.GeoJsonPopup( + fields=["name", "change"], + aliases=["State", "% Change"], + localize=True, + labels=True, + style="background-color: yellow;", +) + +tooltip = folium.GeoJsonTooltip( + fields=["name", "medianincome", "change"], + aliases=["State:", "2015 Median Income(USD):", "Median % Change:"], + localize=True, + sticky=False, + labels=True, + style=""" + background-color: #F0EFEF; + border: 2px solid black; + border-radius: 3px; + box-shadow: 3px; + """, + max_width=800, +) + + +g = folium.GeoJson( + statesmerge, + style_function=lambda x: { + "fillColor": colormap(x["properties"]["change"]) + if x["properties"]["change"] is not None + else "transparent", + "color": "black", + "fillOpacity": 0.4, + }, + tooltip=tooltip, + popup=popup, +).add_to(m) + +colormap.add_to(m) + +m +``` diff --git a/docs/user_guide/geojson/geopandas_and_geo_interface.md b/docs/user_guide/geojson/geopandas_and_geo_interface.md new file mode 100644 index 0000000000..d4fbc05acd --- /dev/null +++ b/docs/user_guide/geojson/geopandas_and_geo_interface.md @@ -0,0 +1,84 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Using GeoPandas.GeoDataFrame in folium + +GeoPandas is a project to add support for geographic data to [pandas](https://pandas.pydata.org) objects. +(See https://github.com/geopandas/geopandas) + +It provides (among other cool things) a `GeoDataFrame` object that represents a Feature collection. +When you have one, you may be willing to use it on a folium map. Here's the simplest way to do so. + +In this example, we'll use the same file as GeoPandas demo ; it's containing the boroughs of New York City. + +```{code-cell} ipython3 +import geopandas + +boros = geopandas.read_file( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/new_york_boroughs.zip" +) + +boros +``` + +To create a map with these features, simply put them in a `GeoJson`: + +```{code-cell} ipython3 +m = folium.Map([40.7, -74], zoom_start=10, tiles="cartodbpositron") + +folium.GeoJson(boros).add_to(m) + +m +``` + +Quite easy. + +## Adding style + +Well, you can also take advantage of your `GeoDataFrame` structure to set the style of the data. For this, just create a column `style` containing each feature's style in a dictionary. + +```{code-cell} ipython3 +boros["style"] = [ + {"fillColor": "#ff0000", "weight": 2, "color": "black"}, + {"fillColor": "#00ff00", "weight": 2, "color": "black"}, + {"fillColor": "#0000ff", "weight": 2, "color": "black"}, + {"fillColor": "#ffff00", "weight": 2, "color": "black"}, + {"fillColor": "#00ffff", "weight": 2, "color": "black"}, +] + +boros +``` + +```{code-cell} ipython3 +m = folium.Map([40.7, -74], zoom_start=10, tiles="cartodbpositron") + +folium.GeoJson(boros).add_to(m) + +m +``` + +## Use any object with `__geo_interface__` + +Folium should work with any object that implements the `__geo_interface__` but be aware that sometimes you may need to convert your data to `epsg='4326'` before sending it to `folium`. + +```{code-cell} ipython3 +import fiona +import shapely + +url = "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/route_farol.gpx" +with fiona.open(url, "r", layer="tracks") as records: + tracks = [shapely.geometry.shape(record["geometry"]) for record in records] + +track = tracks[0] + +m = folium.Map(tiles="cartodbpositron") +folium.GeoJson(track).add_to(m) + +m.fit_bounds(m.get_bounds()) + +m +``` diff --git a/docs/user_guide/geojson/smoothing.md b/docs/user_guide/geojson/smoothing.md new file mode 100644 index 0000000000..2da53e613b --- /dev/null +++ b/docs/user_guide/geojson/smoothing.md @@ -0,0 +1,41 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Smoothing + +The level of smoothing of the geometry can be determined by passing `smooth_factor` as an argument when initialising GeoJson, TopoJson and Choropleth objects. There are no upper or lower bounds to the smoothing level; Leaflet's default is 1. + +```{code-cell} ipython3 +import requests + +m = folium.Map(location=[-59.1759, -11.6016], tiles="cartodbpositron", zoom_start=2) + +topo = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/antarctic_ice_shelf_topo.json" +).json() + +folium.TopoJson( + data=topo, + object_path="objects.antarctic_ice_shelf", + name="default_smoothing", + smooth_factor=1, + style_function=lambda x: {"color": "#004c00", "opacity": "0.7"}, +).add_to(m) + + +folium.TopoJson( + data=topo, + object_path="objects.antarctic_ice_shelf", + name="heavier smoothing", + smooth_factor=10, + style_function=lambda x: {"color": "#1d3060", "opacity": "0.7"}, +).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` diff --git a/docs/user_guide/map.md b/docs/user_guide/map.md new file mode 100644 index 0000000000..8064a4f2bc --- /dev/null +++ b/docs/user_guide/map.md @@ -0,0 +1,58 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Map + +## Scale + +Show a scale on the bottom of the map. + +```{code-cell} ipython3 +folium.Map( + location=(-38.625, -12.875), + control_scale=True, +) +``` + +## Zoom control + +The map shows zoom buttons by default, but you can disable them. + +```{code-cell} ipython3 +folium.Map( + location=(-38.625, -12.875), + zoom_control=False, +) +``` + +## Limits + +You can set limits, so the map won't scroll outside those limits. + +```{code-cell} ipython3 +import folium + +min_lon, max_lon = -45, -35 +min_lat, max_lat = -25, -15 + +m = folium.Map( + max_bounds=True, + location=[-20, -40], + zoom_start=6, + min_lat=min_lat, + max_lat=max_lat, + min_lon=min_lon, + max_lon=max_lon, +) + +folium.CircleMarker([max_lat, min_lon], tooltip="Upper Left Corner").add_to(m) +folium.CircleMarker([min_lat, min_lon], tooltip="Lower Left Corner").add_to(m) +folium.CircleMarker([min_lat, max_lon], tooltip="Lower Right Corner").add_to(m) +folium.CircleMarker([max_lat, max_lon], tooltip="Upper Right Corner").add_to(m) + +m +``` diff --git a/docs/user_guide/plugins.rst b/docs/user_guide/plugins.rst new file mode 100644 index 0000000000..00c3bd2468 --- /dev/null +++ b/docs/user_guide/plugins.rst @@ -0,0 +1,35 @@ +Plugins +------- + +.. toctree:: + :maxdepth: 1 + + plugins/boat_marker + plugins/beautify_icon + plugins/draw + plugins/dual_map + plugins/featuregroup_subgroup + plugins/float_image + plugins/fullscreen + plugins/geocoder + plugins/grouped_layer_control + plugins/heatmap + plugins/heatmap_with_time + plugins/locate_control + plugins/marker_cluster + plugins/mini_map + plugins/measure_control + plugins/mouse_position + plugins/pattern + plugins/polyline_offset + plugins/polyline_textpath_and_antpath + plugins/scroll_zoom_toggler + plugins/search + plugins/semi_circle + plugins/side_by_side_layers + plugins/tag_filter_button + plugins/terminator + plugins/timeslider_choropleth + plugins/timestamped_geojson + plugins/vector_tiles + plugins/WmsTimeDimension diff --git a/docs/user_guide/plugins/WmsTimeDimension.md b/docs/user_guide/plugins/WmsTimeDimension.md new file mode 100644 index 0000000000..055a3584fc --- /dev/null +++ b/docs/user_guide/plugins/WmsTimeDimension.md @@ -0,0 +1,67 @@ +# TimestampedWmsTileLayers + +Add a time dimension to a WMS tile layer. + +Note that we don't render the output in this example, because the web service +used here is not always available. + +### Exploring the WMS with OWSLib + +``` +from owslib.wms import WebMapService + + +url = "https://pae-paha.pacioos.hawaii.edu/thredds/wms/dhw_5km?service=WMS" + +web_map_services = WebMapService(url) + +print("\n".join(web_map_services.contents.keys())) +``` + +### Layer metadata + +``` +layer = "CRW_SST" +wms = web_map_services.contents[layer] + +name = wms.title + +lon = (wms.boundingBox[0] + wms.boundingBox[2]) / 2.0 +lat = (wms.boundingBox[1] + wms.boundingBox[3]) / 2.0 +center = lat, lon + +time_interval = "{0}/{1}".format( + wms.timepositions[0].strip(), wms.timepositions[-1].strip() +) +style = "boxfill/sst_36" + +if style not in wms.styles: + style = None +``` + +### Map with WmsTileLayer and TimestampedWmsTileLayers + +``` +m = folium.Map(location=[-40, -50], zoom_start=5) + +wms_tile_layer = folium.WmsTileLayer( + url=url, + name=name, + styles=style, + fmt="image/png", + transparent=True, + layers=layer, + overlay=True, + COLORSCALERANGE="1.2,28", +).add_to(m) + +folium.plugins.TimestampedWmsTileLayers( + wms_tile_layer, + period="PT1H", + time_interval=time_interval, +).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/beautify_icon.md b/docs/user_guide/plugins/beautify_icon.md new file mode 100644 index 0000000000..70bf0b375b --- /dev/null +++ b/docs/user_guide/plugins/beautify_icon.md @@ -0,0 +1,30 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## BeautifyIcon + +```{code-cell} ipython3 +m = folium.Map([45.5, -122], zoom_start=3) + +icon_plane = folium.plugins.BeautifyIcon( + icon="plane", border_color="#b3334f", text_color="#b3334f", icon_shape="triangle" +) + +icon_number = folium.plugins.BeautifyIcon( + border_color="#00ABDC", + text_color="#00ABDC", + number=10, + inner_icon_style="margin-top:0;", +) + +folium.Marker(location=[46, -122], popup="Portland, OR", icon=icon_plane).add_to(m) + +folium.Marker(location=[50, -122], popup="Portland, OR", icon=icon_number).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/boat_marker.md b/docs/user_guide/plugins/boat_marker.md new file mode 100644 index 0000000000..4acec800f8 --- /dev/null +++ b/docs/user_guide/plugins/boat_marker.md @@ -0,0 +1,23 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## BoatMarker + +```{code-cell} ipython3 +m = folium.Map([30, 0], zoom_start=3) + +folium.plugins.BoatMarker( + location=(34, -43), heading=45, wind_heading=150, wind_speed=45, color="#8f8" +).add_to(m) + +folium.plugins.BoatMarker( + location=(46, -30), heading=-20, wind_heading=46, wind_speed=25, color="#88f" +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/draw.md b/docs/user_guide/plugins/draw.md new file mode 100644 index 0000000000..b93c8acc2a --- /dev/null +++ b/docs/user_guide/plugins/draw.md @@ -0,0 +1,12 @@ +# Draw + +```{code-cell} ipython3 +import folium +from folium.plugins import Draw + +m = folium.Map() + +Draw(export=True).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/dual_map.md b/docs/user_guide/plugins/dual_map.md new file mode 100644 index 0000000000..620d80211c --- /dev/null +++ b/docs/user_guide/plugins/dual_map.md @@ -0,0 +1,61 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# DualMap plugin + +This plugin is using the Leaflet plugin Sync by Jieter: +https://github.com/jieter/Leaflet.Sync + +The goal is to have two maps side by side. When you pan or zoom on one map, the other will move as well. + +The `DualMap` class accepts the same arguments as the normal `Map` class. Except for these: 'width', 'height', 'left', 'top', 'position'. + +In the following example we create a `DualMap`, add layer controls and then show the map. Try panning and zooming to check that both maps are synchronized. + +```{code-cell} ipython3 +folium.plugins.DualMap(location=(52.1, 5.1), zoom_start=8) +``` + +You can access the two submaps with attributes `m1` and `m2`. You can add objects to each map specifically. + +Here we add different tile layers to each map. This way you can see two different tile sets at the same time. + +```{code-cell} ipython3 +m = folium.plugins.DualMap(location=(52.1, 5.1), tiles=None, zoom_start=8) + +folium.TileLayer("openstreetmap").add_to(m.m1) +folium.TileLayer("cartodbpositron").add_to(m.m2) + +folium.LayerControl(collapsed=False).add_to(m) +m +``` + +Now we're going to add feature groups and markers to both maps and to each map individually. We'll color the shared icon red. + +```{code-cell} ipython3 +m = folium.plugins.DualMap(location=(52.1, 5.1), tiles="cartodbpositron", zoom_start=8) + +fg_both = folium.FeatureGroup(name="markers_both").add_to(m) +fg_1 = folium.FeatureGroup(name="markers_1").add_to(m.m1) +fg_2 = folium.FeatureGroup(name="markers_2").add_to(m.m2) + +icon_red = folium.Icon(color="red") +folium.Marker((52.0, 5.0), tooltip="both", icon=icon_red).add_to(fg_both) +folium.Marker((52.4, 5.0), tooltip="1").add_to(fg_1) +folium.Marker((52.0, 5.4), tooltip="2").add_to(fg_2) + +folium.LayerControl(collapsed=False).add_to(m) +m +``` + +Finally, you can use the `layout` argument to change the layout to vertical: + +```{code-cell} ipython3 +m = folium.plugins.DualMap(layout="vertical") +m +``` diff --git a/docs/user_guide/plugins/featuregroup_subgroup.md b/docs/user_guide/plugins/featuregroup_subgroup.md new file mode 100644 index 0000000000..9f67fc60e7 --- /dev/null +++ b/docs/user_guide/plugins/featuregroup_subgroup.md @@ -0,0 +1,63 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## FeatureGroupSubGroup + +### Sub categories + +Disable all markers in the category, or just one of the subgroup. + +```{code-cell} ipython3 +m = folium.Map(location=[0, 0], zoom_start=6) + +fg = folium.FeatureGroup(name="groups") +m.add_child(fg) + +g1 = folium.plugins.FeatureGroupSubGroup(fg, "group1") +m.add_child(g1) + +g2 = folium.plugins.FeatureGroupSubGroup(fg, "group2") +m.add_child(g2) + +folium.Marker([-1, -1]).add_to(g1) +folium.Marker([1, 1]).add_to(g1) + +folium.Marker([-1, 1]).add_to(g2) +folium.Marker([1, -1]).add_to(g2) + +folium.LayerControl(collapsed=False).add_to(m) + +m +``` + +### Marker clusters across groups + +Create two subgroups, but cluster markers together. + +```{code-cell} ipython3 +m = folium.Map(location=[0, 0], zoom_start=6) + +mcg = folium.plugins.MarkerCluster(control=False) +m.add_child(mcg) + +g1 = folium.plugins.FeatureGroupSubGroup(mcg, "group1") +m.add_child(g1) + +g2 = folium.plugins.FeatureGroupSubGroup(mcg, "group2") +m.add_child(g2) + +folium.Marker([-1, -1]).add_to(g1) +folium.Marker([1, 1]).add_to(g1) + +folium.Marker([-1, 1]).add_to(g2) +folium.Marker([1, -1]).add_to(g2) + +folium.LayerControl(collapsed=False).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/float_image.md b/docs/user_guide/plugins/float_image.md new file mode 100644 index 0000000000..ede5c3231c --- /dev/null +++ b/docs/user_guide/plugins/float_image.md @@ -0,0 +1,18 @@ +# FloatImage + +```{code-cell} ipython3 +import folium +from folium.plugins import FloatImage + + +url = ( + "https://raw.githubusercontent.com/ocefpaf/secoora_assets_map/" + "a250729bbcf2ddd12f46912d36c33f7539131bec/secoora_icons/rose.png" +) + +m = folium.Map([-13, -38.15], zoom_start=10) + +FloatImage(url, bottom=40, left=65).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/fullscreen.md b/docs/user_guide/plugins/fullscreen.md new file mode 100644 index 0000000000..3be7d7de48 --- /dev/null +++ b/docs/user_guide/plugins/fullscreen.md @@ -0,0 +1,24 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## Fullscreen + +Add a button to toggle a fullscreen view of the map. + +```{code-cell} ipython3 +m = folium.Map(location=[41.9, -97.3], zoom_start=4) + +folium.plugins.Fullscreen( + position="topright", + title="Expand me", + title_cancel="Exit me", + force_separate_button=True, +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/geocoder.md b/docs/user_guide/plugins/geocoder.md new file mode 100644 index 0000000000..f509ca7ab2 --- /dev/null +++ b/docs/user_guide/plugins/geocoder.md @@ -0,0 +1,21 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## Geocoder + +Add a search box to the map to search for geographic features like cities, countries, etc. You can search with names or addresses. + +Uses the Nomatim service from OpenStreetMap. Please respect their usage policy: https://operations.osmfoundation.org/policies/nominatim/ + +```{code-cell} ipython3 +m = folium.Map() + +folium.plugins.Geocoder().add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/grouped_layer_control.md b/docs/user_guide/plugins/grouped_layer_control.md new file mode 100644 index 0000000000..b100e7e4a1 --- /dev/null +++ b/docs/user_guide/plugins/grouped_layer_control.md @@ -0,0 +1,61 @@ +# GroupedLayerControl + +We can create a GroupedLayerControl and define what layers we want to group together. Those layers won't show up in the regular layer control. + +`GroupedLayerControl` takes the same arguments as `LayerControl`. + +By default, groups are exclusive, meaning only one layer in a group can be active at a time. + +```{code-cell} ipython3 +import folium +from folium.plugins import GroupedLayerControl + +m = folium.Map([40., 70.], zoom_start=6) + +fg1 = folium.FeatureGroup(name='g1') +fg2 = folium.FeatureGroup(name='g2') +fg3 = folium.FeatureGroup(name='g3') +folium.Marker([40, 74]).add_to(fg1) +folium.Marker([38, 72]).add_to(fg2) +folium.Marker([40, 72]).add_to(fg3) +m.add_child(fg1) +m.add_child(fg2) +m.add_child(fg3) + +folium.LayerControl(collapsed=False).add_to(m) + +GroupedLayerControl( + groups={'groups1': [fg1, fg2]}, + collapsed=False, +).add_to(m) + +m +``` + +It's also possible to have check boxes instead of radio buttons, so multiple layers within a group can be active. + +In this example the layers are not shown by default, but can all be activated. + +```{code-cell} ipython3 +m = folium.Map([40., 70.], zoom_start=6) + +fg1 = folium.FeatureGroup(name='g1', show=False) +fg2 = folium.FeatureGroup(name='g2', show=False) +fg3 = folium.FeatureGroup(name='g3') +folium.Marker([40, 74]).add_to(fg1) +folium.Marker([38, 72]).add_to(fg2) +folium.Marker([40, 72]).add_to(fg3) +m.add_child(fg1) +m.add_child(fg2) +m.add_child(fg3) + +folium.LayerControl(collapsed=False).add_to(m) + +GroupedLayerControl( + groups={'groups1': [fg1, fg2]}, + exclusive_groups=False, + collapsed=False, +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/heatmap.md b/docs/user_guide/plugins/heatmap.md new file mode 100644 index 0000000000..6bd3fc7ab2 --- /dev/null +++ b/docs/user_guide/plugins/heatmap.md @@ -0,0 +1,20 @@ +## Heatmap + +```{code-cell} ipython3 +import numpy as np + +data = ( + np.random.normal(size=(100, 3)) * np.array([[1, 1, 1]]) + np.array([[48, 5, 1]]) +).tolist() +``` + +```{code-cell} ipython3 +import folium +from folium.plugins import HeatMap + +m = folium.Map([48.0, 5.0], tiles="stamentoner", zoom_start=6) + +HeatMap(data).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/heatmap_with_time.md b/docs/user_guide/plugins/heatmap_with_time.md new file mode 100644 index 0000000000..3f3cb2f552 --- /dev/null +++ b/docs/user_guide/plugins/heatmap_with_time.md @@ -0,0 +1,76 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# HeatMapWithTime Plugin + +In this example we show the basic usage of the HeatMapWithTime plugin. + +### Data + +We generate a random set of points with lat/lon coordinates to draw on the map, and then move these points slowly in a random direction to simulate a time dimension. The points are arranged into a list of sets of data to draw. + +```{code-cell} ipython3 +import numpy as np + +np.random.seed(3141592) +initial_data = np.random.normal(size=(100, 2)) * np.array([[1, 1]]) + np.array( + [[48, 5]] +) + +move_data = np.random.normal(size=(100, 2)) * 0.01 + +data = [(initial_data + move_data * i).tolist() for i in range(100)] +``` + +### Weights + +In order to control intensity shown on the map, each data entry needs to have a `weight`. Which should be between 0 and 1. +Below we generate weights randomly such that intensity increases over time. + +```{code-cell} ipython3 +time_ = 0 +N = len(data) +itensify_factor = 30 +for time_entry in data: + time_ = time_+1 + for row in time_entry: + weight = min(np.random.uniform()*(time_/(N))*itensify_factor, 1) + row.append(weight) +``` + +```{code-cell} ipython3 +m = folium.Map([48.0, 5.0], tiles="stamentoner", zoom_start=6) + +hm = folium.plugins.HeatMapWithTime(data) + +hm.add_to(m) + +m +``` + +### Options + +Now we show that the time index can be specified, allowing a more meaningful representation of what the time steps mean. We also enable the 'auto_play' option and change the maximum opacity. See the docmentation for a full list of options that can be used. + +```{code-cell} ipython3 +from datetime import datetime, timedelta + +time_index = [ + (datetime.now() + k * timedelta(1)).strftime("%Y-%m-%d") for k in range(len(data)) +] +``` + +```{code-cell} ipython3 +m = folium.Map([48.0, 5.0], tiles="stamentoner", zoom_start=6) + +hm = folium.plugins.HeatMapWithTime(data, index=time_index, auto_play=True, max_opacity=0.3) + +hm.add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/locate_control.md b/docs/user_guide/plugins/locate_control.md new file mode 100644 index 0000000000..c28a26ef08 --- /dev/null +++ b/docs/user_guide/plugins/locate_control.md @@ -0,0 +1,26 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## LocateControl + +Adds a control button that when clicked, the user device geolocation is displayed. +For list of all possible keyword options see 'Possible options' on https://github.com/domoritz/leaflet-locatecontrol. + +To work properly in production, the connection needs to be encrypted (HTTPS), +otherwise the browser will not allow users to share their location. + +```{code-cell} ipython3 +m = folium.Map([41.97, 2.81]) + +folium.plugins.LocateControl().add_to(m) + +# If you want get the user device position after load the map, set auto_start=True +folium.plugins.LocateControl(auto_start=False).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/marker_cluster.md b/docs/user_guide/plugins/marker_cluster.md new file mode 100644 index 0000000000..41d29030e6 --- /dev/null +++ b/docs/user_guide/plugins/marker_cluster.md @@ -0,0 +1,148 @@ +# MarkerCluster + +```{code-cell} ipython3 +import folium +from folium.plugins import MarkerCluster + +m = folium.Map(location=[44, -73], zoom_start=5) + +marker_cluster = MarkerCluster().add_to(m) + +folium.Marker( + location=[40.67, -73.94], + popup="Add popup text here.", + icon=folium.Icon(color="green", icon="ok-sign"), +).add_to(marker_cluster) + +folium.Marker( + location=[44.67, -73.94], + popup="Add popup text here.", + icon=folium.Icon(color="red", icon="remove-sign"), +).add_to(marker_cluster) + +folium.Marker( + location=[44.67, -71.94], + popup="Add popup text here.", + icon=None, +).add_to(marker_cluster) + +m +``` + +```{code-cell} ipython3 +import numpy as np + +size = 100 +lons = np.random.randint(-180, 180, size=size) +lats = np.random.randint(-90, 90, size=size) + +locations = list(zip(lats, lons)) +popups = ["lon:{}
lat:{}".format(lon, lat) for (lat, lon) in locations] +``` + +## Adding all icons in a single call + +```{code-cell} ipython3 +icon_create_function = """\ +function(cluster) { + return L.divIcon({ + html: '' + cluster.getChildCount() + '', + className: 'marker-cluster marker-cluster-large', + iconSize: new L.Point(20, 20) + }); +}""" +``` + +```{code-cell} ipython3 +from folium.plugins import MarkerCluster + +m = folium.Map( + location=[np.mean(lats), np.mean(lons)], tiles="Cartodb Positron", zoom_start=1 +) + +marker_cluster = MarkerCluster( + locations=locations, + popups=popups, + name="1000 clustered icons", + overlay=True, + control=True, + icon_create_function=icon_create_function, +) + +marker_cluster.add_to(m) + +folium.LayerControl().add_to(m) + +m +``` + +## Explicit loop allows for customization in the loop. + +```{code-cell} ipython3 +m = folium.Map( + location=[np.mean(lats), np.mean(lons)], + tiles='Cartodb Positron', + zoom_start=1 +) + +marker_cluster = MarkerCluster( + name='1000 clustered icons', + overlay=True, + control=False, + icon_create_function=None +) + +for k in range(size): + location = lats[k], lons[k] + marker = folium.Marker(location=location) + popup = 'lon:{}
lat:{}'.format(location[1], location[0]) + folium.Popup(popup).add_to(marker) + marker_cluster.add_child(marker) + +marker_cluster.add_to(m) + +folium.LayerControl().add_to(m); + +m +``` + +## FastMarkerCluster + +`FastMarkerCluster` is not as flexible as MarkerCluster but, like the name suggests, it is faster. + +```{code-cell} ipython3 +from folium.plugins import FastMarkerCluster + +m = folium.Map( + location=[np.mean(lats), np.mean(lons)], + tiles='Cartodb Positron', + zoom_start=1 +) + +FastMarkerCluster(data=list(zip(lats, lons))).add_to(m) + +folium.LayerControl().add_to(m); + +m +``` + +```{code-cell} ipython3 +callback = """\ +function (row) { + var icon, marker; + icon = L.AwesomeMarkers.icon({ + icon: "map-marker", markerColor: "red"}); + marker = L.marker(new L.LatLng(row[0], row[1])); + marker.setIcon(icon); + return marker; +}; +""" + +m = folium.Map( + location=[np.mean(lats), np.mean(lons)], tiles="Cartodb Positron", zoom_start=1 +) + +FastMarkerCluster(data=list(zip(lats, lons)), callback=callback).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/measure_control.md b/docs/user_guide/plugins/measure_control.md new file mode 100644 index 0000000000..b15df96558 --- /dev/null +++ b/docs/user_guide/plugins/measure_control.md @@ -0,0 +1,14 @@ +# MeasureControl + +This plugin allows you to measure distances on the map. + +```{code-cell} ipython3 +import folium +from folium.plugins import MeasureControl + +m = folium.Map([-27.5717, -48.6256], zoom_start=10) + +m.add_child(MeasureControl()) + +m +``` diff --git a/docs/user_guide/plugins/mini_map.md b/docs/user_guide/plugins/mini_map.md new file mode 100644 index 0000000000..0685919006 --- /dev/null +++ b/docs/user_guide/plugins/mini_map.md @@ -0,0 +1,52 @@ +## MiniMap + +```{code-cell} ipython3 +import folium +from folium.plugins import MiniMap + +m = folium.Map(location=(30, 20), zoom_start=4) + +MiniMap().add_to(m) + +m +``` + +### Make the minimap collapsible + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=4) +MiniMap(toggle_display=True).add_to(m) +m +``` + +### Change the minimap tile layer + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=4) +MiniMap(tile_layer="Stamen Toner").add_to(m) +m +``` + +### Change the minimap position + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=4) +MiniMap(position="topleft").add_to(m) +m +``` + +### Change the minimap size + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=4) +MiniMap(width=400, height=100).add_to(m) +m +``` + +### Change the zoom offset + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=8) +MiniMap(zoom_level_offset=-8).add_to(m) +m +``` diff --git a/docs/user_guide/plugins/mouse_position.md b/docs/user_guide/plugins/mouse_position.md new file mode 100644 index 0000000000..823054d173 --- /dev/null +++ b/docs/user_guide/plugins/mouse_position.md @@ -0,0 +1,37 @@ +# MousePosition + +This plugin adds a small field to your map that shows the coordinates of your mouse position. +By default it is in the bottom right corner. + +```{code-cell} ipython3 +import folium +from folium.plugins import MousePosition + + +m = folium.Map() + +MousePosition().add_to(m) + +m +``` + +## Options + +```{code-cell} ipython3 +m = folium.Map() + +formatter = "function(num) {return L.Util.formatNum(num, 3) + ' ° ';};" + +MousePosition( + position="topright", + separator=" | ", + empty_string="NaN", + lng_first=True, + num_digits=20, + prefix="Coordinates:", + lat_formatter=formatter, + lng_formatter=formatter, +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/pattern.md b/docs/user_guide/plugins/pattern.md new file mode 100644 index 0000000000..74dbf130b4 --- /dev/null +++ b/docs/user_guide/plugins/pattern.md @@ -0,0 +1,54 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# Pattern plugins + +We have two pattern plugin classes: `StripePattern` and `CirclePattern`. + +```{code-cell} ipython3 +import requests + +m = folium.Map([40.0, -105.0], zoom_start=6) + +stripes = folium.plugins.pattern.StripePattern(angle=-45).add_to(m) + +circles = folium.plugins.pattern.CirclePattern( + width=20, height=20, radius=5, fill_opacity=0.5, opacity=1 +).add_to(m) + + +def style_function(feature): + default_style = { + "opacity": 1.0, + "fillColor": "#ffff00", + "color": "black", + "weight": 2, + } + + if feature["properties"]["name"] == "Colorado": + default_style["fillPattern"] = stripes + default_style["fillOpacity"] = 1.0 + + if feature["properties"]["name"] == "Utah": + default_style["fillPattern"] = circles + default_style["fillOpacity"] = 1.0 + + return default_style + +us_states = requests.get( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/us_states.json" +).json() + +folium.GeoJson( + us_states, + smooth_factor=0.5, + style_function=style_function, +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/polyline_offset.md b/docs/user_guide/plugins/polyline_offset.md new file mode 100644 index 0000000000..e12fade8d8 --- /dev/null +++ b/docs/user_guide/plugins/polyline_offset.md @@ -0,0 +1,204 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# PolylineOffset + +## Basic Demo + +- The dashed line is the "model", with no offset applied. +- The Red line is with a -5px offset, +- The Green line is with a 10px offset. +The three are distinct Polyline objects but uses the same coordinate array + +```{code-cell} ipython3 +m = folium.Map(location=[58.0, -11.0], zoom_start=4, tiles="cartodbpositron") + +coords = [ + [58.44773, -28.65234], + [53, -23.33496], + [53, -14.32617], + [58.1707, -10.37109], + [59, -13], + [57, -15], + [57, -18], + [60, -18], + [63, -5], + [59, -7], + [58, -3], + [56, -3], + [60, -4], +] + +folium.plugins.PolyLineOffset( + coords, weight=2, dash_array="5,10", color="black", opacity=1 +).add_to(m) + +folium.plugins.PolyLineOffset(coords, color="#f00", opacity=1, offset=-5).add_to(m) + +folium.plugins.PolyLineOffset(coords, color="#080", opacity=1, offset=10).add_to(m) + +m +``` + +## Bus Lines + +A more complex demo. +Offsets are computed automatically depending on the number of bus lines using the same segment. +Other non-offset polylines are used to achieve the white and black outline effect. + +```{code-cell} ipython3 +m = folium.Map(location=[48.868, 2.365], zoom_start=15) + +geojson = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"lines": [0, 1]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.357919216156006, 48.87621773324153], + [2.357339859008789, 48.874834693731664], + [2.362983226776123, 48.86855408432749], + [2.362382411956787, 48.86796126699168], + [2.3633265495300293, 48.86735432768131], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [2, 3]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.351503372192383, 48.86443950493823], + [2.361609935760498, 48.866775611250205], + [2.3633265495300293, 48.86735432768131], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [1, 2]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.369627058506012, 48.86619159489603], + [2.3724031448364253, 48.8626397112042], + [2.3728322982788086, 48.8616233285001], + [2.372767925262451, 48.86080456075567], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [0]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.3647427558898926, 48.86653565369396], + [2.3647642135620117, 48.86630981023694], + [2.3666739463806152, 48.86314789481612], + [2.3673176765441895, 48.86066339254944], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [0, 1, 2, 3]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.3633265495300293, 48.86735432768131], + [2.3647427558898926, 48.86653565369396], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [1, 2, 3]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.3647427558898926, 48.86653565369396], + [2.3650002479553223, 48.86660622956524], + [2.365509867668152, 48.866987337550164], + [2.369627058506012, 48.86619159489603], + ], + }, + }, + { + "type": "Feature", + "properties": {"lines": [3]}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.369627058506012, 48.86619159489603], + [2.372349500656128, 48.865702850895744], + ], + }, + }, + ], +} + +# manage overlays in groups to ease superposition order +outlines = folium.FeatureGroup("outlines") +line_bg = folium.FeatureGroup("lineBg") +bus_lines = folium.FeatureGroup("busLines") +bus_stops = folium.FeatureGroup("busStops") + +line_weight = 6 +line_colors = ["red", "#08f", "#0c0", "#f80"] +stops = [] +for line_segment in geojson["features"]: + # Get every bus line coordinates + segment_coords = [[x[1], x[0]] for x in line_segment["geometry"]["coordinates"]] + # Get bus stops coordinates + stops.append(segment_coords[0]) + stops.append(segment_coords[-1]) + # Get number of bus lines sharing the same coordinates + lines_on_segment = line_segment["properties"]["lines"] + # Width of segment proportional to the number of bus lines + segment_width = len(lines_on_segment) * (line_weight + 1) + # For the white and black outline effect + folium.PolyLine( + segment_coords, color="#000", weight=segment_width + 5, opacity=1 + ).add_to(outlines) + folium.PolyLine( + segment_coords, color="#fff", weight=segment_width + 3, opacity=1 + ).add_to(line_bg) + # Draw parallel bus lines with different color and offset + for j, line_number in enumerate(lines_on_segment): + folium.plugins.PolyLineOffset( + segment_coords, + color=line_colors[line_number], + weight=line_weight, + opacity=1, + offset=j * (line_weight + 1) - (segment_width / 2) + ((line_weight + 1) / 2), + ).add_to(bus_lines) + +# Draw bus stops +for stop in stops: + folium.CircleMarker( + stop, + color="#000", + fill_color="#ccc", + fill_opacity=1, + radius=10, + weight=4, + opacity=1, + ).add_to(bus_stops) + +outlines.add_to(m) +line_bg.add_to(m) +bus_lines.add_to(m) +bus_stops.add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/polyline_textpath_and_antpath.md b/docs/user_guide/plugins/polyline_textpath_and_antpath.md new file mode 100644 index 0000000000..d45374b8d8 --- /dev/null +++ b/docs/user_guide/plugins/polyline_textpath_and_antpath.md @@ -0,0 +1,108 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# PolylineTextPath and AntPath + + +## PolyLineTextPath + +```{code-cell} ipython3 +m = folium.Map([30, 0], zoom_start=3) + +wind_locations = [ + [59.35560, -31.992190], + [55.178870, -42.89062], + [47.754100, -43.94531], + [38.272690, -37.96875], + [27.059130, -41.13281], + [16.299050, -36.56250], + [8.4071700, -30.23437], + [1.0546300, -22.50000], + [-8.754790, -18.28125], + [-21.61658, -20.03906], + [-31.35364, -24.25781], + [-39.90974, -30.93750], + [-43.83453, -41.13281], + [-47.75410, -49.92187], + [-50.95843, -54.14062], + [-55.97380, -56.60156], +] + +wind_line = folium.PolyLine(wind_locations, weight=15, color="#8EE9FF").add_to(m) + +attr = {"fill": "#007DEF", "font-weight": "bold", "font-size": "24"} + +folium.plugins.PolyLineTextPath( + wind_line, ") ", repeat=True, offset=7, attributes=attr +).add_to(m) + +danger_line = folium.PolyLine( + [[-40.311, -31.952], [-12.086, -18.727]], weight=10, color="orange", opacity=0.8 +).add_to(m) + +attr = {"fill": "red"} + +folium.plugins.PolyLineTextPath( + danger_line, "\u25BA", repeat=True, offset=6, attributes=attr +).add_to(m) + +plane_line = folium.PolyLine( + [[-49.38237, -37.26562], [-1.75754, -14.41406], [51.61802, -23.20312]], + weight=1, + color="black", +).add_to(m) + +attr = {"font-weight": "bold", "font-size": "24"} + +folium.plugins.PolyLineTextPath( + plane_line, "\u2708 ", repeat=True, offset=8, attributes=attr +).add_to(m) + + +line_to_new_delhi = folium.PolyLine( + [ + [46.67959447, 3.33984375], + [46.5588603, 29.53125], + [42.29356419, 51.328125], + [35.74651226, 68.5546875], + [28.65203063, 76.81640625], + ] +).add_to(m) + + +line_to_hanoi = folium.PolyLine( + [ + [28.76765911, 77.60742188], + [27.83907609, 88.72558594], + [25.68113734, 97.3828125], + [21.24842224, 105.77636719], + ] +).add_to(m) + + +folium.plugins.PolyLineTextPath(line_to_new_delhi, "To New Delhi", offset=-5).add_to(m) + + +folium.plugins.PolyLineTextPath(line_to_hanoi, "To Hanoi", offset=-5).add_to(m) + +m +``` + +## Antpath + +```{code-cell} ipython3 +m = folium.Map() + +folium.plugins.AntPath( + locations=wind_locations, reverse="True", dash_array=[20, 30] +).add_to(m) + +m.fit_bounds(m.get_bounds()) + +m +``` diff --git a/docs/user_guide/plugins/scroll_zoom_toggler.md b/docs/user_guide/plugins/scroll_zoom_toggler.md new file mode 100644 index 0000000000..0fff09185c --- /dev/null +++ b/docs/user_guide/plugins/scroll_zoom_toggler.md @@ -0,0 +1,19 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## ScrollZoomToggler + +Add a button to enable/disable zoom scrolling. + +```{code-cell} ipython3 +m = folium.Map([45, 3], zoom_start=4) + +folium.plugins.ScrollZoomToggler().add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/search.md b/docs/user_guide/plugins/search.md new file mode 100644 index 0000000000..2d5e2e5d65 --- /dev/null +++ b/docs/user_guide/plugins/search.md @@ -0,0 +1,151 @@ +# Search + +The `Search` plugin allows you to search in your `GeoJson`, `TopoJson`, `FeatureGroup` or `MarkerCluster` objects. + +## Data + +Let's get some JSON data from the web - both a point layer and a polygon GeoJson dataset with some population data. + +```{code-cell} ipython3 +import geopandas + +states = geopandas.read_file( + "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json", + driver="GeoJSON", +) + +cities = geopandas.read_file( + "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_populated_places_simple.geojson", + driver="GeoJSON", +) +``` + +And take a look at what our data looks like: + +```{code-cell} ipython3 +states.describe() +``` + +Look how far the minimum and maximum values for the density are from the top and bottom quartile breakpoints! We have some outliers in our data that are well outside the meat of most of the distribution. Let's look into this to find the culprits within the sample. + +```{code-cell} ipython3 +import pandas as pd + +states_sorted = states.sort_values(by="density", ascending=False) + +pd.concat([ + states_sorted.nlargest(5, 'density')[['name', 'density']], + states_sorted.nsmallest(5, 'density')[['name', 'density']] +]) +``` + +Looks like Washington D.C. and Alaska were the culprits on each end of the range. Washington was more dense than the next most dense state, New Jersey, than the least dense state, Alaska was from Wyoming, however. Washington D.C. has a has a relatively small land area for the amount of people that live there, so it makes sense that it's pretty dense. And Alaska has a lot of land area, but not much of it is habitable for humans. + + +However, we're looking at all of the states in the US to look at things on a more regional level. That high figure at the top of our range for Washington D.C. will really hinder the ability for us to differentiate between the other states, so let's account for that in the min and max values for our color scale, by getting the quantile values close to the end of the range. Anything higher or lower than those values will just fall into the 'highest' and 'lowest' bins for coloring. + +```{code-cell} ipython3 +def rd2(x): + return round(x, 2) + +minimum, maximum = states["density"].quantile([0.05, 0.95]).apply(rd2) + +mean = round(states["density"].mean(), 2) + +print(f"minimum: {minimum}", f"maximum: {maximum}", f"Mean: {mean}", sep="\n\n") +``` + +This looks better. Our min and max values for the colorscale are much closer to the mean value now. Let's run with these values, and make a colorscale. I'm just going to use a sequential light-to-dark color palette from the [ColorBrewer](https://colorbrewer2.org/?type=sequential&scheme=Purples&n=5). + +```{code-cell} ipython3 +import branca + + +colormap = branca.colormap.LinearColormap( + colors=["#f2f0f7", "#cbc9e2", "#9e9ac8", "#756bb1", "#54278f"], + index=states["density"].quantile([0.2, 0.4, 0.6, 0.8]), + vmin=minimum, + vmax=maximum, +) + +colormap.caption = "Population Density in the United States" + +colormap +``` + +Let's narrow down these cities to United states cities, by using GeoPandas' spatial join functionality between two GeoDataFrame objects, using the Point 'within' Polygon functionality. + +```{code-cell} ipython3 +us_cities = geopandas.sjoin(cities, states, how="inner", predicate="within") + +pop_ranked_cities = us_cities.sort_values(by="pop_max", ascending=False)[ + ["nameascii", "pop_max", "geometry"] +].iloc[:20] +``` + +Ok, now we have a new GeoDataFrame with our top 20 populated cities. Let's see the top 5. + +```{code-cell} ipython3 +pop_ranked_cities.head(5) +``` + +## Map with Search plugin + +Alright, let's build a map! + +```{code-cell} ipython3 +import folium +from folium.plugins import Search + + +m = folium.Map(location=[38, -97], zoom_start=4) + + +def style_function(x): + return { + "fillColor": colormap(x["properties"]["density"]), + "color": "black", + "weight": 2, + "fillOpacity": 0.5, + } + + +stategeo = folium.GeoJson( + states, + name="US States", + style_function=style_function, + tooltip=folium.GeoJsonTooltip( + fields=["name", "density"], aliases=["State", "Density"], localize=True + ), +).add_to(m) + +citygeo = folium.GeoJson( + pop_ranked_cities, + name="US Cities", + tooltip=folium.GeoJsonTooltip( + fields=["nameascii", "pop_max"], aliases=["", "Population Max"], localize=True + ), +).add_to(m) + +statesearch = Search( + layer=stategeo, + geom_type="Polygon", + placeholder="Search for a US State", + collapsed=False, + search_label="name", + weight=3, +).add_to(m) + +citysearch = Search( + layer=citygeo, + geom_type="Point", + placeholder="Search for a US City", + collapsed=True, + search_label="nameascii", +).add_to(m) + +folium.LayerControl().add_to(m) +colormap.add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/semi_circle.md b/docs/user_guide/plugins/semi_circle.md new file mode 100644 index 0000000000..f7dcf31520 --- /dev/null +++ b/docs/user_guide/plugins/semi_circle.md @@ -0,0 +1,41 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## SemiCircle + +This can be used to display a semicircle or sector on a map. Whilst called SemiCircle it is not limited to 180 degree angles and can be used to display a sector of any angle. + +The semicircle is defined with a location (the central point, if it was a full circle), a radius and will either have a direction and an arc **or** a start angle and a stop angle. + +```{code-cell} ipython3 +m = folium.Map([45, 3], zoom_start=5) + +folium.plugins.SemiCircle( + (45, 3), + radius=400000, + start_angle=50, + stop_angle=200, + color="green", + fill_color="green", + opacity=0, + popup="start angle - 50 degrees, stop angle - 200 degrees", +).add_to(m) + +folium.plugins.SemiCircle( + (46.5, 9.5), + radius=200000, + direction=360, + arc=90, + color="red", + fill_color="red", + opacity=0, + popup="Direction - 0 degrees, arc 90 degrees", +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/side_by_side_layers.md b/docs/user_guide/plugins/side_by_side_layers.md new file mode 100644 index 0000000000..7a2549adce --- /dev/null +++ b/docs/user_guide/plugins/side_by_side_layers.md @@ -0,0 +1,30 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## SideBySideLayers + +This plugin can be used to compare two layers on the same map using a vertical separator managed by the user. + +The SideBySideLayers class must be instantiated with left and right layers, then added to the map along with layers. + +If you want to add a layer control to your map, you can permanently enable the tile layers used for this plugin with `control=False`. + +```{code-cell} ipython3 +m = folium.Map(location=(30, 20), zoom_start=4) + +layer_right = folium.TileLayer('openstreetmap') +layer_left = folium.TileLayer('cartodbpositron') + +sbs = folium.plugins.SideBySideLayers(layer_left=layer_left, layer_right=layer_right) + +layer_left.add_to(m) +layer_right.add_to(m) +sbs.add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/tag_filter_button.md b/docs/user_guide/plugins/tag_filter_button.md new file mode 100644 index 0000000000..b3466f2554 --- /dev/null +++ b/docs/user_guide/plugins/tag_filter_button.md @@ -0,0 +1,39 @@ +# TagFilterButton + +```{code-cell} ipython3 +import os +import folium +``` + +```{code-cell} ipython3 +import numpy as np +import random + +# Generate base data +data = (np.random.normal(size=(100, 2)) * np.array([[1, 1]]) + + np.array([[48, 5]])) +# Generate the data to segment by (levels of another pandas column in practical usage) +categories = ['category{}'.format(i+1) for i in range(5)] +category_column = [random.choice(categories) for i in range(len(data))] +``` + +Create markers, and add tags to each marker. There can be multiple tags per marker, but in this example we add just one. + +Then, create the `TagFilterButton` object and let it know which tags you want to filter on. + +```{code-cell} ipython3 +from folium.plugins import TagFilterButton + +# Create map and add the data with additional parameter tags as the segmentation +m = folium.Map([48., 5.], tiles='stamentoner', zoom_start=7) +for i, latlng in enumerate(data): + category = category_column[i] + folium.Marker( + tuple(latlng), + tags=[category] + ).add_to(m) + +TagFilterButton(categories).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/terminator.md b/docs/user_guide/plugins/terminator.md new file mode 100644 index 0000000000..003f313fcc --- /dev/null +++ b/docs/user_guide/plugins/terminator.md @@ -0,0 +1,20 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## Terminator + +This plugin overlays the current day and night regions on the map. It updates +continuously. Zoom in in the example below to see the regions move. + +```{code-cell} ipython3 +m = folium.Map([45, 3], zoom_start=1) + +folium.plugins.Terminator().add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/timeslider_choropleth.md b/docs/user_guide/plugins/timeslider_choropleth.md new file mode 100644 index 0000000000..8b9c942f41 --- /dev/null +++ b/docs/user_guide/plugins/timeslider_choropleth.md @@ -0,0 +1,182 @@ +# TimeSliderChoropleth + +In this example we'll make a choropleth with a timeslider. + +The class needs at least two arguments to be instantiated. + +1. A string-serielized geojson containing all the features (i.e., the areas) +2. A dictionary with the following structure: + +``` +styledict = { + '0': { + '2017-1-1': {'color': 'ffffff', 'opacity': 1}, + '2017-1-2': {'color': 'fffff0', 'opacity': 1}, + ... + }, + ..., + 'n': { + '2017-1-1': {'color': 'ffffff', 'opacity': 1}, + '2017-1-2': {'color': 'fffff0', 'opacity': 1}, + ... + } +} +``` + +In the above dictionary, the keys are the feature-ids. + +Using both color and opacity gives us the ability to simultaneously visualize two features on the choropleth. I typically use color to visualize the main feature (like, average height) and opacity to visualize how many measurements were in that group. + +## Loading the features + +We use `geopandas` to load a dataset containing the boundaries of all the countries in the world. + +```{code-cell} ipython3 +import geopandas as gpd + +assert "naturalearth_lowres" in gpd.datasets.available +datapath = gpd.datasets.get_path("naturalearth_lowres") +gdf = gpd.read_file(datapath) +``` + +```{code-cell} ipython3 +%matplotlib inline + +ax = gdf.plot(figsize=(10, 10)) +``` + +The `GeoDataFrame` contains the boundary coordinates, as well as some other data such as estimated population. + +```{code-cell} ipython3 +gdf.head() +``` + +## Creating the style dictionary + +Now we generate time series data for each country. + +Data for different areas might be sampled at different times, and `TimeSliderChoropleth` can deal with that. This means that there is no need to resample the data, as long as the number of datapoints isn't too large for the browser to deal with. + +To simulate that data is sampled at different times we random sample data for `n_periods` rows of data and then pick without replacing `n_sample` of those rows. + +```{code-cell} ipython3 +import pandas as pd + +n_periods, n_sample = 48, 40 + +assert n_sample < n_periods + +datetime_index = pd.date_range("2016-1-1", periods=n_periods, freq="M") +dt_index_epochs = datetime_index.astype("int64") // 10 ** 9 +dt_index = dt_index_epochs.astype("U10") + +dt_index +``` + +```{code-cell} ipython3 +import numpy as np + +styledata = {} + +for country in gdf.index: + df = pd.DataFrame( + { + "color": np.random.normal(size=n_periods), + "opacity": np.random.normal(size=n_periods), + }, + index=dt_index, + ) + df = df.cumsum() + df.sample(n_sample, replace=False).sort_index() + styledata[country] = df +``` + +Note that the geodata and random sampled data is linked through the feature_id, which is the index of the `GeoDataFrame`. + +```{code-cell} ipython3 +gdf.loc[0] +``` + +```{code-cell} ipython3 +styledata.get(0).head() +``` + +We see that we generated two series of data for each country; one for color and one for opacity. Let's plot them to see what they look like. + +```{code-cell} ipython3 +ax = df.plot() +``` + +Looks random alright. We want to map the column named `color` to a hex color. To do this we use a normal colormap. To create the colormap, we calculate the maximum and minimum values over all the timeseries. We also need the max/min of the `opacity` column, so that we can map that column into a range [0,1]. + +```{code-cell} ipython3 +max_color, min_color, max_opacity, min_opacity = 0, 0, 0, 0 + +for country, data in styledata.items(): + max_color = max(max_color, data["color"].max()) + min_color = min(max_color, data["color"].min()) + max_opacity = max(max_color, data["opacity"].max()) + max_opacity = min(max_color, data["opacity"].max()) +``` + +Define and apply colormaps: + +```{code-cell} ipython3 +from branca.colormap import linear + +cmap = linear.PuRd_09.scale(min_color, max_color) + + +def norm(x): + return (x - x.min()) / (x.max() - x.min()) + + +for country, data in styledata.items(): + data["color"] = data["color"].apply(cmap) + data["opacity"] = norm(data["opacity"]) +``` + +```{code-cell} ipython3 +styledata.get(0).head() +``` + +Finally we use `pd.DataFrame.to_dict()` to convert each dataframe into a dictionary, and place each of these in a map from country id to data. + +```{code-cell} ipython3 +styledict = { + str(country): data.to_dict(orient="index") for country, data in styledata.items() +} +``` + +## Creating the map + +```{code-cell} ipython3 +import folium +from folium.plugins import TimeSliderChoropleth + + +m = folium.Map([0, 0], tiles="Stamen Toner", zoom_start=2) + +TimeSliderChoropleth( + gdf.to_json(), + styledict=styledict, +).add_to(m) + +m +``` + +### Initial timestamp + +By default the timeslider starts at the beginning. You can also select another timestamp to begin with using the `init_timestamp` parameter. Note that it expects an index to the list of timestamps. In this example we use `-1` to select the last timestamp. + +```{code-cell} ipython3 +m = folium.Map([0, 0], tiles="Stamen Toner", zoom_start=2) + +TimeSliderChoropleth( + gdf.to_json(), + styledict=styledict, + init_timestamp=-1, +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/timestamped_geojson.md b/docs/user_guide/plugins/timestamped_geojson.md new file mode 100644 index 0000000000..d2fb0b9a9f --- /dev/null +++ b/docs/user_guide/plugins/timestamped_geojson.md @@ -0,0 +1,206 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +## TimestampedGeoJson + +### Example 1 + +```{code-cell} ipython3 +m = folium.Map(location=[35.68159659061569, 139.76451516151428], zoom_start=16) + +# Lon, Lat order. +lines = [ + { + "coordinates": [ + [139.76451516151428, 35.68159659061569], + [139.75964426994324, 35.682590062684206], + ], + "dates": ["2017-06-02T00:00:00", "2017-06-02T00:10:00"], + "color": "red", + }, + { + "coordinates": [ + [139.75964426994324, 35.682590062684206], + [139.7575843334198, 35.679505030038506], + ], + "dates": ["2017-06-02T00:10:00", "2017-06-02T00:20:00"], + "color": "blue", + }, + { + "coordinates": [ + [139.7575843334198, 35.679505030038506], + [139.76337790489197, 35.678040905014065], + ], + "dates": ["2017-06-02T00:20:00", "2017-06-02T00:30:00"], + "color": "green", + "weight": 15, + }, + { + "coordinates": [ + [139.76337790489197, 35.678040905014065], + [139.76451516151428, 35.68159659061569], + ], + "dates": ["2017-06-02T00:30:00", "2017-06-02T00:40:00"], + "color": "#FFFFFF", + }, +] + +features = [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": line["coordinates"], + }, + "properties": { + "times": line["dates"], + "style": { + "color": line["color"], + "weight": line["weight"] if "weight" in line else 5, + }, + }, + } + for line in lines +] + +folium.plugins.TimestampedGeoJson( + { + "type": "FeatureCollection", + "features": features, + }, + period="PT1M", + add_last_point=True, +).add_to(m) + +m +``` + +### Example 2 + +```{code-cell} ipython3 +table = """\ + + + + + + + + + + + + + + + + +
FirstnameLastnameAge
JillSmith50
EveJackson94
+""" + +points = [ + { + "time": "2017-06-02", + "popup": "

address1

", + "coordinates": [-2.548828, 51.467697], + }, + { + "time": "2017-07-02", + "popup": "

address2

", + "coordinates": [-0.087891, 51.536086], + }, + { + "time": "2017-08-02", + "popup": "

address3

", + "coordinates": [-6.240234, 53.383328], + }, + { + "time": "2017-09-02", + "popup": "

address4

", + "coordinates": [-1.40625, 60.261617], + }, + {"time": "2017-10-02", "popup": table, "coordinates": [-1.516113, 53.800651]}, +] + +features = [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": point["coordinates"], + }, + "properties": { + "time": point["time"], + "popup": point["popup"], + "id": "house", + "icon": "marker", + "iconstyle": { + "iconUrl": "https://leafletjs.com/examples/geojson/baseball-marker.png", + "iconSize": [20, 20], + }, + }, + } + for point in points +] + +features.append( + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [-2.548828, 51.467697], + [-0.087891, 51.536086], + [-6.240234, 53.383328], + [-1.40625, 60.261617], + [-1.516113, 53.800651], + ], + }, + "properties": { + "popup": "Current address", + "times": [ + "2017-06-02", + "2017-07-02", + "2017-08-02", + "2017-09-02", + "2017-10-02", + ], + "icon": "circle", + "iconstyle": { + "fillColor": "green", + "fillOpacity": 0.6, + "stroke": "false", + "radius": 13, + }, + "style": {"weight": 0}, + "id": "man", + }, + } +) + +m = folium.Map( + location=[56.096555, -3.64746], + tiles="cartodbpositron", + zoom_start=5, +) + +folium.plugins.TimestampedGeoJson( + {"type": "FeatureCollection", "features": features}, + period="P1M", + add_last_point=True, + auto_play=False, + loop=False, + max_speed=1, + loop_button=True, + date_options="YYYY/MM/DD", + time_slider_drag_update=True, + duration="P2M", +).add_to(m) + +m +``` diff --git a/docs/user_guide/plugins/vector_tiles.md b/docs/user_guide/plugins/vector_tiles.md new file mode 100644 index 0000000000..53401ddebb --- /dev/null +++ b/docs/user_guide/plugins/vector_tiles.md @@ -0,0 +1,158 @@ +# Vector tiles using VectorGridProtobuf + +```{code-cell} ipython3 +from folium.plugins import VectorGridProtobuf +import folium +``` + +```{code-cell} ipython3 +styles = { + "water": { + "fill": True, + "weight": 1, + "fillColor": "#06cccc", + "color": "#06cccc", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "admin": { + "weight": 1, + "fillColor": "pink", + "color": "pink", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "waterway": { + "weight": 1, + "fillColor": "#2375e0", + "color": "#2375e0", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "landcover": { + "fill": True, + "weight": 1, + "fillColor": "#53e033", + "color": "#53e033", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "landuse": { + "fill": True, + "weight": 1, + "fillColor": "#e5b404", + "color": "#e5b404", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "park": { + "fill": True, + "weight": 1, + "fillColor": "#84ea5b", + "color": "#84ea5b", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "boundary": { + "weight": 1, + "fillColor": "#c545d3", + "color": "#c545d3", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "aeroway": { + "weight": 1, + "fillColor": "#51aeb5", + "color": "#51aeb5", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "building": { + "fill": True, + "weight": 1, + "fillColor": "#2b2b2b", + "color": "#2b2b2b", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "water_name": { + "weight": 1, + "fillColor": "#022c5b", + "color": "#022c5b", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "transportation_name": { + "weight": 1, + "fillColor": "#bc6b38", + "color": "#bc6b38", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "place": { + "weight": 1, + "fillColor": "#f20e93", + "color": "#f20e93", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "housenumber": { + "weight": 1, + "fillColor": "#ef4c8b", + "color": "#ef4c8b", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "poi": { + "weight": 1, + "fillColor": "#3bb50a", + "color": "#3bb50a", + "fillOpacity": 0.2, + "opacity": 0.4 + }, + "road": { + "weight": 1, + "fillColor": "#f2b648", + "color": "#f2b648", + "fillOpacity": 0.2, + "opacity": 0.4 + }, +} +``` + +```{code-cell} ipython3 +vectorTileLayerStyles = {} +vectorTileLayerStyles["aerodrome_label"] = [] +vectorTileLayerStyles["aeroway"] = [] +vectorTileLayerStyles["area_name"] = [] +vectorTileLayerStyles["boundary"] = styles["admin"] +vectorTileLayerStyles["building"] = [] +vectorTileLayerStyles["building_ln"] = [] +vectorTileLayerStyles["construct"] = [] +vectorTileLayerStyles["contour_line"] = [] +vectorTileLayerStyles["landcover"] = styles["landcover"] +vectorTileLayerStyles["landuse"] = styles["landuse"] +vectorTileLayerStyles["mountain_peak"] = [] +vectorTileLayerStyles["park"] = styles["park"] +vectorTileLayerStyles["place"] = [] +vectorTileLayerStyles["poi"] = [] +vectorTileLayerStyles["spot_elevation"] = [] +vectorTileLayerStyles["transportation"] = styles["road"] +vectorTileLayerStyles["transportation_name"] = [] +vectorTileLayerStyles["water"] = styles["water"] +vectorTileLayerStyles["waterway"] = styles["water"] +vectorTileLayerStyles["water_name"] = [] +``` + +```{code-cell} ipython3 +url = "https://vectortiles3.geo.admin.ch/tiles/ch.swisstopo.leichte-basiskarte.vt/v1.0.0/{z}/{x}/{y}.pbf" +m = folium.Map(tiles=None, location=[46.8, 8.2], zoom_start=14) + +options = { + "vectorTileLayerStyles": vectorTileLayerStyles +} + +VectorGridProtobuf(url, "folium_layer_name", options).add_to(m) + +m +``` diff --git a/docs/user_guide/raster_layers.rst b/docs/user_guide/raster_layers.rst new file mode 100644 index 0000000000..8be966ef71 --- /dev/null +++ b/docs/user_guide/raster_layers.rst @@ -0,0 +1,10 @@ +Raster layers +-------------- + +.. toctree:: + :maxdepth: 2 + + raster_layers/tiles + raster_layers/image_overlay + raster_layers/video_overlay + raster_layers/wms_tile_layer diff --git a/docs/user_guide/raster_layers/image_overlay.md b/docs/user_guide/raster_layers/image_overlay.md new file mode 100644 index 0000000000..9e469c6dd1 --- /dev/null +++ b/docs/user_guide/raster_layers/image_overlay.md @@ -0,0 +1,148 @@ +# ImageOverlay + +It may happen that you want to draw an image on you map. Here are example on how to do that. + + +## Using an image from disk + +If you have a static image file on your disk, you can simply draw it on the map. + +```{code-cell} ipython3 +import os +import folium + +m = folium.Map([37, 0], zoom_start=1, tiles="stamentoner") +merc = os.path.join("data", "Mercator_projection_SW.png") + + +if not os.path.isfile(merc): + print(f"Could not find {merc}") +else: + img = folium.raster_layers.ImageOverlay( + name="Mercator projection SW", + image=merc, + bounds=[[-82, -180], [82, 180]], + opacity=0.6, + interactive=True, + cross_origin=False, + zindex=1, + ) + + folium.Popup("I am an image").add_to(img) + + img.add_to(m) + folium.LayerControl().add_to(m) + +m +``` + +A few remarks: + +* Note that your image has to be in Mercator projection format. + + The image we've used is based on https://en.wikipedia.org/wiki/File:Mercator_projection_SW.jpg ; that you can find in wikipedia's article on Mercator Projection (https://en.wikipedia.org/wiki/Mercator_projection). + + +You can also provide simply URL. In this case, the image will not be embedded in folium's output. + +```{code-cell} ipython3 +m = folium.Map([37, 0], zoom_start=1, tiles="stamentoner") + +folium.raster_layers.ImageOverlay( + image="https://upload.wikimedia.org/wikipedia/commons/f/f4/Mercator_projection_SW.jpg", + name="I am a jpeg", + bounds=[[-82, -180], [82, 180]], + opacity=1, + interactive=False, + cross_origin=False, + zindex=1, + alt="Wikipedia File:Mercator projection SW.jpg", +).add_to(m) + +folium.LayerControl().add_to(m) + + +m +``` + +This works exactly the same way if you want to put a JPG instead of a PNG. + + +## Creating an image with numpy + +Now you may wish to create your own image, based on another python computation. +For this, you'll need to have numpy installed on your machine. + +Let's create an image to draw a rectangle in the bounds `[[0, -60], [60, 60]]`. + +```{code-cell} ipython3 +import numpy as np + +image = np.zeros((61, 61)) +image[0, :] = 1.0 +image[60, :] = 1.0 +image[:, 0] = 1.0 +image[:, 60] = 1.0 +``` + +We can draw it on the map in using: + +```{code-cell} ipython3 +m = folium.Map([37, 0], zoom_start=2) + +folium.raster_layers.ImageOverlay( + image=image, + bounds=[[0, -60], [60, 60]], + colormap=lambda x: (1, 0, 0, x), +).add_to(m) + +m +``` + +Note that you need to provide a colormap of the form `lambda x: (R,G,B,A)` where `R,G,B,A` are floats between 0 and 1. + +Now, let's try to add a line at latitude 45°, and add a polyline to verify it's well rendered. We'll need to specify `origin='lower` to inform folium that the first lines of the array are to be plotted at the bottom of the image (see `numpy.imshow`, it's the same principle). + +```{code-cell} ipython3 +import numpy as np + +image = np.zeros((61, 1)) + +image[45, :] = 1.0 + + +m = folium.Map([37, 0], zoom_start=3) + +folium.raster_layers.ImageOverlay( + image=image, + bounds=[[0, -60], [60, 60]], + colormap=lambda x: (1, 0, 0, x), + origin="lower", +).add_to(m) + +folium.PolyLine([[45, -60], [45, 60]]).add_to(m) + +m +``` + +But even with `origin='lower'`, the red line is not at the good latitude. This is due to Mercator projection used in Leaflet (and most other map systems). + +You can read wikipedia's article on Mercator Projection (https://en.wikipedia.org/wiki/Mercator_projection), or simply let folium do the job, in precising that you want the mercator stuff to be handled. + +```{code-cell} ipython3 +m = folium.Map([37, 0], zoom_start=3) + +folium.PolyLine([[45, -60], [45, 60]]).add_to(m) + +folium.raster_layers.ImageOverlay( + image=image, + bounds=[[0, -60], [60, 60]], + origin="lower", + colormap=lambda x: (1, 0, 0, x), + mercator_project=True, +).add_to(m) + +m +``` + +This time, the lines are properly positioned (at the precision of the array). diff --git a/docs/user_guide/raster_layers/tiles.md b/docs/user_guide/raster_layers/tiles.md new file mode 100644 index 0000000000..bb65c89575 --- /dev/null +++ b/docs/user_guide/raster_layers/tiles.md @@ -0,0 +1,55 @@ +## Tiles + +### Built-in tilesets + +```{code-cell} ipython3 +import folium + + +lon, lat = -38.625, -12.875 + +zoom_start = 8 +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="OpenStreetMap", zoom_start=zoom_start) +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="Stamen Terrain", zoom_start=zoom_start) +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="Stamen Toner", zoom_start=zoom_start) +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="Stamen Watercolor", zoom_start=zoom_start) +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="Cartodb Positron", zoom_start=zoom_start) +``` + +```{code-cell} ipython3 +folium.Map(location=[lat, lon], tiles="Cartodb dark_matter", zoom_start=zoom_start) +``` + + +### Custom tiles + +```{code-cell} ipython3 +attr = ( + '© OpenStreetMap ' + 'contributors, © CartoDB' +) +tiles = "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png" + +folium.Map(location=[lat, lon], tiles=tiles, attr=attr, zoom_start=zoom_start) +``` + +### Other tilesets + +For a list of many more tile providers go to https://leaflet-extras.github.io/leaflet-providers/preview/. + +You can also use the xyzservices package: https://github.com/geopandas/xyzservices. diff --git a/docs/user_guide/raster_layers/video_overlay.md b/docs/user_guide/raster_layers/video_overlay.md new file mode 100644 index 0000000000..38019f55d2 --- /dev/null +++ b/docs/user_guide/raster_layers/video_overlay.md @@ -0,0 +1,21 @@ +# VideoOverlay + +```{code-cell} ipython3 +import folium + + +m = folium.Map(location=[22.5, -115], zoom_start=4) + +video = folium.raster_layers.VideoOverlay( + video_url="https://www.mapbox.com/bites/00188/patricia_nasa.webm", + bounds=[[32, -130], [13, -100]], + opacity=0.65, + attr="Video from patricia_nasa", + autoplay=True, + loop=False, +) + +video.add_to(m) + +m +``` diff --git a/docs/user_guide/raster_layers/wms_tile_layer.md b/docs/user_guide/raster_layers/wms_tile_layer.md new file mode 100644 index 0000000000..25b547eb91 --- /dev/null +++ b/docs/user_guide/raster_layers/wms_tile_layer.md @@ -0,0 +1,27 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# WmsTileLayer + +```{code-cell} ipython3 +m = folium.Map(location=[41, -70], zoom_start=5, tiles="cartodb positron") + +folium.WmsTileLayer( + url="https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", + name="test", + fmt="image/png", + layers="nexrad-n0r-900913", + attr=u"Weather data © 2012 IEM Nexrad", + transparent=True, + overlay=True, + control=True, +).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` diff --git a/docs/user_guide/ui_elements.rst b/docs/user_guide/ui_elements.rst new file mode 100644 index 0000000000..55e02e809c --- /dev/null +++ b/docs/user_guide/ui_elements.rst @@ -0,0 +1,10 @@ +UI elements +-------------- + +.. toctree:: + :maxdepth: 1 + + + ui_elements/layer_control + ui_elements/popups + ui_elements/icons diff --git a/docs/user_guide/ui_elements/icons.md b/docs/user_guide/ui_elements/icons.md new file mode 100644 index 0000000000..0d1f1aa57d --- /dev/null +++ b/docs/user_guide/ui_elements/icons.md @@ -0,0 +1,56 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# Icons + +## Rotate icons + +```{code-cell} ipython3 +m = folium.Map(location=[41, -71], zoom_start=4) + +kw = {"prefix": "fa", "color": "green", "icon": "arrow-up"} + +angle = 180 +icon = folium.Icon(angle=angle, **kw) +folium.Marker(location=[41, -72], icon=icon, tooltip=str(angle)).add_to(m) + +angle = 45 +icon = folium.Icon(angle=angle, **kw) +folium.Marker(location=[41, -75], icon=icon, tooltip=str(angle)).add_to(m) + +angle = 90 +icon = folium.Icon(angle=angle, **kw) +folium.Marker([41, -78], icon=icon, tooltip=str(angle)).add_to(m) + +m +``` + +## Custom icon + +```{code-cell} ipython3 +m = folium.Map(location=[45.3288, -121.6625], zoom_start=12, tiles="Stamen Terrain") + +url = "https://leafletjs.com/examples/custom-icons/{}".format +icon_image = url("leaf-red.png") +shadow_image = url("leaf-shadow.png") + +icon = folium.CustomIcon( + icon_image, + icon_size=(38, 95), + icon_anchor=(22, 94), + shadow_image=shadow_image, + shadow_size=(50, 64), + shadow_anchor=(4, 62), + popup_anchor=(-3, -76), +) + +folium.Marker( + location=[45.3288, -121.6625], icon=icon, popup="Mt. Hood Meadows" +).add_to(m) + +m +``` diff --git a/docs/user_guide/ui_elements/layer_control.md b/docs/user_guide/ui_elements/layer_control.md new file mode 100644 index 0000000000..2f495ee865 --- /dev/null +++ b/docs/user_guide/ui_elements/layer_control.md @@ -0,0 +1,60 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## LayerControl + +Add a control to the map to show or hide layers. + +```{code-cell} ipython3 +m = folium.Map(tiles=None) + +folium.TileLayer("OpenStreetMap").add_to(m) +folium.TileLayer("stamentoner", show=False).add_to(m) + +folium.LayerControl().add_to(m) + +m +``` + +### Common layer arguments + +Every layer element in Folium has a couple common arguments: + +- `name`: how the layer will be named in the layer control. +- `overlay`: True if the layer is an overlay, False if the layer is a base layer. + - base layer: only one of them can be active at a time. Mostly used for tile layers. + - overlay: multiple can be active at the same time. Used for anything else than tile layers. +- `control`: Whether the layer can be controlled in the layer control. +- `show`: Whether the layer will be shown when opening the map. + +Next we'll give some examples using a `FeatureGroup`. + +### Remove from control + +```{code-cell} ipython3 +m = folium.Map() + +fg = folium.FeatureGroup(name="Icon collection", control=False).add_to(m) +folium.Marker(location=(0, 0)).add_to(fg) + +folium.LayerControl().add_to(m) + +m +``` + +### Show manually + +```{code-cell} ipython3 +m = folium.Map() + +fg = folium.FeatureGroup(name="Icon collection", show=False).add_to(m) +folium.Marker(location=(0, 0)).add_to(fg) + +folium.LayerControl().add_to(m) + +m +``` diff --git a/docs/user_guide/ui_elements/popups.md b/docs/user_guide/ui_elements/popups.md new file mode 100644 index 0000000000..7c98b641f9 --- /dev/null +++ b/docs/user_guide/ui_elements/popups.md @@ -0,0 +1,231 @@ +# Popups + +## Simple popups + +You can define your popup at the feature creation, but you can also overwrite them afterwards: + +```{code-cell} ipython3 +import folium + + +m = folium.Map([45, 0], zoom_start=4) + +folium.Marker([45, -30], popup="inline implicit popup").add_to(m) + +folium.CircleMarker( + location=[45, -10], + radius=25, + fill=True, + popup=folium.Popup("inline explicit Popup"), +).add_to(m) + +ls = folium.PolyLine( + locations=[[43, 7], [43, 13], [47, 13], [47, 7], [43, 7]], color="red" +) + +ls.add_child(folium.Popup("outline Popup on Polyline")) +ls.add_to(m) + +gj = folium.GeoJson( + data={"type": "Polygon", "coordinates": [[[27, 43], [33, 43], [33, 47], [27, 47]]]} +) + +gj.add_child(folium.Popup("outline Popup on GeoJSON")) +gj.add_to(m) + +m +``` + +```{code-cell} ipython3 +m = folium.Map([45, 0], zoom_start=2) + +folium.Marker( + location=[45, -10], + popup=folium.Popup("Let's try quotes", parse_html=True, max_width=100), +).add_to(m) + +folium.Marker( + location=[45, -30], + popup=folium.Popup(u"Ça c'est chouette", parse_html=True, max_width="100%"), +).add_to(m) + +m +``` + +## HTML in popup + +```{code-cell} ipython3 +import branca + +m = folium.Map([43, -100], zoom_start=4) + +html = """ +

This is a big popup


+ With a few lines of code... +

+ + from numpy import *
+ exp(-2*pi) +
+

+ """ + + +folium.Marker([30, -100], popup=html).add_to(m) + +m +``` + +## Iframe in popup + +You can also put any HTML code inside a Popup, thaks to the `IFrame` object. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +html = """ +

This popup is an Iframe


+ With a few lines of code... +

+ + from numpy import *
+ exp(-2*pi) +
+

+ """ + +iframe = branca.element.IFrame(html=html, width=500, height=300) +popup = folium.Popup(iframe, max_width=500) + +folium.Marker([30, -100], popup=popup).add_to(m) + +m +``` + +```{code-cell} ipython3 +import pandas as pd + +df = pd.DataFrame( + data=[["apple", "oranges"], ["other", "stuff"]], columns=["cats", "dogs"] +) + +m = folium.Map([43, -100], zoom_start=4) + +html = df.to_html( + classes="table table-striped table-hover table-condensed table-responsive" +) + +popup = folium.Popup(html) + +folium.Marker([30, -100], popup=popup).add_to(m) + +m +``` + +Note that you can put another `Figure` into an `IFrame` ; this should let you do strange things... + +```{code-cell} ipython3 +# Let's create a Figure, with a map inside. +f = branca.element.Figure() +folium.Map([-25, 150], zoom_start=3).add_to(f) + +# Let's put the figure into an IFrame. +iframe = branca.element.IFrame(width=500, height=300) +f.add_to(iframe) + +# Let's put the IFrame in a Popup +popup = folium.Popup(iframe, max_width=2650) + +# Let's create another map. +m = folium.Map([43, -100], zoom_start=4) + +# Let's put the Popup on a marker, in the second map. +folium.Marker([30, -100], popup=popup).add_to(m) + +# We get a map in a Popup. Not really useful, but powerful. +m +``` + + +## Vega chart in popup + +[Vega](https://vega.github.io/vega/) is a way to describe charts. You can embed +a Vega chart in a popup using the `Vega` class. + +```{code-cell} ipython3 +import json + +import numpy as np +import vincent + +multi_iter2 = { + "x": np.random.uniform(size=(100,)), + "y": np.random.uniform(size=(100,)), +} +scatter = vincent.Scatter(multi_iter2, iter_idx="x", height=100, width=200) +data = json.loads(scatter.to_json()) + +m = folium.Map([0, 0], zoom_start=1) +marker = folium.Marker([0, 0]).add_to(m) +popup = folium.Popup("Hello").add_to(marker) +folium.Vega(data, width="100%", height="100%").add_to(popup) + +m +``` + +## Vega-Lite chart in popup + +[Vega-lite](https://vega.github.io/vega-lite/) is a higher-level version of Vega. +Folium supports it as well in the `VegaLite` class. + +```{code-cell} ipython3 +from altair import Chart + +import vega_datasets + +# load built-in dataset as a pandas DataFrame +cars = vega_datasets.data.cars() + +scatter = ( + Chart(cars) + .mark_circle() + .encode( + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + ) +) + +vega_lite = folium.VegaLite( + scatter, + width="100%", + height="100%", +) + +m = folium.Map(location=[-27.5717, -48.6256]) + +marker = folium.Marker([-27.57, -48.62]) + +popup = folium.Popup() + +vega_lite.add_to(popup) +popup.add_to(marker) + +marker.add_to(m) + +m +``` + +## Lazy loading + +If whatever you are showing in the popup is slow or heavy to load and you have many popups, you may not want to render the popup contents immediately. There's an argument to prevent loading until the popup is opened. + +```{code-cell} ipython3 +m = folium.Map([43, -100], zoom_start=4) + +html = "{a resource that is heavy to load, such as an image}" + +folium.Marker([30, -100], popup=html, lazy=True).add_to(m) + +m +``` diff --git a/docs/user_guide/vector_layers.rst b/docs/user_guide/vector_layers.rst new file mode 100644 index 0000000000..ddcea51551 --- /dev/null +++ b/docs/user_guide/vector_layers.rst @@ -0,0 +1,11 @@ +Vector layers +-------------- + +.. toctree:: + :maxdepth: 1 + + vector_layers/circle_and_circle_marker + vector_layers/polyline + vector_layers/rectangle + vector_layers/polygon + vector_layers/colorline diff --git a/docs/user_guide/vector_layers/circle_and_circle_marker.md b/docs/user_guide/vector_layers/circle_and_circle_marker.md new file mode 100644 index 0000000000..3cb153c323 --- /dev/null +++ b/docs/user_guide/vector_layers/circle_and_circle_marker.md @@ -0,0 +1,51 @@ +## Circle and CircleMarker + +`CircleMarker` has a radius specified in pixels, while `Circle` is specified in meters. +That means a `CircleMarker` will not change size on your screen when you zoom, +while `Circle` will have a fixed position on the map. + +```{code-cell} ipython3 +import folium + +m = folium.Map(location=[-27.5717, -48.6256], zoom_start=9) + +radius = 50 +folium.CircleMarker( + location=[-27.55, -48.8], + radius=radius, + color="cornflowerblue", + stroke=False, + fill=True, + fill_opacity=0.6, + opacity=1, + popup="{} pixels".format(radius), + tooltip="I am in pixels", +).add_to(m) + +radius = 25 +folium.CircleMarker( + location=[-27.35, -48.8], + radius=radius, + color="black", + weight=3, + fill=False, + fill_opacity=0.6, + opacity=1, +).add_to(m) + +radius = 10000 +folium.Circle( + location=[-27.551667, -48.478889], + radius=radius, + color="black", + weight=1, + fill_opacity=0.6, + opacity=1, + fill_color="green", + fill=False, # gets overridden by fill_color + popup="{} meters".format(radius), + tooltip="I am in meters", +).add_to(m) + +m +``` diff --git a/docs/user_guide/vector_layers/colorline.md b/docs/user_guide/vector_layers/colorline.md new file mode 100644 index 0000000000..bf828d3bbf --- /dev/null +++ b/docs/user_guide/vector_layers/colorline.md @@ -0,0 +1,26 @@ +### ColorLine + +```{code-cell} ipython3 +import numpy as np + +x = np.linspace(0, 2 * np.pi, 300) + +lats = 20 * np.cos(x) +lons = 20 * np.sin(x) +colors = np.sin(5 * x) +``` + +```{code-cell} ipython3 +import folium + +m = folium.Map([0, 0], zoom_start=3) + +color_line = folium.ColorLine( + positions=list(zip(lats, lons)), + colors=colors, + colormap=["y", "orange", "r"], + weight=10, +).add_to(m) + +m +``` diff --git a/docs/user_guide/vector_layers/polygon.md b/docs/user_guide/vector_layers/polygon.md new file mode 100644 index 0000000000..ce83b2dc01 --- /dev/null +++ b/docs/user_guide/vector_layers/polygon.md @@ -0,0 +1,87 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## Polygon + +```{code-cell} ipython3 +m = folium.Map(location=[35.67, 139.78], zoom_start=13) + +locations = [ + [35.6762, 139.7795], + [35.6718, 139.7831], + [35.6767, 139.7868], + [35.6795, 139.7824], + [35.6787, 139.7791], +] + +folium.Polygon( + locations=locations, + color="blue", + weight=6, + fill_color="red", + fill_opacity=0.5, + fill=True, + popup="Tokyo, Japan", + tooltip="Click me!", +).add_to(m) + +m +``` + +```{code-cell} ipython3 +locations = [ + [ + [7.577794326946673, 8.998503901433935], + [7.577851434795945, 8.998572430673164], + [7.577988491475764, 8.998652380403087], + [7.578105560723088, 8.998426807051544], + [7.577891409660878, 8.998289750371725], + [7.577794326946673, 8.998503901433935], + ], + [ + [7.578139824893071, 8.999291979141560], + [7.578359687549607, 8.999414759083890], + [7.578456769364435, 8.999266281014116], + [7.578471046101925, 8.999197181604700], + [7.578247331649095, 8.999094883721964], + [7.578139824893071, 8.99929197914156], + ], + [ + [7.577851730672876, 8.997811268775080], + [7.578012579816743, 8.997460464828633], + [7.577798113991832, 8.997311104523930], + [7.577667902951418, 8.997663440915119], + [7.577851730672876, 8.997811268775080], + ], + [ + [7.578562417221803, 8.999551816663029], + [7.578688052511666, 8.999654609172921], + [7.578813688700849, 8.999443313458185], + [7.578670920426703, 8.999369073523950], + [7.578562417221803, 8.999551816663029], + ], + [ + [7.577865711533433, 8.998252059784761], + [7.577989601239152, 8.998002756022402], + [7.577648754586391, 8.997784460884190], + [7.577545911714481, 8.998069316645683], + [7.577865711533433, 8.998252059784761], + ], +] + +m = folium.Map(location=[7.577798113991832, 8.997311104523930], zoom_start=16) + +folium.Polygon( + locations=locations, + smooth_factor=2, + color="crimson", + no_clip=True, + tooltip="Hi there!", +).add_to(m) + +m +``` diff --git a/docs/user_guide/vector_layers/polyline.md b/docs/user_guide/vector_layers/polyline.md new file mode 100644 index 0000000000..1003e71a8c --- /dev/null +++ b/docs/user_guide/vector_layers/polyline.md @@ -0,0 +1,174 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +# PolyLine + +```{code-cell} ipython3 +# Coordinates are 15 points on the great circle from Boston to San Francisco. +coordinates = [ + [42.3581, -71.0636], + [42.82995815, -74.78991444], + [43.17929819, -78.56603306], + [43.40320216, -82.37774519], + [43.49975489, -86.20965845], + [43.46811941, -90.04569087], + [43.30857071, -93.86961818], + [43.02248456, -97.66563267], + [42.61228259, -101.41886832], + [42.08133868, -105.11585198], + [41.4338549, -108.74485069], + [40.67471747, -112.29609954], + [39.8093434, -115.76190821], + [38.84352776, -119.13665678], + [37.7833, -122.4167], +] + +# Create the map and add the line +m = folium.Map(location=[41.9, -97.3], zoom_start=4) + +folium.PolyLine( + locations=coordinates, + color="#FF0000", + weight=5, + tooltip="From Boston to San Francisco", +).add_to(m) + +m +``` + +## Smoothing + +PolyLine objects in Leaflet are smoothed by default. This removes points from +the line, putting less load on the browser when drawing. The level of smoothing +can be set with the `smooth_factor` argument. + +```{code-cell} ipython3 +m = folium.Map(location=[41.9, -97.3], zoom_start=4) + +folium.PolyLine( + smooth_factor=50, + locations=coordinates, + color="grey", + tooltip="Too much smoothing?", + weight=5, +).add_to(m) + +m +``` + +## Crossing the date line + +Take notice how lines behave when they cross the date line. + +```{code-cell} ipython3 +lon = lat = 0 +zoom_start = 1 + +m = folium.Map(location=[lat, lon], zoom_start=zoom_start) + +kw = {"opacity": 1.0, "weight": 6} +folium.PolyLine( + locations=[(2, 179), (2, -179)], + tooltip="Wrong", + color="red", + line_cap="round", + **kw, +).add_to(m) + +folium.PolyLine( + locations=[(-2, 179), (-2, 181)], + tooltip="Correct", + line_cap="butt", + color="blue", + **kw, +).add_to(m) + +folium.PolyLine( + locations=[(-6, -179), (-6, 179)], + line_cap="square", + color="green", + tooltip="Correct", + **kw, +).add_to(m) + +folium.PolyLine( + locations=[(12, -179), (12, 190)], + color="orange", + tooltip="Artifact?", + **kw, +).add_to(m) + +m +``` + +## Multi-PolyLine + +You can create multiple polylines by passing multiple sets of coordinates +to a single `PolyLine` object. + +```{code-cell} ipython3 +lat = +38.89399 +lon = -77.03659 +zoom_start = 17 + +m = folium.Map(location=[lat, lon], tiles="stamen toner", zoom_start=zoom_start) + +kw = {"color": "red", "fill": True, "radius": 20} + +folium.CircleMarker([38.89415, -77.03738], **kw).add_to(m) +folium.CircleMarker([38.89415, -77.03578], **kw).add_to(m) + + +locations = [ + [ + (38.893596444352134, -77.03814983367920), + (38.893379333722040, -77.03792452812195), + ], + [ + (38.893379333722040, -77.03792452812195), + (38.893162222428310, -77.03761339187622), + ], + [ + (38.893162222428310, -77.03761339187622), + (38.893028615148424, -77.03731298446655), + ], + [ + (38.893028615148424, -77.03731298446655), + (38.892920059048464, -77.03691601753235), + ], + [ + (38.892920059048464, -77.03691601753235), + (38.892903358095296, -77.03637957572937), + ], + [ + (38.892903358095296, -77.03637957572937), + (38.893011914220770, -77.03592896461487), + ], + [ + (38.893011914220770, -77.03592896461487), + (38.893162222428310, -77.03549981117249), + ], + [ + (38.893162222428310, -77.03549981117249), + (38.893404384982480, -77.03514575958252), + ], + [ + (38.893404384982480, -77.03514575958252), + (38.893596444352134, -77.03496336936950), + ], +] + +folium.PolyLine( + locations=locations, + color="orange", + weight=8, + opacity=1, + smooth_factor=0, +).add_to(m) + +m +``` diff --git a/docs/user_guide/vector_layers/rectangle.md b/docs/user_guide/vector_layers/rectangle.md new file mode 100644 index 0000000000..08701fba19 --- /dev/null +++ b/docs/user_guide/vector_layers/rectangle.md @@ -0,0 +1,46 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +``` + +## Rectangle + +```{code-cell} ipython3 +m = folium.Map(location=[35.685, 139.76], zoom_start=15) + +kw = { + "color": "blue", + "line_cap": "round", + "fill": True, + "fill_color": "red", + "weight": 5, + "popup": "Tokyo, Japan", + "tooltip": "Click me!", +} + +folium.Rectangle( + bounds=[[35.681, 139.766], [35.691, 139.776]], + line_join="round", + dash_array="5, 5", + **kw, +).add_to(m) + +dx = 0.012 +folium.Rectangle( + bounds=[[35.681, 139.766 - dx], [35.691, 139.776 - dx]], + line_join="mitter", + dash_array="5, 10", + **kw, +).add_to(m) + +folium.Rectangle( + bounds=[[35.681, 139.766 - 2 * dx], [35.691, 139.7762 - 2 * dx]], + line_join="bevel", + dash_array="15, 10, 5, 10, 15", + **kw, +).add_to(m) + +m +``` diff --git a/folium/features.py b/folium/features.py index 5792290dcb..c363223e3f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -809,7 +809,7 @@ def render(self, **kwargs) -> None: class GeoJsonStyleMapper: """Create dicts that map styling to GeoJson features. - Used in the GeoJson class. Users don't have to call this class directly. + :meta private: """ def __init__( @@ -1053,11 +1053,9 @@ def get_bounds(self) -> List[List[float]]: class GeoJsonDetail(MacroElement): + """Base class for GeoJsonTooltip and GeoJsonPopup. - """ - Base class for GeoJsonTooltip and GeoJsonPopup to inherit methods and - template structure from. Not for direct usage. - + :meta private: """ base_template = """ diff --git a/folium/plugins/__init__.py b/folium/plugins/__init__.py index f4fc1e4db9..f4f168e2b4 100644 --- a/folium/plugins/__init__.py +++ b/folium/plugins/__init__.py @@ -1,10 +1,4 @@ -""" -Folium plugins --------------- - -Wrap some of the most popular leaflet external plugins. - -""" +"""Wrap some of the most popular leaflet external plugins.""" from folium.plugins.antpath import AntPath from folium.plugins.beautify_icon import BeautifyIcon diff --git a/folium/vector_layers.py b/folium/vector_layers.py index ba170a82ab..6e03506101 100644 --- a/folium/vector_layers.py +++ b/folium/vector_layers.py @@ -124,7 +124,7 @@ def path_options( class BaseMultiLocation(MacroElement): """Base class for vector classes with multiple coordinates. - Not for direct consumption + :meta private: """ diff --git a/requirements-dev.txt b/requirements-dev.txt index d329a0c0fa..61d92d3176 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,7 @@ gpxpy ipykernel jenkspy jupyter +jupytext matplotlib mypy nbconvert @@ -24,6 +25,7 @@ pandas pillow pre-commit pycodestyle +pydata-sphinx-theme pytest scipy selenium diff --git a/tests/selenium/test_selenium.py b/tests/selenium/test_selenium.py index 71bed88311..15429d3936 100644 --- a/tests/selenium/test_selenium.py +++ b/tests/selenium/test_selenium.py @@ -15,9 +15,8 @@ def find_notebooks(): """Return a list of filenames of the example notebooks.""" path = os.path.dirname(__file__) - pattern = os.path.join(path, "..", "..", "examples", "*.ipynb") - files = glob.glob(pattern) - files = [f for f in files if not f.endswith(".nbconvert.ipynb")] + pattern = os.path.join(path, "..", "..", "docs", "**", "*.md") + files = glob.glob(pattern, recursive=True) if files: return files else: @@ -41,19 +40,17 @@ def test_notebook(filepath, driver): def get_notebook_html(filepath_notebook): - """Store iframes from a notebook in html files, remove them when done.""" - # run the notebook to make sure the output is up-to-date + """Convert markdown to notebook to html files, remove them when done.""" subprocess.run( [ - "jupyter", - "nbconvert", + "jupytext", "--to", "notebook", "--execute", filepath_notebook, ] ) - filepath_notebook = filepath_notebook.replace(".ipynb", ".nbconvert.ipynb") + filepath_notebook = filepath_notebook.replace(".md", ".ipynb") html_exporter = nbconvert.HTMLExporter() body, _ = html_exporter.from_filename(filepath_notebook)