From cd033d5775036c245ce547e99319d75ebb53230e Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Mon, 28 Jan 2019 15:18:43 -0600 Subject: [PATCH] Panel-based Datashader dashboard (#676) * Removed outdated Bokeh dashboard directory and associated descriptions * Removed outdated ParamNB dashboard example * Added Panel-based dashboard --- examples/README.md | 31 +- examples/dashboard.ipynb | 246 ++++++++++++ examples/dashboard.yml | 149 +++++++ examples/dashboard/census.yml | 35 -- examples/dashboard/dashboard.py | 535 -------------------------- examples/dashboard/nyc_taxi.yml | 38 -- examples/dashboard/opensky.yml | 30 -- examples/dashboard/osm.yml | 19 - examples/topics/census.ipynb | 8 +- examples/topics/param_dashboard.ipynb | 200 ---------- examples/user_guide/index.ipynb | 5 +- tox.ini | 1 + 12 files changed, 403 insertions(+), 894 deletions(-) create mode 100644 examples/dashboard.ipynb create mode 100644 examples/dashboard.yml delete mode 100644 examples/dashboard/census.yml delete mode 100644 examples/dashboard/dashboard.py delete mode 100644 examples/dashboard/nyc_taxi.yml delete mode 100644 examples/dashboard/opensky.yml delete mode 100644 examples/dashboard/osm.yml delete mode 100644 examples/topics/param_dashboard.ipynb diff --git a/examples/README.md b/examples/README.md index c34337d27..5d51a2620 100644 --- a/examples/README.md +++ b/examples/README.md @@ -77,33 +77,4 @@ with an unreliable connection (e.g. if you see `Loading BokehJS ...` but never BOKEH_RESOURCES=inline jupyter notebook --NotebookApp.iopub_data_rate_limit=100000000 ``` -## Dashboard - -An example interactive dashboard using -[bokeh server](http://bokeh.pydata.org/en/latest/docs/user_guide/server.html) -integrated with a datashading pipeline. - -To start, launch it with one of the supported datasets specified: - -``` -python dashboard/dashboard.py -c dashboard/nyc_taxi.yml -python dashboard/dashboard.py -c dashboard/census.yml -python dashboard/dashboard.py -c dashboard/opensky.yml -python dashboard/dashboard.py -c dashboard/osm.yml -``` - -The '.yml' configuration file sets up the dashboard to use one of the -datasets downloaded above. You can write similar configuration files -for working with other datasets of your own, while adding features to -`dashboard.py` itself if needed to support them. - -For most of these datasets, if you have less than 16GB of RAM on your -machine, you will want to add the "-o" option before "-c" to tell it -to work out of core instead of loading all data into memory. However, -doing so will make interactive use substantially slower than if -sufficient memory were available. - -To launch multiple dashboards at once, you'll need to add "-p 5001" -(etc.) to select a unique port number for the web page to use for -communicating with the Bokeh server. Otherwise, be sure to kill the -server process before launching another instance. +See dashboard.ipynb in this directory for a Datashder dashboard for viewing data. diff --git a/examples/dashboard.ipynb b/examples/dashboard.ipynb new file mode 100644 index 000000000..255129b20 --- /dev/null +++ b/examples/dashboard.ipynb @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Datashader dashboard\n", + "\n", + "This notebook contains the code for an interactive dashboard for making [Datashader](http://datashader.org) plots from any dataset that has latitude and longitude (geographic) values. Apart from Datashader itself, the code relies on other Python packages from the [PyViz](http://pyviz.org) project that are each designed to make it simple to:\n", + "\n", + "- lay out plots and widgets into an app or dashboard, in a notebook or for serving separately ([Panel](http://panel.pyviz.org))\n", + "- build interactive web-based plots without writing JavaScript ([Bokeh](http://bokeh.pydata.org))\n", + "- build interactive Bokeh-based plots backed by Datashader, from concise declarations ([HoloViews](http://holoviews.org))\n", + "- express dependencies between parameters and code to build reactive interfaces declaratively ([Param](http://param.pyviz.org))\n", + "- describe the fields and plotting information needed to plot a dataset, in a text file ([Intake](http://intake.readthedocs.io))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os, colorcet, param as pm, holoviews as hv, panel as pn, datashader as ds\n", + "import intake, geoviews.tile_sources as gts\n", + "from holoviews.operation.datashader import rasterize, shade, spread\n", + "from collections import OrderedDict as odict\n", + "\n", + "hv.extension('bokeh', logo=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can run the dashboard here in the notebook with various datasets by editing the `dataset` below to specify some dataset defined in `dashboard.yml`. You can also launch a separate, standalone server process in a new browser tab with a command like:\n", + "\n", + "```\n", + "DS_DATASET=nyc_taxi panel serve --show dashboard.ipynb\n", + "```\n", + "\n", + "(Where `nyc_taxi` can be replaced with any of the available datasets (`nyc_taxi`, `nyc_taxi_50k` (tiny version), `census`, `opensky`, `osm-1b`) or any dataset whose description you add to `dashboard.yml`). To launch multiple dashboards at once, you'll need to add `-p 5001` (etc.) to select a unique port number for the web page to use for communicating with the Bokeh server. Otherwise, be sure to kill the server process before launching another instance.\n", + "\n", + "For most of these datasets, if you have less than 16GB of RAM on your machine, you should remove the `.persist()` method call below, to tell [Dask](http://dask.pydata.org) to work out of core instead of loading all data into memory. However, doing so will make interactive use substantially slower than if sufficient memory were available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = os.getenv(\"DS_DATASET\", \"nyc_taxi\")\n", + "catalog = intake.open_catalog('dashboard.yml')\n", + "source = getattr(catalog, dataset)\n", + "source.to_dask().persist();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Intake `source` object lets us treat data in many different formats the same in the rest of the code here. We can now build a class that captures some parameters that the user can vary along with how those parameters relate to the code needed to update the displayed plot of that data source:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plots = {source.metadata['plots'][p].get('label',p):p for p in source.plots}\n", + "fields = odict([(v.get('label',k),k) for k,v in source.metadata['fields'].items()])\n", + "field = next(iter(fields.items()))[1]\n", + "aggfns = odict([(f.capitalize(),getattr(ds,f)) for f in ['count','sum','min','max','mean','var','std']])\n", + "\n", + "norms = {'Histogram_Equalization': 'eq_hist', 'Linear': 'linear', 'Log': 'log', 'Cube root': 'cbrt'}\n", + "cmaps = {n: colorcet.palette[n] for n in ['fire', 'bgy', 'bgyw', 'bmy', 'gray', 'kbc']}\n", + "\n", + "maps = ['CartoMidnight', 'StamenWatercolor', 'StamenTonerBackground', 'EsriImagery', 'EsriUSATopo', 'EsriTerrain']\n", + "bases = {name: ts.relabel(name) for name, ts in gts.tile_sources.items() if name in maps}\n", + "\n", + "gopts = hv.opts.WMTS(width=800, height=650, xaxis=None, yaxis=None, bgcolor='black', show_grid=False)\n", + "\n", + "class Explorer(pm.Parameterized):\n", + " plot = pm.ObjectSelector( precedence=0.10, default=source.plots[0], objects=plots)\n", + " field = pm.ObjectSelector( precedence=0.11, default=field, objects=fields)\n", + " agg_fn = pm.ObjectSelector( precedence=0.12, default=ds.count, objects=aggfns)\n", + " \n", + " normalization = pm.ObjectSelector( precedence=0.13, default='eq_hist', objects=norms)\n", + " cmap = pm.ObjectSelector( precedence=0.14, default=cmaps['fire'], objects=cmaps)\n", + " spreading = pm.Integer( precedence=0.16, default=0, bounds=(0, 5))\n", + " \n", + " basemap = pm.ObjectSelector( precedence=0.18, default=bases['EsriImagery'], objects=bases)\n", + " data_opacity = pm.Magnitude( precedence=0.20, default=1.00, doc=\"Alpha value for the data\")\n", + " map_opacity = pm.Magnitude( precedence=0.22, default=0.75, doc=\"Alpha value for the map\")\n", + " show_labels = pm.Boolean( precedence=0.24, default=True)\n", + " \n", + " @pm.depends('plot')\n", + " def elem(self):\n", + " return getattr(source.plot, self.plot)()\n", + "\n", + " @pm.depends('field', 'agg_fn')\n", + " def rasterize(self, element, x_range=None, y_range=None):\n", + " field = None if self.field == \"counts\" else self.field\n", + " return rasterize(element, width=800, height=600, aggregator=self.agg_fn(field),\n", + " x_range=x_range, y_range=y_range, dynamic=False)\n", + "\n", + " @pm.depends('map_opacity','basemap')\n", + " def tiles(self):\n", + " return self.basemap.opts(gopts).opts(alpha=self.map_opacity)\n", + "\n", + " @pm.depends('show_labels')\n", + " def labels(self):\n", + " return gts.StamenLabels.options(level='annotation', alpha=1 if self.show_labels else 0)\n", + " \n", + " @pm.depends('data_opacity')\n", + " def apply_opacity(self, shaded):\n", + " return shaded.opts(alpha=self.data_opacity, show_legend=False)\n", + "\n", + " def viewable(self,**kwargs):\n", + " data_dmap = hv.DynamicMap(self.elem)\n", + " rasterized = hv.util.Dynamic(data_dmap, operation=self.rasterize, streams=[hv.streams.RangeXY])\n", + " \n", + " c_stream = hv.streams.Params(self, ['cmap', 'normalization'])\n", + " s_stream = hv.streams.Params(self, ['spreading'], rename={'spreading': 'px'})\n", + " shaded = spread(shade(rasterized, streams=[c_stream]), streams=[s_stream], how=\"add\")\n", + " shaded = hv.util.Dynamic(shaded, operation=self.apply_opacity)\n", + " \n", + " return hv.DynamicMap(self.tiles) * shaded * hv.DynamicMap(self.labels)\n", + "\n", + "explorer = Explorer(name=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we call the `.viewable` method on the `explorer` object we just created, we'll get a plot that displays itself in a notebook cell. Moreover, because of how we declared the dependencies between each bit of code and each parameters, the corresponding part of that plot will update whenever one of the parameters is changed on it. (Try putting `explorer.viewable()` in one cell, then set some parameter like `explorer.spreading=4` in another cell.) But since what we want is the user to be able to manipulate the values using widgets, let's go ahead and create a dashboard out of this object by laying out a logo, widgets for the parameters, and the viewable object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "logo = \"https://raw.githubusercontent.com/pyviz/datashader/master/doc/_static/logo_horizontal_s.png\"\n", + "\n", + "panel = pn.Row(pn.Column(logo, pn.Param(explorer.param, expand_button=False)), explorer.viewable())\n", + "panel.servable()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you are viewing this notebook with a live Python server process running, adjusting one of the widgets above should now automatically update the plot, re-running only the code needed to update that particular item without re-running datashader if that's not needed. It should work the same when launched as a separate server process, but without the extra text and code visible as in this notebook. Here the `.servable()` method call indicates what should be served when run as a separate dashboard with a command like `panel serve --show dashboard.ipynb`, or you can just copy the code out of this notebook into a .py file that will work the same as this .ipynb file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## How it works\n", + "\n", + "You can use the code above as is, but if you want to adapt it to your own purposes, you can read on to see how it works. The code has three main components:\n", + "\n", + "1. `source`: A dataset with associated metadata managed by [Intake](http://intake.readthedocs.io), which allows this notebook to ignore details like:\n", + " - File formats\n", + " - File locations\n", + " - Column and field names in the data

\n", + " Basically, once the `source` has been defined in the cell starting with `dataset`, this code can treat all datasets the same, as long as their properties have been declared appropriately in the `dashboard.yml` file.

\n", + "\n", + "2. `explorer`: A [Parameterized](http://param.pyviz.org) object that declares:\n", + " - What parameters we want the user to be able to manipulate\n", + " - How to generate the plot specified by those parameters, using [HoloViews](http://holoviews.org), [GeoViews](http://holoviews.org), [Datashader](http://datashader.org), and [Bokeh](http://bokeh.pydata.org).\n", + " - Which bits of the code need to be run when one of the parameters changes

\n", + " All of these things are declared in a general way that's not tied to any particular GUI toolkit, as long as whatever is returned by `viewable()` is something that can be displayed.

\n", + " \n", + "3. `panel`: A [Panel](http://panel.pyviz.org)-based app/dashboard consisting of:\n", + " - a logo (just for pretty!)\n", + " - The user-adjustable parameters of the `explorer` object.\n", + " - The viewable HoloViews object defined by `explorer`.\n", + "\n", + "You can find out more about how to work with these objects at the websites linked for each one. If you want to start working with this code for your own purposes, parts 1 and 3 should be simple to get started with. You should be able to add new datasets easily to `dashboard.yml` by copying the description of the simplest dataset (e.g. `osm-1b`). If you wish, you can then compare that dataset's description to the other datasets, to see how other fields and metadata can be added if you want there to be more options for users to explore a particular dataset. \n", + "\n", + "Similarly, you can easily add additional items to lay out in rows and columns in the `panel` app; it should be trivial to add anything Panel supports (text boxes, images, other separate plots, etc.) to this layout as described at [Panel.org](http://panel.pyviz.org). \n", + "\n", + "Part 2 (the `explorer` object) is the hard part to specify, because that's where the complex relationships between the user-visible parameters and the underlying behavior is expressed. Briefly, the `Explorer` class is used to create an instance `explorer` that serves dual purposes. First, it provides an `explorer.param` object that declares what widgets should be made available for the user to manipulate. Second, it provides an `explorer.viewable` method that returns a displayable object automatically tied to each of those parameters, so that the appropriate part of the plot updates when a parameter is changed. In simple cases you can simply have all computation depend on any parameter, avoiding any complexity by re-running everything. However, Datashader is expected to be used with enormous datasets, so we have chosen to be very careful about not re-running any data-processing code unless it is absolutely necessary.\n", + "\n", + "Let's look more closely at `explorer.viewable()` to see how this is done. What's returned by that method is a [HoloViews](http://holoviews.org) object, in this case an `hv.Overlay` of three components: the underlying tile-based map (like Google Maps), the [datashaded](http://datashader.org) data, and overlaid geographic labels (which also happens to be a tile-based map, but with only text). If you type `explorer.viewable()` in a cell on its own, you can see that the resulting object is viewable outside of Panel and is still controlled by all the same parameters; Panel just adds visible widgets that let the user change the parameters without writing Python code. \n", + "\n", + "To understand this method, first consider a simpler version that doesn't display the data:\n", + "\n", + "```\n", + "def viewable(self,**kwargs):\n", + " return hv.DynamicMap(self.tiles) * hv.DynamicMap(self.labels)\n", + "```\n", + "\n", + "Here, `hv.DynamicMap(callback)` returns a dynamic HoloViews object that calls the provided `callback` whenever the object needs updating. When given a Parameterized method, `hv.DynamicMap` understands the dependency declarations if present. In this case, the map tiles will thus be updated whenever the `map_opacity` or `basemap` parameters change, and the overlaid labels will be updated whenever the `show_labels` parameter changes (because those are the relationships expressed with `param.depends` above). The `viewable()` method here returns an overlay (constructed by the `*` syntax for HoloViews objects), retaining the underlying dynamic behavior of the two overlaid items.\n", + "\n", + "Still following along? If not, try changing `viewable` to the simpler version shown above and play around with the source code to see how those parts fit together. Once that all makes sense, then we can add in a plot of the actual data:\n", + "\n", + "```\n", + "def viewable(self,**kwargs):\n", + " return hv.DynamicMap(self.tiles) * hv.DynamicMap(self.elem) * hv.DynamicMap(self.labels)\n", + "```\n", + "\n", + "Just as before, we use a `DynamicMap` to call the `.elem()` method whenever one of its parameter dependencies changes (`plot` in this case). Don't actually run this version, though, unless you have a very small dataset (even the tiny `nyc_taxi_50k` may be too large for some browsers). As written, this code will pass all the data on to your browser, with disastrous results for large datasets! This is where Datashader comes in; to make it safe for large data, we can instead wrap this object in some HoloViews operations that turn it into something safe to display:\n", + "\n", + "```\n", + "def viewable(self,**kwargs):\n", + " return hv.DynamicMap(self.tiles) * spread(shade(rasterize(hv.DynamicMap(self.elem)))) * hv.DynamicMap(self.labels)\n", + "```\n", + "\n", + "This version is now runnable, with `rasterize()` dynamically aggregating the data using Datashader whenever a new plot is needed, `shade()` then dynamically colormapping the data into an RGB image, and `spread()` dynamically spreading isolated pixels so that they become visible data points. But if you try it, you'll notice that the plot is ignoring all of the rasterization, shading, and spreading parameters we declared above, because those parameters are not declared as dependencies of the `elem` method. \n", + "\n", + "We could add those parameters as dependencies to `.elem`, but if we do that, then the whole set of chained operations will need to be re-run every time any one of those parameters changes. For a large dataset, re-running all those steps can take seconds or even minutes, yet some of the changes only affect the very last (and very cheap) stages of the computation, such as `spread` or `shade`. \n", + "\n", + "So, we come to the final version of `viewable()` that's used in the actual class definition above:\n", + "- first create a `data_dmap` DynamicMap object that updates the HoloViews element when the `plot` parameter changes\n", + "- then create a DynamicMap `rasterized` that applies the rasterize operation to the `data_dmap` while bringing in the `field` and `agg_fn` parameters\n", + "- then create a HoloViews \"stream\" `c_stream` that watches the parameters used in colormapping (`cmap` and `normalization`)\n", + "- then create a HoloViews \"stream\" `s_stream` that watches the parameters used in spreading (`spreading`)\n", + "- then create a DynamicMap that applies shading and spreading driven by the streams just created\n", + "- then return the overlay as in each of the simpler versions of `viewable` above\n", + "\n", + "As if that weren't confusing enough, here we had to use three different ways of making a DynamicMap: \n", + "1. Creating one directly: `hv.DynamicMap(self.callbackmethod)`: makes the result of a callback displayable on updates\n", + "2. Wrapping an existing DynamicMap with an operation (`rasterize`, `shade`, `spread`, etc.): chains an operation on top of the output of something already dynamic, optionally attaching \"streams\" to control the stage-specific parameters dynamically\n", + "3. Using `hv.util.Dynamic`: applies a method to the given object, controlled by supplied streams\n", + "\n", + "As you can see, we had to use some esoteric features of HoloViews, but we were able to precisely characterize which bits of the computation need to be re-run, providing maximal responsiveness where possible (try dragging the opacity sliders or selecting colormaps), while re-running everything when needed (when aggregation-related parameters change). In many cases you can use much simpler approaches than were needed here, as we were able to do for the map tiles and labels above." + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/dashboard.yml b/examples/dashboard.yml new file mode 100644 index 000000000..1e99ee23f --- /dev/null +++ b/examples/dashboard.yml @@ -0,0 +1,149 @@ +sources: + osm-1b: + description: 1-billion-point OpenStreetMap GPS dataset + driver: parquet + args: + urlpath: '{{ CATALOG_DIR }}/data/osm-1billion.snappy.parq' + columns: ['x','y'] + metadata: + fields: + counts: + label: GPS coordinates + plot: + xlim: !!python/tuple [-8240227, -8231284] + ylim: !!python/tuple [ 4974203, 4979238] + kind: points + plots: + counts: + label: 1 billion OpenStreetMap GPS locations + x: x + y: y + + nyc_taxi: + description: Large version of nyc taxi dataset + driver: parquet + args: + urlpath: '{{ CATALOG_DIR }}/data/nyc_taxi_wide.parq' + columns: ['dropoff_x','dropoff_y','pickup_x','pickup_y', + 'dropoff_hour','pickup_hour','passenger_count'] + metadata: + fields: + counts: + label: Ride counts + passenger_count: + label: Passenger Count + dropoff_hour: + label: Drop-off Hour + pickup_hour: + label: Pick-up Hour + plot: + xlim: !!python/tuple [-8240227.037, -8231283.905] + ylim: !!python/tuple [4974203.152, 4979238.441] + kind: points + hover_cols: ['dropoff_hour', 'pickup_hour', 'passenger_count'] + plots: + dropoff: + label: NYC Taxi Dropoffs + x: dropoff_x + y: dropoff_y + pickup: + label: NYC Taxi Pickups + x: pickup_x + y: pickup_y + + nyc_taxi_50k: + description: Small version of nyc taxi dataset + driver: parquet + args: + urlpath: '{{ CATALOG_DIR }}/data/nyc_taxi_50k.parq' + columns: ['dropoff_x','dropoff_y','pickup_x','pickup_y', + 'dropoff_hour','pickup_hour','passenger_count'] + metadata: + fields: + counts: + label: Ride counts + passenger_count: + label: Passenger Count + dropoff_hour: + label: Drop-off Hour + pickup_hour: + label: Pick-up Hour + plot: + xlim: !!python/tuple [-8240227.037, -8231283.905] + ylim: !!python/tuple [4974203.152, 4979238.441] + kind: points + hover_cols: ['dropoff_hour', 'pickup_hour', 'passenger_count'] + plots: + dropoff: + label: NYC Taxi Dropoffs + x: dropoff_x + y: dropoff_y + pickup: + label: NYC Taxi Pickups + x: pickup_x + y: pickup_y + + census: + description: US Census Synthetic Data + driver: parquet + args: + urlpath: '{{ CATALOG_DIR }}/data/census.snappy.parq' + metadata: + fields: + counts: + label: Counts + race: + label: Race +# this doesn't work, but I think something like it might. +# labels: +# w: White +# b: Black +# a: Asian +# h: Hispanic +# o: Other + plot: + x: easting + y: northing + xlim: !!python/tuple [-13914936, -7235767] + ylim: !!python/tuple [2632019, 6446276] + kind: points + plots: + people: + label: US Census Synthetic people + race: + label: US Census Synthetic race + c: race + cmap: + w: blue + b: green + a: red + h: orange + o: saddlebrown + + opensky: + description: OpenSky Flight Paths + driver: netcdf + args: + urlpath: '{{ CATALOG_DIR }}/data/opensky.h5' + chunks: {} + metadata: + fields: + counts: + label: Counts + ascending: + label: Ascending vs. Descending + cat_colors: + True: blue + False: red + cat_names: + True: Ascending + False: Descending + plot: + xlim: !!python/tuple [-2000000, 2500000] + ylim: !!python/tuple [4100000, 7800000] + kind: points + x: longitude + y: latitude + plots: + flight_paths: + label: OpenSky Flight Paths diff --git a/examples/dashboard/census.yml b/examples/dashboard/census.yml deleted file mode 100644 index d51899afd..000000000 --- a/examples/dashboard/census.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- - -file: ../data/census.snappy.parq - - -axes: - - name: US Census Synthetic people - xaxis: easting - yaxis: northing - initial_extent: - is_geo: true - xmin: -13914936 - ymin: 2632019 - xmax: -7235767 - ymax: 6446276 - -summary_fields: - - - name: Race - field: race - cat_colors: - w: blue - b: green - a: red - h: orange - o: saddlebrown - cat_names: - w: White - b: Black - a: Asian - h: Hispanic - o: Other - - - name: Counts - field: None diff --git a/examples/dashboard/dashboard.py b/examples/dashboard/dashboard.py deleted file mode 100644 index 9aa2c8cce..000000000 --- a/examples/dashboard/dashboard.py +++ /dev/null @@ -1,535 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import argparse -from os import path -import yaml -import uuid - -from collections import OrderedDict - -import pandas as pd - -from bokeh.server.server import Server -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler - -from bokeh.plotting import Figure -from bokeh.models import (Range1d, ImageSource, WMTSTileSource, TileRenderer, DynamicImageRenderer, Row, Column) - -from bokeh.models import (Select, Slider, CheckboxGroup) - -import datashader as ds -import datashader.transfer_functions as tf - -from colorcet import palette - -from datashader.bokeh_ext import HoverLayer, create_categorical_legend, create_ramp_legend -from datashader.utils import hold - -from tornado.ioloop import IOLoop -from tornado.web import RequestHandler - -from webargs import fields -from webargs.tornadoparser import use_args - -# http request arguments for datashing HTTP request -ds_args = { - 'width': fields.Int(missing=800), - 'height': fields.Int(missing=600), - 'select': fields.Str(missing=""), -} - - -def odict_to_front(odict,key): - """Given an OrderedDict, move the item with the given key to the front.""" - front_item = [(key,odict[key])] - other_items = [(k,v) for k,v in odict.items() if k is not key] - return OrderedDict(front_item+other_items) - - -class GetDataset(RequestHandler): - """Handles http requests for datashading.""" - - @use_args(ds_args) - def get(self, args): - - # parse args - selection = args['select'].strip(',').split(',') - xmin, ymin, xmax, ymax = map(float, selection) - self.model.map_extent = [xmin, ymin, xmax, ymax] - - glyph = self.model.glyph.get(str(self.model.field), 'points') - - # create image - self.model.agg = self.model.create_aggregate(args['width'], - args['height'], - (xmin, xmax), - (ymin, ymax), - self.model.field, - self.model.active_axes['xaxis'], - self.model.active_axes['yaxis'], - self.model.agg_function_name, glyph) - pix = self.model.render_image() - - def update_plots(): - self.model.update_hover() - self.model.update_legend() - - server.get_sessions('/')[0].with_document_locked(update_plots) - # serialize to image - img_io = pix.to_bytesio() - self.write(img_io.getvalue()) - self.set_header("Content-type", "image/png") - - -class AppState(object): - """Simple value object to hold app state""" - - def __init__(self, config_file, outofcore, app_port): - - self.load_config_file(config_file) - self.plot_height = 600 - self.plot_width = 990 - - self.aggregate_functions = OrderedDict() - self.aggregate_functions['Count'] = ds.count - self.aggregate_functions['Mean'] = ds.mean - self.aggregate_functions['Sum'] = ds.sum - self.aggregate_functions['Min'] = ds.min - self.aggregate_functions['Max'] = ds.max - self.agg_function_name = list(self.aggregate_functions.keys())[0] - - # transfer function configuration - self.transfer_functions = OrderedDict() - self.transfer_functions['Histogram Equalization'] = 'eq_hist' - self.transfer_functions['Linear'] = 'linear' - self.transfer_functions['Log'] = 'log' - self.transfer_functions[u"\u221B - Cube Root"] = 'cbrt' - self.transfer_function = list(self.transfer_functions.values())[0] - - self.basemaps = OrderedDict() - self.basemaps['Imagery'] = ('http://server.arcgisonline.com/arcgis' - '/rest/services/World_Imagery/MapServer' - '/tile/{Z}/{Y}/{X}.png') - self.basemaps['Shaded Relief'] = ('http://services.arcgisonline.com' - '/arcgis/rest/services' - '/World_Shaded_Relief/MapServer' - '/tile/{Z}/{Y}/{X}.png') - self.basemaps['Toner'] = ('http://tile.stamen.com/toner-background' - '/{Z}/{X}/{Y}.png') - - self.labels_url = ('http://tile.stamen.com/toner-labels' - '/{Z}/{X}/{Y}.png') - - self.basemap = list(self.basemaps.values())[0] - - # dynamic image configuration - self.service_url = 'http://{host}:{port}/datashader?' - self.service_url += 'height={HEIGHT}&' - self.service_url += 'width={WIDTH}&' - self.service_url += 'select={XMIN},{YMIN},{XMAX},{YMAX}&' - self.service_url += 'cachebust={cachebust}' - - self.shader_url_vars = {} - self.shader_url_vars['host'] = 'localhost' - self.shader_url_vars['port'] = app_port - self.shader_url_vars['cachebust'] = str(uuid.uuid4()) - - # set defaults - self.load_datasets(outofcore) - - # spreading - self.spread_size = 0 - - # color ramps - default_palette = "fire" - named_palettes = {k:p for k,p in palette.items() if not '_' in k} - sorted_palettes = OrderedDict(sorted(named_palettes.items())) - self.color_ramps = odict_to_front(sorted_palettes,default_palette) - self.color_ramp = palette[default_palette] - - self.hover_layer = None - self.agg = None - - def load_config_file(self, config_path): - '''load and parse yaml config file''' - - if not path.exists(config_path): - raise IOError('Unable to find config file "{}"'.format(config_path)) - - self.config_path = path.abspath(config_path) - - with open(config_path) as f: - self.config = yaml.load(f.read()) - - # parse plots - self.axes = OrderedDict() - for p in self.config['axes']: - self.axes[p['name']] = p - self.active_axes = list(self.axes.values())[0] - - # parse initial extent - extent = self.active_axes['initial_extent'] - self.map_extent = [extent['xmin'], extent['ymin'], - extent['xmax'], extent['ymax']] - - # parse summary field - self.fields = OrderedDict() - self.colormaps = OrderedDict() - self.color_name_maps = OrderedDict() - self.glyph = OrderedDict() - self.ordinal_fields = [] - self.categorical_fields = [] - for f in self.config['summary_fields']: - self.fields[f['name']] = None if f['field'] == 'None' else f['field'] - - if 'cat_colors' in f.keys(): - self.colormaps[f['name']] = f['cat_colors'] - self.categorical_fields.append(f['field']) - self.color_name_maps[f['name']] = f['cat_names'] - - elif f['field'] != 'None': - self.ordinal_fields.append(f['field']) - - if 'glyph' in f.keys(): - self.glyph[f['field']] = f['glyph'] - - self.field = list(self.fields.values())[0] - self.field_title = list(self.fields.keys())[0] - - self.colormap = None - if self.colormaps: - colormap = self.colormaps.get(self.field_title, None) - if colormap: - self.colormap = colormap - self.colornames = self.color_name_maps[self.field_title] - - def load_datasets(self, outofcore): - data_path = self.config['file'] - objpath = self.config.get('objpath', None) - print('Loading Data from {}...'.format(data_path)) - - if not path.isabs(data_path): - config_dir = path.split(self.config_path)[0] - data_path = path.join(config_dir, data_path) - - if not path.exists(data_path): - raise IOError('Unable to find input dataset: "{}"'.format(data_path)) - - axes_fields = [] - for f in self.axes.values(): - axes_fields += [f['xaxis'], f['yaxis']] - - load_fields = [f for f in self.fields.values() if f is not None] + axes_fields - - if data_path.endswith(".csv"): - self.df = pd.read_csv(data_path, usecols=load_fields) - - # parse categorical fields - for f in self.categorical_fields: - self.df[f] = self.df[f].astype('category') - - elif data_path.endswith(".h5"): - if not objpath: - from os.path import basename, splitext - objpath = splitext(basename(data_path))[0] - self.df = pd.read_hdf(data_path, objpath) - - # parse categorical fields - for f in self.categorical_fields: - self.df[f] = self.df[f].astype('category') - - elif data_path.endswith(".parq"): - import dask.dataframe as dd - self.df = dd.io.parquet.read_parquet(data_path) - if not outofcore: - self.df = self.df.persist() - - elif data_path.endswith(".castra"): - import dask.dataframe as dd - self.df = dd.from_castra(data_path) - if not outofcore: - self.df = self.df.cache(cache=dict) - - else: - raise IOError("Unknown data file type; .csv and .castra currently supported") - - @hold - def create_aggregate(self, plot_width, plot_height, x_range, y_range, - agg_field, x_field, y_field, agg_func, glyph): - - canvas = ds.Canvas(plot_width=plot_width, - plot_height=plot_height, - x_range=x_range, - y_range=y_range) - - method = getattr(canvas, glyph) - - # handle categorical field - if agg_field in self.categorical_fields: - agg = method(self.df, x_field, y_field, ds.count_cat(agg_field)) - - # handle ordinal field - elif agg_field in self.ordinal_fields: - func = self.aggregate_functions[agg_func] - agg = method(self.df, x_field, y_field, func(agg_field)) - else: - agg = method(self.df, x_field, y_field) - - return agg - - def render_image(self): - if self.colormaps: - colormap = self.colormaps.get(self.field_title, None) - if colormap: - self.colormap = colormap - self.colornames = self.color_name_maps[self.field_title] - - pix = tf.shade(self.agg, cmap=self.color_ramp, color_key=self.colormap, how=self.transfer_function) - - if self.spread_size > 0: - pix = tf.spread(pix, px=self.spread_size) - - return pix - - def update_hover(self): - - # hover (temporarily disabled) - return - - if not self.hover_layer: - self.hover_layer = HoverLayer(field_name=self.field_title, - extent=self.map_extent, - is_categorical=self.field in self.categorical_fields, - agg=self.agg) - self.fig.renderers.append(self.hover_layer.renderer) - self.fig.add_tools(self.hover_layer.tool) - else: - self.hover_layer.is_categorical = self.field in self.categorical_fields - self.hover_layer.extent = self.map_extent - self.hover_layer.agg = self.agg - - def update_legend(self): - - # legends (temporarily disabled) - return - - if self.field in self.categorical_fields: - cat_legend = create_categorical_legend(self.colormap, aliases=self.colornames) - self.legend_side_vbox.children = [cat_legend] - self.legend_bottom_vbox.children = [] - - else: - legend_fig = create_ramp_legend(self.agg, - self.color_ramp, - how=self.transfer_function, - width=self.plot_width) - - self.legend_bottom_vbox = [legend_fig] - self.legend_side_vbox.children = [] - - -class AppView(object): - def __init__(self, app_model): - self.model = app_model - self.create_layout() - - def create_layout(self): - # create figure - self.x_range = Range1d(start=self.model.map_extent[0], - end=self.model.map_extent[2], bounds=None) - self.y_range = Range1d(start=self.model.map_extent[1], - end=self.model.map_extent[3], bounds=None) - - self.fig = Figure(tools='wheel_zoom,pan', - x_range=self.x_range, - lod_threshold=None, - plot_width=self.model.plot_width, - plot_height=self.model.plot_height, - background_fill_color='black', - y_range=self.y_range) - - self.fig.min_border_top = 0 - self.fig.min_border_bottom = 10 - self.fig.min_border_left = 0 - self.fig.min_border_right = 0 - self.fig.axis.visible = False - - self.fig.xgrid.grid_line_color = None - self.fig.ygrid.grid_line_color = None - - # add tiled basemap - self.tile_source = WMTSTileSource(url=self.model.basemap) - self.tile_renderer = TileRenderer(tile_source=self.tile_source) - self.fig.renderers.append(self.tile_renderer) - - # add datashader layer - self.image_source = ImageSource(url=self.model.service_url, - extra_url_vars=self.model.shader_url_vars) - self.image_renderer = DynamicImageRenderer(image_source=self.image_source) - self.fig.renderers.append(self.image_renderer) - - # add label layer - self.label_source = WMTSTileSource(url=self.model.labels_url) - self.label_renderer = TileRenderer(tile_source=self.label_source) - self.fig.renderers.append(self.label_renderer) - - # Add placeholder for legends (temporarily disabled) - # self.model.legend_side_vbox = Column() - # self.model.legend_bottom_vbox = Column() - - # add ui components - controls = [] - axes_select = Select(name='Axes', options=list(self.model.axes.keys())) - axes_select.on_change('value', self.on_axes_change) - controls.append(axes_select) - - self.field_select = Select(name='Field', options=list(self.model.fields.keys())) - self.field_select.on_change('value', self.on_field_change) - controls.append(self.field_select) - - self.aggregate_select = Select(name='Aggregate', options=list(self.model.aggregate_functions.keys())) - self.aggregate_select.on_change('value', self.on_aggregate_change) - controls.append(self.aggregate_select) - - transfer_select = Select(name='Transfer Function', - options=list(self.model.transfer_functions.keys())) - transfer_select.on_change('value', self.on_transfer_function_change) - controls.append(transfer_select) - - color_ramp_select = Select(name='Color Ramp', options=list(self.model.color_ramps.keys())) - color_ramp_select.on_change('value', self.on_color_ramp_change) - controls.append(color_ramp_select) - - spread_size_slider = Slider(title="Spread Size (px)", value=0, start=0, - end=10, step=1) - spread_size_slider.on_change('value', self.on_spread_size_change) - controls.append(spread_size_slider) - - # hover (temporarily disabled) - #hover_size_slider = Slider(title="Hover Size (px)", value=8, start=4, - # end=30, step=1) - #hover_size_slider.on_change('value', self.on_hover_size_change) - #controls.append(hover_size_slider) - - # legends (temporarily disabled) - # controls.append(self.model.legend_side_vbox) - - # add map components - basemap_select = Select(name='Basemap', value='Imagery', options=list(self.model.basemaps.keys())) - basemap_select.on_change('value', self.on_basemap_change) - - image_opacity_slider = Slider(title="Opacity", value=100, start=0, - end=100, step=1) - image_opacity_slider.on_change('value', self.on_image_opacity_slider_change) - - basemap_opacity_slider = Slider(title="Basemap Opacity", value=100, start=0, - end=100, step=1) - basemap_opacity_slider.on_change('value', self.on_basemap_opacity_slider_change) - - show_labels_chk = CheckboxGroup(labels=["Show Labels"], active=[0]) - show_labels_chk.on_click(self.on_labels_change) - - map_controls = [basemap_select, basemap_opacity_slider, - image_opacity_slider, show_labels_chk] - - self.controls = Column(height=600, children=controls) - self.map_controls = Row(width=self.fig.plot_width, children=map_controls) - - # legends (temporarily disabled) - self.map_area = Column(width=900, height=600, - children=[self.map_controls, self.fig]) - self.layout = Row(width=1300, height=600, - children=[self.controls, self.map_area]) - self.model.fig = self.fig - self.model.update_hover() - - def update_image(self): - self.model.shader_url_vars['cachebust'] = str(uuid.uuid4()) - self.image_renderer.image_source = ImageSource(url=self.model.service_url, - extra_url_vars=self.model.shader_url_vars) - - def on_field_change(self, attr, old, new): - self.model.field_title = new - self.model.field = self.model.fields[new] - - self.model.hover_layer.field_name = new - self.model.hover_layer.is_categorical = self.model.field in self.model.categorical_fields - self.update_image() - - if not self.model.field: - self.aggregate_select.options = [("No Aggregates Available", "")] - elif self.model.field in self.model.categorical_fields: - self.model.hover_layer.is_categorical = True - self.aggregate_select.options = [("Categorical", "count_cat")] - else: - opts = [(k, k) for k in self.model.aggregate_functions.keys()] - self.aggregate_select.options = opts - self.model.hover_layer.is_categorical = False - - def on_basemap_change(self, attr, old, new): - self.model.basemap = self.model.basemaps[new] - self.tile_renderer.tile_source = WMTSTileSource(url=self.model.basemap) - - def on_hover_size_change(self, attr, old, new): - self.model.hover_layer.size = int(new) - - def on_spread_size_change(self, attr, old, new): - self.model.spread_size = int(new) - self.update_image() - - def on_axes_change(self, attr, old, new): - self.model.active_axes = self.model.axes[new] - self.update_image() - - def on_aggregate_change(self, attr, old, new): - self.model.agg_function_name = new - self.update_image() - - def on_transfer_function_change(self, attr, old, new): - self.model.transfer_function = self.model.transfer_functions[new] - self.update_image() - - def on_color_ramp_change(self, attr, old, new): - self.model.color_ramp = self.model.color_ramps[new] - self.update_image() - - def on_image_opacity_slider_change(self, attr, old, new): - self.image_renderer.alpha = new / 100 - - def on_basemap_opacity_slider_change(self, attr, old, new): - self.tile_renderer.alpha = new / 100 - - def on_labels_change(self, new): - self.label_renderer.alpha = 1 if new else 0 - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-c', '--config', help='yaml config file (e.g. nyc_taxi.yml)', required=True) - parser.add_argument('-p', '--port', help='port number to use for communicating with server; defaults to 5000', default=5000) - parser.add_argument('-o', '--outofcore', help='use out-of-core processing if available, for datasets larger than memory', - default=False, action='store_true') - args = vars(parser.parse_args()) - - APP_PORT = args['port'] - - def add_roots(doc): - model = AppState(args['config'], args['outofcore'], APP_PORT) - view = AppView(model) - GetDataset.model = model - doc.add_root(view.layout) - - app = Application(FunctionHandler(add_roots)) - - # Start server object wired to bokeh client. Instantiating ``Server`` - # directly is used to add custom http endpoint into ``extra_patterns``. - url = 'http://localhost:{}/'.format(APP_PORT) - print('Starting server at {}...'.format(url)) - - io_loop = IOLoop.current() - - server = Server({'/': app}, io_loop=io_loop, extra_patterns=[('/datashader', GetDataset)], port=APP_PORT) - server.start() - - io_loop.add_callback(server.show, "/") - io_loop.start() diff --git a/examples/dashboard/nyc_taxi.yml b/examples/dashboard/nyc_taxi.yml deleted file mode 100644 index 964c5e4bb..000000000 --- a/examples/dashboard/nyc_taxi.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- - -file: ../data/nyc_taxi.csv - - -axes: - - name: NYC Taxi Dropoffs - xaxis: dropoff_x - yaxis: dropoff_y - initial_extent: - is_geo: true - xmin: -8240227.037 - ymin: 4974203.152 - xmax: -8231283.905 - ymax: 4979238.441 - - - name: NYC Taxi Pickups - xaxis: pickup_x - yaxis: pickup_y - initial_extent: - is_geo: true - xmin: -8240227.037 - ymin: 4974203.152 - xmax: -8231283.905 - ymax: 4979238.441 - -summary_fields: - - name: Trip Count - field: None - - - name: Passenger Count - field: passenger_count - - - name: Trip Distance - field: trip_distance - - - name: Fare ($) - field: fare_amount diff --git a/examples/dashboard/opensky.yml b/examples/dashboard/opensky.yml deleted file mode 100644 index 468960a35..000000000 --- a/examples/dashboard/opensky.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- - -file: ../data/opensky.h5 -objpath: flights - -axes: - - name: OpenSky Flight Paths - xaxis: longitude - yaxis: latitude - initial_extent: - is_geo: true - xmin: -2000000 - ymin: 4100000 - xmax: 2500000 - ymax: 7800000 - -summary_fields: - - - name: Ascending vs. Descending - field: ascending - cat_colors: - True: blue - False: red - cat_names: - True: Ascending - False: Descending - glyph: line - - - name: Counts - field: None diff --git a/examples/dashboard/osm.yml b/examples/dashboard/osm.yml deleted file mode 100644 index abd4630d0..000000000 --- a/examples/dashboard/osm.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- - -file: ../data/osm-1billion.snappy.parq - - -axes: - - name: Open Street Map Locations - xaxis: x - yaxis: y - initial_extent: - is_geo: true - xmin: -20026376.39 - ymin: -8010550 - xmax: 20026376.39 - ymax: 12015825 - -summary_fields: - - name: Count - field: None diff --git a/examples/topics/census.ipynb b/examples/topics/census.ipynb index 6e6457cd3..ed9ab0dd1 100644 --- a/examples/topics/census.ipynb +++ b/examples/topics/census.ipynb @@ -498,9 +498,9 @@ "outputs": [], "source": [ "cvs = ds.Canvas(plot_width=plot_width, plot_height=plot_height)\n", - "agg = cvs.points(df, 'easting', 'northing', ds.count_cat('race'))\n", + "aggc = cvs.points(df, 'easting', 'northing', ds.count_cat('race'))\n", "\n", - "export(tf.shade(agg.sel(race='b'), cmap=cm(Greys9,0.25), how='eq_hist'),\"USA blacks\")" + "export(tf.shade(aggc.sel(race='b'), cmap=cm(Greys9,0.25), how='eq_hist'),\"USA blacks\")" ] }, { @@ -518,7 +518,7 @@ "metadata": {}, "outputs": [], "source": [ - "agg2 = agg.where((agg.sel(race=['w', 'b', 'a', 'h']) > 0).all(dim='race')).fillna(0)\n", + "agg2 = aggc.where((aggc.sel(race=['w', 'b', 'a', 'h']) > 0).all(dim='race')).fillna(0)\n", "export(tf.shade(agg2, color_key=color_key, how='eq_hist'),\"USA all\")" ] }, @@ -537,7 +537,7 @@ "metadata": {}, "outputs": [], "source": [ - "export(tf.shade(agg.where(agg.sel(race='w') < agg.sel(race='b')).fillna(0), color_key=color_key, how='eq_hist'),\"more_blacks\")" + "export(tf.shade(aggc.where(aggc.sel(race='w') < aggc.sel(race='b')).fillna(0), color_key=color_key, how='eq_hist'),\"more_blacks\")" ] }, { diff --git a/examples/topics/param_dashboard.ipynb b/examples/topics/param_dashboard.ipynb deleted file mode 100644 index 5abc381d9..000000000 --- a/examples/topics/param_dashboard.ipynb +++ /dev/null @@ -1,200 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## NYC Taxi trips, with [Datashader](https://github.com/bokeh/datashader), [HoloViews](http://holoviews.org), [GeoViews](https://github.com/ioam/geoviews/blob/master/README.md#installation) and [ParamNB](https://anaconda.org/jbednar/paramnb)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The InteractiveImage command provided with [datashader](https://github.com/bokeh/datashader) makes it simple to make an interactive plot of a very large dataset, but very often one will want to add additional interactive controls to filter your data, select columns for plotting, etc., which is not supported by InteractiveImage. One way to do that is to use [ParamNB](https://anaconda.org/jbednar/paramnb) to instantiate some parameters and then have it run the subsequent cell whenever one of those parameters changes (via ``paramnb.Widgets(...,next_n=1)``).\n", - "\n", - "This notebook illustrates a cleaner way using a [HoloViews](http://holoviews.org) stream to connect the widgets and the plot. Requires ``conda install -c ioam/label/dev holoviews paramnb`` and installing [GeoViews](https://github.com/ioam/geoviews/blob/master/README.md#installation) (which is only important for the map tile support).\n", - "\n", - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import holoviews as hv\n", - "import geoviews as gv\n", - "import param, paramnb\n", - "import pandas as pd\n", - "\n", - "from colorcet import cm\n", - "from bokeh.models import WMTSTileSource\n", - "from holoviews.operation.datashader import datashade\n", - "from holoviews.streams import RangeXY\n", - "\n", - "hv.notebook_extension('bokeh')\n", - "\n", - "%time df = pd.read_csv('../data/nyc_taxi.csv', usecols = ['passenger_count', \\\n", - " 'pickup_x', 'pickup_y', 'dropoff_x', 'dropoff_y'])\n", - "df.tail()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Interactive plot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tiles = gv.WMTS(WMTSTileSource(url='https://server.arcgisonline.com/ArcGIS/rest/services/'\n", - " 'World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg'))\n", - "tile_options = dict(width=800,height=475,xaxis=None,yaxis=None,bgcolor='black',show_grid=False) \n", - "\n", - "passenger_counts = sorted(df.passenger_count.unique().tolist())\n", - "\n", - "class Options(hv.streams.Stream):\n", - " alpha = param.Magnitude(default=0.75, doc=\"Alpha value for the map opacity\")\n", - " colormap = param.ObjectSelector(default=cm[\"fire\"], objects=cm.values())\n", - " plot = param.ObjectSelector(default=\"pickup\", objects=[\"pickup\",\"dropoff\"])\n", - " passengers = param.ObjectSelector(default=1, objects=passenger_counts)\n", - " \n", - " def make_view(self, x_range=None, y_range=None, **kwargs):\n", - " map_tiles = tiles(style=dict(alpha=self.alpha), plot=tile_options) \n", - "\n", - " df_filt = df[df.passenger_count==self.passengers]\n", - " points = hv.Points(gv.Dataset(df_filt, kdims=[self.plot+'_x', self.plot+'_y'], vdims=[]))\n", - " taxi_trips = datashade(points, width=800, height=475, x_sampling=1, y_sampling=1, \n", - " cmap=self.colormap, element_type=gv.Image,\n", - " dynamic=False, x_range=x_range, y_range=y_range)\n", - " \n", - " return map_tiles * taxi_trips\n", - "\n", - "selector = Options(name=\"\")\n", - "paramnb.Widgets(selector, callback=selector.update)\n", - "hv.DynamicMap(selector.make_view, kdims=[], streams=[selector, RangeXY()])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you are viewing a static copy of this page through Anaconda Cloud, the interactive controls will not be usable, but you can download a copy of this notebook and run it through Jupyter notebook for the interactive version. You can also view the controls and the image as a deployable dashboard using [Jupyter Dashboards](https://github.com/jupyter/dashboards), which can be installed separately using ``conda install -c conda-forge jupyter_dashboards``. Jupyter Dashboards is a Jupyter extension that lets you choose which cells to publish to a dashboard layout, with the result like this [screenshot](../assets/images/nyc_taxi-paramnb.png) that can be deployed as a standalone server." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "
\n", - "
\n", - "
\n", - "
\n", - "
\n", - "
\n", - "
\n", - "
\n", - "\n", - "# How does it work?\n", - "\n", - "In this example, we define a class that declares certain parameters whose value a user might wish to change, with defaults, ranges, and documentation if desired. We then add a method ``make_view`` that constructs a HoloViews object using the values of these parameters, and we instantiate our new class as an object:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "selector2 = Options(name=\"\")\n", - "selector2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we could change the values of the parameters manually if we wanted:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "selector2.alpha=0.2\n", - "selector2.plot='dropoff'\n", - "selector2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "But we'd rather have some interactive controls, so let's make some ipywidgets for the parameters, using ParamNB:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "paramnb.Widgets(selector2, callback=selector2.update)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we have widgets that change the corresponding parameters on `selector2`. Plus, whenever those values change, `Widgets` will call `selector2.update()`. But at this point, that method call does nothing, because we haven't registered anything with `selector2` to watch for such events.\n", - "\n", - "So, now we need to make something that could register for such events and generate the corresponding plot. Just getting a plot is simple, using the method we defined already:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%output size=40\n", - "selector2.make_view()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This makes a HoloViews \"view\" object, which is then automatically turned into a Bokeh plot in a Jupyter notebook. But this plot won't dynamically update when the widgets are adjusted; it just respects whatever parameter values `selector2` had when `make_view()` was invoked. To make the connection between the HoloViews object and the parameter widgets, we need one last line:\n", - "\n", - "```\n", - "hv.DynamicMap(selector2.make_view, kdims=[], streams=[selector, RangeXY()])\n", - "```\n", - "\n", - "Here, a `DynamicMap` is a HoloViews wrapper that accepts a callback (`selector2.make_view` in this case) that it will call when a new HoloViews object is needed. Finally, the `streams` argument subscribes this DynamicMap to two streams, requesting that those streams trigger an update on the DynamicMap when the widget values change (via the `selector` stream) and when the Bokeh zoom range changes (via the `RangeXY()` stream). In either case, the DynamicMap will then execute the provided command to generate a new view, and ultimately the plot on your screen will update. Success!\n", - "\n", - "[The `kdims` argument is unused here, because all of the dynamism here is provided by widgets and range changes, but in general we could also declare some \"key dimensions\" (e.g. \"date\"), and then there would be additional sliders created automatically to select values along those dimensions, which would then also trigger a call to `selector2.make_view`.]\n", - "\n", - "While the approach outlined here is more complicated than having a single callback (which is of course also supported directly by paramnb), what it achieves is to be able to very flexibly create and subscribe to a wide variety of different event streams, each individually very simple and straightforward to create but which can be combined in arbitrary ways to create complex interactive behavior. See the new HoloViews Streams tutorials for more information, which explains how to watch for selection events, click events, keypress events, etc., all of which can be used to provide interactive behavior for Bokeh plots beyond what paramnb's widgets support." - ] - } - ], - "metadata": { - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/user_guide/index.ipynb b/examples/user_guide/index.ipynb index 5dbfd3956..b7e9b9366 100644 --- a/examples/user_guide/index.ipynb +++ b/examples/user_guide/index.ipynb @@ -13,9 +13,8 @@ "Contents:\n", "\n", "- [1. Plotting Pitfalls](1_Plotting_Pitfalls.ipynb) \n", - " Explains why Datashader is important for avoiding various pitfalls\n", - " encountered when using plotting techniques designed for small\n", - " datasets with big data.\n", + " Explains how Datashader avoids pitfalls encountered when plotting big datasets\n", + " using techniques designed for small ones.\n", "\n", "- [2. Points](../topics/nyc_taxi.ipynb) \n", " (Under construction; meanwhile points to the [nyc_taxi](../topics/nyc_taxi.ipynb) notebook.)\n", diff --git a/tox.ini b/tox.ini index d4357ab53..27f44af0a 100644 --- a/tox.ini +++ b/tox.ini @@ -76,6 +76,7 @@ nbsmoke_skip_run = .*nyc_taxi-nongeo\.ipynb$ .*getting_started/1_Introduction\.ipynb$ .*getting_started/3_Interactivity\.ipynb$ .*tiling.ipynb$ + .*dashboard.ipynb$ .*getting_started/8_Geography\.ipynb$ [flake8]