import altair as alt
import geopandas as gpd
import six
import json
import warnings
from toolz.curried import curry
def geopandas_to_dict(data):
try:
if ('geometry' != data.geometry.name) and ('geometry' in data.columns):
warnings.warn(
"Column name 'geometry' is reserved name for " +
"GeoDataFrame. Column named 'geometry' should contain " +
"actual displaying geometry or not be used. Data of column " +
"will not be accessible from the chart description. "
)
if 'type' in data.columns:
warnings.warn(
"Column name 'type' is reserved name for GeoDataFrame. " +
"Data of column 'type' will not be accessible from the " +
"chart description."
)
if 'id' in data.columns:
warnings.warn(
"Column name 'id' is reserved name for GeoDataFrame for " +
"index values. Data of column 'id' will not be accessible " +
"from the chart description."
)
if data.crs:
data = data.to_crs(epsg=4326)
return [dict(
row,
type=feature['type'],
geometry=feature['geometry'],
id=feature['id']
) for row, feature in zip(
data.drop(
data.geometry.name,
axis=1
).to_dict('row'),
data.geometry.__geo_interface__['features']
)
]
except AttributeError as err:
if str(err).startswith('No geometry data set yet'):
warnings.warn("GeoDataFrame has no geometry to display.")
return data.to_dict('row')
else:
raise
[docs]@curry
def gpd_to_values(data):
"""Replace a GeoDataFrame by a data model with values.
For ``geopandas.GeoDataFrame`` columns values are stored as Foreign Members
of GeoJSON feature objects. For all other types uses function
:py:func:`altair.to_values`."""
if isinstance(data, gpd.GeoDataFrame):
data = alt.utils.sanitize_dataframe(data)
values = geopandas_to_dict(data)
return {'values': json.dumps(values)}
else:
return alt.to_values(data)
[docs]@curry
def gpd_to_json(data):
"""Write the data model to a .json file and return a url based data model.
For ``geopandas.GeoDataFrame`` columns values are stored as Foreign Members
of GeoJSON feature objects. For all other types uses function
:py:func:`altair.to_json`."""
if isinstance(data, gpd.GeoDataFrame):
data = alt.utils.sanitize_dataframe(data)
values = geopandas_to_dict(data)
return alt.to_json({'values': values})
else:
return alt.to_json(data)
alt.data_transformers.register(
'gpd_to_values',
lambda data: alt.pipe(data, alt.limit_rows, gpd_to_values)
)
alt.data_transformers.register(
'gpd_to_json',
lambda data: alt.pipe(data, alt.limit_rows, gpd_to_json)
)
alt.data_transformers.enable('gpd_to_values')
[docs]def geojson_feature(data, feature='features', **kwargs):
"""A convenience function for extracting features from a geojson object or url
Parameters
----------
data : anyOf(string, geojson.GeoJSON)
string is interpreted as URL from which to load the data set.
geojson.GeoJSON is interpreted as data set itself.
feature : string
The JSON property containing the GeoJSON object set to convert to
a GeoJSON feature collection. For example ``features[0].geometry``.
\**kwargs :
additional keywords passed to JsonDataFormat
"""
if isinstance(data, six.string_types):
return alt.UrlData(
url=data,
format=alt.JsonDataFormat(
type='json',
property=feature,
**kwargs
)
)
elif hasattr(data, '__geo_interface__'):
if isinstance(data, gpd.GeoDataFrame):
data = alt.utils.sanitize_dataframe(data)
return alt.InlineData(
values=data.__geo_interface__,
format=alt.JsonDataFormat(
type='json',
property=feature,
**kwargs
)
)
else:
warnings.warn("data of type {0} not recognized".format(type(data)))
return data