Skip to content

Commit

Permalink
Merge 8233408 into 2ba25c5
Browse files Browse the repository at this point in the history
  • Loading branch information
adnanhemani committed Jul 2, 2019
2 parents 2ba25c5 + 8233408 commit 6ba47c2
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 3,198 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -26,6 +26,9 @@ pip install datascience

This project adheres to [Semantic Versioning](http://semver.org/).

### v0.12.1
* Update mapping code to work with the latest version of Folium (0.9.1).

### v0.12.0
* Changes `Table#scatter`'s argument name of `colors` to `group` to mirror `Table#hist`.
* Makes a grouped scatterplot's legend identical to a group histogram's legend.
Expand Down
111 changes: 77 additions & 34 deletions datascience/maps.py
Expand Up @@ -5,6 +5,7 @@

import IPython.display
import folium
from folium.plugins import MarkerCluster
import pandas
import numpy as np

Expand Down Expand Up @@ -107,13 +108,22 @@ def __iter__(self):

def _set_folium_map(self):
self._folium_map = self._create_map()
if 'clustered_marker' in self._attrs and self._attrs['clustered_marker']:
marker_cluster = MarkerCluster().add_to(self._folium_map)
clustered = True
else:
clustered = False
for feature in self._features.values():
feature.draw_on(self._folium_map)
if clustered and isinstance(feature, Marker):
feature.draw_on(marker_cluster)
else:
feature.draw_on(self._folium_map)

def _create_map(self):
attrs = {'width': self._width, 'height': self._height}
attrs.update(self._autozoom())
attrs.update(self._attrs.copy())

# Enforce zoom consistency
attrs['max_zoom'] = max(attrs['zoom_start']+2, attrs['max_zoom'])
attrs['min_zoom'] = min(attrs['zoom_start']-2, attrs['min_zoom'])
Expand All @@ -130,11 +140,6 @@ def _autozoom(self):
midpoint(bounds['min_lon'], bounds['max_lon'])
)

# self._folium_map.fit_bounds(
# [bounds['min_long'], bounds['min_lat']],
# [bounds['max_long'], bounds['max_lat']]
# )

# remove the following with new Folium release
# rough approximation, assuming max_zoom is 18
import math
Expand Down Expand Up @@ -249,14 +254,17 @@ def color(self, values, ids=(), key_on='feature.id', palette='YlOrBr', **kwargs)
m = self._create_map()
data = pandas.DataFrame({id_name: ids, value_name: values})
attrs = {
'geo_str': json.dumps(self.geojson()),
'geo_data': json.dumps(self.geojson()),
'data': data,
'columns': [id_name, value_name],
'key_on': key_on,
'fill_color': palette,
}
kwargs.update(attrs)
m.geo_json(**kwargs)
folium.Choropleth(
**kwargs,
name='geojson'
).add_to(m)
colored = self.format()
colored._folium_map = m
return colored
Expand Down Expand Up @@ -355,17 +363,9 @@ def _read_geojson_features(data, features=None, prefix=""):
class _MapFeature(_FoliumWrapper, abc.ABC):
"""A feature displayed on a map. When displayed alone, a map is created."""

# Method name for a folium.Map to add the feature
_map_method_name = ""

# Default dimensions for displaying the feature in isolation
_width = 180
_height = 180

def draw_on(self, folium_map):
"""Add feature to Folium map object."""
f = getattr(folium_map, self._map_method_name)
f(**self._folium_kwargs)
_width = 960
_height = 500

def _set_folium_map(self):
"""A map containing only the feature."""
Expand All @@ -390,13 +390,20 @@ def _folium_kwargs(self):
def geojson(self, feature_id):
"""Return GeoJSON."""

@abc.abstractmethod
def draw_on(self, folium_map):
"""Add feature to Folium map object."""


class Marker(_MapFeature):
"""A marker displayed with Folium's simple_marker method.
popup -- text that pops up when marker is clicked
color -- fill color
color -- The color of the marker. You can use:
[‘red’, ‘blue’, ‘green’, ‘purple’, ‘orange’, ‘darkred’,
’lightred’, ‘beige’, ‘darkblue’, ‘darkgreen’, ‘cadetblue’, ‘darkpurple’,
‘white’, ‘pink’, ‘lightblue’, ‘lightgreen’, ‘gray’, ‘black’, ‘lightgray’]
Defaults from Folium:
marker_icon: string, default 'info-sign'
Expand All @@ -409,18 +416,21 @@ class Marker(_MapFeature):
angle of icon
popup_width: int, default 300
width of popup
"""
_map_method_name = 'simple_marker'
_color_param = 'marker_color'
The icon can be further customized by by passing in attributes
into kwargs by using the attributes listed in
`https://python-visualization.github.io/folium/modules.html#folium.map.Icon`.
"""

def __init__(self, lat, lon, popup='', color='blue', **kwargs):
#TODO: Figure out clustered_marker (Adnan)
assert isinstance(lat, _number)
assert isinstance(lon, _number)
self.lat_lon = (lat, lon)
self._attrs = {
'popup': popup,
self._color_param: color,
'color': color,
**kwargs
}
self._attrs.update(kwargs)

Expand All @@ -436,6 +446,10 @@ def copy(self):
def _folium_kwargs(self):
attrs = self._attrs.copy()
attrs['location'] = self.lat_lon
icon_args = {k: attrs.pop(k) for k in attrs.keys() & {'color', 'marker_icon', 'clustered_marker', 'icon_angle', 'popup_width'}}
if 'marker_icon' in icon_args:
icon_args['icon'] = icon_args.pop('marker_icon')
attrs['icon'] = folium.Icon(**icon_args)
return attrs

def geojson(self, feature_id):
Expand All @@ -457,6 +471,9 @@ def format(self, **kwargs):
lat, lon = self.lat_lon
return type(self)(lat, lon, **attrs)

def draw_on(self, folium_map):
folium.Marker(**self._folium_kwargs).add_to(folium_map)

@classmethod
def _convert_point(cls, feature):
"""Convert a GeoJSON point to a Marker."""
Expand All @@ -465,10 +482,15 @@ def _convert_point(cls, feature):
return cls(lat, lon)

@classmethod
def map(cls, latitudes, longitudes, labels=None, colors=None, areas=None, **kwargs):
def map(cls, latitudes, longitudes, labels=None, colors=None, areas=None, clustered_marker=False, **kwargs):
"""Return markers from columns of coordinates, labels, & colors.
The areas column is not applicable to markers, but sets circle areas.
Arguments: (TODO) document all options
clustered_marker: boolean, default False
boolean of whether or not you want the marker clustered with other markers
"""
assert len(latitudes) == len(longitudes)
assert areas is None or hasattr(cls, '_has_radius'), "A " + cls.__name__ + " has no radius"
Expand All @@ -485,12 +507,12 @@ def map(cls, latitudes, longitudes, labels=None, colors=None, areas=None, **kwar
assert len(areas) == len(latitudes)
inputs.append(np.array(areas) ** 0.5 / math.pi)
ms = [cls(*args, **kwargs) for args in zip(*inputs)]
return Map(ms)
return Map(ms, clustered_marker=clustered_marker)

@classmethod
def map_table(cls, table, **kwargs):
def map_table(cls, table, clustered_marker=False, **kwargs):
"""Return markers from the colums of a table."""
return cls.map(*table.columns, **kwargs)
return cls.map(*table.columns, clustered_marker=clustered_marker, **kwargs)


class Circle(Marker):
Expand All @@ -505,6 +527,9 @@ class Circle(Marker):
fill_opacity: float, default 0.6
Circle fill opacity
More options can be passed into kwargs by following the attributes
listed in `https://leafletjs.com/reference-1.4.0.html#circlemarker`.
For example, to draw three circles::
t = Table().with_columns([
Expand All @@ -517,19 +542,28 @@ class Circle(Marker):
Circle.map_table(t)
"""

_map_method_name = 'circle_marker'
_color_param = 'fill_color'
_has_radius = True

def __init__(self, lat, lon, popup='', color='blue', radius=10, **kwargs):
super().__init__(lat, lon, popup, color, radius=radius, line_color=None, **kwargs)
super().__init__(lat, lon, popup, color, radius=radius, **kwargs)

@property
def _folium_kwargs(self):
attrs = self._attrs.copy()
attrs['location'] = self.lat_lon
if 'color' in attrs:
attrs['fill_color'] = attrs.pop('color')
if 'line_color' in attrs:
attrs['color'] = attrs.pop('line_color')
return attrs

def draw_on(self, folium_map):
folium.CircleMarker(**self._folium_kwargs).add_to(folium_map)


class Region(_MapFeature):
"""A GeoJSON feature displayed with Folium's geo_json method."""

_map_method_name = 'geo_json'

def __init__(self, geojson, **kwargs):
assert 'type' in geojson
assert geojson['type'] == 'Feature'
Expand Down Expand Up @@ -579,7 +613,7 @@ def copy(self):
@property
def _folium_kwargs(self):
attrs = self._attrs.copy()
attrs['geo_str'] = json.dumps(self._geojson)
attrs['data'] = json.dumps(self._geojson)
return attrs

def geojson(self, feature_id):
Expand All @@ -597,6 +631,15 @@ def format(self, **kwargs):
attrs.update(kwargs)
return Region(self._geojson, **attrs)

def draw_on(self, folium_map):
attrs = self._folium_kwargs
data = attrs.pop('data')
folium.GeoJson(
data=data,
style_function=lambda x: attrs,
name='geojson'
).add_to(folium_map)


def _lat_lons_from_geojson(s):
"""Return a latitude-longitude pairs from nested GeoJSON coordinates.
Expand Down
2 changes: 1 addition & 1 deletion datascience/version.py
@@ -1 +1 @@
__version__ = '0.12.0'
__version__ = '0.12.1'
2 changes: 1 addition & 1 deletion requirements.txt
@@ -1,3 +1,3 @@
folium==0.2.1
folium>=0.9.1
sphinx
setuptools

0 comments on commit 6ba47c2

Please sign in to comment.