<commit_before>.. currentmodule:: altair

.. _user-guide-mark-geoshape:

Geoshape
~~~~~~~~~~~~~
``mark_geoshape`` represents an arbitrary shapes whose geometry is determined by specified spatial data.

Basic Map
~~~~~~~~~
Altair can work with many different geographical data formats, including geojson and topojson files. Often, the most convenient input format to use is a ``GeoDataFrame``. Here we load the Natural Earth dataset and create a basic map using ``mark_geoshape``:

.. altair-plot::

    import altair as alt
    from vega_datasets import data
    import geopandas as gpd

    fp = gpd.datasets.get_path('naturalearth_lowres')
    gdf_ne = gpd.read_file(fp)  # shapefile    

    alt.Chart(gdf_ne).mark_geoshape()

In the example above, Altair applies a default blue ``fill`` color and uses a default map projection (``equalEarth``). We can customize the colors and boundary stroke widths using standard mark properties. Using the ``project`` method we can also define the map projection manually:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape(
        fill='lightgrey', stroke='white', strokeWidth=0.5
    ).project(
        type='equalEarth'
    )

Focus & Filtering
~~~~~~~~~~~~~~~~~
By default Altair automatically adjusts the projection so that all the data fits within the width and height of the chart. 
Multiple approaches can be used to focus on specific regions of your spatial data. Namely:

1. Filter the source data within your GeoDataFrame.
2. Filter the source data using a ``transform_filter``.
3. Specify ``scale`` (zoom level) and ``translate`` (panning) within the ``project`` method.
4. Specify ``fit`` (extent) within the ``project`` & ``clip=True`` in the mark properties.

The following examples applies these approaches to focus on continental Africa:

1. Filter the source data within your GeoDataFrame:

.. altair-plot::

    gdf_sel = gdf_ne[gdf_ne.continent == 'Africa']

    alt.Chart(gdf_sel).mark_geoshape()

2. Filter the source data using a ``transform_filter``:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape().transform_filter(
        alt.datum.continent == 'Africa'
    )

3. Specify ``scale`` (zoom level) and ``translate`` (panning) within the ``project`` method:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape().project(
        type='equalEarth', 
        scale=200, 
        translate=[160, 160]  # lon, lat
    )

3. Specify ``fit`` (extent) within the ``project`` method & ``clip=True`` in the mark properties:

.. altair-plot::

    from shapely.ops import orient
    from shapely.geometry import mapping

    extent_roi = gdf_ne.query("continent == 'Africa'").unary_union.envelope

    # fit object should be an array of GeoJSON-like features
    # order polygon exterior needs to be clock-wise (left-hand-rule)
    if extent_roi.exterior.is_ccw:
        extent_roi = orient(extent_roi, -1)
    extent_roi_geojson = [mapping(extent_roi)]

    alt.Chart(gdf_ne).mark_geoshape(clip=True).project(
        type='equalEarth',
        fit=extent_roi_geojson
    )    

Cartesian coordinates
~~~~~~~~~~~~~~~~~~~~~
The default projection of Altair is ``equalEarth``. This default assumes that your geometries are in degrees and referenced by longitude and latitude values. 
Another widely used coordinate system for data visualization is the 2d cartesian coordinate system. This coordinate system does not take into account the curvature of the Earth.

In the following example the input geometry is not projected and is instead rendered directly in raw coordinates using the ``identity`` projection type. We have to define the ``reflectY`` as well since Canvas and SVG treats positive ``y`` as pointing down.

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape().project(
        type='identity', 
        reflectY=True
    )

Mapping Polygons
~~~~~~~~~~~~~~~~
The following example maps the visual property of the ``name`` column using the ``color`` encoding.

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape().encode(
        color='name:N'
    )

Since each country is represented by a (multi)polygon, we can separate the ``stroke`` and ``fill`` definitions as such: 

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape(
        stroke='white',
        strokeWidth=1.5
    ).encode(
        fill='name:N'
    ) 

Mapping Lines
~~~~~~~~~~~~~
By default Altair assumes for ``mark_geoshape`` that the mark's color is used for the fill color instead of the stroke color.
This means that if your source data contain (multi)lines, you will have to explicitly define the ``filled`` value as ``False``.

Compare:

.. altair-plot::
    
    gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
    alt.Chart(gs_line).mark_geoshape().project(
        type='identity', 
        reflectY=True
    )    

With:

.. altair-plot::
    
    gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
    alt.Chart(gs_line).mark_geoshape(
        filled=False
    ).project(
        type='identity', 
        reflectY=True
    ) 

Using this approach one can also style Polygons as if they are Linestrings:

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape(
        filled=False,
        strokeWidth=1.5
    ).encode(
        stroke='name:N'
    )

Mapping Points
~~~~~~~~~~~~~~
Points can be drawn when they are defined as ``Points`` within a GeoDataFrame using ``mark_geoshape``.
We first assign the centroids of Polygons as Point geometry and plot these:

.. altair-plot::

    gdf_centroid = gpd.GeoDataFrame(
        data=gdf_sel.drop('geometry', axis=1), 
        geometry=gdf_sel.geometry.centroid
    )

    alt.Chart(gdf_centroid).mark_geoshape()


Caveat: To use the ``size`` encoding for the Points you will need to use the ``mark_circle`` in combination with the ``latitude`` and ``longitude`` encoding channel definitions.

.. altair-plot::

    gdf_centroid['lon'] = gdf_centroid['geometry'].x
    gdf_centroid['lat'] = gdf_centroid['geometry'].y

    alt.Chart(gdf_centroid).mark_circle().encode(
        longitude='lon:Q',
        latitude='lat:Q',
        size="pop_est:Q"
    )

You could skip the extra assignment to the ``lon`` and ``lat`` column in the GeoDataFrame and use the coordinates directly.  We combine the chart with a basemap to bring some perspective to the points:

.. altair-plot::

    basemap = alt.Chart(gdf_sel).mark_geoshape(
        fill='lightgray', stroke='white', strokeWidth=0.5
    )

    bubbles = alt.Chart(gdf_centroid).mark_circle(
        stroke='black'
    ).encode(<commit_msg>Update doc/user_guide/marks/geoshape.rst<commit_after>.. currentmodule:: altair

.. _user-guide-mark-geoshape:

Geoshape
~~~~~~~~~~~~~
``mark_geoshape`` represents an arbitrary shapes whose geometry is determined by specified spatial data.

Basic Map
~~~~~~~~~
Altair can work with many different geographical data formats, including geojson and topojson files. Often, the most convenient input format to use is a ``GeoDataFrame``. Here we load the Natural Earth dataset and create a basic map using ``mark_geoshape``:

.. altair-plot::

    import altair as alt
    from vega_datasets import data
    import geopandas as gpd

    fp = gpd.datasets.get_path('naturalearth_lowres')
    gdf_ne = gpd.read_file(fp)  # shapefile    

    alt.Chart(gdf_ne).mark_geoshape()

In the example above, Altair applies a default blue ``fill`` color and uses a default map projection (``equalEarth``). We can customize the colors and boundary stroke widths using standard mark properties. Using the ``project`` method we can also define the map projection manually:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape(
        fill='lightgrey', stroke='white', strokeWidth=0.5
    ).project(
        type='equalEarth'
    )

Focus & Filtering
~~~~~~~~~~~~~~~~~
By default Altair automatically adjusts the projection so that all the data fits within the width and height of the chart. 
Multiple approaches can be used to focus on specific regions of your spatial data. Namely:

1. Filter the source data within your GeoDataFrame.
2. Filter the source data using a ``transform_filter``.
3. Specify ``scale`` (zoom level) and ``translate`` (panning) within the ``project`` method.
4. Specify ``fit`` (extent) within the ``project`` & ``clip=True`` in the mark properties.

The following examples applies these approaches to focus on continental Africa:

1. Filter the source data within your GeoDataFrame:

.. altair-plot::

    gdf_sel = gdf_ne[gdf_ne.continent == 'Africa']

    alt.Chart(gdf_sel).mark_geoshape()

2. Filter the source data using a ``transform_filter``:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape().transform_filter(
        alt.datum.continent == 'Africa'
    )

3. Specify ``scale`` (zoom level) and ``translate`` (panning) within the ``project`` method:

.. altair-plot::

    alt.Chart(gdf_ne).mark_geoshape().project(
        type='equalEarth', 
        scale=200, 
        translate=[160, 160]  # lon, lat
    )

3. Specify ``fit`` (extent) within the ``project`` method & ``clip=True`` in the mark properties:

.. altair-plot::

    from shapely.ops import orient
    from shapely.geometry import mapping

    extent_roi = gdf_ne.query("continent == 'Africa'").unary_union.envelope

    # fit object should be an array of GeoJSON-like features
    # order polygon exterior needs to be clock-wise (left-hand-rule)
    if extent_roi.exterior.is_ccw:
        extent_roi = orient(extent_roi, -1)
    extent_roi_geojson = [mapping(extent_roi)]

    alt.Chart(gdf_ne).mark_geoshape(clip=True).project(
        type='equalEarth',
        fit=extent_roi_geojson
    )    

Cartesian coordinates
~~~~~~~~~~~~~~~~~~~~~
The default projection of Altair is ``equalEarth``. This default assumes that your geometries are in degrees and referenced by longitude and latitude values. 
Another widely used coordinate system for data visualization is the 2d cartesian coordinate system. This coordinate system does not take into account the curvature of the Earth.

In the following example the input geometry is not projected and is instead rendered directly in raw coordinates using the ``identity`` projection type. We have to define the ``reflectY`` as well since Canvas and SVG treats positive ``y`` as pointing down.

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape().project(
        type='identity', 
        reflectY=True
    )

Mapping Polygons
~~~~~~~~~~~~~~~~
The following example maps the visual property of the ``name`` column using the ``color`` encoding.

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape().encode(
        color='name:N'
    )

Since each country is represented by a (multi)polygon, we can separate the ``stroke`` and ``fill`` definitions as such: 

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape(
        stroke='white',
        strokeWidth=1.5
    ).encode(
        fill='name:N'
    ) 

Mapping Lines
~~~~~~~~~~~~~
By default Altair assumes for ``mark_geoshape`` that the mark's color is used for the fill color instead of the stroke color.
This means that if your source data contain (multi)lines, you will have to explicitly define the ``filled`` value as ``False``.

Compare:

.. altair-plot::
    
    gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
    alt.Chart(gs_line).mark_geoshape().project(
        type='identity', 
        reflectY=True
    )    

With:

.. altair-plot::
    
    gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
    alt.Chart(gs_line).mark_geoshape(
        filled=False
    ).project(
        type='identity', 
        reflectY=True
    ) 

Using this approach one can also style Polygons as if they are Linestrings:

.. altair-plot::

    alt.Chart(gdf_sel).mark_geoshape(
        filled=False,
        strokeWidth=1.5
    ).encode(
        stroke='name:N'
    )

Mapping Points
~~~~~~~~~~~~~~
Points can be drawn when they are defined as ``Points`` within a GeoDataFrame using ``mark_geoshape``.
We first assign the centroids of Polygons as Point geometry and plot these:

.. altair-plot::

    gdf_centroid = gpd.GeoDataFrame(
        data=gdf_sel.copy(),  # .copy() to prevent changing the original `gdf_sel` variable 
        geometry=gdf_sel.geometry.centroid
    )

    alt.Chart(gdf_centroid).mark_geoshape()


Caveat: To use the ``size`` encoding for the Points you will need to use the ``mark_circle`` in combination with the ``latitude`` and ``longitude`` encoding channel definitions.

.. altair-plot::

    gdf_centroid['lon'] = gdf_centroid['geometry'].x
    gdf_centroid['lat'] = gdf_centroid['geometry'].y

    alt.Chart(gdf_centroid).mark_circle().encode(
        longitude='lon:Q',
        latitude='lat:Q',
        size="pop_est:Q"
    )

You could skip the extra assignment to the ``lon`` and ``lat`` column in the GeoDataFrame and use the coordinates directly.  We combine the chart with a basemap to bring some perspective to the points:

.. altair-plot::

    basemap = alt.Chart(gdf_sel).mark_geoshape(
        fill='lightgray', stroke='white', strokeWidth=0.5
    )

    bubbles = alt.Chart(gdf_centroid).mark_circle(
        stroke='black'
    ).encode(
{'fraction_non_alphanumeric': 0.1188853551416023, 'fraction_numerical': 0.00779948508253824, 'mean_word_length': 3.573060941828255, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2742643', 'n_tokens_mistral': 4135, 'n_tokens_neox': 3997, 'n_words': 1353}