Git Product home page Git Product logo

altair_tiles's Introduction

This package is in an early development stage. You should expect things to break unannounced until we release a version 1.0.0.

You can use altair_tiles to add tiles from any xyz tile provider such as OpenStreetMap to your Altair chart. It is a counterpart to the amazing contextily package which provides this functionality for matplotlib.

You can find the documentation here. For a general introduction to plotting geographic data with Altair, see Geoshape - Vega-Altair and Specifying Data - Vega-Altair.

Installation

pip install altair_tiles

or

conda install -c conda-forge altair_tiles

altair-tiles requires at least Vega version 5.26.0. If you use an IDE such as a Jupyter Notebook or VS Code, you usually don't have to worry about this.

Development

python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'

Run linters and tests with

hatch run test

Build and view the documentation with

hatch run doc:clean
hatch run doc:build
hatch run doc:serve

To run a clean build and publish, run

hatch run doc:build-and-publish

altair_tiles's People

Contributors

binste avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

mattijn

altair_tiles's Issues

enable options to select a layer from a provider

Feature request! I'm trying to use altair_tiles within a coastal project for monitoring. I've multiple satellite images of this location that are available as wmts-links. Currently it is easy to add a single tile-layer, but could this become more dynamic so multiple layers of a provider can be selected as parameter?

An example.
The following works

import geopandas as gpd
import altair as alt
import altair_tiles as til
import xyzservices as xyz

features = [{"type": "Feature", "properties": {}, "geometry": {"type": "Polygon", "coordinates": [[[4.9794, 53.2789], [4.9794, 53.2701], [4.9717, 53.2657], [4.9717, 53.2613], [4.9485, 53.2613], [4.9563, 53.27014], [4.9794, 53.2789]]]}}]
gdf = gpd.GeoDataFrame.from_features(features)
geoshape = alt.Chart(gdf).mark_geoshape(filled=False, stroke='white', strokeWidth=3)

wmts_url = "https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20231229_110351_PNEO-03_1_62_30cm_RD_8bit_NRG_Vlieland/{z}/{x}/{y}"
sdp_provider = xyz.TileProvider(name="sdp", url=wmts_url, attribution="(C) geoserve")
chart = til.create_tiles_chart(provider=sdp_provider, zoom=17) + geoshape
chart

image

But now I like to add more options for the tile provider, like something as such:

wmts_urls = [
    "https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20231229_110351_PNEO-03_1_62_30cm_RD_8bit_NRG_Vlieland/",
    "https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20230905_105214_PNEO-03_1_9_30cm_RD_8bit_NRG_Waddenzee/",
    "https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20230815_104917_PNEO-03_1_1_30cm_RD_8bit_NRG_Vlieland/",
    "https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20230813_110015_PNEO-04_1_1_30cm_RD_8bit_NRG_Vlieland/"
]
wmts_options = alt.binding_select(options=wmts_urls)
param_wmts = alt.param(name="base_url_options", value=wmts_urls[0], bind=wmts_options)
chart = chart.add_params(param_wmts)
chart

image

This is just adding the options, but to make this actual work, something needs to modified to utilize this base_url_options param.
I was inspecting the created vega-lite specification and noticed the following, hardcoded in a transform calculate

chart.to_dict()["layer"][1]["layer"][0]["transform"][2]
{'calculate': "'https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20231229_110351_PNEO-03_1_62_30cm_RD_8bit_NRG_Vlieland/' + zoom_ceil + '/' + ((datum.a + dii_floor + max_one_side_tiles_count) % max_one_side_tiles_count) + '/' + (datum.b + djj_floor) + ''",
 'as': 'url'}

Maybe we can update altair_tiles, so this URL is referenced by a parameter, which can be easily modified to listen to the base_url_options param.
Something as such:

import re

param_base_url = alt.param(name="base_url", value="https://tiles1.geoserve.eu/Pleiades-NEO_NRG/tileserver/20231229_110351_PNEO-03_1_62_30cm_RD_8bit_NRG_Vlieland/")
chart = chart.add_params(param_base_url)

for ix, par in enumerate(chart.params):
    if par.name == 'base_url':
        chart.params[ix] = alt.param(name='base_url', expr='base_url_options').param

pattern = re.compile(r"'(https?://[^']+)'\s*\+")
tf_calculate = chart.layer[1].layer[0].transform[2].calculate
new_tf_calculate = re.sub(pattern, "base_url +", tf_calculate)
chart.layer[1].layer[0].transform[2].calculate = new_tf_calculate
chart
BQxSPJT3eO.mp4

I think this feature require a change in the code around here: https://github.com/altair-viz/altair_tiles/blob/main/altair_tiles/__init__.py#L262

(btw, I'm not sure if these wmts-URLs are accessible outside the Netherlands.)

Replace alt.Projection function argument with explicit values

Some functions such as _create_nonstandalone_tiles_chart have projection: alt.Projection as an argument. I think it's better to be explicit and just ask for the scale value which seems to be the only value we need. Validation of the projection type could be done only in add_tiles. For create_tiles_chart, we don't need to validate it.

Thoughts on API

  • Does it make sense to more directly integrate with Altair so a user could do mark_geoshape(tile=True) as proposed in vega/vega-lite#8885? Once it's implemented in Vega-Lite, I think this is a great syntax but for this package I'm not sure as it would then change the type of the Altair chart to a layered chart. As a user I'd find this transition from alt.Chart -> alt.LayerChart unexpected. Also, this way it's clearer that the functionality comes from a separate package.
  • Are add_tiles and create_tiles_chart good names? add_tiles is inspired from add_basemap in contextily
  • Is it user friendly enough to pass an alt.Projection object to create_tiles_chart or better to expand the arguments, i.e. create_tiles_chart(scale=..., translate=...)?
  • Instead of providing add_tiles and add_attribution functions, we could rely solely on create_..._chart functions and then the returned charts can be layered by the user themselves. For attribution, this would work and might simplify it. For add_tiles, we would always need to create the empty geoshape chart which is created in create_..._chart as else it is not a standalone working chart.
    • Way around this could be to introduce a new class TilesChart which only adds the geoshape layer when to_dict is called and if it is added to another chart it would loose that behavior (overwrite __add__).
    • TilesChart could also help with always keeping the attribution layer on top. There could be an escape hatch to convert it to a normal Altair class.

Add support for `fit` in `project`

The current implementation does not work if fit is used in project, see point 4 on this page for an example. The placement of the tiles is off. This also happens if neither scale nor transpose are provided (and also not fit) as then Vega-Lite adds a fit parameter itself to prevent the chart size from exploding, this is called "autofitting" and was introduced in vega/vega-lite#4843.

Part of Vega spec with autofitting:

    {
      "name": "projection",
      "size": {"signal": "[width, height]"},
      "fit": {"signal": "data('source_0')"},
      "center": [0, 40],
      "rotate": [-10, 0, 0],
      "type": "mercator"
    }

Same but translate was specified in Vega-Lite spec:

    {
      "name": "projection",
      "translate": [0, 10],
      "center": [0, 40],
      "rotate": [-10, 0, 0],
      "type": "mercator"
    }

From Vega docs:

  • fit: GeoJSON data to which the projection should attempt to automatically fit the translate and scale parameters.
  • size: Used in conjunction with fit, provides the width and height in pixels of the area to which the projection should be automatically fit.

As fit is very useful and it's even added in the default case of only projection type = 'mercator' without any additional settings is set, we need to support it.

See this example in the Vega Editor for a spec where the placement is off due to fit. How can we replicate the behavior of fit for the placement of the tiles? As a first step, what exactly happens if fit is set? Is this the relevant code?

Updates

  • Tile size will need to depend on chart size as with fit it depends on it, see signals in Vega spec above
  • If the chart aspect ratio is not the same as the one of the bounding box, there is some space either left and right or top and bottom and geoshape mark are centered -> Need to adjust for this (in basepoint?) and decide if white space is ok or if tiles should fill out whole chart (seems preferable to me)
    • Can maybe get coordinates of bounding box with geoBounds and then shift tiles by (chart width - box width) / 2 on x and similar on y

Create and host documentation

Create it using Jupyter book. Easy to use but still extensibility of Sphinx underneath it, can render Altair charts, see the Altair ally docs, and it has a nice default theme. Host with github pages.

Sections:

  • Intro incl. installation instructions
  • Tutorial
  • API References

Tutorial could simply be the Examples.ipynb notebook for now. Don't have to write too much as long as API is not more stable.

Add documentation link to README and github repo on the right side.

Create basemap chart without geoshape chart

add_basemap requires an existing Altair chart with a geoshape mark. Would be nice to be able to create a standalone chart with only a basemap.

An extra feature would be if that basemap chart can be created first and then another geoshape mark chart added with the usual basemap_chart + geoshape_chart syntax.

Rethink package name and org

  • I chose altair_basemap following the tradition of naming extensions altair_* and based on the matplotlib basemap package. But maybe there are better names? altair_tiles, altair_maptiles, altair_geotiles, ...
  • Could be moved to altair-viz org

Only load tile URLs which are valid

Got some JavaScript errors when I made the charts too big and then the tile URLs were no longer valid. The chart does then no longer show up:

[Error] Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.

If I switch to the SVG renderer, the chart at least shows up but with "invalid image" icons:

image

import altair as alt
from vega_datasets import data

import altair_tiles as til

source = alt.topo_feature(data.world_110m.url, "countries")
geoshape_countries = (
    alt.Chart(source, width=600, height=600)
    .mark_geoshape(
        stroke="orange", 
        strokeWidth=2, 
        fillOpacity=0.1
    )
    .encode(fill=alt.Fill("id:Q").legend(None))
    .project(
        type="mercator",
        scale=2000,
        center=[10, 45],
    )
)

chart = til.add_tiles(
    geoshape_countries, zoom=5, provider=til.providers.SwissFederalGeoportal.NationalMapColor
)
chart = chart.properties(width=700)
chart

Open the Chart in the Vega Editor

  • How can we determine which tiles are valid? Some xyz providers have bounds defined https://github.com/geopandas/xyzservices/blob/main/provider_sources/xyzservices-providers.json#L216 -> If no bounds defined let's assume that tiles exist for whole earth.
  • Could maybe subset the tile urls with a filter transform to a predetermined range based on the zoom level? Ok if calculation of these restrictions happens in Python but even better if it can be pushed to Vega Expressions.
  • Can maybe also use this approach to fix the issue with the grid size which currently is hard coded as data generators do not accept signals (see comments in code) -> Could make a grid which very likely is large enough for all use cases and then use transform_filter based on height and width signals to subset to only the ones which are needed. Can then remove the num_grid_* arguments to simplify function signatures.

Add support for layered charts in add_tiles

add_tiles currently only works for altair.Chart instances. We could extend this to work for alt.LayerChart where we could iterate through all layers (recursively in case it is a layered chart again?) and find the geoshape chart. Raise an error if multiple ones? Then read projection information from that one.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.