Repository: CartoDB/cartoframes
Pull request #1563 (2020-02-26 14:14:41)

Add last updates


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d15a1881..cfdfff81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,25 +5,6 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [1.0.1] - 2020-
-
-### Added
-- Add encode_data param to Layer (#1536)
-- Add WKT case in CSV example (#1545)
-- Add Data Management guide (#1547)
-
-### Changed
-- Include user_id in metrics (#1539)
-- Disable default param exclusive for isolines (#1540)
-- Raise an error when trying to visualise a multi-geom GeoDataFrame (#1541)
-- Update installation guide structure (#1549)
-- Minor examples/guides improvements (#1534, #1552)
-
-### Fixed
-- Fix legend footer in published maps (#1523)
-- Fix default legend for cluster_size_style (#1533)
-- Fix DO doc reference generation (#1550)
-
 ## [1.0.0] - 2020-01-20
 
 [1.0.0 Migration Guide](/docs/developers/migrations/1.0.0.md)
diff --git a/README.rst b/README.rst
index bc6f684b..e1ee8640 100644
--- a/README.rst
+++ b/README.rst
@@ -4,8 +4,8 @@ CARTOframes
 
 .. image:: https://travis-ci.org/CartoDB/cartoframes.svg?branch=develop
     :target: https://travis-ci.org/CartoDB/cartoframes
-.. image:: https://img.shields.io/badge/pypi-v1.0.1-orange
-    :target: https://pypi.org/project/cartoframes/1.0.1
+.. image:: https://img.shields.io/badge/pypi-v1.0.0-orange
+    :target: https://pypi.org/project/cartoframes/1.0.0
 
 A Python package for integrating `CARTO <https://carto.com/>`__ maps, analysis, and data services into data science workflows.
 
@@ -14,11 +14,11 @@ Python data analysis workflows often rely on the de facto standards `pandas <htt
 Try it Out
 ==========
 
-* Stable (1.0.1): |stable|
+* Stable (1.0.0): |stable|
 * Latest (develop branch): |develop|
 
 .. |stable| image:: https://mybinder.org/badge_logo.svg
-    :target: https://mybinder.org/v2/gh/cartodb/cartoframes/v1.0.1?filepath=examples
+    :target: https://mybinder.org/v2/gh/cartodb/cartoframes/v1.0.0?filepath=examples
 
 .. |develop| image:: https://mybinder.org/badge_logo.svg
     :target: https://mybinder.org/v2/gh/cartodb/cartoframes/develop?filepath=examples
diff --git a/binder/requirements.txt b/binder/requirements.txt
index 836b0256..b5a28724 100644
--- a/binder/requirements.txt
+++ b/binder/requirements.txt
@@ -1,4 +1,4 @@
-cartoframes==1.0.1
+cartoframes==1.0.0
 # Additional dependencies from examples
 matplotlib
 dask
diff --git a/cartoframes/_version.py b/cartoframes/_version.py
index cd7ca498..3edda78a 100644
--- a/cartoframes/_version.py
+++ b/cartoframes/_version.py
@@ -1,1 +1,1 @@
-__version__ = '1.0.1'
+__version__ = '1.0.1.dev'
diff --git a/cartoframes/assets/init.js.j2 b/cartoframes/assets/init.js.j2
index 5cc054a3..008d427f 100644
--- a/cartoframes/assets/init.js.j2
+++ b/cartoframes/assets/init.js.j2
@@ -7,6 +7,7 @@ document
     const camera = {{ camera|tojson }};
     const has_legends = '{{has_legends}}' === 'True';
     const is_static = '{{is_static}}' === 'True';
+    const layer_selector = '{{layer_selector}}' === 'True'; // TODO it will be a helper
     const layers = {{ layers|tojson }};
     const mapboxtoken = '{{mapboxtoken}}';
     const show_info = '{{show_info}}' === 'True';
@@ -18,6 +19,7 @@ document
       camera,
       has_legends,
       is_static,
+      layer_selector,
       layers,
       mapboxtoken,
       show_info
diff --git a/cartoframes/assets/init_layout.js.j2 b/cartoframes/assets/init_layout.js.j2
index 256bb624..4efb5be0 100644
--- a/cartoframes/assets/init_layout.js.j2
+++ b/cartoframes/assets/init_layout.js.j2
@@ -4,6 +4,7 @@ const is_static = '{{is_static}}' === 'True';
 
 maps.forEach((map) => {
   map.has_legends = false;
+  map.layer_selector = false;
 });
 
 init({
diff --git a/cartoframes/assets/src/bundle.js b/cartoframes/assets/src/bundle.js
index 9645409b..a54f8dfe 100644
--- a/cartoframes/assets/src/bundle.js
+++ b/cartoframes/assets/src/bundle.js
@@ -406,8 +406,9 @@ var init = (function () {
       const othersLabel = 'Others';   // TODO: i18n
       const prop = legend.prop;
       const dynamic = legend.dynamic;
+      const order = legend.ascending ? 'ASC' : 'DESC';
       const variable = legend.variable;
-      const config = { othersLabel, variable };
+      const config = { othersLabel, variable, order };
       const options = { format, config, dynamic };
 
       if (legend.type.startsWith('size-continuous')) {
@@ -588,6 +589,10 @@ var init = (function () {
       mapIndex
     );
 
+    if (settings.layer_selector) {
+      addLayersSelector(layers.reverse(), mapLayers.reverse());
+    }
+
     setInteractiveLayers(map, layers, mapLayers);
 
     return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);
@@ -623,6 +628,21 @@ var init = (function () {
     }
   }
 
+  function addLayersSelector(layers, mapLayers) {
+    const layerSelector$ = document.querySelector(`#layer-selector`);
+      const layersInfo = mapLayers.map((layer, index) => {
+        return {
+          title: layers[index].title || `Layer ${index}`,
+          id: layer.id,
+          checked: true
+        };
+      });
+    
+    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);
+    
+    layerSelector.build();
+  }
+
   function createMap(container, basemapStyle, bounds, accessToken) {
     const map = createMapboxGLMap(container, basemapStyle, accessToken);
 
diff --git a/cartoframes/assets/src/legends.js b/cartoframes/assets/src/legends.js
index d8fc805f..2b131c7b 100644
--- a/cartoframes/assets/src/legends.js
+++ b/cartoframes/assets/src/legends.js
@@ -15,8 +15,9 @@ function _createLegend(layer, legend, layerIndex, legendIndex, mapIndex=0) {
     const othersLabel = 'Others';   // TODO: i18n
     const prop = legend.prop;
     const dynamic = legend.dynamic;
+    const order = legend.ascending ? 'ASC' : 'DESC';
     const variable = legend.variable;
-    const config = { othersLabel, variable };
+    const config = { othersLabel, variable, order };
     const options = { format, config, dynamic };
 
     if (legend.type.startsWith('size-continuous')) {
diff --git a/cartoframes/assets/src/map.js b/cartoframes/assets/src/map.js
index c825dab0..0f6fe4d8 100644
--- a/cartoframes/assets/src/map.js
+++ b/cartoframes/assets/src/map.js
@@ -48,6 +48,10 @@ export function initLayers(map, settings, mapIndex) {
     mapIndex
   );
 
+  if (settings.layer_selector) {
+    addLayersSelector(layers.reverse(), mapLayers.reverse());
+  }
+
   setInteractiveLayers(map, layers, mapLayers);
 
   return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);
@@ -83,6 +87,21 @@ export function setInteractiveLayers(map, layers, mapLayers) {
   }
 }
 
+export function addLayersSelector(layers, mapLayers) {
+  const layerSelector$ = document.querySelector(`#layer-selector`);
+    const layersInfo = mapLayers.map((layer, index) => {
+      return {
+        title: layers[index].title || `Layer ${index}`,
+        id: layer.id,
+        checked: true
+      };
+    });
+  
+  const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);
+  
+  layerSelector.build();
+}
+
 export function createMap(container, basemapStyle, bounds, accessToken) {
   const map = createMapboxGLMap(container, basemapStyle, accessToken);
 
diff --git a/cartoframes/assets/templates/viz/legends.html.j2 b/cartoframes/assets/templates/viz/legends.html.j2
index f561e493..4273a2ce 100644
--- a/cartoframes/assets/templates/viz/legends.html.j2
+++ b/cartoframes/assets/templates/viz/legends.html.j2
@@ -3,30 +3,35 @@
 {%- endmacro %}
 
 <div class="as-panel__element" id="legends">
-  {% for layer in layers|reverse %}
-    {% if layer.legends %}
-    {% set layer_index = loop.index0 %}
-      {% if layer.has_legend_list %}
-        {% for legend in layer.legends %}
-          <as-legend
-            heading="{{legend.title}}"
-            description="{{legend.description}}">
-            {{ createLegend(legend, 'layer%d_map%d_legend%d' | format(layer_index, 0, loop.index0)) }}
-            {% if legend.footer %}
-              <span slot="footer">{{legend.footer | safe }}</span>
-            {% endif %}
-          </as-legend>
-        {% endfor %}
-      {% else %}
-        <as-legend
-          heading="{{layer.legends.title}}"
-          description="{{layer.legends.description}}">
-          {{ createLegend(layer.legends, 'layer%d_map%d_legend%d' | format(layer_index, 0, 0)) }}
-          {% if layer.legends.footer %}
-            <span slot="footer">{{layer.legends.footer | safe }}</span>
+  <as-layer-selector id="layer-selector">
+    {% for layer in layers|reverse %}
+      {% if layer.legends %}
+        {% set layer_index = loop.index0 %}
+        {% set slot = 'as-checkbox-layer-%d-slot' | format(layer_index) %}
+        <div slot="{{slot}}">
+          {% if layer.has_legend_list %}
+            {% for legend in layer.legends %}
+              <as-legend
+                heading="{{legend.title}}"
+                description="{{legend.description}}">
+                {{ createLegend(legend, 'layer%d_map%d_legend%d' | format(layer_index, 0, loop.index0)) }}
+                {% if legend.footer %}
+                  <span slot="footer">{{legend.footer | safe }}</span>
+                {% endif %}
+              </as-legend>
+            {% endfor %}
+          {% else %}
+            <as-legend
+              heading="{{layer.legends.title}}"
+              description="{{layer.legends.description}}">
+              {{ createLegend(layer.legends, 'layer%d_map%d_legend%d' | format(layer_index, 0, 0)) }}
+              {% if layer.legends.footer %}
+                <span slot="footer">{{layer.legends.footer | safe }}</span>
+              {% endif %}
+            </as-legend>
           {% endif %}
-        </as-legend>
+        </div>
       {% endif %}
-    {% endif %}
-  {% endfor %}
+    {% endfor %}
+  </as-layer-selector>
 </div>
\ No newline at end of file
diff --git a/cartoframes/assets/templates/viz/main.html.j2 b/cartoframes/assets/templates/viz/main.html.j2
index 927a3dac..137db916 100644
--- a/cartoframes/assets/templates/viz/main.html.j2
+++ b/cartoframes/assets/templates/viz/main.html.j2
@@ -53,9 +53,9 @@
         {% if show_info %}
           {% include 'templates/viz/info.html.j2' %}
         {% endif %}
-        {% if has_legends %}
+        {% if has_legends or layer_selector%}
           <div class="as-map-panels" data-name="Legends">
-            <div class="as-panel as-panel--left as-panel--top">
+            <div class="as-panel as-panel--vertical as-panel--left as-panel--top">
               {% include 'templates/viz/legends.html.j2' %}
             </div> <!-- as-panel -->
           </div> <!-- as-map-panels -->
diff --git a/cartoframes/viz/constants.py b/cartoframes/viz/constants.py
index 921cca77..17f1ad4d 100644
--- a/cartoframes/viz/constants.py
+++ b/cartoframes/viz/constants.py
@@ -2,7 +2,7 @@ CARTO_VL_VERSION = 'v1.4'
 CARTO_VL_DEV = '/dist/carto-vl.js'
 CARTO_VL_URL = 'https://libs.cartocdn.com/carto-vl/{}/carto-vl.min.js'.format(CARTO_VL_VERSION)
 
-AIRSHIP_VERSION = 'v2.2'
+AIRSHIP_VERSION = 'v2.3'
 AIRSHIP_COMPONENTS_DEV = '/packages/components/dist/airship.js'
 AIRSHIP_BRIDGE_DEV = '/packages/bridge/dist/asbridge.js'
 AIRSHIP_MODULE_DEV = '/packages/components/dist/airship/airship.esm.js'
diff --git a/cartoframes/viz/html/html_map.py b/cartoframes/viz/html/html_map.py
index 3e3efde7..27ce4372 100644
--- a/cartoframes/viz/html/html_map.py
+++ b/cartoframes/viz/html/html_map.py
@@ -28,18 +28,18 @@ class HTMLMap(object):
             self, size, layers, bounds, camera=None, basemap=None, show_info=None,
             theme=None, _carto_vl_path=None,
             _airship_path=None, title='CARTOframes', description=None,
-            is_embed=False, is_static=False):
+            is_embed=False, is_static=False, layer_selector=False):
 
         self.html = self._parse_html_content(
             size, layers, bounds, camera, basemap,
             show_info, theme, _carto_vl_path, _airship_path, title, description,
-            is_embed, is_static)
+            is_embed, is_static, layer_selector)
 
     def _parse_html_content(
             self, size, layers, bounds, camera=None,
             basemap=None, show_info=None,
             theme=None, _carto_vl_path=None, _airship_path=None,
-            title=None, description=None, is_embed=False, is_static=False):
+            title=None, description=None, is_embed=False, is_static=False, layer_selector=False):
 
         token = ''
         basecolor = ''
@@ -107,7 +107,8 @@ class HTMLMap(object):
             title=title,
             description=description,
             is_embed=is_embed,
-            is_static=is_static
+            is_static=is_static,
+            layer_selector=layer_selector
         )
 
     def _repr_html_(self):
diff --git a/cartoframes/viz/kuviz.py b/cartoframes/viz/kuviz.py
index d46c5695..bd2c0bb9 100644
--- a/cartoframes/viz/kuviz.py
+++ b/cartoframes/viz/kuviz.py
@@ -80,8 +80,8 @@ class KuvizPublisher:
         if len(private_sources) > 0:
             maps_api_key, private_tables_names = self._auth_api_client.create_api_key(private_sources, ['maps'])
             log.info(
-                'Publishing a map that uses private datasets requires a Maps API key. '
-                'The "{0}" API key is being used for these datasets {1}. '
+                'The map has been published. '
+                'The "{0}" Maps API key is being used for these datasets {1}. '
                 'You can manage your API keys on your account'.format(maps_api_key, private_tables_names))
             return maps_api_key
 
diff --git a/cartoframes/viz/layer.py b/cartoframes/viz/layer.py
index c0d7c535..c8cc3788 100644
--- a/cartoframes/viz/layer.py
+++ b/cartoframes/viz/layer.py
@@ -104,21 +104,25 @@ class Layer:
                  default_popup_hover=True,
                  default_popup_click=False,
                  title=None,
+                 parent_map=None,
                  encode_data=True):
 
         self.is_basemap = False
+        self.default_legend = default_legend
         self.source = _set_source(source, credentials, geom_col, encode_data)
         self.style = _set_style(style)
         self.encode_data = encode_data
+        self.parent_map = None
         self.popups = self._init_popups(
             popup_hover, popup_click, default_popup_hover, default_popup_click, title)
         self.legends = self._init_legends(legends, default_legend, title)
         self.widgets = self._init_widgets(widgets, default_widget, title)
-
+        self.title = title
         geom_type = self.source.get_geom_type()
         popups_variables = self.popups.get_variables()
         widget_variables = self.widgets.get_variables()
         external_variables = merge_dicts(popups_variables, widget_variables)
+
         self.viz = self.style.compute_viz(geom_type, external_variables)
         viz_columns = extract_viz_columns(self.viz)
 
@@ -186,6 +190,12 @@ class Layer:
         from .map import Map
         return Map(self)._repr_html_()
 
+    def reset_legends(self, parent_map):
+        if parent_map.layer_selector:
+            self.style.default_legend.set_title('')
+            self.legends = self._init_legends(self.legends, self.default_legend, '')
+            self.legends_info = self.legends.get_info() if self.legends is not None else None
+
 
 def _set_source(source, credentials, geom_col, encode_data):
     if isinstance(source, (str, pandas.DataFrame)):
diff --git a/cartoframes/viz/legend.py b/cartoframes/viz/legend.py
index fab86b38..a341cc7f 100644
--- a/cartoframes/viz/legend.py
+++ b/cartoframes/viz/legend.py
@@ -15,7 +15,7 @@ class Legend:
 
     """
     def __init__(self, legend_type=None, title=None, description=None,
-                 footer=None, prop=None, variable='', dynamic=True):
+                 footer=None, prop=None, variable='', dynamic=True, ascending=False):
         self._check_type(legend_type)
         self._check_prop(prop)
         self._type = legend_type
@@ -25,6 +25,7 @@ class Legend:
         self._prop = prop
         self._variable = variable
         self._dynamic = dynamic
+        self._ascending = ascending
 
     def add_defaults(self, title=None):
         self._title = self._title or title
@@ -42,6 +43,7 @@ class Legend:
                 'prop': _prop,
                 'variable': self._variable,
                 'dynamic': self._dynamic,
+                'ascending': self._ascending,
                 'title': self._title or '',
                 'description': self._description or '',
                 'footer': self._footer or ''
diff --git a/cartoframes/viz/legends/color_bins_legend.py b/cartoframes/viz/legends/color_bins_legend.py
index 0c8253d4..7a68c0a7 100644
--- a/cartoframes/viz/legends/color_bins_legend.py
+++ b/cartoframes/viz/legends/color_bins_legend.py
@@ -2,7 +2,7 @@ from ..legend import Legend
 
 
 def color_bins_legend(title=None, description=None, footer=None, prop='color',
-                      variable=None, dynamic=True):
+                      variable=None, dynamic=True, ascending=False):
     """Helper function for quickly creating a color bins legend.
 
     Args:
@@ -21,6 +21,9 @@ def color_bins_legend(title=None, description=None, footer=None, prop='color',
         dynamic (boolean, optional):
             Update and render the legend depending on viewport changes.
             Defaults to ``True``.
+        ascending (boolean, optional):
+            If set to ``True`` the values are sorted in ascending order.
+            Defaults to ``False``.
 
     Returns:
         cartoframes.viz.legend.Legend
@@ -33,4 +36,4 @@ def color_bins_legend(title=None, description=None, footer=None, prop='color',
         ...     dynamic=False)
 
     """
-    return Legend('color-bins', title, description, footer, prop, variable, dynamic)
+    return Legend('color-bins', title, description, footer, prop, variable, dynamic, ascending)
diff --git a/cartoframes/viz/legends/color_continuous_legend.py b/cartoframes/viz/legends/color_continuous_legend.py
index 3727cee2..93ccbe30 100644
--- a/cartoframes/viz/legends/color_continuous_legend.py
+++ b/cartoframes/viz/legends/color_continuous_legend.py
@@ -2,7 +2,7 @@ from ..legend import Legend
 
 
 def color_continuous_legend(title=None, description=None, footer=None, prop='color',
-                            variable=None, dynamic=True):
+                            variable=None, dynamic=True, ascending=False):
     """Helper function for quickly creating a color continuous legend.
 
     Args:
@@ -21,6 +21,9 @@ def color_continuous_legend(title=None, description=None, footer=None, prop='col
         dynamic (boolean, optional):
             Update and render the legend depending on viewport changes.
             Defaults to ``True``.
+        ascending (boolean, optional):
+            If set to ``True`` the values are sorted in ascending order.
+            Defaults to ``False``.
 
     Returns:
         cartoframes.viz.legend.Legend
@@ -33,4 +36,4 @@ def color_continuous_legend(title=None, description=None, footer=None, prop='col
         ...     dynamic=False)
 
     """
-    return Legend('color-continuous', title, description, footer, prop, variable, dynamic)
+    return Legend('color-continuous', title, description, footer, prop, variable, dynamic, ascending)
diff --git a/cartoframes/viz/legends/size_bins_legend.py b/cartoframes/viz/legends/size_bins_legend.py
index f293df21..7a2d4c1b 100644
--- a/cartoframes/viz/legends/size_bins_legend.py
+++ b/cartoframes/viz/legends/size_bins_legend.py
@@ -2,7 +2,7 @@ from ..legend import Legend
 
 
 def size_bins_legend(title=None, description=None, footer=None, prop='size',
-                     variable=None, dynamic=True):
+                     variable=None, dynamic=True, ascending=False):
     """Helper function for quickly creating a size bins legend.
 
     Args:
@@ -21,6 +21,9 @@ def size_bins_legend(title=None, description=None, footer=None, prop='size',
         dynamic (boolean, optional):
             Update and render the legend depending on viewport changes.
             Defaults to ``True``.
+        ascending (boolean, optional):
+            If set to ``True`` the values are sorted in ascending order.
+            Defaults to ``False``.
 
     Returns:
         cartoframes.viz.legend.Legend
@@ -33,4 +36,4 @@ def size_bins_legend(title=None, description=None, footer=None, prop='size',
         ...     dynamic=False)
 
     """
-    return Legend('size-bins', title, description, footer, prop, variable, dynamic)
+    return Legend('size-bins', title, description, footer, prop, variable, dynamic, ascending)
diff --git a/cartoframes/viz/legends/size_category_legend.py b/cartoframes/viz/legends/size_category_legend.py
index 9d437536..613eab47 100644
--- a/cartoframes/viz/legends/size_category_legend.py
+++ b/cartoframes/viz/legends/size_category_legend.py
@@ -22,7 +22,6 @@ def size_category_legend(title=None, description=None, footer=None, prop='size',
             Update and render the legend depending on viewport changes.
             Defaults to ``True``.
 
-
     Returns:
         cartoframes.viz.legend.Legend
 
diff --git a/cartoframes/viz/legends/size_continuous_legend.py b/cartoframes/viz/legends/size_continuous_legend.py
index 35086d63..25aa58e1 100644
--- a/cartoframes/viz/legends/size_continuous_legend.py
+++ b/cartoframes/viz/legends/size_continuous_legend.py
@@ -2,7 +2,7 @@ from ..legend import Legend
 
 
 def size_continuous_legend(title=None, description=None, footer=None, prop='size',
-                           variable='size_value', dynamic=True):
+                           variable='size_value', dynamic=True, ascending=False):
     """Helper function for quickly creating a size continuous legend.
 
     Args:
@@ -20,6 +20,9 @@ def size_continuous_legend(title=None, description=None, footer=None, prop='size
             If the information in the legend depends on a different value than the
             information set to the style property, it is possible to set an independent
             variable.
+        ascending (boolean, optional):
+            If set to ``True`` the values are sorted in ascending order.
+            Defaults to ``False``.
 
     Returns:
         cartoframes.viz.legend.Legend
@@ -32,4 +35,4 @@ def size_continuous_legend(title=None, description=None, footer=None, prop='size
         ...     dynamic=False)
 
     """
-    return Legend('size-continuous', title, description, footer, prop, variable, dynamic)
+    return Legend('size-continuous', title, description, footer, prop, variable, dynamic, ascending)
diff --git a/cartoframes/viz/map.py b/cartoframes/viz/map.py
index 827157a1..a9023bfa 100644
--- a/cartoframes/viz/map.py
+++ b/cartoframes/viz/map.py
@@ -106,19 +106,22 @@ class Map:
                  title=None,
                  description=None,
                  is_static=None,
+                 layer_selector=False,
                  **kwargs):
 
-        self.layers = _init_layers(layers)
+        self.layer_selector = layer_selector
         self.basemap = basemap
         self.size = size
         self.viewport = viewport
         self.title = title
         self.description = description
         self.show_info = show_info
+        self.layers = _init_layers(layers, self)
         self.layer_defs = _get_layer_defs(self.layers)
         self.bounds = _get_bounds(bounds, self.layers)
         self.theme = _get_theme(theme, basemap)
         self.is_static = is_static
+
         self.token = get_token(basemap)
         self.basecolor = get_basecolor(basemap)
 
@@ -152,6 +155,7 @@ class Map:
             title=self.title,
             description=self.description,
             is_static=self.is_static,
+            layer_selector=self.layer_selector,
             _carto_vl_path=self._carto_vl_path,
             _airship_path=self._airship_path)
 
@@ -177,6 +181,7 @@ class Map:
             'title': self.title,
             'description': self.description,
             'is_static': self.is_static,
+            'layer_selector': self.layer_selector,
             '_carto_vl_path': self._carto_vl_path,
             '_airship_path': self._airship_path
         }
@@ -279,12 +284,15 @@ def _get_bounds(bounds, layers):
         return _compute_bounds(layers)
 
 
-def _init_layers(layers):
+def _init_layers(layers, parent_map):
     if layers is None:
         return []
     if not isinstance(layers, collections.Iterable):
+        layers.reset_legends(parent_map)
         return [layers]
     else:
+        for layer in layers:
+            layer.reset_legends(parent_map)
         return layers
 
 
@@ -304,6 +312,7 @@ def _get_layer_def(layer):
         'widgets': layer.widgets_info,
         'data': layer.source_data,
         'type': layer.source_type,
+        'title': layer.title,
         'options': layer.options,
         'viz': layer.viz
     }
diff --git a/examples/layers/add_multiple_layers.ipynb b/examples/layers/add_multiple_layers.ipynb
index c6b0e3c7..e72b4f98 100644
--- a/examples/layers/add_multiple_layers.ipynb
+++ b/examples/layers/add_multiple_layers.ipynb
@@ -54,11 +54,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2.0-rc.2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2.0-rc.2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2.0-rc.2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -228,8 +228,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -367,9 +365,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -655,8 +652,9 @@
        "      const othersLabel = 'Others';   // TODO: i18n\n",
        "      const prop = legend.prop;\n",
        "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
        "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
+       "      const config = { othersLabel, variable, order };\n",
        "      const options = { format, config, dynamic };\n",
        "\n",
        "      if (legend.type.startsWith('size-continuous')) {\n",
@@ -677,7 +675,7 @@
        "\n",
        "  function GeoJSON(layer) {\n",
        "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
        "\n",
        "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
@@ -699,8 +697,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -825,6 +835,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -860,6 +874,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -901,7 +930,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;countries\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#826DBA\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;global_power_plants\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[2,10])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;world_rivers\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#4CC8A3\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[0.5,4])\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;countries\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#826DBA\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;global_power_plants\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[2,10])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;world_rivers\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#4CC8A3\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[0.5,4])\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -912,6 +942,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -924,7 +955,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x11368aad0>"
+       "<cartoframes.viz.map.Map at 0x115937c10>"
       ]
      },
      "execution_count": 2,
@@ -939,6 +970,984 @@
     "    Layer('world_rivers')\n",
     "])"
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Adding a Layer Selector"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<iframe\n",
+       "  frameborder=\"0\"\n",
+       "  style=\"\n",
+       "    border: 1px solid #cfcfcf;\n",
+       "    width: 100%;\n",
+       "    height: 632px;\n",
+       "    \"\n",
+       "  srcDoc=\"\n",
+       "  <!DOCTYPE html>\n",
+       "<html lang=&quot;en&quot;>\n",
+       "<head>\n",
+       "  <title>None</title>\n",
+       "  <meta name=&quot;description&quot; content=&quot;None&quot;>\n",
+       "  <meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;>\n",
+       "  <meta charset=&quot;UTF-8&quot;>\n",
+       "  <!-- Include CARTO VL JS -->\n",
+       "  <script src=&quot;https://libs.cartocdn.com/carto-vl/v1.4/carto-vl.min.js&quot;></script>\n",
+       "  <!-- Include Mapbox GL JS -->\n",
+       "  <script src=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.js&quot;></script>\n",
+       "  <!-- Include Mapbox GL CSS -->\n",
+       "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
+       "\n",
+       "  <!-- Include Airship -->\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "\n",
+       "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
+       "\n",
+       "  <!-- External libraries -->\n",
+       "\n",
+       "  <!-- pako -->\n",
+       "  <script src=&quot;https://libs.cartocdn.com/cartoframes/dependencies/pako_inflate.min.js&quot;></script>\n",
+       "  \n",
+       "  <!-- html2canvas -->\n",
+       "  \n",
+       "\n",
+       "  \n",
+       "  <style>\n",
+       "  body {\n",
+       "    margin: 0;\n",
+       "    padding: 0;\n",
+       "  }\n",
+       "\n",
+       "  aside.as-sidebar {\n",
+       "    min-width: 300px;\n",
+       "  }\n",
+       "\n",
+       "  .map-image {\n",
+       "    display: none;\n",
+       "    max-width: 100%;\n",
+       "    height: auto;\n",
+       "  }\n",
+       "</style>\n",
+       "  <style>\n",
+       "  .map {\n",
+       "    position: absolute;\n",
+       "    height: 100%;\n",
+       "    width: 100%;\n",
+       "  }\n",
+       "\n",
+       "  .map-info {\n",
+       "    position: absolute;\n",
+       "    bottom: 0;\n",
+       "    padding: 0 5px;\n",
+       "    background-color: rgba(255, 255, 255, 0.5);\n",
+       "    margin: 0;\n",
+       "    color: rgba(0, 0, 0, 0.75);\n",
+       "    font-size: 12px;\n",
+       "    width: auto;\n",
+       "    height: 18px;\n",
+       "    font-family: 'Open Sans';\n",
+       "  }\n",
+       "\n",
+       "  .map-footer {\n",
+       "    background: #F2F6F9;\n",
+       "    font-family: Roboto;\n",
+       "    font-size: 12px;\n",
+       "    line-height: 24px;\n",
+       "    color: #162945;\n",
+       "    text-align: center;\n",
+       "    z-index: 2;\n",
+       "  }\n",
+       "\n",
+       "  .map-footer a {\n",
+       "    text-decoration: none;\n",
+       "  }\n",
+       "\n",
+       "  .map-footer a:hover {\n",
+       "    text-decoration: underline;\n",
+       "  }\n",
+       "</style>\n",
+       "    <style>\n",
+       "    #error-container {\n",
+       "      position: absolute;\n",
+       "      width: 100%;\n",
+       "      height: 100%;\n",
+       "      background-color: white;\n",
+       "      visibility: hidden;\n",
+       "      padding: 1em;\n",
+       "      font-family: &quot;Courier New&quot;, Courier, monospace;\n",
+       "      margin: 0 auto;\n",
+       "      font-size: 14px;\n",
+       "      overflow: auto;\n",
+       "      z-index: 1000;\n",
+       "      color: black;\n",
+       "    }\n",
+       "\n",
+       "    .error-section {\n",
+       "      padding: 1em;\n",
+       "      border-radius: 5px;\n",
+       "      background-color: #fee;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-highlight {\n",
+       "      font-weight: bold;\n",
+       "      color: inherit;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-type {\n",
+       "      color: #008000;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-name {\n",
+       "      color: #ba2121;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-content {\n",
+       "      margin-top: 0.4em;\n",
+       "    }\n",
+       "\n",
+       "    .error-details {\n",
+       "      margin-top: 1em;\n",
+       "    }\n",
+       "\n",
+       "    #error-stacktrace {\n",
+       "      list-style: none;\n",
+       "    }\n",
+       "</style>\n",
+       "  <style>\n",
+       "    .popup-content {\n",
+       "      display: flex;\n",
+       "      flex-direction: column;\n",
+       "      padding: 8px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-name {\n",
+       "      font-size: 12px;\n",
+       "      font-weight: 400;\n",
+       "      line-height: 20px;\n",
+       "      margin-bottom: 4px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-value {\n",
+       "      font-size: 16px;\n",
+       "      font-weight: 600;\n",
+       "      line-height: 20px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-value:not(:last-of-type) {\n",
+       "      margin-bottom: 16px;\n",
+       "    }\n",
+       "</style>\n",
+       "  <style>\n",
+       "  as-widget-header .as-widget-header__header {\n",
+       "    margin-bottom: 8px;\n",
+       "    overflow-wrap: break-word;\n",
+       "  }\n",
+       "\n",
+       "  as-widget-header .as-widget-header__subheader {\n",
+       "    margin-bottom: 12px;\n",
+       "  }\n",
+       "\n",
+       "  as-category-widget {\n",
+       "    max-height: 250px;\n",
+       "  }\n",
+       "</style>\n",
+       "</head>\n",
+       "\n",
+       "<body class=&quot;as-app-body as-app&quot;>\n",
+       "  <img id=&quot;map-image&quot; class=&quot;map-image&quot; alt='Static map image' />\n",
+       "  <as-responsive-content id=&quot;main-container&quot;>\n",
+       "    \n",
+       "    <main class=&quot;as-main&quot;>\n",
+       "      <div class=&quot;as-map-area&quot;>\n",
+       "        <div id=&quot;map&quot; class=&quot;map&quot;></div>\n",
+       "        \n",
+       "        \n",
+       "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
+       "              \n",
+       "\n",
+       "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
+       "    \n",
+       "      \n",
+       "    \n",
+       "      \n",
+       "    \n",
+       "      \n",
+       "    \n",
+       "  </as-layer-selector>\n",
+       "</div>\n",
+       "            </div> <!-- as-panel -->\n",
+       "          </div> <!-- as-map-panels -->\n",
+       "        \n",
+       "      </div> <!-- as-map-area -->\n",
+       "    </main> <!-- as-main -->\n",
+       "  </as-responsive-content>\n",
+       "\n",
+       "  \n",
+       "\n",
+       "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
+       "  <section class=&quot;error-section&quot;>\n",
+       "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
+       "    <section id=&quot;error-content&quot;>\n",
+       "      <span class=&quot;errors&quot; id=&quot;error-type&quot;></span>\n",
+       "      <span class=&quot;errors&quot; id=&quot;error-message&quot;></span>\n",
+       "    </section>\n",
+       "  </section>\n",
+       "\n",
+       "  <details class=&quot;error-details&quot;>\n",
+       "    <summary>StackTrace</summary>\n",
+       "    <ul id=&quot;error-stacktrace&quot;></ul>\n",
+       "  </details>\n",
+       "</div>\n",
+       "</body>\n",
+       "\n",
+       "<script>\n",
+       "  var init = (function () {\n",
+       "  'use strict';\n",
+       "\n",
+       "  const BASEMAPS = {\n",
+       "    DarkMatter: carto.basemaps.darkmatter,\n",
+       "    Voyager: carto.basemaps.voyager,\n",
+       "    Positron: carto.basemaps.positron\n",
+       "  };\n",
+       "\n",
+       "  const attributionControl = new mapboxgl.AttributionControl({\n",
+       "    compact: false\n",
+       "  });\n",
+       "\n",
+       "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\n",
+       "\n",
+       "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
+       "\n",
+       "  /**\n",
+       "   * This parses the different stack traces and puts them into one format\n",
+       "   * This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)\n",
+       "   */\n",
+       "\n",
+       "  const UNKNOWN_FUNCTION = '<unknown>';\n",
+       "  const chromeRe = /^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\\/).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$/i;\n",
+       "  const chromeEvalRe = /\\((\\S*)(?::(\\d+))(?::(\\d+))\\)/;\n",
+       "  const winjsRe = /^\\s*at (?:((?:\\[object object\\])?.+) )?\\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\\d+)(?::(\\d+))?\\)?\\s*$/i;\n",
+       "  const geckoRe = /^\\s*(.*?)(?:\\((.*?)\\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\\[native).*?|[^@]*bundle)(?::(\\d+))?(?::(\\d+))?\\s*$/i;\n",
+       "  const geckoEvalRe = /(\\S+) line (\\d+)(?: > eval line \\d+)* > eval/i;\n",
+       "\n",
+       "  function parse(stackString) {\n",
+       "    const lines = stackString.split('\\n');\n",
+       "\n",
+       "    return lines.reduce((stack, line) => {\n",
+       "      const parseResult =\n",
+       "        parseChrome(line) ||\n",
+       "        parseWinjs(line) ||\n",
+       "        parseGecko(line);\n",
+       "\n",
+       "      if (parseResult) {\n",
+       "        stack.push(parseResult);\n",
+       "      }\n",
+       "\n",
+       "      return stack;\n",
+       "    }, []);\n",
+       "  }\n",
+       "\n",
+       "  function parseChrome(line) {\n",
+       "    const parts = chromeRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line\n",
+       "    const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line\n",
+       "\n",
+       "    const submatch = chromeEvalRe.exec(parts[2]);\n",
+       "    if (isEval && submatch != null) {\n",
+       "      // throw out eval line/column and use top-most line/column number\n",
+       "      parts[2] = submatch[1]; // url\n",
+       "      parts[3] = submatch[2]; // line\n",
+       "      parts[4] = submatch[3]; // column\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: !isNative ? parts[2] : null,\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: isNative ? [parts[2]] : [],\n",
+       "      lineNumber: parts[3] ? +parts[3] : null,\n",
+       "      column: parts[4] ? +parts[4] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function parseWinjs(line) {\n",
+       "    const parts = winjsRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: parts[2],\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: [],\n",
+       "      lineNumber: +parts[3],\n",
+       "      column: parts[4] ? +parts[4] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function parseGecko(line) {\n",
+       "    const parts = geckoRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;\n",
+       "\n",
+       "    const submatch = geckoEvalRe.exec(parts[3]);\n",
+       "    if (isEval && submatch != null) {\n",
+       "      // throw out eval line/column and use top-most line number\n",
+       "      parts[3] = submatch[1];\n",
+       "      parts[4] = submatch[2];\n",
+       "      parts[5] = null; // no column when eval\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: parts[3],\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: parts[2] ? parts[2].split(',') : [],\n",
+       "      lineNumber: parts[4] ? +parts[4] : null,\n",
+       "      column: parts[5] ? +parts[5] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function displayError(e) {\n",
+       "    const error$ = document.getElementById('error-container');\n",
+       "    const errors$ = error$.getElementsByClassName('errors');\n",
+       "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
+       "\n",
+       "    errors$[0].innerHTML = e.name;\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
+       "\n",
+       "    error$.style.visibility = 'visible';\n",
+       "\n",
+       "    const stack = parse(e.stack);\n",
+       "    const list = stack.map(item => {\n",
+       "      return `<li>\n",
+       "      at <span class=&quot;stacktrace-method&quot;>${item.methodName}:</span>\n",
+       "      (${item.file}:${item.lineNumber}:${item.column})\n",
+       "    </li>`;\n",
+       "    });\n",
+       "\n",
+       "    stacktrace$.innerHTML = list.join('\\n');\n",
+       "  }\n",
+       "\n",
+       "  function format(value) {\n",
+       "    if (Array.isArray(value)) {\n",
+       "      const [first, second] = value;\n",
+       "      if (first === -Infinity) {\n",
+       "        return `< ${formatValue(second)}`;\n",
+       "      }\n",
+       "      if (second === Infinity) {\n",
+       "        return `> ${formatValue(first)}`;\n",
+       "      }\n",
+       "      return `${formatValue(first)} - ${formatValue(second)}`;\n",
+       "    }\n",
+       "    return formatValue(value);\n",
+       "  }\n",
+       "\n",
+       "  function formatValue(value) {\n",
+       "    if (typeof value === 'number') {\n",
+       "      return formatNumber(value);\n",
+       "    }\n",
+       "    return value;\n",
+       "  }\n",
+       "\n",
+       "  function formatNumber(value) {\n",
+       "    const log = Math.log10(Math.abs(value));\n",
+       "\n",
+       "    if ((log > 4 || log < -2.00000001) && value) {\n",
+       "      return value.toExponential(2);\n",
+       "    }\n",
+       "    \n",
+       "    if (!Number.isInteger(value)) {\n",
+       "      return value.toLocaleString(undefined, {\n",
+       "        minimumFractionDigits: 2,\n",
+       "        maximumFractionDigits: 3\n",
+       "      });\n",
+       "    }\n",
+       "    \n",
+       "    return value.toLocaleString();\n",
+       "  }\n",
+       "\n",
+       "  function updateViewport(map) {\n",
+       "    function updateMapInfo() {\n",
+       "      const mapInfo$ = document.getElementById('map-info');\n",
+       "      const center = map.getCenter();\n",
+       "      const lat = center.lat.toFixed(6);\n",
+       "      const lng = center.lng.toFixed(6);\n",
+       "      const zoom = map.getZoom().toFixed(2);\n",
+       "    \n",
+       "      mapInfo$.innerText = `viewport={'zoom': ${zoom}, 'lat': ${lat}, 'lng': ${lng}}`;\n",
+       "    }\n",
+       "\n",
+       "    updateMapInfo();\n",
+       "\n",
+       "    map.on('zoom', updateMapInfo);\n",
+       "    map.on('move', updateMapInfo); \n",
+       "  }\n",
+       "\n",
+       "  function getBasecolorSettings(basecolor) {\n",
+       "    return {\n",
+       "      'version': 8,\n",
+       "      'sources': {},\n",
+       "      'layers': [{\n",
+       "          'id': 'background',\n",
+       "          'type': 'background',\n",
+       "          'paint': {\n",
+       "              'background-color': basecolor\n",
+       "          }\n",
+       "      }]\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function getImageElement(mapIndex) {\n",
+       "    const id = mapIndex !== undefined ? `map-image-${mapIndex}` : 'map-image';\n",
+       "    return document.getElementById(id);\n",
+       "  }\n",
+       "\n",
+       "  function getContainerElement(mapIndex) {\n",
+       "    const id = mapIndex !== undefined ? `main-container-${mapIndex}` : 'main-container';\n",
+       "    return document.getElementById(id);\n",
+       "  }\n",
+       "\n",
+       "  function saveImage(mapIndex) {\n",
+       "    const img = getImageElement(mapIndex);\n",
+       "    const container = getContainerElement(mapIndex);\n",
+       "\n",
+       "    html2canvas(container)\n",
+       "      .then((canvas) => setMapImage(canvas, img, container));\n",
+       "  }\n",
+       "\n",
+       "  function setMapImage(canvas, img, container) {\n",
+       "    const src = canvas.toDataURL();\n",
+       "    img.setAttribute('src', src);\n",
+       "    img.style.display = 'block';\n",
+       "    container.style.display = 'none';\n",
+       "  }\n",
+       "\n",
+       "  function resetPopupClick(interactivity) {\n",
+       "    interactivity.off('featureClick');\n",
+       "  }\n",
+       "\n",
+       "  function resetPopupHover(interactivity) {\n",
+       "    interactivity.off('featureHover');\n",
+       "  }\n",
+       "\n",
+       "  function setPopupsClick(map, clickPopup, hoverPopup, interactivity, attrs) {\n",
+       "    interactivity.on('featureClick', (event) => {\n",
+       "      updatePopup(map, clickPopup, event, attrs);\n",
+       "      hoverPopup.remove();\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function setPopupsHover(map, hoverPopup, interactivity, attrs) {\n",
+       "    interactivity.on('featureHover', (event) => {\n",
+       "      updatePopup(map, hoverPopup, event, attrs);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function updatePopup(map, popup, event, attrs) {\n",
+       "    if (event.features.length > 0) {\n",
+       "      let popupHTML = '';\n",
+       "      const layerIDs = [];\n",
+       "\n",
+       "      for (const feature of event.features) {\n",
+       "        if (layerIDs.includes(feature.layerId)) {\n",
+       "          continue;\n",
+       "        }\n",
+       "        // Track layers to add only one feature per layer\n",
+       "        layerIDs.push(feature.layerId);\n",
+       "    \n",
+       "        for (const item of attrs) {\n",
+       "          const variable = feature.variables[item.name];\n",
+       "          if (variable) {\n",
+       "            let value = variable.value;\n",
+       "            value = formatValue(value);\n",
+       "\n",
+       "            popupHTML = `\n",
+       "            <span class=&quot;popup-name&quot;>${item.title}</span>\n",
+       "            <span class=&quot;popup-value&quot;>${value}</span>\n",
+       "          ` + popupHTML;\n",
+       "          }\n",
+       "        }\n",
+       "      }\n",
+       "\n",
+       "      popup\n",
+       "          .setLngLat([event.coordinates.lng, event.coordinates.lat])\n",
+       "          .setHTML(`<div class=&quot;popup-content&quot;>${popupHTML}</div>`);\n",
+       "\n",
+       "      if (!popup.isOpen()) {\n",
+       "        popup.addTo(map);\n",
+       "      }\n",
+       "    } else {\n",
+       "      popup.remove();\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function setInteractivity(map, interactiveLayers, interactiveMapLayers) {\n",
+       "    const interactivity = new carto.Interactivity(interactiveMapLayers);\n",
+       "\n",
+       "    const clickPopup = new mapboxgl.Popup({\n",
+       "      closeButton: true,\n",
+       "      closeOnClick: false\n",
+       "    });\n",
+       "\n",
+       "    const hoverPopup = new mapboxgl.Popup({\n",
+       "      closeButton: false,\n",
+       "      closeOnClick: false\n",
+       "    });\n",
+       "\n",
+       "    const { clickAttrs, hoverAttrs } = _setInteractivityAttrs(interactiveLayers);\n",
+       "\n",
+       "    resetPopupClick(map);\n",
+       "    resetPopupHover(map);\n",
+       "\n",
+       "    if (clickAttrs.length > 0) {\n",
+       "      setPopupsClick(map, clickPopup, hoverPopup, interactivity, clickAttrs);\n",
+       "    }\n",
+       "\n",
+       "    if (hoverAttrs.length > 0) {\n",
+       "      setPopupsHover(map, hoverPopup, interactivity, hoverAttrs);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function _setInteractivityAttrs(interactiveLayers) {\n",
+       "    let clickAttrs = [];\n",
+       "    let hoverAttrs = [];\n",
+       "\n",
+       "    interactiveLayers.forEach((interactiveLayer) => {\n",
+       "      interactiveLayer.interactivity.forEach((interactivityDef) => {\n",
+       "        if (interactivityDef.event === 'click') {\n",
+       "          clickAttrs = clickAttrs.concat(interactivityDef.attrs);\n",
+       "        } else if (interactivityDef.event === 'hover') {\n",
+       "          hoverAttrs = hoverAttrs.concat(interactivityDef.attrs);\n",
+       "        }\n",
+       "      });\n",
+       "    });\n",
+       "\n",
+       "    return { clickAttrs, hoverAttrs };\n",
+       "  }\n",
+       "\n",
+       "  function renderWidget(widget, value) {\n",
+       "    widget.element = widget.element || document.querySelector(`#${widget.id}-value`);\n",
+       "\n",
+       "    if (value && widget.element) {\n",
+       "      widget.element.innerText = typeof value === 'number' ? format(value) : value;\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function renderBridge(bridge, widget, mapLayer) {\n",
+       "    widget.element = widget.element || document.querySelector(`#${widget.id}`);\n",
+       "\n",
+       "    switch (widget.type) {\n",
+       "      case 'histogram':\n",
+       "        const type = _getWidgetType(mapLayer, widget.value, widget.prop);\n",
+       "        const histogram = type === 'category' ? 'categoricalHistogram' : 'numericalHistogram';\n",
+       "        bridge[histogram](widget.element, widget.value, widget.options);\n",
+       "\n",
+       "        break;\n",
+       "      case 'category':\n",
+       "        bridge.category(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "      case 'animation':\n",
+       "        widget.options.propertyName = widget.prop;\n",
+       "        bridge.animationControls(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "      case 'time-series':\n",
+       "        widget.options.propertyName = widget.prop;\n",
+       "        bridge.timeSeries(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function bridgeLayerWidgets(map, mapLayer, mapSource, widgets) {\n",
+       "    const bridge = new AsBridge.VL.Bridge({\n",
+       "      carto: carto,\n",
+       "      layer: mapLayer,\n",
+       "      source: mapSource,\n",
+       "      map: map\n",
+       "    });\n",
+       "\n",
+       "    widgets\n",
+       "      .filter((widget) => widget.has_bridge)\n",
+       "      .forEach((widget) => renderBridge(bridge, widget, mapLayer));\n",
+       "\n",
+       "    bridge.build();\n",
+       "  }\n",
+       "\n",
+       "  function _getWidgetType(layer, property, value) {\n",
+       "    return layer.metadata && layer.metadata.properties[value] ?\n",
+       "      layer.metadata.properties[value].type\n",
+       "      : _getWidgetPropertyType(layer, property);\n",
+       "  }\n",
+       "\n",
+       "  function _getWidgetPropertyType(layer, property) {\n",
+       "    return layer.metadata && layer.metadata.properties[property] ?\n",
+       "      layer.metadata.properties[property].type\n",
+       "      : null;\n",
+       "  }\n",
+       "\n",
+       "  function createLegends(layer, legends, layerIndex, mapIndex=0) {\n",
+       "    if (legends.length) {\n",
+       "      legends.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legends, layerIndex, 0, mapIndex);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function _createLegend(layer, legend, layerIndex, legendIndex, mapIndex=0) {\n",
+       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend${legendIndex}`);\n",
+       "\n",
+       "    if (legend.prop) {\n",
+       "      const othersLabel = 'Others';   // TODO: i18n\n",
+       "      const prop = legend.prop;\n",
+       "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
+       "      const variable = legend.variable;\n",
+       "      const config = { othersLabel, variable, order };\n",
+       "      const options = { format, config, dynamic };\n",
+       "\n",
+       "      if (legend.type.startsWith('size-continuous')) {\n",
+       "        config.samples = 4;\n",
+       "      }\n",
+       "\n",
+       "      AsBridge.VL.Legends.rampLegend(element, layer, prop, options);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function SourceFactory() {\n",
+       "    const sourceTypes = { GeoJSON, Query, MVT };\n",
+       "\n",
+       "    this.createSource = (layer) => {\n",
+       "      return sourceTypes[layer.type](layer);\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function GeoJSON(layer) {\n",
+       "    const options = JSON.parse(JSON.stringify(layer.options));\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
+       "\n",
+       "    return new carto.source.GeoJSON(data, options);\n",
+       "  }\n",
+       "\n",
+       "  function Query(layer) {\n",
+       "    const auth = {\n",
+       "      username: layer.credentials.username,\n",
+       "      apiKey: layer.credentials.api_key || 'default_public'\n",
+       "    };\n",
+       "\n",
+       "    const config = {\n",
+       "      serverURL: layer.credentials.base_url || `https://${layer.credentials.username}.carto.com/`\n",
+       "    };\n",
+       "\n",
+       "    return new carto.source.SQL(layer.data, auth, config);\n",
+       "  }\n",
+       "\n",
+       "  function MVT(layer) {\n",
+       "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
+       "  }\n",
+       "\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  const factory = new SourceFactory();\n",
+       "\n",
+       "  function initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex) {\n",
+       "    const mapSource = factory.createSource(layer);\n",
+       "    const mapViz = new carto.Viz(layer.viz);\n",
+       "    const mapLayer = new carto.Layer(`layer${layerIndex}`, mapSource, mapViz);\n",
+       "    const mapLayerIndex = numLayers - layerIndex - 1;\n",
+       "\n",
+       "    try {\n",
+       "      mapLayer._updateLayer.catch(displayError);\n",
+       "    } catch (e) {\n",
+       "      throw e;\n",
+       "    }\n",
+       "\n",
+       "\n",
+       "    mapLayer.addTo(map);\n",
+       "\n",
+       "    setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends);\n",
+       "    setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource);\n",
+       "\n",
+       "    return mapLayer;\n",
+       "  }\n",
+       "\n",
+       "  function getInteractiveLayers(layers, mapLayers) {\n",
+       "    const interactiveLayers = [];\n",
+       "    const interactiveMapLayers = [];\n",
+       "\n",
+       "    layers.forEach((layer, index) => {\n",
+       "      if (layer.interactivity) {\n",
+       "        interactiveLayers.push(layer);\n",
+       "        interactiveMapLayers.push(mapLayers[index]);\n",
+       "      }\n",
+       "    });\n",
+       "\n",
+       "    return { interactiveLayers, interactiveMapLayers };\n",
+       "  }\n",
+       "\n",
+       "  function setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends) {\n",
+       "    if (hasLegends && layer.legends) {\n",
+       "      createLegends(mapLayer, layer.legends, mapLayerIndex, mapIndex);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource) {\n",
+       "    if (layer.widgets.length) {\n",
+       "      initLayerWidgets(layer.widgets, mapLayerIndex);\n",
+       "      updateLayerWidgets(layer.widgets, mapLayer);\n",
+       "      bridgeLayerWidgets(map, mapLayer, mapSource, layer.widgets);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function initLayerWidgets(widgets, mapLayerIndex) {\n",
+       "    widgets.forEach((widget, widgetIndex) => {\n",
+       "      const id = `layer${mapLayerIndex}_widget${widgetIndex}`;\n",
+       "      widget.id = id;\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function updateLayerWidgets(widgets, mapLayer) {\n",
+       "    mapLayer.on('updated', () => renderLayerWidgets(widgets, mapLayer));\n",
+       "  }\n",
+       "\n",
+       "  function renderLayerWidgets(widgets, mapLayer) {\n",
+       "    const variables = mapLayer.viz.variables;\n",
+       "\n",
+       "    widgets\n",
+       "      .filter((widget) => !widget.has_bridge)\n",
+       "      .forEach((widget) => {\n",
+       "        const name = widget.variable_name;\n",
+       "        const value = getWidgetValue(name, variables);\n",
+       "        renderWidget(widget, value);\n",
+       "      });\n",
+       "  }\n",
+       "\n",
+       "  function getWidgetValue(name, variables) {\n",
+       "    return name && variables[name] ? variables[name].value : null;\n",
+       "  }\n",
+       "\n",
+       "  function setReady(settings) {\n",
+       "    try {\n",
+       "      return settings.maps ? initMaps(settings.maps) : initMap(settings);\n",
+       "    } catch (e) {\n",
+       "      displayError(e);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function initMaps(maps) {\n",
+       "    return maps.map((mapSettings, mapIndex) => {\n",
+       "      return initMap(mapSettings, mapIndex);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function initMap(settings, mapIndex) {\n",
+       "    const basecolor = getBasecolorSettings(settings.basecolor);\n",
+       "    const basemapStyle =  BASEMAPS[settings.basemap] || settings.basemap || basecolor;\n",
+       "    const container = mapIndex !== undefined ? `map-${mapIndex}` : 'map';\n",
+       "    const map = createMap(container, basemapStyle, settings.bounds, settings.mapboxtoken);\n",
+       "\n",
+       "    if (settings.show_info) {\n",
+       "      updateViewport(map);\n",
+       "    }\n",
+       "\n",
+       "    if (settings.camera) {\n",
+       "      map.flyTo(settings.camera);\n",
+       "    }\n",
+       "\n",
+       "    return initLayers(map, settings, mapIndex);\n",
+       "  }\n",
+       "\n",
+       "  function initLayers(map, settings, mapIndex) {\n",
+       "    const numLayers = settings.layers.length;\n",
+       "    const hasLegends = settings.has_legends;\n",
+       "    const isStatic = settings.is_static;\n",
+       "    const layers = settings.layers;\n",
+       "    const mapLayers = getMapLayers(\n",
+       "      layers,\n",
+       "      numLayers,\n",
+       "      hasLegends,\n",
+       "      map,\n",
+       "      mapIndex\n",
+       "    );\n",
+       "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
+       "    setInteractiveLayers(map, layers, mapLayers);\n",
+       "\n",
+       "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
+       "  }\n",
+       "\n",
+       "  function waitForMapLayersLoad(isStatic, mapIndex, mapLayers) {\n",
+       "    return new Promise((resolve) => {\n",
+       "      carto.on('loaded', mapLayers, onMapLayersLoaded.bind(\n",
+       "        this, isStatic, mapIndex, mapLayers, resolve)\n",
+       "      );\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function onMapLayersLoaded(isStatic, mapIndex, mapLayers, resolve) {\n",
+       "    if (isStatic) {\n",
+       "      saveImage(mapIndex);\n",
+       "    }\n",
+       "\n",
+       "    resolve(mapLayers);\n",
+       "  }\n",
+       "\n",
+       "  function getMapLayers(layers, numLayers, hasLegends, map, mapIndex) {\n",
+       "    return layers.map((layer, layerIndex) => {\n",
+       "      return initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function setInteractiveLayers(map, layers, mapLayers) {\n",
+       "    const { interactiveLayers, interactiveMapLayers } = getInteractiveLayers(layers, mapLayers);\n",
+       "\n",
+       "    if (interactiveLayers && interactiveLayers.length > 0) {\n",
+       "      setInteractivity(map, interactiveLayers, interactiveMapLayers);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
+       "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
+       "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
+       "\n",
+       "    map.addControl(attributionControl);\n",
+       "    map.fitBounds(bounds, FIT_BOUNDS_SETTINGS);\n",
+       "\n",
+       "    return map;\n",
+       "  }\n",
+       "\n",
+       "  function createMapboxGLMap(container, style, accessToken) {\n",
+       "    if (accessToken) {\n",
+       "      mapboxgl.accessToken = accessToken;\n",
+       "    }\n",
+       "\n",
+       "    return new mapboxgl.Map({\n",
+       "      container,\n",
+       "      style,\n",
+       "      zoom: 9,\n",
+       "      dragRotate: false,\n",
+       "      attributionControl: false\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function init(settings) {\n",
+       "    setReady(settings);\n",
+       "  }\n",
+       "\n",
+       "  return init;\n",
+       "\n",
+       "}());\n",
+       "</script>\n",
+       "<script>\n",
+       "  document\n",
+       "  .querySelector('as-responsive-content')\n",
+       "  .addEventListener('ready', () => {\n",
+       "    const basecolor = '';\n",
+       "    const basemap = 'Positron';\n",
+       "    const bounds = [[-180, -90], [180, 83.64513]];\n",
+       "    const camera = null;\n",
+       "    const has_legends = 'False' === 'true';\n",
+       "    const is_static = 'None' === 'true';\n",
+       "    const layer_selector = 'true' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;countries\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: &quot;Countries&quot;, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#826DBA\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;global_power_plants\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: &quot;Global Power Plants&quot;, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[2,10])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;world_rivers\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#4CC8A3\\&quot;)\\nwidth: ramp(linear(zoom(),0,18),[0.5,4])\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const mapboxtoken = '';\n",
+       "    const show_info = 'None' === 'true';\n",
+       "\n",
+       "    init({\n",
+       "      basecolor,\n",
+       "      basemap,\n",
+       "      bounds,\n",
+       "      camera,\n",
+       "      has_legends,\n",
+       "      is_static,\n",
+       "      layer_selector,\n",
+       "      layers,\n",
+       "      mapboxtoken,\n",
+       "      show_info\n",
+       "    });\n",
+       "});\n",
+       "</script>\n",
+       "</html>\n",
+       "\">\n",
+       "\n",
+       "</iframe>"
+      ],
+      "text/plain": [
+       "<cartoframes.viz.map.Map at 0x115a6e950>"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from cartoframes.viz import basic_legend\n",
+    "\n",
+    "Map([\n",
+    "    Layer('countries', title='Countries'),\n",
+    "    Layer('global_power_plants', title='Global Power Plants'),\n",
+    "    Layer('world_rivers')\n",
+    "],\n",
+    "layer_selector=True)"
+   ]
   }
  ],
  "metadata": {
diff --git a/examples/legends/color_category_legend.ipynb b/examples/legends/color_category_legend.ipynb
index 95df0b14..ac9e7adc 100644
--- a/examples/legends/color_category_legend.ipynb
+++ b/examples/legends/color_category_legend.ipynb
@@ -30,933 +30,16 @@
    "metadata": {},
    "outputs": [
     {
-     "data": {
-      "text/html": [
-       "<iframe\n",
-       "  frameborder=\"0\"\n",
-       "  style=\"\n",
-       "    border: 1px solid #cfcfcf;\n",
-       "    width: 100%;\n",
-       "    height: 632px;\n",
-       "    \"\n",
-       "  srcDoc=\"\n",
-       "  <!DOCTYPE html>\n",
-       "<html lang=&quot;en&quot;>\n",
-       "<head>\n",
-       "  <title>None</title>\n",
-       "  <meta name=&quot;description&quot; content=&quot;None&quot;>\n",
-       "  <meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;>\n",
-       "  <meta charset=&quot;UTF-8&quot;>\n",
-       "  <!-- Include CARTO VL JS -->\n",
-       "  <script src=&quot;https://libs.cartocdn.com/carto-vl/v1.4/carto-vl.min.js&quot;></script>\n",
-       "  <!-- Include Mapbox GL JS -->\n",
-       "  <script src=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.js&quot;></script>\n",
-       "  <!-- Include Mapbox GL CSS -->\n",
-       "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
-       "\n",
-       "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "\n",
-       "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
-       "\n",
-       "  <!-- External libraries -->\n",
-       "\n",
-       "  <!-- pako -->\n",
-       "  <script src=&quot;https://libs.cartocdn.com/cartoframes/dependencies/pako_inflate.min.js&quot;></script>\n",
-       "  \n",
-       "  <!-- html2canvas -->\n",
-       "  \n",
-       "\n",
-       "  \n",
-       "  <style>\n",
-       "  body {\n",
-       "    margin: 0;\n",
-       "    padding: 0;\n",
-       "  }\n",
-       "\n",
-       "  aside.as-sidebar {\n",
-       "    min-width: 300px;\n",
-       "  }\n",
-       "\n",
-       "  .map-image {\n",
-       "    display: none;\n",
-       "    max-width: 100%;\n",
-       "    height: auto;\n",
-       "  }\n",
-       "</style>\n",
-       "  <style>\n",
-       "  .map {\n",
-       "    position: absolute;\n",
-       "    height: 100%;\n",
-       "    width: 100%;\n",
-       "  }\n",
-       "\n",
-       "  .map-info {\n",
-       "    position: absolute;\n",
-       "    bottom: 0;\n",
-       "    padding: 0 5px;\n",
-       "    background-color: rgba(255, 255, 255, 0.5);\n",
-       "    margin: 0;\n",
-       "    color: rgba(0, 0, 0, 0.75);\n",
-       "    font-size: 12px;\n",
-       "    width: auto;\n",
-       "    height: 18px;\n",
-       "    font-family: 'Open Sans';\n",
-       "  }\n",
-       "\n",
-       "  .map-footer {\n",
-       "    background: #F2F6F9;\n",
-       "    font-family: Roboto;\n",
-       "    font-size: 12px;\n",
-       "    line-height: 24px;\n",
-       "    color: #162945;\n",
-       "    text-align: center;\n",
-       "    z-index: 2;\n",
-       "  }\n",
-       "\n",
-       "  .map-footer a {\n",
-       "    text-decoration: none;\n",
-       "  }\n",
-       "\n",
-       "  .map-footer a:hover {\n",
-       "    text-decoration: underline;\n",
-       "  }\n",
-       "</style>\n",
-       "    <style>\n",
-       "    #error-container {\n",
-       "      position: absolute;\n",
-       "      width: 100%;\n",
-       "      height: 100%;\n",
-       "      background-color: white;\n",
-       "      visibility: hidden;\n",
-       "      padding: 1em;\n",
-       "      font-family: &quot;Courier New&quot;, Courier, monospace;\n",
-       "      margin: 0 auto;\n",
-       "      font-size: 14px;\n",
-       "      overflow: auto;\n",
-       "      z-index: 1000;\n",
-       "      color: black;\n",
-       "    }\n",
-       "\n",
-       "    .error-section {\n",
-       "      padding: 1em;\n",
-       "      border-radius: 5px;\n",
-       "      background-color: #fee;\n",
-       "    }\n",
-       "\n",
-       "    #error-container #error-highlight {\n",
-       "      font-weight: bold;\n",
-       "      color: inherit;\n",
-       "    }\n",
-       "\n",
-       "    #error-container #error-type {\n",
-       "      color: #008000;\n",
-       "    }\n",
-       "\n",
-       "    #error-container #error-name {\n",
-       "      color: #ba2121;\n",
-       "    }\n",
-       "\n",
-       "    #error-container #error-content {\n",
-       "      margin-top: 0.4em;\n",
-       "    }\n",
-       "\n",
-       "    .error-details {\n",
-       "      margin-top: 1em;\n",
-       "    }\n",
-       "\n",
-       "    #error-stacktrace {\n",
-       "      list-style: none;\n",
-       "    }\n",
-       "</style>\n",
-       "  <style>\n",
-       "    .popup-content {\n",
-       "      display: flex;\n",
-       "      flex-direction: column;\n",
-       "      padding: 8px;\n",
-       "    }\n",
-       "\n",
-       "    .popup-name {\n",
-       "      font-size: 12px;\n",
-       "      font-weight: 400;\n",
-       "      line-height: 20px;\n",
-       "      margin-bottom: 4px;\n",
-       "    }\n",
-       "\n",
-       "    .popup-value {\n",
-       "      font-size: 16px;\n",
-       "      font-weight: 600;\n",
-       "      line-height: 20px;\n",
-       "    }\n",
-       "\n",
-       "    .popup-value:not(:last-of-type) {\n",
-       "      margin-bottom: 16px;\n",
-       "    }\n",
-       "</style>\n",
-       "  <style>\n",
-       "  as-widget-header .as-widget-header__header {\n",
-       "    margin-bottom: 8px;\n",
-       "    overflow-wrap: break-word;\n",
-       "  }\n",
-       "\n",
-       "  as-widget-header .as-widget-header__subheader {\n",
-       "    margin-bottom: 12px;\n",
-       "  }\n",
-       "\n",
-       "  as-category-widget {\n",
-       "    max-height: 250px;\n",
-       "  }\n",
-       "</style>\n",
-       "</head>\n",
-       "\n",
-       "<body class=&quot;as-app-body as-app&quot;>\n",
-       "  <img id=&quot;map-image&quot; class=&quot;map-image&quot; alt='Static map image' />\n",
-       "  <as-responsive-content id=&quot;main-container&quot;>\n",
-       "    \n",
-       "    <main class=&quot;as-main&quot;>\n",
-       "      <div class=&quot;as-map-area&quot;>\n",
-       "        <div id=&quot;map&quot; class=&quot;map&quot;></div>\n",
-       "        \n",
-       "        \n",
-       "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
-       "              \n",
-       "\n",
-       "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
-       "  \n",
-       "    \n",
-       "    \n",
-       "      \n",
-       "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;Trees in San Francisco&quot;\n",
-       "            description=&quot;Top 3 species&quot;>\n",
-       "            <as-legend-color-category id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-category>\n",
-       "            \n",
-       "              <span slot=&quot;footer&quot;>Data: City of SF</span>\n",
-       "            \n",
-       "          </as-legend>\n",
-       "        \n",
-       "      \n",
-       "    \n",
-       "  \n",
-       "</div>\n",
-       "            </div> <!-- as-panel -->\n",
-       "          </div> <!-- as-map-panels -->\n",
-       "        \n",
-       "      </div> <!-- as-map-area -->\n",
-       "    </main> <!-- as-main -->\n",
-       "  </as-responsive-content>\n",
-       "\n",
-       "  \n",
-       "\n",
-       "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
-       "  <section class=&quot;error-section&quot;>\n",
-       "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
-       "    <section id=&quot;error-content&quot;>\n",
-       "      <span class=&quot;errors&quot; id=&quot;error-type&quot;></span>\n",
-       "      <span class=&quot;errors&quot; id=&quot;error-message&quot;></span>\n",
-       "    </section>\n",
-       "  </section>\n",
-       "\n",
-       "  <details class=&quot;error-details&quot;>\n",
-       "    <summary>StackTrace</summary>\n",
-       "    <ul id=&quot;error-stacktrace&quot;></ul>\n",
-       "  </details>\n",
-       "</div>\n",
-       "</body>\n",
-       "\n",
-       "<script>\n",
-       "  var init = (function () {\n",
-       "  'use strict';\n",
-       "\n",
-       "  const BASEMAPS = {\n",
-       "    DarkMatter: carto.basemaps.darkmatter,\n",
-       "    Voyager: carto.basemaps.voyager,\n",
-       "    Positron: carto.basemaps.positron\n",
-       "  };\n",
-       "\n",
-       "  const attributionControl = new mapboxgl.AttributionControl({\n",
-       "    compact: false\n",
-       "  });\n",
-       "\n",
-       "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\n",
-       "\n",
-       "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
-       "\n",
-       "  /**\n",
-       "   * This parses the different stack traces and puts them into one format\n",
-       "   * This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)\n",
-       "   */\n",
-       "\n",
-       "  const UNKNOWN_FUNCTION = '<unknown>';\n",
-       "  const chromeRe = /^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\\/).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$/i;\n",
-       "  const chromeEvalRe = /\\((\\S*)(?::(\\d+))(?::(\\d+))\\)/;\n",
-       "  const winjsRe = /^\\s*at (?:((?:\\[object object\\])?.+) )?\\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\\d+)(?::(\\d+))?\\)?\\s*$/i;\n",
-       "  const geckoRe = /^\\s*(.*?)(?:\\((.*?)\\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\\[native).*?|[^@]*bundle)(?::(\\d+))?(?::(\\d+))?\\s*$/i;\n",
-       "  const geckoEvalRe = /(\\S+) line (\\d+)(?: > eval line \\d+)* > eval/i;\n",
-       "\n",
-       "  function parse(stackString) {\n",
-       "    const lines = stackString.split('\\n');\n",
-       "\n",
-       "    return lines.reduce((stack, line) => {\n",
-       "      const parseResult =\n",
-       "        parseChrome(line) ||\n",
-       "        parseWinjs(line) ||\n",
-       "        parseGecko(line);\n",
-       "\n",
-       "      if (parseResult) {\n",
-       "        stack.push(parseResult);\n",
-       "      }\n",
-       "\n",
-       "      return stack;\n",
-       "    }, []);\n",
-       "  }\n",
-       "\n",
-       "  function parseChrome(line) {\n",
-       "    const parts = chromeRe.exec(line);\n",
-       "\n",
-       "    if (!parts) {\n",
-       "      return null;\n",
-       "    }\n",
-       "\n",
-       "    const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line\n",
-       "    const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line\n",
-       "\n",
-       "    const submatch = chromeEvalRe.exec(parts[2]);\n",
-       "    if (isEval && submatch != null) {\n",
-       "      // throw out eval line/column and use top-most line/column number\n",
-       "      parts[2] = submatch[1]; // url\n",
-       "      parts[3] = submatch[2]; // line\n",
-       "      parts[4] = submatch[3]; // column\n",
-       "    }\n",
-       "\n",
-       "    return {\n",
-       "      file: !isNative ? parts[2] : null,\n",
-       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
-       "      arguments: isNative ? [parts[2]] : [],\n",
-       "      lineNumber: parts[3] ? +parts[3] : null,\n",
-       "      column: parts[4] ? +parts[4] : null,\n",
-       "    };\n",
-       "  }\n",
-       "\n",
-       "  function parseWinjs(line) {\n",
-       "    const parts = winjsRe.exec(line);\n",
-       "\n",
-       "    if (!parts) {\n",
-       "      return null;\n",
-       "    }\n",
-       "\n",
-       "    return {\n",
-       "      file: parts[2],\n",
-       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
-       "      arguments: [],\n",
-       "      lineNumber: +parts[3],\n",
-       "      column: parts[4] ? +parts[4] : null,\n",
-       "    };\n",
-       "  }\n",
-       "\n",
-       "  function parseGecko(line) {\n",
-       "    const parts = geckoRe.exec(line);\n",
-       "\n",
-       "    if (!parts) {\n",
-       "      return null;\n",
-       "    }\n",
-       "\n",
-       "    const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;\n",
-       "\n",
-       "    const submatch = geckoEvalRe.exec(parts[3]);\n",
-       "    if (isEval && submatch != null) {\n",
-       "      // throw out eval line/column and use top-most line number\n",
-       "      parts[3] = submatch[1];\n",
-       "      parts[4] = submatch[2];\n",
-       "      parts[5] = null; // no column when eval\n",
-       "    }\n",
-       "\n",
-       "    return {\n",
-       "      file: parts[3],\n",
-       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
-       "      arguments: parts[2] ? parts[2].split(',') : [],\n",
-       "      lineNumber: parts[4] ? +parts[4] : null,\n",
-       "      column: parts[5] ? +parts[5] : null,\n",
-       "    };\n",
-       "  }\n",
-       "\n",
-       "  function displayError(e) {\n",
-       "    const error$ = document.getElementById('error-container');\n",
-       "    const errors$ = error$.getElementsByClassName('errors');\n",
-       "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
-       "\n",
-       "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
-       "\n",
-       "    error$.style.visibility = 'visible';\n",
-       "\n",
-       "    const stack = parse(e.stack);\n",
-       "    const list = stack.map(item => {\n",
-       "      return `<li>\n",
-       "      at <span class=&quot;stacktrace-method&quot;>${item.methodName}:</span>\n",
-       "      (${item.file}:${item.lineNumber}:${item.column})\n",
-       "    </li>`;\n",
-       "    });\n",
-       "\n",
-       "    stacktrace$.innerHTML = list.join('\\n');\n",
-       "  }\n",
-       "\n",
-       "  function format(value) {\n",
-       "    if (Array.isArray(value)) {\n",
-       "      const [first, second] = value;\n",
-       "      if (first === -Infinity) {\n",
-       "        return `< ${formatValue(second)}`;\n",
-       "      }\n",
-       "      if (second === Infinity) {\n",
-       "        return `> ${formatValue(first)}`;\n",
-       "      }\n",
-       "      return `${formatValue(first)} - ${formatValue(second)}`;\n",
-       "    }\n",
-       "    return formatValue(value);\n",
-       "  }\n",
-       "\n",
-       "  function formatValue(value) {\n",
-       "    if (typeof value === 'number') {\n",
-       "      return formatNumber(value);\n",
-       "    }\n",
-       "    return value;\n",
-       "  }\n",
-       "\n",
-       "  function formatNumber(value) {\n",
-       "    const log = Math.log10(Math.abs(value));\n",
-       "\n",
-       "    if ((log > 4 || log < -2.00000001) && value) {\n",
-       "      return value.toExponential(2);\n",
-       "    }\n",
-       "    \n",
-       "    if (!Number.isInteger(value)) {\n",
-       "      return value.toLocaleString(undefined, {\n",
-       "        minimumFractionDigits: 2,\n",
-       "        maximumFractionDigits: 3\n",
-       "      });\n",
-       "    }\n",
-       "    \n",
-       "    return value.toLocaleString();\n",
-       "  }\n",
-       "\n",
-       "  function updateViewport(map) {\n",
-       "    function updateMapInfo() {\n",
-       "      const mapInfo$ = document.getElementById('map-info');\n",
-       "      const center = map.getCenter();\n",
-       "      const lat = center.lat.toFixed(6);\n",
-       "      const lng = center.lng.toFixed(6);\n",
-       "      const zoom = map.getZoom().toFixed(2);\n",
-       "    \n",
-       "      mapInfo$.innerText = `viewport={'zoom': ${zoom}, 'lat': ${lat}, 'lng': ${lng}}`;\n",
-       "    }\n",
-       "\n",
-       "    updateMapInfo();\n",
-       "\n",
-       "    map.on('zoom', updateMapInfo);\n",
-       "    map.on('move', updateMapInfo); \n",
-       "  }\n",
-       "\n",
-       "  function getBasecolorSettings(basecolor) {\n",
-       "    return {\n",
-       "      'version': 8,\n",
-       "      'sources': {},\n",
-       "      'layers': [{\n",
-       "          'id': 'background',\n",
-       "          'type': 'background',\n",
-       "          'paint': {\n",
-       "              'background-color': basecolor\n",
-       "          }\n",
-       "      }]\n",
-       "    };\n",
-       "  }\n",
-       "\n",
-       "  function getImageElement(mapIndex) {\n",
-       "    const id = mapIndex !== undefined ? `map-image-${mapIndex}` : 'map-image';\n",
-       "    return document.getElementById(id);\n",
-       "  }\n",
-       "\n",
-       "  function getContainerElement(mapIndex) {\n",
-       "    const id = mapIndex !== undefined ? `main-container-${mapIndex}` : 'main-container';\n",
-       "    return document.getElementById(id);\n",
-       "  }\n",
-       "\n",
-       "  function saveImage(mapIndex) {\n",
-       "    const img = getImageElement(mapIndex);\n",
-       "    const container = getContainerElement(mapIndex);\n",
-       "\n",
-       "    html2canvas(container)\n",
-       "      .then((canvas) => setMapImage(canvas, img, container));\n",
-       "  }\n",
-       "\n",
-       "  function setMapImage(canvas, img, container) {\n",
-       "    const src = canvas.toDataURL();\n",
-       "    img.setAttribute('src', src);\n",
-       "    img.style.display = 'block';\n",
-       "    container.style.display = 'none';\n",
-       "  }\n",
-       "\n",
-       "  function resetPopupClick(interactivity) {\n",
-       "    interactivity.off('featureClick');\n",
-       "  }\n",
-       "\n",
-       "  function resetPopupHover(interactivity) {\n",
-       "    interactivity.off('featureHover');\n",
-       "  }\n",
-       "\n",
-       "  function setPopupsClick(map, clickPopup, hoverPopup, interactivity, attrs) {\n",
-       "    interactivity.on('featureClick', (event) => {\n",
-       "      updatePopup(map, clickPopup, event, attrs);\n",
-       "      hoverPopup.remove();\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function setPopupsHover(map, hoverPopup, interactivity, attrs) {\n",
-       "    interactivity.on('featureHover', (event) => {\n",
-       "      updatePopup(map, hoverPopup, event, attrs);\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function updatePopup(map, popup, event, attrs) {\n",
-       "    if (event.features.length > 0) {\n",
-       "      let popupHTML = '';\n",
-       "      const layerIDs = [];\n",
-       "\n",
-       "      for (const feature of event.features) {\n",
-       "        if (layerIDs.includes(feature.layerId)) {\n",
-       "          continue;\n",
-       "        }\n",
-       "        // Track layers to add only one feature per layer\n",
-       "        layerIDs.push(feature.layerId);\n",
-       "    \n",
-       "        for (const item of attrs) {\n",
-       "          const variable = feature.variables[item.name];\n",
-       "          if (variable) {\n",
-       "            let value = variable.value;\n",
-       "            value = formatValue(value);\n",
-       "\n",
-       "            popupHTML = `\n",
-       "            <span class=&quot;popup-name&quot;>${item.title}</span>\n",
-       "            <span class=&quot;popup-value&quot;>${value}</span>\n",
-       "          ` + popupHTML;\n",
-       "          }\n",
-       "        }\n",
-       "      }\n",
-       "\n",
-       "      popup\n",
-       "          .setLngLat([event.coordinates.lng, event.coordinates.lat])\n",
-       "          .setHTML(`<div class=&quot;popup-content&quot;>${popupHTML}</div>`);\n",
-       "\n",
-       "      if (!popup.isOpen()) {\n",
-       "        popup.addTo(map);\n",
-       "      }\n",
-       "    } else {\n",
-       "      popup.remove();\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function setInteractivity(map, interactiveLayers, interactiveMapLayers) {\n",
-       "    const interactivity = new carto.Interactivity(interactiveMapLayers);\n",
-       "\n",
-       "    const clickPopup = new mapboxgl.Popup({\n",
-       "      closeButton: true,\n",
-       "      closeOnClick: false\n",
-       "    });\n",
-       "\n",
-       "    const hoverPopup = new mapboxgl.Popup({\n",
-       "      closeButton: false,\n",
-       "      closeOnClick: false\n",
-       "    });\n",
-       "\n",
-       "    const { clickAttrs, hoverAttrs } = _setInteractivityAttrs(interactiveLayers);\n",
-       "\n",
-       "    resetPopupClick(map);\n",
-       "    resetPopupHover(map);\n",
-       "\n",
-       "    if (clickAttrs.length > 0) {\n",
-       "      setPopupsClick(map, clickPopup, hoverPopup, interactivity, clickAttrs);\n",
-       "    }\n",
-       "\n",
-       "    if (hoverAttrs.length > 0) {\n",
-       "      setPopupsHover(map, hoverPopup, interactivity, hoverAttrs);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function _setInteractivityAttrs(interactiveLayers) {\n",
-       "    let clickAttrs = [];\n",
-       "    let hoverAttrs = [];\n",
-       "\n",
-       "    interactiveLayers.forEach((interactiveLayer) => {\n",
-       "      interactiveLayer.interactivity.forEach((interactivityDef) => {\n",
-       "        if (interactivityDef.event === 'click') {\n",
-       "          clickAttrs = clickAttrs.concat(interactivityDef.attrs);\n",
-       "        } else if (interactivityDef.event === 'hover') {\n",
-       "          hoverAttrs = hoverAttrs.concat(interactivityDef.attrs);\n",
-       "        }\n",
-       "      });\n",
-       "    });\n",
-       "\n",
-       "    return { clickAttrs, hoverAttrs };\n",
-       "  }\n",
-       "\n",
-       "  function renderWidget(widget, value) {\n",
-       "    widget.element = widget.element || document.querySelector(`#${widget.id}-value`);\n",
-       "\n",
-       "    if (value && widget.element) {\n",
-       "      widget.element.innerText = typeof value === 'number' ? format(value) : value;\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function renderBridge(bridge, widget, mapLayer) {\n",
-       "    widget.element = widget.element || document.querySelector(`#${widget.id}`);\n",
-       "\n",
-       "    switch (widget.type) {\n",
-       "      case 'histogram':\n",
-       "        const type = _getWidgetType(mapLayer, widget.value, widget.prop);\n",
-       "        const histogram = type === 'category' ? 'categoricalHistogram' : 'numericalHistogram';\n",
-       "        bridge[histogram](widget.element, widget.value, widget.options);\n",
-       "\n",
-       "        break;\n",
-       "      case 'category':\n",
-       "        bridge.category(widget.element, widget.value, widget.options);\n",
-       "        break;\n",
-       "      case 'animation':\n",
-       "        widget.options.propertyName = widget.prop;\n",
-       "        bridge.animationControls(widget.element, widget.value, widget.options);\n",
-       "        break;\n",
-       "      case 'time-series':\n",
-       "        widget.options.propertyName = widget.prop;\n",
-       "        bridge.timeSeries(widget.element, widget.value, widget.options);\n",
-       "        break;\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function bridgeLayerWidgets(map, mapLayer, mapSource, widgets) {\n",
-       "    const bridge = new AsBridge.VL.Bridge({\n",
-       "      carto: carto,\n",
-       "      layer: mapLayer,\n",
-       "      source: mapSource,\n",
-       "      map: map\n",
-       "    });\n",
-       "\n",
-       "    widgets\n",
-       "      .filter((widget) => widget.has_bridge)\n",
-       "      .forEach((widget) => renderBridge(bridge, widget, mapLayer));\n",
-       "\n",
-       "    bridge.build();\n",
-       "  }\n",
-       "\n",
-       "  function _getWidgetType(layer, property, value) {\n",
-       "    return layer.metadata && layer.metadata.properties[value] ?\n",
-       "      layer.metadata.properties[value].type\n",
-       "      : _getWidgetPropertyType(layer, property);\n",
-       "  }\n",
-       "\n",
-       "  function _getWidgetPropertyType(layer, property) {\n",
-       "    return layer.metadata && layer.metadata.properties[property] ?\n",
-       "      layer.metadata.properties[property].type\n",
-       "      : null;\n",
-       "  }\n",
-       "\n",
-       "  function createLegends(layer, legends, layerIndex, mapIndex=0) {\n",
-       "    if (legends.length) {\n",
-       "      legends.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legends, layerIndex, 0, mapIndex);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function _createLegend(layer, legend, layerIndex, legendIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend${legendIndex}`);\n",
-       "\n",
-       "    if (legend.prop) {\n",
-       "      const othersLabel = 'Others';   // TODO: i18n\n",
-       "      const prop = legend.prop;\n",
-       "      const dynamic = legend.dynamic;\n",
-       "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
-       "      const options = { format, config, dynamic };\n",
-       "\n",
-       "      if (legend.type.startsWith('size-continuous')) {\n",
-       "        config.samples = 4;\n",
-       "      }\n",
-       "\n",
-       "      AsBridge.VL.Legends.rampLegend(element, layer, prop, options);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function SourceFactory() {\n",
-       "    const sourceTypes = { GeoJSON, Query, MVT };\n",
-       "\n",
-       "    this.createSource = (layer) => {\n",
-       "      return sourceTypes[layer.type](layer);\n",
-       "    };\n",
-       "  }\n",
-       "\n",
-       "  function GeoJSON(layer) {\n",
-       "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
-       "\n",
-       "    return new carto.source.GeoJSON(data, options);\n",
-       "  }\n",
-       "\n",
-       "  function Query(layer) {\n",
-       "    const auth = {\n",
-       "      username: layer.credentials.username,\n",
-       "      apiKey: layer.credentials.api_key || 'default_public'\n",
-       "    };\n",
-       "\n",
-       "    const config = {\n",
-       "      serverURL: layer.credentials.base_url || `https://${layer.credentials.username}.carto.com/`\n",
-       "    };\n",
-       "\n",
-       "    return new carto.source.SQL(layer.data, auth, config);\n",
-       "  }\n",
-       "\n",
-       "  function MVT(layer) {\n",
-       "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
-       "  }\n",
-       "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
-       "  }\n",
-       "\n",
-       "  const factory = new SourceFactory();\n",
-       "\n",
-       "  function initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex) {\n",
-       "    const mapSource = factory.createSource(layer);\n",
-       "    const mapViz = new carto.Viz(layer.viz);\n",
-       "    const mapLayer = new carto.Layer(`layer${layerIndex}`, mapSource, mapViz);\n",
-       "    const mapLayerIndex = numLayers - layerIndex - 1;\n",
-       "\n",
-       "    try {\n",
-       "      mapLayer._updateLayer.catch(displayError);\n",
-       "    } catch (e) {\n",
-       "      throw e;\n",
-       "    }\n",
-       "\n",
-       "\n",
-       "    mapLayer.addTo(map);\n",
-       "\n",
-       "    setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends);\n",
-       "    setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource);\n",
-       "\n",
-       "    return mapLayer;\n",
-       "  }\n",
-       "\n",
-       "  function getInteractiveLayers(layers, mapLayers) {\n",
-       "    const interactiveLayers = [];\n",
-       "    const interactiveMapLayers = [];\n",
-       "\n",
-       "    layers.forEach((layer, index) => {\n",
-       "      if (layer.interactivity) {\n",
-       "        interactiveLayers.push(layer);\n",
-       "        interactiveMapLayers.push(mapLayers[index]);\n",
-       "      }\n",
-       "    });\n",
-       "\n",
-       "    return { interactiveLayers, interactiveMapLayers };\n",
-       "  }\n",
-       "\n",
-       "  function setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends) {\n",
-       "    if (hasLegends && layer.legends) {\n",
-       "      createLegends(mapLayer, layer.legends, mapLayerIndex, mapIndex);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource) {\n",
-       "    if (layer.widgets.length) {\n",
-       "      initLayerWidgets(layer.widgets, mapLayerIndex);\n",
-       "      updateLayerWidgets(layer.widgets, mapLayer);\n",
-       "      bridgeLayerWidgets(map, mapLayer, mapSource, layer.widgets);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function initLayerWidgets(widgets, mapLayerIndex) {\n",
-       "    widgets.forEach((widget, widgetIndex) => {\n",
-       "      const id = `layer${mapLayerIndex}_widget${widgetIndex}`;\n",
-       "      widget.id = id;\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function updateLayerWidgets(widgets, mapLayer) {\n",
-       "    mapLayer.on('updated', () => renderLayerWidgets(widgets, mapLayer));\n",
-       "  }\n",
-       "\n",
-       "  function renderLayerWidgets(widgets, mapLayer) {\n",
-       "    const variables = mapLayer.viz.variables;\n",
-       "\n",
-       "    widgets\n",
-       "      .filter((widget) => !widget.has_bridge)\n",
-       "      .forEach((widget) => {\n",
-       "        const name = widget.variable_name;\n",
-       "        const value = getWidgetValue(name, variables);\n",
-       "        renderWidget(widget, value);\n",
-       "      });\n",
-       "  }\n",
-       "\n",
-       "  function getWidgetValue(name, variables) {\n",
-       "    return name && variables[name] ? variables[name].value : null;\n",
-       "  }\n",
-       "\n",
-       "  function setReady(settings) {\n",
-       "    try {\n",
-       "      return settings.maps ? initMaps(settings.maps) : initMap(settings);\n",
-       "    } catch (e) {\n",
-       "      displayError(e);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function initMaps(maps) {\n",
-       "    return maps.map((mapSettings, mapIndex) => {\n",
-       "      return initMap(mapSettings, mapIndex);\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function initMap(settings, mapIndex) {\n",
-       "    const basecolor = getBasecolorSettings(settings.basecolor);\n",
-       "    const basemapStyle =  BASEMAPS[settings.basemap] || settings.basemap || basecolor;\n",
-       "    const container = mapIndex !== undefined ? `map-${mapIndex}` : 'map';\n",
-       "    const map = createMap(container, basemapStyle, settings.bounds, settings.mapboxtoken);\n",
-       "\n",
-       "    if (settings.show_info) {\n",
-       "      updateViewport(map);\n",
-       "    }\n",
-       "\n",
-       "    if (settings.camera) {\n",
-       "      map.flyTo(settings.camera);\n",
-       "    }\n",
-       "\n",
-       "    return initLayers(map, settings, mapIndex);\n",
-       "  }\n",
-       "\n",
-       "  function initLayers(map, settings, mapIndex) {\n",
-       "    const numLayers = settings.layers.length;\n",
-       "    const hasLegends = settings.has_legends;\n",
-       "    const isStatic = settings.is_static;\n",
-       "    const layers = settings.layers;\n",
-       "    const mapLayers = getMapLayers(\n",
-       "      layers,\n",
-       "      numLayers,\n",
-       "      hasLegends,\n",
-       "      map,\n",
-       "      mapIndex\n",
-       "    );\n",
-       "\n",
-       "    setInteractiveLayers(map, layers, mapLayers);\n",
-       "\n",
-       "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
-       "  }\n",
-       "\n",
-       "  function waitForMapLayersLoad(isStatic, mapIndex, mapLayers) {\n",
-       "    return new Promise((resolve) => {\n",
-       "      carto.on('loaded', mapLayers, onMapLayersLoaded.bind(\n",
-       "        this, isStatic, mapIndex, mapLayers, resolve)\n",
-       "      );\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function onMapLayersLoaded(isStatic, mapIndex, mapLayers, resolve) {\n",
-       "    if (isStatic) {\n",
-       "      saveImage(mapIndex);\n",
-       "    }\n",
-       "\n",
-       "    resolve(mapLayers);\n",
-       "  }\n",
-       "\n",
-       "  function getMapLayers(layers, numLayers, hasLegends, map, mapIndex) {\n",
-       "    return layers.map((layer, layerIndex) => {\n",
-       "      return initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex);\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function setInteractiveLayers(map, layers, mapLayers) {\n",
-       "    const { interactiveLayers, interactiveMapLayers } = getInteractiveLayers(layers, mapLayers);\n",
-       "\n",
-       "    if (interactiveLayers && interactiveLayers.length > 0) {\n",
-       "      setInteractivity(map, interactiveLayers, interactiveMapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
-       "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
-       "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
-       "\n",
-       "    map.addControl(attributionControl);\n",
-       "    map.fitBounds(bounds, FIT_BOUNDS_SETTINGS);\n",
-       "\n",
-       "    return map;\n",
-       "  }\n",
-       "\n",
-       "  function createMapboxGLMap(container, style, accessToken) {\n",
-       "    if (accessToken) {\n",
-       "      mapboxgl.accessToken = accessToken;\n",
-       "    }\n",
-       "\n",
-       "    return new mapboxgl.Map({\n",
-       "      container,\n",
-       "      style,\n",
-       "      zoom: 9,\n",
-       "      dragRotate: false,\n",
-       "      attributionControl: false\n",
-       "    });\n",
-       "  }\n",
-       "\n",
-       "  function init(settings) {\n",
-       "    setReady(settings);\n",
-       "  }\n",
-       "\n",
-       "  return init;\n",
-       "\n",
-       "}());\n",
-       "</script>\n",
-       "<script>\n",
-       "  document\n",
-       "  .querySelector('as-responsive-content')\n",
-       "  .addEventListener('ready', () => {\n",
-       "    const basecolor = '';\n",
-       "    const basemap = 'Positron';\n",
-       "    const bounds = [[-122.5085209, 37.70792829], [-122.3724441, 37.80902219]];\n",
-       "    const camera = null;\n",
-       "    const has_legends = 'true' === 'true';\n",
-       "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;trees_sf\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;v8ab58a&quot;, &quot;title&quot;: &quot;common_species&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;Top 3 species&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: City of SF&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Trees in San Francisco&quot;, &quot;type&quot;: &quot;color-category&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@v8ab58a: $common_species\\ncolor: opacity(ramp(top($common_species, 11), bold),1)\\nwidth: ramp(linear(zoom(),0,18),[2,10])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
-       "    const mapboxtoken = '';\n",
-       "    const show_info = 'None' === 'true';\n",
-       "\n",
-       "    init({\n",
-       "      basecolor,\n",
-       "      basemap,\n",
-       "      bounds,\n",
-       "      camera,\n",
-       "      has_legends,\n",
-       "      is_static,\n",
-       "      layers,\n",
-       "      mapboxtoken,\n",
-       "      show_info\n",
-       "    });\n",
-       "});\n",
-       "</script>\n",
-       "</html>\n",
-       "\">\n",
-       "\n",
-       "</iframe>"
-      ],
-      "text/plain": [
-       "<cartoframes.viz.layer.Layer at 0x11a05c9d0>"
-      ]
-     },
-     "execution_count": 2,
-     "metadata": {},
-     "output_type": "execute_result"
+     "ename": "TypeError",
+     "evalue": "_init_legends() missing 1 required positional argument: 'title'",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "\u001b[0;32m<ipython-input-2-060c7561ea1c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      5\u001b[0m         \u001b[0mtitle\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Trees in San Francisco'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      6\u001b[0m         \u001b[0mdescription\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Top 3 species'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m         \u001b[0mfooter\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'Data: City of SF'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      8\u001b[0m     )\n\u001b[1;32m      9\u001b[0m )\n",
+      "\u001b[0;32m~/Projects/CARTO/carto-data-science-stack/cartoframes/cartoframes/viz/layer.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, source, style, legends, widgets, popup_hover, popup_click, credentials, bounds, geom_col, default_legend, default_widget, default_popup_hover, default_popup_click, title, encode_data)\u001b[0m\n\u001b[1;32m    113\u001b[0m         self.popups = self._init_popups(\n\u001b[1;32m    114\u001b[0m             popup_hover, popup_click, default_popup_hover, default_popup_click, title)\n\u001b[0;32m--> 115\u001b[0;31m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlegends\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_init_legends\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlegends\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdefault_legend\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    116\u001b[0m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwidgets\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_init_widgets\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mwidgets\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdefault_widget\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    117\u001b[0m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtitle\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;31mTypeError\u001b[0m: _init_legends() missing 1 required positional argument: 'title'"
+     ]
     }
    ],
    "source": [
@@ -970,6 +53,13 @@
     "    )\n",
     ")"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
diff --git a/examples/legends/color_continuous_legend.ipynb b/examples/legends/color_continuous_legend.ipynb
index bf63d242..534ec082 100644
--- a/examples/legends/color_continuous_legend.ipynb
+++ b/examples/legends/color_continuous_legend.ipynb
@@ -56,11 +56,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -224,27 +224,32 @@
        "        \n",
        "        \n",
        "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
        "              \n",
        "\n",
        "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
-       "  \n",
-       "    \n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
        "    \n",
        "      \n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;Percent Master's Degree&quot;\n",
-       "            description=&quot;by block group&quot;>\n",
-       "            <as-legend-color-continuous id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-continuous>\n",
+       "        \n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
        "            \n",
-       "              <span slot=&quot;footer&quot;>Data: US Census</span>\n",
+       "              <as-legend\n",
+       "                heading=&quot;Percent Master's Degree&quot;\n",
+       "                description=&quot;by block group&quot;>\n",
+       "                <as-legend-color-continuous id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-continuous>\n",
+       "                \n",
+       "                  <span slot=&quot;footer&quot;>Data: US Census</span>\n",
+       "                \n",
+       "              </as-legend>\n",
        "            \n",
-       "          </as-legend>\n",
-       "        \n",
+       "          \n",
+       "        </div>\n",
        "      \n",
        "    \n",
-       "  \n",
+       "  </as-layer-selector>\n",
        "</div>\n",
        "            </div> <!-- as-panel -->\n",
        "          </div> <!-- as-map-panels -->\n",
@@ -256,8 +261,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -395,9 +398,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -683,8 +685,9 @@
        "      const othersLabel = 'Others';   // TODO: i18n\n",
        "      const prop = legend.prop;\n",
        "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
        "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
+       "      const config = { othersLabel, variable, order };\n",
        "      const options = { format, config, dynamic };\n",
        "\n",
        "      if (legend.type.startsWith('size-continuous')) {\n",
@@ -705,7 +708,7 @@
        "\n",
        "  function GeoJSON(layer) {\n",
        "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
        "\n",
        "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
@@ -727,8 +730,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -853,6 +868,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -888,6 +907,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -929,7 +963,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'true' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;by block group&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: US Census&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Percent Master\\u0027s Degree&quot;, &quot;type&quot;: &quot;color-continuous&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(linear($masters_percent, globalMIN($masters_percent), globalMAX($masters_percent)), bluyl), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;by block group&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: US Census&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Percent Master\\u0027s Degree&quot;, &quot;type&quot;: &quot;color-continuous&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(linear($masters_percent, globalMIN($masters_percent), globalMAX($masters_percent)), bluyl), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -940,6 +975,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -952,7 +988,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.layer.Layer at 0x110228050>"
+       "<cartoframes.viz.layer.Layer at 0x117a21d90>"
       ]
      },
      "execution_count": 2,
diff --git a/examples/legends/default_legend.ipynb b/examples/legends/default_legend.ipynb
index 85e7e3ed..5c6eff73 100644
--- a/examples/legends/default_legend.ipynb
+++ b/examples/legends/default_legend.ipynb
@@ -9,7 +9,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 1,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -20,7 +20,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [
     {
@@ -49,11 +49,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2.0-rc.2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2.0-rc.2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2.0-rc.2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -217,25 +217,30 @@
        "        \n",
        "        \n",
        "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
        "              \n",
        "\n",
        "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
-       "  \n",
-       "    \n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
        "    \n",
        "      \n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;Percent Master's Degree&quot;\n",
-       "            description=&quot;&quot;>\n",
-       "            <as-legend-color-bins id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-bins>\n",
-       "            \n",
-       "          </as-legend>\n",
        "        \n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
+       "            \n",
+       "              <as-legend\n",
+       "                heading=&quot;Percent Master's Degree&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-color-bins id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-bins>\n",
+       "                \n",
+       "              </as-legend>\n",
+       "            \n",
+       "          \n",
+       "        </div>\n",
        "      \n",
        "    \n",
-       "  \n",
+       "  </as-layer-selector>\n",
        "</div>\n",
        "            </div> <!-- as-panel -->\n",
        "          </div> <!-- as-map-panels -->\n",
@@ -247,8 +252,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -386,9 +389,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -674,8 +676,9 @@
        "      const othersLabel = 'Others';   // TODO: i18n\n",
        "      const prop = legend.prop;\n",
        "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
        "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
+       "      const config = { othersLabel, variable, order };\n",
        "      const options = { format, config, dynamic };\n",
        "\n",
        "      if (legend.type.startsWith('size-continuous')) {\n",
@@ -696,7 +699,7 @@
        "\n",
        "  function GeoJSON(layer) {\n",
        "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
        "\n",
        "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
@@ -718,8 +721,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -844,6 +859,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -879,6 +898,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -920,7 +954,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'true' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Percent Master\\u0027s Degree&quot;, &quot;type&quot;: &quot;color-bins&quot;, &quot;variable&quot;: &quot;&quot;}], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(globalQuantiles($masters_percent, 5), purpor), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Percent Master\\u0027s Degree&quot;, &quot;type&quot;: &quot;color-bins&quot;, &quot;variable&quot;: &quot;&quot;}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(globalQuantiles($masters_percent, 5), purpor), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -931,6 +966,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -943,10 +979,10 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x10fc3b2d0>"
+       "<cartoframes.viz.map.Map at 0x111d2a110>"
       ]
      },
-     "execution_count": 7,
+     "execution_count": 2,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -965,7 +1001,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
@@ -994,11 +1030,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2.0-rc.2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2.0-rc.2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2.0-rc.2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -1162,25 +1198,30 @@
        "        \n",
        "        \n",
        "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
        "              \n",
        "\n",
        "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
-       "  \n",
-       "    \n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
        "    \n",
        "      \n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;masters_percent&quot;\n",
-       "            description=&quot;&quot;>\n",
-       "            <as-legend-color-bins id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-bins>\n",
-       "            \n",
-       "          </as-legend>\n",
        "        \n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
+       "            \n",
+       "              <as-legend\n",
+       "                heading=&quot;masters_percent&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-color-bins id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-bins>\n",
+       "                \n",
+       "              </as-legend>\n",
+       "            \n",
+       "          \n",
+       "        </div>\n",
        "      \n",
        "    \n",
-       "  \n",
+       "  </as-layer-selector>\n",
        "</div>\n",
        "            </div> <!-- as-panel -->\n",
        "          </div> <!-- as-map-panels -->\n",
@@ -1192,8 +1233,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -1331,9 +1370,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -1619,8 +1657,9 @@
        "      const othersLabel = 'Others';   // TODO: i18n\n",
        "      const prop = legend.prop;\n",
        "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
        "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
+       "      const config = { othersLabel, variable, order };\n",
        "      const options = { format, config, dynamic };\n",
        "\n",
        "      if (legend.type.startsWith('size-continuous')) {\n",
@@ -1641,7 +1680,7 @@
        "\n",
        "  function GeoJSON(layer) {\n",
        "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
        "\n",
        "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
@@ -1663,8 +1702,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -1789,6 +1840,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -1824,6 +1879,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -1865,7 +1935,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'true' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;masters_percent&quot;, &quot;type&quot;: &quot;color-bins&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(globalQuantiles($masters_percent, 5), purpor), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;demographics_sf\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;vb4e077&quot;, &quot;title&quot;: &quot;masters_percent&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;masters_percent&quot;, &quot;type&quot;: &quot;color-bins&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@vb4e077: $masters_percent\\ncolor: opacity(ramp(globalQuantiles($masters_percent, 5), purpor), 0.9)\\nstrokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -1876,6 +1947,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -1888,10 +1960,10 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x10fc3b590>"
+       "<cartoframes.viz.map.Map at 0x11542aa50>"
       ]
      },
-     "execution_count": 6,
+     "execution_count": 3,
      "metadata": {},
      "output_type": "execute_result"
     }
diff --git a/examples/legends/multiple_legends.ipynb b/examples/legends/multiple_legends.ipynb
index 8c5c4503..7c5abef1 100644
--- a/examples/legends/multiple_legends.ipynb
+++ b/examples/legends/multiple_legends.ipynb
@@ -16,7 +16,7 @@
    "outputs": [],
    "source": [
     "from cartoframes.auth import set_default_credentials\n",
-    "from cartoframes.viz import Layer, color_bins_legend, size_bins_legend\n",
+    "from cartoframes.viz import Map, Layer, color_category_legend, size_bins_legend, color_category_style, size_bins_style\n",
     "\n",
     "set_default_credentials('cartoframes')"
    ]
@@ -52,11 +52,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.1/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.1/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2.0-rc.2.1/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2.0-rc.2.1/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2.0-rc.2.1/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -220,34 +220,1057 @@
        "        \n",
        "        \n",
        "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
        "              \n",
        "\n",
        "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
+       "    \n",
+       "      \n",
+       "        \n",
+       "        \n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
+       "            \n",
+       "              <as-legend\n",
+       "                heading=&quot;Road Travel Direction&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-color-category id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-category>\n",
+       "                \n",
+       "              </as-legend>\n",
+       "            \n",
+       "          \n",
+       "        </div>\n",
+       "      \n",
+       "    \n",
+       "      \n",
+       "        \n",
+       "        \n",
+       "        <div slot=&quot;as-checkbox-layer-1-slot&quot;>\n",
+       "          \n",
+       "            \n",
+       "              <as-legend\n",
+       "                heading=&quot;Road Travel Speed&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-size-bins id=&quot;layer1_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-size-bins>\n",
+       "                \n",
+       "                  <span slot=&quot;footer&quot;>Data: City of SF</span>\n",
+       "                \n",
+       "              </as-legend>\n",
+       "            \n",
+       "          \n",
+       "        </div>\n",
+       "      \n",
+       "    \n",
+       "  </as-layer-selector>\n",
+       "</div>\n",
+       "            </div> <!-- as-panel -->\n",
+       "          </div> <!-- as-map-panels -->\n",
+       "        \n",
+       "      </div> <!-- as-map-area -->\n",
+       "    </main> <!-- as-main -->\n",
+       "  </as-responsive-content>\n",
+       "\n",
+       "  \n",
+       "\n",
+       "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
+       "  <section class=&quot;error-section&quot;>\n",
+       "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
+       "    <section id=&quot;error-content&quot;>\n",
+       "      <span class=&quot;errors&quot; id=&quot;error-type&quot;></span>\n",
+       "      <span class=&quot;errors&quot; id=&quot;error-message&quot;></span>\n",
+       "    </section>\n",
+       "  </section>\n",
+       "\n",
+       "  <details class=&quot;error-details&quot;>\n",
+       "    <summary>StackTrace</summary>\n",
+       "    <ul id=&quot;error-stacktrace&quot;></ul>\n",
+       "  </details>\n",
+       "</div>\n",
+       "</body>\n",
+       "\n",
+       "<script>\n",
+       "  var init = (function () {\n",
+       "  'use strict';\n",
+       "\n",
+       "  const BASEMAPS = {\n",
+       "    DarkMatter: carto.basemaps.darkmatter,\n",
+       "    Voyager: carto.basemaps.voyager,\n",
+       "    Positron: carto.basemaps.positron\n",
+       "  };\n",
+       "\n",
+       "  const attributionControl = new mapboxgl.AttributionControl({\n",
+       "    compact: false\n",
+       "  });\n",
+       "\n",
+       "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\n",
+       "\n",
+       "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
+       "\n",
+       "  /**\n",
+       "   * This parses the different stack traces and puts them into one format\n",
+       "   * This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)\n",
+       "   */\n",
+       "\n",
+       "  const UNKNOWN_FUNCTION = '<unknown>';\n",
+       "  const chromeRe = /^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\\/).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$/i;\n",
+       "  const chromeEvalRe = /\\((\\S*)(?::(\\d+))(?::(\\d+))\\)/;\n",
+       "  const winjsRe = /^\\s*at (?:((?:\\[object object\\])?.+) )?\\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\\d+)(?::(\\d+))?\\)?\\s*$/i;\n",
+       "  const geckoRe = /^\\s*(.*?)(?:\\((.*?)\\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\\[native).*?|[^@]*bundle)(?::(\\d+))?(?::(\\d+))?\\s*$/i;\n",
+       "  const geckoEvalRe = /(\\S+) line (\\d+)(?: > eval line \\d+)* > eval/i;\n",
+       "\n",
+       "  function parse(stackString) {\n",
+       "    const lines = stackString.split('\\n');\n",
+       "\n",
+       "    return lines.reduce((stack, line) => {\n",
+       "      const parseResult =\n",
+       "        parseChrome(line) ||\n",
+       "        parseWinjs(line) ||\n",
+       "        parseGecko(line);\n",
+       "\n",
+       "      if (parseResult) {\n",
+       "        stack.push(parseResult);\n",
+       "      }\n",
+       "\n",
+       "      return stack;\n",
+       "    }, []);\n",
+       "  }\n",
+       "\n",
+       "  function parseChrome(line) {\n",
+       "    const parts = chromeRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line\n",
+       "    const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line\n",
+       "\n",
+       "    const submatch = chromeEvalRe.exec(parts[2]);\n",
+       "    if (isEval && submatch != null) {\n",
+       "      // throw out eval line/column and use top-most line/column number\n",
+       "      parts[2] = submatch[1]; // url\n",
+       "      parts[3] = submatch[2]; // line\n",
+       "      parts[4] = submatch[3]; // column\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: !isNative ? parts[2] : null,\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: isNative ? [parts[2]] : [],\n",
+       "      lineNumber: parts[3] ? +parts[3] : null,\n",
+       "      column: parts[4] ? +parts[4] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function parseWinjs(line) {\n",
+       "    const parts = winjsRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: parts[2],\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: [],\n",
+       "      lineNumber: +parts[3],\n",
+       "      column: parts[4] ? +parts[4] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function parseGecko(line) {\n",
+       "    const parts = geckoRe.exec(line);\n",
+       "\n",
+       "    if (!parts) {\n",
+       "      return null;\n",
+       "    }\n",
+       "\n",
+       "    const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;\n",
+       "\n",
+       "    const submatch = geckoEvalRe.exec(parts[3]);\n",
+       "    if (isEval && submatch != null) {\n",
+       "      // throw out eval line/column and use top-most line number\n",
+       "      parts[3] = submatch[1];\n",
+       "      parts[4] = submatch[2];\n",
+       "      parts[5] = null; // no column when eval\n",
+       "    }\n",
+       "\n",
+       "    return {\n",
+       "      file: parts[3],\n",
+       "      methodName: parts[1] || UNKNOWN_FUNCTION,\n",
+       "      arguments: parts[2] ? parts[2].split(',') : [],\n",
+       "      lineNumber: parts[4] ? +parts[4] : null,\n",
+       "      column: parts[5] ? +parts[5] : null,\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function displayError(e) {\n",
+       "    const error$ = document.getElementById('error-container');\n",
+       "    const errors$ = error$.getElementsByClassName('errors');\n",
+       "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
+       "\n",
+       "    errors$[0].innerHTML = e.name;\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
+       "\n",
+       "    error$.style.visibility = 'visible';\n",
+       "\n",
+       "    const stack = parse(e.stack);\n",
+       "    const list = stack.map(item => {\n",
+       "      return `<li>\n",
+       "      at <span class=&quot;stacktrace-method&quot;>${item.methodName}:</span>\n",
+       "      (${item.file}:${item.lineNumber}:${item.column})\n",
+       "    </li>`;\n",
+       "    });\n",
+       "\n",
+       "    stacktrace$.innerHTML = list.join('\\n');\n",
+       "  }\n",
+       "\n",
+       "  function format(value) {\n",
+       "    if (Array.isArray(value)) {\n",
+       "      const [first, second] = value;\n",
+       "      if (first === -Infinity) {\n",
+       "        return `< ${formatValue(second)}`;\n",
+       "      }\n",
+       "      if (second === Infinity) {\n",
+       "        return `> ${formatValue(first)}`;\n",
+       "      }\n",
+       "      return `${formatValue(first)} - ${formatValue(second)}`;\n",
+       "    }\n",
+       "    return formatValue(value);\n",
+       "  }\n",
+       "\n",
+       "  function formatValue(value) {\n",
+       "    if (typeof value === 'number') {\n",
+       "      return formatNumber(value);\n",
+       "    }\n",
+       "    return value;\n",
+       "  }\n",
+       "\n",
+       "  function formatNumber(value) {\n",
+       "    const log = Math.log10(Math.abs(value));\n",
+       "\n",
+       "    if ((log > 4 || log < -2.00000001) && value) {\n",
+       "      return value.toExponential(2);\n",
+       "    }\n",
+       "    \n",
+       "    if (!Number.isInteger(value)) {\n",
+       "      return value.toLocaleString(undefined, {\n",
+       "        minimumFractionDigits: 2,\n",
+       "        maximumFractionDigits: 3\n",
+       "      });\n",
+       "    }\n",
+       "    \n",
+       "    return value.toLocaleString();\n",
+       "  }\n",
+       "\n",
+       "  function updateViewport(map) {\n",
+       "    function updateMapInfo() {\n",
+       "      const mapInfo$ = document.getElementById('map-info');\n",
+       "      const center = map.getCenter();\n",
+       "      const lat = center.lat.toFixed(6);\n",
+       "      const lng = center.lng.toFixed(6);\n",
+       "      const zoom = map.getZoom().toFixed(2);\n",
+       "    \n",
+       "      mapInfo$.innerText = `viewport={'zoom': ${zoom}, 'lat': ${lat}, 'lng': ${lng}}`;\n",
+       "    }\n",
+       "\n",
+       "    updateMapInfo();\n",
+       "\n",
+       "    map.on('zoom', updateMapInfo);\n",
+       "    map.on('move', updateMapInfo); \n",
+       "  }\n",
+       "\n",
+       "  function getBasecolorSettings(basecolor) {\n",
+       "    return {\n",
+       "      'version': 8,\n",
+       "      'sources': {},\n",
+       "      'layers': [{\n",
+       "          'id': 'background',\n",
+       "          'type': 'background',\n",
+       "          'paint': {\n",
+       "              'background-color': basecolor\n",
+       "          }\n",
+       "      }]\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function getImageElement(mapIndex) {\n",
+       "    const id = mapIndex !== undefined ? `map-image-${mapIndex}` : 'map-image';\n",
+       "    return document.getElementById(id);\n",
+       "  }\n",
+       "\n",
+       "  function getContainerElement(mapIndex) {\n",
+       "    const id = mapIndex !== undefined ? `main-container-${mapIndex}` : 'main-container';\n",
+       "    return document.getElementById(id);\n",
+       "  }\n",
+       "\n",
+       "  function saveImage(mapIndex) {\n",
+       "    const img = getImageElement(mapIndex);\n",
+       "    const container = getContainerElement(mapIndex);\n",
+       "\n",
+       "    html2canvas(container)\n",
+       "      .then((canvas) => setMapImage(canvas, img, container));\n",
+       "  }\n",
+       "\n",
+       "  function setMapImage(canvas, img, container) {\n",
+       "    const src = canvas.toDataURL();\n",
+       "    img.setAttribute('src', src);\n",
+       "    img.style.display = 'block';\n",
+       "    container.style.display = 'none';\n",
+       "  }\n",
+       "\n",
+       "  function resetPopupClick(interactivity) {\n",
+       "    interactivity.off('featureClick');\n",
+       "  }\n",
+       "\n",
+       "  function resetPopupHover(interactivity) {\n",
+       "    interactivity.off('featureHover');\n",
+       "  }\n",
+       "\n",
+       "  function setPopupsClick(map, clickPopup, hoverPopup, interactivity, attrs) {\n",
+       "    interactivity.on('featureClick', (event) => {\n",
+       "      updatePopup(map, clickPopup, event, attrs);\n",
+       "      hoverPopup.remove();\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function setPopupsHover(map, hoverPopup, interactivity, attrs) {\n",
+       "    interactivity.on('featureHover', (event) => {\n",
+       "      updatePopup(map, hoverPopup, event, attrs);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function updatePopup(map, popup, event, attrs) {\n",
+       "    if (event.features.length > 0) {\n",
+       "      let popupHTML = '';\n",
+       "      const layerIDs = [];\n",
+       "\n",
+       "      for (const feature of event.features) {\n",
+       "        if (layerIDs.includes(feature.layerId)) {\n",
+       "          continue;\n",
+       "        }\n",
+       "        // Track layers to add only one feature per layer\n",
+       "        layerIDs.push(feature.layerId);\n",
+       "    \n",
+       "        for (const item of attrs) {\n",
+       "          const variable = feature.variables[item.name];\n",
+       "          if (variable) {\n",
+       "            let value = variable.value;\n",
+       "            value = formatValue(value);\n",
+       "\n",
+       "            popupHTML = `\n",
+       "            <span class=&quot;popup-name&quot;>${item.title}</span>\n",
+       "            <span class=&quot;popup-value&quot;>${value}</span>\n",
+       "          ` + popupHTML;\n",
+       "          }\n",
+       "        }\n",
+       "      }\n",
+       "\n",
+       "      popup\n",
+       "          .setLngLat([event.coordinates.lng, event.coordinates.lat])\n",
+       "          .setHTML(`<div class=&quot;popup-content&quot;>${popupHTML}</div>`);\n",
+       "\n",
+       "      if (!popup.isOpen()) {\n",
+       "        popup.addTo(map);\n",
+       "      }\n",
+       "    } else {\n",
+       "      popup.remove();\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function setInteractivity(map, interactiveLayers, interactiveMapLayers) {\n",
+       "    const interactivity = new carto.Interactivity(interactiveMapLayers);\n",
+       "\n",
+       "    const clickPopup = new mapboxgl.Popup({\n",
+       "      closeButton: true,\n",
+       "      closeOnClick: false\n",
+       "    });\n",
+       "\n",
+       "    const hoverPopup = new mapboxgl.Popup({\n",
+       "      closeButton: false,\n",
+       "      closeOnClick: false\n",
+       "    });\n",
+       "\n",
+       "    const { clickAttrs, hoverAttrs } = _setInteractivityAttrs(interactiveLayers);\n",
+       "\n",
+       "    resetPopupClick(map);\n",
+       "    resetPopupHover(map);\n",
+       "\n",
+       "    if (clickAttrs.length > 0) {\n",
+       "      setPopupsClick(map, clickPopup, hoverPopup, interactivity, clickAttrs);\n",
+       "    }\n",
+       "\n",
+       "    if (hoverAttrs.length > 0) {\n",
+       "      setPopupsHover(map, hoverPopup, interactivity, hoverAttrs);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function _setInteractivityAttrs(interactiveLayers) {\n",
+       "    let clickAttrs = [];\n",
+       "    let hoverAttrs = [];\n",
+       "\n",
+       "    interactiveLayers.forEach((interactiveLayer) => {\n",
+       "      interactiveLayer.interactivity.forEach((interactivityDef) => {\n",
+       "        if (interactivityDef.event === 'click') {\n",
+       "          clickAttrs = clickAttrs.concat(interactivityDef.attrs);\n",
+       "        } else if (interactivityDef.event === 'hover') {\n",
+       "          hoverAttrs = hoverAttrs.concat(interactivityDef.attrs);\n",
+       "        }\n",
+       "      });\n",
+       "    });\n",
+       "\n",
+       "    return { clickAttrs, hoverAttrs };\n",
+       "  }\n",
+       "\n",
+       "  function renderWidget(widget, value) {\n",
+       "    widget.element = widget.element || document.querySelector(`#${widget.id}-value`);\n",
+       "\n",
+       "    if (value && widget.element) {\n",
+       "      widget.element.innerText = typeof value === 'number' ? format(value) : value;\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function renderBridge(bridge, widget, mapLayer) {\n",
+       "    widget.element = widget.element || document.querySelector(`#${widget.id}`);\n",
+       "\n",
+       "    switch (widget.type) {\n",
+       "      case 'histogram':\n",
+       "        const type = _getWidgetType(mapLayer, widget.value, widget.prop);\n",
+       "        const histogram = type === 'category' ? 'categoricalHistogram' : 'numericalHistogram';\n",
+       "        bridge[histogram](widget.element, widget.value, widget.options);\n",
+       "\n",
+       "        break;\n",
+       "      case 'category':\n",
+       "        bridge.category(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "      case 'animation':\n",
+       "        widget.options.propertyName = widget.prop;\n",
+       "        bridge.animationControls(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "      case 'time-series':\n",
+       "        widget.options.propertyName = widget.prop;\n",
+       "        bridge.timeSeries(widget.element, widget.value, widget.options);\n",
+       "        break;\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function bridgeLayerWidgets(map, mapLayer, mapSource, widgets) {\n",
+       "    const bridge = new AsBridge.VL.Bridge({\n",
+       "      carto: carto,\n",
+       "      layer: mapLayer,\n",
+       "      source: mapSource,\n",
+       "      map: map\n",
+       "    });\n",
+       "\n",
+       "    widgets\n",
+       "      .filter((widget) => widget.has_bridge)\n",
+       "      .forEach((widget) => renderBridge(bridge, widget, mapLayer));\n",
+       "\n",
+       "    bridge.build();\n",
+       "  }\n",
+       "\n",
+       "  function _getWidgetType(layer, property, value) {\n",
+       "    return layer.metadata && layer.metadata.properties[value] ?\n",
+       "      layer.metadata.properties[value].type\n",
+       "      : _getWidgetPropertyType(layer, property);\n",
+       "  }\n",
+       "\n",
+       "  function _getWidgetPropertyType(layer, property) {\n",
+       "    return layer.metadata && layer.metadata.properties[property] ?\n",
+       "      layer.metadata.properties[property].type\n",
+       "      : null;\n",
+       "  }\n",
+       "\n",
+       "  function createLegends(layer, legends, layerIndex, mapIndex=0) {\n",
+       "    if (legends.length) {\n",
+       "      legends.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legends, layerIndex, 0, mapIndex);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function _createLegend(layer, legend, layerIndex, legendIndex, mapIndex=0) {\n",
+       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend${legendIndex}`);\n",
+       "\n",
+       "    if (legend.prop) {\n",
+       "      const othersLabel = 'Others';   // TODO: i18n\n",
+       "      const prop = legend.prop;\n",
+       "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
+       "      const variable = legend.variable;\n",
+       "      const config = { othersLabel, variable, order };\n",
+       "      const options = { format, config, dynamic };\n",
+       "\n",
+       "      if (legend.type.startsWith('size-continuous')) {\n",
+       "        config.samples = 4;\n",
+       "      }\n",
+       "\n",
+       "      AsBridge.VL.Legends.rampLegend(element, layer, prop, options);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function SourceFactory() {\n",
+       "    const sourceTypes = { GeoJSON, Query, MVT };\n",
+       "\n",
+       "    this.createSource = (layer) => {\n",
+       "      return sourceTypes[layer.type](layer);\n",
+       "    };\n",
+       "  }\n",
+       "\n",
+       "  function GeoJSON(layer) {\n",
+       "    const options = JSON.parse(JSON.stringify(layer.options));\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
+       "\n",
+       "    return new carto.source.GeoJSON(data, options);\n",
+       "  }\n",
+       "\n",
+       "  function Query(layer) {\n",
+       "    const auth = {\n",
+       "      username: layer.credentials.username,\n",
+       "      apiKey: layer.credentials.api_key || 'default_public'\n",
+       "    };\n",
+       "\n",
+       "    const config = {\n",
+       "      serverURL: layer.credentials.base_url || `https://${layer.credentials.username}.carto.com/`\n",
+       "    };\n",
+       "\n",
+       "    return new carto.source.SQL(layer.data, auth, config);\n",
+       "  }\n",
+       "\n",
+       "  function MVT(layer) {\n",
+       "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
+       "  }\n",
+       "\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  const factory = new SourceFactory();\n",
+       "\n",
+       "  function initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex) {\n",
+       "    const mapSource = factory.createSource(layer);\n",
+       "    const mapViz = new carto.Viz(layer.viz);\n",
+       "    const mapLayer = new carto.Layer(`layer${layerIndex}`, mapSource, mapViz);\n",
+       "    const mapLayerIndex = numLayers - layerIndex - 1;\n",
+       "\n",
+       "    try {\n",
+       "      mapLayer._updateLayer.catch(displayError);\n",
+       "    } catch (e) {\n",
+       "      throw e;\n",
+       "    }\n",
+       "\n",
+       "\n",
+       "    mapLayer.addTo(map);\n",
+       "\n",
+       "    setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends);\n",
+       "    setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource);\n",
+       "\n",
+       "    return mapLayer;\n",
+       "  }\n",
+       "\n",
+       "  function getInteractiveLayers(layers, mapLayers) {\n",
+       "    const interactiveLayers = [];\n",
+       "    const interactiveMapLayers = [];\n",
+       "\n",
+       "    layers.forEach((layer, index) => {\n",
+       "      if (layer.interactivity) {\n",
+       "        interactiveLayers.push(layer);\n",
+       "        interactiveMapLayers.push(mapLayers[index]);\n",
+       "      }\n",
+       "    });\n",
+       "\n",
+       "    return { interactiveLayers, interactiveMapLayers };\n",
+       "  }\n",
+       "\n",
+       "  function setLayerLegend(layer, mapLayerIndex, mapLayer, mapIndex, hasLegends) {\n",
+       "    if (hasLegends && layer.legends) {\n",
+       "      createLegends(mapLayer, layer.legends, mapLayerIndex, mapIndex);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function setLayerWidgets(map, layer, mapLayer, mapLayerIndex, mapSource) {\n",
+       "    if (layer.widgets.length) {\n",
+       "      initLayerWidgets(layer.widgets, mapLayerIndex);\n",
+       "      updateLayerWidgets(layer.widgets, mapLayer);\n",
+       "      bridgeLayerWidgets(map, mapLayer, mapSource, layer.widgets);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function initLayerWidgets(widgets, mapLayerIndex) {\n",
+       "    widgets.forEach((widget, widgetIndex) => {\n",
+       "      const id = `layer${mapLayerIndex}_widget${widgetIndex}`;\n",
+       "      widget.id = id;\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function updateLayerWidgets(widgets, mapLayer) {\n",
+       "    mapLayer.on('updated', () => renderLayerWidgets(widgets, mapLayer));\n",
+       "  }\n",
+       "\n",
+       "  function renderLayerWidgets(widgets, mapLayer) {\n",
+       "    const variables = mapLayer.viz.variables;\n",
+       "\n",
+       "    widgets\n",
+       "      .filter((widget) => !widget.has_bridge)\n",
+       "      .forEach((widget) => {\n",
+       "        const name = widget.variable_name;\n",
+       "        const value = getWidgetValue(name, variables);\n",
+       "        renderWidget(widget, value);\n",
+       "      });\n",
+       "  }\n",
+       "\n",
+       "  function getWidgetValue(name, variables) {\n",
+       "    return name && variables[name] ? variables[name].value : null;\n",
+       "  }\n",
+       "\n",
+       "  function setReady(settings) {\n",
+       "    try {\n",
+       "      return settings.maps ? initMaps(settings.maps) : initMap(settings);\n",
+       "    } catch (e) {\n",
+       "      displayError(e);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function initMaps(maps) {\n",
+       "    return maps.map((mapSettings, mapIndex) => {\n",
+       "      return initMap(mapSettings, mapIndex);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function initMap(settings, mapIndex) {\n",
+       "    const basecolor = getBasecolorSettings(settings.basecolor);\n",
+       "    const basemapStyle =  BASEMAPS[settings.basemap] || settings.basemap || basecolor;\n",
+       "    const container = mapIndex !== undefined ? `map-${mapIndex}` : 'map';\n",
+       "    const map = createMap(container, basemapStyle, settings.bounds, settings.mapboxtoken);\n",
+       "\n",
+       "    if (settings.show_info) {\n",
+       "      updateViewport(map);\n",
+       "    }\n",
+       "\n",
+       "    if (settings.camera) {\n",
+       "      map.flyTo(settings.camera);\n",
+       "    }\n",
+       "\n",
+       "    return initLayers(map, settings, mapIndex);\n",
+       "  }\n",
+       "\n",
+       "  function initLayers(map, settings, mapIndex) {\n",
+       "    const numLayers = settings.layers.length;\n",
+       "    const hasLegends = settings.has_legends;\n",
+       "    const isStatic = settings.is_static;\n",
+       "    const layers = settings.layers;\n",
+       "    const mapLayers = getMapLayers(\n",
+       "      layers,\n",
+       "      numLayers,\n",
+       "      hasLegends,\n",
+       "      map,\n",
+       "      mapIndex\n",
+       "    );\n",
+       "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
+       "    setInteractiveLayers(map, layers, mapLayers);\n",
+       "\n",
+       "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
+       "  }\n",
+       "\n",
+       "  function waitForMapLayersLoad(isStatic, mapIndex, mapLayers) {\n",
+       "    return new Promise((resolve) => {\n",
+       "      carto.on('loaded', mapLayers, onMapLayersLoaded.bind(\n",
+       "        this, isStatic, mapIndex, mapLayers, resolve)\n",
+       "      );\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function onMapLayersLoaded(isStatic, mapIndex, mapLayers, resolve) {\n",
+       "    if (isStatic) {\n",
+       "      saveImage(mapIndex);\n",
+       "    }\n",
+       "\n",
+       "    resolve(mapLayers);\n",
+       "  }\n",
+       "\n",
+       "  function getMapLayers(layers, numLayers, hasLegends, map, mapIndex) {\n",
+       "    return layers.map((layer, layerIndex) => {\n",
+       "      return initMapLayer(layer, layerIndex, numLayers, hasLegends, map, mapIndex);\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function setInteractiveLayers(map, layers, mapLayers) {\n",
+       "    const { interactiveLayers, interactiveMapLayers } = getInteractiveLayers(layers, mapLayers);\n",
+       "\n",
+       "    if (interactiveLayers && interactiveLayers.length > 0) {\n",
+       "      setInteractivity(map, interactiveLayers, interactiveMapLayers);\n",
+       "    }\n",
+       "  }\n",
+       "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
+       "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
+       "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
+       "\n",
+       "    map.addControl(attributionControl);\n",
+       "    map.fitBounds(bounds, FIT_BOUNDS_SETTINGS);\n",
+       "\n",
+       "    return map;\n",
+       "  }\n",
+       "\n",
+       "  function createMapboxGLMap(container, style, accessToken) {\n",
+       "    if (accessToken) {\n",
+       "      mapboxgl.accessToken = accessToken;\n",
+       "    }\n",
+       "\n",
+       "    return new mapboxgl.Map({\n",
+       "      container,\n",
+       "      style,\n",
+       "      zoom: 9,\n",
+       "      dragRotate: false,\n",
+       "      attributionControl: false\n",
+       "    });\n",
+       "  }\n",
+       "\n",
+       "  function init(settings) {\n",
+       "    setReady(settings);\n",
+       "  }\n",
+       "\n",
+       "  return init;\n",
+       "\n",
+       "}());\n",
+       "</script>\n",
+       "<script>\n",
+       "  document\n",
+       "  .querySelector('as-responsive-content')\n",
+       "  .addEventListener('ready', () => {\n",
+       "    const basecolor = '';\n",
+       "    const basemap = 'Positron';\n",
+       "    const bounds = [[-122.513606194578, 37.7081003674475], [-122.358856546873, 37.814208369554]];\n",
+       "    const camera = null;\n",
+       "    const has_legends = 'true' === 'true';\n",
+       "    const is_static = 'None' === 'true';\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_road_travel\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;ve3866a&quot;, &quot;title&quot;: &quot;auto_speed&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: City of SF&quot;, &quot;prop&quot;: &quot;width&quot;, &quot;title&quot;: &quot;Road Travel Speed&quot;, &quot;type&quot;: &quot;size-bins&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@ve3866a: $auto_speed\\ncolor: opacity(pink, 0.5)\\nwidth: ramp(globalQuantiles($auto_speed, 5), [1, 10])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_road_travel\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;v08256c&quot;, &quot;title&quot;: &quot;direction&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Road Travel Direction&quot;, &quot;type&quot;: &quot;color-category&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@v08256c: $direction\\ncolor: opacity(ramp(top($direction, 11), bold),1)\\nwidth: ramp(linear(zoom(),0,18),[0.5,4])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const mapboxtoken = '';\n",
+       "    const show_info = 'None' === 'true';\n",
+       "\n",
+       "    init({\n",
+       "      basecolor,\n",
+       "      basemap,\n",
+       "      bounds,\n",
+       "      camera,\n",
+       "      has_legends,\n",
+       "      is_static,\n",
+       "      layer_selector,\n",
+       "      layers,\n",
+       "      mapboxtoken,\n",
+       "      show_info\n",
+       "    });\n",
+       "});\n",
+       "</script>\n",
+       "</html>\n",
+       "\">\n",
+       "\n",
+       "</iframe>"
+      ],
+      "text/plain": [
+       "<cartoframes.viz.map.Map at 0x116745fd0>"
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "Map([\n",
+    "    Layer(\n",
+    "        'sf_road_travel',\n",
+    "        style=size_bins_style('auto_speed', opacity=0.5, color='pink'),\n",
+    "        legends=size_bins_legend(\n",
+    "            title='Road Travel Speed',\n",
+    "            footer='Data: City of SF'\n",
+    "        )\n",
+    "    ),\n",
+    "    Layer(\n",
+    "        'sf_road_travel',\n",
+    "        style=color_category_style('direction', palette='bold'),\n",
+    "        legends=color_category_legend(\n",
+    "            title='Road Travel Direction'\n",
+    "        )\n",
+    "    )\n",
+    "])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<iframe\n",
+       "  frameborder=\"0\"\n",
+       "  style=\"\n",
+       "    border: 1px solid #cfcfcf;\n",
+       "    width: 100%;\n",
+       "    height: 632px;\n",
+       "    \"\n",
+       "  srcDoc=\"\n",
+       "  <!DOCTYPE html>\n",
+       "<html lang=&quot;en&quot;>\n",
+       "<head>\n",
+       "  <title>None</title>\n",
+       "  <meta name=&quot;description&quot; content=&quot;None&quot;>\n",
+       "  <meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;>\n",
+       "  <meta charset=&quot;UTF-8&quot;>\n",
+       "  <!-- Include CARTO VL JS -->\n",
+       "  <script src=&quot;https://libs.cartocdn.com/carto-vl/v1.4/carto-vl.min.js&quot;></script>\n",
+       "  <!-- Include Mapbox GL JS -->\n",
+       "  <script src=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.js&quot;></script>\n",
+       "  <!-- Include Mapbox GL CSS -->\n",
+       "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
+       "\n",
+       "  <!-- Include Airship -->\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "\n",
+       "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
+       "\n",
+       "  <!-- External libraries -->\n",
+       "\n",
+       "  <!-- pako -->\n",
+       "  <script src=&quot;https://libs.cartocdn.com/cartoframes/dependencies/pako_inflate.min.js&quot;></script>\n",
+       "  \n",
+       "  <!-- html2canvas -->\n",
+       "  \n",
+       "\n",
        "  \n",
+       "  <style>\n",
+       "  body {\n",
+       "    margin: 0;\n",
+       "    padding: 0;\n",
+       "  }\n",
+       "\n",
+       "  aside.as-sidebar {\n",
+       "    min-width: 300px;\n",
+       "  }\n",
+       "\n",
+       "  .map-image {\n",
+       "    display: none;\n",
+       "    max-width: 100%;\n",
+       "    height: auto;\n",
+       "  }\n",
+       "</style>\n",
+       "  <style>\n",
+       "  .map {\n",
+       "    position: absolute;\n",
+       "    height: 100%;\n",
+       "    width: 100%;\n",
+       "  }\n",
+       "\n",
+       "  .map-info {\n",
+       "    position: absolute;\n",
+       "    bottom: 0;\n",
+       "    padding: 0 5px;\n",
+       "    background-color: rgba(255, 255, 255, 0.5);\n",
+       "    margin: 0;\n",
+       "    color: rgba(0, 0, 0, 0.75);\n",
+       "    font-size: 12px;\n",
+       "    width: auto;\n",
+       "    height: 18px;\n",
+       "    font-family: 'Open Sans';\n",
+       "  }\n",
+       "\n",
+       "  .map-footer {\n",
+       "    background: #F2F6F9;\n",
+       "    font-family: Roboto;\n",
+       "    font-size: 12px;\n",
+       "    line-height: 24px;\n",
+       "    color: #162945;\n",
+       "    text-align: center;\n",
+       "    z-index: 2;\n",
+       "  }\n",
+       "\n",
+       "  .map-footer a {\n",
+       "    text-decoration: none;\n",
+       "  }\n",
+       "\n",
+       "  .map-footer a:hover {\n",
+       "    text-decoration: underline;\n",
+       "  }\n",
+       "</style>\n",
+       "    <style>\n",
+       "    #error-container {\n",
+       "      position: absolute;\n",
+       "      width: 100%;\n",
+       "      height: 100%;\n",
+       "      background-color: white;\n",
+       "      visibility: hidden;\n",
+       "      padding: 1em;\n",
+       "      font-family: &quot;Courier New&quot;, Courier, monospace;\n",
+       "      margin: 0 auto;\n",
+       "      font-size: 14px;\n",
+       "      overflow: auto;\n",
+       "      z-index: 1000;\n",
+       "      color: black;\n",
+       "    }\n",
+       "\n",
+       "    .error-section {\n",
+       "      padding: 1em;\n",
+       "      border-radius: 5px;\n",
+       "      background-color: #fee;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-highlight {\n",
+       "      font-weight: bold;\n",
+       "      color: inherit;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-type {\n",
+       "      color: #008000;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-name {\n",
+       "      color: #ba2121;\n",
+       "    }\n",
+       "\n",
+       "    #error-container #error-content {\n",
+       "      margin-top: 0.4em;\n",
+       "    }\n",
+       "\n",
+       "    .error-details {\n",
+       "      margin-top: 1em;\n",
+       "    }\n",
+       "\n",
+       "    #error-stacktrace {\n",
+       "      list-style: none;\n",
+       "    }\n",
+       "</style>\n",
+       "  <style>\n",
+       "    .popup-content {\n",
+       "      display: flex;\n",
+       "      flex-direction: column;\n",
+       "      padding: 8px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-name {\n",
+       "      font-size: 12px;\n",
+       "      font-weight: 400;\n",
+       "      line-height: 20px;\n",
+       "      margin-bottom: 4px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-value {\n",
+       "      font-size: 16px;\n",
+       "      font-weight: 600;\n",
+       "      line-height: 20px;\n",
+       "    }\n",
+       "\n",
+       "    .popup-value:not(:last-of-type) {\n",
+       "      margin-bottom: 16px;\n",
+       "    }\n",
+       "</style>\n",
+       "  <style>\n",
+       "  as-widget-header .as-widget-header__header {\n",
+       "    margin-bottom: 8px;\n",
+       "    overflow-wrap: break-word;\n",
+       "  }\n",
+       "\n",
+       "  as-widget-header .as-widget-header__subheader {\n",
+       "    margin-bottom: 12px;\n",
+       "  }\n",
+       "\n",
+       "  as-category-widget {\n",
+       "    max-height: 250px;\n",
+       "  }\n",
+       "</style>\n",
+       "</head>\n",
+       "\n",
+       "<body class=&quot;as-app-body as-app&quot;>\n",
+       "  <img id=&quot;map-image&quot; class=&quot;map-image&quot; alt='Static map image' />\n",
+       "  <as-responsive-content id=&quot;main-container&quot;>\n",
        "    \n",
+       "    <main class=&quot;as-main&quot;>\n",
+       "      <div class=&quot;as-map-area&quot;>\n",
+       "        <div id=&quot;map&quot; class=&quot;map&quot;></div>\n",
+       "        \n",
+       "        \n",
+       "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
+       "              \n",
+       "\n",
+       "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
        "    \n",
        "      \n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;Road Congestion&quot;\n",
-       "            description=&quot;Travel Direction&quot;>\n",
-       "            <as-legend-color-bins id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-bins>\n",
-       "            \n",
-       "          </as-legend>\n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;&quot;\n",
-       "            description=&quot;Travel Speeds&quot;>\n",
-       "            <as-legend-size-bins id=&quot;layer0_map0_legend1&quot; slot=&quot;legends&quot;></as-legend-size-bins>\n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
        "            \n",
-       "              <span slot=&quot;footer&quot;>Data: City of SF</span>\n",
+       "              <as-legend\n",
+       "                heading=&quot;&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-color-category id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-color-category>\n",
+       "                \n",
+       "              </as-legend>\n",
        "            \n",
-       "          </as-legend>\n",
+       "          \n",
+       "        </div>\n",
+       "      \n",
+       "    \n",
+       "      \n",
        "        \n",
+       "        \n",
+       "        <div slot=&quot;as-checkbox-layer-1-slot&quot;>\n",
+       "          \n",
+       "            \n",
+       "              <as-legend\n",
+       "                heading=&quot;&quot;\n",
+       "                description=&quot;&quot;>\n",
+       "                <as-legend-size-bins id=&quot;layer1_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-size-bins>\n",
+       "                \n",
+       "                  <span slot=&quot;footer&quot;>Data: City of SF</span>\n",
+       "                \n",
+       "              </as-legend>\n",
+       "            \n",
+       "          \n",
+       "        </div>\n",
        "      \n",
        "    \n",
-       "  \n",
+       "  </as-layer-selector>\n",
        "</div>\n",
        "            </div> <!-- as-panel -->\n",
        "          </div> <!-- as-map-panels -->\n",
@@ -259,8 +1282,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -398,9 +1419,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -517,15 +1537,16 @@
        "    interactivity.off('featureHover');\n",
        "  }\n",
        "\n",
-       "  function setPopupsClick(map, popup, interactivity, attrs) {\n",
+       "  function setPopupsClick(map, clickPopup, hoverPopup, interactivity, attrs) {\n",
        "    interactivity.on('featureClick', (event) => {\n",
-       "      updatePopup(map, popup, event, attrs);\n",
+       "      updatePopup(map, clickPopup, event, attrs);\n",
+       "      hoverPopup.remove();\n",
        "    });\n",
        "  }\n",
        "\n",
-       "  function setPopupsHover(map, popup, interactivity, attrs) {\n",
+       "  function setPopupsHover(map, hoverPopup, interactivity, attrs) {\n",
        "    interactivity.on('featureHover', (event) => {\n",
-       "      updatePopup(map, popup, event, attrs);\n",
+       "      updatePopup(map, hoverPopup, event, attrs);\n",
        "    });\n",
        "  }\n",
        "\n",
@@ -569,7 +1590,13 @@
        "\n",
        "  function setInteractivity(map, interactiveLayers, interactiveMapLayers) {\n",
        "    const interactivity = new carto.Interactivity(interactiveMapLayers);\n",
-       "    const popup = new mapboxgl.Popup({\n",
+       "\n",
+       "    const clickPopup = new mapboxgl.Popup({\n",
+       "      closeButton: true,\n",
+       "      closeOnClick: false\n",
+       "    });\n",
+       "\n",
+       "    const hoverPopup = new mapboxgl.Popup({\n",
        "      closeButton: false,\n",
        "      closeOnClick: false\n",
        "    });\n",
@@ -580,11 +1607,11 @@
        "    resetPopupHover(map);\n",
        "\n",
        "    if (clickAttrs.length > 0) {\n",
-       "      setPopupsClick(map, popup, interactivity, clickAttrs);\n",
+       "      setPopupsClick(map, clickPopup, hoverPopup, interactivity, clickAttrs);\n",
        "    }\n",
        "\n",
        "    if (hoverAttrs.length > 0) {\n",
-       "      setPopupsHover(map, popup, interactivity, hoverAttrs);\n",
+       "      setPopupsHover(map, hoverPopup, interactivity, hoverAttrs);\n",
        "    }\n",
        "  }\n",
        "\n",
@@ -679,8 +1706,9 @@
        "      const othersLabel = 'Others';   // TODO: i18n\n",
        "      const prop = legend.prop;\n",
        "      const dynamic = legend.dynamic;\n",
+       "      const order = legend.ascending ? 'ASC' : 'DESC';\n",
        "      const variable = legend.variable;\n",
-       "      const config = { othersLabel, variable };\n",
+       "      const config = { othersLabel, variable, order };\n",
        "      const options = { format, config, dynamic };\n",
        "\n",
        "      if (legend.type.startsWith('size-continuous')) {\n",
@@ -700,7 +1728,10 @@
        "  }\n",
        "\n",
        "  function GeoJSON(layer) {\n",
-       "    return new carto.source.GeoJSON(_decodeJSONData(layer.data));\n",
+       "    const options = JSON.parse(JSON.stringify(layer.options));\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
+       "\n",
+       "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
        "\n",
        "  function Query(layer) {\n",
@@ -720,8 +1751,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -846,6 +1889,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -881,6 +1928,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -922,7 +1984,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'true' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_road_travel\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [], &quot;legends&quot;: [{&quot;description&quot;: &quot;Travel Direction&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;Road Congestion&quot;, &quot;type&quot;: &quot;color-bins&quot;, &quot;variable&quot;: null}, {&quot;description&quot;: &quot;Travel Speeds&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: City of SF&quot;, &quot;prop&quot;: &quot;width&quot;, &quot;title&quot;: &quot;&quot;, &quot;type&quot;: &quot;size-bins&quot;, &quot;variable&quot;: null}], &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;\\n    color: ramp($direction, bold)\\n    width: ramp(globalQuantiles($auto_speed, 3), [1,6])\\n    &quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'true' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_road_travel\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;ve3866a&quot;, &quot;title&quot;: &quot;Road Travel Speed&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Data: City of SF&quot;, &quot;prop&quot;: &quot;width&quot;, &quot;title&quot;: &quot;&quot;, &quot;type&quot;: &quot;size-bins&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: &quot;Road Travel Speed&quot;, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@ve3866a: $auto_speed\\ncolor: opacity(pink, 0.5)\\nwidth: ramp(globalQuantiles($auto_speed, 5), [1, 10])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_road_travel\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;v08256c&quot;, &quot;title&quot;: &quot;Road Travel Speed&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;ascending&quot;: false, &quot;description&quot;: &quot;&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;&quot;, &quot;prop&quot;: &quot;color&quot;, &quot;title&quot;: &quot;&quot;, &quot;type&quot;: &quot;color-category&quot;, &quot;variable&quot;: null}], &quot;options&quot;: {}, &quot;title&quot;: &quot;Road Travel Speed&quot;, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@v08256c: $direction\\ncolor: opacity(ramp(top($direction, 11), bold),1)\\nwidth: ramp(linear(zoom(),0,18),[0.5,4])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -933,6 +1996,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -945,32 +2009,31 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.layer.Layer at 0x7efd34ef60f0>"
+       "<cartoframes.viz.map.Map at 0x11677e410>"
       ]
      },
-     "execution_count": 2,
+     "execution_count": 3,
      "metadata": {},
      "output_type": "execute_result"
     }
    ],
    "source": [
-    "Layer(\n",
-    "    'sf_road_travel',\n",
-    "    '''\n",
-    "    color: ramp($direction, bold)\n",
-    "    width: ramp(globalQuantiles($auto_speed, 3), [1,6])\n",
-    "    ''',\n",
-    "    legends=[\n",
-    "        color_bins_legend(\n",
-    "            title='Road Congestion',\n",
-    "            description='Travel Direction'\n",
-    "        ),\n",
-    "        size_bins_legend(\n",
-    "            description='Travel Speeds',\n",
+    "Map([\n",
+    "    Layer(\n",
+    "        'sf_road_travel',\n",
+    "        title='Road Travel Speed',\n",
+    "        style=size_bins_style('auto_speed', opacity=0.5, color='pink'),\n",
+    "        legends=size_bins_legend(\n",
     "            footer='Data: City of SF'\n",
     "        )\n",
-    "    ]\n",
-    ")"
+    "    ),\n",
+    "    Layer(\n",
+    "        'sf_road_travel',\n",
+    "        title='Road Travel Speed',\n",
+    "        style=color_category_style('direction', palette='bold')\n",
+    "    )\n",
+    "],\n",
+    "layer_selector=True)"
    ]
   }
  ],
@@ -990,7 +2053,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.7"
+   "version": "3.7.4"
   }
  },
  "nbformat": 4,
diff --git a/examples/legends/size_continuous_legend.ipynb b/examples/legends/size_continuous_legend.ipynb
index 05bfd318..cfc43cc7 100644
--- a/examples/legends/size_continuous_legend.ipynb
+++ b/examples/legends/size_continuous_legend.ipynb
@@ -56,11 +56,11 @@
        "  <link href=&quot;https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css&quot; rel=&quot;stylesheet&quot; />\n",
        "\n",
        "  <!-- Include Airship -->\n",
-       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship.js&quot;></script>\n",
-       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.2.0-rc.2.2/airship/airship.esm.js&quot;></script>\n",
-       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.2.0-rc.2.2/asbridge.min.js&quot;></script>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.2.0-rc.2.2/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
-       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.2.0-rc.2.2/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <script nomodule=&quot;&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship.js&quot;></script>\n",
+       "  <script type=&quot;module&quot; src=&quot;https://libs.cartocdn.com/airship-components/v2.3/airship/airship.esm.js&quot;></script>\n",
+       "  <script src=&quot;https://libs.cartocdn.com/airship-bridge/v2.3/asbridge.min.js&quot;></script>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-style/v2.3/airship.min.css&quot; rel=&quot;stylesheet&quot;>\n",
+       "  <link href=&quot;https://libs.cartocdn.com/airship-icons/v2.3/icons.css&quot; rel=&quot;stylesheet&quot;>\n",
        "\n",
        "  <link href=&quot;https://fonts.googleapis.com/css?family=Roboto&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;>\n",
        "\n",
@@ -224,27 +224,32 @@
        "        \n",
        "        \n",
        "          <div class=&quot;as-map-panels&quot; data-name=&quot;Legends&quot;>\n",
-       "            <div class=&quot;as-panel as-panel--left as-panel--top&quot;>\n",
+       "            <div class=&quot;as-panel as-panel--vertical as-panel--left as-panel--top&quot;>\n",
        "              \n",
        "\n",
        "<div class=&quot;as-panel__element&quot; id=&quot;legends&quot;>\n",
-       "  \n",
-       "    \n",
+       "  <as-layer-selector id=&quot;layer-selector&quot;>\n",
        "    \n",
        "      \n",
        "        \n",
-       "          <as-legend\n",
-       "            heading=&quot;Crime Counts&quot;\n",
-       "            description=&quot;by neighborhood&quot;>\n",
-       "            <as-legend-size-continuous id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-size-continuous>\n",
+       "        \n",
+       "        <div slot=&quot;as-checkbox-layer-0-slot&quot;>\n",
+       "          \n",
        "            \n",
-       "              <span slot=&quot;footer&quot;>Source: City of SF</span>\n",
+       "              <as-legend\n",
+       "                heading=&quot;Crime Counts&quot;\n",
+       "                description=&quot;by neighborhood&quot;>\n",
+       "                <as-legend-size-continuous id=&quot;layer0_map0_legend0&quot; slot=&quot;legends&quot;></as-legend-size-continuous>\n",
+       "                \n",
+       "                  <span slot=&quot;footer&quot;>Source: City of SF</span>\n",
+       "                \n",
+       "              </as-legend>\n",
        "            \n",
-       "          </as-legend>\n",
-       "        \n",
+       "          \n",
+       "        </div>\n",
        "      \n",
        "    \n",
-       "  \n",
+       "  </as-layer-selector>\n",
        "</div>\n",
        "            </div> <!-- as-panel -->\n",
        "          </div> <!-- as-map-panels -->\n",
@@ -256,8 +261,6 @@
        "  \n",
        "\n",
        "  <div id=&quot;error-container&quot; class=&quot;error&quot;>\n",
-       "  <p>There is a <span class=&quot;errors&quot; id=&quot;error-highlight&quot;></span>\n",
-       "  from the <a href=&quot;https://carto.com/developers/carto-vl/&quot; target=&quot;_blank&quot;>CARTO VL</a> library:</p>\n",
        "  <section class=&quot;error-section&quot;>\n",
        "    <span class=&quot;errors&quot; id=&quot;error-name&quot;></span>:\n",
        "    <section id=&quot;error-content&quot;>\n",
@@ -395,9 +398,8 @@
        "    const stacktrace$ = document.getElementById('error-stacktrace');\n",
        "\n",
        "    errors$[0].innerHTML = e.name;\n",
-       "    errors$[1].innerHTML = e.name;\n",
-       "    errors$[2].innerHTML = e.type;\n",
-       "    errors$[3].innerHTML = e.message.replace(e.type, '');\n",
+       "    errors$[1].innerHTML = e.type;\n",
+       "    errors$[2].innerHTML = e.message.replace(e.type, '');\n",
        "\n",
        "    error$.style.visibility = 'visible';\n",
        "\n",
@@ -705,7 +707,7 @@
        "\n",
        "  function GeoJSON(layer) {\n",
        "    const options = JSON.parse(JSON.stringify(layer.options));\n",
-       "    const data = _decodeJSONData(layer.data);\n",
+       "    const data = _decodeJSONData(layer.data, layer.encode_data);\n",
        "\n",
        "    return new carto.source.GeoJSON(data, options);\n",
        "  }\n",
@@ -727,8 +729,20 @@
        "    return new carto.source.MVT(layer.data.file, JSON.parse(layer.data.metadata));\n",
        "  }\n",
        "\n",
-       "  function _decodeJSONData(b64Data) {\n",
-       "    return JSON.parse(pako.inflate(atob(b64Data), { to: 'string' }));\n",
+       "  function _decodeJSONData(data, encodeData) {\n",
+       "    try {\n",
+       "      if (encodeData) {\n",
+       "        const decodedJSON = pako.inflate(atob(data), { to: 'string' });\n",
+       "        return JSON.parse(decodedJSON);\n",
+       "      } else {\n",
+       "        return JSON.parse(data);\n",
+       "      }\n",
+       "    } catch(error) {\n",
+       "      throw new Error(`\n",
+       "      Error: &quot;${error}&quot;. CARTOframes is not able to parse your local data because it is too large.\n",
+       "      Please, disable the data compresion with encode_data=False in your Layer class.\n",
+       "    `);\n",
+       "    }\n",
        "  }\n",
        "\n",
        "  const factory = new SourceFactory();\n",
@@ -853,6 +867,10 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
+       "    if (settings.layer_selector) {\n",
+       "      addLayersSelector(layers.reverse(), mapLayers.reverse());\n",
+       "    }\n",
+       "\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -888,6 +906,21 @@
        "    }\n",
        "  }\n",
        "\n",
+       "  function addLayersSelector(layers, mapLayers) {\n",
+       "    const layerSelector$ = document.querySelector(`#layer-selector`);\n",
+       "      const layersInfo = mapLayers.map((layer, index) => {\n",
+       "        return {\n",
+       "          title: layers[index].title || `Layer ${index}`,\n",
+       "          id: layer.id,\n",
+       "          checked: true\n",
+       "        };\n",
+       "      });\n",
+       "    \n",
+       "    const layerSelector = new AsBridge.VL.Layers(layerSelector$, carto, layersInfo, mapLayers);\n",
+       "    \n",
+       "    layerSelector.build();\n",
+       "  }\n",
+       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -929,7 +962,8 @@
        "    const camera = null;\n",
        "    const has_legends = 'true' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_nbhd_crime\\&quot;&quot;, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;v101966&quot;, &quot;title&quot;: &quot;value&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;by neighborhood&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Source: City of SF&quot;, &quot;prop&quot;: &quot;width&quot;, &quot;title&quot;: &quot;Crime Counts&quot;, &quot;type&quot;: &quot;size-continuous&quot;, &quot;variable&quot;: &quot;size_value&quot;}], &quot;options&quot;: {}, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@v101966: $value\\ncolor: opacity(#FFB927, 0.8)\\nwidth: ramp(linear(sqrt($value), sqrt(globalMIN($value)), sqrt(globalMAX($value))), [8, 40])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\n@size_value: ramp(linear($value, globalMIN($value), globalMAX($value)), [8, 40])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layer_selector = 'False' === 'true'; // TODO it will be a helper\n",
+       "    const layers = [{&quot;credentials&quot;: {&quot;api_key&quot;: &quot;default_public&quot;, &quot;base_url&quot;: &quot;https://cartoframes.carto.com&quot;, &quot;username&quot;: &quot;cartoframes&quot;}, &quot;data&quot;: &quot;SELECT * FROM \\&quot;cartoframes\\&quot;.\\&quot;sf_nbhd_crime\\&quot;&quot;, &quot;encode_data&quot;: true, &quot;has_legend_list&quot;: true, &quot;interactivity&quot;: [{&quot;attrs&quot;: {&quot;name&quot;: &quot;v101966&quot;, &quot;title&quot;: &quot;value&quot;}, &quot;event&quot;: &quot;hover&quot;}], &quot;legends&quot;: [{&quot;description&quot;: &quot;by neighborhood&quot;, &quot;dynamic&quot;: true, &quot;footer&quot;: &quot;Source: City of SF&quot;, &quot;prop&quot;: &quot;width&quot;, &quot;title&quot;: &quot;Crime Counts&quot;, &quot;type&quot;: &quot;size-continuous&quot;, &quot;variable&quot;: &quot;size_value&quot;}], &quot;options&quot;: {}, &quot;title&quot;: null, &quot;type&quot;: &quot;Query&quot;, &quot;viz&quot;: &quot;@v101966: $value\\ncolor: opacity(#FFB927, 0.8)\\nwidth: ramp(linear(sqrt($value), sqrt(globalMIN($value)), sqrt(globalMAX($value))), [8, 40])\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\n@size_value: ramp(linear($value, globalMIN($value), globalMAX($value)), [8, 40])\\nfilter: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -940,6 +974,7 @@
        "      camera,\n",
        "      has_legends,\n",
        "      is_static,\n",
+       "      layer_selector,\n",
        "      layers,\n",
        "      mapboxtoken,\n",
        "      show_info\n",
@@ -952,7 +987,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.layer.Layer at 0x1161d5ad0>"
+       "<cartoframes.viz.layer.Layer at 0x11408b990>"
       ]
      },
      "execution_count": 2,
diff --git a/guides/01-Installation.ipynb b/guides/01-Installation.ipynb
index cb706c02..216ab742 100644
--- a/guides/01-Installation.ipynb
+++ b/guides/01-Installation.ipynb
@@ -6,7 +6,7 @@
    "source": [
     "## Installation\n",
     "\n",
-    "This guide is intended for those who are using CARTOframes for the first time and provides the steps to install it **locally**, in a **Jupyter Notebook**, or using a **Virtual Environment**."
+    "This guide is intended for those who are using CARTOframes for the first time and provides the steps to install it **locally**, in a **Jupyter Notebook**, or using a **Virtual Environment**. You need **Python 3** to use CARTOframes."
    ]
   },
   {
@@ -117,7 +117,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.7"
+   "version": "3.7.3"
   }
  },
  "nbformat": 4,
diff --git a/guides/03-Quickstart.ipynb b/guides/03-Quickstart.ipynb
index a2787467..a468d57f 100644
--- a/guides/03-Quickstart.ipynb
+++ b/guides/03-Quickstart.ipynb
@@ -10,7 +10,7 @@
     "\n",
     "Hi! Glad to see you made it to the Quickstart guide! In this guide you are introduced to how CARTOframes can be used by data scientists in spatial analysis workflows. Using simulated Starbucks revenue data, this guide walks through some common steps a data scientist takes to answer the following question: which stores are performing better than others?\n",
     "\n",
-    "Before you get started, we encourage you to have CARTOframes installed so you can get a feel for the library by using it:"
+    "Before you get started, we encourage you to have CARTOframes and Python 3 installed so you can get a feel for the library by using it:"
    ]
   },
   {
@@ -53,7 +53,7 @@
    "source": [
     "### Get and explore your company's data\n",
     "\n",
-    "[Use this dataset](http://libs.cartocdn.com/cartoframes/files/starbucks_brooklyn.csv) to start your exploration. It contains information about the location of Starbucks and each store's annual revenue.\n",
+    "We are going to use a dataset that contains information about the location of Starbucks and each store's annual revenue.\n",
     "\n",
     "As a first exploratory step, read it into a Jupyter Notebook using [pandas](https://pandas.pydata.org/)."
    ]
@@ -6031,7 +6031,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.7"
+   "version": "3.7.3"
   }
  },
  "nbformat": 4,
diff --git a/package.json b/package.json
index b39f251e..026af253 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cartoframes",
-  "version": "1.0.1",
+  "version": "1.0.0",
   "description": "*********** CARTOframes ***********",
   "main": "./cartoframes/assets/src/index.js",
   "directories": {


Test patch:
diff --git a/tests/unit/viz/test_legend.py b/tests/unit/viz/test_legend.py
index 29f15814..f7185afa 100644
--- a/tests/unit/viz/test_legend.py
+++ b/tests/unit/viz/test_legend.py
@@ -32,6 +32,7 @@ class TestLegend(object):
                         footer='[footer]')
 
         assert legend.get_info() == {
+            'ascending': False,
             'type': 'color-category',
             'prop': 'color',
             'title': '[title]',