Repository: CartoDB/cartoframes
Pull request #1317 (2019-12-04 15:18:07)

Remove cartodb_id in DS output
Closes https://github.com/CartoDB/cartoframes/issues/1302

NOTE: DS tests are e2e, not unit, so we need to create a task to generate all the unit tests for DS. 


Patch:
diff --git a/cartoframes/data/observatory/catalog/dataset.py b/cartoframes/data/observatory/catalog/dataset.py
index aa203c9a..9076c453 100644
--- a/cartoframes/data/observatory/catalog/dataset.py
+++ b/cartoframes/data/observatory/catalog/dataset.py
@@ -246,7 +246,7 @@ class Dataset(CatalogEntity):
     def summary(self):
         """JSON object with extra metadata that summarizes different properties of the dataset content."""
 
-        return self._get_summary_data()
+        return self.data['summary_json']
 
     def head(self):
         """Returns a sample of the 10 first rows of the dataset data.
@@ -257,8 +257,8 @@ class Dataset(CatalogEntity):
             pandas.DataFrame
         """
 
-        data = self._get_summary_data()
-        return head(self.__class__, data) if data else None
+        data = self.data['summary_json']
+        return head(self.__class__, data)
 
     def tail(self):
         """"Returns the last ten rows of the dataset"
@@ -268,9 +268,8 @@ class Dataset(CatalogEntity):
         Returns:
             pandas.DataFrame
         """
-
-        data = self._get_summary_data()
-        return tail(self.__class__, data) if data else None
+        data = self.data['summary_json']
+        return tail(self.__class__, data)
 
     def counts(self):
         """Returns a summary of different counts over the actual dataset data.
@@ -288,8 +287,8 @@ class Dataset(CatalogEntity):
                 # null_cells_percent:   percent of cells with null value in the dataset
         """
 
-        data = self._get_summary_data()
-        return counts(data) if data else None
+        data = self.data['summary_json']
+        return counts(data)
 
     def fields_by_type(self):
         """Returns a summary of the number of columns per data type in the dataset.
@@ -305,9 +304,8 @@ class Dataset(CatalogEntity):
                 # string       number of columns with type string in the dataset
                 # integer      number of columns with type integer in the dataset
         """
-
-        data = self._get_summary_data()
-        return fields_by_type(data) if data else None
+        data = self.data['summary_json']
+        return fields_by_type(data)
 
     def geom_coverage(self):
         """Shows a map to visualize the geographical coverage of the dataset.
@@ -487,12 +485,3 @@ class Dataset(CatalogEntity):
         datasets = Dataset.get_all({}, _credentials)
 
         return self in datasets
-
-    def _get_summary_data(self):
-        data = self.data.get('summary_json')
-
-        if data:
-            return data
-        else:
-            print('Summary information is not available')
-            return None
diff --git a/cartoframes/data/services/geocoding.py b/cartoframes/data/services/geocoding.py
index f3c9a228..5a6ce284 100644
--- a/cartoframes/data/services/geocoding.py
+++ b/cartoframes/data/services/geocoding.py
@@ -240,6 +240,9 @@ class Geocoding(Service):
 
         cdf = read_carto(input_table_name, self._credentials)
 
+        if self._source_manager.is_dataframe():
+            del cdf['cartodb_id']
+
         if is_temporary:
             delete_table(input_table_name, self._credentials, log_enabled=False)
 
diff --git a/cartoframes/data/services/isolines.py b/cartoframes/data/services/isolines.py
index aead9f8c..e683c67e 100644
--- a/cartoframes/data/services/isolines.py
+++ b/cartoframes/data/services/isolines.py
@@ -184,6 +184,9 @@ class Isolines(Service):
             # save result in a table
             to_carto(cdf, table_name, self._credentials, if_exists, log_enabled=dry_run)
 
+        if source_manager.is_dataframe():
+            del cdf['cartodb_id']
+
         if temporary_table_name:
             delete_table(temporary_table_name, self._credentials, log_enabled=False)
 
diff --git a/examples/data_services/geocoding_dataframe.ipynb b/examples/data_services/geocoding_dataframe.ipynb
index 08fa75be..8960433b 100644
--- a/examples/data_services/geocoding_dataframe.ipynb
+++ b/examples/data_services/geocoding_dataframe.ipynb
@@ -131,7 +131,6 @@
        "  <thead>\n",
        "    <tr style=\"text-align: right;\">\n",
        "      <th></th>\n",
-       "      <th>cartodb_id</th>\n",
        "      <th>the_geom</th>\n",
        "      <th>address</th>\n",
        "      <th>city</th>\n",
@@ -142,7 +141,6 @@
        "  <tbody>\n",
        "    <tr>\n",
        "      <td>0</td>\n",
-       "      <td>1</td>\n",
        "      <td>POINT (-3.70588 40.42049)</td>\n",
        "      <td>Gran Vía 46</td>\n",
        "      <td>Madrid</td>\n",
@@ -151,7 +149,6 @@
        "    </tr>\n",
        "    <tr>\n",
        "      <td>1</td>\n",
-       "      <td>2</td>\n",
        "      <td>POINT (-5.98312 37.35547)</td>\n",
        "      <td>Ebro 1</td>\n",
        "      <td>Sevilla</td>\n",
@@ -163,9 +160,9 @@
        "</div>"
       ],
       "text/plain": [
-       "   cartodb_id                   the_geom      address     city  gc_status_rel  \\\n",
-       "0           1  POINT (-3.70588 40.42049)  Gran Vía 46   Madrid           0.84   \n",
-       "1           2  POINT (-5.98312 37.35547)       Ebro 1  Sevilla           0.70   \n",
+       "                    the_geom      address     city  gc_status_rel  \\\n",
+       "0  POINT (-3.70588 40.42049)  Gran Vía 46   Madrid           0.84   \n",
+       "1  POINT (-5.98312 37.35547)       Ebro 1  Sevilla           0.70   \n",
        "\n",
        "                 carto_geocode_hash  \n",
        "0  95e4f39284efeab8e759aaa547d84567  \n",
@@ -447,142 +444,6 @@
        "\n",
        "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\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 createDefaultLegend(layers) {\n",
-       "    const defaultLegendContainer = document.getElementById('default-legend-container');\n",
-       "    defaultLegendContainer.style.display = 'none';\n",
-       "\n",
-       "    AsBridge.VL.Legends.layersLegend(\n",
-       "      '#default-legend',\n",
-       "      layers,\n",
-       "      {\n",
-       "        onLoad: () => defaultLegendContainer.style.display = 'unset'\n",
-       "      }\n",
-       "    );\n",
-       "  }\n",
-       "\n",
-       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend`);\n",
-       "    \n",
-       "    if (legendData.length) {\n",
-       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legendData, 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",
        "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
        "\n",
        "  /**\n",
@@ -706,6 +567,100 @@
        "    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",
@@ -861,6 +816,33 @@
        "      : null;\n",
        "  }\n",
        "\n",
+       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
+       "    if (legendData.length) {\n",
+       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legendData, 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",
@@ -1006,7 +988,6 @@
        "  function initLayers(map, settings, mapIndex) {\n",
        "    const numLayers = settings.layers.length;\n",
        "    const hasLegends = settings.has_legends;\n",
-       "    const isDefaultLegend = settings.default_legend;\n",
        "    const isStatic = settings.is_static;\n",
        "    const layers = settings.layers;\n",
        "    const mapLayers = getMapLayers(\n",
@@ -1017,7 +998,6 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
-       "    createLegend$1(isDefaultLegend, mapLayers);\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -1053,12 +1033,6 @@
        "    }\n",
        "  }\n",
        "\n",
-       "  function createLegend$1(isDefaultLegend, mapLayers) {\n",
-       "    if (isDefaultLegend) {\n",
-       "      createDefaultLegend(mapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -1101,7 +1075,7 @@
        "    const default_legend = 'False' === 'true';\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAFQ54V0C/62NPw/CIBBHv8vNSGiBQLuaOLubDk17GpLKNXgODeG7i/8Gd8d7eb97GXhbEXo44Mj3hHtaFpw4UAQB5ze7QX/KEOZqqUp/BxWsiVZMHJ5iLgIuSFfktNXrKx8pRK7qRJTmEEd+Pd1p6ZT1XhglTatMN5QiPqXmvyUrO6+bVmgntbXG1dJQHq61Lu39AAAA&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 20&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIABDE510C/62NPw/CIBBHv8vNSGiBQLuaOLubDk17GpLKNXgODeG7i/8Gd8d7eb97GXhbEXo44Mj3hHtaFpw4UAQB5ze7QX/KEOZqqUp/BxWsiVZMHJ5iLgIuSFfktNXrKx8pRK7qRJTmEEd+Pd1p6ZT1XhglTatMN5QiPqXmvyUrO6+bVmgntbXG1dJQHq61Lu39AAAA&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 20&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -1125,7 +1099,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x7ff82c1b4668>"
+       "<cartoframes.viz.map.Map at 0x7ff6982530f0>"
       ]
      },
      "execution_count": 6,
diff --git a/examples/data_services/isochrones.ipynb b/examples/data_services/isochrones.ipynb
index 7fafd8f7..f4e90c9f 100644
--- a/examples/data_services/isochrones.ipynb
+++ b/examples/data_services/isochrones.ipynb
@@ -283,142 +283,6 @@
        "\n",
        "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\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 createDefaultLegend(layers) {\n",
-       "    const defaultLegendContainer = document.getElementById('default-legend-container');\n",
-       "    defaultLegendContainer.style.display = 'none';\n",
-       "\n",
-       "    AsBridge.VL.Legends.layersLegend(\n",
-       "      '#default-legend',\n",
-       "      layers,\n",
-       "      {\n",
-       "        onLoad: () => defaultLegendContainer.style.display = 'unset'\n",
-       "      }\n",
-       "    );\n",
-       "  }\n",
-       "\n",
-       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend`);\n",
-       "    \n",
-       "    if (legendData.length) {\n",
-       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legendData, 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",
        "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
        "\n",
        "  /**\n",
@@ -542,6 +406,100 @@
        "    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",
@@ -697,6 +655,33 @@
        "      : null;\n",
        "  }\n",
        "\n",
+       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
+       "    if (legendData.length) {\n",
+       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legendData, 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",
@@ -842,7 +827,6 @@
        "  function initLayers(map, settings, mapIndex) {\n",
        "    const numLayers = settings.layers.length;\n",
        "    const hasLegends = settings.has_legends;\n",
-       "    const isDefaultLegend = settings.default_legend;\n",
        "    const isStatic = settings.is_static;\n",
        "    const layers = settings.layers;\n",
        "    const mapLayers = getMapLayers(\n",
@@ -853,7 +837,6 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
-       "    createLegend$1(isDefaultLegend, mapLayers);\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -889,12 +872,6 @@
        "    }\n",
        "  }\n",
        "\n",
-       "  function createLegend$1(isDefaultLegend, mapLayers) {\n",
-       "    if (isDefaultLegend) {\n",
-       "      createDefaultLegend(mapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -937,7 +914,7 @@
        "    const default_legend = 'False' === 'true';\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAGM54V0C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&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]))\\nopacity: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAIfM510C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&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]))\\nopacity: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -961,7 +938,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x7fdff1080da0>"
+       "<cartoframes.viz.map.Map at 0x7fe0c9f1d0b8>"
       ]
      },
      "execution_count": 3,
@@ -990,12 +967,135 @@
     "from cartoframes.data.services import Isolines\n",
     "\n",
     "iso_service = Isolines()\n",
-    "isochrones = iso_service.isochrones(cdf, [100, 200, 300], mode='walk')"
+    "isochrones_cdf, isochrones_metadata = iso_service.isochrones(cdf, [100, 200, 300], mode='walk')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source_id</th>\n",
+       "      <th>data_range</th>\n",
+       "      <th>lower_data_range</th>\n",
+       "      <th>the_geom</th>\n",
+       "      <th>range_label</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68849 40.42532, -3.68860 40...</td>\n",
+       "      <td>2 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>1</td>\n",
+       "      <td>2</td>\n",
+       "      <td>200</td>\n",
+       "      <td>100</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68901 40.42543, -3.68890 40...</td>\n",
+       "      <td>3 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>2</td>\n",
+       "      <td>3</td>\n",
+       "      <td>300</td>\n",
+       "      <td>200</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68910 40.42422, -3.69121 40...</td>\n",
+       "      <td>5 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>3</td>\n",
+       "      <td>4</td>\n",
+       "      <td>100</td>\n",
+       "      <td>0</td>\n",
+       "      <td>MULTIPOLYGON (((-3.69500 40.41181, -3.69511 40...</td>\n",
+       "      <td>2 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>4</td>\n",
+       "      <td>5</td>\n",
+       "      <td>200</td>\n",
+       "      <td>100</td>\n",
+       "      <td>POLYGON ((-3.69568 40.41163, -3.69611 40.41267...</td>\n",
+       "      <td>3 min.</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   source_id  data_range  lower_data_range  \\\n",
+       "0          1         100                 0   \n",
+       "1          2         200               100   \n",
+       "2          3         300               200   \n",
+       "3          4         100                 0   \n",
+       "4          5         200               100   \n",
+       "\n",
+       "                                            the_geom range_label  \n",
+       "0  MULTIPOLYGON (((-3.68849 40.42532, -3.68860 40...      2 min.  \n",
+       "1  MULTIPOLYGON (((-3.68901 40.42543, -3.68890 40...      3 min.  \n",
+       "2  MULTIPOLYGON (((-3.68910 40.42422, -3.69121 40...      5 min.  \n",
+       "3  MULTIPOLYGON (((-3.69500 40.41181, -3.69511 40...      2 min.  \n",
+       "4  POLYGON ((-3.69568 40.41163, -3.69611 40.41267...      3 min.  "
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "isochrones_cdf.head(5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'required_quota': 21}"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "isochrones_metadata"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [
     {
@@ -1231,142 +1331,6 @@
        "\n",
        "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\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 createDefaultLegend(layers) {\n",
-       "    const defaultLegendContainer = document.getElementById('default-legend-container');\n",
-       "    defaultLegendContainer.style.display = 'none';\n",
-       "\n",
-       "    AsBridge.VL.Legends.layersLegend(\n",
-       "      '#default-legend',\n",
-       "      layers,\n",
-       "      {\n",
-       "        onLoad: () => defaultLegendContainer.style.display = 'unset'\n",
-       "      }\n",
-       "    );\n",
-       "  }\n",
-       "\n",
-       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend`);\n",
-       "    \n",
-       "    if (legendData.length) {\n",
-       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legendData, 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",
        "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
        "\n",
        "  /**\n",
@@ -1490,6 +1454,100 @@
        "    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",
@@ -1645,6 +1703,33 @@
        "      : null;\n",
        "  }\n",
        "\n",
+       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
+       "    if (legendData.length) {\n",
+       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legendData, 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",
@@ -1790,7 +1875,6 @@
        "  function initLayers(map, settings, mapIndex) {\n",
        "    const numLayers = settings.layers.length;\n",
        "    const hasLegends = settings.has_legends;\n",
-       "    const isDefaultLegend = settings.default_legend;\n",
        "    const isStatic = settings.is_static;\n",
        "    const layers = settings.layers;\n",
        "    const mapLayers = getMapLayers(\n",
@@ -1801,7 +1885,6 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
-       "    createLegend$1(isDefaultLegend, mapLayers);\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -1837,12 +1920,6 @@
        "    }\n",
        "  }\n",
        "\n",
-       "  function createLegend$1(isDefaultLegend, mapLayers) {\n",
-       "    if (isDefaultLegend) {\n",
-       "      createDefaultLegend(mapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -1880,12 +1957,12 @@
        "  .addEventListener('ready', () => {\n",
        "    const basecolor = '';\n",
        "    const basemap = 'Positron';\n",
-       "    const bounds = [[-3.71044, 40.40783], [-3.68521, 40.46872]];\n",
+       "    const bounds = [[-3.71044, 40.40782], [-3.68521, 40.46872]];\n",
        "    const camera = null;\n",
        "    const default_legend = 'False' === 'true';\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;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&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;strokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nopacity: 0.9\\ncolor: opacity(green, 0.3)&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAG054V0C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 3&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;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&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;strokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nopacity: 0.9\\ncolor: opacity(green, 0.3)&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAJDM510C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 3&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -1909,10 +1986,10 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x7fdff0a75978>"
+       "<cartoframes.viz.map.Map at 0x7fe0c8fed780>"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1921,7 +1998,7 @@
     "from cartoframes.viz import Map, Layer\n",
     "\n",
     "Map([\n",
-    "    Layer(isochrones.data, 'color: opacity(green, 0.3)'),\n",
+    "    Layer(isochrones_cdf, 'color: opacity(green, 0.3)'),\n",
     "    Layer(cdf, 'width: 3')\n",
     "])"
    ]
diff --git a/examples/data_services/isodistances.ipynb b/examples/data_services/isodistances.ipynb
index 94ca3245..c9280819 100644
--- a/examples/data_services/isodistances.ipynb
+++ b/examples/data_services/isodistances.ipynb
@@ -19,7 +19,7 @@
    "source": [
     "from cartoframes.auth import set_default_credentials\n",
     "\n",
-    "set_default_credentials('../_debug/creds.json')"
+    "set_default_credentials('creds.json')"
    ]
   },
   {
@@ -283,142 +283,6 @@
        "\n",
        "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\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 createDefaultLegend(layers) {\n",
-       "    const defaultLegendContainer = document.getElementById('default-legend-container');\n",
-       "    defaultLegendContainer.style.display = 'none';\n",
-       "\n",
-       "    AsBridge.VL.Legends.layersLegend(\n",
-       "      '#default-legend',\n",
-       "      layers,\n",
-       "      {\n",
-       "        onLoad: () => defaultLegendContainer.style.display = 'unset'\n",
-       "      }\n",
-       "    );\n",
-       "  }\n",
-       "\n",
-       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend`);\n",
-       "    \n",
-       "    if (legendData.length) {\n",
-       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legendData, 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",
        "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
        "\n",
        "  /**\n",
@@ -542,6 +406,100 @@
        "    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",
@@ -697,6 +655,33 @@
        "      : null;\n",
        "  }\n",
        "\n",
+       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
+       "    if (legendData.length) {\n",
+       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legendData, 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",
@@ -842,7 +827,6 @@
        "  function initLayers(map, settings, mapIndex) {\n",
        "    const numLayers = settings.layers.length;\n",
        "    const hasLegends = settings.has_legends;\n",
-       "    const isDefaultLegend = settings.default_legend;\n",
        "    const isStatic = settings.is_static;\n",
        "    const layers = settings.layers;\n",
        "    const mapLayers = getMapLayers(\n",
@@ -853,7 +837,6 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
-       "    createLegend$1(isDefaultLegend, mapLayers);\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -889,12 +872,6 @@
        "    }\n",
        "  }\n",
        "\n",
-       "  function createLegend$1(isDefaultLegend, mapLayers) {\n",
-       "    if (isDefaultLegend) {\n",
-       "      createDefaultLegend(mapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -937,7 +914,7 @@
        "    const default_legend = 'False' === 'true';\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAHw54V0C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&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]))\\nopacity: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIACjM510C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&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]))\\nopacity: 1\\n&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -961,7 +938,7 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x7f5a77c43e80>"
+       "<cartoframes.viz.map.Map at 0x7f8db46ef5f8>"
       ]
      },
      "execution_count": 3,
@@ -990,12 +967,135 @@
     "from cartoframes.data.services import Isolines\n",
     "\n",
     "iso_service = Isolines()\n",
-    "isodistances = iso_service.isodistances(cdf, [1200, 2400, 3600])"
+    "isodistances_cdf, isodistances_metadata = iso_service.isodistances(cdf, [1200, 2400, 3600])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>source_id</th>\n",
+       "      <th>data_range</th>\n",
+       "      <th>lower_data_range</th>\n",
+       "      <th>the_geom</th>\n",
+       "      <th>range_label</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68847 40.42473, -3.68855 40...</td>\n",
+       "      <td>20 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>1</td>\n",
+       "      <td>2</td>\n",
+       "      <td>2400</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68847 40.42473, -3.68871 40...</td>\n",
+       "      <td>40 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>2</td>\n",
+       "      <td>3</td>\n",
+       "      <td>3600</td>\n",
+       "      <td>2400</td>\n",
+       "      <td>MULTIPOLYGON (((-3.68871 40.42430, -3.68885 40...</td>\n",
+       "      <td>60 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>3</td>\n",
+       "      <td>4</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>MULTIPOLYGON (((-3.69494 40.41090, -3.69494 40...</td>\n",
+       "      <td>20 min.</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <td>4</td>\n",
+       "      <td>5</td>\n",
+       "      <td>2400</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>MULTIPOLYGON (((-3.69558 40.41060, -3.69575 40...</td>\n",
+       "      <td>40 min.</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   source_id  data_range  lower_data_range  \\\n",
+       "0          1        1200                 0   \n",
+       "1          2        2400              1200   \n",
+       "2          3        3600              2400   \n",
+       "3          4        1200                 0   \n",
+       "4          5        2400              1200   \n",
+       "\n",
+       "                                            the_geom range_label  \n",
+       "0  MULTIPOLYGON (((-3.68847 40.42473, -3.68855 40...     20 min.  \n",
+       "1  MULTIPOLYGON (((-3.68847 40.42473, -3.68871 40...     40 min.  \n",
+       "2  MULTIPOLYGON (((-3.68871 40.42430, -3.68885 40...     60 min.  \n",
+       "3  MULTIPOLYGON (((-3.69494 40.41090, -3.69494 40...     20 min.  \n",
+       "4  MULTIPOLYGON (((-3.69558 40.41060, -3.69575 40...     40 min.  "
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "isodistances_cdf.head(5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'required_quota': 21}"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "isodistances_metadata"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [
     {
@@ -1231,142 +1331,6 @@
        "\n",
        "  const FIT_BOUNDS_SETTINGS = { animate: false, padding: 50, maxZoom: 16 };\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 createDefaultLegend(layers) {\n",
-       "    const defaultLegendContainer = document.getElementById('default-legend-container');\n",
-       "    defaultLegendContainer.style.display = 'none';\n",
-       "\n",
-       "    AsBridge.VL.Legends.layersLegend(\n",
-       "      '#default-legend',\n",
-       "      layers,\n",
-       "      {\n",
-       "        onLoad: () => defaultLegendContainer.style.display = 'unset'\n",
-       "      }\n",
-       "    );\n",
-       "  }\n",
-       "\n",
-       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
-       "    const element = document.querySelector(`#layer${layerIndex}_map${mapIndex}_legend`);\n",
-       "    \n",
-       "    if (legendData.length) {\n",
-       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
-       "    } else {\n",
-       "      _createLegend(layer, legendData, 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",
        "  /** From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js */\n",
        "\n",
        "  /**\n",
@@ -1490,6 +1454,100 @@
        "    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",
@@ -1645,6 +1703,33 @@
        "      : null;\n",
        "  }\n",
        "\n",
+       "  function createLegend(layer, legendData, layerIndex, mapIndex=0) {\n",
+       "    if (legendData.length) {\n",
+       "      legendData.forEach((legend, legendIndex) => _createLegend(layer, legend, layerIndex, legendIndex, mapIndex));\n",
+       "    } else {\n",
+       "      _createLegend(layer, legendData, 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",
@@ -1790,7 +1875,6 @@
        "  function initLayers(map, settings, mapIndex) {\n",
        "    const numLayers = settings.layers.length;\n",
        "    const hasLegends = settings.has_legends;\n",
-       "    const isDefaultLegend = settings.default_legend;\n",
        "    const isStatic = settings.is_static;\n",
        "    const layers = settings.layers;\n",
        "    const mapLayers = getMapLayers(\n",
@@ -1801,7 +1885,6 @@
        "      mapIndex\n",
        "    );\n",
        "\n",
-       "    createLegend$1(isDefaultLegend, mapLayers);\n",
        "    setInteractiveLayers(map, layers, mapLayers);\n",
        "\n",
        "    return waitForMapLayersLoad(isStatic, mapIndex, mapLayers);\n",
@@ -1837,12 +1920,6 @@
        "    }\n",
        "  }\n",
        "\n",
-       "  function createLegend$1(isDefaultLegend, mapLayers) {\n",
-       "    if (isDefaultLegend) {\n",
-       "      createDefaultLegend(mapLayers);\n",
-       "    }\n",
-       "  }\n",
-       "\n",
        "  function createMap(container, basemapStyle, bounds, accessToken) {\n",
        "    const map = createMapboxGLMap(container, basemapStyle, accessToken);\n",
        "\n",
@@ -1880,12 +1957,12 @@
        "  .addEventListener('ready', () => {\n",
        "    const basecolor = '';\n",
        "    const basemap = 'Positron';\n",
-       "    const bounds = [[-3.71016, 40.40952], [-3.68765, 40.46699]];\n",
+       "    const bounds = [[-3.71015, 40.40944], [-3.68753, 40.46699]];\n",
        "    const camera = null;\n",
        "    const default_legend = 'False' === 'true';\n",
        "    const has_legends = 'False' === 'true';\n",
        "    const is_static = 'None' === 'true';\n",
-       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAIY54V0C/+2bTWtdNxCG/8tdu0EazYeUbaG7QvfFi5C4wZDWwXUWIfi/VzfWzOhY43ud0OuWcEMI5LGko493Xulojr/s7j5/vNq93v1y9ebu0+3VzzcfPly9vbu++Wt3sfvjgf29e/37l931u14qdbqt0MHH25uPV7d31/uCX+4vdu+vbv68urv93P+nhX/99OHu+rebD5/ff2347c3N7bvrv97cfW27//mpvOJaUSpJRuj/wgWmVwgoZYCMlxdajB5+SKk6E60ws4PNXfaC9uSqBcCrG8sBM0Q8UKlBMfiGx5Tn9jx+zOX9/cVYpnzaZQomQaffx8BjlRCaMclLMbGaPiW1KPNytSmLx/+85Ti6Gjrfs7ZYAja6SK4N1lI+XsK1JuVAvaGiv1ED5bj8oscM5FNQdCD58Ew/t4NRZE4d2QgXTircQH5VVZp91ZoWA1vclnQCgI1llSmIM5WfTUADbQ5hkRRyOzjHkeqj6IiiaBrtMdVXXcDqIV21eisBY2e6lB4J0pR5MdFyWQ4GVhSAT/pcOaFcGraH2MgW0GdkiLgOxGiM2mDk5UirkhhDY2CsKEMvB8a8XNZn+GOTokLOtKoFSR+FlcvOYDCo50X+cdHGNfCUrkEsrWRONZdi+mVM0P82EleokP7QlGfx48U4abFqMcCjWAaPgXEeyR06A2W8soIBqyujoD0uB53hyAT4ZhRVL/KAUhNfycSDNesNjm21M49c1GhOZiJIalR2Jmlo5pW9rigDf0Y1JpNj9Jb6IUm6Z5l3ILV9L6kcFR+dVny2RGZx/DBPGZhdVEWZ67Fqucm8QUZzdTJgUpbdbcvCcJyH8zzFK1LJTwhlQaClJk1kWNlKIn2uMt7M26TPfnpj6JIULFnjNUNpjK2mvo3Z7Ano0Hyrqikv/Yljco3d4w8+EkW5juhwSZamEeNbdhl7Yi8HB6MoirYgKsPofSoU+HShIInqOFzaCeBHYnVhnI2hMsFxEE/6JtPL6QE7gSF940v6EtRZUgbWnBGyjoyl7YydlYBBUDcHbEU1YhKwoHv6drdhuAyMagkYBGxFErAfWnr/U7ZxFjmlswQR1PKw+tZEWUXTtzE2Rh6QqMIlfImgt43jicekslQnsurgTFbGZWX+aH+GYMCOBNJmgetJF3j1vn5SHBt89RVuwzZyQ1eColoN6XWIvZ92cejCZReRXq5kN2abJ5nMWhmTM5UVu9RAByEuA10K8ap6iZXEvV9bq/mQICPhRgKPAiEKmO/ZmY6IGbPVd2ZxlNBZ0+7QQTFHoo+CIwqiJ8XcTnnp2fLDYxn8+nFiZ3RGZ/SSSNML7Ff6bdxgMuo59VHcbpN76YX8wrqiPZ4TImPv6y+kbivjPr8zLwfKavOhPRzLmbLnB5pU6TYthYbncxFAyrmV/dn+2VMVTPyc/Wh5v6lAplJGEopLaUJU+rMbPs6I7H/o6QpUNmXzFHlWA7T/xXotzZg9QsRYOSyXwz3eauOkmd9wgbXLtOR6mJOnmBouqg9kEMplfuq0lutktTSOFWxZupZUMGCpts5GB8Fykp2N5YA21dUJT3bTk0Yyqg/E28vGPBPYlPEUSLrk1Ze8aSClifWzJXT9U38zRdV72iddKkLJx0QeKZB1KCLO2qLAkUhkzyOKaCh4hk/GwbRP4KRnejT3LnFAOiDxKBKiiNmMayt7OOX7AIwuU/Vz/sTsZAkAg5GdImGsXz/92W0IoJar3t44THfm7ZE+w94lYBxKe3PNWVXm5YYxdWaH3HGJ2lEJRpHP7MzObET51l2+9zODp43lyV5gUr/wl+Wi8Y1mKyVrMPsbPzRtzm8LvscHNkPHlxu65ixIqo9z5B06M78rNh/JL5jHi34v5y/64x6DavYXfcmLlUdzGUx5tDJPz9tJ03zQxqqyZ20AldnBuPXzgzI708C4BOpMnLWVjU25MzsjQQ1QCZgosyMStACVgPHCysijkefrSiorkgVZvewIVwZWzvNYwCsrOnP+TUkhY0tOrTP2bCUubLOKUzovWt2iuvUv6/qzUZn3pzRl1dmYGZnGXDRePOdfLIY8D1mSPcPrLkR1IeJyrNphXhQlXpFxQVQXpGcj8QSnnbUkue4CtJ3jOUL5pN+Opb48tRtPFwjZyvP+GwhsPCXJVT7o06s2zjQx1Xud0rlad0q/OrNpL1UndMrwtkduuc/wqkrq1Jw5sgtRHS9570TPs+jlUB3UJaeKaFOaWpfM08ogasf2DWUXhXbYxwVo7dGigIaH9BSoLtBmoOBV52swhCEThVYUgmGoBiEdhX5gEZGVHJPm5EShM6qcaFpGM/xUFu3wNOTAGUMHjZw2cuS5f7ODRt5gkbEi/2oFQH2aa8AUZeugfTbc32tt6/Lmsu2Evib6Rjhv1MiJS4X9JZi1QiSIXFOlQ8s5j3VrcHJSgyspZekx2nxv686Ruwr71k3+qZUemnwrhLKgx0DDoPq3SAXLyuzk5gzGNQS1KQy0Fy1P86YGNS1Nsq55e3mxlFw1Ill89c0t+NB+FLFoewtcK/C2yAIjqwwsNbLe0KJXI4/sPtoW1t0j2mSOKmk2pOj4ph48nfz0SzOi6cM90XL+JbJ+CEh+U9mZLMZlrw3+5ZTYeXP+wiraptftPN72A2+MPDTy2siTI++ep2/jl4vDJVGUDjpc5IRzc1tLqqe0pDS+7GHyA9aZ7aN/3DCTv6Jl1HLoSz4UyOSKTobEH0GPLvzPs3xm/w3buks7xV1VHBc4blYYeflwmf0XRvvpNmucLac4Jn9x+9fidvPLdOnlJoR1Qop3DM0p/LeBNJM3IS2E09fGI+XEviGVcWzsaLrt0ESj73vhTAYzHq1MtILRSi8zfnn/DyQczI9IPQAA&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;strokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nopacity: 0.9\\ncolor: opacity(green, 0.3)&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIAIY54V0C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 3&quot;, &quot;widgets&quot;: []}];\n",
+       "    const layers = [{&quot;credentials&quot;: null, &quot;data&quot;: &quot;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&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;strokeWidth: ramp(linear(zoom(),2,18),[0.5,1])\\nstrokeColor: opacity(#2c2c2c,ramp(linear(zoom(),2,18),[0.2,0.6]))\\nopacity: 0.9\\ncolor: opacity(green, 0.3)&quot;, &quot;widgets&quot;: []}, {&quot;credentials&quot;: null, &quot;data&quot;: &quot;H4sIADHM510C/7XRvQ7CIBQF4He5Mzb8l7qaOLubDk2LhqSWBnFoGt5dLDXBHUdODnzh3hX8Mms4wll3/uX0yY6j7r2xEyC4pewJx+sKZogtHNPfCzGYnZ218+ZTXAOCu7YP7d0ST9/yxZrJx2pvrRvM1Pnt0QOrpFKMII4rTnmt2hDQLpHiUsOV2iSCVZNJtLRUY7FLFPNcYn+YXpMkSVku8fJ/ojTtSSieQaL8mqiSCZKCZJIsL+GGpeGJWkSpDW9dlv+dDgMAAA==&quot;, &quot;has_legend_list&quot;: false, &quot;interactivity&quot;: [], &quot;legend&quot;: {}, &quot;type&quot;: &quot;GeoJSON&quot;, &quot;viz&quot;: &quot;color: hex(\\&quot;#EE4D5A\\&quot;)\\nstrokeWidth: ramp(linear(zoom(),0,18),[0,1])\\nstrokeColor: opacity(#222,ramp(linear(zoom(),0,18),[0,0.6]))\\nopacity: 1\\nwidth: 3&quot;, &quot;widgets&quot;: []}];\n",
        "    const mapboxtoken = '';\n",
        "    const show_info = 'None' === 'true';\n",
        "\n",
@@ -1909,10 +1986,10 @@
        "</iframe>"
       ],
       "text/plain": [
-       "<cartoframes.viz.map.Map at 0x7f5a778403c8>"
+       "<cartoframes.viz.map.Map at 0x7f8d79ab9358>"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1921,7 +1998,7 @@
     "from cartoframes.viz import Map, Layer\n",
     "\n",
     "Map([\n",
-    "    Layer(isodistances.data, 'color: opacity(green, 0.3)'),\n",
+    "    Layer(isodistances_cdf, 'color: opacity(green, 0.3)'),\n",
     "    Layer(cdf, 'width: 3')\n",
     "])"
    ]


Test patch:
diff --git a/tests/unit/data/observatory/catalog/examples.py b/tests/unit/data/observatory/catalog/examples.py
index 1b18abc7..6605ead7 100644
--- a/tests/unit/data/observatory/catalog/examples.py
+++ b/tests/unit/data/observatory/catalog/examples.py
@@ -83,7 +83,7 @@ db_dataset1 = {
     'update_frequency': 'monthly',
     'version': '20190203',
     'is_public_data': True,
-    'summary_json': None,
+    'summary_json': {},
     'available_in': ['bq']
 }
 db_dataset2 = {
@@ -106,23 +106,7 @@ db_dataset2 = {
     'update_frequency': 'monthly',
     'version': '20190203',
     'is_public_data': False,
-    'summary_json': {
-        'glimpses': {
-            'head': ['a', 'b', 'c'],
-            'tail': ['e', 'f', 'g']
-        },
-        'counts': {
-            'rows': 3,
-            'columns': 3,
-            'null_cells': 0,
-            'null_cells_percent': 0
-        },
-        'fields_by_type': {
-            'float': 1,
-            'string': 1,
-            'integer': 1
-        }
-    },
+    'summary_json': {},
     'available_in': []
 }
 test_dataset1 = Dataset(db_dataset1)
@@ -140,7 +124,7 @@ db_variable1 = {
     'agg_method': '',
     'variable_group_id': 'vargroup1',
     'starred': True,
-    'summary_json': None
+    'summary_json': {}
 }
 db_variable2 = {
     'id': 'carto-do.variable.var2',
@@ -153,7 +137,7 @@ db_variable2 = {
     'agg_method': '',
     'variable_group_id': 'vargroup1',
     'starred': False,
-    'summary_json': [{'key': 'value'}]
+    'summary_json': {}
 }
 test_variable1 = Variable(db_variable1)
 test_variable2 = Variable(db_variable2)
diff --git a/tests/unit/data/observatory/catalog/test_dataset.py b/tests/unit/data/observatory/catalog/test_dataset.py
index 24c14100..763ea3f6 100644
--- a/tests/unit/data/observatory/catalog/test_dataset.py
+++ b/tests/unit/data/observatory/catalog/test_dataset.py
@@ -173,57 +173,6 @@ class TestDataset(object):
         # Then
         assert dataset_str == 'Dataset({dict_str})'.format(dict_str=str(db_dataset1))
 
-    def test_summary_values(self):
-        # Given
-        dataset = Dataset(db_dataset2)
-
-        # When
-        summary = dataset.summary
-
-        # Then
-
-        assert summary == dataset.data['summary_json']
-
-    def test_summary_head(self):
-        # Given
-        dataset = Dataset(db_dataset2)
-
-        # When
-        summary = dataset.head()
-
-        # Then
-        assert isinstance(summary, pd.DataFrame)
-
-    def test_summary_tail(self):
-        # Given
-        dataset = Dataset(db_dataset2)
-
-        # When
-        summary = dataset.tail()
-
-        # Then
-        assert isinstance(summary, pd.DataFrame)
-
-    def test_summary_counts(self):
-        # Given
-        dataset = Dataset(db_dataset2)
-
-        # When
-        summary = dataset.counts()
-
-        # Then
-        assert isinstance(summary, pd.Series)
-
-    def test_summary_fields_by_type(self):
-        # Given
-        dataset = Dataset(db_dataset2)
-
-        # When
-        summary = dataset.fields_by_type()
-
-        # Then
-        assert isinstance(summary, pd.Series)
-
     @patch.object(DatasetRepository, 'get_all')
     def test_get_all_datasets(self, mocked_repo):
         # Given