diff --git a/NEWS.rst b/NEWS.rst
index 5e0f29fd9..0d4a88c5f 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -13,26 +13,30 @@ Updates
- Add new visualization API (#662)
- Add Source class (with param detection)
- Add Dataset methods
- .from_table(name, context)
- .from_query(query, context)
- .from_geojson(data)
- .from_dataframe(data)
- - Add set_default_context methods
- - Add Context in cartoframes namespace
- - Add Map, Layer, Source, Style in cartoframes.viz namespace
+ .from_table(...)
+ .from_query(...)
+ .from_geojson(...)
+ .from_dataframe(...)
+ - Add set_default_context method
- Use sources' context (credentials, bounds)
- Fix Style class API for variables
- Remove Dataset, SQL, GeoJSON sources
- Remove sources namespace
- Remove context from Map
- Remove contrib namespace
- - Update docs in vis classes
- - Add/Update vis tests
+ - Update docs in viz classes
+ - Add/Update viz tests
- Pass PEP 8
- Add default style, based on the geom type (#648)
-- Add basemap None, bool and color interface (#635)
+- Add basemap None and color interface (#635)
- Add Popup API (click and hover) (#677)
- Apply default style for not overwritten properties (#684)
+- Add namespaces (#683)
+ - cartoframes.viz: Map, Layer, Source, Style, Popup, basemaps, helpers
+ - cartoframes.auth: Context, set_default_context
+- Add color helpers (#692)
+ - color_category
+ - color_bins
0.9.2
-----
diff --git a/cartoframes/__init__.py b/cartoframes/__init__.py
index d7b68e27e..10d8847d0 100644
--- a/cartoframes/__init__.py
+++ b/cartoframes/__init__.py
@@ -17,7 +17,7 @@
'BinMethod',
# New API
- 'Dataset'
+ 'Dataset',
'__version__'
]
diff --git a/cartoframes/assets/templates/viz/basic.html.j2 b/cartoframes/assets/templates/viz/basic.html.j2
index 22a0a4931..07d914565 100644
--- a/cartoframes/assets/templates/viz/basic.html.j2
+++ b/cartoframes/assets/templates/viz/basic.html.j2
@@ -103,9 +103,10 @@
-
-
-
+
+ {% if has_legends %}
+ {% include 'viz/legends.html.j2' %}
+ {% endif %}
diff --git a/cartoframes/assets/templates/viz/legends.html.j2 b/cartoframes/assets/templates/viz/legends.html.j2
new file mode 100644
index 000000000..63fe1dfed
--- /dev/null
+++ b/cartoframes/assets/templates/viz/legends.html.j2
@@ -0,0 +1,35 @@
+{% macro legend(legend_data, id) -%}
+ {% set type = legend_data.type %}
+ {% if type == 'basic' %}
+
+ {% elif type == 'gradient' %}
+
+ {% elif type == 'color_steps' %}
+
+ {% else %}
+
+
Unknown legend type {{ type }}
+
+ {% endif %}
+{%- endmacro %}
+
+
+ {% if default_legend != none %}
+
+
+
+
+ {% endif %}
+
+ {% for source in sources %}
+ {% if source.legend %}
+
+ {{ legend(source.legend, 'source%d_legend' % loop.index0) }}
+
+ {% endif %}
+ {% endfor %}
+
\ No newline at end of file
diff --git a/cartoframes/assets/templates/viz/legends.js.j2 b/cartoframes/assets/templates/viz/legends.js.j2
new file mode 100644
index 000000000..7c2858539
--- /dev/null
+++ b/cartoframes/assets/templates/viz/legends.js.j2
@@ -0,0 +1,26 @@
+function createDefaultLegend(layers) {
+ const defaultLegendContainer = document.querySelector('#basicLegendContainer');
+ defaultLegendContainer.style.display = 'none';
+
+ AsBridge.VL.Legends.layersLegend(
+ '#basicLegend',
+ layers,
+ {
+ onLoad: () => defaultLegendContainer.style.display = 'unset'
+ }
+ )
+}
+
+function createLegend(layer, legendData, layerIndex) {
+ const element = document.querySelector(`#source${layerIndex}_legend`);
+
+ if (legendData.ramp) {
+ AsBridge.VL.Legends.rampLegend(
+ element,
+ layer,
+ legendData.ramp
+ );
+ } else {
+ // TODO: we don't have a bridge for this case, should this even be a case?
+ }
+}
\ No newline at end of file
diff --git a/cartoframes/assets/templates/viz/map.js.j2 b/cartoframes/assets/templates/viz/map.js.j2
index eeb19f3e6..fc84d4e1d 100644
--- a/cartoframes/assets/templates/viz/map.js.j2
+++ b/cartoframes/assets/templates/viz/map.js.j2
@@ -1,3 +1,6 @@
+{% if has_legends %}
+ {% include 'viz/legends.js.j2' %}
+{% endif %}
{% include 'error/parser.js.j2' %}
{% include 'utils/base64.js.j2' %}
@@ -39,12 +42,16 @@ function onReady() {
map.flyTo({{ camera|clear_none|tojson }});
{% endif %}
+ const layers = [];
+
sources.forEach((elem, idx) => {
const factory = new SourceFactory();
const source = factory.createSource(elem);
const viz = new carto.Viz(elem['viz']);
const layer = new carto.Layer(`layer${idx}`, source, viz);
+ layers.push(layer);
+
try {
layer._updateLayer.catch(displayError);
} catch (err) {
@@ -73,6 +80,10 @@ function onReady() {
}
}
+ if (elem.legend) {
+ createLegend(layer, elem.legend, idx);
+ }
+
function setPopupsClick(tempPopup, interactivity, attrs) {
interactivity.on('featureClick', (event) => {
updatePopup(tempPopup, event, attrs)
@@ -139,6 +150,10 @@ function onReady() {
}
}
});
+
+ {% if default_legend %}
+ createDefaultLegend(layers);
+ {% endif %}
}
function setReady () {
diff --git a/cartoframes/utils.py b/cartoframes/utils.py
index acff7f418..27e437782 100644
--- a/cartoframes/utils.py
+++ b/cartoframes/utils.py
@@ -1,4 +1,6 @@
"""general utility functions"""
+
+import re
import sys
import hashlib
@@ -111,3 +113,7 @@ def merge_dicts(dict1, dict2):
d = dict1.copy()
d.update(dict2)
return d
+
+
+def text_match(regex, text):
+ return len(re.findall(regex, text, re.MULTILINE)) > 0
diff --git a/cartoframes/viz/defaults.py b/cartoframes/viz/defaults.py
index bb9cecdc4..4b207a039 100644
--- a/cartoframes/viz/defaults.py
+++ b/cartoframes/viz/defaults.py
@@ -4,10 +4,10 @@
AIRSHIP_BRIDGE_SCRIPT = '/packages/bridge/dist/asbridge.js'
AIRSHIP_STYLE = '/packages/styles/dist/airship.css'
AIRSHIP_ICONS_STYLE = '/packages/icons/dist/icons.css'
-AIRSHIP_COMPONENTS_PATH = 'https://libs.cartocdn.com/airship-components/v2.0/airship.js'
-AIRSHIP_BRIDGE_PATH = 'https://libs.cartocdn.com/airship-bridge/v2.0/asbridge.js'
-AIRSHIP_STYLES_PATH = 'https://libs.cartocdn.com/airship-style/v2.0/airship.css'
-AIRSHIP_ICONS_PATH = 'https://libs.cartocdn.com/airship-icons/v2.0/icons.css'
+AIRSHIP_COMPONENTS_PATH = 'https://libs.cartocdn.com/airship-components/cartoframes/airship.js'
+AIRSHIP_BRIDGE_PATH = 'https://libs.cartocdn.com/airship-bridge/cartoframes/asbridge.js'
+AIRSHIP_STYLES_PATH = 'https://libs.cartocdn.com/airship-style/cartoframes/airship.css'
+AIRSHIP_ICONS_PATH = 'https://libs.cartocdn.com/airship-icons/cartoframes/icons.css'
CREDENTIALS = {
'username': 'cartoframes',
diff --git a/cartoframes/viz/helpers/__init__.py b/cartoframes/viz/helpers/__init__.py
new file mode 100644
index 000000000..acf3b9b35
--- /dev/null
+++ b/cartoframes/viz/helpers/__init__.py
@@ -0,0 +1,16 @@
+from __future__ import absolute_import
+
+from .color_category_layer import color_category_layer
+from .color_bins_layer import color_bins_layer
+
+
+def inspect(helper):
+ import inspect
+ lines = inspect.getsource(helper)
+ print(lines)
+
+
+__all__ = [
+ 'color_category_layer',
+ 'color_bins_layer'
+]
diff --git a/cartoframes/viz/helpers/color_bins_layer.py b/cartoframes/viz/helpers/color_bins_layer.py
new file mode 100644
index 000000000..4162691d2
--- /dev/null
+++ b/cartoframes/viz/helpers/color_bins_layer.py
@@ -0,0 +1,32 @@
+from __future__ import absolute_import
+
+from ..layer import Layer
+
+
+def color_bins_layer(source, value, bins=5, palette='purpor', title=''):
+ return Layer(
+ source,
+ style={
+ 'point': {
+ 'color': 'ramp(globalQuantiles(${0},{1}),reverse({2}))'.format(value, bins, palette)
+ },
+ 'line': {
+ 'color': 'ramp(globalQuantiles(${0},{1}),reverse({2}))'.format(value, bins, palette)
+ },
+ 'polygon': {
+ 'color': 'opacity(ramp(globalQuantiles(${0},{1}),reverse({2})),0.9)'.format(value, bins, palette)
+ }
+ },
+ popup={
+ 'hover': {
+ 'label': title or value,
+ 'value': '$' + value
+ }
+ },
+ legend={
+ 'type': 'basic',
+ 'ramp': 'color',
+ 'heading': title or value,
+ 'description': ''
+ }
+ )
diff --git a/cartoframes/viz/helpers/color_category_layer.py b/cartoframes/viz/helpers/color_category_layer.py
new file mode 100644
index 000000000..877fe9b96
--- /dev/null
+++ b/cartoframes/viz/helpers/color_category_layer.py
@@ -0,0 +1,32 @@
+from __future__ import absolute_import
+
+from ..layer import Layer
+
+
+def color_category_layer(source, value, top=11, palette='bold', title=''):
+ return Layer(
+ source,
+ style={
+ 'point': {
+ 'color': 'ramp(top(${0}, {1}), {2})'.format(value, top, palette)
+ },
+ 'line': {
+ 'color': 'ramp(top(${0}, {1}), {2})'.format(value, top, palette)
+ },
+ 'polygon': {
+ 'color': 'opacity(ramp(top(${0}, {1}), {2}),0.9)'.format(value, top, palette)
+ }
+ },
+ popup={
+ 'hover': {
+ 'label': title or value,
+ 'value': '$' + value
+ }
+ },
+ legend={
+ 'type': 'basic',
+ 'ramp': 'color',
+ 'heading': title or value,
+ 'description': ''
+ }
+ )
diff --git a/cartoframes/viz/map.py b/cartoframes/viz/map.py
index 5c406d071..766c7bc6f 100644
--- a/cartoframes/viz/map.py
+++ b/cartoframes/viz/map.py
@@ -140,6 +140,7 @@ def __init__(self,
size=None,
viewport=None,
template=None,
+ default_legend=None,
**kwargs):
self.layers = _init_layers(layers)
@@ -153,12 +154,18 @@ def __init__(self,
self._airship_path = kwargs.get('_airship_path', None)
self._htmlMap = HTMLMap()
+ if default_legend is None and all(layer.legend is None for layer in self.layers):
+ self.default_legend = True
+ else:
+ self.default_legend = default_legend
+
self._htmlMap.set_content(
size=self.size,
sources=self.sources,
bounds=self.bounds,
viewport=self.viewport,
basemap=self.basemap,
+ default_legend=self.default_legend,
_carto_vl_path=self._carto_vl_path,
_airship_path=self._airship_path)
@@ -179,7 +186,7 @@ def _get_bounds(bounds, layers):
def _init_layers(layers):
if layers is None:
- return None
+ return []
if not isinstance(layers, collections.Iterable):
return [layers]
else:
@@ -380,14 +387,15 @@ def __init__(self):
def set_content(
self, size, sources, bounds, viewport=None, basemap=None,
+ default_legend=None,
_carto_vl_path=defaults.CARTO_VL_PATH, _airship_path=None):
self.html = self._parse_html_content(
- size, sources, bounds, viewport, basemap,
+ size, sources, bounds, viewport, basemap, default_legend,
_carto_vl_path, _airship_path)
def _parse_html_content(
- self, size, sources, bounds, viewport, basemap=None,
+ self, size, sources, bounds, viewport, basemap=None, default_legend=None,
_carto_vl_path=defaults.CARTO_VL_PATH, _airship_path=None):
token = ''
@@ -397,14 +405,6 @@ def _parse_html_content(
# No basemap
basecolor = 'white'
basemap = ''
- elif isinstance(basemap, bool):
- if basemap is True:
- # Default basemap
- basemap = Basemaps.darkmatter
- else:
- # No basemap
- basecolor = 'white'
- basemap = ''
elif isinstance(basemap, str):
if basemap not in [Basemaps.voyager, Basemaps.positron, Basemaps.darkmatter]:
# Basemap is a color
@@ -441,6 +441,8 @@ def _parse_html_content(
'pitch': viewport.get('pitch')
}
+ has_legends = any(source['legend'] is not None for source in sources) or default_legend
+
return self._template.render(
width=size[0] if size is not None else None,
height=size[1] if size is not None else None,
@@ -450,6 +452,8 @@ def _parse_html_content(
mapboxtoken=token,
bounds=bounds,
camera=camera,
+ has_legends=has_legends,
+ default_legend=default_legend,
carto_vl_path=_carto_vl_path,
airship_components_path=airship_components_path,
airship_bridge_path=airship_bridge_path,
diff --git a/cartoframes/viz/popup.py b/cartoframes/viz/popup.py
index d98c21f11..a640cf5b5 100644
--- a/cartoframes/viz/popup.py
+++ b/cartoframes/viz/popup.py
@@ -64,9 +64,17 @@ def _init_popup(self, data):
if isinstance(data, dict):
# TODO: error control
if 'click' in data:
- self._click = data.get('click', [])
+ click_data = data.get('click', [])
+ if isinstance(click_data, list):
+ self._click = click_data
+ else:
+ self._click = [click_data]
if 'hover' in data:
- self._hover = data.get('hover', [])
+ hover_data = data.get('hover', [])
+ if isinstance(hover_data, list):
+ self._hover = hover_data
+ else:
+ self._hover = [hover_data]
else:
raise ValueError('Wrong popup input')
diff --git a/cartoframes/viz/style.py b/cartoframes/viz/style.py
index 1471073f6..0d5b79648 100644
--- a/cartoframes/viz/style.py
+++ b/cartoframes/viz/style.py
@@ -1,7 +1,7 @@
from __future__ import absolute_import
from .defaults import STYLE_DEFAULTS, STYLE_PROPERTIES
-from ..utils import merge_dicts
+from ..utils import merge_dicts, text_match
class Style(object):
@@ -116,12 +116,12 @@ def _serialize_properties(self, properties={}):
def _prune_defaults(self, defaults, style):
output = defaults.copy()
- if 'color:' in style:
+ if text_match(r'color\s*:', style):
del output['color']
- if 'width:' in style:
+ if text_match(r'width\s*:', style):
del output['width']
- if 'strokeWidth:' in style:
- del output['strokeWidth']
- if 'strokeColor:' in style:
+ if text_match(r'strokeColor\s*:', style):
del output['strokeColor']
+ if text_match(r'strokeWidth\s*:', style):
+ del output['strokeWidth']
return output
diff --git a/examples/debug/API/basemaps.ipynb b/examples/debug/API/basemaps.ipynb
index 8a65bc4a6..c955cf490 100644
--- a/examples/debug/API/basemaps.ipynb
+++ b/examples/debug/API/basemaps.ipynb
@@ -47,10 +47,10 @@
"
\n",
" \n",
" \n",
- " \n",
- " \n",
- "
\n",
- "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
\n",
"\n",
" \n",
- " \n",
- "\n",
- " \n",
- "
\n",
- " \n",
- " \n",
- "
\n",
- "
\n",
- "
\n",
- "
\n",
- "
\n",
- "
\n",
- "
\n",
- "
\n",
- " \n",
- " \n",
- "\n",
- "
\n",
- "
There is a \n",
- " from the CARTO VL library:
\n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " \n",
- "\n",
- "
\n",
- " StackTrace
\n",
- " \n",
- " \n",
- "
\n",
- "\n",
- " \n",
- " \n",
- "