Repository: developmentseed/rio-viz (language: python)

Issue:
add expression support in the API



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 38c0f5b..f6e065e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,6 @@ on:
     branches:
     - master
     tags:
-    - '*'
   pull_request:
 
 jobs:
@@ -13,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: [3.7, 3.8, 3.9]
 
     steps:
       - uses: actions/checkout@v2
@@ -21,19 +20,20 @@ jobs:
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python-version }}
+
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install tox codecov pre-commit
+          python -m pip install codecov pre-commit
+
+      - name: Install module
+        run: python -m pip install -e .["mvt,test"]
 
-      # Run pre-commit (only for python-3.8)
       - name: run pre-commit
-        if: matrix.python-version == 3.8
         run: pre-commit run --all-files
 
-      # Run tox using the version of Python in `PATH`
-      - name: Run Tox
-        run: tox -e py
+      - name: Run tests
+        run: python -m pytest --cov rio_viz --cov-report xml --cov-report term-missing --ignore=venv
 
       - name: Upload Results
         if: success()
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4533b39..f81da5a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 19.10b0
+    rev: 21.10b0
     hooks:
       - id: black
         language_version: python
diff --git a/README.md b/README.md
index 86258c1..26e2430 100644
--- a/README.md
+++ b/README.md
@@ -53,36 +53,26 @@ Usage: rio viz [OPTIONS] SRC_PATH
   Rasterio Viz cli.
 
 Options:
-  --nodata NUMBER|nan        Set nodata masking values for input dataset.
-  --minzoom INTEGER          Overwrite minzoom
-  --maxzoom INTEGER          Overwrite maxzoom
-  --style [satellite|basic]  Mapbox basemap
-  --port INTEGER             Webserver port (default: 8080)
-  --host TEXT                Webserver host url (default: 127.0.0.1)
-  --mapbox-token TOKEN       Pass Mapbox token
-  --no-check                 Ignore COG validation
-  --reader TEXT              rio-tiler Reader (BaseReader or AsyncBaseReader). Default is `rio_tiler.io.COGReader`
-  --layers TEXT              limit to specific layers (for Multi* readers)
-  --server-only              Launch API without opening the rio-viz web-page.
-  --config NAME=VALUE        GDAL configuration options.
-  --help                     Show this message and exit.
-
+  --nodata NUMBER|nan  Set nodata masking values for input dataset.
+  --minzoom INTEGER    Overwrite minzoom
+  --maxzoom INTEGER    Overwrite maxzoom
+  --port INTEGER       Webserver port (default: 8080)
+  --host TEXT          Webserver host url (default: 127.0.0.1)
+  --no-check           Ignore COG validation
+  --reader TEXT        rio-tiler Reader (BaseReader or AsyncBaseReader). Default is `rio_tiler.io.COGReader`
+  --layers TEXT        limit to specific layers (only used for MultiBand and MultiBase Readers). (e.g --layers b1 --layers b2).
+  --server-only        Launch API without opening the rio-viz web-page.
+  --config NAME=VALUE  GDAL configuration options.
+  --help               Show this message and exit.
 ```
 
-## 3D (Experimental)
-
-rio-viz supports Mapbox VectorTiles encoding from a raster array. This feature was added to visualize sparse data stored as raster but will also work for dense array. This is highly experimental and might be slow to render in certain browser and/or for big rasters.
-
-![](https://user-images.githubusercontent.com/10407788/56853984-4713b800-68fd-11e9-86a2-efbb041daeb0.gif)
-
-
 ## Multi Reader support
 
 rio-viz support multiple/custom reader as long they are subclass of `rio_tiler.io.base.BaseReader` or `rio_tiler.io.base.AsyncBaseReader`.
 
 ```bash
-# MultiFiles
-$ rio viz "cog_band{2,3,4}.tif" --reader rio_viz.io.MultiFilesReader
+# Multi Files as Bands
+$ rio viz "cog_band{2,3,4}.tif" --reader rio_viz.io.MultiFilesBandsReader
 
 # Simple Mosaic
 $ rio viz "tests/fixtures/mosaic_cog{1,2}.tif" --reader rio_viz.io.MosaicReader
@@ -134,6 +124,14 @@ You can see the full API documentation over `http://127.0.0.1:8080/docs`
 
 ![API documentation](https://user-images.githubusercontent.com/10407788/99135093-a7a53b80-25ee-11eb-98ba-0ce932775791.png)
 
+
+## 3D (Experimental)
+
+rio-viz supports Mapbox VectorTiles encoding from a raster array. This feature was added to visualize sparse data stored as raster but will also work for dense array. This is highly experimental and might be slow to render in certain browser and/or for big rasters.
+
+![](https://user-images.githubusercontent.com/10407788/56853984-4713b800-68fd-11e9-86a2-efbb041daeb0.gif)
+
+
 ## Contribution & Development
 
 See [CONTRIBUTING.md](https://github.com/developmentseed/rio-viz/blob/master/CONTRIBUTING.md)
diff --git a/rio_viz/app.py b/rio_viz/app.py
index cb67de3..ef9bfc7 100644
--- a/rio_viz/app.py
+++ b/rio_viz/app.py
@@ -3,7 +3,7 @@
 import pathlib
 import urllib.parse
 from functools import partial
-from typing import Any, Dict, Optional, Type, Union
+from typing import Any, Dict, List, Optional, Type, Union
 
 import attr
 import rasterio
@@ -11,26 +11,32 @@
 from fastapi import APIRouter, Depends, FastAPI, HTTPException, Path, Query
 from geojson_pydantic.features import Feature
 from rio_tiler.io import AsyncBaseReader
-from rio_tiler.models import ImageStatistics, Info, Metadata
+from rio_tiler.models import BandStatistics, Info
 from starlette.concurrency import run_in_threadpool
 from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.middleware.cors import CORSMiddleware
-from starlette.middleware.gzip import GZipMiddleware
 from starlette.requests import Request
 from starlette.responses import HTMLResponse, Response
 from starlette.templating import Jinja2Templates
 from starlette.types import ASGIApp
+from starlette_cramjam.middleware import CompressionMiddleware
 
-from rio_viz.dependencies import AssetsParams, BandsParams, IndexesParams
 from rio_viz.resources.enums import RasterFormat, VectorTileFormat, VectorTileType
 
 from titiler.core.dependencies import (
+    AssetsBidxParams,
+    AssetsParams,
+    BandsExprParamsOptional,
+    BandsParams,
+    BidxExprParams,
     ColorMapParams,
     DatasetParams,
     DefaultDependency,
+    HistogramParams,
     ImageParams,
-    MetadataParams,
-    RenderParams,
+    ImageRenderingParams,
+    PostProcessParams,
+    StatisticsParams,
 )
 from titiler.core.models.mapbox import TileJSON
 from titiler.core.resources.responses import JSONResponse, XMLResponse
@@ -79,16 +85,14 @@ class viz:
 
     app: FastAPI = attr.ib(default=attr.Factory(FastAPI))
 
-    token: Optional[str] = attr.ib(default=None)
     port: int = attr.ib(default=8080)
     host: str = attr.ib(default="127.0.0.1")
-    style: str = attr.ib(default="satellite")
     config: Dict = attr.ib(default=dict)
 
     minzoom: Optional[int] = attr.ib(default=None)
     maxzoom: Optional[int] = attr.ib(default=None)
 
-    layers: Optional[str] = attr.ib(default=None)
+    layers: Optional[List[str]] = attr.ib(default=None)
     nodata: Optional[Union[str, int, float]] = attr.ib(default=None)
 
     # cog / bands / assets
@@ -110,21 +114,16 @@ def __attrs_post_init__(self):
 
         if self.reader_type == "cog":
             # For simple BaseReader (e.g COGReader) we don't add more dependencies.
-            self.layer_dependency = DefaultDependency
+            self.info_dependency = DefaultDependency
+            self.layer_dependency = BidxExprParams
 
         elif self.reader_type == "bands":
-            self.layer_dependency = type(
-                "BandsParams",
-                (BandsParams,),
-                {"default_bands": self.layers.split(",") if self.layers else None},
-            )
+            self.info_dependency = BandsParams
+            self.layer_dependency = BandsExprParamsOptional
 
         elif self.reader_type == "assets":
-            self.layer_dependency = type(
-                "AssetsParams",
-                (AssetsParams,),
-                {"default_assets": self.layers.split(",") if self.layers else None},
-            )
+            self.info_dependency = AssetsParams
+            self.layer_dependency = AssetsBidxParams
 
         self.register_middleware()
         self.register_routes()
@@ -132,6 +131,17 @@ def __attrs_post_init__(self):
 
     def register_middleware(self):
         """Register Middleware to the FastAPI app."""
+        self.app.add_middleware(
+            CompressionMiddleware,
+            minimum_size=0,
+            exclude_mediatype={
+                "image/jpeg",
+                "image/jpg",
+                "image/png",
+                "image/jp2",
+                "image/webp",
+            },
+        )
         self.app.add_middleware(
             CORSMiddleware,
             allow_origins=["*"],
@@ -139,18 +149,24 @@ def register_middleware(self):
             allow_methods=["GET"],
             allow_headers=["*"],
         )
-        self.app.add_middleware(GZipMiddleware, minimum_size=0)
         self.app.add_middleware(CacheControlMiddleware)
 
-    def _update_layer_params(self, src_dst, options: Dict[str, Any]):
+    def _update_params(self, src_dst, options: Type[DefaultDependency]):
         """Create Reader options."""
-        assets = getattr(src_dst, "assets", None)
-        if assets and not options.get("assets"):
-            options["assets"] = assets
-
-        bands = getattr(src_dst, "bands", None)
-        if bands and not options.get("bands"):
-            options["bands"] = bands
+        if not getattr(options, "expression", None):
+            if self.reader_type == "bands":
+                # get default bands from self.layers or reader.bands
+                bands = self.layers or getattr(src_dst, "bands", None)
+                # check if bands is not in options and overwrite
+                if bands and not getattr(options, "bands", None):
+                    options.bands = bands
+
+            if self.reader_type == "assets":
+                # get default assets from self.layers or reader.assets
+                assets = self.layers or getattr(src_dst, "assets", None)
+                # check if assets is not in options and overwrite
+                if assets and not getattr(options, "assets", None):
+                    options.assets = assets
 
     def register_routes(self):  # noqa
         """Register routes to the FastAPI app."""
@@ -177,84 +193,45 @@ def register_routes(self):  # noqa
             responses={200: {"description": "Return the info of the COG."}},
             tags=["API"],
         )
-        async def info(layer_params=Depends(self.layer_dependency)):
+        async def info(params=Depends(self.info_dependency)):
             """Handle /info requests."""
-            async with self.reader(self.src_path) as src_dst:  # type: ignore
-
+            async with self.reader(self.src_path) as src_dst:
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
-
-                return await src_dst.info(**layer_kwargs)
+                self._update_params(src_dst, params)
+                return await src_dst.info(**params)
 
         @self.router.get(
-            "/stats",
+            "/statistics",
             # for MultiBaseReader the output in `Dict[str, Dict[str, ImageStatistics]]`
-            response_model=Dict[str, Dict[str, ImageStatistics]]
+            response_model=Dict[str, Dict[str, BandStatistics]]
             if self.reader_type == "assets"
-            else Dict[str, ImageStatistics],
-            response_model_exclude={"minzoom", "maxzoom", "center"},
+            else Dict[str, BandStatistics],
             response_model_exclude_none=True,
             response_class=JSONResponse,
             responses={200: {"description": "Return the statistics of the COG."}},
             tags=["API"],
         )
-        async def stats(
-            metadata_params: MetadataParams = Depends(),
+        async def statistics(
             layer_params=Depends(self.layer_dependency),
+            image_params: ImageParams = Depends(),
             dataset_params: DatasetParams = Depends(),
+            stats_params: StatisticsParams = Depends(),
+            histogram_params: HistogramParams = Depends(),
         ):
             """Handle /stats requests."""
-            async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
-
-                # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
-
-                return await src_dst.stats(
-                    metadata_params.pmin,
-                    metadata_params.pmax,
-                    **layer_kwargs,
-                    **dataset_kwargs,
-                    **metadata_params.kwargs,
-                )
-
-        @self.router.get(
-            "/metadata",
-            # for MultiBaseReader the output in `Dict[str, Metadata]`
-            response_model=Dict[str, Metadata]
-            if self.reader_type == "assets"
-            else Metadata,
-            response_model_exclude={"minzoom", "maxzoom", "center"},
-            response_model_exclude_none=True,
-            response_class=JSONResponse,
-            responses={200: {"description": "Return the metadata of the COG."}},
-            tags=["API"],
-        )
-        async def metadata(
-            metadata_params: MetadataParams = Depends(),
-            layer_params=Depends(self.layer_dependency),
-            dataset_params: DatasetParams = Depends(),
-        ):
-            """Handle /metadata requests."""
-            async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+            async with self.reader(self.src_path) as src_dst:
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
-
-                return await src_dst.metadata(
-                    metadata_params.pmin,
-                    metadata_params.pmax,
-                    **layer_kwargs,
-                    **dataset_kwargs,
-                    **metadata_params.kwargs,
+                self._update_params(src_dst, layer_params)
+
+                return await src_dst.statistics(
+                    **layer_params,
+                    **dataset_params,
+                    **image_params,
+                    **stats_params,
+                    hist_options={**histogram_params},
                 )
 
         @self.router.get(
@@ -267,26 +244,26 @@ async def point(
             coordinates: str = Query(
                 ..., description="Coma (',') delimited lon,lat coordinates"
             ),
-            indexes_params: IndexesParams = Depends(),
             layer_params=Depends(self.layer_dependency),
             dataset_params: DatasetParams = Depends(),
         ):
             """Handle /point requests."""
             lon, lat = list(map(float, coordinates.split(",")))
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
+                self._update_params(src_dst, layer_params)
 
                 results = await src_dst.point(
-                    lon, lat, **indexes_params.kwargs, **layer_kwargs, **dataset_kwargs,
+                    lon,
+                    lat,
+                    **layer_params,
+                    **dataset_params,
                 )
 
-            return {"coordinates": [lon, lat], "value": results}
+            return {"coordinates": [lon, lat], "values": results}
 
         preview_params = dict(
             responses={
@@ -300,45 +277,38 @@ async def point(
         @self.router.get(r"/preview.{format}", **preview_params, tags=["API"])
         async def preview(
             format: Optional[RasterFormat] = None,
-            indexes_params: IndexesParams = Depends(),
             layer_params=Depends(self.layer_dependency),
             img_params: ImageParams = Depends(),
             dataset_params: DatasetParams = Depends(),
-            render_params: RenderParams = Depends(),
+            render_params: ImageRenderingParams = Depends(),
+            postprocess_params: PostProcessParams = Depends(),
             colormap: ColorMapParams = Depends(),
         ):
             """Handle /preview requests."""
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
+                self._update_params(src_dst, layer_params)
 
                 data = await src_dst.preview(
-                    **img_params.kwargs,
-                    **indexes_params.kwargs,
-                    **layer_kwargs,
-                    **dataset_kwargs,
+                    **layer_params,
+                    **dataset_params,
+                    **img_params,
                 )
                 dst_colormap = getattr(src_dst, "colormap", None)
 
             if not format:
                 format = RasterFormat.jpeg if data.mask.all() else RasterFormat.png
 
-            image = data.post_process(
-                in_range=render_params.rescale_range,
-                color_formula=render_params.color_formula,
-            )
+            image = data.post_process(**postprocess_params)
 
             content = image.render(
                 img_format=format.driver,
                 colormap=colormap or dst_colormap,
-                add_mask=render_params.return_mask,
                 **format.profile,
-                **render_params.kwargs,
+                **render_params,
             )
 
             return Response(content, media_type=format.mediatype)
@@ -355,7 +325,9 @@ async def preview(
         )
 
         @self.router.get(
-            r"/crop/{minx},{miny},{maxx},{maxy}.{format}", **part_params, tags=["API"],
+            r"/crop/{minx},{miny},{maxx},{maxy}.{format}",
+            **part_params,
+            tags=["API"],
         )
         @self.router.get(
             r"/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}",
@@ -370,43 +342,36 @@ async def part(
             format: RasterFormat = Query(
                 RasterFormat.png, description="Output image type."
             ),
-            indexes_params: IndexesParams = Depends(),
             layer_params=Depends(self.layer_dependency),
             img_params: ImageParams = Depends(),
             dataset_params: DatasetParams = Depends(),
-            render_params: RenderParams = Depends(),
+            render_params: ImageRenderingParams = Depends(),
+            postprocess_params: PostProcessParams = Depends(),
             colormap: ColorMapParams = Depends(),
         ):
             """Create image from part of a dataset."""
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
+                self._update_params(src_dst, layer_params)
 
                 data = await src_dst.part(
                     [minx, miny, maxx, maxy],
-                    **indexes_params.kwargs,
-                    **img_params.kwargs,
-                    **layer_kwargs,
-                    **dataset_kwargs,
+                    **layer_params,
+                    **dataset_params,
+                    **img_params,
                 )
                 dst_colormap = getattr(src_dst, "colormap", None)
 
-            image = data.post_process(
-                in_range=render_params.rescale_range,
-                color_formula=render_params.color_formula,
-            )
+            image = data.post_process(**postprocess_params)
 
             content = image.render(
                 img_format=format.driver,
                 colormap=colormap or dst_colormap,
-                add_mask=render_params.return_mask,
                 **format.profile,
-                **render_params.kwargs,
+                **render_params,
             )
 
             return Response(content, media_type=format.mediatype)
@@ -435,38 +400,33 @@ async def geojson_part(
             layer_params=Depends(self.layer_dependency),
             img_params: ImageParams = Depends(),
             dataset_params: DatasetParams = Depends(),
-            render_params: RenderParams = Depends(),
+            render_params: ImageRenderingParams = Depends(),
+            postprocess_params: PostProcessParams = Depends(),
             colormap: ColorMapParams = Depends(),
         ):
             """Handle /feature requests."""
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
+                self._update_params(src_dst, layer_params)
 
                 data = await src_dst.feature(
-                    geom.dict(exclude_none=True), **layer_kwargs, **dataset_kwargs
+                    geom.dict(exclude_none=True), **layer_params, **dataset_params
                 )
                 dst_colormap = getattr(src_dst, "colormap", None)
 
             if not format:
                 format = RasterFormat.jpeg if data.mask.all() else RasterFormat.png
 
-            image = data.post_process(
-                in_range=render_params.rescale_range,
-                color_formula=render_params.color_formula,
-            )
+            image = data.post_process(**postprocess_params)
 
             content = image.render(
                 img_format=format.driver,
                 colormap=colormap or dst_colormap,
-                add_mask=render_params.return_mask,
                 **format.profile,
-                **render_params.kwargs,
+                **render_params,
             )
 
             return Response(content, media_type=format.mediatype)
@@ -489,13 +449,14 @@ async def tile(
             x: int,
             y: int,
             format: Optional[TileFormat] = None,
-            indexes_params: IndexesParams = Depends(),
             layer_params=Depends(self.layer_dependency),
             dataset_params: DatasetParams = Depends(),
-            render_params: RenderParams = Depends(),
+            render_params: ImageRenderingParams = Depends(),
+            postprocess_params: PostProcessParams = Depends(),
             colormap: ColorMapParams = Depends(),
             feature_type: Optional[VectorTileType] = Query(
-                None, title="Feature type (Only for MVT)",
+                None,
+                title="Feature type (Only for MVT)",
             ),
         ):
             """Handle /tiles requests."""
@@ -505,28 +466,21 @@ async def tile(
                 tilesize = 128
 
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                dataset_kwargs = dataset_params.kwargs
-                if self.nodata is not None and not dataset_kwargs.get("nodata"):
-                    dataset_kwargs["nodata"] = self.nodata
+                if self.nodata is not None and dataset_params.nodata is not None:
+                    dataset_params.nodata = self.nodata
 
                 # Adapt options for each reader type
-                layer_kwargs = layer_params.kwargs
-                self._update_layer_params(src_dst, layer_kwargs)
+                self._update_params(src_dst, layer_params)
 
                 tile_data = await src_dst.tile(
                     x,
                     y,
                     z,
                     tilesize=tilesize,
-                    **indexes_params.kwargs,
-                    **dataset_kwargs,
-                    **layer_kwargs,
+                    **layer_params,
+                    **dataset_params,
                 )
 
-                bandnames = layer_kwargs.get(
-                    "bands", layer_kwargs.get("assets", None)
-                ) or [f"{ix + 1}" for ix in range(tile_data.count)]
-
                 dst_colormap = getattr(src_dst, "colormap", None)
 
             # Vector Tile
@@ -539,14 +493,15 @@ async def tile(
 
                 if not feature_type:
                     raise HTTPException(
-                        status_code=500, detail="missing feature_type for vector tile.",
+                        status_code=500,
+                        detail="missing feature_type for vector tile.",
                     )
                 _mvt_encoder = partial(run_in_threadpool, pixels_encoder)
 
                 content = await _mvt_encoder(
                     tile_data.data,
                     tile_data.mask,
-                    bandnames,
+                    tile_data.band_names,
                     feature_type=feature_type.value,
                 )  # type: ignore
 
@@ -557,17 +512,13 @@ async def tile(
                         RasterFormat.jpeg if tile_data.mask.all() else RasterFormat.png
                     )
 
-                image = tile_data.post_process(
-                    in_range=render_params.rescale_range,
-                    color_formula=render_params.color_formula,
-                )
+                image = tile_data.post_process(**postprocess_params)
 
                 content = image.render(
                     img_format=format.driver,
                     colormap=colormap or dst_colormap,
-                    add_mask=render_params.return_mask,
                     **format.profile,
-                    **render_params.kwargs,
+                    **render_params,
                 )
 
             return Response(content, media_type=format.mediatype)
@@ -582,10 +533,10 @@ async def tile(
         async def tilejson(
             request: Request,
             tile_format: Optional[TileFormat] = None,
-            indexes_params: IndexesParams = Depends(),  # noqa
             layer_params=Depends(self.layer_dependency),  # noqa
             dataset_params: DatasetParams = Depends(),  # noqa
-            render_params: RenderParams = Depends(),  # noqa
+            render_params: ImageRenderingParams = Depends(),  # noqa
+            postprocess_params: PostProcessParams = Depends(),  # noqa
             colormap: ColorMapParams = Depends(),  # noqa
             feature_type: str = Query(  # noqa
                 None, title="Feature type", regex="^(point)|(polygon)$"
@@ -607,14 +558,12 @@ async def tilejson(
                 tile_url += f"?{urllib.parse.urlencode(qs)}"
 
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                bounds = src_dst.bounds
-                center = src_dst.center
+                bounds = src_dst.geographic_bounds
                 minzoom = self.minzoom if self.minzoom is not None else src_dst.minzoom
                 maxzoom = self.maxzoom if self.maxzoom is not None else src_dst.maxzoom
 
             return dict(
                 bounds=bounds,
-                center=center,
                 minzoom=minzoom,
                 maxzoom=maxzoom,
                 name="rio-viz",
@@ -630,10 +579,10 @@ async def wmts(
             tile_format: RasterFormat = Query(
                 RasterFormat.png, description="Output image type. Default is png."
             ),
-            indexes_params: IndexesParams = Depends(),  # noqa
             layer_params=Depends(self.layer_dependency),  # noqa
             dataset_params: DatasetParams = Depends(),  # noqa
-            render_params: RenderParams = Depends(),  # noqa
+            render_params: ImageRenderingParams = Depends(),  # noqa
+            postprocess_params: PostProcessParams = Depends(),  # noqa
             colormap: ColorMapParams = Depends(),  # noqa
             feature_type: str = Query(  # noqa
                 None, title="Feature type", regex="^(point)|(polygon)$"
@@ -663,7 +612,7 @@ async def wmts(
                 tiles_endpoint += f"?{urllib.parse.urlencode(qs)}"
 
             async with self.reader(self.src_path) as src_dst:  # type: ignore
-                bounds = src_dst.bounds
+                bounds = src_dst.geographic_bounds
                 minzoom = self.minzoom if self.minzoom is not None else src_dst.minzoom
                 maxzoom = self.maxzoom if self.maxzoom is not None else src_dst.maxzoom
 
@@ -714,11 +663,9 @@ def viewer(request: Request):
                 context={
                     "request": request,
                     "tilejson_endpoint": request.url_for("tilejson"),
-                    "stats_endpoint": request.url_for("stats"),
+                    "stats_endpoint": request.url_for("statistics"),
                     "info_endpoint": request.url_for("info"),
                     "point_endpoint": request.url_for("point"),
-                    "mapbox_access_token": self.token,
-                    "mapbox_style": self.style,
                     "allow_3d": has_mvt,
                 },
                 media_type="text/html",
diff --git a/rio_viz/compat.py b/rio_viz/compat.py
index 45a616d..e9501d8 100644
--- a/rio_viz/compat.py
+++ b/rio_viz/compat.py
@@ -4,13 +4,17 @@
 
 """
 
-from typing import Any, Coroutine, Dict, List, Tuple, Type
+from typing import Any, Coroutine, Dict, List, Optional, Tuple, Type, Union
 
 import attr
-import morecantile
-from rio_tiler import constants
-from rio_tiler.io import AsyncBaseReader, BaseReader, COGReader
-from rio_tiler.models import ImageData, ImageStatistics, Info, Metadata
+from rio_tiler.io import (
+    AsyncBaseReader,
+    BaseReader,
+    COGReader,
+    MultiBandReader,
+    MultiBaseReader,
+)
+from rio_tiler.models import BandStatistics, ImageData, Info
 from starlette.concurrency import run_in_threadpool
 
 
@@ -18,20 +22,31 @@
 class AsyncReader(AsyncBaseReader):
     """Async Reader class."""
 
-    src_path: str = attr.ib()
-    tms: morecantile.TileMatrixSet = attr.ib(default=constants.WEB_MERCATOR_TMS)
+    dataset: Union[Type[BaseReader], Type[MultiBandReader], Type[MultiBaseReader]]
 
-    reader: Type[BaseReader] = COGReader
+    assets: Optional[List[str]]
+    bands: Optional[List[str]]
+    colormap: Optional[Dict]
+
+    reader: Union[
+        Type[BaseReader],
+        Type[MultiBandReader],
+        Type[MultiBaseReader],
+    ] = COGReader
 
     def __attrs_post_init__(self):
         """PostInit."""
-        self.dataset = self.reader(self.src_path)
+        self.dataset = self.reader(self.input)
         self.bounds = self.dataset.bounds
+        self.crs = self.dataset.crs
+
         self.minzoom = self.dataset.minzoom
         self.maxzoom = self.dataset.maxzoom
 
+        # MultiBase or MultiBand
         self.assets = getattr(self.dataset, "assets", None)
         self.bands = getattr(self.dataset, "bands", None)
+
         self.colormap = getattr(self.dataset, "colormap", None)
 
         return self
@@ -49,17 +64,12 @@ async def info(self, **kwargs: Any) -> Coroutine[Any, Any, Info]:
         """Return Dataset's info."""
         return await run_in_threadpool(self.dataset.info, **kwargs)  # type: ignore
 
-    async def stats(
-        self, pmin: float = 2.0, pmax: float = 98.0, **kwargs: Any
-    ) -> Coroutine[Any, Any, Dict[str, ImageStatistics]]:
-        """Return Dataset's statistics."""
-        return await run_in_threadpool(self.dataset.stats, pmin, pmax, **kwargs)  # type: ignore
-
-    async def metadata(
-        self, pmin: float = 2.0, pmax: float = 98.0, **kwargs: Any
-    ) -> Coroutine[Any, Any, Metadata]:
+    async def statistics(
+        self,
+        **kwargs: Any,
+    ) -> Coroutine[Any, Any, Dict[str, BandStatistics]]:
         """Return Dataset's statistics."""
-        return await run_in_threadpool(self.dataset.metadata, pmin, pmax, **kwargs)  # type: ignore
+        return await run_in_threadpool(self.dataset.statistics, **kwargs)  # type: ignore
 
     async def tile(
         self, tile_x: int, tile_y: int, tile_z: int, **kwargs: Any
diff --git a/rio_viz/dependencies.py b/rio_viz/dependencies.py
deleted file mode 100644
index 9b5f9cf..0000000
--- a/rio_viz/dependencies.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""rio-viz app dependencies."""
-
-from dataclasses import dataclass, field
-from typing import List, Optional
-
-from fastapi import Query
-
-from titiler.core.dependencies import DefaultDependency
-
-
-# Dependencies for simple BaseReader
-@dataclass
-class IndexesParams(DefaultDependency):
-    """Band Indexes parameters."""
-
-    bidx: Optional[List[int]] = Query(
-        None, title="Band indexes", description="band indexes",
-    )
-
-    def __post_init__(self):
-        """Post Init."""
-        if self.bidx is not None:
-            self.kwargs["indexes"] = self.bidx
-
-
-# Dependencies for  MultiBaseReader
-@dataclass
-class AssetsParams(DefaultDependency):
-    """Asset and Band indexes parameters."""
-
-    asset: Optional[List[str]] = Query(
-        None, title="Asset indexes", description="asset names."
-    )
-
-    default_assets: Optional[List[str]] = field(init=False)
-
-    def __post_init__(self):
-        """Post Init."""
-        if self.asset or self.default_assets:
-            self.kwargs["assets"] = self.asset or self.default_assets
-
-
-# Dependencies for  MultiBandReader
-@dataclass
-class BandsParams(DefaultDependency):
-    """Band names parameters."""
-
-    band: Optional[List[str]] = Query(
-        None, title="bands names", description="bands names.",
-    )
-
-    default_bands: Optional[List[str]] = field(init=False)
-
-    def __post_init__(self):
-        """Post Init."""
-        if self.band or self.default_bands:
-            self.kwargs["bands"] = self.band or self.default_bands
diff --git a/rio_viz/io/__init__.py b/rio_viz/io/__init__.py
index d372ec1..c134ce2 100644
--- a/rio_viz/io/__init__.py
+++ b/rio_viz/io/__init__.py
@@ -1,4 +1,4 @@
 """rio_viz.io."""
 
 from rio_viz.io.mosaic import MosaicReader  # noqa
-from rio_viz.io.reader import MultiFilesReader  # noqa
+from rio_viz.io.reader import MultiFilesAssetsReader, MultiFilesBandsReader  # noqa
diff --git a/rio_viz/io/mosaic.py b/rio_viz/io/mosaic.py
index 29a0a06..49992d0 100644
--- a/rio_viz/io/mosaic.py
+++ b/rio_viz/io/mosaic.py
@@ -5,10 +5,10 @@
 import attr
 from braceexpand import braceexpand
 from morecantile import TileMatrixSet
-from rio_tiler.constants import WEB_MERCATOR_TMS, BBox
+from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
 from rio_tiler.errors import PointOutsideBounds
 from rio_tiler.io import BaseReader, COGReader
-from rio_tiler.models import ImageData, ImageStatistics, Info
+from rio_tiler.models import BandStatistics, ImageData, Info
 from rio_tiler.mosaic import mosaic_reader
 from rio_tiler.tasks import multi_values
 
@@ -18,29 +18,34 @@ class MosaicReader(BaseReader):
     """Simple Mosaic reader.
 
     Args:
-        filepath (str): Brace Expandable path (e.g: file{1,2,3}.tif).
+        input (str): Brace Expandable path (e.g: file{1,2,3}.tif).
 
     """
 
-    filepath: str = attr.ib()
-    reader: Type[BaseReader] = attr.ib(default=COGReader)
+    input: str = attr.ib()
     tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
-    bounds: BBox = attr.ib(init=False)
-    minzoom: int = attr.ib(init=False)
-    maxzoom: int = attr.ib(init=False)
+
+    reader: Type[COGReader] = attr.ib(default=COGReader)
 
     colormap: Dict = attr.ib(init=False)
 
+    datasets: Dict[str, Type[COGReader]] = attr.ib(init=False)
+
     def __attrs_post_init__(self):
         """Fetch Reference band to get the bounds."""
         self.datasets = {
-            src_path: COGReader(src_path, tms=self.tms)
-            for src_path in braceexpand(self.filepath)
+            src_path: self.reader(src_path, tms=self.tms)
+            for src_path in braceexpand(self.input)
         }
 
         self.minzoom = min([cog.minzoom for cog in self.datasets.values()])
         self.maxzoom = max([cog.maxzoom for cog in self.datasets.values()])
 
+        self.crs = WGS84_CRS
+        bounds = [cog.geographic_bounds for cog in self.datasets.values()]
+        minx, miny, maxx, maxy = zip(*bounds)
+        self.bounds = [min(minx), min(miny), max(maxx), max(maxy)]
+
         # check for unique dtype
         dtypes = {cog.dataset.dtypes[0] for cog in self.datasets.values()}
         if len(dtypes) > 1:
@@ -56,14 +61,6 @@ def __attrs_post_init__(self):
             # !!! We take the first one ¡¡¡
             self.colormap = list(cmaps)[0]
 
-        xs = []
-        ys = []
-        for dataset in self.datasets.values():
-            left, bottom, right, top = dataset.bounds
-            xs.extend([left, right])
-            ys.extend([bottom, top])
-        self.bounds = (min(xs), min(ys), max(xs), max(ys))
-
     def __exit__(self, exc_type, exc_value, traceback):
         """Support using with Context Managers."""
         for dataset in self.datasets.values():
@@ -79,9 +76,7 @@ def tile(
     ) -> ImageData:
         """Get Tile"""
         mosaic_assets = (
-            list(reversed(self.datasets.keys()))
-            if reverse
-            else list(self.datasets.keys())
+            list(reversed(list(self.datasets))) if reverse else list(self.datasets)
         )
 
         def _reader(
@@ -94,13 +89,15 @@ def _reader(
         )[0]
 
     def point(
-        self, lon: float, lat: float, reverse: bool = False, **kwargs: Any,
+        self,
+        lon: float,
+        lat: float,
+        reverse: bool = False,
+        **kwargs: Any,
     ) -> List:
         """Get Point value."""
         mosaic_assets = (
-            list(reversed(self.datasets.keys()))
-            if reverse
-            else list(self.datasets.keys())
+            list(reversed(list(self.datasets))) if reverse else list(self.datasets)
         )
 
         def _reader(asset: str, lon: float, lat: float, **kwargs) -> List:
@@ -122,45 +119,44 @@ def _reader(asset: str, lon: float, lat: float, **kwargs) -> List:
     def info(self) -> Info:
         """info."""
         # !!! We return info from the first dataset
-        # Most of the info should be simirlar in other files ¡¡¡
-        item = list(self.datasets.keys())[0]
+        # Most of the info should be similar in other files ¡¡¡
+        item = list(self.datasets)[0]
         info_metadata = (
             self.datasets[item]
             .info()
             .dict(
                 exclude={
                     "bounds",
-                    "center",
                     "minzoom",
                     "maxzoom",
                     "width",
                     "height",
                     "overviews",
-                }
+                },
             )
         )
-        spatial_metadata = self.spatial_info.dict()
-        return Info(**info_metadata, **spatial_metadata)
+        info_metadata["bounds"] = self.bounds
+        info_metadata["minzoom"] = self.minzoom
+        info_metadata["maxzoom"] = self.maxzoom
+        return Info(**info_metadata)
 
-    def stats(
-        self, pmin: float = 2.0, pmax: float = 98.0, **kwargs: Any,
-    ) -> Dict[str, ImageStatistics]:
+    def statistics(self, **kwargs: Any) -> Dict[str, BandStatistics]:
         """Return Dataset's statistics."""
         # FOR NOW WE ONLY RETURN VALUE FROM THE FIRST FILE
-        item = list(self.datasets.keys())[0]
-        return self.datasets[item].stats(pmin, pmax, **kwargs)
+        item = list(self.datasets)[0]
+        return self.datasets[item].statistics(**kwargs)
 
     ############################################################################
     # Not Implemented methods
     # BaseReader required those method to be implemented
-    def preview(self):
+    def preview(self, *args, **kwargs):
         """PlaceHolder for BaseReader.preview."""
         raise NotImplementedError
 
-    def part(self):
+    def part(self, *args, **kwargs):
         """PlaceHolder for BaseReader.part."""
         raise NotImplementedError
 
-    def feature(self):
+    def feature(self, *args, **kwargs):
         """PlaceHolder for BaseReader.feature."""
         raise NotImplementedError
diff --git a/rio_viz/io/reader.py b/rio_viz/io/reader.py
index a6001a7..4cf51c5 100644
--- a/rio_viz/io/reader.py
+++ b/rio_viz/io/reader.py
@@ -1,37 +1,35 @@
 """rio-viz multifile reader."""
 
-from typing import Dict, Type
+from typing import Any, Dict, List, Type
 
 import attr
 from braceexpand import braceexpand
 from morecantile import TileMatrixSet
 from rio_tiler.constants import WEB_MERCATOR_TMS
 from rio_tiler.errors import InvalidBandName
-from rio_tiler.io import BaseReader, COGReader, MultiBandReader
+from rio_tiler.io import BaseReader, COGReader, MultiBandReader, MultiBaseReader
 
 
 @attr.s
-class MultiFilesReader(MultiBandReader):
-    """Multiple Files reader.
+class MultiFilesBandsReader(MultiBandReader):
+    """Multiple Files as Bands."""
 
-    Args:
-        filepath (str): Brace Expandable path (e.g: band{1,2,3}.tif).
-
-    """
+    input: Any = attr.ib()
+    tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
 
-    filepath: str = attr.ib()
-    reader: Type[BaseReader] = attr.ib(default=COGReader)
     reader_options: Dict = attr.ib(factory=dict)
-    tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
+    reader: Type[BaseReader] = attr.ib(default=COGReader)
+
+    files: List[str] = attr.ib(init=False)
 
     def __attrs_post_init__(self):
         """Fetch Reference band to get the bounds."""
-        self.files = list(braceexpand(self.filepath))
-        self.indexes = [ix + 1 for ix in range(len(self.files))]
-        self.bands = [f"file{ix}" for ix in self.indexes]
+        self.files = list(braceexpand(self.input))
+        self.bands = [f"b{ix + 1}" for ix in range(len(self.files))]
 
         with self.reader(self.files[0], tms=self.tms, **self.reader_options) as cog:
             self.bounds = cog.bounds
+            self.crs = cog.crs
             self.minzoom = cog.minzoom
             self.maxzoom = cog.maxzoom
 
@@ -42,3 +40,35 @@ def _get_band_url(self, band: str) -> str:
 
         index = self.bands.index(band)
         return self.files[index]
+
+
+@attr.s
+class MultiFilesAssetsReader(MultiBaseReader):
+    """Multiple Files as Assets."""
+
+    input: Any = attr.ib()
+    tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
+
+    reader_options: Dict = attr.ib(factory=dict)
+    reader: Type[BaseReader] = attr.ib(default=COGReader)
+
+    files: List[str] = attr.ib(init=False)
+
+    def __attrs_post_init__(self):
+        """Fetch Reference band to get the bounds."""
+        self.files = list(braceexpand(self.input))
+        self.assets = [f"asset{ix + 1}" for ix in range(len(self.files))]
+
+        with self.reader(self.files[0], tms=self.tms, **self.reader_options) as cog:
+            self.bounds = cog.bounds
+            self.crs = cog.crs
+            self.minzoom = cog.minzoom
+            self.maxzoom = cog.maxzoom
+
+    def _get_asset_url(self, asset: str) -> str:
+        """Validate band's name and return band's url."""
+        if asset not in self.assets:
+            raise InvalidBandName(f"{asset} is not valid")
+
+        index = self.assets.index(asset)
+        return self.files[index]
diff --git a/rio_viz/scripts/cli.py b/rio_viz/scripts/cli.py
index 64ef8fb..815f6a6 100644
--- a/rio_viz/scripts/cli.py
+++ b/rio_viz/scripts/cli.py
@@ -34,31 +34,8 @@ def TemporaryRasterFile(suffix=".tif"):
         os.remove(fileobj.name)
 
 
-class MbxTokenType(click.ParamType):
-    """Mapbox token type."""
-
-    name = "token"
-
-    def convert(self, value, param, ctx):
-        """Validate token."""
-        try:
-            if not value:
-                return ""
-
-            assert value.startswith("pk")
-            return value
-
-        except (AttributeError, AssertionError):
-            raise click.ClickException(
-                "Mapbox access token must be public (pk). "
-                "Please sign up at https://www.mapbox.com/signup/ to get a public token. "
-                "If you already have an account, you can retreive your "
-                "token at https://www.mapbox.com/account/."
-            )
-
-
 class NodataParamType(click.ParamType):
-    """Nodata inddex type."""
+    """Nodata index type."""
 
     name = "nodata"
 
@@ -84,16 +61,14 @@ def convert(self, value, param, ctx):
     help="Set nodata masking values for input dataset.",
 )
 @click.option(
-    "--minzoom", type=int, help="Overwrite minzoom",
+    "--minzoom",
+    type=int,
+    help="Overwrite minzoom",
 )
 @click.option(
-    "--maxzoom", type=int, help="Overwrite maxzoom",
-)
-@click.option(
-    "--style",
-    type=click.Choice(["dark", "satellite", "basic"]),
-    default="dark",
-    help="Mapbox basemap",
+    "--maxzoom",
+    type=int,
+    help="Overwrite maxzoom",
 )
 @click.option("--port", type=int, default=8080, help="Webserver port (default: 8080)")
 @click.option(
@@ -102,13 +77,6 @@ def convert(self, value, param, ctx):
     default="127.0.0.1",
     help="Webserver host url (default: 127.0.0.1)",
 )
-@click.option(
-    "--mapbox-token",
-    type=MbxTokenType(),
-    metavar="TOKEN",
-    default=lambda: os.environ.get("MAPBOX_ACCESS_TOKEN", ""),
-    help="Pass Mapbox token",
-)
 @click.option("--no-check", is_flag=True, help="Ignore COG validation")
 @click.option(
     "--reader",
@@ -118,7 +86,8 @@ def convert(self, value, param, ctx):
 @click.option(
     "--layers",
     type=str,
-    help="limit to specific layers (only used for MultiBand and MultiBase Readers).",
+    help="limit to specific layers (only used for MultiBand and MultiBase Readers) (e.g --layers b1 --layers b2).",
+    multiple=True,
 )
 @click.option(
     "--server-only",
@@ -139,10 +108,8 @@ def viz(
     nodata,
     minzoom,
     maxzoom,
-    style,
     port,
     host,
-    mapbox_token,
     no_check,
     reader,
     layers,
@@ -153,8 +120,10 @@ def viz(
     if reader:
         module, classname = reader.rsplit(".", 1)
         reader = getattr(importlib.import_module(module), classname)  # noqa
-        if not issubclass(reader, (BaseReader, AsyncBaseReader)):
-            warnings.warn("Reader should be a subclass of rio_tiler.io.BaseReader")
+        if not issubclass(
+            reader, (BaseReader, AsyncBaseReader, MultiBandReader, MultiBaseReader)
+        ):
+            warnings.warn(f"Invalid reader type: {type(reader)}")
 
     dataset_reader = reader or COGReader
 
@@ -174,7 +143,7 @@ def viz(
             and not cog_validate(src_path)[0]
         ):
             # create tmp COG
-            click.echo("create temporaty COG")
+            click.echo("create temporary COG")
             tmp_path = ctx.enter_context(TemporaryRasterFile())
 
             output_profile = cog_profiles.get("deflate")
@@ -192,10 +161,8 @@ def viz(
         application = app.viz(
             src_path=src_path,
             reader=dataset_reader,
-            token=mapbox_token,
             port=port,
             host=host,
-            style=style,
             config=config,
             minzoom=minzoom,
             maxzoom=maxzoom,
diff --git a/rio_viz/templates/assets.html b/rio_viz/templates/assets.html
index 16188e2..039737e 100644
--- a/rio_viz/templates/assets.html
+++ b/rio_viz/templates/assets.html
@@ -307,49 +307,68 @@
         </div>
 
         <script>
-    var scope = {
-        assets: undefined,
-        info: undefined,
-        metadata: undefined,
-    }
-
-    mapboxgl.accessToken = '{{ mapbox_access_token  }}'
-
-    const tilejson_endpoint = '{{ tilejson_endpoint }}'
-    const info_endpoint = '{{ info_endpoint }}'
-    const stats_endpoint = '{{ stats_endpoint }}'
-    const point_endpoint = '{{ point_endpoint }}'
-
-    const dtype_ranges = {
-      'int8': [-128, 127],
-      'uint8': [0, 255],
-      'uint16': [0, 65535],
-      'int16': [-32768, 32767],
-      'uint32': [0, 4294967295],
-      'int32': [-2147483648, 2147483647],
-      'float32': [-3.4028235e+38, 3.4028235e+38],
-      'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
-    }
+var scope = {
+    assets: undefined,
+    info: undefined,
+    metadata: undefined,
+}
 
-    if ('{{ allow_3d }}' !== "True") {
-      document.getElementById('3d-point').classList.add('none')
-      document.getElementById('3d-poly').classList.add('none')
-    }
+mapboxgl.accessToken = ''
+
+const tilejson_endpoint = '{{ tilejson_endpoint }}'
+const info_endpoint = '{{ info_endpoint }}'
+const stats_endpoint = '{{ stats_endpoint }}'
+const point_endpoint = '{{ point_endpoint }}'
+
+const dtype_ranges = {
+  'int8': [-128, 127],
+  'uint8': [0, 255],
+  'uint16': [0, 65535],
+  'int16': [-32768, 32767],
+  'uint32': [0, 4294967295],
+  'int32': [-2147483648, 2147483647],
+  'float32': [-3.4028235e+38, 3.4028235e+38],
+  'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
+}
 
-let style
-if (mapboxgl.accessToken !== '') {
-    style = 'mapbox://styles/mapbox/{{ mapbox_style }}-v9'
-} else {
-    style = { version: 8, sources: {}, layers: [] }
+if ('{{ allow_3d }}' !== "True") {
+  document.getElementById('3d-point').classList.add('none')
+  document.getElementById('3d-poly').classList.add('none')
 }
 
 var map = new mapboxgl.Map({
-    container: 'map',
-    style: style,
-    center: [0, 0],
-    zoom: 1
+  container: 'map',
+  style: {
+    version: 8,
+    sources: {
+      'toner-lite': {
+        type: 'raster',
+        tiles: [
+          'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png'
+        ],
+        tileSize: 256,
+        attribution:
+          'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
+      }
+    },
+    layers: [
+      {
+        'id': 'basemap',
+        'type': 'raster',
+        'source': 'toner-lite',
+        'minzoom': 0,
+        'maxzoom': 20
+      }
+    ]
+  },
+  center: [0, 0],
+  zoom: 1
 })
 
+
 map.on('zoom', function (e) {
     const z = (map.getZoom()).toString().slice(0, 6)
     document.getElementById('zoom').textContent = z
@@ -359,10 +378,10 @@
     params = {}
 
     const asset = document.getElementById('asset-selector').value
-    params.asset = asset
+    params.assets = asset
 
     const bidx = document.getElementById('layer-selector').value
-    params.bidx = bidx
+    params.asset_bidx = `${asset}|${bidx}`
 
     if (['uint8','int8'].indexOf(scope.info[asset].dtype) === -1) {
         params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
@@ -393,17 +412,16 @@
 
     params = {}
 
-    let assets
-    let indexes
+    let asset_params
+    let indexes_params
     if (document.getElementById("compose-switch").checked === true) {
-        assets = [r, g, b]
-        indexes = [1]
+        asset_params = [r, g, b].map(i => `assets=${i}`).join('&')
+        indexes_params =  [r, g, b].map(i => `asset_bidx=${i}|1`).join('&')
         params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
-
     } else {
         const asset = document.getElementById('asset-selector').value
-        assets = [asset]
-        indexes = [r, g, b]
+        asset_params = `assets=${asset}`
+        indexes_params =  `asset_bidx=${asset}|${r},${g},${b}`
 
         if (['uint8','int8'].indexOf(scope.info[asset].dtype) === -1) {
             params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
@@ -415,8 +433,6 @@
     }
 
     const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
-    const asset_params = assets.map(i => `asset=${i}`).join('&')
-    const indexes_params = indexes.map(i => `bidx=${i}`).join('&')
     let url = `{{ tilejson_endpoint }}?${url_params}&${asset_params}&${indexes_params}`
 
     map.addSource('raster', { type: 'raster', url: url, tileSize: 256 })
diff --git a/rio_viz/templates/bands.html b/rio_viz/templates/bands.html
index dfa3ee5..8f20457 100644
--- a/rio_viz/templates/bands.html
+++ b/rio_viz/templates/bands.html
@@ -311,399 +311,334 @@
     </div>
 
     <script>
-    var scope = {
-      dataset_statistics: undefined,
-      data_type: undefined,
-      band_descriptions: undefined
-    }
-
-    mapboxgl.accessToken = '{{ mapbox_access_token  }}'
-
-    const tilejson_endpoint = '{{ tilejson_endpoint }}'
-    const info_endpoint = '{{ info_endpoint }}'
-    const stats_endpoint = '{{ stats_endpoint }}'
-    const point_endpoint = '{{ point_endpoint }}'
-
-    const dtype_ranges = {
-      'int8': [-128, 127],
-      'uint8': [0, 255],
-      'uint16': [0, 65535],
-      'int16': [-32768, 32767],
-      'uint32': [0, 4294967295],
-      'int32': [-2147483648, 2147483647],
-      'float32': [-3.4028235e+38, 3.4028235e+38],
-      'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
-    }
-
-    if ('{{ allow_3d }}' !== "True") {
-      document.getElementById('3d-point').classList.add('none')
-      document.getElementById('3d-poly').classList.add('none')
-    }
-
-    let style
-    if (mapboxgl.accessToken !== '') {
-      style = 'mapbox://styles/mapbox/{{ mapbox_style }}-v9'
-    } else {
-      style = { version: 8, sources: {}, layers: [] }
-    }
-
-    var map = new mapboxgl.Map({
-        container: 'map',
-        style: style,
-        center: [0, 0],
-        zoom: 1
-    })
-
-    map.on('zoom', function (e) {
-      const z = (map.getZoom()).toString().slice(0, 6)
-      document.getElementById('zoom').textContent = z
-    })
-
-    const set1bViz = () => {
-      params = {}
-      const vizType = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
-      switch (vizType) {
-        case 'raster':
-          params.band = document.getElementById('layer-selector').selectedOptions[0].getAttribute("band")
-
-          if (['uint8','int8'].indexOf(scope.data_type) === -1) {
-            params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
-          }
-
-          const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex]
-          if (cmap.value !== 'b&w') params.colormap_name = cmap.value
-
-          const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
-          let url = `${tilejson_endpoint}?${url_params}`
-
-          map.addSource('raster', { type: 'raster', url: url , tileSize: 256})
-          addLayer(vizType)
-          break
-
-        case 'point':
-          map.addSource('mvt', {
-            type: 'vector',
-            url: `${tilejson_endpoint}?tile_format=pbf&feature_type=point`
-          })
-          addLayer(vizType)
-          break
-
-        case 'polygon':
-          map.addSource('mvt', {
-            type: 'vector',
-            url: `${tilejson_endpoint}?tile_format=pbf&feature_type=polygon`
-          })
-          addLayer(vizType)
-          break
-
-        default:
-          throw new Error(`Invalid ${vizType}`)
+var scope = {
+  dataset_statistics: undefined,
+  data_type: undefined,
+  band_descriptions: undefined
+}
+
+mapboxgl.accessToken = ''
+
+const tilejson_endpoint = '{{ tilejson_endpoint }}'
+const info_endpoint = '{{ info_endpoint }}'
+const stats_endpoint = '{{ stats_endpoint }}'
+const point_endpoint = '{{ point_endpoint }}'
+
+const dtype_ranges = {
+  'int8': [-128, 127],
+  'uint8': [0, 255],
+  'uint16': [0, 65535],
+  'int16': [-32768, 32767],
+  'uint32': [0, 4294967295],
+  'int32': [-2147483648, 2147483647],
+  'float32': [-3.4028235e+38, 3.4028235e+38],
+  'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
+}
+
+if ('{{ allow_3d }}' !== "True") {
+  document.getElementById('3d-point').classList.add('none')
+  document.getElementById('3d-poly').classList.add('none')
+}
+
+var map = new mapboxgl.Map({
+  container: 'map',
+  style: {
+    version: 8,
+    sources: {
+      'toner-lite': {
+        type: 'raster',
+        tiles: [
+          'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png'
+        ],
+        tileSize: 256,
+        attribution:
+          'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
       }
-    }
-
-    const set3bViz = () => {
-      const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("band")
-      const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("band")
-      const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("band")
+    },
+    layers: [
+      {
+        'id': 'basemap',
+        'type': 'raster',
+        'source': 'toner-lite',
+        'minzoom': 0,
+        'maxzoom': 20
+      }
+    ]
+  },
+  center: [0, 0],
+  zoom: 1
+})
+
+map.on('zoom', function (e) {
+  const z = (map.getZoom()).toString().slice(0, 6)
+  document.getElementById('zoom').textContent = z
+})
+
+const set1bViz = () => {
+  params = {}
+  const vizType = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
+  switch (vizType) {
+    case 'raster':
+      params.bands = document.getElementById('layer-selector').selectedOptions[0].getAttribute("band")
 
-      params = {}
       if (['uint8','int8'].indexOf(scope.data_type) === -1) {
         params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
       }
-      let url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
-      if (url_params !== '') url_params = `${url_params}&`
 
-      const bands_params = [r, g, b].map(i => `band=${i}`).join('&')
-      let url = `${tilejson_endpoint}?${url_params}${bands_params}`
+      const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex]
+      if (cmap.value !== 'b&w') params.colormap_name = cmap.value
 
-      map.addSource('raster', { type: 'raster', url: url, tileSize: 256})
-      map.addLayer({id: 'raster', type: 'raster', source: 'raster'})
-      if (scope.dataset_statistics) addHisto3Bands()
-    }
+      const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
+      let url = `${tilejson_endpoint}?${url_params}`
 
-    const switchViz = () => {
-      if (map.getLayer('raster')) map.removeLayer('raster')
-      if (map.getSource('raster')) map.removeSource('raster')
-
-      if (map.getLayer('mvt')) map.removeLayer('mvt')
-      if (map.getSource('mvt')) { map.removeSource('mvt') }
-
-      const rasterType = document.getElementById('toolbar').querySelector(".active").id
-      switch (rasterType) {
-        case '1b':
-          set1bViz()
-          break
-        case '3b':
-          set3bViz()
-          break
-        default:
-          throw new Error(`Invalid ${rasterType}`)
-      }
-    }
-
-    const addLayer = (layerType) => {
-      if (map.getLayer('raster')) map.removeLayer('raster')
-      if (map.getLayer('mvt')) map.removeLayer('mvt')
-
-      let sMin = parseFloat(document.getElementById('data-min').value)
-      const sMax = parseFloat(document.getElementById('data-max').value)
-      if (sMin === sMax) sMin = sMin - 0.00001
-
-      if (scope.dataset_statistics) addHisto1Band()
-
-      const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
-      const propName = active_layer.value
-      const exag = parseFloat(document.getElementById('ex-value').value)
-
-      switch (layerType) {
-        case 'raster':
-          map.addLayer({
-            id: 'raster',
-            type: 'raster',
-            source: 'raster'
-          })
-          break
-
-        case 'point':
-          map.addLayer({
-            id: 'mvt',
-            source: 'mvt',
-            'source-layer': 'my_layer',
-            type: 'circle',
-            paint: {
-              'circle-color': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, '#3700f0',
-                sMax, '#ed0707'
-              ],
-              'circle-radius': {
-                'base': 1,
-                'stops': [
-                  [0, 10],
-                  [9, 5]
-                ]
-              }
-            }
-          })
-
-          break
-
-        case 'polygon':
-          map.addLayer({
-            id: 'mvt',
-            source: 'mvt',
-            'source-layer': 'my_layer',
-            type: 'fill-extrusion',
-            paint: {
-              'fill-extrusion-opacity': 1,
-              'fill-extrusion-height': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, 0,
-                sMax, sMax * exag
-              ],
-              'fill-extrusion-color': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, '#3700f0',
-                sMax, '#ed0707'
-              ]
-            }
-          })
+      map.addSource('raster', { type: 'raster', url: url , tileSize: 256})
+      addLayer(vizType)
+      break
 
-          break
-
-        default:
-          throw new Error(`Invalid ${layerType}`)
-      }
-    }
-
-    const addHisto3Bands = () => {
-      const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("name")
-      const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("name")
-      const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("name")
-
-      const rStats = scope.dataset_statistics[r]
-      const gStats = scope.dataset_statistics[g]
-      const bStats = scope.dataset_statistics[b]
-
-      const minV = Math.min(...[rStats.min, gStats.min, bStats.min])
-      const maxV = Math.max(...[rStats.max, gStats.max, bStats.max])
-
-      let rCounts = rStats.histogram[0]
-      let gCounts = gStats.histogram[0]
-      let bCounts = bStats.histogram[0]
-
-      const rValues = rStats.histogram[1]
-      const gValues = gStats.histogram[1]
-      const bValues = bStats.histogram[1]
-
-      const add = (a, b) => a + b
-
-      const sumR = rCounts.reduce(add)
-      const sumG = gCounts.reduce(add)
-      const sumB = bCounts.reduce(add)
-
-      rCounts = rCounts.map((e) => {return e / sumR * 100})
-      gCounts = gCounts.map((e) => {return e / sumG * 100})
-      bCounts = bCounts.map((e) => {return e / sumB * 100})
-      const maxH = Math.max(...rCounts, ...gCounts, ...bCounts)
-
-      const bbox = d3.select('#histogram').node().getBoundingClientRect()
-
-      // set the dimensions and margins of the graph
-      const margin = { top: 10, right: 30, bottom: 30, left: 40 }
-      const width = bbox.width - margin.left - margin.right
-      const height = bbox.height - margin.top - margin.bottom
-
-      d3.select('#histogram').selectAll('*').remove()
-      // append the svg object to the body of the page
-      var svg = d3.select('#histogram')
-        .append('svg')
-        .attr('width', width + margin.left + margin.right)
-        .attr('height', height + margin.top + margin.bottom)
-        .append('g')
-        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
-
-      // X axis: scale and draw:
-      var x = d3.scaleLinear()
-        .domain([minV, maxV])
-        .range([0, width])
-
-      svg.append('g')
-        .attr('transform', 'translate(0,' + height + ')')
-        .call(d3.axisBottom(x))
-
-      // Y axis: scale and draw:
-      var y = d3.scaleLinear().range([height, 0])
-      y.domain([0, maxH + 5])
-      svg.append('g').call(d3.axisLeft(y))
-
-      const addLine = (counts, values, color) => {
-        const data = []
-        for (var i = 0; i < counts.length; i++) {
-          data.push({count: counts[i], value: values[i]})
-        }
+    case 'point':
+      map.addSource('mvt', {
+        type: 'vector',
+        url: `${tilejson_endpoint}?tile_format=pbf&feature_type=point`
+      })
+      addLayer(vizType)
+      break
 
-        var guide = d3.line()
-                      .x(function(d){ return x(d.value) })
-                      .y(function(d){ return y(d.count) });
+    case 'polygon':
+      map.addSource('mvt', {
+        type: 'vector',
+        url: `${tilejson_endpoint}?tile_format=pbf&feature_type=polygon`
+      })
+      addLayer(vizType)
+      break
+
+    default:
+      throw new Error(`Invalid ${vizType}`)
+  }
+}
+
+const set3bViz = () => {
+  const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("band")
+  const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("band")
+  const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("band")
+
+  params = {}
+  if (['uint8','int8'].indexOf(scope.data_type) === -1) {
+    params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
+  }
+  let url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
+  if (url_params !== '') url_params = `${url_params}&`
+
+  const bands_params = [r, g, b].map(i => `bands=${i}`).join('&')
+  let url = `${tilejson_endpoint}?${url_params}${bands_params}`
+
+  map.addSource('raster', { type: 'raster', url: url, tileSize: 256})
+  map.addLayer({id: 'raster', type: 'raster', source: 'raster'})
+  if (scope.dataset_statistics) addHisto3Bands()
+}
+
+const switchViz = () => {
+  if (map.getLayer('raster')) map.removeLayer('raster')
+  if (map.getSource('raster')) map.removeSource('raster')
+
+  if (map.getLayer('mvt')) map.removeLayer('mvt')
+  if (map.getSource('mvt')) { map.removeSource('mvt') }
+
+  const rasterType = document.getElementById('toolbar').querySelector(".active").id
+  switch (rasterType) {
+    case '1b':
+      set1bViz()
+      break
+    case '3b':
+      set3bViz()
+      break
+    default:
+      throw new Error(`Invalid ${rasterType}`)
+  }
+}
+
+const addLayer = (layerType) => {
+  if (map.getLayer('raster')) map.removeLayer('raster')
+  if (map.getLayer('mvt')) map.removeLayer('mvt')
+
+  let sMin = parseFloat(document.getElementById('data-min').value)
+  const sMax = parseFloat(document.getElementById('data-max').value)
+  if (sMin === sMax) sMin = sMin - 0.00001
+
+  if (scope.dataset_statistics) addHisto1Band()
+
+  const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
+  const propName = active_layer.value
+  const exag = parseFloat(document.getElementById('ex-value').value)
+
+  switch (layerType) {
+    case 'raster':
+      map.addLayer({
+        id: 'raster',
+        type: 'raster',
+        source: 'raster'
+      })
+      break
 
-        var line = svg.append('path')
-                      .datum(data)
-                      .attr('d', guide)
-                      .attr('class', `line-${color}`);
-      }
-      addLine(rCounts, rValues, "red")
-      addLine(gCounts, gValues, "green")
-      addLine(bCounts, bValues, "blue")
-
-      //Draw axes
-      svg.append("g")
-          .attr("class", "x axis")
-          .attr("transform", "translate(0," + maxH + 5 + ")")
-          .call(x);
-
-      svg.append("g")
-          .attr("class", "y axis")
-          .call(y)
-          .append("text")
-          .attr("transform", "rotate(-90)")
-          .attr("y", 6)
-          .attr("dy", ".71em")
-          .style("text-anchor", "end")
-
-          const data = {
-            "Min": `${rStats.min}, ${gStats.min}, ${bStats.min}`,
-            "Max": `${rStats.max}, ${gStats.max}, ${bStats.max}`,
-            "Stddev": `${rStats.std.toFixed(2)}, ${gStats.std.toFixed(2)}, ${bStats.std.toFixed(2)}`,
+    case 'point':
+      map.addLayer({
+        id: 'mvt',
+        source: 'mvt',
+        'source-layer': 'my_layer',
+        type: 'circle',
+        paint: {
+          'circle-color': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, '#3700f0',
+            sMax, '#ed0707'
+          ],
+          'circle-radius': {
+            'base': 1,
+            'stops': [
+              [0, 10],
+              [9, 5]
+            ]
           }
+        }
+      })
 
-          let table = document.getElementById("histogram-table")
-          table.innerHTML = ""
+      break
 
-          for (let element of Object.entries(data)) {
-            let row = table.insertRow();
-            for (key in element) {
-              let cell = row.insertCell();
-              let text = document.createTextNode(element[key]);
-              cell.appendChild(text);
-            }
-          }
-          table.classList.remove('none')
+    case 'polygon':
+      map.addLayer({
+        id: 'mvt',
+        source: 'mvt',
+        'source-layer': 'my_layer',
+        type: 'fill-extrusion',
+        paint: {
+          'fill-extrusion-opacity': 1,
+          'fill-extrusion-height': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, 0,
+            sMax, sMax * exag
+          ],
+          'fill-extrusion-color': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, '#3700f0',
+            sMax, '#ed0707'
+          ]
         }
+      })
 
-    const addHisto1Band = () => {
-      const stats = scope.dataset_statistics[document.getElementById('layer-selector').selectedOptions[0].getAttribute("name")]
-
-      let counts = stats.histogram[0]
-      const sum = counts.reduce(function(a, b){
-          return a + b;
-      }, 0);
-      counts = counts.map((e) => {return e / sum * 100})
-      const maxH = Math.max(...counts)
-
-      const values = stats.histogram[1]
-      const bbox = d3.select('#histogram').node().getBoundingClientRect()
-
-      // set the dimensions and margins of the graph
-      const margin = { top: 10, right: 30, bottom: 30, left: 40 }
-      const width = bbox.width - margin.left - margin.right
-      const height = bbox.height - margin.top - margin.bottom
-
-      d3.select('#histogram').selectAll('*').remove()
-      // append the svg object to the body of the page
-      var svg = d3.select('#histogram')
-        .append('svg')
-        .attr('width', width + margin.left + margin.right)
-        .attr('height', height + margin.top + margin.bottom)
-        .append('g')
-        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
-
-      const min = stats.min
-      const max = stats.max
-
-      // X axis: scale and draw:
-      var x = d3.scaleLinear()
-        .domain([min, max])
-        .range([0, width])
-
-      svg.append('g')
-        .attr('transform', 'translate(0,' + height + ')')
-        .call(d3.axisBottom(x))
-
-      // Y axis: scale and draw:
-      var y = d3.scaleLinear().range([height, 0])
-      y.domain([0, maxH])
-      svg.append('g').call(d3.axisLeft(y))
-
-      const bins = []
-      for (var i = 0; i < counts.length; i++) {
-        bins.push({
-          count: counts[i],
-          value: values[i]
-        })
-      }
+      break
+
+    default:
+      throw new Error(`Invalid ${layerType}`)
+  }
+}
+
+const addHisto3Bands = () => {
+  const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("name")
+  const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("name")
+  const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("name")
+
+  const rStats = scope.dataset_statistics[r]
+  const gStats = scope.dataset_statistics[g]
+  const bStats = scope.dataset_statistics[b]
+
+  const minV = Math.min(...[rStats.min, gStats.min, bStats.min])
+  const maxV = Math.max(...[rStats.max, gStats.max, bStats.max])
+
+  let rCounts = rStats.histogram[0]
+  let gCounts = gStats.histogram[0]
+  let bCounts = bStats.histogram[0]
+
+  const rValues = rStats.histogram[1]
+  const gValues = gStats.histogram[1]
+  const bValues = bStats.histogram[1]
+
+  const add = (a, b) => a + b
+
+  const sumR = rCounts.reduce(add)
+  const sumG = gCounts.reduce(add)
+  const sumB = bCounts.reduce(add)
+
+  rCounts = rCounts.map((e) => {return e / sumR * 100})
+  gCounts = gCounts.map((e) => {return e / sumG * 100})
+  bCounts = bCounts.map((e) => {return e / sumB * 100})
+  const maxH = Math.max(...rCounts, ...gCounts, ...bCounts)
+
+  const bbox = d3.select('#histogram').node().getBoundingClientRect()
+
+  // set the dimensions and margins of the graph
+  const margin = { top: 10, right: 30, bottom: 30, left: 40 }
+  const width = bbox.width - margin.left - margin.right
+  const height = bbox.height - margin.top - margin.bottom
+
+  d3.select('#histogram').selectAll('*').remove()
+  // append the svg object to the body of the page
+  var svg = d3.select('#histogram')
+    .append('svg')
+    .attr('width', width + margin.left + margin.right)
+    .attr('height', height + margin.top + margin.bottom)
+    .append('g')
+    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+  // X axis: scale and draw:
+  var x = d3.scaleLinear()
+    .domain([minV, maxV])
+    .range([0, width])
+
+  svg.append('g')
+    .attr('transform', 'translate(0,' + height + ')')
+    .call(d3.axisBottom(x))
+
+  // Y axis: scale and draw:
+  var y = d3.scaleLinear().range([height, 0])
+  y.domain([0, maxH + 5])
+  svg.append('g').call(d3.axisLeft(y))
+
+  const addLine = (counts, values, color) => {
+    const data = []
+    for (var i = 0; i < counts.length; i++) {
+      data.push({count: counts[i], value: values[i]})
+    }
 
-      // append the bar rectangles to the svg element
-      svg.selectAll('rect')
-        .data(bins)
-        .enter()
-        .append('rect')
-        .attr('x', 1)
-        .attr('transform', d => { return 'translate(' + x(d.value) + ',' + y(d.count) + ')' })
-        .attr('width', 10)
-        .attr('height', d => { return height - y(d.count) })
-        .style('fill', '#69b3a2')
-
-        const data = {
-        "Min": stats.min,
-        "Max": stats.max,
-        "Stddev": stats.std.toFixed(2),
+    var guide = d3.line()
+                  .x(function(d){ return x(d.value) })
+                  .y(function(d){ return y(d.count) });
+
+    var line = svg.append('path')
+                  .datum(data)
+                  .attr('d', guide)
+                  .attr('class', `line-${color}`);
+  }
+  addLine(rCounts, rValues, "red")
+  addLine(gCounts, gValues, "green")
+  addLine(bCounts, bValues, "blue")
+
+  //Draw axes
+  svg.append("g")
+      .attr("class", "x axis")
+      .attr("transform", "translate(0," + maxH + 5 + ")")
+      .call(x);
+
+  svg.append("g")
+      .attr("class", "y axis")
+      .call(y)
+      .append("text")
+      .attr("transform", "rotate(-90)")
+      .attr("y", 6)
+      .attr("dy", ".71em")
+      .style("text-anchor", "end")
+
+      const data = {
+        "Min": `${rStats.min}, ${gStats.min}, ${bStats.min}`,
+        "Max": `${rStats.max}, ${gStats.max}, ${bStats.max}`,
+        "Stddev": `${rStats.std.toFixed(2)}, ${gStats.std.toFixed(2)}, ${bStats.std.toFixed(2)}`,
       }
 
       let table = document.getElementById("histogram-table")
@@ -720,321 +655,404 @@
       table.classList.remove('none')
     }
 
-    document.getElementById('btn-stats').addEventListener('click', () => {
-      document.getElementById('fetch-stats-div').classList.add('none')
-      document.getElementById('histogram').classList.remove('none')
-      document.getElementById('histogram-table').classList.remove('none')
+const addHisto1Band = () => {
+  const stats = scope.dataset_statistics[document.getElementById('layer-selector').selectedOptions[0].getAttribute("name")]
+
+  let counts = stats.histogram[0]
+  const sum = counts.reduce(function(a, b){
+      return a + b;
+  }, 0);
+  counts = counts.map((e) => {return e / sum * 100})
+  const maxH = Math.max(...counts)
+
+  const values = stats.histogram[1]
+  const bbox = d3.select('#histogram').node().getBoundingClientRect()
+
+  // set the dimensions and margins of the graph
+  const margin = { top: 10, right: 30, bottom: 30, left: 40 }
+  const width = bbox.width - margin.left - margin.right
+  const height = bbox.height - margin.top - margin.bottom
+
+  d3.select('#histogram').selectAll('*').remove()
+  // append the svg object to the body of the page
+  var svg = d3.select('#histogram')
+    .append('svg')
+    .attr('width', width + margin.left + margin.right)
+    .attr('height', height + margin.top + margin.bottom)
+    .append('g')
+    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+  const min = stats.min
+  const max = stats.max
+
+  // X axis: scale and draw:
+  var x = d3.scaleLinear()
+    .domain([min, max])
+    .range([0, width])
+
+  svg.append('g')
+    .attr('transform', 'translate(0,' + height + ')')
+    .call(d3.axisBottom(x))
+
+  // Y axis: scale and draw:
+  var y = d3.scaleLinear().range([height, 0])
+  y.domain([0, maxH])
+  svg.append('g').call(d3.axisLeft(y))
+
+  const bins = []
+  for (var i = 0; i < counts.length; i++) {
+    bins.push({
+      count: counts[i],
+      value: values[i]
+    })
+  }
+
+  // append the bar rectangles to the svg element
+  svg.selectAll('rect')
+    .data(bins)
+    .enter()
+    .append('rect')
+    .attr('x', 1)
+    .attr('transform', d => { return 'translate(' + x(d.value) + ',' + y(d.count) + ')' })
+    .attr('width', 10)
+    .attr('height', d => { return height - y(d.count) })
+    .style('fill', '#69b3a2')
+
+    const data = {
+    "Min": stats.min,
+    "Max": stats.max,
+    "Stddev": stats.std.toFixed(2),
+  }
+
+  let table = document.getElementById("histogram-table")
+  table.innerHTML = ""
+
+  for (let element of Object.entries(data)) {
+    let row = table.insertRow();
+    for (key in element) {
+      let cell = row.insertCell();
+      let text = document.createTextNode(element[key]);
+      cell.appendChild(text);
+    }
+  }
+  table.classList.remove('none')
+}
+
+document.getElementById('btn-stats').addEventListener('click', () => {
+  document.getElementById('fetch-stats-div').classList.add('none')
+  document.getElementById('histogram').classList.remove('none')
+  document.getElementById('histogram-table').classList.remove('none')
+
+  fetch(`${stats_endpoint}?max_size=256`)
+    .then(res => {
+      if (res.ok) return res.json()
+      throw new Error('Network response was not ok.')
+    })
+    .then(data => {
+      scope.dataset_statistics = data
+      if (document.getElementById('toolbar').querySelector(".active").id === '1b') {
+        addHisto1Band()
+      } else {
+        addHisto3Bands()
+      }
+      document.getElementById('histogram').classList.remove('loading')
+    })
+    .catch(err => {
+      console.warn(err)
+    })
+})
+
+document.getElementById('btn-hide').addEventListener('click', () => {
+  document.getElementById('hide-arrow').classList.toggle('off')
+  document.getElementById('menu').classList.toggle('off')
+})
+
+document.getElementById('viz-selector').addEventListener('change', (e) => {
+  switch (e.target.value) {
+    case 'raster':
+      document.getElementById('colormap-section').classList.remove('none')
+      document.getElementById('extrusion-section').classList.add('none')
+      break
+
+    case 'point':
+      document.getElementById('colormap-section').classList.add('none')
+      document.getElementById('extrusion-section').classList.add('none')
+      break
+
+    case 'polygon':
+      document.getElementById('colormap-section').classList.add('none')
+      document.getElementById('extrusion-section').classList.remove('none')
+      break
+
+    default:
+  }
+  switchViz()
+})
+
+// MVT have already all the layers while for raster we need to fetch new tiles
+const updateViz = () => {
+  const newViz = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
+  if (newViz === "raster") {
+    switchViz()
+  } else {
+    addLayer(newViz)
+  }
+}
+
+document.getElementById('layer-selector').addEventListener('change', () => {
+  const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
+  updateViz()
+})
+
+document.getElementById('r-selector').addEventListener('change', () => {switchViz()})
+document.getElementById('g-selector').addEventListener('change', () => {switchViz()})
+document.getElementById('b-selector').addEventListener('change', () => {switchViz()})
+
+document.getElementById('updateExag').addEventListener('click', () => {
+  updateViz()
+})
+
+document.getElementById('colormap-selector').addEventListener('change', () => {
+  updateViz()
+})
+
+document.getElementById('btn-rescale').addEventListener('click', () => {
+  updateViz()
+})
+
+const switchPane = (event) => {
+  const cur = document.getElementById('toolbar').querySelector(".active")
+  const activeViz = cur.id
+  const nextViz = event.id
+  cur.classList.toggle('active')
+  event.classList.toggle('active')
+
+  const curSection = document.getElementById(`${activeViz}-section`)
+  curSection.classList.toggle('active')
+  const nextSection = document.getElementById(`${nextViz}-section`)
+  nextSection.classList.toggle('active')
+  switchViz()
+}
+
+const bboxPolygon = (bounds) => {
+    return {
+        'type': 'Feature',
+        'geometry': {
+            'type': 'Polygon',
+            'coordinates': [[
+                [bounds[0], bounds[1]],
+                [bounds[2], bounds[1]],
+                [bounds[2], bounds[3]],
+                [bounds[0], bounds[3]],
+                [bounds[0], bounds[1]]
+            ]]
+        },
+        'properties': {}
+    }
+}
+
+const addAOI = (bounds) => {
+  const geojson = {
+      "type": "FeatureCollection",
+      "features": [bboxPolygon(bounds)]
+  }
+
+  map.addSource('aoi', {
+    'type': 'geojson',
+    'data': geojson
+  })
+
+  map.addLayer({
+    id: 'aoi-polygon',
+    type: 'line',
+    source: 'aoi',
+    layout: {
+      'line-cap': 'round',
+      'line-join': 'round'
+    },
+    paint: {
+      'line-color': '#3bb2d0',
+      'line-width': 1
+    }
+  })
+  return
+}
+
+const truncate_lnglat = (lng, lat) => {
+  if (lng > 180.0) {
+    lng = 180.0
+  } else if (lng < -180.0) {
+    lng = -180.0
+  }
+  if (lat > 90.0) {
+    lat = 90.0
+  } else if (lat < -90.0) {
+    lat = -90.0
+  }
+  return [lng, lat]
+}
+
+map.on('load', () => {
+  map.on('mousemove', (e) => {
+    if (!map.getLayer('mvt')) return
+    const mouseRadius = 1
+    const feature = map.queryRenderedFeatures([
+      [e.point.x - mouseRadius, e.point.y - mouseRadius],
+      [e.point.x + mouseRadius, e.point.y + mouseRadius]
+    ], { layers: ['mvt'] })[0]
+    if (feature) {
+      map.getCanvas().style.cursor = 'pointer'
+    } else {
+      map.getCanvas().style.cursor = 'inherit'
+    }
+  })
+
+  map.on('click', 'mvt', (e) => {
+    let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
+    let values = Object.keys(e.features[0].properties)
+    for (var i = 0; i < values.length; i++) {
+      let value = e.features[0].properties[values[i]]
+      let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
+      html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
+    }
 
-      fetch(`${stats_endpoint}?max_size=256`)
+    html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
+    html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
+    html += '</table>'
+    new mapboxgl.Popup()
+      .setLngLat(e.lngLat)
+      .setHTML(html)
+      .addTo(map)
+  })
+
+  // we cannot click on raster layer (mapbox-gl bug)
+  map.on('click', (e) => {
+    if (!map.getLayer('raster')) return
+    const bounds = map.getSource('raster').bounds
+    if (
+      (e.lngLat.lng >= bounds[0] && e.lngLat.lng <= bounds[2]) &&
+      (e.lngLat.lat >= bounds[1] && e.lngLat.lat <= bounds[3])
+    ) {
+      const coord = `${e.lngLat.lng},${e.lngLat.lat}`
+      fetch(`${point_endpoint}?coordinates=${coord}`)
         .then(res => {
           if (res.ok) return res.json()
-          throw new Error('Network response was not ok.')
+          throw new Error('Network response was not ok.');
         })
         .then(data => {
-          scope.dataset_statistics = data
-          if (document.getElementById('toolbar').querySelector(".active").id === '1b') {
-            addHisto1Band()
-          } else {
-            addHisto3Bands()
+          let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
+          let values = Object.keys(data.value)
+          for (var i = 0; i < values.length; i++) {
+            let value = data.value[values[i]]
+            let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
+            html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
           }
-          document.getElementById('histogram').classList.remove('loading')
+          html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
+          html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
+          html += '</table>'
+          new mapboxgl.Popup()
+            .setLngLat(e.lngLat)
+            .setHTML(html)
+            .addTo(map)
         })
         .catch(err => {
           console.warn(err)
         })
-    })
-
-    document.getElementById('btn-hide').addEventListener('click', () => {
-      document.getElementById('hide-arrow').classList.toggle('off')
-      document.getElementById('menu').classList.toggle('off')
-    })
-
-    document.getElementById('viz-selector').addEventListener('change', (e) => {
-      switch (e.target.value) {
-        case 'raster':
-          document.getElementById('colormap-section').classList.remove('none')
-          document.getElementById('extrusion-section').classList.add('none')
-          break
-
-        case 'point':
-          document.getElementById('colormap-section').classList.add('none')
-          document.getElementById('extrusion-section').classList.add('none')
-          break
-
-        case 'polygon':
-          document.getElementById('colormap-section').classList.add('none')
-          document.getElementById('extrusion-section').classList.remove('none')
-          break
-
-        default:
-      }
-      switchViz()
-    })
-
-    // MVT have already all the layers while for raster we need to fetch new tiles
-    const updateViz = () => {
-      const newViz = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
-      if (newViz === "raster") {
-        switchViz()
-      } else {
-        addLayer(newViz)
-      }
     }
+  })
 
-    document.getElementById('layer-selector').addEventListener('change', () => {
-      const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
-      updateViz()
+  fetch(`${info_endpoint}`)
+    .then(res => {
+      if (res.ok) return res.json()
+      throw new Error('Network response was not ok.')
     })
-
-    document.getElementById('r-selector').addEventListener('change', () => {switchViz()})
-    document.getElementById('g-selector').addEventListener('change', () => {switchViz()})
-    document.getElementById('b-selector').addEventListener('change', () => {switchViz()})
-
-    document.getElementById('updateExag').addEventListener('click', () => {
-      updateViz()
-    })
-
-    document.getElementById('colormap-selector').addEventListener('change', () => {
-      updateViz()
-    })
-
-    document.getElementById('btn-rescale').addEventListener('click', () => {
-      updateViz()
-    })
-
-    const switchPane = (event) => {
-      const cur = document.getElementById('toolbar').querySelector(".active")
-      const activeViz = cur.id
-      const nextViz = event.id
-      cur.classList.toggle('active')
-      event.classList.toggle('active')
-
-      const curSection = document.getElementById(`${activeViz}-section`)
-      curSection.classList.toggle('active')
-      const nextSection = document.getElementById(`${nextViz}-section`)
-      nextSection.classList.toggle('active')
-      switchViz()
-    }
-
-    const bboxPolygon = (bounds) => {
-        return {
-            'type': 'Feature',
-            'geometry': {
-                'type': 'Polygon',
-                'coordinates': [[
-                    [bounds[0], bounds[1]],
-                    [bounds[2], bounds[1]],
-                    [bounds[2], bounds[3]],
-                    [bounds[0], bounds[3]],
-                    [bounds[0], bounds[1]]
-                ]]
-            },
-            'properties': {}
-        }
-    }
-
-    const addAOI = (bounds) => {
-      const geojson = {
-          "type": "FeatureCollection",
-          "features": [bboxPolygon(bounds)]
+    .then(data => {
+      console.log(data)
+      scope.data_type = data.dtype
+
+      if (['uint8','int8'].indexOf(scope.data_type) === -1) document.getElementById('minmax-data').classList.remove('none')
+
+      const mm = dtype_ranges[scope.data_type]
+      document.getElementById('data-min').value = mm[0]
+      document.getElementById('data-max').value = mm[1]
+
+      scope.band_descriptions = data.band_descriptions
+      const band_descr = data.band_descriptions
+      const nbands = band_descr.length
+
+      //Populate Band (1b) selector
+      const layerList = document.getElementById('layer-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        layerList.appendChild(l)
       }
 
-      map.addSource('aoi', {
-        'type': 'geojson',
-        'data': geojson
-      })
-
-      map.addLayer({
-        id: 'aoi-polygon',
-        type: 'line',
-        source: 'aoi',
-        layout: {
-          'line-cap': 'round',
-          'line-join': 'round'
-        },
-        paint: {
-          'line-color': '#3bb2d0',
-          'line-width': 1
-        }
-      })
-      return
-    }
-
-    const truncate_lnglat = (lng, lat) => {
-      if (lng > 180.0) {
-        lng = 180.0
-      } else if (lng < -180.0) {
-        lng = -180.0
+      // Populate R/G/B (3b) selectors
+      const rList = document.getElementById('r-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (i === 0) l.selected="selected"
+        rList.appendChild(l)
       }
-      if (lat > 90.0) {
-        lat = 90.0
-      } else if (lat < -90.0) {
-        lat = -90.0
+
+      const gList = document.getElementById('g-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (i === 1) l.selected="selected"
+        gList.appendChild(l)
       }
-      return [lng, lat]
-    }
 
-    map.on('load', () => {
-      map.on('mousemove', (e) => {
-        if (!map.getLayer('mvt')) return
-        const mouseRadius = 1
-        const feature = map.queryRenderedFeatures([
-          [e.point.x - mouseRadius, e.point.y - mouseRadius],
-          [e.point.x + mouseRadius, e.point.y + mouseRadius]
-        ], { layers: ['mvt'] })[0]
-        if (feature) {
-          map.getCanvas().style.cursor = 'pointer'
+      const bList = document.getElementById('b-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (band_descr.length > 2 && i === 2) {
+          l.selected="selected"
         } else {
-          map.getCanvas().style.cursor = 'inherit'
+          l.selected="selected"
         }
-      })
-
-      map.on('click', 'mvt', (e) => {
-        let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
-        let values = Object.keys(e.features[0].properties)
-        for (var i = 0; i < values.length; i++) {
-          let value = e.features[0].properties[values[i]]
-          let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
-          html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
-        }
-
-        html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
-        html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
-        html += '</table>'
-        new mapboxgl.Popup()
-          .setLngLat(e.lngLat)
-          .setHTML(html)
-          .addTo(map)
-      })
-
-      // we cannot click on raster layer (mapbox-gl bug)
-      map.on('click', (e) => {
-        if (!map.getLayer('raster')) return
-        const bounds = map.getSource('raster').bounds
-        if (
-          (e.lngLat.lng >= bounds[0] && e.lngLat.lng <= bounds[2]) &&
-          (e.lngLat.lat >= bounds[1] && e.lngLat.lat <= bounds[3])
-        ) {
-          const coord = `${e.lngLat.lng},${e.lngLat.lat}`
-          fetch(`${point_endpoint}?coordinates=${coord}`)
-            .then(res => {
-              if (res.ok) return res.json()
-              throw new Error('Network response was not ok.');
-            })
-            .then(data => {
-              let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
-              let values = Object.keys(data.value)
-              for (var i = 0; i < values.length; i++) {
-                let value = data.value[values[i]]
-                let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
-                html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
-              }
-              html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
-              html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
-              html += '</table>'
-              new mapboxgl.Popup()
-                .setLngLat(e.lngLat)
-                .setHTML(html)
-                .addTo(map)
-            })
-            .catch(err => {
-              console.warn(err)
-            })
-        }
-      })
-
-      fetch(`${info_endpoint}`)
-        .then(res => {
-          if (res.ok) return res.json()
-          throw new Error('Network response was not ok.')
-        })
-        .then(data => {
-          console.log(data)
-          scope.data_type = data.dtype
-
-          if (['uint8','int8'].indexOf(scope.data_type) === -1) document.getElementById('minmax-data').classList.remove('none')
-
-          const mm = dtype_ranges[scope.data_type]
-          document.getElementById('data-min').value = mm[0]
-          document.getElementById('data-max').value = mm[1]
-
-          scope.band_descriptions = data.band_descriptions
-          const band_descr = data.band_descriptions
-          const nbands = band_descr.length
-
-          //Populate Band (1b) selector
-          const layerList = document.getElementById('layer-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            layerList.appendChild(l)
-          }
-
-          // Populate R/G/B (3b) selectors
-          const rList = document.getElementById('r-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (i === 0) l.selected="selected"
-            rList.appendChild(l)
-          }
-
-          const gList = document.getElementById('g-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (i === 1) l.selected="selected"
-            gList.appendChild(l)
-          }
-
-          const bList = document.getElementById('b-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (band_descr.length > 2 && i === 2) {
-              l.selected="selected"
-            } else {
-              l.selected="selected"
-            }
-            bList.appendChild(l)
-          }
+        bList.appendChild(l)
+      }
 
-          // remove loader
-          document.getElementById('loader').classList.toggle('off')
-          document.getElementById('hide-arrow').classList.toggle('off')
-          document.getElementById('menu').classList.toggle('off')
+      // remove loader
+      document.getElementById('loader').classList.toggle('off')
+      document.getElementById('hide-arrow').classList.toggle('off')
+      document.getElementById('menu').classList.toggle('off')
 
-          const ll = truncate_lnglat(data.bounds[0], data.bounds[1])
-          const ur = truncate_lnglat(data.bounds[2], data.bounds[3])
-          map.fitBounds([ll, ur])
+      const ll = truncate_lnglat(data.bounds[0], data.bounds[1])
+      const ur = truncate_lnglat(data.bounds[2], data.bounds[3])
+      map.fitBounds([ll, ur])
 
-          addAOI(data.bounds)
+      addAOI(data.bounds)
 
-          if (nbands === 1) {
-            document.getElementById('3b').classList.add('disabled')
-            document.getElementById('3b').classList.remove('active')
-            document.getElementById('3b-section').classList.toggle('active')
-            document.getElementById('1b').classList.add('active')
-            document.getElementById('1b-section').classList.toggle('active')
-          }
-          switchViz()
-        })
-        .catch(err => {
-          console.warn(err)
-        })
+      if (nbands === 1) {
+        document.getElementById('3b').classList.add('disabled')
+        document.getElementById('3b').classList.remove('active')
+        document.getElementById('3b-section').classList.toggle('active')
+        document.getElementById('1b').classList.add('active')
+        document.getElementById('1b-section').classList.toggle('active')
+      }
+      switchViz()
+    })
+    .catch(err => {
+      console.warn(err)
     })
+})
 
     </script>
 
diff --git a/rio_viz/templates/index.html b/rio_viz/templates/index.html
index f1e4325..9fe574f 100644
--- a/rio_viz/templates/index.html
+++ b/rio_viz/templates/index.html
@@ -311,737 +311,753 @@
     </div>
 
     <script>
-    var scope = {
-      dataset_statistics: undefined,
-      data_type: undefined,
-      colormap: undefined,
-      band_descriptions: undefined
-    }
-
-    mapboxgl.accessToken = '{{ mapbox_access_token  }}'
-
-    const tilejson_endpoint = '{{ tilejson_endpoint }}'
-    const info_endpoint = '{{ info_endpoint }}'
-    const stats_endpoint = '{{ stats_endpoint }}'
-    const point_endpoint = '{{ point_endpoint }}'
-
-    const dtype_ranges = {
-      'int8': [-128, 127],
-      'uint8': [0, 255],
-      'uint16': [0, 65535],
-      'int16': [-32768, 32767],
-      'uint32': [0, 4294967295],
-      'int32': [-2147483648, 2147483647],
-      'float32': [-3.4028235e+38, 3.4028235e+38],
-      'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
-    }
-
-    if ('{{ allow_3d }}' !== "True") {
-      document.getElementById('3d-point').classList.add('none')
-      document.getElementById('3d-poly').classList.add('none')
-    }
-
-    let style
-    if (mapboxgl.accessToken !== '') {
-      style = 'mapbox://styles/mapbox/{{ mapbox_style }}-v9'
-    } else {
-      style = { version: 8, sources: {}, layers: [] }
-    }
-
-    var map = new mapboxgl.Map({
-        container: 'map',
-        style: style,
-        center: [0, 0],
-        zoom: 1
-    })
-
-    map.on('zoom', function (e) {
-      const z = (map.getZoom()).toString().slice(0, 6)
-      document.getElementById('zoom').textContent = z
-    })
-
-    const set1bViz = () => {
-      params = {}
-      const vizType = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
-      switch (vizType) {
-        case 'raster':
-          params.bidx = document.getElementById('layer-selector').selectedOptions[0].getAttribute("band")
-
-          if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) {
-            params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
-          }
-
-          const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex]
-          if (cmap.value !== 'b&w') params.colormap_name = cmap.value
-
-          const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
-          let url = `${tilejson_endpoint}?${url_params}`
-
-          map.addSource('raster', { type: 'raster', url: url , tileSize: 256})
-          addLayer(vizType)
-          break
-
-        case 'point':
-          map.addSource('mvt', {
-            type: 'vector',
-            url: `${tilejson_endpoint}?tile_format=pbf&feature_type=point`
-          })
-          addLayer(vizType)
-          break
-
-        case 'polygon':
-          map.addSource('mvt', {
-            type: 'vector',
-            url: `${tilejson_endpoint}?tile_format=pbf&feature_type=polygon`
-          })
-          addLayer(vizType)
-          break
-
-        default:
-          throw new Error(`Invalid ${vizType}`)
+var scope = {
+  dataset_statistics: undefined,
+  data_type: undefined,
+  colormap: undefined,
+  band_descriptions: undefined
+}
+
+mapboxgl.accessToken = ''
+
+const tilejson_endpoint = '{{ tilejson_endpoint }}'
+const info_endpoint = '{{ info_endpoint }}'
+const stats_endpoint = '{{ stats_endpoint }}'
+const point_endpoint = '{{ point_endpoint }}'
+
+const dtype_ranges = {
+  'int8': [-128, 127],
+  'uint8': [0, 255],
+  'uint16': [0, 65535],
+  'int16': [-32768, 32767],
+  'uint32': [0, 4294967295],
+  'int32': [-2147483648, 2147483647],
+  'float32': [-3.4028235e+38, 3.4028235e+38],
+  'float64': [-1.7976931348623157e+308, 1.7976931348623157e+308]
+}
+
+if ('{{ allow_3d }}' !== "True") {
+  document.getElementById('3d-point').classList.add('none')
+  document.getElementById('3d-poly').classList.add('none')
+}
+
+var map = new mapboxgl.Map({
+  container: 'map',
+  style: {
+    version: 8,
+    sources: {
+      'toner-lite': {
+        type: 'raster',
+        tiles: [
+          'https://stamen-tiles-a.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-b.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png',
+          'https://stamen-tiles-d.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png'
+        ],
+        tileSize: 256,
+        attribution:
+          'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
       }
-    }
-
-    const set3bViz = () => {
-      const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("band")
-      const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("band")
-      const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("band")
+    },
+    layers: [
+      {
+        'id': 'basemap',
+        'type': 'raster',
+        'source': 'toner-lite',
+        'minzoom': 0,
+        'maxzoom': 20
+      }
+    ]
+  },
+  center: [0, 0],
+  zoom: 1
+})
+
+map.on('zoom', function (e) {
+  const z = (map.getZoom()).toString().slice(0, 6)
+  document.getElementById('zoom').textContent = z
+})
+
+const set1bViz = () => {
+  params = {}
+  const vizType = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
+  switch (vizType) {
+    case 'raster':
+      params.bidx = document.getElementById('layer-selector').selectedOptions[0].getAttribute("band")
 
-      params = {}
       if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) {
         params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
       }
-      let url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
-      if (url_params !== '') url_params = `${url_params}&`
 
+      const cmap = document.getElementById('colormap-selector')[document.getElementById('colormap-selector').selectedIndex]
+      if (cmap.value !== 'b&w') params.colormap_name = cmap.value
 
-      const indexes_params = [r, g, b].map(i => `bidx=${i}`).join('&')
-      let url = `${tilejson_endpoint}?${url_params}${indexes_params}`
+      const url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
+      let url = `${tilejson_endpoint}?${url_params}`
 
-      map.addSource('raster', { type: 'raster', url: url, tileSize: 256})
-      map.addLayer({id: 'raster', type: 'raster', source: 'raster'})
-      if (scope.dataset_statistics) addHisto3Bands()
-    }
+      map.addSource('raster', { type: 'raster', url: url , tileSize: 256})
+      addLayer(vizType)
+      break
 
-    const switchViz = () => {
-      if (map.getLayer('raster')) map.removeLayer('raster')
-      if (map.getSource('raster')) map.removeSource('raster')
-
-      if (map.getLayer('mvt')) map.removeLayer('mvt')
-      if (map.getSource('mvt')) { map.removeSource('mvt') }
-
-      const rasterType = document.getElementById('toolbar').querySelector(".active").id
-      switch (rasterType) {
-        case '1b':
-          set1bViz()
-          break
-        case '3b':
-          set3bViz()
-          break
-        default:
-          throw new Error(`Invalid ${rasterType}`)
-      }
-    }
+    case 'point':
+      map.addSource('mvt', {
+        type: 'vector',
+        url: `${tilejson_endpoint}?tile_format=pbf&feature_type=point`
+      })
+      addLayer(vizType)
+      break
 
-    const addLayer = (layerType) => {
-      if (map.getLayer('raster')) map.removeLayer('raster')
-      if (map.getLayer('mvt')) map.removeLayer('mvt')
-
-      let sMin = parseFloat(document.getElementById('data-min').value)
-      const sMax = parseFloat(document.getElementById('data-max').value)
-      if (sMin === sMax) sMin = sMin - 0.00001
-
-      if (scope.dataset_statistics) addHisto1Band()
-
-      const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
-      const propName = active_layer.value
-      const exag = parseFloat(document.getElementById('ex-value').value)
-
-      switch (layerType) {
-        case 'raster':
-          map.addLayer({
-            id: 'raster',
-            type: 'raster',
-            source: 'raster'
-          })
-          break
-
-        case 'point':
-          map.addLayer({
-            id: 'mvt',
-            source: 'mvt',
-            'source-layer': 'my_layer',
-            type: 'circle',
-            paint: {
-              'circle-color': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, '#3700f0',
-                sMax, '#ed0707'
-              ],
-              'circle-radius': {
-                'base': 1,
-                'stops': [
-                  [0, 10],
-                  [9, 5]
-                ]
-              }
-            }
-          })
-
-          break
-
-        case 'polygon':
-          map.addLayer({
-            id: 'mvt',
-            source: 'mvt',
-            'source-layer': 'my_layer',
-            type: 'fill-extrusion',
-            paint: {
-              'fill-extrusion-opacity': 1,
-              'fill-extrusion-height': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, 0,
-                sMax, sMax * exag
-              ],
-              'fill-extrusion-color': [
-                'interpolate',
-                ['linear'],
-                ['to-number', ['get', propName]],
-                sMin, '#3700f0',
-                sMax, '#ed0707'
-              ]
-            }
-          })
+    case 'polygon':
+      map.addSource('mvt', {
+        type: 'vector',
+        url: `${tilejson_endpoint}?tile_format=pbf&feature_type=polygon`
+      })
+      addLayer(vizType)
+      break
+
+    default:
+      throw new Error(`Invalid ${vizType}`)
+  }
+}
+
+const set3bViz = () => {
+  const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("band")
+  const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("band")
+  const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("band")
+
+  params = {}
+  if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) {
+    params.rescale = `${document.getElementById('data-min').value},${document.getElementById('data-max').value}`
+  }
+  let url_params = Object.keys(params).map(i => `${i}=${params[i]}`).join('&')
+  if (url_params !== '') url_params = `${url_params}&`
+
+
+  const indexes_params = [r, g, b].map(i => `bidx=${i}`).join('&')
+  let url = `${tilejson_endpoint}?${url_params}${indexes_params}`
+
+  map.addSource('raster', { type: 'raster', url: url, tileSize: 256})
+  map.addLayer({id: 'raster', type: 'raster', source: 'raster'})
+  if (scope.dataset_statistics) addHisto3Bands()
+}
+
+const switchViz = () => {
+  if (map.getLayer('raster')) map.removeLayer('raster')
+  if (map.getSource('raster')) map.removeSource('raster')
+
+  if (map.getLayer('mvt')) map.removeLayer('mvt')
+  if (map.getSource('mvt')) { map.removeSource('mvt') }
+
+  const rasterType = document.getElementById('toolbar').querySelector(".active").id
+  switch (rasterType) {
+    case '1b':
+      set1bViz()
+      break
+    case '3b':
+      set3bViz()
+      break
+    default:
+      throw new Error(`Invalid ${rasterType}`)
+  }
+}
+
+const addLayer = (layerType) => {
+  if (map.getLayer('raster')) map.removeLayer('raster')
+  if (map.getLayer('mvt')) map.removeLayer('mvt')
+
+  let sMin = parseFloat(document.getElementById('data-min').value)
+  const sMax = parseFloat(document.getElementById('data-max').value)
+  if (sMin === sMax) sMin = sMin - 0.00001
+
+  if (scope.dataset_statistics) addHisto1Band()
+
+  const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
+  const propName = active_layer.value
+  const exag = parseFloat(document.getElementById('ex-value').value)
+
+  switch (layerType) {
+    case 'raster':
+      map.addLayer({
+        id: 'raster',
+        type: 'raster',
+        source: 'raster'
+      })
+      break
 
-          break
+    case 'point':
+      map.addLayer({
+        id: 'mvt',
+        source: 'mvt',
+        'source-layer': 'my_layer',
+        type: 'circle',
+        paint: {
+          'circle-color': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, '#3700f0',
+            sMax, '#ed0707'
+          ],
+          'circle-radius': {
+            'base': 1,
+            'stops': [
+              [0, 10],
+              [9, 5]
+            ]
+          }
+        }
+      })
 
-        default:
-          throw new Error(`Invalid ${layerType}`)
-      }
-    }
+      break
 
-    const addHisto3Bands = () => {
-      const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("name")
-      const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("name")
-      const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("name")
-
-      const rStats = scope.dataset_statistics[r]
-      const gStats = scope.dataset_statistics[g]
-      const bStats = scope.dataset_statistics[b]
-
-      const minV = Math.min(...[rStats.min, gStats.min, bStats.min])
-      const maxV = Math.max(...[rStats.max, gStats.max, bStats.max])
-
-      let rCounts = rStats.histogram[0]
-      let gCounts = gStats.histogram[0]
-      let bCounts = bStats.histogram[0]
-
-      const rValues = rStats.histogram[1]
-      const gValues = gStats.histogram[1]
-      const bValues = bStats.histogram[1]
-
-      const add = (a, b) => a + b
-
-      const sumR = rCounts.reduce(add)
-      const sumG = gCounts.reduce(add)
-      const sumB = bCounts.reduce(add)
-
-      rCounts = rCounts.map((e) => {return e / sumR * 100})
-      gCounts = gCounts.map((e) => {return e / sumG * 100})
-      bCounts = bCounts.map((e) => {return e / sumB * 100})
-      const maxH = Math.max(...rCounts, ...gCounts, ...bCounts)
-
-      const bbox = d3.select('#histogram').node().getBoundingClientRect()
-
-      // set the dimensions and margins of the graph
-      const margin = { top: 10, right: 30, bottom: 30, left: 40 }
-      const width = bbox.width - margin.left - margin.right
-      const height = bbox.height - margin.top - margin.bottom
-
-      d3.select('#histogram').selectAll('*').remove()
-      // append the svg object to the body of the page
-      var svg = d3.select('#histogram')
-        .append('svg')
-        .attr('width', width + margin.left + margin.right)
-        .attr('height', height + margin.top + margin.bottom)
-        .append('g')
-        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
-
-      // X axis: scale and draw:
-      var x = d3.scaleLinear()
-        .domain([minV, maxV])
-        .range([0, width])
-
-      svg.append('g')
-        .attr('transform', 'translate(0,' + height + ')')
-        .call(d3.axisBottom(x))
-
-      // Y axis: scale and draw:
-      var y = d3.scaleLinear().range([height, 0])
-      y.domain([0, maxH + 5])
-      svg.append('g').call(d3.axisLeft(y))
-
-      const addLine = (counts, values, color) => {
-        const data = []
-        for (var i = 0; i < counts.length; i++) {
-          data.push({count: counts[i], value: values[i]})
+    case 'polygon':
+      map.addLayer({
+        id: 'mvt',
+        source: 'mvt',
+        'source-layer': 'my_layer',
+        type: 'fill-extrusion',
+        paint: {
+          'fill-extrusion-opacity': 1,
+          'fill-extrusion-height': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, 0,
+            sMax, sMax * exag
+          ],
+          'fill-extrusion-color': [
+            'interpolate',
+            ['linear'],
+            ['to-number', ['get', propName]],
+            sMin, '#3700f0',
+            sMax, '#ed0707'
+          ]
         }
+      })
 
-        var guide = d3.line()
-                      .x(function(d){ return x(d.value) })
-                      .y(function(d){ return y(d.count) });
+      break
+
+    default:
+      throw new Error(`Invalid ${layerType}`)
+  }
+}
+
+const addHisto3Bands = () => {
+  const r = document.getElementById('r-selector').selectedOptions[0].getAttribute("name")
+  const g = document.getElementById('g-selector').selectedOptions[0].getAttribute("name")
+  const b = document.getElementById('b-selector').selectedOptions[0].getAttribute("name")
+
+  const rStats = scope.dataset_statistics[r]
+  const gStats = scope.dataset_statistics[g]
+  const bStats = scope.dataset_statistics[b]
+
+  const minV = Math.min(...[rStats.min, gStats.min, bStats.min])
+  const maxV = Math.max(...[rStats.max, gStats.max, bStats.max])
+
+  let rCounts = rStats.histogram[0]
+  let gCounts = gStats.histogram[0]
+  let bCounts = bStats.histogram[0]
+
+  const rValues = rStats.histogram[1]
+  const gValues = gStats.histogram[1]
+  const bValues = bStats.histogram[1]
+
+  const add = (a, b) => a + b
+
+  const sumR = rCounts.reduce(add)
+  const sumG = gCounts.reduce(add)
+  const sumB = bCounts.reduce(add)
+
+  rCounts = rCounts.map((e) => {return e / sumR * 100})
+  gCounts = gCounts.map((e) => {return e / sumG * 100})
+  bCounts = bCounts.map((e) => {return e / sumB * 100})
+  const maxH = Math.max(...rCounts, ...gCounts, ...bCounts)
+
+  const bbox = d3.select('#histogram').node().getBoundingClientRect()
+
+  // set the dimensions and margins of the graph
+  const margin = { top: 10, right: 30, bottom: 30, left: 40 }
+  const width = bbox.width - margin.left - margin.right
+  const height = bbox.height - margin.top - margin.bottom
+
+  d3.select('#histogram').selectAll('*').remove()
+  // append the svg object to the body of the page
+  var svg = d3.select('#histogram')
+    .append('svg')
+    .attr('width', width + margin.left + margin.right)
+    .attr('height', height + margin.top + margin.bottom)
+    .append('g')
+    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+  // X axis: scale and draw:
+  var x = d3.scaleLinear()
+    .domain([minV, maxV])
+    .range([0, width])
+
+  svg.append('g')
+    .attr('transform', 'translate(0,' + height + ')')
+    .call(d3.axisBottom(x))
+
+  // Y axis: scale and draw:
+  var y = d3.scaleLinear().range([height, 0])
+  y.domain([0, maxH + 5])
+  svg.append('g').call(d3.axisLeft(y))
+
+  const addLine = (counts, values, color) => {
+    const data = []
+    for (var i = 0; i < counts.length; i++) {
+      data.push({count: counts[i], value: values[i]})
+    }
 
-        var line = svg.append('path')
-                      .datum(data)
-                      .attr('d', guide)
-                      .attr('class', `line-${color}`);
-      }
-      addLine(rCounts, rValues, "red")
-      addLine(gCounts, gValues, "green")
-      addLine(bCounts, bValues, "blue")
-
-      //Draw axes
-      svg.append("g")
-          .attr("class", "x axis")
-          .attr("transform", "translate(0," + maxH + 5 + ")")
-          .call(x);
-
-      svg.append("g")
-          .attr("class", "y axis")
-          .call(y)
-          .append("text")
-          .attr("transform", "rotate(-90)")
-          .attr("y", 6)
-          .attr("dy", ".71em")
-          .style("text-anchor", "end")
-
-      const data = {
-        "Min": `${rStats.min}, ${gStats.min}, ${bStats.min}`,
-        "Max": `${rStats.max}, ${gStats.max}, ${bStats.max}`,
-        "Stddev": `${rStats.std.toFixed(2)}, ${gStats.std.toFixed(2)}, ${bStats.std.toFixed(2)}`,
-      }
+    var guide = d3.line()
+                  .x(function(d){ return x(d.value) })
+                  .y(function(d){ return y(d.count) });
+
+    var line = svg.append('path')
+                  .datum(data)
+                  .attr('d', guide)
+                  .attr('class', `line-${color}`);
+  }
+  addLine(rCounts, rValues, "red")
+  addLine(gCounts, gValues, "green")
+  addLine(bCounts, bValues, "blue")
+
+  //Draw axes
+  svg.append("g")
+      .attr("class", "x axis")
+      .attr("transform", "translate(0," + maxH + 5 + ")")
+      .call(x);
+
+  svg.append("g")
+      .attr("class", "y axis")
+      .call(y)
+      .append("text")
+      .attr("transform", "rotate(-90)")
+      .attr("y", 6)
+      .attr("dy", ".71em")
+      .style("text-anchor", "end")
+
+  const data = {
+    "Min": `${rStats.min}, ${gStats.min}, ${bStats.min}`,
+    "Max": `${rStats.max}, ${gStats.max}, ${bStats.max}`,
+    "Stddev": `${rStats.std.toFixed(2)}, ${gStats.std.toFixed(2)}, ${bStats.std.toFixed(2)}`,
+  }
+
+  let table = document.getElementById("histogram-table")
+  table.innerHTML = ""
+
+  for (let element of Object.entries(data)) {
+    let row = table.insertRow();
+    for (key in element) {
+      let cell = row.insertCell();
+      let text = document.createTextNode(element[key]);
+      cell.appendChild(text);
+    }
+  }
+  table.classList.remove('none')
+}
+
+const addHisto1Band = () => {
+  const stats = scope.dataset_statistics[document.getElementById('layer-selector').selectedOptions[0].getAttribute("name")]
+
+  let counts = stats.histogram[0]
+  const sum = counts.reduce(function(a, b){
+      return a + b;
+  }, 0);
+  counts = counts.map((e) => {return e / sum * 100})
+  const maxH = Math.max(...counts)
+
+  const values = stats.histogram[1]
+  const bbox = d3.select('#histogram').node().getBoundingClientRect()
+
+  // set the dimensions and margins of the graph
+  const margin = { top: 10, right: 30, bottom: 30, left: 40 }
+  const width = bbox.width - margin.left - margin.right
+  const height = bbox.height - margin.top - margin.bottom
+
+  d3.select('#histogram').selectAll('*').remove()
+  // append the svg object to the body of the page
+  var svg = d3.select('#histogram')
+    .append('svg')
+    .attr('width', width + margin.left + margin.right)
+    .attr('height', height + margin.top + margin.bottom)
+    .append('g')
+    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+
+  const min = stats.min
+  const max = stats.max
+
+  // X axis: scale and draw:
+  var x = d3.scaleLinear()
+    .domain([min, max])
+    .range([0, width])
+
+  svg.append('g')
+    .attr('transform', 'translate(0,' + height + ')')
+    .call(d3.axisBottom(x))
+
+  // Y axis: scale and draw:
+  var y = d3.scaleLinear().range([height, 0])
+  y.domain([0, maxH])
+  svg.append('g').call(d3.axisLeft(y))
+
+  const bins = []
+  for (var i = 0; i < counts.length; i++) {
+    bins.push({
+      count: counts[i],
+      value: values[i]
+    })
+  }
+
+  // append the bar rectangles to the svg element
+  svg.selectAll('rect')
+    .data(bins)
+    .enter()
+    .append('rect')
+    .attr('x', 1)
+    .attr('transform', d => { return 'translate(' + x(d.value) + ',' + y(d.count) + ')' })
+    .attr('width', 10)
+    .attr('height', d => { return height - y(d.count) })
+    .style('fill', '#69b3a2')
+
+    const data = {
+      "Min": stats.min,
+      "Max": stats.max,
+      "Stddev": stats.std.toFixed(2),
+    }
 
-      let table = document.getElementById("histogram-table")
-      table.innerHTML = ""
+    let table = document.getElementById("histogram-table")
+    table.innerHTML = ""
 
-      for (let element of Object.entries(data)) {
-        let row = table.insertRow();
-        for (key in element) {
-          let cell = row.insertCell();
-          let text = document.createTextNode(element[key]);
-          cell.appendChild(text);
-        }
+    for (let element of Object.entries(data)) {
+      let row = table.insertRow();
+      for (key in element) {
+        let cell = row.insertCell();
+        let text = document.createTextNode(element[key]);
+        cell.appendChild(text);
       }
-      table.classList.remove('none')
     }
-
-    const addHisto1Band = () => {
-      const stats = scope.dataset_statistics[document.getElementById('layer-selector').selectedOptions[0].getAttribute("name")]
-
-      let counts = stats.histogram[0]
-      const sum = counts.reduce(function(a, b){
-          return a + b;
-      }, 0);
-      counts = counts.map((e) => {return e / sum * 100})
-      const maxH = Math.max(...counts)
-
-      const values = stats.histogram[1]
-      const bbox = d3.select('#histogram').node().getBoundingClientRect()
-
-      // set the dimensions and margins of the graph
-      const margin = { top: 10, right: 30, bottom: 30, left: 40 }
-      const width = bbox.width - margin.left - margin.right
-      const height = bbox.height - margin.top - margin.bottom
-
-      d3.select('#histogram').selectAll('*').remove()
-      // append the svg object to the body of the page
-      var svg = d3.select('#histogram')
-        .append('svg')
-        .attr('width', width + margin.left + margin.right)
-        .attr('height', height + margin.top + margin.bottom)
-        .append('g')
-        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
-
-      const min = stats.min
-      const max = stats.max
-
-      // X axis: scale and draw:
-      var x = d3.scaleLinear()
-        .domain([min, max])
-        .range([0, width])
-
-      svg.append('g')
-        .attr('transform', 'translate(0,' + height + ')')
-        .call(d3.axisBottom(x))
-
-      // Y axis: scale and draw:
-      var y = d3.scaleLinear().range([height, 0])
-      y.domain([0, maxH])
-      svg.append('g').call(d3.axisLeft(y))
-
-      const bins = []
-      for (var i = 0; i < counts.length; i++) {
-        bins.push({
-          count: counts[i],
-          value: values[i]
-        })
+    table.classList.remove('none')
+}
+
+document.getElementById('btn-stats').addEventListener('click', () => {
+  document.getElementById('fetch-stats-div').classList.add('none')
+  document.getElementById('histogram').classList.remove('none')
+  document.getElementById('histogram-table').classList.remove('none')
+
+  fetch(`${stats_endpoint}?max_size=256`)
+    .then(res => {
+      if (res.ok) return res.json()
+      throw new Error('Network response was not ok.')
+    })
+    .then(data => {
+      scope.dataset_statistics = data
+      if (document.getElementById('toolbar').querySelector(".active").id === '1b') {
+        addHisto1Band()
+      } else {
+        addHisto3Bands()
       }
-
-      // append the bar rectangles to the svg element
-      svg.selectAll('rect')
-        .data(bins)
-        .enter()
-        .append('rect')
-        .attr('x', 1)
-        .attr('transform', d => { return 'translate(' + x(d.value) + ',' + y(d.count) + ')' })
-        .attr('width', 10)
-        .attr('height', d => { return height - y(d.count) })
-        .style('fill', '#69b3a2')
-
-        const data = {
-          "Min": stats.min,
-          "Max": stats.max,
-          "Stddev": stats.std.toFixed(2),
-        }
-
-        let table = document.getElementById("histogram-table")
-        table.innerHTML = ""
-
-        for (let element of Object.entries(data)) {
-          let row = table.insertRow();
-          for (key in element) {
-            let cell = row.insertCell();
-            let text = document.createTextNode(element[key]);
-            cell.appendChild(text);
-          }
-        }
-        table.classList.remove('none')
+      document.getElementById('histogram').classList.remove('loading')
+    })
+    .catch(err => {
+      console.warn(err)
+    })
+  })
+
+document.getElementById('btn-hide').addEventListener('click', () => {
+  document.getElementById('hide-arrow').classList.toggle('off')
+  document.getElementById('menu').classList.toggle('off')
+})
+
+document.getElementById('viz-selector').addEventListener('change', (e) => {
+  switch (e.target.value) {
+    case 'raster':
+      document.getElementById('colormap-section').classList.remove('none')
+      document.getElementById('extrusion-section').classList.add('none')
+      break
+
+    case 'point':
+      document.getElementById('colormap-section').classList.add('none')
+      document.getElementById('extrusion-section').classList.add('none')
+      break
+
+    case 'polygon':
+      document.getElementById('colormap-section').classList.add('none')
+      document.getElementById('extrusion-section').classList.remove('none')
+      break
+
+    default:
+  }
+  switchViz()
+})
+
+// MVT have already all the layers while for raster we need to fetch new tiles
+const updateViz = () => {
+  const newViz = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
+  if (newViz === "raster") {
+    switchViz()
+  } else {
+    addLayer(newViz)
+  }
+}
+
+document.getElementById('layer-selector').addEventListener('change', () => {
+  const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
+  updateViz()
+})
+
+document.getElementById('r-selector').addEventListener('change', () => {switchViz()})
+document.getElementById('g-selector').addEventListener('change', () => {switchViz()})
+document.getElementById('b-selector').addEventListener('change', () => {switchViz()})
+
+document.getElementById('updateExag').addEventListener('click', () => {
+  updateViz()
+})
+
+document.getElementById('colormap-selector').addEventListener('change', () => {
+  updateViz()
+})
+
+document.getElementById('btn-rescale').addEventListener('click', () => {
+  updateViz()
+})
+
+const switchPane = (event) => {
+  const cur = document.getElementById('toolbar').querySelector(".active")
+  const activeViz = cur.id
+  const nextViz = event.id
+  cur.classList.toggle('active')
+  event.classList.toggle('active')
+
+  const curSection = document.getElementById(`${activeViz}-section`)
+  curSection.classList.toggle('active')
+  const nextSection = document.getElementById(`${nextViz}-section`)
+  nextSection.classList.toggle('active')
+  switchViz()
+}
+
+const bboxPolygon = (bounds) => {
+    return {
+        'type': 'Feature',
+        'geometry': {
+            'type': 'Polygon',
+            'coordinates': [[
+                [bounds[0], bounds[1]],
+                [bounds[2], bounds[1]],
+                [bounds[2], bounds[3]],
+                [bounds[0], bounds[3]],
+                [bounds[0], bounds[1]]
+            ]]
+        },
+        'properties': {}
+    }
+}
+
+const addAOI = (bounds) => {
+  if (map.getLayer('aoi-polygon')) map.removeLayer('aoi-polygon')
+  if (map.getSource('aoi')) map.removeSource('aoi')
+  const geojson = {
+      "type": "FeatureCollection",
+      "features": [bboxPolygon(bounds)]
+  }
+
+  map.addSource('aoi', {
+    'type': 'geojson',
+    'data': geojson
+  })
+
+  map.addLayer({
+    id: 'aoi-polygon',
+    type: 'line',
+    source: 'aoi',
+    layout: {
+      'line-cap': 'round',
+      'line-join': 'round'
+    },
+    paint: {
+      'line-color': '#3bb2d0',
+      'line-width': 1
+    }
+  })
+  return
+}
+
+const truncate_lnglat = (lng, lat) => {
+  if (lng > 180.0) {
+    lng = 180.0
+  } else if (lng < -180.0) {
+    lng = -180.0
+  }
+  if (lat > 90.0) {
+    lat = 90.0
+  } else if (lat < -90.0) {
+    lat = -90.0
+  }
+  return [lng, lat]
+}
+
+map.on('load', () => {
+  map.on('mousemove', (e) => {
+    if (!map.getLayer('mvt')) return
+    const mouseRadius = 1
+    const feature = map.queryRenderedFeatures([
+      [e.point.x - mouseRadius, e.point.y - mouseRadius],
+      [e.point.x + mouseRadius, e.point.y + mouseRadius]
+    ], { layers: ['mvt'] })[0]
+    if (feature) {
+      map.getCanvas().style.cursor = 'pointer'
+    } else {
+      map.getCanvas().style.cursor = 'inherit'
+    }
+  })
+
+  map.on('click', 'mvt', (e) => {
+    let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
+    let values = Object.keys(e.features[0].properties)
+    for (var i = 0; i < values.length; i++) {
+      let value = e.features[0].properties[values[i]]
+      let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
+      html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
     }
 
-    document.getElementById('btn-stats').addEventListener('click', () => {
-      document.getElementById('fetch-stats-div').classList.add('none')
-      document.getElementById('histogram').classList.remove('none')
-      document.getElementById('histogram-table').classList.remove('none')
-
-      fetch(`${stats_endpoint}?max_size=256`)
+    html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
+    html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
+    html += '</table>'
+    new mapboxgl.Popup()
+      .setLngLat(e.lngLat)
+      .setHTML(html)
+      .addTo(map)
+  })
+
+  // we cannot click on raster layer (mapbox-gl bug)
+  map.on('click', (e) => {
+    if (!map.getLayer('raster')) return
+    const bounds = map.getSource('raster').bounds
+    if (
+      (e.lngLat.lng >= bounds[0] && e.lngLat.lng <= bounds[2]) &&
+      (e.lngLat.lat >= bounds[1] && e.lngLat.lat <= bounds[3])
+    ) {
+      const coord = `${e.lngLat.lng},${e.lngLat.lat}`
+      fetch(`${point_endpoint}?coordinates=${coord}`)
         .then(res => {
           if (res.ok) return res.json()
-          throw new Error('Network response was not ok.')
+          throw new Error('Network response was not ok.');
         })
         .then(data => {
-          scope.dataset_statistics = data
-          if (document.getElementById('toolbar').querySelector(".active").id === '1b') {
-            addHisto1Band()
-          } else {
-            addHisto3Bands()
+          let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
+          let values = Object.keys(data.value)
+          for (var i = 0; i < values.length; i++) {
+            let value = data.value[values[i]]
+            let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
+            html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
           }
-          document.getElementById('histogram').classList.remove('loading')
+          html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
+          html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
+          html += '</table>'
+          new mapboxgl.Popup()
+            .setLngLat(e.lngLat)
+            .setHTML(html)
+            .addTo(map)
         })
         .catch(err => {
           console.warn(err)
         })
-      })
-
-    document.getElementById('btn-hide').addEventListener('click', () => {
-      document.getElementById('hide-arrow').classList.toggle('off')
-      document.getElementById('menu').classList.toggle('off')
-    })
-
-    document.getElementById('viz-selector').addEventListener('change', (e) => {
-      switch (e.target.value) {
-        case 'raster':
-          document.getElementById('colormap-section').classList.remove('none')
-          document.getElementById('extrusion-section').classList.add('none')
-          break
-
-        case 'point':
-          document.getElementById('colormap-section').classList.add('none')
-          document.getElementById('extrusion-section').classList.add('none')
-          break
-
-        case 'polygon':
-          document.getElementById('colormap-section').classList.add('none')
-          document.getElementById('extrusion-section').classList.remove('none')
-          break
-
-        default:
-      }
-      switchViz()
-    })
-
-    // MVT have already all the layers while for raster we need to fetch new tiles
-    const updateViz = () => {
-      const newViz = document.getElementById('viz-selector').querySelector("input[name='toggle-viz']:checked").value
-      if (newViz === "raster") {
-        switchViz()
-      } else {
-        addLayer(newViz)
-      }
     }
+  })
 
-    document.getElementById('layer-selector').addEventListener('change', () => {
-      const active_layer = document.getElementById('layer-selector')[document.getElementById('layer-selector').selectedIndex]
-      updateViz()
+  fetch(`${info_endpoint}`)
+    .then(res => {
+      if (res.ok) return res.json()
+      throw new Error('Network response was not ok.')
     })
-
-    document.getElementById('r-selector').addEventListener('change', () => {switchViz()})
-    document.getElementById('g-selector').addEventListener('change', () => {switchViz()})
-    document.getElementById('b-selector').addEventListener('change', () => {switchViz()})
-
-    document.getElementById('updateExag').addEventListener('click', () => {
-      updateViz()
-    })
-
-    document.getElementById('colormap-selector').addEventListener('change', () => {
-      updateViz()
-    })
-
-    document.getElementById('btn-rescale').addEventListener('click', () => {
-      updateViz()
-    })
-
-    const switchPane = (event) => {
-      const cur = document.getElementById('toolbar').querySelector(".active")
-      const activeViz = cur.id
-      const nextViz = event.id
-      cur.classList.toggle('active')
-      event.classList.toggle('active')
-
-      const curSection = document.getElementById(`${activeViz}-section`)
-      curSection.classList.toggle('active')
-      const nextSection = document.getElementById(`${nextViz}-section`)
-      nextSection.classList.toggle('active')
-      switchViz()
-    }
-
-    const bboxPolygon = (bounds) => {
-        return {
-            'type': 'Feature',
-            'geometry': {
-                'type': 'Polygon',
-                'coordinates': [[
-                    [bounds[0], bounds[1]],
-                    [bounds[2], bounds[1]],
-                    [bounds[2], bounds[3]],
-                    [bounds[0], bounds[3]],
-                    [bounds[0], bounds[1]]
-                ]]
-            },
-            'properties': {}
-        }
-    }
-
-    const addAOI = (bounds) => {
-      if (map.getLayer('aoi-polygon')) map.removeLayer('aoi-polygon')
-      if (map.getSource('aoi')) map.removeSource('aoi')
-      const geojson = {
-          "type": "FeatureCollection",
-          "features": [bboxPolygon(bounds)]
+    .then(data => {
+      console.log(data)
+      scope.data_type = data.dtype
+      scope.colormap = data.colormap
+
+      if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) document.getElementById('minmax-data').classList.remove('none')
+
+      const mm = dtype_ranges[scope.data_type]
+      document.getElementById('data-min').value = mm[0]
+      document.getElementById('data-max').value = mm[1]
+
+      scope.band_descriptions = data.band_descriptions
+      const band_descr = data.band_descriptions
+      const nbands = band_descr.length
+
+      //Populate Band (1b) selector
+      const layerList = document.getElementById('layer-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        layerList.appendChild(l)
       }
 
-      map.addSource('aoi', {
-        'type': 'geojson',
-        'data': geojson
-      })
-
-      map.addLayer({
-        id: 'aoi-polygon',
-        type: 'line',
-        source: 'aoi',
-        layout: {
-          'line-cap': 'round',
-          'line-join': 'round'
-        },
-        paint: {
-          'line-color': '#3bb2d0',
-          'line-width': 1
-        }
-      })
-      return
-    }
-
-    const truncate_lnglat = (lng, lat) => {
-      if (lng > 180.0) {
-        lng = 180.0
-      } else if (lng < -180.0) {
-        lng = -180.0
+      // Populate R/G/B (3b) selectors
+      const rList = document.getElementById('r-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (i === 0) l.selected="selected"
+        rList.appendChild(l)
       }
-      if (lat > 90.0) {
-        lat = 90.0
-      } else if (lat < -90.0) {
-        lat = -90.0
+
+      const gList = document.getElementById('g-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (i === 1) l.selected="selected"
+        gList.appendChild(l)
       }
-      return [lng, lat]
-    }
 
-    map.on('load', () => {
-      map.on('mousemove', (e) => {
-        if (!map.getLayer('mvt')) return
-        const mouseRadius = 1
-        const feature = map.queryRenderedFeatures([
-          [e.point.x - mouseRadius, e.point.y - mouseRadius],
-          [e.point.x + mouseRadius, e.point.y + mouseRadius]
-        ], { layers: ['mvt'] })[0]
-        if (feature) {
-          map.getCanvas().style.cursor = 'pointer'
+      const bList = document.getElementById('b-selector')
+      for (var i = 0; i < nbands; i++) {
+        let l = document.createElement('option')
+        l.setAttribute('band', band_descr[i][0])
+        let name = band_descr[i][1] || band_descr[i][0]
+        l.setAttribute('name', name)
+        l.text = name
+        if (band_descr.length > 2 && i === 2) {
+          l.selected="selected"
         } else {
-          map.getCanvas().style.cursor = 'inherit'
+          l.selected="selected"
         }
-      })
-
-      map.on('click', 'mvt', (e) => {
-        let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
-        let values = Object.keys(e.features[0].properties)
-        for (var i = 0; i < values.length; i++) {
-          let value = e.features[0].properties[values[i]]
-          let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
-          html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
-        }
-
-        html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
-        html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
-        html += '</table>'
-        new mapboxgl.Popup()
-          .setLngLat(e.lngLat)
-          .setHTML(html)
-          .addTo(map)
-      })
-
-      // we cannot click on raster layer (mapbox-gl bug)
-      map.on('click', (e) => {
-        if (!map.getLayer('raster')) return
-        const bounds = map.getSource('raster').bounds
-        if (
-          (e.lngLat.lng >= bounds[0] && e.lngLat.lng <= bounds[2]) &&
-          (e.lngLat.lat >= bounds[1] && e.lngLat.lat <= bounds[3])
-        ) {
-          const coord = `${e.lngLat.lng},${e.lngLat.lat}`
-          fetch(`${point_endpoint}?coordinates=${coord}`)
-            .then(res => {
-              if (res.ok) return res.json()
-              throw new Error('Network response was not ok.');
-            })
-            .then(data => {
-              let html = '<table><tr><th class="align-l">property</th><th class="px3 align-r">value</th></tr>'
-              let values = Object.keys(data.value)
-              for (var i = 0; i < values.length; i++) {
-                let value = data.value[values[i]]
-                let key = scope.band_descriptions[i][1] || scope.band_descriptions[i][0]
-                html += `<tr><td class="align-l">${key}</td><td class="px3 align-r">${value}</td></tr>`
-              }
-              html += `<tr><td class="align-l">lon</td><td class="px3 align-r">${e.lngLat.lng.toString().slice(0, 7)}</td></tr>`
-              html += `<tr><td class="align-l">lat</td><td class="px3 align-r">${e.lngLat.lat.toString().slice(0, 7)}</td></tr>`
-              html += '</table>'
-              new mapboxgl.Popup()
-                .setLngLat(e.lngLat)
-                .setHTML(html)
-                .addTo(map)
-            })
-            .catch(err => {
-              console.warn(err)
-            })
-        }
-      })
-
-      fetch(`${info_endpoint}`)
-        .then(res => {
-          if (res.ok) return res.json()
-          throw new Error('Network response was not ok.')
-        })
-        .then(data => {
-          console.log(data)
-          scope.data_type = data.dtype
-          scope.colormap = data.colormap
-
-          if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) document.getElementById('minmax-data').classList.remove('none')
-
-          const mm = dtype_ranges[scope.data_type]
-          document.getElementById('data-min').value = mm[0]
-          document.getElementById('data-max').value = mm[1]
-
-          scope.band_descriptions = data.band_descriptions
-          const band_descr = data.band_descriptions
-          const nbands = band_descr.length
-
-          //Populate Band (1b) selector
-          const layerList = document.getElementById('layer-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            layerList.appendChild(l)
-          }
-
-          // Populate R/G/B (3b) selectors
-          const rList = document.getElementById('r-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (i === 0) l.selected="selected"
-            rList.appendChild(l)
-          }
-
-          const gList = document.getElementById('g-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (i === 1) l.selected="selected"
-            gList.appendChild(l)
-          }
-
-          const bList = document.getElementById('b-selector')
-          for (var i = 0; i < nbands; i++) {
-            let l = document.createElement('option')
-            l.setAttribute('band', band_descr[i][0])
-            let name = band_descr[i][1] || band_descr[i][0]
-            l.setAttribute('name', name)
-            l.text = name
-            if (band_descr.length > 2 && i === 2) {
-              l.selected="selected"
-            } else {
-              l.selected="selected"
-            }
-            bList.appendChild(l)
-          }
+        bList.appendChild(l)
+      }
 
-          // remove loader
-          document.getElementById('loader').classList.toggle('off')
-          document.getElementById('hide-arrow').classList.toggle('off')
-          document.getElementById('menu').classList.toggle('off')
+      // remove loader
+      document.getElementById('loader').classList.toggle('off')
+      document.getElementById('hide-arrow').classList.toggle('off')
+      document.getElementById('menu').classList.toggle('off')
 
-          const ll = truncate_lnglat(data.bounds[0], data.bounds[1])
-          const ur = truncate_lnglat(data.bounds[2], data.bounds[3])
-          map.fitBounds([ll, ur])
+      const ll = truncate_lnglat(data.bounds[0], data.bounds[1])
+      const ur = truncate_lnglat(data.bounds[2], data.bounds[3])
+      map.fitBounds([ll, ur])
 
-          addAOI(data.bounds)
+      addAOI(data.bounds)
 
-          if (nbands === 1) {
-            document.getElementById('3b').classList.add('disabled')
-            document.getElementById('3b').classList.remove('active')
-            document.getElementById('3b-section').classList.toggle('active')
-            document.getElementById('1b').classList.add('active')
-            document.getElementById('1b-section').classList.toggle('active')
-          }
-          switchViz()
-        })
-        .catch(err => {
-          console.warn(err)
-        })
+      if (nbands === 1) {
+        document.getElementById('3b').classList.add('disabled')
+        document.getElementById('3b').classList.remove('active')
+        document.getElementById('3b-section').classList.toggle('active')
+        document.getElementById('1b').classList.add('active')
+        document.getElementById('1b-section').classList.toggle('active')
+      }
+      switchViz()
     })
-
+    .catch(err => {
+      console.warn(err)
+    })
+})
     </script>
-
     </body>
     </html>
diff --git a/setup.cfg b/setup.cfg
index ca74a28..217233d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,7 +27,6 @@ max-line-length = 90
 
 [mypy]
 no_strict_optional = True
-ignore_missing_imports = True
 
 [pydocstyle]
 select = D1
diff --git a/setup.py b/setup.py
index 0211323..dfca44f 100644
--- a/setup.py
+++ b/setup.py
@@ -8,9 +8,9 @@
 # Runtime requirements.
 inst_reqs = [
     "braceexpand",
-    "rio-tiler>=2.1,<3.0",
-    "rio-cogeo>=2.2,<3.0",
-    "titiler.core>=0.3.10,<0.4",
+    "rio-cogeo>=3.0",
+    "titiler.core>=0.4.0a2,<0.5",
+    "starlette-cramjam>=0.1.0,<0.2",
     "uvicorn",
 ]
 
@@ -23,7 +23,7 @@
 setup(
     name="rio-viz",
     version="0.7.2",
-    python_requires=">=3.6",
+    python_requires=">=3.7",
     description=u"Visualize Cloud Optimized GeoTIFF in browser",
     long_description=long_description,
     long_description_content_type="text/markdown",
@@ -31,7 +31,6 @@
         "Intended Audience :: Information Technology",
         "Intended Audience :: Science/Research",
         "License :: OSI Approved :: BSD License",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
diff --git a/tox.ini b/tox.ini
index a3ab467..c6a051c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py36,py37,py38,py39
+envlist = py37,py38,py39
 
 [testenv]
 extras =


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
index 253137f..f39d5f2 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -10,10 +10,14 @@
 
 from rio_viz.app import viz
 from rio_viz.compat import AsyncReader
-from rio_viz.io.reader import MultiFilesReader
+from rio_viz.io.mosaic import MosaicReader
+from rio_viz.io.reader import MultiFilesAssetsReader, MultiFilesBandsReader
 
 cog_path = os.path.join(os.path.dirname(__file__), "fixtures", "cog.tif")
 cogb1b2b3_path = os.path.join(os.path.dirname(__file__), "fixtures", "cogb{1,2,3}.tif")
+cog_mosaic_path = os.path.join(
+    os.path.dirname(__file__), "fixtures", "mosaic_cog{1,2}.tif"
+)
 
 
 def test_viz():
@@ -101,7 +105,7 @@ def test_viz():
     assert response.status_code == 200
     assert response.headers["content-type"] == "application/json"
 
-    response = client.get("/metadata")
+    response = client.get("/statistics")
     assert response.status_code == 200
     assert response.headers["content-type"] == "application/json"
 
@@ -130,7 +134,7 @@ def test_viz():
     response = client.get("/point?coordinates=-2,48")
     assert response.status_code == 200
     assert response.headers["content-type"] == "application/json"
-    assert response.json() == {"coordinates": [-2.0, 48.0], "value": [110]}
+    assert response.json() == {"coordinates": [-2.0, 48.0], "values": [110]}
 
     feat = json.dumps(
         {
@@ -186,12 +190,14 @@ def test_viz_custom():
     assert app.endpoint == "http://0.0.0.0:5050"
 
 
-def test_viz_multi():
+def test_viz_multibands():
     """Should work as expected (create TileServer object)."""
-    src_path = cogb1b2b3_path
-    dataset_reader = type("AsyncReader", (AsyncReader,), {"reader": MultiFilesReader})
+    dataset_reader = type(
+        "AsyncReader", (AsyncReader,), {"reader": MultiFilesBandsReader}
+    )
 
-    app = viz(src_path, reader=dataset_reader)
+    # Use default bands from the reader
+    app = viz(cogb1b2b3_path, reader=dataset_reader, reader_type="bands")
     assert app.port == 8080
     assert app.endpoint == "http://127.0.0.1:8080"
     client = TestClient(app.app)
@@ -200,7 +206,203 @@ def test_viz_multi():
     assert response.status_code == 200
     assert response.headers["content-type"] == "application/json"
     assert response.json()["band_descriptions"] == [
-        ["file1", ""],
-        ["file2", ""],
-        ["file3", ""],
+        ["b1", ""],
+        ["b2", ""],
+        ["b3", ""],
+    ]
+
+    response = client.get("/info?bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.json()["band_descriptions"] == [
+        ["b1", ""],
+        ["b2", ""],
+    ]
+
+    response = client.get("/statistics")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["b1", "b2", "b3"] == list(response.json())
+
+    response = client.get("/statistics?bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["b1", "b2"] == list(response.json())
+
+    response = client.get("/point?coordinates=-2.0,48.0")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 3
+
+    response = client.get("/point?coordinates=-2.0,48.0&bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 2
+
+    # Set default bands (other bands might still be available within the reader)
+    app = viz(cogb1b2b3_path, reader=dataset_reader, reader_type="bands", layers=["b1"])
+    assert app.port == 8080
+    assert app.endpoint == "http://127.0.0.1:8080"
+    client = TestClient(app.app)
+
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.json()["band_descriptions"] == [
+        ["b1", ""],
+    ]
+
+    response = client.get("/info?bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.json()["band_descriptions"] == [
+        ["b1", ""],
+        ["b2", ""],
     ]
+
+    response = client.get("/statistics")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["b1"] == list(response.json())
+
+    response = client.get("/statistics?bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["b1", "b2"] == list(response.json())
+
+    response = client.get("/point?coordinates=-2.0,48.0")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 1
+
+    response = client.get("/point?coordinates=-2.0,48.0&bands=b1&bands=b2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 2
+
+
+def test_viz_multiassets():
+    """Should work as expected (create TileServer object)."""
+    dataset_reader = type(
+        "AsyncReader", (AsyncReader,), {"reader": MultiFilesAssetsReader}
+    )
+
+    # Use default bands from the reader
+    app = viz(cogb1b2b3_path, reader=dataset_reader, reader_type="assets")
+    assert app.port == 8080
+    assert app.endpoint == "http://127.0.0.1:8080"
+    client = TestClient(app.app)
+
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2", "asset3"] == list(response.json())
+    assert response.json()["asset1"]["band_descriptions"]
+
+    response = client.get("/info?assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2"] == list(response.json())
+
+    response = client.get("/statistics")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2", "asset3"] == list(response.json())
+
+    response = client.get("/statistics?assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2"] == list(response.json())
+
+    response = client.get("/point?coordinates=-2.0,48.0")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 3
+    assert len(response.json()["values"][0]) == 1
+
+    response = client.get("/point?coordinates=-2.0,48.0&assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 2
+
+    # Set default bands (other bands might still be available within the reader)
+    app = viz(
+        cogb1b2b3_path, reader=dataset_reader, reader_type="assets", layers=["asset1"]
+    )
+    assert app.port == 8080
+    assert app.endpoint == "http://127.0.0.1:8080"
+    client = TestClient(app.app)
+
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1"] == list(response.json())
+
+    response = client.get("/info?assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2"] == list(response.json())
+
+    response = client.get("/statistics")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1"] == list(response.json())
+
+    response = client.get("/statistics?assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert ["asset1", "asset2"] == list(response.json())
+
+    response = client.get("/point?coordinates=-2.0,48.0")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 1
+
+    response = client.get("/point?coordinates=-2.0,48.0&assets=asset1&assets=asset2")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert len(response.json()["values"]) == 2
+
+
+def test_viz_mosaic():
+    """Should work as expected (create TileServer object)."""
+    src_path = cog_path
+    dataset_reader = type("AsyncReader", (AsyncReader,), {"reader": MosaicReader})
+
+    app = viz(src_path, reader=dataset_reader, reader_type="cog")
+
+    assert app.port == 8080
+    assert app.endpoint == "http://127.0.0.1:8080"
+    assert app.template_url == "http://127.0.0.1:8080/index.html"
+
+    client = TestClient(app.app)
+    response = client.get("/")
+    assert response.status_code == 404
+
+    response = client.get("/index.html")
+    assert response.status_code == 200
+    assert response.headers["cache-control"] == "no-cache"
+
+    response = client.get("/info")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+
+    response = client.get("/statistics")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+
+    response = client.get("/tiles/7/64/43?rescale=1,10")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "image/png"
+    assert response.headers["cache-control"] == "no-cache"
+
+    with pytest.raises(NotImplementedError):
+        client.get("/preview")
+
+    with pytest.raises(NotImplementedError):
+        client.get("/crop/-2.00,48.5,-1,49.5.png")
+
+    response = client.get("/point?coordinates=-2,48")
+    assert response.status_code == 200
+    assert response.headers["content-type"] == "application/json"
+    assert response.json() == {"coordinates": [-2.0, 48.0], "values": [110]}
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0dda5fa..583fcd3 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -3,7 +3,6 @@
 import os
 from unittest.mock import patch
 
-import pytest
 from click.testing import CliRunner
 
 from rio_viz.scripts.cli import viz
@@ -12,12 +11,6 @@
 noncog_path = os.path.join(os.path.dirname(__file__), "fixtures", "noncog.tif")
 
 
-@pytest.fixture(autouse=True)
-def testing_env_var(monkeypatch):
-    """Set testing env variable."""
-    monkeypatch.delenv("MAPBOX_ACCESS_TOKEN", raising=False)
-
-
 @patch("rio_viz.app.viz")
 @patch("click.launch")
 def test_viz_valid(launch, app):
@@ -34,68 +27,6 @@ def test_viz_valid(launch, app):
     assert result.exit_code == 0
 
 
-@patch("rio_viz.app.viz")
-@patch("click.launch")
-def test_viz_valid_style(launch, app):
-    """Should work as expected."""
-    app.return_value.get_template_url.return_value = "http://127.0.0.1:8080/index.html"
-    app.return_value.start.return_value = True
-
-    launch.return_value = True
-
-    runner = CliRunner()
-    result = runner.invoke(viz, [cog_path, "--style", "satellite"])
-    app.assert_called_once()
-    assert not result.exception
-    assert result.exit_code == 0
-
-
-@patch("rio_viz.app.viz")
-@patch("click.launch")
-def test_viz_validEnvToken(launch, app, monkeypatch):
-    """Should work as expected."""
-    monkeypatch.setenv("MAPBOX_ACCESS_TOKEN", "pk.afakemapboxtoken")
-
-    app.return_value.get_template_url.return_value = "http://127.0.0.1:8080/index.html"
-    app.return_value.start.return_value = True
-
-    launch.return_value = True
-
-    runner = CliRunner()
-    result = runner.invoke(viz, [cog_path])
-    app.assert_called_once()
-    assert not result.exception
-    assert result.exit_code == 0
-
-
-@patch("rio_viz.app.viz")
-@patch("click.launch")
-def test_viz_validToken(launch, app):
-    """Should work as expected."""
-    app.return_value.get_template_url.return_value = "http://127.0.0.1:8080/index.html"
-    app.return_value.start.return_value = True
-
-    launch.return_value = True
-
-    runner = CliRunner()
-    result = runner.invoke(viz, [cog_path, "--mapbox-token", "pk.afakemapboxtoken"])
-    app.assert_called_once()
-    assert not result.exception
-    assert result.exit_code == 0
-
-
-@patch("rio_viz.app.viz")
-@patch("click.launch")
-def test_viz_validInvalidToken(launch, app):
-    """Should work as expected."""
-    runner = CliRunner()
-    result = runner.invoke(viz, [cog_path, "--mapbox-token", "sk.afakemapboxtoken"])
-    app.assert_not_called()
-    launch.assert_not_called()
-    assert result.exception
-    assert result.exit_code == 1
-
-
 @patch("rio_viz.app.viz")
 @patch("click.launch")
 def test_viz_invalidCog(launch, app):