Spaces:
Running
Running
Upload 52 files
Browse files- hvplot_docs/01-Annotating_Data.md +135 -0
- hvplot_docs/05-Dimensioned_Containers.md +162 -0
- hvplot_docs/06-Building_Composite_Objects.md +191 -0
- hvplot_docs/07-Live_Data.md +354 -0
- hvplot_docs/08-Tabular_Datasets.md +297 -0
- hvplot_docs/09-Gridded_Datasets.md +294 -0
- hvplot_docs/10-Indexing_and_Selecting_Data.md +268 -0
- hvplot_docs/11-Transforming_Elements.md +336 -0
- hvplot_docs/12-Responding_to_Events.md +511 -0
- hvplot_docs/13-Custom_Interactivity.md +222 -0
- hvplot_docs/14-Data_Pipelines.md +123 -0
- hvplot_docs/15-Large_Data.md +576 -0
- hvplot_docs/16-Streaming_Data.md +338 -0
- hvplot_docs/17-Dashboards.md +159 -0
- hvplot_docs/Annotators.md +229 -0
- hvplot_docs/Colormaps.md +234 -0
- hvplot_docs/Continuous_Coordinates.md +201 -0
- hvplot_docs/Customization.md +185 -0
- hvplot_docs/Customizing_Plots.md +439 -0
- hvplot_docs/Deploying_Bokeh_Apps.md +553 -0
- hvplot_docs/Explorer.md +123 -0
- hvplot_docs/Exporting_and_Archiving.md +246 -0
- hvplot_docs/Geographic_Data.md +152 -0
- hvplot_docs/Geometry_Data.md +134 -0
- hvplot_docs/Gridded_Data.md +129 -0
- hvplot_docs/Installing_and_Configuring.md +107 -0
- hvplot_docs/Integrations.md +251 -0
- hvplot_docs/Interactive.md +238 -0
- hvplot_docs/Introduction.md +115 -0
- hvplot_docs/Linked_Brushing.md +0 -0
- hvplot_docs/Linking_Plots.md +159 -0
- hvplot_docs/NetworkX.md +552 -0
- hvplot_docs/Network_Graphs.md +249 -0
- hvplot_docs/Notebook_Magics.md +156 -0
- hvplot_docs/Pandas_API.md +628 -0
- hvplot_docs/Plots_and_Renderers.md +343 -0
- hvplot_docs/Plotting.md +325 -0
- hvplot_docs/Plotting_Extensions.md +109 -0
- hvplot_docs/Plotting_with_Bokeh.md +549 -0
- hvplot_docs/Plotting_with_Matplotlib.md +341 -0
- hvplot_docs/Plotting_with_Plotly.md +340 -0
- hvplot_docs/Statistical_Plots.md +68 -0
- hvplot_docs/Streaming.md +139 -0
- hvplot_docs/Subplots.md +70 -0
- hvplot_docs/Timeseries_Data.md +103 -0
- hvplot_docs/Viewing.md +90 -0
- hvplot_docs/Widgets.md +92 -0
- hvplot_docs/plot.html +66 -0
hvplot_docs/01-Annotating_Data.md
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Annotating Your Data
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import holoviews as hv
|
6 |
+
hv.extension('bokeh')
|
7 |
+
```
|
8 |
+
|
9 |
+
As introduced in the [Getting Started guide](../getting_started/1-Introduction.ipynb), HoloViews relies heavily on semantic *annotations*, i.e., metadata you declare that lets HoloViews interpret what your data represents. With these annotations, HoloViews can perform complex tasks like visualization automatically.
|
10 |
+
|
11 |
+
There are three main kinds of annotation that can be associated with each element:
|
12 |
+
1. **Type**, used to declare the sort of data you have, which is required before it can be visualized,
|
13 |
+
2. **Dimensions**, used to specify the abstract space in which the data resides, allowing axis labeling and indexing, and
|
14 |
+
3. **Group/Label**, used to declare a meaningful category and human-readable description of the element, allowing plot labeling and selecting related sets of elements.
|
15 |
+
|
16 |
+
This user guide explains each of these three types of annotation, describing why you would need or want to use them.
|
17 |
+
|
18 |
+
## 1. Specifying element type
|
19 |
+
|
20 |
+
Basic Python data structures like dataframes, arrays, lists, and dictionaries can be used to represent an infinite variety of different types of data, and thus they cannot be visualized as any particular type of graphical representation without some additional information from the user that says what sort of data it is meant to be. The user can declare this information by selecting a suitable HoloViews element type from the many different ones available (see the [Reference Gallery](http://holoviews.org/reference/index.html)).
|
21 |
+
|
22 |
+
For instance, let's say you have two lists of numbers:
|
23 |
+
|
24 |
+
|
25 |
+
```python
|
26 |
+
xs = range(-10,11)
|
27 |
+
ys = [100-x**2 for x in xs]
|
28 |
+
```
|
29 |
+
|
30 |
+
As far as Python is concerned, ``xs`` and ``ys`` are just two arbitrary lists, which could represent nearly anything imaginable. But we as humans can see that each of the ``ys`` is a value computed from one of the ``xs`` by evaluating the function $y=100-x^2$. We can convey some of that information to HoloViews by choosing a ``Curve`` element type, which is a convenient shorthand for "a discrete set of real-valued samples from a continuous function of one real-valued variable":
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
curve = hv.Curve((xs, ys))
|
35 |
+
curve
|
36 |
+
```
|
37 |
+
|
38 |
+
As you can see, declaring the element type is the only *required* bit of annotation, instantly making your data visualizable. However, this initial visualization relies on various defaults that may not be appropriate for your data, and you can override these defaults by declaring additional annotations as described below.
|
39 |
+
|
40 |
+
## 2. Specifying element dimensionality
|
41 |
+
|
42 |
+
Each element type can process a certain number and type of *dimensions*, i.e., ways in which the data can vary. For instance, the ``Curve`` object above has two dimensions, $x$ and $y$. If you look at how we generated the data, you can see that these two dimensions are semantically different -- we chose an arbitrary set of values for the ``xs``, and then calculated a corresponding value to make each of the ``ys``. In mathematical terms, $x$ is thus an independent variable (selected by the creator of the data), and $y$ is a dependent variable (typically measured or calculated from the independent variable(s)).
|
43 |
+
|
44 |
+
HoloViews elements call these two different types of variables *key dimensions* (``kdims``) and *value dimensions* (``vdims``). The *key dimensions* are the dimensions you can index *by* to get the values corresponding to the *value* dimensions. You can learn more about indexing data in the later [Indexing and Selecting Data](./10-Indexing_and_Selecting_Data.ipynb) user guide.
|
45 |
+
|
46 |
+
Different elements have different numbers of required key dimensions and value dimensions. For instance, a ``Curve`` always has one key dimension and one value dimension. As we did not explicitly specify anything regarding dimensions when declaring the curve above, the ``kdims`` and ``vidms`` use their default names 'x' and 'y':
|
47 |
+
|
48 |
+
|
49 |
+
```python
|
50 |
+
"Object 'curve' has kdims {kdims} and vdims {vdims}".format(kdims=curve.kdims, vdims=curve.vdims)
|
51 |
+
```
|
52 |
+
|
53 |
+
The easiest way to override the default dimension names is to provide strings for the dimensions, where the second argument in the Element constructor will always be the ``kdims``, and the third will always be the ``vdims``:
|
54 |
+
|
55 |
+
|
56 |
+
```python
|
57 |
+
trajectory = hv.Curve((xs, ys), 'distance', 'height')
|
58 |
+
trajectory
|
59 |
+
```
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
"Object 'trajectory' has kdims {kdims} and vdims {vdims} ".format(kdims=trajectory.kdims, vdims=trajectory.vdims)
|
64 |
+
```
|
65 |
+
|
66 |
+
We can see that the strings we provided have been 'promoted' to dimension objects. The ``kdims`` and ``vdims`` *always* contain instances of the ``Dimension`` class, described in the following section. Here, the immediate effect is to use the new names for the displayed axis labels.
|
67 |
+
|
68 |
+
### Dimension parameters
|
69 |
+
|
70 |
+
``Dimensions`` are not just names, they are rich objects with numerous parameters that can be used to describe the space in which the data resides. Only two of these are considered *core* parameters that uniquely identify the dimension object; the rest are auxiliary metadata. The most important parameters are:
|
71 |
+
|
72 |
+
<br>
|
73 |
+
<dl class="dl-horizontal">
|
74 |
+
<dt>name</dt><dd>(core) A concise name for the dimension, which for convenient usage as a keyword argument should usually be a legal Python identifier.</dd>
|
75 |
+
<dt>label <dd>(core) A optional longer description of the dimension, which is convenient if you want the displayed label to contain arbitrary spaces, symbols, or unicode.</dd>
|
76 |
+
<dt>range <dd>The minimum and maximum allowable values for the dimension, for error checking and generating widgets when needed.</dd>
|
77 |
+
<dt>soft_range <dd>Suggested minimum and maximum values within the allowed range, used to specify a useful portion of the range for widgets and animations.</dd>
|
78 |
+
<dt>step <dd>Suggested interval for sampling a continuous range, if needed for a widget or animation.</dd>
|
79 |
+
<dt>unit <dd>The name of the unit to be associated with the dimension, if any, for labelling.</dd>
|
80 |
+
<dt>values <dd>Explicit list of allowed dimension values, for error checking, widgets, and animations.</dd>
|
81 |
+
</dl>
|
82 |
+
|
83 |
+
|
84 |
+
For the full list of parameters, you can call ``hv.help(hv.Dimension)``.
|
85 |
+
|
86 |
+
Similar to how you can just use a string if all you want to specify is the name, you can provide a ``(name,label)`` tuple if you want to specify the ``name`` and the ``label`` to ``kdims`` and ``vdims`` without building an explicit ``Dimension``:
|
87 |
+
|
88 |
+
|
89 |
+
```python
|
90 |
+
wo_unit = hv.Curve((xs, ys),
|
91 |
+
('distance','Horizontal distance'),
|
92 |
+
('height','Height above sea level'))
|
93 |
+
|
94 |
+
distance = hv.Dimension('distance', label='Horizontal distance', unit='m')
|
95 |
+
height = hv.Dimension(('height','Height above sea level'), unit='m')
|
96 |
+
with_unit = hv.Curve((xs, ys), distance, height)
|
97 |
+
|
98 |
+
# (using + to compose elements is described in the next guide)
|
99 |
+
wo_unit + with_unit
|
100 |
+
```
|
101 |
+
|
102 |
+
Note that after supplying the longer labels, you can still use the short name to specify the dimension in keyword arguments. For instance, try using ``with_unit.select(distance=(5,8))`` in the cell above.
|
103 |
+
|
104 |
+
### Setting properties with redim
|
105 |
+
|
106 |
+
Declaring dimension objects with appropriate parameters can be awkward and verbose if you only want to set a few specific parameters. You can often avoid declaring explicit dimension objects using the ``redim`` method, which returns a *clone* of the element: the same data, wrapped in a new instance of the same element type with the new dimension settings.
|
107 |
+
|
108 |
+
Let's use ``redim`` to swap out the 'height' dimension for an 'altitude' dimension:
|
109 |
+
|
110 |
+
|
111 |
+
```python
|
112 |
+
renamed_height = trajectory.redim(height='altitude')
|
113 |
+
renamed_height
|
114 |
+
```
|
115 |
+
|
116 |
+
The ``redim`` "method" is actually a utility that can be used to set any of the dimension parameters, such as the label, unit, range, or values. For instance, the label can be updated on an existing object by specifying the dimension name and then the new value for that parameter:
|
117 |
+
|
118 |
+
|
119 |
+
```python
|
120 |
+
renamed_height.redim.label(altitude='Altitude above sea-level', distance='Horizontal distance')
|
121 |
+
```
|
122 |
+
|
123 |
+
## 3. Organizing your elements with groups and labels
|
124 |
+
|
125 |
+
A complex visualization you build with HoloViews may include many instances of the same element type, each built from different bits of data and potentially representing categorically distinct types of information to you. To help you keep track of these distinctions when you need to, HoloViews provides a ``group`` parameter you can use to declare semantically distinct categories for elements, and a ``label`` parameter you can use to identify which specific item the element represents within that category:
|
126 |
+
|
127 |
+
|
128 |
+
```python
|
129 |
+
low_ys = [25-(0.5*el)**2 for el in xs]
|
130 |
+
shallow = hv.Curve((xs, low_ys), group='Trajectory', label='Shallow')
|
131 |
+
medium = hv.Curve((xs, ys), group='Trajectory', label='Medium')
|
132 |
+
shallow + medium
|
133 |
+
```
|
134 |
+
|
135 |
+
As you can see, the ``group`` and ``label`` information will be used to generate sensible titles, here indicating that both sets of data represent trajectories, and that there are two different specific trajectories being shown. Once the group and/or label have been specified, they can be used for [Applying Customization](./03-Applying_Customizations.ipynb) (e.g. to make all trajectories have the same line width and style, or to customize one particular plot out of many of the same type). The group and label are also used for indexing, as we will see in the following [Composing_Elements](./02-Composing_Elements.ipynb) guide.
|
hvplot_docs/05-Dimensioned_Containers.md
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dimensioned Containers
|
2 |
+
|
3 |
+
So far we've seen how to [wrap data in elements](./01-Annotating_Data.ipynb) and [compose](./02-Composing_Elements.ipynb) those Elements into Overlays and Layout. In this guide will see how we can use containers to hold Elements and declare parameter spaces to explore multi-dimensional parameter spaces visually. These containers allow faceting your data by one or more variables and exploring the resulting parameter space with widgets, positioning plots on a grid or simply laying them out consecutively. Here we will introduce the ``HoloMap``, ``NdOverlay``, ``NdLayout`` and ``GridSpace``, which make all this possible.
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import numpy as np
|
8 |
+
import holoviews as hv
|
9 |
+
|
10 |
+
from holoviews import opts
|
11 |
+
|
12 |
+
hv.extension('bokeh')
|
13 |
+
|
14 |
+
opts.defaults(opts.Curve(line_width=1))
|
15 |
+
```
|
16 |
+
|
17 |
+
### Declaring n-dimensional collections
|
18 |
+
|
19 |
+
Python users will be familiar with dictionaries as a way to collect data together in a conveniently accessible manner. Unlike NumPy arrays, dictionaries are sparse and do not have to be declared with a fixed size. The dimensioned types are therefore closely modeled on dictionaries but support n-dimensional keys.
|
20 |
+
|
21 |
+
Therefore they allow you to express a mapping between a multi-variable key and other viewable objects, letting you structure your multi-dimensional data to facilitate exploration. The key here can represent anything from an ID, a filename, a parameter controlling some custom processing to some category representing a subset of your data.
|
22 |
+
|
23 |
+
As a simple example we will use a function which varies with multiple parameters, in this case a function which generates a simple frequency modulation signal, with two main parameters a carrier frequency and a modulation frequency (see [Wikipedia](https://en.wikipedia.org/wiki/Frequency_modulation)). All we have to know here is that the function generates a ``Curve`` based on the parameters that we give it.
|
24 |
+
|
25 |
+
|
26 |
+
```python
|
27 |
+
def fm_modulation(f_carrier=220, f_mod=220, mod_index=1, length=0.1, sampleRate=2000):
|
28 |
+
sampleInc = 1.0/sampleRate
|
29 |
+
x = np.arange(0,length, sampleInc)
|
30 |
+
y = np.sin(2*np.pi*f_carrier*x + mod_index*np.sin(2*np.pi*f_mod*x))
|
31 |
+
return hv.Curve((x, y), 'Time', 'Amplitude')
|
32 |
+
```
|
33 |
+
|
34 |
+
Next we have to declare the parameter space we want to explore. Combinatorial parameter spaces can quickly get very large so here we will declare just 5 carrier frequencies and 5 modulation frequencies. If we have many more parameters or a larger set of values we could instead use a ``DynamicMap``, which does lazy evaluation and is introduced in the next section on [Live Data](./07-Live_Data.ipynb).
|
35 |
+
|
36 |
+
However when working with relatively small datasets a ``HoloMap`` is preferred as ``HoloMap``s can be exported to static HTML. To declare the data we use a dictionary comprehension to compute an FM modulation curve for each combination of carrier and modulation frequencies and then pass that dictionary to a newly declared ``HoloMap``, which declares our two parameters as key dimensions. If we want to customize the starting value of the widgets a ``default`` value may be supplied on the ``Dimension`` objects:
|
37 |
+
|
38 |
+
|
39 |
+
```python
|
40 |
+
f_carrier = np.linspace(20, 60, 3)
|
41 |
+
f_mod = np.linspace(20, 100, 5)
|
42 |
+
|
43 |
+
curve_dict = {(fc, fm): fm_modulation(fc, fm) for fc in f_carrier for fm in f_mod}
|
44 |
+
|
45 |
+
kdims = [hv.Dimension(('f_carrier', 'Carrier frequency'), default=40),
|
46 |
+
hv.Dimension(('f_mod', 'Modulation frequency'), default=60)]
|
47 |
+
holomap = hv.HoloMap(curve_dict, kdims=kdims)
|
48 |
+
holomap.opts(opts.Curve(width=600))
|
49 |
+
```
|
50 |
+
|
51 |
+
Any numeric key dimension values will automatically generate sliders while any non-numeric value will present you with a dropdown widget to select between the values. A HoloMap is therefore an easy way to quickly explore a parameter space. Since only one ``Curve`` is on the screen at the same time it is difficult to compare the data, we can however easily facet the data in other ways.
|
52 |
+
|
53 |
+
## Casting between n-dimensional containers
|
54 |
+
|
55 |
+
Exploring a parameter space using widgets is one of the most flexible approaches but as we noted above it also makes it difficult to compare between different parameter values. HoloViews therefore supplies several container objects, which behave similarly to a ``HoloMap`` but have a different visual representation:
|
56 |
+
|
57 |
+
* NdOverlay - An n-dimensional container which overlays the elements
|
58 |
+
* NdLayout - An n-dimensional container which displays the data in separate plot axes and adds titles for each value
|
59 |
+
* GridSpace - A 1D or 2D container which lays out up to two dimensions on a grid.
|
60 |
+
|
61 |
+
Since all these classes share the same baseclass we can trivially cast between them, e.g. we can cast the ``HoloMap`` to a ``GridSpace``.
|
62 |
+
|
63 |
+
|
64 |
+
```python
|
65 |
+
grid = hv.GridSpace(holomap)
|
66 |
+
grid.opts(
|
67 |
+
opts.GridSpace(plot_size=75),
|
68 |
+
opts.Curve(width=100))
|
69 |
+
```
|
70 |
+
|
71 |
+
Similarly we can select just a few values and lay the data out in an ``NdLayout``:
|
72 |
+
|
73 |
+
|
74 |
+
```python
|
75 |
+
ndlayout = hv.NdLayout(grid[20, 20:81])
|
76 |
+
ndlayout.opts(opts.Curve(width=500, height=200)).cols(2)
|
77 |
+
```
|
78 |
+
|
79 |
+
## Faceting by dimension
|
80 |
+
|
81 |
+
Casting between container types as we did above allows us to facet all dimensions but often it is more desirable to facet a specific dimension in some way. We may for example want to overlay the carrier frequencies, while still having a slider vary the modulation frequencies. For this purpose ``HoloMap`` and ``DynamicMap`` have ``.overlay``, ``.grid`` and ``.layout`` methods, which accept one or more dimensions as input, e.g. here we overlay the carrier frequencies:
|
82 |
+
|
83 |
+
|
84 |
+
```python
|
85 |
+
holomap.overlay('f_carrier').opts(width=600)
|
86 |
+
```
|
87 |
+
|
88 |
+
We can chain these faceting operations but we have to be careful about the order. We should always ``overlay`` first and only then use ``.grid`` or ``.layout`` . To better understand why that is, look at the [Building Composite objects](./06-Building_Composite_Objects.ipynb) guide, for now it suffices to say that objects are built to nest in a specific order. Here we will ``overlay`` the carrier frequencies and ``grid`` the modulation frequency:
|
89 |
+
|
90 |
+
|
91 |
+
```python
|
92 |
+
gridspace = holomap.overlay('f_carrier').grid('f_mod')
|
93 |
+
gridspace.opts(opts.Curve(width=200,height=200))
|
94 |
+
```
|
95 |
+
|
96 |
+
This ability to facet data in different ways becomes incredibly useful when working with [tabular](./08-Tabular_Datasets.ipynb) and [gridded](./09-Gridded_Datasets.ipynb) datasets, which vary by multiple dimensions. By faceting the data by one or more dimensions and utilizing the wide range of elements we can quickly explore huge and complex datasets particularly when working with using ``DynamicMap``, which allows you to define dynamic and lazy [data processing pipelines](./14-Data_Pipelines.ipynb), which visualize themselves and allow you to build complex interactive dashboards.
|
97 |
+
|
98 |
+
## Methods
|
99 |
+
|
100 |
+
### Selecting by value
|
101 |
+
|
102 |
+
Just like ``Elements`` dimensioned containers can be sliced and indexed by value using the regular indexing syntax and the ``select`` method. As a simple example we can index the carrier frequency 20 and modulation frequency 40 using both the indexing and select syntax:
|
103 |
+
|
104 |
+
|
105 |
+
```python
|
106 |
+
layout = holomap[20, 40] + holomap.select(f_carrier=20, f_mod=40)
|
107 |
+
layout.opts(opts.Curve(width=400))
|
108 |
+
```
|
109 |
+
|
110 |
+
For more detail on indexing both Elements and containers see the [Indexing and Selecting user guide](./10-Indexing_and_Selecting_Data.ipynb).
|
111 |
+
|
112 |
+
### Collapsing an n-dimensional container
|
113 |
+
|
114 |
+
If we inspect one of the containers we created in the examples above we can clearly see the structure and the dimensionality of each container and the underlying ``Curve`` elements:
|
115 |
+
|
116 |
+
|
117 |
+
```python
|
118 |
+
print(grid)
|
119 |
+
```
|
120 |
+
|
121 |
+
Since this simply represents a multi-dimensional parameter space we can collapse this datastructure into a single `Dataset` containing columns for each of the dimensions we have declared:
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
ds = grid.collapse()
|
126 |
+
ds.data.head()
|
127 |
+
```
|
128 |
+
|
129 |
+
A ``Dataset`` is an element type which does not itself have a visual representation and may contain any type of data supported by HoloViews interfaces.
|
130 |
+
|
131 |
+
### Reindexing containers
|
132 |
+
|
133 |
+
Something we might want to do quite frequently is to change the order of dimensions in a dimensioned container. Let us inspect the key dimensions on our HoloMap from above:
|
134 |
+
|
135 |
+
|
136 |
+
```python
|
137 |
+
holomap.kdims
|
138 |
+
```
|
139 |
+
|
140 |
+
Using the ``reindex`` method we can easily change the order of dimensions:
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
reindexed = holomap.reindex(['f_mod', 'f_carrier'])
|
145 |
+
reindexed.kdims
|
146 |
+
```
|
147 |
+
|
148 |
+
### Add dimensions to a container
|
149 |
+
|
150 |
+
Another common operation is adding a new dimension to a HoloMap, which can be useful when you want to merge two HoloMaps which vary by yet another dimension. The ``add_dimension`` method takes the new dimension name, the position in the list of key dimensions and the actual value as arguments:
|
151 |
+
|
152 |
+
|
153 |
+
```python
|
154 |
+
new_holomap = holomap.add_dimension('New dimension', dim_pos=0, dim_val=1)
|
155 |
+
new_holomap.kdims
|
156 |
+
```
|
157 |
+
|
158 |
+
As you can see the 'New dimension' was added at position zero.
|
159 |
+
|
160 |
+
## Onwards
|
161 |
+
|
162 |
+
As we have seen Dimensioned containers make it easy to explore multi-dimensional datasets by mapping the dimensions onto visual layers (`NdOverlay`), 2D grid axes (`GridSpace`), widgets (`HoloMap`) or an arbitrary layout (`NdLayout`). In the [next section](06-Building_Composite_Objects.ipynb) we will discover how to construct composite objects of heterogeneous data.
|
hvplot_docs/06-Building_Composite_Objects.md
ADDED
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Building Composite Objects
|
2 |
+
|
3 |
+
The [reference gallery](http://holoviews.org/reference/index.html) shows examples of each of the container types in HoloViews. As you work through this guide, it will be useful to look at the description of each type there.
|
4 |
+
|
5 |
+
This guide shows you how to combine the various container types, in order to build data structures that can contain all of the data that you want to visualize or analyze, in an extremely flexible way. For instance, you may have a large set of measurements of different types of data (numerical, image, textual notations, etc.) from different experiments done on different days, with various different parameter values associated with each one. HoloViews can store all of this data together, which will allow you to select just the right bit of data "on the fly" for any particular analysis or visualization, by indexing, slicing, selecting, and sampling in this data structure.
|
6 |
+
|
7 |
+
## Nesting hierarchy <a id='NestingHierarchy'></a>
|
8 |
+
|
9 |
+
To illustrate the full functionality provided, we will create an example of the maximally nested object structure currently possible with HoloViews:
|
10 |
+
|
11 |
+
|
12 |
+
```python
|
13 |
+
import numpy as np
|
14 |
+
import holoviews as hv
|
15 |
+
hv.extension('bokeh')
|
16 |
+
np.random.seed(10)
|
17 |
+
|
18 |
+
def sine_curve(phase, freq, amp, power, samples=102):
|
19 |
+
xvals = [0.1* i for i in range(samples)]
|
20 |
+
return [(x, amp*np.sin(phase+freq*x)**power) for x in xvals]
|
21 |
+
|
22 |
+
phases = [0, np.pi/2, np.pi, 3*np.pi/2]
|
23 |
+
powers = [1,2,3]
|
24 |
+
amplitudes = [0.5,0.75, 1.0]
|
25 |
+
frequencies = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75]
|
26 |
+
|
27 |
+
|
28 |
+
gridspace = hv.GridSpace(kdims=['Amplitude', 'Power'], group='Parameters', label='Sines')
|
29 |
+
|
30 |
+
for power in powers:
|
31 |
+
for amplitude in amplitudes:
|
32 |
+
holomap = hv.HoloMap(kdims='Frequency')
|
33 |
+
for frequency in frequencies:
|
34 |
+
sines = {phase : hv.Curve(sine_curve(phase, frequency, amplitude, power))
|
35 |
+
for phase in phases}
|
36 |
+
ndoverlay = hv.NdOverlay(sines, kdims='Phase').relabel(group='Phases',
|
37 |
+
label='Sines', depth=1)
|
38 |
+
overlay = ndoverlay * hv.Points([(i,0) for i in range(0,10)], group='Markers', label='Dots')
|
39 |
+
holomap[frequency] = overlay
|
40 |
+
gridspace[amplitude, power] = holomap
|
41 |
+
|
42 |
+
penguins = hv.RGB.load_image('../reference/elements/assets/penguins.png').relabel(group="Family", label="Penguin")
|
43 |
+
|
44 |
+
layout = gridspace + penguins.opts(axiswise=True)
|
45 |
+
```
|
46 |
+
|
47 |
+
This code produces what looks like a relatively simple animation of two side-by-side figures, but is actually a deeply nested data structure:
|
48 |
+
|
49 |
+
|
50 |
+
```python
|
51 |
+
layout
|
52 |
+
```
|
53 |
+
|
54 |
+
To help us understand this structure, here is a schematic for us to refer to as we unpack this object, level by level:
|
55 |
+
|
56 |
+
<center><img src="https://assets.holoviews.org/nesting-diagram.png"></center>
|
57 |
+
|
58 |
+
Everything that is *displayable* in HoloViews has this same basic hierarchical structure, although any of the levels can be omitted in simpler cases, and many different Element types (not containers) can be substituted for any other.
|
59 |
+
|
60 |
+
Since HoloViews 1.3.0, you are allowed to build data-structures that violate this hierarchy (e.g., you can put ``Layout`` objects into ``HoloMaps``) but the resulting object cannot be displayed. Instead, you will be prompted with a message to call the ``collate`` method. Using the ``collate`` method will allow you to generate the appropriate object that correctly obeys the hierarchy shown above, so that it can be displayed.
|
61 |
+
|
62 |
+
As shown in the diagram, there are three different types of container involved:
|
63 |
+
|
64 |
+
- Basic Element: elementary HoloViews object containing raw data in an external format like Numpy or pandas.
|
65 |
+
- Homogeneous container (UniformNdMapping): collections of Elements or other HoloViews components that are all the same type. These are indexed using array-style key access with values sorted along some dimension(s), e.g. ``[0.50]`` or ``["a",7.6]``.
|
66 |
+
- Heterogeneous container (AttrTree): collections of data of different types, e.g. different types of Element. These are accessed by categories using attributes, e.g. ``.Parameters.Sines``, which does not assume any ordering of a dimension.
|
67 |
+
|
68 |
+
We will now go through each of the containers of these different types, at each level.
|
69 |
+
|
70 |
+
### ``Layout`` Level
|
71 |
+
|
72 |
+
Above, we have already viewed the highest level of our data structure as a Layout. Here is the representation of the entire Layout object, which reflects all the levels shown in the diagram:
|
73 |
+
|
74 |
+
|
75 |
+
```python
|
76 |
+
print(layout)
|
77 |
+
```
|
78 |
+
|
79 |
+
In the examples below, we will unpack this data structure using attribute access (explained in the [Annotating Data](./01-Annotating_Data.ipynb) user guide) as well as indexing and slicing (explained in the [Indexing and Selecting Data](./10-Indexing_and_Selecting_Data.ipynb) user guide).
|
80 |
+
|
81 |
+
### ``GridSpace`` Level
|
82 |
+
|
83 |
+
Elements within a ``Layout``, such as the ``GridSpace`` in this example, are reached via attribute access:
|
84 |
+
|
85 |
+
|
86 |
+
```python
|
87 |
+
layout.Parameters.Sines
|
88 |
+
```
|
89 |
+
|
90 |
+
### ``HoloMap`` Level
|
91 |
+
|
92 |
+
This ``GridSpace`` consists of nine ``HoloMap``s arranged in a two-dimensional space. Let's now select one of these ``HoloMap`` objects by indexing to retrieve the one at [Amplitude,Power] ``[0.5,1.0]``, i.e. the lowest amplitude and power:
|
93 |
+
|
94 |
+
|
95 |
+
```python
|
96 |
+
layout.Parameters.Sines[0.5, 1]
|
97 |
+
```
|
98 |
+
|
99 |
+
As shown in the schematic above, a ``HoloMap`` contains many elements with associated keys. In this example, these keys are indexed with a dimension ``Frequency``, which is why the ``Frequency`` varies when you play the animation here.
|
100 |
+
|
101 |
+
### ``Overlay`` Level
|
102 |
+
|
103 |
+
The printed representation showed us that the ``HoloMap`` is composed of ``Overlay`` objects, six in this case (giving six frames to the animation above). Let us access one of these elements, i.e. one frame of the animation above, by indexing to retrieve an ``Overlay`` associated with the key with a ``Frequency`` of *1.0*:
|
104 |
+
|
105 |
+
|
106 |
+
```python
|
107 |
+
layout.Parameters.Sines[0.5, 1][1.0]
|
108 |
+
```
|
109 |
+
|
110 |
+
### NdOverlay Level
|
111 |
+
|
112 |
+
As the representation shows, the ``Overlay`` contains a ``Points`` object and an ``NdOverlay`` object. We can access either one of these using the attribute access supported by ``Overlay``:
|
113 |
+
|
114 |
+
|
115 |
+
```python
|
116 |
+
(layout.Parameters.Sines[0.5, 1][1].Phases.Sines +
|
117 |
+
layout.Parameters.Sines[0.5, 1][1].Markers.Dots)
|
118 |
+
```
|
119 |
+
|
120 |
+
### ``Curve`` Level
|
121 |
+
|
122 |
+
The ``NdOverlay`` is so named because it is an overlay of items indexed by dimensions, unlike the regular attribute-access overlay types. In this case it is indexed by ``Phase``, with four values. If we index to select one of these values, we will get an individual ``Curve``, e.g. the one with zero phase:
|
123 |
+
|
124 |
+
|
125 |
+
```python
|
126 |
+
l=layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0]
|
127 |
+
l
|
128 |
+
```
|
129 |
+
|
130 |
+
|
131 |
+
```python
|
132 |
+
print(l)
|
133 |
+
```
|
134 |
+
|
135 |
+
### Data Level
|
136 |
+
|
137 |
+
At this point, we have reached the end of the HoloViews objects; below this object is only the raw data stored in one of the supported formats (e.g. NumPy arrays or Pandas DataFrames):
|
138 |
+
|
139 |
+
|
140 |
+
```python
|
141 |
+
type(layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0].data)
|
142 |
+
```
|
143 |
+
|
144 |
+
Actually, HoloViews will let you go even further down, accessing data inside the Numpy array using the continuous (floating-point) coordinate systems declared in HoloViews. E.g. here we can ask for a single datapoint, such as the value at x=5.2:
|
145 |
+
|
146 |
+
|
147 |
+
```python
|
148 |
+
layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0][5.2]
|
149 |
+
```
|
150 |
+
|
151 |
+
Indexing into 1D Elements like Curve and higher-dimensional but regularly gridded Elements like Image, Surface, and HeatMap will return the nearest defined value (i.e., the results "snap" to the nearest data item):
|
152 |
+
|
153 |
+
|
154 |
+
```python
|
155 |
+
layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0][5.23], layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0][5.27]
|
156 |
+
```
|
157 |
+
|
158 |
+
For other Element types, such as Points, snapping is not supported and thus indexing down into the .data array will be less useful, because it will only succeed for a perfect floating-point match on the key dimensions. In those cases, you can still use all of the access methods provided by the numpy array itself, via ``.data``, e.g. ``.data[52]``, but note that such native operations force you to use the native indexing scheme of the array, i.e. integer access starting at zero, not the more convenient and semantically meaningful [continuous coordinate systems](./Continuous_Coordinates.ipynb) we provide through HoloViews.
|
159 |
+
|
160 |
+
### Indexing using ``.select``
|
161 |
+
|
162 |
+
The curve displayed immediately above shows the final, deepest Element access possible in HoloViews for this object:
|
163 |
+
|
164 |
+
```python
|
165 |
+
layout.Parameters.Sines[0.5, 1][1].Phases.Sines[0.0]
|
166 |
+
```
|
167 |
+
This is the curve with an amplitude of *0.5*, raised to a power of *1.0* with frequency of *1.0* and *0* phase. These are all the numbers, in order, used in the access shown above.
|
168 |
+
|
169 |
+
The ``.select`` method is a more explicit way to use key access, with both of these equivalent to each other:
|
170 |
+
|
171 |
+
|
172 |
+
```python
|
173 |
+
o1 = layout.Parameters.Sines.select(Amplitude=0.5, Power=1.0).select(Frequency=1.0)
|
174 |
+
o2 = layout.Parameters.Sines.select(Amplitude=0.5, Power=1.0, Frequency=1.0)
|
175 |
+
o1 + o2
|
176 |
+
```
|
177 |
+
|
178 |
+
The second form demonstrates HoloViews' **deep indexing** feature, which allows indices to cross nested container boundaries. The above is as far as we can index before reaching a heterogeneous type (the ``Overlay``), where we need to use attribute access. Here is the more explicit method of indexing down to a curve, using ``.select`` to specify dimensions by name instead of bracket-based indexing by position:
|
179 |
+
|
180 |
+
|
181 |
+
```python
|
182 |
+
layout.Parameters.Sines.select(Amplitude=0.5,Power=1.0, Frequency=1.0, Phase=0.0).Phases.Sines
|
183 |
+
```
|
184 |
+
|
185 |
+
## Summary
|
186 |
+
|
187 |
+
As you can see, HoloViews lets you compose objects of heterogeneous types, and objects covering many different numerical or other dimensions, laying them out spatially, temporally, or overlaid. The resulting data structures are complex, but they are composed of simple elements with well-defined interactions, making it feasible to express nearly any relationship that will characterize your data. In practice, you will probably not need this many levels, but given this complete example, you should be able to construct an appropriate hierarchy for whatever type of data that you want to represent or visualize.
|
188 |
+
|
189 |
+
As emphasized above, it is not recommended to combine these objects in other orderings. Of course, any ``Element`` can be substituted for any other, which doesn't change the structure. But you should not e.g. display an ``Overlay`` of ``Layout`` objects. The display system will generally attempt to figure out the correct arrangement and warn you to call the `.collate` method to reorganize the objects in the recommended format.
|
190 |
+
|
191 |
+
Another important thing to observe is that ``Layout`` and ``Overlay`` types may not be nested, e.g. using the ``+`` operator on two `Layout` objects will not create a nested `Layout` instead combining the contents of the two objects. The same applies to the ``*`` operator and combining of `Overlay` objects.
|
hvplot_docs/07-Live_Data.md
ADDED
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Live Data
|
2 |
+
|
3 |
+
The [HoloMap](../reference/containers/bokeh/HoloMap.ipynb) is a core HoloViews data structure that allows easy exploration of parameter spaces. The essence of a HoloMap is that it contains a collection of [Elements](http://holoviews.org/reference/index.html) (e.g. ``Image``s and ``Curve``s) that you can easily select and visualize.
|
4 |
+
|
5 |
+
HoloMaps hold fully constructed Elements at specifically sampled points in a multidimensional space. Although HoloMaps are useful for exploring high-dimensional parameter spaces, they can very quickly consume huge amounts of memory to store all these Elements. For instance, a hundred samples along four orthogonal dimensions would need a HoloMap containing a hundred *million* Elements, each of which could be a substantial object that takes time to create and costs memory to store. Thus ``HoloMaps`` have some clear limitations:
|
6 |
+
|
7 |
+
* HoloMaps may require the generation of millions of Elements before the first element can be viewed.
|
8 |
+
* HoloMaps can easily exhaust all the memory available to Python.
|
9 |
+
* HoloMaps can even more easily exhaust all the memory in the browser when displayed.
|
10 |
+
* Static export of a notebook containing HoloMaps can result in impractically large HTML files.
|
11 |
+
|
12 |
+
The ``DynamicMap`` addresses these issues by computing and displaying elements dynamically, allowing exploration of much larger datasets:
|
13 |
+
|
14 |
+
* DynamicMaps generate elements on the fly, allowing the process of exploration to begin immediately.
|
15 |
+
* DynamicMaps do not require fixed sampling, allowing exploration of parameters with arbitrary resolution.
|
16 |
+
* DynamicMaps are lazy in the sense they only compute as much data as the user wishes to explore.
|
17 |
+
|
18 |
+
Of course, these advantages come with some limitations:
|
19 |
+
|
20 |
+
* DynamicMaps require a live notebook server and cannot be fully exported to static HTML.
|
21 |
+
* DynamicMaps store only a portion of the underlying data, in the form of an Element cache and their output is dependent on the particular version of the executed code.
|
22 |
+
* DynamicMaps (and particularly their element caches) are typically stateful (with values that depend on patterns of user interaction), which can make them more difficult to reason about.
|
23 |
+
|
24 |
+
In addition to the different computational requirements of ``DynamicMaps``, they can be used to build sophisticated, interactive visualisations that cannot be achieved using only ``HoloMaps``. This notebook demonstrates some basic examples and the [Responding to Events](./12-Responding_to_Events.ipynb) guide follows on by introducing the streams system. The [Custom Interactivity](./13-Custom_Interactivity.ipynb) shows how you can directly interact with your plots when using the Bokeh backend.
|
25 |
+
|
26 |
+
When DynamicMap was introduced in version 1.6, it supported multiple different 'modes' which have now been deprecated. This notebook demonstrates the simpler, more flexible and more powerful DynamicMap introduced in version 1.7. Users who have been using the previous version of DynamicMap should be unaffected as backwards compatibility has been preserved for the most common cases.
|
27 |
+
|
28 |
+
All this will make much more sense once we've tried out some ``DynamicMaps`` and showed how they work, so let's create one!
|
29 |
+
|
30 |
+
|
31 |
+
<center><div class="alert alert-info" role="alert">To use visualize and use a <b>DynamicMap</b> you need to be running a live Jupyter server.<br>When viewing this user guide as part of the documentation DynamicMaps will be sampled with a limited number of states.<br>
|
32 |
+
It's also best to run this notebook one cell at a time, not via "Run All",<br> so that subsequent cells can reflect your dynamic interaction with widgets in previous cells.</div></center>
|
33 |
+
|
34 |
+
## ``DynamicMap`` <a id='DynamicMap'></a>
|
35 |
+
|
36 |
+
Let's start by importing HoloViews and loading the extension:
|
37 |
+
|
38 |
+
|
39 |
+
```python
|
40 |
+
import numpy as np
|
41 |
+
import holoviews as hv
|
42 |
+
from holoviews import opts
|
43 |
+
|
44 |
+
hv.extension('matplotlib')
|
45 |
+
```
|
46 |
+
|
47 |
+
We will now create ``DynamicMap`` similar to the ``HoloMap`` introduced in the [Introductory guide](../getting_started/1-Introduction.ipynb). The ``HoloMap`` in that introduction consisted of ``Image`` elements defined by a function returning NumPy arrays called ``sine_array``. Here we will define a ``waves_image`` function that returns an array pattern parameterized by arbitrary ``alpha`` and ``beta`` parameters inside a HoloViews
|
48 |
+
[``Image``](../reference/elements/bokeh/Image.ipynb) element:
|
49 |
+
|
50 |
+
|
51 |
+
```python
|
52 |
+
xvals = np.linspace(-4, 0, 202)
|
53 |
+
yvals = np.linspace(4, 0, 202)
|
54 |
+
xs, ys = np.meshgrid(xvals, yvals)
|
55 |
+
|
56 |
+
def waves_image(alpha, beta):
|
57 |
+
return hv.Image(np.sin(((ys/alpha)**alpha+beta)*xs))
|
58 |
+
|
59 |
+
waves_image(1,0) + waves_image(1,4)
|
60 |
+
```
|
61 |
+
|
62 |
+
Now we can demonstrate the possibilities for exploration enabled by the simplest declaration of a ``DynamicMap``.
|
63 |
+
|
64 |
+
### Basic ``DynamicMap`` declaration<a id='BasicDeclaration'></a>
|
65 |
+
|
66 |
+
A simple ``DynamicMap`` declaration looks identical to that needed to declare a ``HoloMap``. Instead of supplying some initial data, we will supply the ``waves_image`` function with key dimensions simply declaring the arguments of that function:
|
67 |
+
|
68 |
+
|
69 |
+
```python
|
70 |
+
dmap = hv.DynamicMap(waves_image, kdims=['alpha', 'beta'])
|
71 |
+
dmap
|
72 |
+
```
|
73 |
+
|
74 |
+
This object is created instantly, but because it doesn't generate any `hv.Image` objects initially it only shows the printed representation of this object along with some information about how to display it. We will refer to a ``DynamicMap`` that doesn't have enough information to display itself as 'unbounded'.
|
75 |
+
|
76 |
+
The textual representation of all ``DynamicMaps`` look similar, differing only in the listed dimensions until they have been evaluated at least once.
|
77 |
+
|
78 |
+
#### Explicit indexing
|
79 |
+
|
80 |
+
Unlike a corresponding ``HoloMap`` declaration, this simple unbounded ``DynamicMap`` cannot yet visualize itself. To view it, we can follow the advice in the warning message. First we will explicitly index into our ``DynamicMap`` in the same way you would access a key on a ``HoloMap``:
|
81 |
+
|
82 |
+
|
83 |
+
```python
|
84 |
+
dmap[1,2] + dmap.select(alpha=1, beta=2)
|
85 |
+
```
|
86 |
+
|
87 |
+
Note that the declared kdims are specifying the arguments *by position* as they do not match the argument names of the ``waves_image`` function. If you *do* match the argument names *exactly*, you can map a kdim position to any argument position of the callable. For instance, the declaration ``kdims=['freq', 'phase']`` would index first by frequency, then phase without mixing up the arguments to ``waves_image`` when indexing.
|
88 |
+
|
89 |
+
#### Setting dimension ranges
|
90 |
+
|
91 |
+
The second suggestion in the warning message was to supply dimension ranges using the ``redim.range`` method:
|
92 |
+
|
93 |
+
|
94 |
+
```python
|
95 |
+
dmap.redim.range(alpha=(1, 5.0), beta=(1, 6.0))
|
96 |
+
```
|
97 |
+
|
98 |
+
Here each `hv.Image` object visualizing a particular sine ring pattern with the given parameters is created dynamically, whenever the slider is set to a new value. Any value in the allowable range can be requested by dragging the sliders or by tweaking the values using the left and right arrow keys.
|
99 |
+
|
100 |
+
Of course, we didn't have to use the ``redim.range`` method and we could have simply declared the ranges right away using explicit ``hv.Dimension`` objects. This would allow us to declare other dimension properties such as the step size used by the sliders: by default each slider can select around a thousand distinct values along its range but you can specify your own step value via the dimension ``step`` parameter. If you use integers in your range declarations, integer stepping will be assumed with a step size of one.
|
101 |
+
|
102 |
+
Note that whenever the ``redim`` method is used, a new ``DynamicMap`` is returned with the updated dimensions. In other words, the original ``dmap`` remains unbounded with default dimension objects.
|
103 |
+
|
104 |
+
|
105 |
+
#### Setting dimension values
|
106 |
+
|
107 |
+
The ``DynamicMap`` above allows exploration of *any* phase and frequency within the declared range unlike an equivalent ``HoloMap`` which would have to be composed of a finite set of samples. We can achieve a similar discrete sampling using ``DynamicMap`` by setting the ``values`` parameter on the dimensions:
|
108 |
+
|
109 |
+
|
110 |
+
```python
|
111 |
+
dmap.redim.values(alpha=[1, 2, 3], beta=[0.1, 1.0, 2.5])
|
112 |
+
```
|
113 |
+
|
114 |
+
The sliders now snap to the specified dimension values and if you are running this live, the above cell should look like a [HoloMap](../reference/containers/bokeh/HoloMap.ipynb). ``DynamicMap`` is in fact a subclass of ``HoloMap`` with some crucial differences:
|
115 |
+
|
116 |
+
* You can now pick as many values of **alpha** or **beta** as allowed by the slider.
|
117 |
+
* What you see in the cell above will not be exported in any HTML snapshot of the notebook
|
118 |
+
|
119 |
+
#### Getting the current key
|
120 |
+
|
121 |
+
The ``current_key`` property returns the current key of the ``DynamicMap``, i.e. either a value (one-dimensional) or a tuple (multi-dimensional) that represents the current state of the widgets. If you move the sliders above and re-execute the cell below you will see that the tuple returned reflects the values of *alpha* and *beta*.
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
dmap.current_key
|
126 |
+
```
|
127 |
+
|
128 |
+
We will now explore how ``DynamicMaps`` relate to ``HoloMaps`` including conversion operations between the two types. As we will see, there are other ways to display a ``DynamicMap`` without using explicit indexing or redim.
|
129 |
+
|
130 |
+
## Interaction with ``HoloMap``s
|
131 |
+
|
132 |
+
To explore the relationship between ``DynamicMap`` and ``HoloMap``, let's declare another callable to draw some shapes we will use in a new ``DynamicMap``:
|
133 |
+
|
134 |
+
|
135 |
+
```python
|
136 |
+
def shapes(N, radius=0.5): # Positional keyword arguments are fine
|
137 |
+
paths = [hv.Path([[(radius*np.sin(a), radius*np.cos(a))
|
138 |
+
for a in np.linspace(-np.pi, np.pi, n+2)]],
|
139 |
+
extents=(-1,-1,1,1))
|
140 |
+
for n in range(N,N+3)]
|
141 |
+
return hv.Overlay(paths)
|
142 |
+
```
|
143 |
+
|
144 |
+
#### Sampling ``DynamicMap`` from a ``HoloMap``
|
145 |
+
|
146 |
+
When combining a ``HoloMap`` with a ``DynamicMap``, it would be very awkward to have to match the declared dimension ``values`` of the DynamicMap with the keys of the ``HoloMap``. Fortunately you don't have to:
|
147 |
+
|
148 |
+
|
149 |
+
```python
|
150 |
+
holomap = hv.HoloMap({(N,r):shapes(N, r) for N in [3,4,5] for r in [0.5,0.75]}, kdims=['N', 'radius'])
|
151 |
+
dmap = hv.DynamicMap(shapes, kdims=['N','radius'])
|
152 |
+
holomap + dmap
|
153 |
+
```
|
154 |
+
|
155 |
+
Here we declared a ``DynamicMap`` without using ``redim``, but we can view its output because it is presented alongside a ``HoloMap`` which defines the available keys. This convenience is subject to three particular restrictions:
|
156 |
+
|
157 |
+
|
158 |
+
* You cannot display a layout consisting of unbounded ``DynamicMaps`` only, because at least one HoloMap is needed to define the samples.
|
159 |
+
* The HoloMaps provide the necessary information required to sample the DynamicMap.
|
160 |
+
|
161 |
+
Note that there is one way ``DynamicMap`` is less restricted than ``HoloMap``: you can freely combine bounded ``DynamicMaps`` together in a ``Layout``, even if they don't share key dimensions.
|
162 |
+
|
163 |
+
#### Converting from ``DynamicMap`` to ``HoloMap``
|
164 |
+
|
165 |
+
Above we mentioned that ``DynamicMap`` is an instance of ``HoloMap``. Does this mean it has a ``.data`` attribute?
|
166 |
+
|
167 |
+
|
168 |
+
```python
|
169 |
+
dtype = type(dmap.data).__name__
|
170 |
+
length = len(dmap.data)
|
171 |
+
print("DynamicMap 'dmap' has an {dtype} .data attribute of length {length}".format(dtype=dtype, length=length))
|
172 |
+
```
|
173 |
+
|
174 |
+
This is exactly the same sort of ``.data`` as the equivalent ``HoloMap``, except that its values will vary according to how much you explored the parameter space of ``dmap`` using the sliders above. In a ``HoloMap``, ``.data`` contains a defined sampling along the different dimensions, whereas in a ``DynamicMap``, the ``.data`` is simply the *cache*.
|
175 |
+
|
176 |
+
The cache serves two purposes:
|
177 |
+
|
178 |
+
* Avoids recomputation of an element should we revisit a particular point in the parameter space. This works well for categorical or integer dimensions, but doesn't help much when using continuous sliders for real-valued dimensions.
|
179 |
+
* Records the space that has been explored with the ``DynamicMap`` for any later conversion to a ``HoloMap`` up to the allowed cache size.
|
180 |
+
|
181 |
+
We can always convert *any* ``DynamicMap`` directly to a ``HoloMap`` as follows:
|
182 |
+
|
183 |
+
|
184 |
+
```python
|
185 |
+
hv.HoloMap(dmap)
|
186 |
+
```
|
187 |
+
|
188 |
+
This is in fact equivalent to declaring a HoloMap with the same parameters (dimensions, etc.) using ``dmap.data`` as input, but is more convenient. Note that the slider positions reflect those we sampled from the ``HoloMap`` in the previous section.
|
189 |
+
|
190 |
+
Although creating a HoloMap this way is easy, the result is poorly controlled, as the keys in the DynamicMap cache are usually defined by how you moved the sliders around. If you instead want to specify a specific set of samples, you can easily do so by using the same key-selection semantics as for a ``HoloMap`` to define exactly which elements are to be sampled and put into the cache:
|
191 |
+
|
192 |
+
|
193 |
+
```python
|
194 |
+
hv.HoloMap(dmap[{(2,0.3), (2,0.6), (3,0.3), (3,0.6)}])
|
195 |
+
```
|
196 |
+
|
197 |
+
Here we index the ``dmap`` with specified keys to return a *new* DynamicMap with those keys in its cache, which we then cast to a ``HoloMap``. This allows us to export specific contents of ``DynamicMap`` to static HTML which will display the data at the sampled slider positions.
|
198 |
+
|
199 |
+
The key selection above happens to define a Cartesian product, which is one of the most common ways to sample across dimensions. Because the list of such dimension values can quickly get very large when enumerated as above, we provide a way to specify a Cartesian product directly, which also works with ``HoloMaps``. Here is an equivalent way of defining the same set of four points in that two-dimensional space:
|
200 |
+
|
201 |
+
|
202 |
+
```python
|
203 |
+
samples = hv.HoloMap(dmap[{2,3},{0.5,1.0}])
|
204 |
+
samples
|
205 |
+
```
|
206 |
+
|
207 |
+
|
208 |
+
```python
|
209 |
+
samples.data.keys()
|
210 |
+
```
|
211 |
+
|
212 |
+
The default cache size of 500 Elements is relatively high so that interactive exploration will work smoothly, but you can reduce it using the ``cache_size`` parameter if you find you are running into issues with memory consumption. A bounded ``DynamicMap`` with ``cache_size=1`` requires the least memory, but will recompute a new Element every time the sliders are moved, making it less responsive.
|
213 |
+
|
214 |
+
#### Converting from ``HoloMap`` to ``DynamicMap``
|
215 |
+
|
216 |
+
We have now seen how to convert from a ``DynamicMap`` to a ``HoloMap`` for the purposes of static export, but why would you ever want to do the inverse?
|
217 |
+
|
218 |
+
Although having a ``HoloMap`` to start with means it will not save you memory, converting to a ``DynamicMap`` does mean that the rendering process can be deferred until a new slider value requests an update. You can achieve this conversion using the ``Dynamic`` utility as demonstrated here by applying it to the previously defined ``HoloMap`` called ``samples``:
|
219 |
+
|
220 |
+
|
221 |
+
```python
|
222 |
+
from holoviews.util import Dynamic
|
223 |
+
dynamic = Dynamic(samples)
|
224 |
+
print('After apply Dynamic, the type is a {dtype}'.format(dtype=type(dynamic).__name__))
|
225 |
+
dynamic
|
226 |
+
```
|
227 |
+
|
228 |
+
In this particular example, there is no real need to use ``Dynamic`` as each frame renders quickly enough. For visualizations that are slow to render, using ``Dynamic`` can result in more responsive visualizations.
|
229 |
+
|
230 |
+
The ``Dynamic`` utility is very versatile and is discussed in more detail in the [Transforming Elements](./11-Transforming_Elements.ipynb) guide.
|
231 |
+
|
232 |
+
### Slicing ``DynamicMaps``
|
233 |
+
|
234 |
+
As we have seen we can either declare dimension ranges directly in the kdims or use the ``redim.range`` convenience method:
|
235 |
+
|
236 |
+
|
237 |
+
```python
|
238 |
+
dmap = hv.DynamicMap(shapes, kdims=['N','radius']).redim.range(N=(2,20), radius=(0.5,1.0))
|
239 |
+
```
|
240 |
+
|
241 |
+
The declared dimension ranges define the absolute limits allowed for exploration in this continuous, bounded DynamicMap . That said, you can use the soft_range parameter to view subregions within that range. Setting the soft_range parameter on dimensions can be done conveniently using slicing:
|
242 |
+
|
243 |
+
|
244 |
+
```python
|
245 |
+
sliced = dmap[4:8, :]
|
246 |
+
sliced
|
247 |
+
```
|
248 |
+
|
249 |
+
|
250 |
+
Notice that N is now restricted to the range 4:8. Open slices are used to release any ``soft_range`` values, which resets the limits back to those defined by the full range:
|
251 |
+
|
252 |
+
|
253 |
+
```python
|
254 |
+
sliced[:, 0.8:1.0]
|
255 |
+
```
|
256 |
+
|
257 |
+
The ``[:]`` slice leaves the soft_range values alone and can be used as a convenient way to clone a ``DynamicMap``. Note that mixing slices with any other object type is not supported. In other words, once you use a single slice, you can only use slices in that indexing operation.
|
258 |
+
|
259 |
+
## Using groupby to discretize a DynamicMap
|
260 |
+
|
261 |
+
A DynamicMap also makes it easy to partially or completely discretize a function to evaluate in a complex plot. By grouping over specific dimensions that define a fixed sampling via the Dimension values parameter, the DynamicMap can be viewed as a ``GridSpace``, ``NdLayout``, or ``NdOverlay``. If a dimension specifies only a continuous range it can't be grouped over, but it may still be explored using the widgets. This means we can plot partial or completely discretized views of a parameter space easily.
|
262 |
+
|
263 |
+
#### Partially discretize
|
264 |
+
|
265 |
+
The implementation for all the groupby operations uses the ``.groupby`` method internally, but we also provide three higher-level convenience methods to group dimensions into an ``NdOverlay`` (``.overlay``), ``GridSpace`` (``.grid``), or ``NdLayout`` (``.layout``).
|
266 |
+
|
267 |
+
Here we will evaluate a simple sine function with three dimensions, the phase, frequency, and amplitude. We assign the frequency and amplitude discrete samples, while defining a continuous range for the phase:
|
268 |
+
|
269 |
+
|
270 |
+
```python
|
271 |
+
xs = np.linspace(0, 2*np.pi,100)
|
272 |
+
|
273 |
+
def sin(ph, f, amp):
|
274 |
+
return hv.Curve((xs, np.sin(xs*f+ph)*amp))
|
275 |
+
|
276 |
+
kdims=[hv.Dimension('phase', range=(0, np.pi)),
|
277 |
+
hv.Dimension('frequency', values=[0.1, 1, 2, 5, 10]),
|
278 |
+
hv.Dimension('amplitude', values=[0.5, 5, 10])]
|
279 |
+
|
280 |
+
waves_dmap = hv.DynamicMap(sin, kdims=kdims)
|
281 |
+
```
|
282 |
+
|
283 |
+
Next we define the amplitude dimension to be overlaid and the frequency dimension to be gridded:
|
284 |
+
|
285 |
+
|
286 |
+
```python
|
287 |
+
wave_grid = waves_dmap.overlay('amplitude').grid('frequency')
|
288 |
+
wave_grid.opts(fig_size=200, show_legend=True)
|
289 |
+
```
|
290 |
+
|
291 |
+
As you can see, instead of having three sliders (one per dimension), we've now laid out the frequency dimension as a discrete set of values in a grid, and the amplitude dimension as a discrete set of values in an overlay, leaving one slider for the remaining dimension (phase). This approach can help you visualize a large, multi-dimensional space efficiently, with full control over how each dimension is made visible.
|
292 |
+
|
293 |
+
|
294 |
+
#### Fully discretize
|
295 |
+
|
296 |
+
Given a continuous function defined over a space, we could sample it manually, but here we'll look at an example of evaluating it using the groupby method. Let's look at a spiral function with a frequency and first- and second-order phase terms. Then we define the dimension values for all the parameters and declare the DynamicMap:
|
297 |
+
|
298 |
+
|
299 |
+
```python
|
300 |
+
opts.defaults(opts.Path(color=hv.Palette('Blues')))
|
301 |
+
|
302 |
+
def spiral_equation(f, ph, ph2):
|
303 |
+
r = np.arange(0, 1, 0.005)
|
304 |
+
xs, ys = (r * fn(f*np.pi*np.sin(r+ph)+ph2) for fn in (np.cos, np.sin))
|
305 |
+
return hv.Path((xs, ys))
|
306 |
+
|
307 |
+
spiral_dmap = hv.DynamicMap(spiral_equation, kdims=['f','ph','ph2'])
|
308 |
+
spiral_dmap = spiral_dmap.redim.values(
|
309 |
+
f=np.linspace(1, 10, 10),
|
310 |
+
ph=np.linspace(0, np.pi, 10),
|
311 |
+
ph2=np.linspace(0, np.pi, 4))
|
312 |
+
```
|
313 |
+
|
314 |
+
Now we can make use of the ``.groupby`` method to group over the frequency and phase dimensions, which we will display as part of a GridSpace by setting the ``container_type``. This leaves the second phase variable, which we assign to an NdOverlay by setting the ``group_type``:
|
315 |
+
|
316 |
+
|
317 |
+
```python
|
318 |
+
spiral_grid = spiral_dmap.groupby(['f', 'ph'], group_type=hv.NdOverlay, container_type=hv.GridSpace)
|
319 |
+
spiral_grid.opts(
|
320 |
+
opts.GridSpace(xaxis=None, yaxis=None),
|
321 |
+
opts.Path(bgcolor='white', xaxis=None, yaxis=None))
|
322 |
+
```
|
323 |
+
|
324 |
+
This grid shows a range of frequencies `f` on the x axis, a range of the first phase variable `ph` on the `y` axis, and a range of different `ph2` phases as overlays within each location in the grid. As you can see, these techniques can help you visualize multidimensional parameter spaces compactly and conveniently.
|
325 |
+
|
326 |
+
|
327 |
+
## DynamicMaps and normalization
|
328 |
+
|
329 |
+
By default, a ``HoloMap`` normalizes the display of elements using the minimum and maximum values found across the ``HoloMap``. This automatic behavior is not possible in a ``DynamicMap``, where arbitrary new elements are being generated on the fly. Consider the following examples where the arrays contained within the returned ``Image`` objects are scaled with time:
|
330 |
+
|
331 |
+
|
332 |
+
```python
|
333 |
+
ls = np.linspace(0, 10, 200)
|
334 |
+
xx, yy = np.meshgrid(ls, ls)
|
335 |
+
|
336 |
+
def cells(time):
|
337 |
+
return hv.Image(time*np.sin(xx+time)*np.cos(yy+time), vdims='Intensity')
|
338 |
+
|
339 |
+
dmap = hv.DynamicMap(cells, kdims='time').redim.range(time=(1,20))
|
340 |
+
(dmap + dmap.redim.range(Intensity=(0,10))).opts(
|
341 |
+
opts.Image(axiswise=True))
|
342 |
+
```
|
343 |
+
|
344 |
+
Here we use ``axiswise=True`` to see the behavior of the two cases independently. We see in **A** that when only the time dimension is given a range, no automatic normalization occurs (unlike a ``HoloMap``). In **B** we see that normalization is applied, but only when the value dimension ('Intensity') range has been specified.
|
345 |
+
|
346 |
+
In other words, ``DynamicMaps`` cannot support automatic normalization across their elements, but do support the same explicit normalization behavior as ``HoloMaps``. Values that are generated outside this range are simply clipped in accord with the usual semantics of explicit value dimension ranges.
|
347 |
+
|
348 |
+
Note that we always have the option of casting a ``DynamicMap`` to a ``HoloMap`` in order to automatically normalize across the cached values, without needing explicit value dimension ranges.
|
349 |
+
|
350 |
+
## Using DynamicMaps in your code
|
351 |
+
|
352 |
+
As you can see, ``DynamicMaps`` let you use HoloViews with a very wide range of dynamic data formats and sources, making it simple to visualize ongoing processes or very large data spaces.
|
353 |
+
|
354 |
+
Given unlimited computational resources, the functionality covered in this guide would match that offered by ``HoloMap`` but with fewer normalization options. ``DynamicMap`` actually enables a vast range of new possibilities for dynamic, interactive visualizations as covered in the [Responding to Events](./Responding_to_Events.ipynb) guide. Following on from that, the [Custom Interactivity](./13-Custom_Interactivity.ipynb) guide shows how you can directly interact with your plots when using the Bokeh backend.
|
hvplot_docs/08-Tabular_Datasets.md
ADDED
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Tabular Datasets
|
2 |
+
|
3 |
+
In this guide we will explore how to work with tabular data in HoloViews. Tabular data has a fixed list of column headings, with values stored in an arbitrarily long list of rows. Spreadsheets, relational databases, CSV files, and many other typical data sources fit naturally into this format. HoloViews defines an extensible system of interfaces to load, manipulate, and visualize this kind of data, as well as allowing conversion of any of the non-tabular data types into tabular data for analysis or data interchange.
|
4 |
+
|
5 |
+
By default HoloViews will use one of these data storage formats for tabular data:
|
6 |
+
|
7 |
+
* A pure Python dictionary containing 1D NumPy-arrays for each column.
|
8 |
+
|
9 |
+
``{'x': np.array([0, 1, 2]), 'y': np.array([0, 1, 2])}``
|
10 |
+
|
11 |
+
* A purely NumPy array format for numeric data.
|
12 |
+
|
13 |
+
``np.array([[0, 0], [1, 1], [2, 3]])``
|
14 |
+
|
15 |
+
* Pandas DataFrames
|
16 |
+
|
17 |
+
``pd.DataFrame(np.array([[0, 0], [1, 1], [2, 3]]), columns=['x', 'y'])``
|
18 |
+
|
19 |
+
* Dask DataFrames
|
20 |
+
|
21 |
+
* cuDF Dataframes
|
22 |
+
|
23 |
+
A number of additional standard constructors are supported:
|
24 |
+
|
25 |
+
* A tuple of array (or array-like) objects
|
26 |
+
|
27 |
+
``([0, 1, 2], [0, 1, 2])``
|
28 |
+
|
29 |
+
* A list of tuples:
|
30 |
+
|
31 |
+
``[(0, 0), (1, 1), (2, 2)]``
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
```python
|
36 |
+
import numpy as np
|
37 |
+
import pandas as pd
|
38 |
+
import holoviews as hv
|
39 |
+
from holoviews import opts
|
40 |
+
|
41 |
+
hv.extension('bokeh', 'matplotlib')
|
42 |
+
|
43 |
+
opts.defaults(opts.Scatter(size=10))
|
44 |
+
```
|
45 |
+
|
46 |
+
# A simple Dataset
|
47 |
+
|
48 |
+
Usually when working with data we have one or more independent variables, taking the form of categories, labels, discrete sample coordinates, or bins. We refer to these independent variables as key dimensions (or ``kdims`` for short) in HoloViews. The observer or dependent variables, on the other hand, are referred to as value dimensions (``vdims``), and are ordinarily measured or calculated given the independent variables. The simplest useful form of a ``Dataset`` object is therefore a column 'x' and a column 'y' corresponding to the key dimensions and value dimensions respectively. An obvious visual representation of this data is a ``Table``:
|
49 |
+
|
50 |
+
|
51 |
+
```python
|
52 |
+
xs = np.linspace(0, 10, 11)
|
53 |
+
ys = np.sin(xs)
|
54 |
+
|
55 |
+
table = hv.Table((xs, ys), 'x', 'y')
|
56 |
+
table
|
57 |
+
```
|
58 |
+
|
59 |
+
However, this data has many more meaningful visual representations, and therefore the first important concept is that ``Dataset`` objects can be converted to other objects as long as their dimensionality allows it, meaning that you can easily create the different objects from the same data (and cast between the objects once created):
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
(hv.Scatter(table) + hv.Curve(table) + hv.Area(table) + hv.Bars(table)).cols(2)
|
64 |
+
```
|
65 |
+
|
66 |
+
Each of these three plots uses the same data, but represents a different assumption about the semantic meaning of that data -- the ``Scatter`` plot is appropriate if that data consists of independent samples, the ``Curve`` plot is appropriate for samples chosen from an underlying smooth function, and the ``Bars`` plot is appropriate for independent categories of data. Since all these plots have the same dimensionality, they can easily be converted to each other, but there is normally only one of these representations that is semantically appropriate for the underlying data. For this particular data, the semantically appropriate choice is ``Curve``, since the *y* values are samples from the continuous function ``exp``.
|
67 |
+
|
68 |
+
As a guide to which Elements can be converted to each other, those of the same dimensionality here should be interchangeable, because of the underlying similarity of their columnar representation:
|
69 |
+
|
70 |
+
* 0D: BoxWhisker, Spikes, Distribution
|
71 |
+
* 1D: Area, Bars, BoxWhisker, Curve, ErrorBars, Scatter, Spread
|
72 |
+
* 2D: Bars, Bivariate, BoxWhisker, HeatMap, Points, VectorField
|
73 |
+
* 3D: Scatter3D, TriSurface, VectorField
|
74 |
+
|
75 |
+
This categorization is based only on the ``kdims``, which define the space in which the data has been sampled or defined. An Element can also have any number of value dimensions (``vdims``), which may be mapped onto various attributes of a plot such as the color, size, and orientation of the plotted items. For a reference of how to use these various Element types, see the [Elements Reference](http://holoviews.org/reference/index.html#elements).
|
76 |
+
|
77 |
+
## Data types and Constructors
|
78 |
+
|
79 |
+
As discussed above, ``Dataset`` provides an extensible interface to store and operate on data in different formats. All interfaces support a number of standard constructors.
|
80 |
+
|
81 |
+
#### Storage formats
|
82 |
+
|
83 |
+
Dataset types can be constructed using one of three supported formats, (a) a dictionary of columns, (b) an NxD array with N rows and D columns, or (c) pandas dataframes:
|
84 |
+
|
85 |
+
|
86 |
+
```python
|
87 |
+
print(hv.Scatter({'x': xs, 'y': ys}) +
|
88 |
+
hv.Scatter(np.column_stack([xs, ys])) +
|
89 |
+
hv.Scatter(pd.DataFrame({'x': xs, 'y': ys})))
|
90 |
+
```
|
91 |
+
|
92 |
+
#### Literals
|
93 |
+
|
94 |
+
In addition to the main storage formats, Dataset Elements support construction from three Python literal formats: (a) An iterator of y-values, (b) a tuple of columns, and (c) an iterator of row tuples.
|
95 |
+
|
96 |
+
|
97 |
+
```python
|
98 |
+
print(hv.Scatter(ys) + hv.Scatter((xs, ys)) + hv.Scatter(zip(xs, ys)))
|
99 |
+
```
|
100 |
+
|
101 |
+
For these inputs, the data will need to be copied to a new data structure, having one of the three storage formats above. By default Dataset will try to construct a simple array, falling back to either pandas dataframes (if available) or the dictionary-based format if the data is not purely numeric. Additionally, the interfaces will try to maintain the provided data's type, so numpy arrays and pandas DataFrames will always be parsed first by their respective array and dataframe interfaces.
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
df = pd.DataFrame({'x': xs, 'y': ys, 'z': ys*2})
|
106 |
+
print(type(hv.Scatter(df).data))
|
107 |
+
```
|
108 |
+
|
109 |
+
Dataset will attempt to parse the supplied data, falling back to each consecutive interface if the previous could not interpret the data. The default list of fallbacks and simultaneously the list of allowed datatypes is:
|
110 |
+
|
111 |
+
|
112 |
+
```python
|
113 |
+
hv.Dataset.datatype
|
114 |
+
```
|
115 |
+
|
116 |
+
Note these include grid based datatypes, which are covered in [Gridded Datasets](http://holoviews.org/user_guide/Gridded_Datasets.html). To select a particular storage format explicitly, supply one or more allowed datatypes (note that the 'array' interface only supports data with matching types):
|
117 |
+
|
118 |
+
|
119 |
+
```python
|
120 |
+
print(type(hv.Scatter((xs.astype('float64'), ys), datatype=['array']).data))
|
121 |
+
print(type(hv.Scatter((xs, ys), datatype=['dictionary']).data))
|
122 |
+
print(type(hv.Scatter((xs, ys), datatype=['dataframe']).data))
|
123 |
+
```
|
124 |
+
|
125 |
+
#### Sharing Data
|
126 |
+
|
127 |
+
Since the formats with labelled columns do not require any specific order, each Element can effectively become a view into a single set of data. By specifying different key and value dimensions, many Elements can show different values, while sharing the same underlying data source.
|
128 |
+
|
129 |
+
|
130 |
+
```python
|
131 |
+
overlay = hv.Scatter(df, 'x', 'y') * hv.Scatter(df, 'x', 'z')
|
132 |
+
overlay
|
133 |
+
```
|
134 |
+
|
135 |
+
We can quickly confirm that the data is actually shared:
|
136 |
+
|
137 |
+
|
138 |
+
```python
|
139 |
+
overlay.Scatter.I.data is overlay.Scatter.II.data
|
140 |
+
```
|
141 |
+
|
142 |
+
For columnar data, this approach is much more efficient than creating copies of the data for each Element, and allows for some advanced features like linked brushing in the [Bokeh backend](./Plotting_with_Bokeh.ipynb).
|
143 |
+
|
144 |
+
#### Converting to raw data
|
145 |
+
|
146 |
+
Column types make it easy to export the data to the three basic formats: arrays, dataframes, and a dictionary of columns.
|
147 |
+
|
148 |
+
###### Array
|
149 |
+
|
150 |
+
|
151 |
+
```python
|
152 |
+
table.array()
|
153 |
+
```
|
154 |
+
|
155 |
+
###### Pandas DataFrame
|
156 |
+
|
157 |
+
|
158 |
+
```python
|
159 |
+
table.dframe().head()
|
160 |
+
```
|
161 |
+
|
162 |
+
###### Dataset dictionary
|
163 |
+
|
164 |
+
|
165 |
+
```python
|
166 |
+
table.columns()
|
167 |
+
```
|
168 |
+
|
169 |
+
# Creating tabular data from Elements using the .table and .dframe methods
|
170 |
+
|
171 |
+
If you have data in some other HoloViews element and would like to use the columnar data features, you can easily tabularize any of the core Element types into a ``Table`` Element. Similarly, the ``.dframe()`` method will convert an Element into a pandas DataFrame. These methods are very useful if you want to then transform the data into a different Element type, or to perform different types of analysis.
|
172 |
+
|
173 |
+
## Tabularizing simple Elements
|
174 |
+
|
175 |
+
For a simple example, we can create a ``Curve`` of an exponential function and cast it to a ``Table``, with the same result as creating the Table directly from the data as done earlier in this user guide:
|
176 |
+
|
177 |
+
|
178 |
+
```python
|
179 |
+
xs = np.arange(10)
|
180 |
+
curve = hv.Curve(zip(xs, np.sin(xs)))
|
181 |
+
curve * hv.Scatter(curve) + hv.Table(curve)
|
182 |
+
```
|
183 |
+
|
184 |
+
Similarly, we can get a pandas dataframe of the Curve using ``curve.dframe()``:
|
185 |
+
|
186 |
+
|
187 |
+
```python
|
188 |
+
curve.dframe()
|
189 |
+
```
|
190 |
+
|
191 |
+
## Collapsing dimensioned containers
|
192 |
+
|
193 |
+
Even deeply nested objects can be deconstructed in this way, serializing them to make it easier to get your raw data out of a collection of specialized ``Element`` types. Let's say we want to make multiple observations of a noisy signal. We can collect the data into a ``HoloMap`` to visualize it and then call ``.collapse()`` to get a ``Dataset`` object to which we can apply operations or transformations to other ``Element`` types. Deconstructing nested data in this way only works if the data is homogeneous. In practical terms this requires that your data structure contains Elements (of any type) held in these Container types: ``NdLayout``, ``GridSpace``, ``HoloMap``, and ``NdOverlay``, with all dimensions consistent throughout (so that they can all fit into the same set of columns). To read more about these containers see the [Dimensioned Containers](./Dimensioned_Containers.ipynb) guide.
|
194 |
+
|
195 |
+
Let's now go back to the ``Image`` example. We will collect a number of observations of some noisy data into a ``HoloMap`` and display it:
|
196 |
+
|
197 |
+
|
198 |
+
```python
|
199 |
+
obs_hmap = hv.HoloMap({i: hv.Image(np.random.randn(10, 10), bounds=(0,0,3,3))
|
200 |
+
for i in range(3)}, kdims='Observation')
|
201 |
+
obs_hmap
|
202 |
+
```
|
203 |
+
|
204 |
+
Now we can serialize this data just as before, where this time we get a four-column (4D) table. The key dimensions of both the HoloMap and the Images, as well as the z-values of each ``Image``, are all merged into a single table. We can visualize the samples we have collected by converting it to a ``Scatter3D`` object.
|
205 |
+
|
206 |
+
|
207 |
+
```python
|
208 |
+
hv.output(backend='matplotlib', size=150)
|
209 |
+
|
210 |
+
collapsed = obs_hmap.collapse()
|
211 |
+
scatter_layout = collapsed.to.scatter3d() + hv.Table(collapsed)
|
212 |
+
scatter_layout.opts(
|
213 |
+
opts.Scatter3D(color='z', cmap='hot', edgecolor='black', s=50))
|
214 |
+
```
|
215 |
+
|
216 |
+
Here the `z` dimension is shown by color, as in the original images, and the other three dimensions determine where the datapoint is shown in 3D. This way of deconstructing objects will work for any data structure that satisfies the conditions described above, no matter how nested. If we vary the amount of noise while continuing to performing multiple observations, we can create an ``NdLayout`` of HoloMaps, one for each noise level, and animated by the observation number.
|
217 |
+
|
218 |
+
|
219 |
+
```python
|
220 |
+
extents = (0, 0, 3, 3)
|
221 |
+
|
222 |
+
error_hmap = hv.HoloMap({
|
223 |
+
(i, j): hv.Image(j*np.random.randn(3, 3), bounds=extents)
|
224 |
+
for i in range(3) for j in np.linspace(0, 1, 3)},
|
225 |
+
['Observation', 'noise'])
|
226 |
+
|
227 |
+
noise_layout = error_hmap.layout('noise')
|
228 |
+
noise_layout
|
229 |
+
```
|
230 |
+
|
231 |
+
And again, we can easily convert the object to a ``Table``:
|
232 |
+
|
233 |
+
|
234 |
+
```python
|
235 |
+
hv.Table(noise_layout.collapse())
|
236 |
+
```
|
237 |
+
|
238 |
+
# Applying operations to the data
|
239 |
+
|
240 |
+
#### Sorting by columns
|
241 |
+
|
242 |
+
Once data is in columnar form, it is simple to apply a variety of operations. For instance, Dataset can be sorted by their dimensions using the ``.sort()`` method. By default, this method will sort by the key dimensions in an ascending order, but any other dimension(s) can be sorted by providing them as an argument list to the sort method. The ``reverse`` argument also allows sorting in descending order:
|
243 |
+
|
244 |
+
|
245 |
+
```python
|
246 |
+
hv.output(backend='bokeh')
|
247 |
+
|
248 |
+
bars = hv.Bars((['C', 'A', 'B', 'D'], [2, 7, 3, 4]))
|
249 |
+
(bars +
|
250 |
+
bars.sort().relabel('sorted') +
|
251 |
+
bars.sort(['y']).relabel('y-sorted') +
|
252 |
+
bars.sort(reverse=True).relabel('reverse sorted')
|
253 |
+
).opts(shared_axes=False).cols(2)
|
254 |
+
```
|
255 |
+
|
256 |
+
#### Working with categorical or grouped data
|
257 |
+
|
258 |
+
Data is often grouped in various ways, and the Dataset interface provides various means to easily compare between groups and apply statistical aggregates. We'll start by generating some synthetic data with two groups along the x axis and 4 groups along the y axis.
|
259 |
+
|
260 |
+
|
261 |
+
```python
|
262 |
+
n = np.arange(1000)
|
263 |
+
xs = np.repeat(range(2), 500)
|
264 |
+
ys = n%4
|
265 |
+
zs = np.random.randn(1000)
|
266 |
+
table = hv.Table((xs, ys, zs), ['x', 'y'], 'z')
|
267 |
+
table
|
268 |
+
```
|
269 |
+
|
270 |
+
Since there are repeat observations of the same x- and y-values, we may want to reduce the data before we display it or else use a datatype that supports plotting distributions in this way. The ``BoxWhisker`` type allows doing exactly that:
|
271 |
+
|
272 |
+
|
273 |
+
```python
|
274 |
+
hv.BoxWhisker(table)
|
275 |
+
```
|
276 |
+
|
277 |
+
### Aggregating/Reducing dimensions
|
278 |
+
|
279 |
+
Most types require the data to be non-duplicated before being displayed. For this purpose, HoloViews makes it easy to ``aggregate`` and ``reduce`` the data. These two operations are simple complements of each other--aggregate computes a statistic for each group in the supplied dimensions, while reduce combines all the groups except the supplied dimensions. Supplying only a function and no dimensions will simply aggregate or reduce all available key dimensions.
|
280 |
+
|
281 |
+
|
282 |
+
```python
|
283 |
+
hv.Bars(table).aggregate('x', function=np.mean) + hv.Bars(table).reduce(x=np.mean)
|
284 |
+
```
|
285 |
+
|
286 |
+
(**A**) aggregates over both the x and y dimension, computing the mean for each x/y group, while (**B**) reduces the x dimension leaving just the mean for each group along y.
|
287 |
+
|
288 |
+
##### Collapsing multiple Dataset Elements
|
289 |
+
|
290 |
+
When multiple observations are broken out into a ``HoloMap`` they can easily be combined using the ``collapse`` method. Here we create a number of Curves with increasingly larger y-values. By collapsing them with a ``function`` and a ``spreadfn`` we can compute the mean curve with a confidence interval. We then simply cast the collapsed ``Curve`` to a ``Spread`` and ``Curve`` Element to visualize them.
|
291 |
+
|
292 |
+
|
293 |
+
```python
|
294 |
+
hmap = hv.HoloMap({i: hv.Curve(np.arange(10)*i) for i in range(10)})
|
295 |
+
collapsed = hmap.collapse(function=np.mean, spreadfn=np.std)
|
296 |
+
hv.Spread(collapsed) * hv.Curve(collapsed) + hv.Table(collapsed)
|
297 |
+
```
|
hvplot_docs/09-Gridded_Datasets.md
ADDED
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Gridded Datasets
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import xarray as xr
|
6 |
+
import numpy as np
|
7 |
+
import holoviews as hv
|
8 |
+
from holoviews import opts
|
9 |
+
hv.extension('matplotlib')
|
10 |
+
|
11 |
+
opts.defaults(opts.Scatter3D(color='Value', cmap='fire', edgecolor='black', s=50))
|
12 |
+
```
|
13 |
+
|
14 |
+
In the [Tabular Data](./08-Tabular_Datasets.ipynb) guide we covered how to work with columnar data in HoloViews. Apart from tabular or column based data there is another data format that is particularly common in the science and engineering contexts, namely multi-dimensional arrays. The gridded data interfaces allow working with grid-based datasets directly.
|
15 |
+
|
16 |
+
Grid-based datasets have two types of dimensions:
|
17 |
+
|
18 |
+
* they have coordinate or key dimensions, which describe the sampling of each dimension in the value arrays
|
19 |
+
* they have value dimensions which describe the quantity of the multi-dimensional value arrays
|
20 |
+
|
21 |
+
There are many different types of gridded datasets, which each approximate or measure a surface or space at discretely specified coordinates. In HoloViews, gridded datasets are typically one of three possible types: Regular rectilinear grids, irregular rectilinear grids, and curvilinear grids. Regular rectilinear grids can be defined by 1D coordinate arrays specifying the spacing along each dimension, while the other types require grid coordinates with the same dimensionality as the underlying value arrays, specifying the full n-dimensional coordinates of the corresponding array value. HoloViews provides many different elements supporting regularly spaced rectilinear grids, but currently only QuadMesh supports irregularly spaced rectilinear and curvilinear grids.
|
22 |
+
|
23 |
+
The difference between uniform, rectilinear and curvilinear grids is best illustrated by the figure below:
|
24 |
+
|
25 |
+
<figure>
|
26 |
+
<img src="http://earthsystemmodeling.org/docs/release/ESMF_8_1_1/ESMC_crefdoc/img11.png" alt="grid-types">
|
27 |
+
<figcaption>Types of logically rectangular grid tiles. Red circles show the values needed to specify grid coordinates for each type. Reproduced from <a href="http://earthsystemmodeling.org/docs/release/ESMF_8_1_1/ESMC_crefdoc/node5.html">ESMF documentation</a></figcaption>
|
28 |
+
</figure>
|
29 |
+
|
30 |
+
|
31 |
+
In this section we will first discuss how to work with the simpler rectilinear grids and then describe how to define a curvilinear grid with 2D coordinate arrays.
|
32 |
+
|
33 |
+
## Declaring gridded data
|
34 |
+
|
35 |
+
All Elements that support a ColumnInterface also support the GridInterface. The simplest example of a multi-dimensional (or more precisely 2D) gridded dataset is an image, which has implicit or explicit x-coordinates, y-coordinates and an array representing the values for each combination of these coordinates. Let us start by declaring an Image with explicit x- and y-coordinates:
|
36 |
+
|
37 |
+
|
38 |
+
```python
|
39 |
+
img = hv.Image((range(10), range(5), np.random.rand(5, 10)), datatype=['grid'])
|
40 |
+
img
|
41 |
+
```
|
42 |
+
|
43 |
+
In the above example we defined that there would be 10 samples along the x-axis, 5 samples along the y-axis and then defined a random ``5x10`` array, matching those dimensions. This follows the NumPy (row, column) indexing convention. When passing a tuple HoloViews will use the first gridded data interface, which stores the coordinates and value arrays as a dictionary mapping the dimension name to a NumPy array representing the data:
|
44 |
+
|
45 |
+
|
46 |
+
```python
|
47 |
+
img.data
|
48 |
+
```
|
49 |
+
|
50 |
+
However HoloViews also ships with an interface for ``xarray`` and the [GeoViews](https://geoviews.org) library ships with an interface for ``iris`` objects, which are two common libraries for working with multi-dimensional datasets:
|
51 |
+
|
52 |
+
|
53 |
+
```python
|
54 |
+
arr_img = img.clone(datatype=['image'])
|
55 |
+
print(type(arr_img.data))
|
56 |
+
|
57 |
+
try:
|
58 |
+
xr_img = img.clone(datatype=['xarray'])
|
59 |
+
|
60 |
+
print(type(xr_img.data))
|
61 |
+
except:
|
62 |
+
print('xarray interface could not be imported.')
|
63 |
+
```
|
64 |
+
|
65 |
+
In the case of an Image HoloViews also has a simple image representation which stores the data as a single array and converts the x- and y-coordinates to a set of bounds:
|
66 |
+
|
67 |
+
|
68 |
+
```python
|
69 |
+
print("Array type: %s with bounds %s" % (type(arr_img.data), arr_img.bounds))
|
70 |
+
```
|
71 |
+
|
72 |
+
To summarize the constructor accepts a number of formats where the value arrays should always match the shape of the coordinate arrays:
|
73 |
+
|
74 |
+
1. A simple np.ndarray along with (l, b, r, t) bounds
|
75 |
+
2. A tuple of the coordinate and value arrays
|
76 |
+
3. A dictionary of the coordinate and value arrays indexed by their dimension names
|
77 |
+
3. XArray DataArray or XArray Dataset
|
78 |
+
4. An Iris cube
|
79 |
+
|
80 |
+
# Working with a multi-dimensional dataset
|
81 |
+
|
82 |
+
A gridded Dataset may have as many dimensions as desired, however individual Element types only support data of a certain dimensionality. Therefore we usually declare a ``Dataset`` to hold our multi-dimensional data and take it from there.
|
83 |
+
|
84 |
+
|
85 |
+
```python
|
86 |
+
dataset3d = hv.Dataset((range(3), range(5), range(7), np.random.randn(7, 5, 3)),
|
87 |
+
['x', 'y', 'z'], 'Value')
|
88 |
+
dataset3d
|
89 |
+
```
|
90 |
+
|
91 |
+
This is because even a 3D multi-dimensional array represents volumetric data which we can display easily only if it contains few samples. In this simple case we can get an overview of what this data looks like by casting it to a ``Scatter3D`` Element (which will help us visualize the operations we are applying to the data:
|
92 |
+
|
93 |
+
|
94 |
+
```python
|
95 |
+
hv.Scatter3D(dataset3d)
|
96 |
+
```
|
97 |
+
|
98 |
+
### Indexing
|
99 |
+
|
100 |
+
In order to explore the dataset we therefore often want to define a lower dimensional slice into the array and then convert the dataset:
|
101 |
+
|
102 |
+
|
103 |
+
```python
|
104 |
+
dataset3d.select(x=1).to(hv.Image, ['y', 'z']) + hv.Scatter3D(dataset3d.select(x=1))
|
105 |
+
```
|
106 |
+
|
107 |
+
### Groupby
|
108 |
+
|
109 |
+
Another common method to apply to our data is to facet or animate the data using ``groupby`` operations. HoloViews provides a convenient interface to apply ``groupby`` operations and select which dimensions to visualize.
|
110 |
+
|
111 |
+
|
112 |
+
```python
|
113 |
+
(dataset3d.to(hv.Image, ['y', 'z'], 'Value', ['x']) +
|
114 |
+
hv.HoloMap({x: hv.Scatter3D(dataset3d.select(x=x)) for x in range(3)}, kdims='x'))
|
115 |
+
```
|
116 |
+
|
117 |
+
### Aggregating
|
118 |
+
|
119 |
+
Another common operation is to aggregate the data with a function thereby reducing a dimension. You can either ``aggregate`` the data by passing the dimensions to aggregate or ``reduce`` a specific dimension. Both have the same function:
|
120 |
+
|
121 |
+
|
122 |
+
```python
|
123 |
+
hv.Image(dataset3d.aggregate(['x', 'y'], np.mean)) + hv.Image(dataset3d.reduce(z=np.mean))
|
124 |
+
```
|
125 |
+
|
126 |
+
By aggregating the data we can reduce it to any number of dimensions we want. We can for example compute the spread of values for each z-coordinate and plot it using a ``Spread`` and ``Curve`` Element. We simply aggregate by that dimension and pass the aggregation functions we want to apply:
|
127 |
+
|
128 |
+
|
129 |
+
```python
|
130 |
+
hv.Spread(dataset3d.aggregate('z', np.mean, np.std)) * hv.Curve(dataset3d.aggregate('z', np.mean))
|
131 |
+
```
|
132 |
+
|
133 |
+
It is also possible to generate lower-dimensional views into the dataset which can be useful to summarize the statistics of the data along a particular dimension. A simple example is a box-whisker of the ``Value`` for each x-coordinate. Using the ``.to`` conversion interface we declare that we want a ``BoxWhisker`` Element indexed by the ``x`` dimension showing the ``Value`` dimension. Additionally we have to ensure to set ``groupby`` to an empty list because by default the interface will group over any remaining dimension.
|
134 |
+
|
135 |
+
|
136 |
+
```python
|
137 |
+
dataset3d.to(hv.BoxWhisker, 'x', 'Value', groupby=[])
|
138 |
+
```
|
139 |
+
|
140 |
+
Similarly we can generate a ``Distribution`` Element showing the ``Value`` dimension, group by the 'x' dimension and then overlay the distributions, giving us another statistical summary of the data:
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
dataset3d.to(hv.Distribution, 'Value', [], groupby='x').overlay()
|
145 |
+
```
|
146 |
+
|
147 |
+
## Categorical dimensions
|
148 |
+
|
149 |
+
The key dimensions of the multi-dimensional arrays do not have to represent continuous values, we can display datasets with categorical variables as a ``HeatMap`` Element:
|
150 |
+
|
151 |
+
|
152 |
+
```python
|
153 |
+
heatmap = hv.HeatMap((['A', 'B', 'C'], ['a', 'b', 'c', 'd', 'e'], np.random.rand(5, 3)))
|
154 |
+
heatmap + hv.Table(heatmap)
|
155 |
+
```
|
156 |
+
|
157 |
+
## Non-uniform rectilinear grids
|
158 |
+
|
159 |
+
As discussed above, there are two main types of grids handled by HoloViews. So far, we have mainly dealt with uniform, rectilinear grids, but we can use the ``QuadMesh`` element to work with non-uniform rectilinear grids and curvilinear grids.
|
160 |
+
|
161 |
+
In order to define a non-uniform, rectilinear grid we can declare explicit irregularly spaced x- and y-coordinates. In the example below we specify the x/y-coordinate bin edges of the grid as arrays of shape ``M+1`` and ``N+1`` and a value array (``zs``) of shape ``NxM``:
|
162 |
+
|
163 |
+
|
164 |
+
```python
|
165 |
+
n = 8 # Number of bins in each direction
|
166 |
+
xs = np.logspace(1, 3, n)
|
167 |
+
ys = np.linspace(1, 10, n)
|
168 |
+
zs = np.arange((n-1)**2).reshape(n-1, n-1)
|
169 |
+
print('Shape of x-coordinates:', xs.shape)
|
170 |
+
print('Shape of y-coordinates:', ys.shape)
|
171 |
+
print('Shape of value array:', zs.shape)
|
172 |
+
hv.QuadMesh((xs, ys, zs))
|
173 |
+
```
|
174 |
+
|
175 |
+
## Curvilinear grids
|
176 |
+
|
177 |
+
To define a curvilinear grid the x/y-coordinates of the grid should be defined as 2D arrays of shape ``NxM`` or ``N+1xM+1``, i.e. either as the bin centers or the bin edges of each 2D bin.
|
178 |
+
|
179 |
+
|
180 |
+
```python
|
181 |
+
n=20
|
182 |
+
coords = np.linspace(-1.5,1.5,n)
|
183 |
+
X,Y = np.meshgrid(coords, coords);
|
184 |
+
Qx = np.cos(Y) - np.cos(X)
|
185 |
+
Qy = np.sin(Y) + np.sin(X)
|
186 |
+
Z = np.sqrt(X**2 + Y**2)
|
187 |
+
|
188 |
+
print('Shape of x-coordinates:', Qx.shape)
|
189 |
+
print('Shape of y-coordinates:', Qy.shape)
|
190 |
+
print('Shape of value array:', Z.shape)
|
191 |
+
|
192 |
+
qmesh = hv.QuadMesh((Qx, Qy, Z))
|
193 |
+
qmesh
|
194 |
+
```
|
195 |
+
|
196 |
+
## Working with xarray data types
|
197 |
+
As demonstrated previously, `Dataset` comes with support for the `xarray` library, which offers a powerful way to work with multi-dimensional, regularly spaced data. In this example, we'll load an example dataset, turn it into a HoloViews `Dataset` and visualize it. First, let's have a look at the xarray dataset's contents:
|
198 |
+
|
199 |
+
|
200 |
+
```python
|
201 |
+
xr_ds = xr.tutorial.open_dataset("air_temperature").load()
|
202 |
+
xr_ds
|
203 |
+
```
|
204 |
+
|
205 |
+
It is trivial to turn this xarray Dataset into a Holoviews `Dataset` (the same also works for DataArray):
|
206 |
+
|
207 |
+
|
208 |
+
```python
|
209 |
+
hv_ds = hv.Dataset(xr_ds)[:, :, "2013-01-01"]
|
210 |
+
print(hv_ds)
|
211 |
+
```
|
212 |
+
|
213 |
+
We have used the usual slice notation in order to select one single day in the rather large dataset. Finally, let's visualize the dataset by converting it to a `HoloMap` of `Images` using the `to()` method. We need to specify which of the dataset's key dimensions will be consumed by the images (in this case "lat" and "lon"), where the remaining key dimensions will be associated with the HoloMap (here: "time"). We'll use the slice notation again to clip the longitude.
|
214 |
+
|
215 |
+
|
216 |
+
```python
|
217 |
+
airtemp = hv_ds.to(hv.Image, kdims=["lon", "lat"], dynamic=False)
|
218 |
+
airtemp[:, 220:320, :].opts(colorbar=True, fig_size=200)
|
219 |
+
```
|
220 |
+
|
221 |
+
Here, we have explicitly specified the default behaviour `dynamic=False`, which returns a HoloMap. Note, that this approach immediately converts all available data to images, which will take up a lot of RAM for large datasets. For these situations, use `dynamic=True` to generate a [DynamicMap](./07-Live_Data.ipynb) instead. Additionally, [xarray features dask support](http://xarray.pydata.org/en/stable/dask.html), which is helpful when dealing with large amounts of data.
|
222 |
+
|
223 |
+
It is also possible to render curvilinear grids with xarray, and here we will load one such example. The dataset below defines a curvilinear grid of air temperatures varying over time. The curvilinear grid can be identified by the fact that the ``xc`` and ``yc`` coordinates are defined as two-dimensional arrays:
|
224 |
+
|
225 |
+
|
226 |
+
```python
|
227 |
+
rasm = xr.tutorial.open_dataset("rasm").load()
|
228 |
+
rasm.coords
|
229 |
+
```
|
230 |
+
|
231 |
+
To simplify the example we will select a single timepoint and add explicit coordinates for the x and y dimensions:
|
232 |
+
|
233 |
+
|
234 |
+
```python
|
235 |
+
rasm = rasm.isel(time=0, x=slice(0, 200)).assign_coords(x=np.arange(200), y=np.arange(205))
|
236 |
+
rasm.coords
|
237 |
+
```
|
238 |
+
|
239 |
+
Now that we have defined both rectilinear and curvilinear coordinates we can visualize the difference between the two by explicitly defining which set of coordinates to use:
|
240 |
+
|
241 |
+
|
242 |
+
```python
|
243 |
+
hv.QuadMesh(rasm, ['x', 'y']) + hv.QuadMesh(rasm, ['xc', 'yc'])
|
244 |
+
```
|
245 |
+
|
246 |
+
|
247 |
+
|
248 |
+
Additional examples of visualizing xarrays in the context of geographical data can be found in the GeoViews documentation: [Gridded Datasets I](http://geoviews.org/user_guide/Gridded_Datasets_I.html) and
|
249 |
+
[Gridded Datasets II](http://geoviews.org/user_guide/Gridded_Datasets_II.html). These guides also contain useful information on the interaction between xarray data structures and HoloViews Datasets in general.
|
250 |
+
|
251 |
+
# API
|
252 |
+
|
253 |
+
## Accessing the data
|
254 |
+
|
255 |
+
In order to be able to work with data in different formats Holoviews defines a general interface to access the data. The dimension_values method allows returning underlying arrays.
|
256 |
+
|
257 |
+
#### Key dimensions (coordinates)
|
258 |
+
|
259 |
+
By default ``dimension_values`` will return the expanded columnar format of the data:
|
260 |
+
|
261 |
+
|
262 |
+
```python
|
263 |
+
heatmap.dimension_values('x')
|
264 |
+
```
|
265 |
+
|
266 |
+
To access just the unique coordinates along a dimension simply supply the ``expanded=False`` keyword:
|
267 |
+
|
268 |
+
|
269 |
+
```python
|
270 |
+
heatmap.dimension_values('x', expanded=False)
|
271 |
+
```
|
272 |
+
|
273 |
+
Finally we can also get a non-flattened, expanded coordinate array returning a coordinate array of the same shape as the value arrays
|
274 |
+
|
275 |
+
|
276 |
+
```python
|
277 |
+
heatmap.dimension_values('x', flat=False)
|
278 |
+
```
|
279 |
+
|
280 |
+
#### Value dimensions
|
281 |
+
|
282 |
+
When accessing a value dimension the method will similarly return a flat view of the data:
|
283 |
+
|
284 |
+
|
285 |
+
```python
|
286 |
+
heatmap.dimension_values('z')
|
287 |
+
```
|
288 |
+
|
289 |
+
We can pass the ``flat=False`` argument to access the multi-dimensional array:
|
290 |
+
|
291 |
+
|
292 |
+
```python
|
293 |
+
heatmap.dimension_values('z', flat=False)
|
294 |
+
```
|
hvplot_docs/10-Indexing_and_Selecting_Data.md
ADDED
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Indexing and Selecting data
|
2 |
+
|
3 |
+
As explained in the [Building composite objects](./06-Building_Composite_Objects.ipynb) and [Dimensioned Containers](./05-Dimensioned_Containers.ipynb) guides, HoloViews allows building up hierarchical containers that express the natural relationships between data items, in whatever multidimensional space best characterizes the application domain. Once your data is in such containers, individual visualizations are then made by choosing subregions of this multidimensional space, either smaller numeric ranges (as in cropping of photographic images), or lower-dimensional subsets (as in selecting frames from a movie, or a specific movie from a large library), or both (as in selecting a cropped version of a frame from a specific movie from a large library).
|
4 |
+
|
5 |
+
In this user guide, we show how to specify such selections, using five different (but related) operations that can act on an element ``e``:
|
6 |
+
|
7 |
+
| Operation | Example syntax | Description |
|
8 |
+
|:---------------|:----------------:|:-------------|
|
9 |
+
| **indexing** | e[5.5], e[3,5.5] | Selecting a single data value, returning one actual numerical value from the existing data
|
10 |
+
| **slice** | e[3:5.5], e[3:5.5,0:1] | Selecting a contiguous portion from an Element, returning the same type of Element
|
11 |
+
| **sample** | e.sample(y=5.5),<br>e.sample((3,3)) | Selecting one or more regularly spaced data values, returning a new type of Element
|
12 |
+
| **select** | e.select(y=5.5),<br>e.select(y=(3,5.5)) | More verbose notation covering all supported slice and index operations by dimension name.
|
13 |
+
| **iloc** | e[2, :],<br>e[2:5, :] | Indexes and slices by row and column tabular index supporting integer indexes, slices, lists and boolean indices.
|
14 |
+
|
15 |
+
These operations are all concerned with selecting some subset of the data values, without combining across data values (e.g. averaging) or otherwise transforming the actual data. In the [Tabular Data](./08-Tabular_Datasets.ipynb) user guide we will look at additional operations on the data that reduce, summarize, or transform the data in other ways, in addition to the selections covered here.
|
16 |
+
|
17 |
+
We'll be going through each operation in detail and provide a visual illustration to help make the semantics of each operation clear. This user guide assumes that you are familiar with continuous and discrete coordinate systems, so please review our [Continuous Coordinates](Continuous_Coordinates.ipynb) guide if you have not done so already.
|
18 |
+
|
19 |
+
|
20 |
+
```python
|
21 |
+
import numpy as np
|
22 |
+
import holoviews as hv
|
23 |
+
from holoviews import opts
|
24 |
+
|
25 |
+
hv.extension('bokeh', 'matplotlib')
|
26 |
+
|
27 |
+
opts.defaults(
|
28 |
+
opts.Bounds(line_width=2, color='red', axiswise=True),
|
29 |
+
opts.Image(cmap='Blues'),
|
30 |
+
opts.Points(size=8, padding=0.1),
|
31 |
+
opts.Text(text_font_size='16pt'), opts.Scatter(size=5))
|
32 |
+
```
|
33 |
+
|
34 |
+
# Indexing and slicing Elements
|
35 |
+
|
36 |
+
In the [Dimensioned Containers](./05-Dimensioned_Containers.ipynb) guide we saw examples of how to select individual elements embedded in a multi-dimensional space. The [Continuous Coordinates](Continuous_Coordinates.ipynb) user guide covered slicing and indexing in Elements representing continuous coordinate coordinate systems such as ``Image`` types. Here we'll be going through each operation in full detail, providing a visual illustration to help make the semantics of each operation clear.
|
37 |
+
|
38 |
+
How the ``Element`` may be indexed depends on the key dimensions (or ``kdims``) of the ``Element``. It is thus important to consider the nature and dimensionality of your data when choosing the ``Element`` type for it.
|
39 |
+
|
40 |
+
## 1D Elements: Slicing and indexing
|
41 |
+
|
42 |
+
Certain Chart elements support both single-dimensional indexing and slicing: ``Scatter``, ``Curve``, ``Histogram``, and ``ErrorBars``. Here we'll look at how we can easily slice a ``Histogram`` to select a subregion of it:
|
43 |
+
|
44 |
+
|
45 |
+
```python
|
46 |
+
np.random.seed(42)
|
47 |
+
edges, data = np.histogram(np.random.randn(100))
|
48 |
+
hist = hv.Histogram((edges, data))
|
49 |
+
subregion = hist[0:1]
|
50 |
+
hist * subregion
|
51 |
+
```
|
52 |
+
|
53 |
+
The two bins in a different color show the selected region, overlaid on top of the full histogram. We can also access the value for a specific bin in the ``Histogram``. A continuous-valued index that falls inside a particular bin will return the corresponding value or frequency.
|
54 |
+
|
55 |
+
|
56 |
+
```python
|
57 |
+
hist[0.25], hist[0.5], hist[0.55]
|
58 |
+
```
|
59 |
+
|
60 |
+
We can slice a ``Curve`` the same way:
|
61 |
+
|
62 |
+
|
63 |
+
```python
|
64 |
+
xs = np.linspace(0, np.pi*2, 21)
|
65 |
+
curve = hv.Curve((xs, np.sin(xs)))
|
66 |
+
subregion = curve[np.pi/2:np.pi*1.5]
|
67 |
+
curve * subregion * hv.Scatter(curve)
|
68 |
+
```
|
69 |
+
|
70 |
+
Here again the region in a different color is the specified subregion. We've also marked each discrete point with a dot using the ``Scatter`` ``Element``. As before we can also get the value for a specific sample point; whatever x-index is provided will snap to the closest sample point and return the dependent value:
|
71 |
+
|
72 |
+
|
73 |
+
```python
|
74 |
+
curve[4.05], curve[4.1], curve[4.17], curve[4.3]
|
75 |
+
```
|
76 |
+
|
77 |
+
It is important to note that an index (or a list of indices, as for the 2D and 3D cases below) will always return the raw indexed (dependent) value, i.e. a number. A slice (indicated with `:`), on the other hand, will retain the Element type even in cases where the plot might not be useful, such as having only a single value, two values, or no value at all in that range:
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
curve[4:4.5]
|
82 |
+
```
|
83 |
+
|
84 |
+
## 2D and 3D Elements: slicing
|
85 |
+
|
86 |
+
For data defined in a 2D space, there are 2D equivalents of the 1D ``Curve`` and ``Scatter`` types. ``Points``, for example, can be thought of as a number of points in a 2D space.
|
87 |
+
|
88 |
+
|
89 |
+
```python
|
90 |
+
r = np.arange(0, 1, 0.005)
|
91 |
+
xs, ys = (r * fn(85*np.pi*r) for fn in (np.cos, np.sin))
|
92 |
+
paths = hv.Points((xs, ys))
|
93 |
+
paths + paths[0:1, 0:1]
|
94 |
+
```
|
95 |
+
|
96 |
+
However, indexing is not supported in this space, because there could be many possible points near a given set of coordinates, and finding the nearest one would require a search across potentially incommensurable dimensions, which is poorly defined and difficult to support.
|
97 |
+
|
98 |
+
Slicing in 3D works much like slicing in 2D, but indexing is not supported for the same reason as in 2D:
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
xs = np.linspace(0, np.pi*8, 201)
|
103 |
+
scatter = hv.Scatter3D((xs, np.sin(xs), np.cos(xs)))
|
104 |
+
layout = scatter + scatter[5:10, :, 0:]
|
105 |
+
hv.output(layout, backend='matplotlib')
|
106 |
+
```
|
107 |
+
|
108 |
+
## 2D Raster and Image: slicing and indexing
|
109 |
+
|
110 |
+
Raster and the various other image-like objects (Images, RGB, HSV, etc.) can all be sliced and indexed, as can Surface, because they all have an underlying regular grid of key dimension values:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
np.random.seed(0)
|
115 |
+
extents = (0, 0, 10, 10)
|
116 |
+
img = hv.Image(np.random.rand(10, 10), bounds=extents)
|
117 |
+
img_slice = img[1:9,4:5]
|
118 |
+
box = hv.Bounds((1,4,9,5))
|
119 |
+
img*box + img_slice
|
120 |
+
```
|
121 |
+
|
122 |
+
|
123 |
+
```python
|
124 |
+
img[4.2,4.2], img[4.3,4.2], img[5.0,4.2]
|
125 |
+
```
|
126 |
+
|
127 |
+
# Tabular indexing and slicing
|
128 |
+
|
129 |
+
While most indexing in HoloViews works by selecting the values along a dimension it is also frequently useful to index and slice using integer row and column indices. For this purpose most HoloViews objects have a ``.iloc`` indexing interface (mirroring the [pandas](http://pandas.pydata.org/pandas-docs/stable/indexing.html#different-choices-for-indexing) API), which supports all the usual indexing semantics. Supported iloc arguments include:
|
130 |
+
|
131 |
+
* An integer e.g. 5
|
132 |
+
|
133 |
+
* A list or array of integers [4, 3, 0]
|
134 |
+
|
135 |
+
* A slice object with ints 1:7
|
136 |
+
|
137 |
+
* A boolean array
|
138 |
+
|
139 |
+
#### Indexing
|
140 |
+
|
141 |
+
In this way we can for example select the x- and y-values in the 8th row of our ``Curve``:
|
142 |
+
|
143 |
+
|
144 |
+
```python
|
145 |
+
xs = np.linspace(0, np.pi*2, 21)
|
146 |
+
curve = hv.Curve((xs, np.sin(xs)))
|
147 |
+
print('x: %s, y: %s' % (curve.iloc[8, 0], curve.iloc[8, 1]))
|
148 |
+
curve * hv.Scatter(curve.iloc[8])
|
149 |
+
```
|
150 |
+
|
151 |
+
#### Slicing
|
152 |
+
|
153 |
+
Alternatively we can select every second sample between indices 5 and 16 of a ``Curve``:
|
154 |
+
|
155 |
+
|
156 |
+
```python
|
157 |
+
curve + curve.iloc[5:16:2]
|
158 |
+
```
|
159 |
+
|
160 |
+
#### Lists of integers and boolean indices
|
161 |
+
|
162 |
+
Finally we may also pass a list of the integer samples to select, or use boolean indices. This mode of indexing can be very useful for randomly sampling an Element or picking a specific set of rows or (columns):
|
163 |
+
|
164 |
+
|
165 |
+
```python
|
166 |
+
curve.iloc[[0, 5, 10, 15, 20]] + curve.iloc[xs>3]
|
167 |
+
```
|
168 |
+
|
169 |
+
# Sampling
|
170 |
+
|
171 |
+
Sampling is essentially a process of indexing an Element at multiple index locations, and collecting the results. Thus any Element that can be indexed can also be sampled. Compared to regular indexing, sampling is different in that multiple indices may be supplied at the same time. Also, indexing will only return the value at that location, whereas the return type from a sampling operation is another ``Element`` type, usually either a ``Table`` or a ``Curve``, to allow both key and value dimensions to be returned.
|
172 |
+
|
173 |
+
### Sampling Elements
|
174 |
+
|
175 |
+
Sampling can use either an explicit list of indexes, or pass an index value for each dimension keyword argument.
|
176 |
+
|
177 |
+
We'll start by taking a single sample of an Image object, to make clear how sampling and indexing are similar operations yet different in their results:
|
178 |
+
|
179 |
+
|
180 |
+
```python
|
181 |
+
img_coords = hv.Points(img, extents=extents)
|
182 |
+
labeled_img = img * img_coords * hv.Points([img.closest([(4.1,4.3)])]).opts(color='r')
|
183 |
+
img + labeled_img + img.sample([(4.1,4.3)])
|
184 |
+
```
|
185 |
+
|
186 |
+
|
187 |
+
```python
|
188 |
+
img[4.1,4.3]
|
189 |
+
```
|
190 |
+
|
191 |
+
Here, the output of the indexing operation is the value (0.20887675609483469) from the location closest to the specified indexes, whereas ``.sample()`` returns a Table that lists both the coordinates *and* the value, and slicing (in previous section) returns an Element of the same type, not a Table.
|
192 |
+
|
193 |
+
|
194 |
+
Next we can try sampling along only one Dimension on our 2D Image, leaving us with a 1D Element (in this case a ``Curve``):
|
195 |
+
|
196 |
+
|
197 |
+
```python
|
198 |
+
sampled = img.sample(y=5)
|
199 |
+
labeled_img = img * img_coords * hv.Points(zip(sampled['x'], [img.closest(y=5)]*10))
|
200 |
+
img + labeled_img + sampled
|
201 |
+
```
|
202 |
+
|
203 |
+
Sampling works on any regularly sampled Element type. For example, we can select multiple samples along the x-axis of a Curve.
|
204 |
+
|
205 |
+
|
206 |
+
```python
|
207 |
+
xs = np.arange(10)
|
208 |
+
samples = [2, 4, 6, 8]
|
209 |
+
curve = hv.Curve(zip(xs, np.sin(xs)))
|
210 |
+
curve_samples = hv.Scatter(zip(xs, [0] * 10)) * hv.Scatter(zip(samples, [0]*len(samples)))
|
211 |
+
curve + curve_samples + curve.sample(samples)
|
212 |
+
```
|
213 |
+
|
214 |
+
### Sampling HoloMaps
|
215 |
+
|
216 |
+
Sampling is often useful when you have more data than you wish to visualize or analyze at one time. First, let's create a HoloMap containing a number of observations of some noisy data.
|
217 |
+
|
218 |
+
|
219 |
+
```python
|
220 |
+
obs_hmap = hv.HoloMap({i: hv.Image(np.random.randn(10, 10), bounds=extents)
|
221 |
+
for i in range(3)}, kdims='Observation')
|
222 |
+
```
|
223 |
+
|
224 |
+
A `HoloMap` may not be sampled directly, instead we can use the `.apply` method to sample each element in the HoloMap and consequently use the `.collapse` method to produce a single `Dataset`. In this case we'll take 3x3 subsamples of each of the Images:
|
225 |
+
|
226 |
+
|
227 |
+
```python
|
228 |
+
hv.output(backend='matplotlib', size=120)
|
229 |
+
|
230 |
+
sample_style = dict(edgecolors='k', alpha=1)
|
231 |
+
all_samples = obs_hmap.collapse().to.scatter3d().opts(alpha=0.15, xticks=4)
|
232 |
+
sampled = obs_hmap.apply.sample((3,3)).collapse()
|
233 |
+
subsamples = sampled.to.scatter3d().opts(**sample_style)
|
234 |
+
all_samples * subsamples + hv.Table(sampled)
|
235 |
+
```
|
236 |
+
|
237 |
+
By supplying bounds in as a (left, bottom, right, top) tuple we can also sample a subregion of our images:
|
238 |
+
|
239 |
+
|
240 |
+
```python
|
241 |
+
sampled = obs_hmap.apply.sample((3,3), bounds=(2,5,5,10)).collapse()
|
242 |
+
subsamples = sampled.to.scatter3d().opts(xticks=4, **sample_style)
|
243 |
+
all_samples * subsamples + hv.Table(sampled)
|
244 |
+
```
|
245 |
+
|
246 |
+
Since this kind of sampling is only well supported for continuous coordinate systems, we can only apply this kind of sampling to Image types for now.
|
247 |
+
|
248 |
+
### Sampling Charts
|
249 |
+
|
250 |
+
Sampling Chart-type Elements like Curve, Scatter, Histogram is only supported by providing an explicit list of samples, since those Elements have no underlying regular grid.
|
251 |
+
|
252 |
+
|
253 |
+
```python
|
254 |
+
hv.output(backend='bokeh')
|
255 |
+
|
256 |
+
xs = np.arange(10)
|
257 |
+
extents = (0, 0, 2, 10)
|
258 |
+
curve = hv.HoloMap({(i) : hv.Curve(zip(xs, np.sin(xs)*i))
|
259 |
+
for i in np.linspace(0.5, 1.5, 3)},
|
260 |
+
kdims='Observation')
|
261 |
+
all_samples = curve.collapse().to.points()
|
262 |
+
sampled = curve.apply.sample([0, 2, 4, 6, 8]).collapse()
|
263 |
+
sample_points = sampled.to.points(extents=extents)
|
264 |
+
sampling = all_samples * sample_points.opts(color='red')
|
265 |
+
sampling + hv.Table(sampled)
|
266 |
+
```
|
267 |
+
|
268 |
+
These tools should help you index, slice, sample, and select your data with ease. The [Tabular Data](./07-Tabular_Data.ipynb) guide explains how to do other types of operations, such as averaging and other reduction operations.
|
hvplot_docs/11-Transforming_Elements.md
ADDED
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Applying Transformations
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import param
|
6 |
+
import numpy as np
|
7 |
+
import holoviews as hv
|
8 |
+
from holoviews import opts
|
9 |
+
|
10 |
+
hv.extension('bokeh', 'matplotlib')
|
11 |
+
```
|
12 |
+
|
13 |
+
HoloViews objects provide a convenient way of wrapping your data along with some metadata for exploration and visualization. For the simplest visualizations, you can simply declare a small collection of elements which can then be composed or placed in an appropriate container. As soon as the task becomes more complex, it is natural to write functions that output HoloViews objects.
|
14 |
+
|
15 |
+
In this user guide, we will introduce to related concepts to express transforms of some data, first we will cover `dim` transforms to express simple transforms of some data and then ``Operation`` classes to express more complex transformations. Operations provide a consistent structure for such code, making it possible to write general functions that can process HoloViews objects. This enables powerful new ways of specifying HoloViews objects computed from existing data, allowing the construction of flexible data processing pipelines. Examples of such operations are ``histogram``, ``rolling``, ``datashade`` or ``decimate``, which apply some computation on certain types of Element and return a new Element with the transformed data.
|
16 |
+
|
17 |
+
In this user guide we will discover how transforms and operations work, how to control their parameters and how to chain them. The [Data Processing Pipelines](./14-Data_Pipelines.ipynb) guide extends what we will have learned to demonstrate how operations can be applied lazily by using the ``dynamic`` flag, letting us define deferred processing pipelines that can drive highly complex visualizations and dashboards.
|
18 |
+
|
19 |
+
## Transforms
|
20 |
+
|
21 |
+
A transform is expressed using a `dim` expression, which we originally introduced in the context of the [Style Mapping](./04-Style_Mapping.ipynb) user guide. It allows expressing some deferred computation on a HoloViews Element. This can be a powerful way to transform some
|
22 |
+
data quickly and easily. Let us start by declaring a `Dataset` with a single dimension `x`:
|
23 |
+
|
24 |
+
|
25 |
+
```python
|
26 |
+
ds = hv.Dataset(np.linspace(0, np.pi), 'x')
|
27 |
+
ds
|
28 |
+
```
|
29 |
+
|
30 |
+
The `Dataset` x values consist of an array of monotonically increasing values from 0 to np.pi. We can now define a transform which takes these values and transform them:
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
expr = np.sin(hv.dim('x')*10+5)
|
35 |
+
expr
|
36 |
+
```
|
37 |
+
|
38 |
+
This expression takes these values multiplies them by 10, adds 5 and then calculates the `sine`. Using the `.transform` method we can now apply this expression to the `Dataset` and assign the values to a newly created `y` dimension by supplying it as a keyword (in the same way we could override the `x` dimension):
|
39 |
+
|
40 |
+
|
41 |
+
```python
|
42 |
+
transformed = ds.transform(y=expr)
|
43 |
+
transformed
|
44 |
+
```
|
45 |
+
|
46 |
+
We can see the result of this by casting it to a `Curve`:
|
47 |
+
|
48 |
+
|
49 |
+
```python
|
50 |
+
hv.Curve(transformed)
|
51 |
+
```
|
52 |
+
|
53 |
+
This allows almost any mathematical transformation to be expressed and applied on a `Dataset` in a deferred way. The regular `dim` expression supports all the standard mathematical operators and NumPy array methods. However if we want to use methods which exist only on specific datatypes we can invoke them using `.df` or `.xr`, which let you make (pandas) dataframe and xarray API (method and accessor) calls respectively. Let us for example load an XArray Dataset, which has a number of custom methods to perform complex computations on the data, e.g. the quantile method:
|
54 |
+
|
55 |
+
|
56 |
+
```python
|
57 |
+
import xarray as xr
|
58 |
+
|
59 |
+
air_temp = xr.tutorial.load_dataset('air_temperature')
|
60 |
+
print(air_temp.quantile.__doc__)
|
61 |
+
```
|
62 |
+
|
63 |
+
We can construct an expression to apply this method on the data and compute the 95th percentile of air temperatures along the 'time' dimension:
|
64 |
+
|
65 |
+
|
66 |
+
```python
|
67 |
+
quantile_expr = hv.dim('air').xr.quantile(0.95, dim='time')
|
68 |
+
quantile_expr
|
69 |
+
```
|
70 |
+
|
71 |
+
Now we can apply this to the `Dataset` using the `transform` method, in the resulting dataset we can see that the time dimension has been dropped:
|
72 |
+
|
73 |
+
|
74 |
+
```python
|
75 |
+
temp_ds = hv.Dataset(air_temp, ['lon', 'lat', 'time'])
|
76 |
+
|
77 |
+
transformed_ds = temp_ds.transform(air=quantile_expr)
|
78 |
+
|
79 |
+
transformed_ds
|
80 |
+
```
|
81 |
+
|
82 |
+
To visualize this data we will cast it to an `Image`:
|
83 |
+
|
84 |
+
|
85 |
+
```python
|
86 |
+
hv.Image(transformed_ds)
|
87 |
+
```
|
88 |
+
|
89 |
+
The real power of `dim` transforms comes in when combining them with parameters. We will look at this in more detail later as part of the [Pipeline user guide](14-Data_Pipelines.ipynb) but let's quickly see what this looks like. We will create a [Panel](https://panel.holoviz.org) slider to control the `q` value in the call to the `quantile` method:
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
import panel as pn
|
94 |
+
|
95 |
+
q = pn.widgets.FloatSlider(name='quantile')
|
96 |
+
|
97 |
+
quantile_expr = hv.dim('air').xr.quantile(q, dim='time')
|
98 |
+
quantile_expr
|
99 |
+
```
|
100 |
+
|
101 |
+
Now that we have expressed this dynamic `dim` transform let us apply it using `.apply.transform`:
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
temp_ds = hv.Dataset(air_temp, ['lon', 'lat'])
|
106 |
+
transformed = temp_ds.apply.transform(air=quantile_expr).apply(hv.Image)
|
107 |
+
|
108 |
+
pn.Column(q, transformed.opts(colorbar=True, width=400))
|
109 |
+
```
|
110 |
+
|
111 |
+
`dim` expressions provide a very powerful way to apply transforms on your data either statically or controlled by some external parameter, e.g. one driven by a Panel widget.
|
112 |
+
|
113 |
+
## Operations are parameterized
|
114 |
+
|
115 |
+
In cases a simple transform is not sufficient or you want to encapsulate some transformation in a more rigorous way an `Operation` allows encapsulating the parameters of a transform on a function-like object. Operations in HoloViews are subclasses of ``Operation``, which transform one Element or ``Overlay`` of Elements by returning a new Element that may be a transformation of the original. All operations are parameterized using the [param](https://github.com/holoviz/param) library which allows easy validation and documentation of the operation arguments. In particular, operations are instances of ``param.ParameterizedFunction`` which allows operations to be used in the same way as normal python functions.
|
116 |
+
|
117 |
+
This approach has several advantages, one of which is that we can manipulate the parameters of operations at several different levels: at the class-level, at the instance-level or when it is called. Another advantage is that using parameterizing operations allows them to be inspected just like any other HoloViews object using ``hv.help``. We will now do this for the ``histogram`` operation:
|
118 |
+
|
119 |
+
|
120 |
+
```python
|
121 |
+
from holoviews.operation import histogram
|
122 |
+
hv.help(histogram)
|
123 |
+
```
|
124 |
+
|
125 |
+
## Applying operations
|
126 |
+
|
127 |
+
Above we can see a listing of all the parameters of the operation, with the defaults, the expected types and detailed docstrings for each one. The ``histogram`` operation can be applied to any Element and will by default generate a histogram for the first value dimension defined on the object it is applied to. As a simple example we can create an ``BoxWhisker`` Element containing samples from a normal distribution, and then apply the ``histogram`` operation to those samples in two ways: 1) by creating an instance on which we will change the ``num_bins`` and 2) by passing ``bin_range`` directly when calling the operation:
|
128 |
+
|
129 |
+
|
130 |
+
```python
|
131 |
+
boxw = hv.BoxWhisker(np.random.randn(10000))
|
132 |
+
histop_instance = histogram.instance(num_bins=50)
|
133 |
+
|
134 |
+
boxw + histop_instance(boxw).relabel('num_bins=50') + histogram(boxw, bin_range=(0, 3)).relabel('bin_range=(0, 3)')
|
135 |
+
```
|
136 |
+
|
137 |
+
We can see that these two ways of using operations gives us convenient control over how the parameters are applied. An instance allows us to persist some defaults which will be used in all subsequent calls, while passing keyword arguments to the operations applies the parameters for just that particular call.
|
138 |
+
|
139 |
+
The third way to manipulate parameters is to set them at the class level. If we always want to use ``num_bins=30`` instead of the default of ``num_bins=20`` shown in the help output above, we can simply set ``histogram.num_bins=30``.
|
140 |
+
|
141 |
+
## Operations on containers
|
142 |
+
|
143 |
+
``Operations`` in HoloViews are applied to individual elements, which means that when you apply an operation to a container object (such as ``NdLayout``, ``GridSpace`` and ``HoloMap``) the operation is applied once per element. For an operation to work, all the elements must be of the same type which means the operation effectively acts to map the operation over all the contained elements. As a simple example we can define a HoloMap of ``BoxWhisker`` Elements by varying the width of the distribution via the ``Sigma`` value and then apply the histogram operation to it:
|
144 |
+
|
145 |
+
|
146 |
+
```python
|
147 |
+
holomap = hv.HoloMap({(i*0.1+0.1): hv.BoxWhisker(np.random.randn(10000)*(i*0.1+0.1)) for i in range(5)},
|
148 |
+
kdims='Sigma')
|
149 |
+
holomap + histogram(holomap)
|
150 |
+
```
|
151 |
+
|
152 |
+
As you can see the operation has generated a ``Histogram`` for each value of ``Sigma`` in the ``HoloMap``. In this way we can apply the operation to the entire parameter space defined by a ``HoloMap``, ``GridSpace``, and ``NdLayout``.
|
153 |
+
|
154 |
+
## Combining operations
|
155 |
+
|
156 |
+
Since operations take a HoloViews object as input and return another HoloViews object we can very easily chain and combine multiple operations to perform complex analyses quickly and easily, while instantly visualizing the output.
|
157 |
+
|
158 |
+
In this example we'll work with operations on timeseries. We first define a small function to generate a random, noisy timeseries:
|
159 |
+
|
160 |
+
|
161 |
+
```python
|
162 |
+
from holoviews.operation import timeseries
|
163 |
+
|
164 |
+
def time_series(T = 1, N = 100, mu = 0.1, sigma = 0.1, S0 = 20):
|
165 |
+
"""Parameterized noisy time series"""
|
166 |
+
dt = float(T)/N
|
167 |
+
t = np.linspace(0, T, N)
|
168 |
+
W = np.random.standard_normal(size = N)
|
169 |
+
W = np.cumsum(W)*np.sqrt(dt) # standard brownian motion
|
170 |
+
X = (mu-0.5*sigma**2)*t + sigma*W
|
171 |
+
S = S0*np.exp(X) # geometric brownian motion
|
172 |
+
return S
|
173 |
+
|
174 |
+
curve = hv.Curve(time_series(N=1000)).opts(width=600)
|
175 |
+
```
|
176 |
+
|
177 |
+
Now we will start applying some operations to this data. HoloViews ships with two ready-to-use timeseries operations: the ``rolling`` operation, which applies a function over a rolling window, and a ``rolling_outlier_std`` operation that computes outlier points in a timeseries by excluding points less than ``sigma`` standard deviation removed from the rolling mean:
|
178 |
+
|
179 |
+
|
180 |
+
```python
|
181 |
+
smoothed = curve * timeseries.rolling(curve) * timeseries.rolling_outlier_std(curve)
|
182 |
+
smoothed.opts(opts.Scatter(color='black'))
|
183 |
+
```
|
184 |
+
|
185 |
+
In the next section we will define a custom operation that will compose with the ``smoothed`` operation output above to form a short operation pipeline.
|
186 |
+
|
187 |
+
## Defining custom operations
|
188 |
+
|
189 |
+
We can now define our own custom ``Operation`` which as you may recall can process either elements and overlays. This means we can define a simple operation that takes our ``smoothed`` overlay and computes the difference between the raw and smoothed curves that it contains. Such a subtraction will give us the residual between the smoothed and unsmoothed ``Curve`` elements, removing long-term trends and leaving the short-term variation.
|
190 |
+
|
191 |
+
Defining an operation is very simple. An ``Operation`` subclass should define a ``_process`` method, which simply accepts an ``element`` argument. Optionally we can also define parameters on the operation, which we can access using the ``self.p`` attribute on the operation. In this case we define a ``String`` parameter, which specifies the name of the subtracted value dimension on the returned Element.
|
192 |
+
|
193 |
+
|
194 |
+
```python
|
195 |
+
from holoviews.operation import Operation
|
196 |
+
|
197 |
+
class residual(Operation):
|
198 |
+
"""
|
199 |
+
Subtracts two curves from one another.
|
200 |
+
"""
|
201 |
+
|
202 |
+
label = param.String(default='Residual', doc="""
|
203 |
+
Defines the label of the returned Element.""")
|
204 |
+
|
205 |
+
def _process(self, element, key=None):
|
206 |
+
# Get first and second Element in overlay
|
207 |
+
el1, el2 = element.get(0), element.get(1)
|
208 |
+
|
209 |
+
# Get x-values and y-values of curves
|
210 |
+
xvals = el1.dimension_values(0)
|
211 |
+
yvals = el1.dimension_values(1)
|
212 |
+
yvals2 = el2.dimension_values(1)
|
213 |
+
|
214 |
+
# Return new Element with subtracted y-values
|
215 |
+
# and new label
|
216 |
+
return el1.clone((xvals, yvals-yvals2),
|
217 |
+
vdims=self.p.label)
|
218 |
+
```
|
219 |
+
|
220 |
+
Having defined the residual operation let's try it out right away by applying it to our original and smoothed ``Curve``. We'll place the two objects on top of each other so they can share an x-axis and we can compare them directly:
|
221 |
+
|
222 |
+
|
223 |
+
```python
|
224 |
+
(smoothed + residual(smoothed).opts(xaxis=None)).cols(1)
|
225 |
+
```
|
226 |
+
|
227 |
+
In this view we can immediately see that only a very small residual is left when applying this level of smoothing. However we have only tried one particular ``rolling_window`` value, the default value of ``10``. To assess how this parameter affects the residual we can evaluate the operation over a number different parameter settings, as we will see in the next section.
|
228 |
+
|
229 |
+
## Evaluating operation parameters
|
230 |
+
|
231 |
+
When applying an operation there are often parameters to vary. Using traditional plotting approaches it's often difficult to evaluate them interactively to get a detailed understanding of what they do. Here we will apply the ``rolling`` operations with varying ``rolling_window`` widths and ``window_type``s across a ``HoloMap``:
|
232 |
+
|
233 |
+
|
234 |
+
```python
|
235 |
+
rolled = hv.HoloMap({(w, str(wt)): timeseries.rolling(curve, rolling_window=w, window_type=wt)
|
236 |
+
for w in [10, 25, 50, 100, 200] for wt in [None, 'hamming', 'triang']},
|
237 |
+
kdims=['Window', 'Window Type'])
|
238 |
+
rolled
|
239 |
+
```
|
240 |
+
|
241 |
+
This visualization is already useful since we can compare the effect of various parameter values by moving the slider and trying different window options. However since we can also chain operations we can easily compute the residual and view the two together.
|
242 |
+
|
243 |
+
To do this we simply overlay the ``HoloMap`` of smoothed curves on top of the original curve and pass it to our new ``residual`` function. Then we can combine the smoothed view with the original and see how the smoothing and residual curves vary across parameter values:
|
244 |
+
|
245 |
+
|
246 |
+
```python
|
247 |
+
(curve * rolled + residual(curve * rolled)).cols(1)
|
248 |
+
```
|
249 |
+
|
250 |
+
Using a few additional lines we have now evaluated the operation over a number of different parameters values, allowing us to process the data with different smoothing parameters. In addition, by interacting with this visualization we can gain a better understanding of the operation parameters as well as gain insights into the structure of the underlying data.
|
251 |
+
|
252 |
+
## Operations on 2D elements
|
253 |
+
|
254 |
+
Let's look at another example of operations in action, this time applying a simple filter to an `Image`. The basic idea is the same as above, although accessing the values to be transformed is a bit more complicated. First, we prepare an example image:
|
255 |
+
|
256 |
+
|
257 |
+
```python
|
258 |
+
hv.output(backend='matplotlib', size=200)
|
259 |
+
|
260 |
+
from scipy.misc import ascent
|
261 |
+
|
262 |
+
stairs_image = hv.Image(ascent()[200:500, :], bounds=[0, 0, ascent().shape[1], 300], label="stairs")
|
263 |
+
stairs_image
|
264 |
+
```
|
265 |
+
|
266 |
+
We'll define a simple ``Operation``, which takes an ``Image`` and applies a high-pass or low-pass filter. We then use this to build a ``HoloMap`` of images filtered with different sigma values:
|
267 |
+
|
268 |
+
|
269 |
+
```python
|
270 |
+
from scipy import ndimage
|
271 |
+
|
272 |
+
class image_filter(hv.Operation):
|
273 |
+
|
274 |
+
sigma = param.Number(default=5)
|
275 |
+
|
276 |
+
type_ = param.String(default="low-pass")
|
277 |
+
|
278 |
+
def _process(self, element, key=None):
|
279 |
+
xs = element.dimension_values(0, expanded=False)
|
280 |
+
ys = element.dimension_values(1, expanded=False)
|
281 |
+
|
282 |
+
# setting flat=False will preserve the matrix shape
|
283 |
+
data = element.dimension_values(2, flat=False)
|
284 |
+
|
285 |
+
if self.p.type_ == "high-pass":
|
286 |
+
new_data = data - ndimage.gaussian_filter(data, self.p.sigma)
|
287 |
+
else:
|
288 |
+
new_data = ndimage.gaussian_filter(data, self.p.sigma)
|
289 |
+
|
290 |
+
label = element.label + " ({} filtered)".format(self.p.type_)
|
291 |
+
# make an exact copy of the element with all settings, just with different data and label:
|
292 |
+
element = element.clone((xs, ys, new_data), label=label)
|
293 |
+
return element
|
294 |
+
|
295 |
+
stairs_map = hv.HoloMap({sigma: image_filter(stairs_image, sigma=sigma)
|
296 |
+
for sigma in range(0, 12, 1)}, kdims="sigma")
|
297 |
+
|
298 |
+
stairs_map.opts(framewise=True)
|
299 |
+
```
|
300 |
+
|
301 |
+
Just as in the previous example, it is quite straight-forward to build a HoloMap containing results for different parameter values. Inside the ``_process()`` method, the given parameters can be accessed as ``self.p.<parameter-name>`` (note that ``self.<parameter_name>`` always contains the default value!). Since we did not specify the ``type_`` parameter, it defaulted to "low-pass".
|
302 |
+
|
303 |
+
There are some peculiarities when applying operations to two-dimensional elements:
|
304 |
+
|
305 |
+
- Understanding the ``dimension_values()`` method: In principle, one could use ``element.data`` to access the element's data, however, since HoloViews can wrap a wide range of data formats, ``dimension_values()`` provides an API that lets you access the data without having to worry about the type of the data. The first parameter specifies the dimension to be returned. On a 2D element like an Image or Raster the first two dimensions reference the key dimensions, so passing an index of 0 or 1 will return the x- and y-axis values respectively. Any subsequent dimensions will be value dimensions, e.g. on an Image index value 2 will refer to the intensity values and on an RGB index values 2, 3, and 4 will return the Red, Green and Blue intensities instead. Setting ``expanded=False`` yields only the axis, while the default setting ``expanded=True`` returns a value for every pixel. Specifying ``flat=False`` means that the data's matrix shape will be preserved, which is what we need for this kind of filter.
|
306 |
+
- ``Image`` and related classes come with convenient methods to convert between matrix indices and data coordinates and vice versa: ``matrix2sheet()`` and ``sheet2matrix()``. This is useful when searching for features such as peaks.
|
307 |
+
|
308 |
+
A very powerful aspect of operations is the fact that they understand Holoviews data structures. This means it is very straight-forward to apply an operation to every element in a container. As an example, let's apply an additional high-pass filter to our HoloMap:
|
309 |
+
|
310 |
+
|
311 |
+
```python
|
312 |
+
image_filter(stairs_map, type_="high-pass").opts(framewise=True)
|
313 |
+
```
|
314 |
+
|
315 |
+
Note, that the sigma value for the high-pass filter has defaulted to 5, and the sigma value in the HoloMap still corresponds to the original low-pass filter.
|
316 |
+
|
317 |
+
|
318 |
+
## Benefits of using ``Operation``
|
319 |
+
|
320 |
+
Now that we have seen some operations in action we can get some appreciation of what makes them useful. When working with data interactively we often end up applying a lot of ad-hoc data transforms, which provides maximum flexibility but is neither reproducible nor maintainable. Operations allow us to encapsulate analysis code using a well defined interface that is well suited for building complex analysis pipelines:
|
321 |
+
|
322 |
+
1. ``Operation`` parameters are well defined by declaring parameters on the class. These parameters can be easily documented and automatically carry out validation on the types and ranges of the inputs. These parameters are documented using ``hv.help``.
|
323 |
+
|
324 |
+
2. Both inputs and outputs of an operation are instantly visualizable, because the data **is** the visualization. This means you're not constantly context switching between data processing and visualization --- visualization comes for free as you build your data processing pipeline.
|
325 |
+
|
326 |
+
3. Operations understand HoloViews datastructures and can be immediately applied to any appropriate collection of elements, allowing you to evaluate the operation with permutations of parameter values. This flexibility makes it easy to assess the effect of operation parameters and their effect on your data.
|
327 |
+
|
328 |
+
4. As we will discover in the [Data processing pipelines](./14-Data_Pipelines.ipynb) guide, operations can be applied lazily to build up complex deferred data-processing pipelines, which can aid your data exploration and drive interactive visualizations and dashboards.
|
329 |
+
|
330 |
+
## Other types of operation
|
331 |
+
|
332 |
+
As we have seen ``Operation`` is defined at the level of processing HoloViews elements or overlays of elements. In some situations, you may want to compute a new HoloViews datastructure from a number of elements contained in a structure other than an overlay, such as a HoloMap or a Layout.
|
333 |
+
|
334 |
+
One such pattern is an operation that accepts and returns a ``HoloMap`` where each of the output element depends on all the data in the input ``HoloMap``. For situations such as these, subclassing ``Operation`` is not appropriate and we recommend defining your own function. These custom operation types won't automatically gain support for lazy pipelines as described in the [Data processing pipelines](./14-Data_Pipelines.ipynb) guide and how these custom operations are pipelined is left as a design decision for the user. Note that as long as these functions return simple elements or containers, their output can be used by subclasses of ``Operation`` as normal.
|
335 |
+
|
336 |
+
What we *do* recommend is that you subclass from ``param.ParameterizedFunction`` so that you can declare well-documented and validated parameters, add a description of your operation with a class level docstring and gain automatic documentation support via ``hv.help``.
|
hvplot_docs/12-Responding_to_Events.md
ADDED
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Responding to Events
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import holoviews as hv
|
7 |
+
from holoviews import opts
|
8 |
+
|
9 |
+
hv.extension('bokeh')
|
10 |
+
```
|
11 |
+
|
12 |
+
In the [Live Data](./07-Live_Data.ipynb) guide we saw how ``DynamicMap`` allows us to explore high dimensional data using the widgets in the same style as ``HoloMaps``. Although suitable for unbounded exploration of large parameter spaces, the ``DynamicMaps`` described in that notebook support exactly the same mode of interaction as ``HoloMaps``. In particular, the key dimensions are used to specify a set of widgets that when manipulated apply the appropriate indexing to invoke the user-supplied callable.
|
13 |
+
|
14 |
+
In this user guide we will explore the HoloViews streams system that allows *any* sort of value to be supplied from *anywhere*. This system opens a huge set of new possible visualization types, including continuously updating plots that reflect live data as well as dynamic visualizations that can be interacted with directly, as described in the [Custom Interactivity](./13-Custom_Interactivity.ipynb) guide.
|
15 |
+
|
16 |
+
<center><div class="alert alert-info" role="alert">To use visualize and use a <b>DynamicMap</b> you need to be running a live Jupyter server.<br>When viewing this user guide as part of the documentation DynamicMaps will be sampled with a limited number of states.<br></div></center>
|
17 |
+
|
18 |
+
|
19 |
+
```python
|
20 |
+
# Styles and plot options used in this user guide
|
21 |
+
|
22 |
+
opts.defaults(
|
23 |
+
opts.Area(fill_color='cornsilk', line_width=2,
|
24 |
+
line_color='black'),
|
25 |
+
opts.Ellipse(bgcolor='white', color='black'),
|
26 |
+
opts.HLine(color='red', line_width=2),
|
27 |
+
opts.Image(cmap='viridis'),
|
28 |
+
opts.Path(bgcolor='white', color='black', line_dash='dashdot',
|
29 |
+
show_grid=False),
|
30 |
+
opts.VLine(color='red', line_width=2))
|
31 |
+
```
|
32 |
+
|
33 |
+
## A simple ``DynamicMap``
|
34 |
+
|
35 |
+
Before introducing streams, let us declare a simple ``DynamicMap`` of the sort discussed in the [Live Data](07-Live_Data.ipynb) user guide. This example consists of a ``Curve`` element showing a [Lissajous curve](https://en.wikipedia.org/wiki/Lissajous_curve) with ``VLine`` and ``HLine`` annotations to form a crosshair:
|
36 |
+
|
37 |
+
|
38 |
+
```python
|
39 |
+
lin = np.linspace(-np.pi,np.pi,300)
|
40 |
+
|
41 |
+
def lissajous(t, a=3, b=5, delta=np.pi/2.):
|
42 |
+
return (np.sin(a * t + delta), np.sin(b * t))
|
43 |
+
|
44 |
+
def lissajous_crosshair(t, a=3, b=5, delta=np.pi/2):
|
45 |
+
(x,y) = lissajous(t,a,b,delta)
|
46 |
+
return hv.VLine(x) * hv.HLine(y)
|
47 |
+
|
48 |
+
crosshair = hv.DynamicMap(lissajous_crosshair, kdims='t').redim.range(t=(-3.,3.))
|
49 |
+
|
50 |
+
path = hv.Path(lissajous(lin))
|
51 |
+
|
52 |
+
path * crosshair
|
53 |
+
```
|
54 |
+
|
55 |
+
As expected, the declared key dimension (``kdims``) has turned into a slider widget that lets us move the crosshair along the curve. Now let's see how to position the crosshair using streams.
|
56 |
+
|
57 |
+
## Introducing streams
|
58 |
+
|
59 |
+
|
60 |
+
|
61 |
+
The core concept behind a stream is simple: it defines one or more parameters that can change over time that automatically refreshes code depending on those parameter values.
|
62 |
+
|
63 |
+
Like all objects in HoloViews, these parameters are declared using [param](https://param.holoviz.org/) and streams are defined as a parameterized subclass of the ``holoviews.streams.Stream``. A more convenient way is to use the ``Stream.define`` classmethod:
|
64 |
+
|
65 |
+
|
66 |
+
```python
|
67 |
+
from holoviews.streams import Stream, param
|
68 |
+
Time = Stream.define('Time', t=0.0)
|
69 |
+
```
|
70 |
+
|
71 |
+
This results in a ``Time`` class with a numeric ``t`` parameter that defaults to zero. As this object is parameterized, we can use ``hv.help`` to view its parameters:
|
72 |
+
|
73 |
+
|
74 |
+
```python
|
75 |
+
hv.help(Time)
|
76 |
+
```
|
77 |
+
|
78 |
+
This parameter is a ``param.Number`` as we supplied a float, if we had supplied an integer it would have been a ``param.Integer``. Notice that there is no docstring in the help output above but we can add one by explicitly defining the parameter as follows:
|
79 |
+
|
80 |
+
|
81 |
+
```python
|
82 |
+
Time = Stream.define('Time', t=param.Number(default=0.0, doc='A time parameter'))
|
83 |
+
hv.help(Time)
|
84 |
+
```
|
85 |
+
|
86 |
+
Now we have defined this ``Time`` stream class, we can make of an instance of it and look at its parameters:
|
87 |
+
|
88 |
+
|
89 |
+
```python
|
90 |
+
time_dflt = Time()
|
91 |
+
print('This Time instance has parameter t={t}'.format(t=time_dflt.t))
|
92 |
+
```
|
93 |
+
|
94 |
+
As with all parameterized classes, we can choose to instantiate our parameters with suitable values instead of relying on defaults.
|
95 |
+
|
96 |
+
|
97 |
+
```python
|
98 |
+
time = Time(t=np.pi/4)
|
99 |
+
print('This Time instance has parameter t={t}'.format(t=time.t))
|
100 |
+
```
|
101 |
+
|
102 |
+
For more information on defining ``Stream`` classes this way, use ``hv.help(Stream.define)``.
|
103 |
+
|
104 |
+
### Simple streams example
|
105 |
+
|
106 |
+
We can now supply this streams object to a ``DynamicMap`` using the same ``lissajous_crosshair`` callback from above by adding it to the ``streams`` list:
|
107 |
+
|
108 |
+
|
109 |
+
```python
|
110 |
+
dmap = hv.DynamicMap(lissajous_crosshair, streams=[time])
|
111 |
+
path * dmap + path * lissajous_crosshair(t=np.pi/4.)
|
112 |
+
```
|
113 |
+
|
114 |
+
Immediately we see that the crosshair position of the ``DynamicMap`` reflects the ``t`` parameter values we set on the ``Time`` stream. This means that the ``t`` parameter was supplied as the argument to the ``lissajous_curve`` callback. As we now have no key dimensions, there is no longer a widget for the ``t`` dimensions.
|
115 |
+
|
116 |
+
Although we have what looks like a static plot, it is in fact dynamic and can be updated in place at any time. To see this, we can call the ``event`` method on our ``DynamicMap``:
|
117 |
+
|
118 |
+
|
119 |
+
|
120 |
+
```python
|
121 |
+
dmap.event(t=0.2)
|
122 |
+
```
|
123 |
+
|
124 |
+
Running this cell will have updated the crosshair from its original position where $t=\frac{\pi}{4}$ to a new position where ``t=0.2``. Try running the cell above with different values of ``t`` and watch the plot update!
|
125 |
+
|
126 |
+
This ``event`` method is the recommended way of updating the stream parameters on a ``DynamicMap`` but if you have a handle on the relevant stream instance, you can also call the ``event`` method on that:
|
127 |
+
|
128 |
+
|
129 |
+
```python
|
130 |
+
time.event(t=-0.2)
|
131 |
+
```
|
132 |
+
|
133 |
+
Running the cell above also moves the crosshair to a new position. As there are no key dimensions, there is only a single valid (empty) key that can be accessed with ``dmap[()]`` or ``dmap.select()`` making ``event`` the only way to explore new parameters.
|
134 |
+
|
135 |
+
We will examine the ``event`` method and the machinery that powers streams in more detail later in the user guide after we have looked at more examples of how streams are used in practice.
|
136 |
+
|
137 |
+
### Working with multiple streams
|
138 |
+
|
139 |
+
The previous example showed a curve parameterized by a single dimension ``t``. Often you will have multiple stream parameters you would like to declare as follows:
|
140 |
+
|
141 |
+
|
142 |
+
```python
|
143 |
+
ls = np.linspace(0, 10, 200)
|
144 |
+
xx, yy = np.meshgrid(ls, ls)
|
145 |
+
|
146 |
+
XY = Stream.define('XY',x=0.0,y=0.0)
|
147 |
+
|
148 |
+
def marker(x,y):
|
149 |
+
return hv.VLine(x) * hv.HLine(y)
|
150 |
+
|
151 |
+
image = hv.Image(np.sin(xx)*np.cos(yy))
|
152 |
+
|
153 |
+
dmap = hv.DynamicMap(marker, streams=[XY()])
|
154 |
+
|
155 |
+
image * dmap
|
156 |
+
```
|
157 |
+
|
158 |
+
You can update both ``x`` and ``y`` by passing multiple keywords to the ``event`` method:
|
159 |
+
|
160 |
+
|
161 |
+
```python
|
162 |
+
dmap.event(x=-0.2, y=0.1)
|
163 |
+
```
|
164 |
+
|
165 |
+
Note that the definition above behaves the same as the following definition where we define separate ``X`` and ``Y`` stream classes:
|
166 |
+
|
167 |
+
```python
|
168 |
+
X = Stream.define('X',x=0.0)
|
169 |
+
Y = Stream.define('Y',y=0.0)
|
170 |
+
hv.DynamicMap(marker, streams=[X(), Y()])
|
171 |
+
```
|
172 |
+
|
173 |
+
The reason why you might want to list multiple streams instead of always defining a single stream containing all the required stream parameters will be made clear in the [Custom Interactivity](./13-Custom_Interactivity.ipynb) guide.
|
174 |
+
|
175 |
+
## Using Parameterized classes as a stream
|
176 |
+
|
177 |
+
Creating a custom ``Stream`` class is one easy way to declare parameters. However, there's no need to make a Stream if you have already expressed your domain knowledge on a ``Parameterized`` class. For instance, let's assume you have made a simple parameterized `BankAccount` class:
|
178 |
+
|
179 |
+
|
180 |
+
|
181 |
+
```python
|
182 |
+
class BankAccount(param.Parameterized):
|
183 |
+
balance = param.Number(default=0, doc="Bank balance in USD")
|
184 |
+
overdraft = param.Number(default=200, doc="Overdraft limit")
|
185 |
+
|
186 |
+
account = BankAccount(name='Jane', balance=300)
|
187 |
+
```
|
188 |
+
|
189 |
+
You can link parameter changes straight to DynamicMap callable parameters by passing a keyword:param dictionary to the `streams` argument (for HoloViews version >= 1.14.2):
|
190 |
+
|
191 |
+
|
192 |
+
```python
|
193 |
+
streams = dict(total=account.param.balance, overdraft=account.param.overdraft, owner=account.param.name)
|
194 |
+
|
195 |
+
def table(owner, total, overdraft):
|
196 |
+
return hv.Table([(owner, overdraft, total)], ['Owner', 'Overdraft ($)', 'Total ($)'])
|
197 |
+
|
198 |
+
bank_dmap = hv.DynamicMap(table, streams=streams)
|
199 |
+
bank_dmap.opts(height=100)
|
200 |
+
```
|
201 |
+
|
202 |
+
Now as you set the `balance` parameter on the `janes_account` instance, the DynamicMap above updates. Note that the dictionary specifies that the `balance` parameter is mapped to the `total` argument of the callable.
|
203 |
+
|
204 |
+
|
205 |
+
```python
|
206 |
+
account.balance=65.4
|
207 |
+
account.overdraft=350
|
208 |
+
```
|
209 |
+
|
210 |
+
### Use with `panel`
|
211 |
+
|
212 |
+
This dictionary format is particularly useful when used with the [Panel](http://panel.pyviz.org/) library (a dependency of HoloViews that should always be available), because `panel` widgets always reflect their values on the `value` parameter. This means that if you declare two Panel widgets as follows:
|
213 |
+
|
214 |
+
|
215 |
+
```python
|
216 |
+
import panel as pn
|
217 |
+
|
218 |
+
slider = pn.widgets.FloatSlider(start=0, end=500, name='Balance')
|
219 |
+
checkbox = pn.widgets.Select(options=['student','regular', 'savings'], name='Account Type')
|
220 |
+
pn.Row(slider, checkbox)
|
221 |
+
```
|
222 |
+
|
223 |
+
You can map both widget values into a `DynamicMap` callback without having a name clash as follows:
|
224 |
+
|
225 |
+
|
226 |
+
```python
|
227 |
+
overdraft_limits = {'student':300, 'regular':100, 'savings':0} # Overdraft limits for different account types
|
228 |
+
streams = dict(owner=account.param.name, total=slider.param.value, acc=checkbox.param.value)
|
229 |
+
|
230 |
+
def account_info(owner, total, acc):
|
231 |
+
return hv.Table([(owner, acc, overdraft_limits[acc], total)],
|
232 |
+
['Owner', 'Account Type', 'Overdraft ($)', 'Total ($)'])
|
233 |
+
|
234 |
+
widget_dmap = hv.DynamicMap(account_info, streams=streams)
|
235 |
+
widget_dmap.opts(height=100)
|
236 |
+
```
|
237 |
+
|
238 |
+
|
239 |
+
You can now update the plot above using the slider and dropdown widgets. Note that for all these examples, a `Params` stream is created internally. This type of stream can wrap Parameterized objects or sets of Parameters but (since HoloViews 1.10.8) it is rare that an explicit stream object like that needs to be used directly at the user level. To see more examples of how to use Panel with HoloViews, see the [Dashboards user guide](./16-Dashboards.ipynb).
|
240 |
+
|
241 |
+
### Using `.apply.opts`
|
242 |
+
|
243 |
+
You can supplying Parameters in a similar manner to the `.apply.opts` method. In the following example, a `Style` class has Parameters that indicate the desired colorma and color levels for the `image` instance defined earlier. We can link these together as follows:
|
244 |
+
|
245 |
+
|
246 |
+
```python
|
247 |
+
class Style(param.Parameterized):
|
248 |
+
|
249 |
+
colormap = param.ObjectSelector(default='viridis', objects=['viridis', 'plasma', 'magma'])
|
250 |
+
|
251 |
+
color_levels = param.Integer(default=255, bounds=(1, 255))
|
252 |
+
|
253 |
+
style = Style()
|
254 |
+
image.apply.opts(colorbar=True, width=400, cmap=style.param.colormap, color_levels=style.param.color_levels)
|
255 |
+
```
|
256 |
+
|
257 |
+
Using the `.apply` accessor in this automatically makes the resulting `DynamicMap` depend on the streams specified by the Parameters. Unlike a regular streams class, the plot will update whenever a Parameter on the instance or class changes. For instance, we can update the ``cmap`` and ``color_level`` parameters and watch the plot update in response:
|
258 |
+
|
259 |
+
|
260 |
+
```python
|
261 |
+
style.color_levels = 10
|
262 |
+
style.colormap = 'plasma' # Note that this is mapped to the 'cmap' keyword in .apply.opts
|
263 |
+
```
|
264 |
+
|
265 |
+
## Combining streams and key dimensions
|
266 |
+
|
267 |
+
|
268 |
+
All the ``DynamicMap`` examples above can't be indexed with anything other than ``dmap[()]`` or ``dmap.select()`` as none of them had any key dimensions. This was to focus exclusively on the streams system at the start of the user guide and not because you can't combine key dimensions and streams:
|
269 |
+
|
270 |
+
|
271 |
+
```python
|
272 |
+
xs = np.linspace(-3, 3, 400)
|
273 |
+
|
274 |
+
def function(xs, time):
|
275 |
+
"Some time varying function"
|
276 |
+
return np.exp(np.sin(xs+np.pi/time))
|
277 |
+
|
278 |
+
def integral(limit, time):
|
279 |
+
curve = hv.Curve((xs, function(xs, time)))[limit:]
|
280 |
+
area = hv.Area ((xs, function(xs, time)))[:limit]
|
281 |
+
summed = area.dimension_values('y').sum() * 0.015 # Numeric approximation
|
282 |
+
return (area * curve * hv.VLine(limit) * hv.Text(limit + 0.5, 2.0, '%.2f' % summed))
|
283 |
+
|
284 |
+
Time = Stream.define('Time', time=1.0)
|
285 |
+
dmap = hv.DynamicMap(integral, kdims='limit', streams=[Time()]).redim.range(limit=(-3,2))
|
286 |
+
dmap
|
287 |
+
```
|
288 |
+
|
289 |
+
In this example, you can drag the slider to see a numeric approximation to the integral on the left side on the ``VLine``.
|
290 |
+
|
291 |
+
As ``'limit'`` is declared as a key dimension, it is given a normal HoloViews slider. As we have also defined a ``time`` stream, we can update the displayed curve for any time value:
|
292 |
+
|
293 |
+
|
294 |
+
```python
|
295 |
+
dmap.event(time=8)
|
296 |
+
```
|
297 |
+
|
298 |
+
We now see how to control the ``time`` argument of the integral function by triggering an event with a new time value, and how to control the ``limit`` argument by moving a slider. Controlling ``limit`` with a slider this way is valid but also a little unintuitive: what if you could control ``limit`` just by hovering over the plot?
|
299 |
+
|
300 |
+
In the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, we will see how we can do exactly this by switching to the bokeh backend and using the linked streams system.
|
301 |
+
|
302 |
+
### Matching names to arguments
|
303 |
+
|
304 |
+
Note that in the example above, the key dimension names and the stream parameter names match the arguments to the callable. This *must* be true for stream parameters but this isn't a requirement for key dimensions: if you replace the word 'radius' with 'size' in the example above after ``XY`` is defined, the example still works.
|
305 |
+
|
306 |
+
Here are the rules regarding the callback argument names:
|
307 |
+
|
308 |
+
* If your key dimensions and stream parameters match the callable argument names, the definition is valid.
|
309 |
+
* If your callable accepts mandatory positional arguments and their number matches the number of key dimensions, the names don't need to match and these arguments will be passed key dimensions values.
|
310 |
+
|
311 |
+
As stream parameters always need to match the argument names, there is a method to allow them to be easily renamed. Let's say you imported a stream class as shown in [Custom_Interactivity](13-Custom_Interactivity.ipynb) or for this example, reuse the existing ``XY`` stream class. You can then use the ``rename`` method allowing the following definition:
|
312 |
+
|
313 |
+
|
314 |
+
```python
|
315 |
+
def integral2(lim, t):
|
316 |
+
'Same as integral with different argument names'
|
317 |
+
return integral(lim, t)
|
318 |
+
|
319 |
+
dmap = hv.DynamicMap(integral2, kdims='limit', streams=[Time().rename(time='t')]).redim.range(limit=(-3.,3.))
|
320 |
+
dmap
|
321 |
+
```
|
322 |
+
|
323 |
+
Occasionally, it is useful to suppress some of the stream parameters of a stream class, especially when using the *linked streams* described in [Custom_Interactivity](13-Custom_Interactivity.ipynb). To do this you can rename the stream parameter to ``None`` so that you no longer need to worry about it being passed as an argument to the callable. To re-enable a stream parameter, it is sufficient to either give the stream parameter its original string name or a new string name.
|
324 |
+
|
325 |
+
## Overlapping stream and key dimensions
|
326 |
+
|
327 |
+
In the above example above, the stream parameters do not overlap with the declared key dimension. What happens if we add 'time' to the declared key dimensions?
|
328 |
+
|
329 |
+
|
330 |
+
|
331 |
+
```python
|
332 |
+
dmap=hv.DynamicMap(integral, kdims=['time','limit'], streams=[Time()]).redim.range(limit=(-3.,3.))
|
333 |
+
dmap
|
334 |
+
```
|
335 |
+
|
336 |
+
First you might notice that the 'time' value is now shown in the title but that there is no corresponding time slider as its value is supplied by the stream.
|
337 |
+
|
338 |
+
The 'time' parameter is now an instance of what are called 'dimensioned streams' which re-enable indexing of these dimensions:
|
339 |
+
|
340 |
+
|
341 |
+
```python
|
342 |
+
dmap[1,0] + dmap.select(time=3,limit=1.5) + dmap[None,1.5]
|
343 |
+
```
|
344 |
+
|
345 |
+
In **A**, we supply our own values for the 'time and 'limit' parameters. This doesn't change the values of the 'time' parameters on the stream itself but it does allow us to see what would happen when the time value is one. Note the use of ``None`` in **C** as a way of leaving an explicit value unspecified, allowing the current stream value to be used.
|
346 |
+
|
347 |
+
This is one good reason to use dimensioned streams - it restores access to convenient indexing and selecting operation as a way of exploring your visualizations. The other reason it is useful is that if you keep all your parameters dimensioned, it re-enables the ``DynamicMap`` cache described in the [Live Data](07-Live_Data.ipynb), allowing you to record your interaction with streams and allowing you to cast to ``HoloMap`` for export:
|
348 |
+
|
349 |
+
|
350 |
+
```python
|
351 |
+
dmap.reset() # Reset the cache, we don't want the values from the cell above
|
352 |
+
# TODO: redim the limit dimension to a default of 0
|
353 |
+
dmap.event(time=1)
|
354 |
+
dmap.event(time=1.5)
|
355 |
+
dmap.event(time=2)
|
356 |
+
hv.HoloMap(dmap)
|
357 |
+
```
|
358 |
+
|
359 |
+
One use of this would be to have a simulator drive a visualization forward using ``event`` in a loop. You could then stop your simulation and retain the recent history of the output as long as the allowed ``DynamicMap`` cache.
|
360 |
+
|
361 |
+
## Generators and argument-free callables
|
362 |
+
|
363 |
+
In addition to callables, Python supports [generators](https://docs.python.org/3/glossary.html#term-generator) that can be defined with the ``yield`` keyword. Calling a function that uses yield returns a [generator iterator](https://docs.python.org/3/glossary.html#term-generator-iterator) object that accepts no arguments but returns new values when iterated or when ``next()`` is applied to it.
|
364 |
+
|
365 |
+
HoloViews supports Python generators for completeness and [generator expressions](https://docs.python.org/3/glossary.html#term-generator-expression) can be a convenient way to define code inline instead of using lambda functions. As generators expressions don't accept arguments and can get 'exhausted' ***we recommend using callables with ``DynamicMap``*** - exposing the relevant arguments also exposes control over your visualization.
|
366 |
+
|
367 |
+
Unlike generators, callables that have arguments allow you to re-visit portions of your parameter space instead of always being forced in one direction via calls to ``next()``. With this caveat in mind, here is an example of a generator and the corresponding generator iterator that returns a ``BoxWhisker`` element:
|
368 |
+
|
369 |
+
|
370 |
+
```python
|
371 |
+
def sample_distributions(samples=10, tol=0.04):
|
372 |
+
np.random.seed(42)
|
373 |
+
while True:
|
374 |
+
gauss1 = np.random.normal(size=samples)
|
375 |
+
gauss2 = np.random.normal(size=samples)
|
376 |
+
data = (['A']*samples + ['B']*samples, np.hstack([gauss1, gauss2]))
|
377 |
+
yield hv.BoxWhisker(data, 'Group', 'Value')
|
378 |
+
samples+=1
|
379 |
+
|
380 |
+
sample_generator = sample_distributions()
|
381 |
+
```
|
382 |
+
|
383 |
+
This returns two box whiskers representing samples from two Gaussian distributions of 10 samples. Iterating over this generator simply resamples from these distributions using an additional sample each time.
|
384 |
+
|
385 |
+
As with a callable, we can pass our generator iterator to ``DynamicMap``:
|
386 |
+
|
387 |
+
|
388 |
+
```python
|
389 |
+
hv.DynamicMap(sample_generator)
|
390 |
+
```
|
391 |
+
|
392 |
+
Without using streams, we now have a problem as there is no way to trigger the generator to view the next distribution in the sequence. We can solve this by defining a stream with no parameters:
|
393 |
+
|
394 |
+
|
395 |
+
```python
|
396 |
+
dmap = hv.DynamicMap(sample_generator, streams=[Stream.define('Next')()])
|
397 |
+
dmap
|
398 |
+
```
|
399 |
+
|
400 |
+
### Stream event update loops
|
401 |
+
|
402 |
+
Now we can simply use ``event()`` to drive the generator forward and update the plot, showing how the two Gaussian distributions converge as the number of samples increase.
|
403 |
+
|
404 |
+
|
405 |
+
```python
|
406 |
+
for i in range(40):
|
407 |
+
dmap.event()
|
408 |
+
```
|
409 |
+
|
410 |
+
Note that there is a better way to run loops that drive ``dmap.event()`` which supports a ``period`` (in seconds) between updates and a ``timeout`` argument (also in seconds):
|
411 |
+
|
412 |
+
|
413 |
+
```python
|
414 |
+
dmap.periodic(0.1, 1000, timeout=3)
|
415 |
+
```
|
416 |
+
|
417 |
+
In this generator example, ``event`` does not require any arguments but you can set the ``param_fn`` argument to a callable that takes an iteration counter and returns a dictionary for setting the stream parameters. In addition you can use ``block=False`` to avoid blocking the notebook using a threaded loop. This can be very useful although it has two downsides 1. all running visualizations using non-blocking updates will be competing for computing resources 2. if you override a variable that the thread is actively using, there can be issues with maintaining consistent state in the notebook.
|
418 |
+
|
419 |
+
Generally, the ``periodic`` utility is recommended for all such event update loops and it will be used instead of explicit loops in the rest of the user guides involving streams.
|
420 |
+
|
421 |
+
|
422 |
+
### Using ``next()``
|
423 |
+
|
424 |
+
The approach shown above of using an empty stream works in an exactly analogous fashion for callables that take no arguments. In both cases, the ``DynamicMap`` ``next()`` method is enabled:
|
425 |
+
|
426 |
+
|
427 |
+
```python
|
428 |
+
hv.HoloMap({i:next(dmap) for i in range(10)}, kdims='Iteration')
|
429 |
+
```
|
430 |
+
|
431 |
+
## Next steps
|
432 |
+
|
433 |
+
The streams system allows you to update plots in place making it possible to build live visualizations that update in response to incoming live data or any other type of event. As we have seen in this user guide, you can use streams together with key dimensions to add additional interactivity to your plots while retaining the familiar widgets.
|
434 |
+
|
435 |
+
This user guide used examples that work with either the matplotlib or bokeh backends. In the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, you will see how you can directly interact with dynamic visualizations when using the bokeh backend.
|
436 |
+
|
437 |
+
## [Advanced] How streams work
|
438 |
+
|
439 |
+
|
440 |
+
|
441 |
+
This optional section is not necessary for users who simply want to use the streams system, but it does describe how streams actually work in more detail.
|
442 |
+
|
443 |
+
A stream class is one that inherits from ``Stream`` that typically defines some new parameters. We have already seen one convenient way of defining a stream class:
|
444 |
+
|
445 |
+
|
446 |
+
```python
|
447 |
+
defineXY = Stream.define('defineXY', x=0.0, y=0.0)
|
448 |
+
```
|
449 |
+
|
450 |
+
This is equivalent to the following definition which would be more appropriate in library code or for complex stream class requiring lots of parameters that need to be documented:
|
451 |
+
|
452 |
+
|
453 |
+
```python
|
454 |
+
class XY(Stream):
|
455 |
+
x = param.Number(default=0.0, constant=True, doc='An X position.')
|
456 |
+
y = param.Number(default=0.0, constant=True, doc='A Y position.')
|
457 |
+
```
|
458 |
+
|
459 |
+
As we have already seen, we can make an instance of ``XY`` with some initial values for ``x`` and ``y``.
|
460 |
+
|
461 |
+
|
462 |
+
```python
|
463 |
+
xy = XY(x=2,y=3)
|
464 |
+
```
|
465 |
+
|
466 |
+
However, trying to modify these parameters directly will result in an exception as they have been declared constant (e.g ``xy.x=4`` will throw an error). This is because there are two allowed ways of modifying these parameters, the simplest one being ``update``:
|
467 |
+
|
468 |
+
|
469 |
+
```python
|
470 |
+
xy.update(x=4,y=50)
|
471 |
+
xy.rename(x='xpos', y='ypos').contents
|
472 |
+
```
|
473 |
+
|
474 |
+
This shows how you can update the parameters and also shows the correct way to view the stream parameter values via the ``contents`` property as this will apply any necessary renaming.
|
475 |
+
|
476 |
+
So far, using ``update`` has done nothing but force us to access parameter a certain way. What makes streams work are the side-effects you can trigger when changing a value via the ``event`` method. The relevant side-effect is to invoke callables called 'subscribers'
|
477 |
+
|
478 |
+
### Subscribers
|
479 |
+
|
480 |
+
Without defining any subscribes, the ``event`` method is identical to ``update``:
|
481 |
+
|
482 |
+
|
483 |
+
```python
|
484 |
+
xy = XY()
|
485 |
+
xy.event(x=4,y=50)
|
486 |
+
xy.contents
|
487 |
+
```
|
488 |
+
|
489 |
+
Now let's add a subscriber:
|
490 |
+
|
491 |
+
|
492 |
+
```python
|
493 |
+
def subscriber(xpos,ypos):
|
494 |
+
print('The subscriber received xpos={xpos} and ypos={ypos}'.format(xpos=xpos,ypos=ypos))
|
495 |
+
|
496 |
+
xy = XY().rename(x='xpos', y='ypos')
|
497 |
+
xy.add_subscriber(subscriber)
|
498 |
+
xy.event(x=4,y=50)
|
499 |
+
```
|
500 |
+
|
501 |
+
As we can see, now when you call ``event``, our subscriber is called with the updated parameter values, renamed as appropriate. The ``event`` method accepts the original parameter names and the subscriber receives the new values after any renaming is applied. You can add as many subscribers as you want and you can clear them using the ``clear`` method:
|
502 |
+
|
503 |
+
|
504 |
+
```python
|
505 |
+
xy.clear()
|
506 |
+
xy.event(x=0,y=0)
|
507 |
+
```
|
508 |
+
|
509 |
+
When you define a ``DynamicMap`` using streams, the HoloViews plotting system installs the necessary callbacks as subscribers to update the plot when the stream parameters change. The above example clears all subscribers (it is equivalent to ``clear('all')``. To clear only the subscribers you define yourself use ``clear('user')`` and to clear any subscribers installed by the HoloViews plotting system use ``clear('internal')``.
|
510 |
+
|
511 |
+
When using linked streams as described in the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, the plotting system recognizes the stream class and registers the necessary machinery with Bokeh to update the stream values based on direct interaction with the plot.
|
hvplot_docs/13-Custom_Interactivity.md
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Custom Interactivity
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import param
|
6 |
+
import numpy as np
|
7 |
+
import holoviews as hv
|
8 |
+
from holoviews import opts
|
9 |
+
|
10 |
+
hv.extension('bokeh', 'matplotlib')
|
11 |
+
```
|
12 |
+
|
13 |
+
In previous notebooks we discovered how the ``DynamicMap`` class allows us to declare objects in a lazy way to enable exploratory analysis of large parameter spaces. In the [Responding to Events](./12-Responding_to_Events.ipynb) guide we learned how to interactively push updates to existing plots by declaring Streams on a DynamicMap. In this user guide we will extend the idea to so called *linked* Streams, which allows complex interactions to be declared by specifying which events should be exposed when a plot is interacted with. By passing information about live interactions to a simple Python based callback, you will be able to build richer, even more interactive visualizations that enable seamless data exploration.
|
14 |
+
|
15 |
+
Some of the possibilities this opens up include:
|
16 |
+
|
17 |
+
* Dynamically aggregating datasets of billions of datapoints depending on the plot axis ranges using the [datashader](./15-Large_Data.ipynb) library.
|
18 |
+
* Responding to ``Tap`` and ``DoubleTap`` events to reveal more information in subplots.
|
19 |
+
* Computing statistics in response to selections applied with box- and lasso-select tools.
|
20 |
+
|
21 |
+
Currently only the bokeh backend for HoloViews supports the linked streams system but the principles used should extend to any backend that can define callbacks that fire when a user zooms or pans or interacts with a plot.
|
22 |
+
|
23 |
+
<center><div class="alert alert-info" role="alert">To use and visualize <b>DynamicMap</b> with linked <b>Stream</b> objects you need to be running a live Jupyter server.<br>This user guide assumes that it will be run in a live notebook environment.<br>
|
24 |
+
When viewed statically, DynamicMaps on this page will only show the first available Element.<br></div></center>
|
25 |
+
|
26 |
+
## Available Linked Streams
|
27 |
+
|
28 |
+
There are a huge number of ways one might want to interact with a plot. The HoloViews streams module aims to expose many of the most common interactions you might want want to employ, while also supporting extensibility via custom linked Streams.
|
29 |
+
|
30 |
+
Here is the full list of linked Stream that are all descendants of the ``LinkedStream`` baseclass:
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
from holoviews import streams
|
35 |
+
listing = ', '.join(sorted([str(s.name) for s in param.descendents(streams.LinkedStream)]))
|
36 |
+
print('The linked stream classes supported by HoloViews are:\n\n{listing}'.format(listing=listing))
|
37 |
+
```
|
38 |
+
|
39 |
+
As you can see, most of these events are about specific interactions with a plot such as the current axis ranges (the ``RangeX``, ``RangeY`` and ``RangeXY`` streams), the mouse pointer position (the ``PointerX``, ``PointerY`` and ``PointerXY`` streams), click or tap positions (``Tap``, ``DoubleTap``). Additionally there a streams to access plotting selections made using box- and lasso-select tools (``Selection1D``), the plot size (``PlotSize``) and the ``Bounds`` of a selection. Finally there are a number of drawing/editing streams such as ``BoxEdit``, ``PointDraw``, ``FreehandDraw``, ``PolyDraw`` and ``PolyEdit``.
|
40 |
+
|
41 |
+
Each of these linked Stream types has a corresponding backend specific ``Callback``, which defines which plot attributes or events to link the stream to and triggers events on the ``Stream`` in response to changes on the plot. Defining custom ``Stream`` and ``Callback`` types will be covered in future guides.
|
42 |
+
|
43 |
+
## Linking streams to plots
|
44 |
+
|
45 |
+
At the end of the [Responding to Events](./12-Responding_to_Events.ipynb) guide we discovered that streams have ``subscribers``, which allow defining user defined callbacks on events, but also allow HoloViews to install subscribers that let plots respond to Stream updates. Linked streams add another concept on top of ``subscribers``, namely the Stream ``source``.
|
46 |
+
|
47 |
+
The source of a linked stream defines which plot element to receive events from. Any plot containing the ``source`` object will be attached to the corresponding linked stream and will send event values in response to the appropriate interactions.
|
48 |
+
|
49 |
+
Let's start with a simple example. We will declare one of the linked Streams from above, the ``PointerXY`` stream. This stream sends the current mouse position in plot axes coordinates, which may be continuous or categorical. The first thing to note is that we haven't specified a ``source`` which means it uses the default value of ``None``.
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
pointer = streams.PointerXY()
|
54 |
+
print(pointer.source)
|
55 |
+
```
|
56 |
+
|
57 |
+
Before continuing, we can check the stream parameters that are made available to user callbacks from a given stream instance by looking at its contents:
|
58 |
+
|
59 |
+
|
60 |
+
```python
|
61 |
+
print('The %s stream has contents %r' % (pointer, pointer.contents))
|
62 |
+
```
|
63 |
+
|
64 |
+
#### Automatic linking
|
65 |
+
|
66 |
+
A stream instance is automatically linked to the first ``DynamicMap`` we pass it to, which we can confirm by inspecting the stream's ``source`` attribute after supplying it to a ``DynamicMap``:
|
67 |
+
|
68 |
+
|
69 |
+
```python
|
70 |
+
pointer_dmap = hv.DynamicMap(lambda x, y: hv.Points([(x, y)]), streams=[pointer])
|
71 |
+
print(pointer.source is pointer_dmap)
|
72 |
+
```
|
73 |
+
|
74 |
+
The ``DynamicMap`` we defined above simply defines returns a ``Points`` object composed of a single point that marks the current ``x`` and ``y`` position supplied by our ``PointerXY`` stream. The stream is linked whenever this ``DynamicMap`` object is displayed as it is the stream source:
|
75 |
+
|
76 |
+
|
77 |
+
```python
|
78 |
+
pointer_dmap.opts(size=10)
|
79 |
+
```
|
80 |
+
|
81 |
+
If you hover over the plot canvas above you can see that the point tracks the current mouse position. We can also inspect the last cursor position by examining the stream contents:
|
82 |
+
|
83 |
+
|
84 |
+
```python
|
85 |
+
pointer.contents
|
86 |
+
```
|
87 |
+
|
88 |
+
In the [Responding to Events](12-Responding_to_Events.ipynb) user guide, we introduced an integration example that would work more intuitively with linked streams. Here it is again with the ``limit`` value controlled by the ``PointerX`` linked stream:
|
89 |
+
|
90 |
+
|
91 |
+
```python
|
92 |
+
xs = np.linspace(-3, 3, 400)
|
93 |
+
|
94 |
+
def function(xs, time):
|
95 |
+
"Some time varying function"
|
96 |
+
return np.exp(np.sin(xs+np.pi/time))
|
97 |
+
|
98 |
+
def integral(limit, time):
|
99 |
+
limit = -3 if limit is None else np.clip(limit,-3,3)
|
100 |
+
curve = hv.Curve((xs, function(xs, time)))[limit:]
|
101 |
+
area = hv.Area ((xs, function(xs, time)))[:limit]
|
102 |
+
summed = area.dimension_values('y').sum() * 0.015 # Numeric approximation
|
103 |
+
return (area * curve * hv.VLine(limit) * hv.Text(limit + 0.8, 2.0, '%.2f' % summed))
|
104 |
+
|
105 |
+
integral_streams = [
|
106 |
+
streams.Stream.define('Time', time=1.0)(),
|
107 |
+
streams.PointerX().rename(x='limit')]
|
108 |
+
|
109 |
+
integral_dmap = hv.DynamicMap(integral, streams=integral_streams)
|
110 |
+
|
111 |
+
integral_dmap.opts(
|
112 |
+
opts.Area(color='#fff8dc', line_width=2),
|
113 |
+
opts.Curve(color='black'),
|
114 |
+
opts.VLine(color='red'))
|
115 |
+
```
|
116 |
+
|
117 |
+
We only needed to import and use the ``PointerX`` stream and rename the ``x`` parameter that tracks the cursor position to 'limit' so that it maps to the corresponding argument. Otherwise, the example only required bokeh specific style options to match the matplotlib example as closely as possible.
|
118 |
+
|
119 |
+
#### Explicit linking
|
120 |
+
|
121 |
+
In the example above, we took advantage of the fact that a ``DynamicMap`` automatically becomes the stream source if a source isn't explicitly specified. If we want to link the stream instance to a different object we can specify our source explicitly. Here we will create a 2D ``Image`` of sine gratings, and then declare that this image is the ``source`` of the ``PointerXY`` stream. This pointer stream is then used to generate a single point that tracks the cursor when hovering over the image:
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
xvals = np.linspace(0,4,202)
|
126 |
+
ys,xs = np.meshgrid(xvals, -xvals[::-1])
|
127 |
+
img = hv.Image(np.sin(((ys)**3)*xs))
|
128 |
+
|
129 |
+
pointer = streams.PointerXY(x=0,y=0, source=img)
|
130 |
+
pointer_dmap = hv.DynamicMap(lambda x, y: hv.Points([(x, y)]), streams=[pointer])
|
131 |
+
```
|
132 |
+
|
133 |
+
Now if we display a ``Layout`` consisting of the ``Image`` acting as the source together with the ``DynamicMap``, the point shown on the right tracks the cursor position when hovering over the image on the left:
|
134 |
+
|
135 |
+
|
136 |
+
```python
|
137 |
+
img + pointer_dmap.opts(size=10, xlim=(-.5, .5), ylim=(-.5, .5))
|
138 |
+
```
|
139 |
+
|
140 |
+
This will even work across different cells. If we use this particular stream instance in another ``DynamicMap`` and display it, this new visualization will also be supplied with the cursor position when hovering over the image.
|
141 |
+
|
142 |
+
To illustrate this, we will now use the pointer ``x`` and ``y`` position to generate cross-sections of the image at the cursor position on the ``Image``, making use of the ``Image.sample`` method. Note the use of ``np.clip`` to make sure the cross-section is well defined when the cusor goes out of bounds:
|
143 |
+
|
144 |
+
|
145 |
+
```python
|
146 |
+
x_sample = hv.DynamicMap(lambda x, y: img.sample(x=np.clip(x,-.49,.49)), streams=[pointer])
|
147 |
+
y_sample = hv.DynamicMap(lambda x, y: img.sample(y=np.clip(y,-.49,.49)), streams=[pointer])
|
148 |
+
|
149 |
+
(x_sample + y_sample).opts(opts.Curve(framewise=True))
|
150 |
+
```
|
151 |
+
|
152 |
+
Now when you hover over the ``Image`` above, you will see the cross-sections update while the point position to the right of the ``Image`` simultaneously updates.
|
153 |
+
|
154 |
+
#### Unlinking objects
|
155 |
+
|
156 |
+
Sometimes we just want to display an object designated as a source without linking it to the stream. If the object is not a ``DynamicMap``, like the ``Image`` we designated as a ``source`` above, we can make a copy of the object using the ``clone`` method. We can do the same with ``DynamicMap`` though we just need to supply ``link_inputs=False`` as an extra argument.
|
157 |
+
|
158 |
+
Here we will create a ``DynamicMap`` that draws a cross-hair at the cursor position:
|
159 |
+
|
160 |
+
|
161 |
+
```python
|
162 |
+
pointer = streams.PointerXY(x=0, y=0)
|
163 |
+
cross_dmap = hv.DynamicMap(lambda x, y: (hv.VLine(x) * hv.HLine(y)), streams=[pointer])
|
164 |
+
```
|
165 |
+
|
166 |
+
Now we will add two copies of the ``cross_dmap`` into a Layout but the subplot on the right will not be linking the inputs. Try hovering over the two subplots and observe what happens:
|
167 |
+
|
168 |
+
|
169 |
+
```python
|
170 |
+
cross_dmap + cross_dmap.clone(link=False)
|
171 |
+
```
|
172 |
+
|
173 |
+
Notice how hovering over the left plot updates the crosshair position on both subplots, while hovering over the right subplot has no effect.
|
174 |
+
|
175 |
+
## Transient linked streams
|
176 |
+
|
177 |
+
In the basic [Responding to Events](12-Responding_to_Events.ipynb) user guide we saw that stream parameters can be updated and those values are then passed to the callback. This model works well for many different types of streams that have well-defined values at all times.
|
178 |
+
|
179 |
+
This approach is not suitable for certain events which only have a well defined value at a particular point in time. For instance, when you hover your mouse over a plot, the hover position always has a well-defined value but the click position is only defined when a click occurs (if it occurs).
|
180 |
+
|
181 |
+
This latter case is an example of what are called 'transient' streams. These streams are supplied new values only when they occur and fall back to a default value at all other times. This default value is typically ``None`` to indicate that the event is not occurring and therefore has no data.
|
182 |
+
|
183 |
+
|
184 |
+
Transient streams are particularly useful when you are subscribed to multiple streams, some of which are only occasionally triggered. A good example are the ``Tap`` and ``DoubleTap`` streams; while you sometimes just want to know the last tapped position, we can only tell the two events apart if their values are ``None`` when not active.
|
185 |
+
|
186 |
+
We'll start by declaring a ``SingleTap`` and a ``DoubleTap`` stream as ``transient``. Since both streams supply 'x' and 'y' parameters, we will rename the ``DoubleTap`` parameters to 'x2' and 'y2'.
|
187 |
+
|
188 |
+
|
189 |
+
```python
|
190 |
+
tap = streams.SingleTap(transient=True)
|
191 |
+
double_tap = streams.DoubleTap(rename={'x': 'x2', 'y': 'y2'}, transient=True)
|
192 |
+
```
|
193 |
+
|
194 |
+
Next we define a list of taps we can append to, and a function that accumulates the tap and double tap coordinates along with the number of taps, returning a ``Points`` Element of the tap positions.
|
195 |
+
|
196 |
+
|
197 |
+
```python
|
198 |
+
taps = []
|
199 |
+
|
200 |
+
def record_taps(x, y, x2, y2):
|
201 |
+
if None not in [x,y]:
|
202 |
+
taps.append((x, y, 1))
|
203 |
+
elif None not in [x2, y2]:
|
204 |
+
taps.append((x2, y2, 2))
|
205 |
+
return hv.Points(taps, vdims='Taps')
|
206 |
+
```
|
207 |
+
|
208 |
+
Finally we can create a ``DynamicMap`` from our callback and attach the streams. We also apply some styling so the points are colored depending on the number of taps.
|
209 |
+
|
210 |
+
|
211 |
+
```python
|
212 |
+
taps_dmap = hv.DynamicMap(record_taps, streams=[tap, double_tap])
|
213 |
+
|
214 |
+
taps_dmap.opts(color='Taps', cmap={1: 'red', 2: 'gray'}, size=10, tools=['hover'])
|
215 |
+
```
|
216 |
+
|
217 |
+
Now try single- and double-tapping within the plot area, each time you tap a new point is appended to the list and displayed. Single taps show up in red and double taps show up in grey. We can also inspect the list of taps directly:
|
218 |
+
|
219 |
+
|
220 |
+
```python
|
221 |
+
taps
|
222 |
+
```
|
hvplot_docs/14-Data_Pipelines.md
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Data Processing Pipelines
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import pandas as pd
|
6 |
+
import holoviews as hv
|
7 |
+
|
8 |
+
from holoviews import opts
|
9 |
+
from bokeh.sampledata import stocks
|
10 |
+
from holoviews.operation.timeseries import rolling, rolling_outlier_std
|
11 |
+
|
12 |
+
hv.extension('bokeh')
|
13 |
+
|
14 |
+
opts.defaults(opts.Curve(width=600, framewise=True))
|
15 |
+
```
|
16 |
+
|
17 |
+
In the previous guides we discovered how to load and declare [dynamic, live data](./07-Live_Data.ipynb) and how to [transform elements](./11-Transforming_Elements.ipynb) using `dim` expressions and operations. In this guide we will discover how to combine dynamic data with operations to declare lazy and declarative data processing pipelines, which can be used for interactive exploration but can also drive complex dashboards or even bokeh apps.
|
18 |
+
|
19 |
+
## Declaring dynamic data
|
20 |
+
|
21 |
+
We will begin by declaring a function which loads some data. In this case we will just load some stock data from the bokeh but you could imagine querying this data using REST interface or some other API or even loading some large collection of data from disk or generating the data from some simulation or data processing job.
|
22 |
+
|
23 |
+
|
24 |
+
```python
|
25 |
+
def load_symbol(symbol, **kwargs):
|
26 |
+
df = pd.DataFrame(getattr(stocks, symbol))
|
27 |
+
df['date'] = df.date.astype('datetime64[ns]')
|
28 |
+
return hv.Curve(df, ('date', 'Date'), ('adj_close', 'Adjusted Close'))
|
29 |
+
|
30 |
+
stock_symbols = ['AAPL', 'FB', 'GOOG', 'IBM', 'MSFT']
|
31 |
+
dmap = hv.DynamicMap(load_symbol, kdims='Symbol').redim.values(Symbol=stock_symbols)
|
32 |
+
```
|
33 |
+
|
34 |
+
We begin by displaying our DynamicMap to see what we are dealing with. Recall that a ``DynamicMap`` is only evaluated when you request the key so the ``load_symbol`` function is only executed when first displaying the ``DynamicMap`` and whenever we change the widget dropdown:
|
35 |
+
|
36 |
+
|
37 |
+
```python
|
38 |
+
dmap
|
39 |
+
```
|
40 |
+
|
41 |
+
## Processing data
|
42 |
+
|
43 |
+
It is very common to want to process some data, for this purpose HoloViews provides so-called ``Operations``, which are described in detail in the [Transforming Elements](./11-Transforming_Elements.ipynb). ``Operations`` are simply parameterized functions, which take HoloViews objects as input, transform them in some way and then return the output.
|
44 |
+
|
45 |
+
In combination with [Dimensioned Containers](./05-Dimensioned_Containers.ipynb) such as ``HoloMap`` and ``GridSpace`` they are a powerful way to explore how the parameters of your transform affect the data. We will start with a simple example. HoloViews provides a ``rolling`` function which smoothes timeseries data with a rolling window. We will apply this operation with a ``rolling_window`` of 30, i.e. roughly a month of our daily timeseries data:
|
46 |
+
|
47 |
+
|
48 |
+
```python
|
49 |
+
smoothed = rolling(dmap, rolling_window=30)
|
50 |
+
smoothed
|
51 |
+
```
|
52 |
+
|
53 |
+
As you can see the ``rolling`` operation applies directly to our ``DynamicMap``, smoothing each ``Curve`` before it is displayed. Applying an operation to a ``DynamicMap`` keeps the data as a ``DynamicMap``, this means the operation is also applied lazily whenever we display or select a different symbol in the dropdown widget.
|
54 |
+
|
55 |
+
### Dynamically evaluating parameters on operations and transforms with ``.apply``
|
56 |
+
|
57 |
+
The ``.apply`` method allows us to automatically build a dynamic pipeline given an object and some operation or function along with parameter, stream or widget instances passed in as keyword arguments. Internally it will then build a `Stream` to ensure that whenever one of these changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).
|
58 |
+
|
59 |
+
This mechanism allows us to build powerful pipelines by linking parameters on a user defined class or even an external widget, e.g. here we import an ``IntSlider`` widget from [``panel``](https://pyviz.panel.org):
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
import panel as pn
|
64 |
+
|
65 |
+
slider = pn.widgets.IntSlider(name='rolling_window', start=1, end=100, value=50)
|
66 |
+
```
|
67 |
+
|
68 |
+
Using the ``.apply`` method we could now apply the ``rolling`` operation to the DynamicMap and link the slider to the operation's ``rolling_window`` parameter (which also works for simple functions as will be shown below). However, to further demonstrate the features of `dim` expressions and the `.transform` method, which we first introduced in the [Transforming elements user guide](11-Transforming_Elements.ipynb), we will instead apply the rolling mean using the `.df` namespace accessor on a `dim` expression:
|
69 |
+
|
70 |
+
|
71 |
+
```python
|
72 |
+
rolled_dmap = dmap.apply.transform(adj_close=hv.dim('adj_close').df.rolling(slider).mean())
|
73 |
+
|
74 |
+
rolled_dmap
|
75 |
+
```
|
76 |
+
|
77 |
+
The ``rolled_dmap`` is another DynamicMap that defines a simple two-step pipeline, which calls the original callback when the ``symbol`` changes and reapplies the expression whenever the slider value changes. Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
slider
|
82 |
+
```
|
83 |
+
|
84 |
+
The power of building pipelines is that different visual components can share the same inputs but compute very different things from that data. The part of the pipeline that is shared is only evaluated once making it easy to build efficient data processing code. To illustrate this we will also apply the ``rolling_outlier_std`` operation which computes outliers within the ``rolling_window`` and again we will supply the widget ``value``:
|
85 |
+
|
86 |
+
|
87 |
+
```python
|
88 |
+
outliers = dmap.apply(rolling_outlier_std, rolling_window=slider.param.value)
|
89 |
+
|
90 |
+
rolled_dmap * outliers.opts(color='red', marker='triangle')
|
91 |
+
```
|
92 |
+
|
93 |
+
We can chain operations like this indefinitely and attach parameters or explicit streams to each stage. By chaining we can watch our visualization update whenever we change a stream value anywhere in the pipeline and HoloViews will be smart about which parts of the pipeline are recomputed, which allows us to build complex visualizations very quickly.
|
94 |
+
|
95 |
+
The ``.apply`` method is also not limited to operations. We can just as easily apply a simple Python function to each object in the ``DynamicMap``. Here we define a function to compute the residual between the original ``dmap`` and the ``rolled_dmap``.
|
96 |
+
|
97 |
+
|
98 |
+
```python
|
99 |
+
def residual_fn(overlay):
|
100 |
+
# Get first and second Element in overlay
|
101 |
+
el1, el2 = overlay.get(0), overlay.get(1)
|
102 |
+
|
103 |
+
# Get x-values and y-values of curves
|
104 |
+
xvals = el1.dimension_values(0)
|
105 |
+
yvals = el1.dimension_values(1)
|
106 |
+
yvals2 = el2.dimension_values(1)
|
107 |
+
|
108 |
+
# Return new Element with subtracted y-values
|
109 |
+
# and new label
|
110 |
+
return el1.clone((xvals, yvals-yvals2),
|
111 |
+
vdims='Residual')
|
112 |
+
```
|
113 |
+
|
114 |
+
If we overlay the two DynamicMaps we can then dynamically broadcast this function to each of the overlays, producing a new DynamicMap which responds to both the symbol selector widget and the slider:
|
115 |
+
|
116 |
+
|
117 |
+
```python
|
118 |
+
residual = (dmap * rolled_dmap).apply(residual_fn)
|
119 |
+
|
120 |
+
residual
|
121 |
+
```
|
122 |
+
|
123 |
+
In later guides we will see how we can combine HoloViews plots and Panel widgets into custom layouts allowing us to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) and [Dashboards](./17-Dashboards.ipynb) guides. To get a quick idea of what this might look like let's compose all the components we have no built:
|
hvplot_docs/15-Large_Data.md
ADDED
@@ -0,0 +1,576 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Working with large data using Datashader
|
2 |
+
|
3 |
+
The various plotting-library backends supported by HoloViews, such as Matplotlib, Bokeh, and Plotly, each have limitations on the amount of data that is practical to work with. Bokeh and Plotly in particular mirror your data directly into an HTML page viewable in your browser, which can cause problems when data sizes approach the limited memory available for each web page in current browsers.
|
4 |
+
|
5 |
+
Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend plotting library and thus to your local web browser. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.
|
6 |
+
|
7 |
+
Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but [Datashader's examples](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [HoloViz.org](http://holoviz.org) shows how to install and work with HoloViews and Datashader together.
|
8 |
+
|
9 |
+
<style>.container { width:100% !important; }</style>
|
10 |
+
|
11 |
+
|
12 |
+
```python
|
13 |
+
import datashader as ds
|
14 |
+
import numpy as np
|
15 |
+
import holoviews as hv
|
16 |
+
import pandas as pd
|
17 |
+
import numpy as np
|
18 |
+
|
19 |
+
from holoviews import opts
|
20 |
+
from holoviews.operation.datashader import datashade, rasterize, shade, dynspread, spread
|
21 |
+
from holoviews.operation.resample import ResampleOperation2D
|
22 |
+
from holoviews.operation import decimate
|
23 |
+
|
24 |
+
hv.extension('bokeh','matplotlib', width=100)
|
25 |
+
|
26 |
+
# Default values suitable for this notebook
|
27 |
+
decimate.max_samples=1000
|
28 |
+
dynspread.max_px=20
|
29 |
+
dynspread.threshold=0.5
|
30 |
+
ResampleOperation2D.width=500
|
31 |
+
ResampleOperation2D.height=500
|
32 |
+
|
33 |
+
def random_walk(n, f=5000):
|
34 |
+
"""Random walk in a 2D space, smoothed with a filter of length f"""
|
35 |
+
xs = np.convolve(np.random.normal(0, 0.1, size=n), np.ones(f)/f).cumsum()
|
36 |
+
ys = np.convolve(np.random.normal(0, 0.1, size=n), np.ones(f)/f).cumsum()
|
37 |
+
xs += 0.1*np.sin(0.1*np.array(range(n-1+f))) # add wobble on x axis
|
38 |
+
xs += np.random.normal(0, 0.005, size=n-1+f) # add measurement noise
|
39 |
+
ys += np.random.normal(0, 0.005, size=n-1+f)
|
40 |
+
return np.column_stack([xs, ys])
|
41 |
+
|
42 |
+
def random_cov():
|
43 |
+
"""Random covariance for use in generating 2D Gaussian distributions"""
|
44 |
+
A = np.random.randn(2,2)
|
45 |
+
return np.dot(A, A.T)
|
46 |
+
|
47 |
+
def time_series(T = 1, N = 100, mu = 0.1, sigma = 0.1, S0 = 20):
|
48 |
+
"""Parameterized noisy time series"""
|
49 |
+
dt = float(T)/N
|
50 |
+
t = np.linspace(0, T, N)
|
51 |
+
W = np.random.standard_normal(size = N)
|
52 |
+
W = np.cumsum(W)*np.sqrt(dt) # standard brownian motion
|
53 |
+
X = (mu-0.5*sigma**2)*t + sigma*W
|
54 |
+
S = S0*np.exp(X) # geometric brownian motion
|
55 |
+
return S
|
56 |
+
```
|
57 |
+
|
58 |
+
<center><div class="alert alert-info" role="alert">This notebook makes use of dynamic updates, which require a running a live Jupyter or Bokeh server.<br>
|
59 |
+
When viewed statically, the plots will not update fully when you zoom and pan.<br></div></center>
|
60 |
+
|
61 |
+
# Principles of datashading
|
62 |
+
|
63 |
+
Because HoloViews elements are fundamentally data containers, not visualizations, you can very quickly declare elements such as ``Points`` or ``Path`` containing datasets that may be as large as the full memory available on your machine (or even larger if using Dask dataframes). So even for very large datasets, you can easily specify a data structure that you can work with for making selections, sampling, aggregations, and so on. However, as soon as you try to visualize it directly with either the Matplotlib, Plotly, or Bokeh plotting extensions, the rendering process may be prohibitively expensive.
|
64 |
+
|
65 |
+
Let's start with a simple example that's easy to visualize in any plotting library:
|
66 |
+
|
67 |
+
|
68 |
+
```python
|
69 |
+
np.random.seed(1)
|
70 |
+
points = hv.Points(np.random.multivariate_normal((0,0), [[0.1, 0.1], [0.1, 1.0]], (1000,)),label="Points")
|
71 |
+
paths = hv.Path([random_walk(2000,30)], kdims=["u","v"], label="Paths")
|
72 |
+
|
73 |
+
points + paths
|
74 |
+
```
|
75 |
+
|
76 |
+
These browser-based plots are fully interactive, as you can see if you select the Wheel Zoom or Box Zoom tools and use your scroll wheel or click and drag.
|
77 |
+
|
78 |
+
Because all of the data in these plots gets transferred directly into the web browser, the interactive functionality will be available even on a static export of this figure as a web page. Note that even though the visualization above is not computationally expensive, even with just 1000 points as in the scatterplot above, the plot already suffers from [overplotting](https://anaconda.org/jbednar/plotting_pitfalls), with later points obscuring previously plotted points.
|
79 |
+
|
80 |
+
With much larger datasets, these issues will quickly make it impossible to see the true structure of the data. We can easily declare 50X or 1000X larger versions of the same plots above, but if we tried to visualize them directly they would be unusably slow even if the browser did not crash:
|
81 |
+
|
82 |
+
|
83 |
+
```python
|
84 |
+
np.random.seed(1)
|
85 |
+
points = hv.Points(np.random.multivariate_normal((0,0), [[0.1, 0.1], [0.1, 1.0]], (1000000,)),label="Points")
|
86 |
+
paths = hv.Path([0.15*random_walk(100000) for i in range(10)], kdims=["u","v"], label="Paths")
|
87 |
+
|
88 |
+
#points + paths ## Danger! Browsers can't handle 1 million points!
|
89 |
+
```
|
90 |
+
|
91 |
+
Luckily, HoloViews Elements are just containers for data and associated metadata, not plots, so HoloViews can generate entirely different types of visualizations from the same data structure when appropriate. For instance, in the plot on the left below you can see the result of applying a `decimate()` operation acting on the `points` object, which will automatically downsample this million-point dataset to at most 1000 points at any time as you zoom in or out:
|
92 |
+
|
93 |
+
|
94 |
+
```python
|
95 |
+
decimate( points).relabel("Decimated Points") + \
|
96 |
+
rasterize(points).relabel("Rasterized Points").opts(colorbar=True, width=350) + \
|
97 |
+
rasterize(paths ).relabel("Rasterized Paths")
|
98 |
+
```
|
99 |
+
|
100 |
+
Decimating a plot in this way can be useful, but it discards most of the data even while still suffering from overplotting.
|
101 |
+
|
102 |
+
If you have Datashader installed, you can instead use Datashader operations like `rasterize()` to create a dynamic Datashader-based Bokeh plot. The middle plot above shows the result of using `rasterize()` to create a dynamic Datashader-based plot out of an Element with arbitrarily large data. In the rasterized version, the data is binned into a fixed-size 2D array automatically on every zoom or pan event, revealing all the data available at that zoom level and avoiding issues with overplotting by dynamically rescaling the colors used. Each pixel is colored by how many datapoints fall in that pixel, faithfully revealing the data's distribution in a easy-to-display plot. The colorbar indicates the number of points indicated by that color, up to 300 or so for the pixels with the most points here. The same process is used for the line-based data in the Paths plot, where darker colors represent path intersections.
|
103 |
+
|
104 |
+
These two Datashader-based plots are similar to the native Bokeh plots above, but instead of making a static Bokeh plot that embeds points or line segments directly into the browser, HoloViews sets up a Bokeh plot with dynamic callbacks instructing Datashader to rasterize the data into a fixed-size array (effectively a 2D histogram) instead. The dynamic re-rendering provides an interactive user experience, even though the data itself is never provided directly to the browser. Of course, because the full data is not in the browser, a static export of this page (e.g. on holoviews.org or on anaconda.org) will only show the initially rendered version, and will not update with new rasterized arrays when zooming as it will when there is a live Python process available.
|
105 |
+
|
106 |
+
Though you can no longer have a completely interactive exported file, with the Datashader version on a live server you can now change the number of data points from 1000000 to 10000000 or more to see how well your machine will handle larger datasets. It will get a bit slower, but if you have enough memory, it should still be very usable, and should never crash your browser like transferring the whole dataset into your browser would. If you don't have enough memory, you can instead set up a [Dask](http://dask.pydata.org) dataframe as shown in other Datashader examples, which will provide out-of-core and/or distributed processing to handle even the largest datasets if you have enough computational power and memory or are willing to wait for out-of-core computation.
|
107 |
+
|
108 |
+
# HoloViews operations for datashading
|
109 |
+
|
110 |
+
HoloViews provides several operations for calling Datashader on HoloViews elements, including `rasterize()`, `shade()`, and `datashade()`.
|
111 |
+
|
112 |
+
`rasterize()` uses Datashader to render the data into what is by default a 2D histogram, where every array cell counts the data points falling into that pixel. Bokeh then colormaps that array, turning each cell into a pixel in an image.
|
113 |
+
|
114 |
+
Instead of having Bokeh do the colormapping, you can instruct Datashader to do so, by wrapping the output of `rasterize()` in a call to `shade()`, where `shade()` is Datashader's colormapping function. The `datashade()` operation is also provided as a simple macro, where `datashade(x)` is equivalent to `shade(rasterize(x))`:
|
115 |
+
|
116 |
+
|
117 |
+
```python
|
118 |
+
ropts = dict(colorbar=True, tools=["hover"], width=350)
|
119 |
+
|
120 |
+
rasterize( points).opts(cmap="kbc_r", cnorm="linear").relabel('rasterize()').opts(**ropts).hist() + \
|
121 |
+
shade(rasterize(points), cmap="kbc_r", cnorm="linear").relabel("shade(rasterize())") + \
|
122 |
+
datashade( points, cmap="kbc_r", cnorm="linear").relabel("datashade()")
|
123 |
+
```
|
124 |
+
|
125 |
+
In all three of the above plots, `rasterize()` is being called to aggregate the data (a large set of x,y locations) into a rectangular grid, with each grid cell counting up the number of points that fall into it. In the first plot, only `rasterize()` is done, and the resulting numeric array of counts is passed to Bokeh for colormapping. That way hover and colorbars can be supported (as shown), and Bokeh can then provide dynamic (client-side, browser-based) colormapping tools in JavaScript, allowing users to have dynamic control over even static HTML plots. For instance, in this case, users can use the Box Select tool and select a range of the histogram shown, dynamically remapping the colors used in the plot to cover the selected range.
|
126 |
+
|
127 |
+
The other two plots should be identical in appearance, but with the numerical array output of `rasterize()` mapped into RGB colors by Datashader itself, in Python ("server-side"), which allows some special Datashader computations described below but prevents other Bokeh-based features like hover and colorbars from being used. Here we've instructed Datashader to use the same colormap used by bokeh, so that the plots look similar, but as you can see the `rasterize()` colormap is determined by a HoloViews plot option, while the `shade` and `datashade` colormap is determined by an argument to those operations. See ``hv.help(rasterize)``, ``hv.help(shade)``, and ``hv.help(datashade)`` for options that can be selected, and the [Datashader web site](http://datashader.org) for all the details. HoloViews also provides lower-level `aggregate()` and `regrid()` operations that implement `rasterize()` and give more control over how the data is aggregated, but these are not needed for typical usage.
|
128 |
+
|
129 |
+
# Setting options
|
130 |
+
|
131 |
+
By their nature, the datashading operations accept one HoloViews Element type and return a different Element type. Regardless of what type they are given, `rasterize()` returns an `hv.Image`, while `shade()` and `datashade()` return an `hv.RGB`. It is important to keep this transformation in mind, because HoloViews options that you set on your original Element type are not normally transferred to your new Element:
|
132 |
+
|
133 |
+
|
134 |
+
```python
|
135 |
+
points2 = decimate(points, dynamic=False, max_samples=3000)
|
136 |
+
points2.opts(color="green", size=6, marker="s")
|
137 |
+
|
138 |
+
points2 + rasterize(points2).relabel("Rasterized") + datashade(points2).relabel("Datashaded")
|
139 |
+
```
|
140 |
+
|
141 |
+
The datashaded plot represents each point as a single pixel, many of which are very difficult to see, and you can see that the color, size, and marker shape that you set on the Points element will not be applied to the rasterized or datashaded plot, because `size` and `marker` are not directly applicable to the numerical arrays of `hv.Image` and the pixel arrays of `hv.RGB`.
|
142 |
+
|
143 |
+
If you want to use Datashader to recreate the options from the original plot, you can usually do so, but you will have to use the various Datashader-specific features explained in the sections below along with HoloViews options specifically for `hv.Image` or `hv.RGB`. For example:
|
144 |
+
|
145 |
+
|
146 |
+
```python
|
147 |
+
w=225
|
148 |
+
|
149 |
+
points2 + \
|
150 |
+
spread(rasterize(points2, width=w, height=w), px=4, shape='square').opts(cmap=["green"]).relabel("Rasterized") + \
|
151 |
+
spread(datashade(points2, width=w, height=w, cmap=["green"]), px=4, shape='square').relabel("Datashaded")
|
152 |
+
```
|
153 |
+
|
154 |
+
Note that by forcing the single-color colormap `["green"]`, Datashader's support for avoiding overplotting has been lost. In most cases you will want to reveal the underlying distribution while avoiding overplotting, either by using a proper colormap (**Rasterized** below) or by using the alpha channel to convey the number of overlapping points (**Datashaded** below).
|
155 |
+
|
156 |
+
|
157 |
+
```python
|
158 |
+
import bokeh.palettes as bp
|
159 |
+
greens = bp.Greens[256][::-1][64:]
|
160 |
+
```
|
161 |
+
|
162 |
+
|
163 |
+
```python
|
164 |
+
points2 + \
|
165 |
+
spread(rasterize(points2, width=w, height=w), px=4, shape='square').opts(cmap=greens, cnorm='eq_hist').relabel("Rasterized") +\
|
166 |
+
spread(datashade(points2, width=w, height=w, cmap="green", cnorm='eq_hist'), px=4, shape='square').relabel("Datashaded")
|
167 |
+
```
|
168 |
+
|
169 |
+
# Colormapping
|
170 |
+
|
171 |
+
As you can see above, the choice of colormap and the various colormapping options can be very important for datashaded plots. One issue often seen in large, real-world datasets is that there is structure at many spatial and value scales, which requires special attention to colormapping options. This example dataset from the [Datashader documentation](https://datashader.org/getting_started/Pipeline.html) illustrates the issues, with data clustering at five different spatial scales:
|
172 |
+
|
173 |
+
|
174 |
+
```python
|
175 |
+
num=10000
|
176 |
+
np.random.seed(1)
|
177 |
+
|
178 |
+
dists = {cat: pd.DataFrame(dict([('x',np.random.normal(x,s,num)),
|
179 |
+
('y',np.random.normal(y,s,num)),
|
180 |
+
('val',val),
|
181 |
+
('cat',cat)]))
|
182 |
+
for x, y, s, val, cat in
|
183 |
+
[( 2, 2, 0.03, 10, "d1"),
|
184 |
+
( 2, -2, 0.10, 20, "d2"),
|
185 |
+
( -2, -2, 0.50, 30, "d3"),
|
186 |
+
( -2, 2, 1.00, 40, "d4"),
|
187 |
+
( 0, 0, 3.00, 50, "d5")] }
|
188 |
+
|
189 |
+
df = pd.concat(dists,ignore_index=True)
|
190 |
+
df["cat"]=df["cat"].astype("category")
|
191 |
+
df
|
192 |
+
```
|
193 |
+
|
194 |
+
Each of the five categories has 10000 points, but distributed over different spatial areas. Bokeh supports three colormap normalization options, which each behave differently:
|
195 |
+
|
196 |
+
|
197 |
+
```python
|
198 |
+
ropts = dict(tools=["hover"], height=380, width=330, colorbar=True, colorbar_position="bottom")
|
199 |
+
|
200 |
+
hv.Layout([rasterize(hv.Points(df)).opts(**ropts).opts(cnorm=n).relabel(n)
|
201 |
+
for n in ["linear", "log", "eq_hist"]])
|
202 |
+
```
|
203 |
+
|
204 |
+
Here, the `linear` map is easy to interpret, but nearly all of the pixels are drawn in the lightest blue, because the highest-count pixel (around a count of 6000) is much larger in value than the typical pixels. The other two plots show the full structure (five concentrations of data points, including one in the background), with `log` using a standard logarithmic transformation of the count data before colormapping, and `eq_hist` using a histogram-equalization technique (see the [Datashader docs](https://datashader.org/getting_started/Pipeline.html)) to reveal structure without any assumptions about the incoming distribution (but with an irregularly spaced colormap that makes the numeric values difficult to reason about). In practice, it is generally a good idea to use `eq_hist` when exploring a large dataset initially, so that you will see any structure present, then switch to `log` or `linear` as appropriate to share the plots with a simpler-to-explain colormap. All three of these options are supported by the various backends (including Bokeh version 2.2.3 or later) and by `shade()` and `datashade()` except that `eq_hist` is not yet available for the Plotly backend.
|
205 |
+
|
206 |
+
Since datashader only sends the data currently in view to the plotting backend, the default behavior is to rescale the colormap to the range of the visible data as the zoom level changes. This behavior may not be desirable when working with images; to instead use a fixed colormap range, the `clim` parameter can be passed to the `bokeh` backend via the `opts()` method. Note that this approach works with `rasterize()` where the colormapping is done by the `bokeh` backend. With `datashade()`, the colormapping is done with the `shade()` function which takes a `clims` parameter directly instead of passing additional parameters to the backend via `opts()`. For example (removing the semicolon in a live notebook to see the output):
|
207 |
+
|
208 |
+
|
209 |
+
```python
|
210 |
+
pts1 = rasterize(hv.Points(df)).opts(**ropts).opts(tools=[], cnorm='log', axiswise=True)
|
211 |
+
pts2 = rasterize(hv.Points(df)).opts(**ropts).opts(tools=[], cnorm='log', axiswise=True)
|
212 |
+
|
213 |
+
pts1 + pts2.opts(clim=(0, 10000));
|
214 |
+
```
|
215 |
+
|
216 |
+
<img src="http://assets.holoviews.org/gifs/guides/user_guide/Large_Data/rasterize_clim_example.gif"></img>
|
217 |
+
|
218 |
+
By default, pixels with an integer count of zero or a floating-point value of NaN are transparent, letting the plot background show through so that the data can be used in overlays. If you want zero to map to the lowest colormap color instead to make a dense, fully filled-in image, you can use `redim.nodata` to set the `Dimension.nodata` parameter to None:
|
219 |
+
|
220 |
+
|
221 |
+
```python
|
222 |
+
hv.Layout([rasterize(hv.Points(df), vdim_prefix='').redim.nodata(Count=n)\
|
223 |
+
.opts(**ropts, cnorm="eq_hist").relabel("nodata="+str(n))
|
224 |
+
for n in [0, None]])
|
225 |
+
```
|
226 |
+
|
227 |
+
## Spreading and antialiasing
|
228 |
+
|
229 |
+
By default, Datashader treats points and lines as infinitesimal in width, such that a given point or small bit of line segment appears in at most one pixel. This approach ensures that the overall distribution of the points will be mathematically well founded -- each pixel will scale in value directly by the number of points that fall into it, or by the lines that cross it. As a consequence, Datashader's "marker size" and "line width" are effectively one pixel by default.
|
230 |
+
|
231 |
+
However, many monitors are sufficiently high resolution that a single-pixel point or line can be difficult to see---one pixel may not be visible at all on its own, and even if it is visible it is often difficult to see its color. To compensate for this, HoloViews provides access to Datashader's raster-based "spreading" (a generalization of image dilation and convolution), which makes isolated nonzero cells "spread" into adjacent ones for visibility. There are two varieties of spreading supported:
|
232 |
+
|
233 |
+
1. ``spread``: fixed spreading of a certain number of cells (pixels), which is useful if you want to be sure how much spreading is done regardless of the properties of the data.
|
234 |
+
2. ``dynspread``: spreads up to a maximum size as long as it does not exceed a specified fraction of adjacency between cells (pixels) (controlled by a `threshold` parameter).
|
235 |
+
|
236 |
+
Dynamic spreading is typically more useful for interactive plotting, because it adjusts depending on how close the datapoints are to each other on screen. As of Datashader 0.12, both types of spreading are supported for both `rasterize()` and `shade()`, but previous Datashader versions only support spreading on the RGB output of `shade()`.
|
237 |
+
|
238 |
+
As long as you have Datashader 0.12 or later, you can compare the results when you zoom the two plots below; when you zoom in far enough you should be able to see that the in the two zoomed-in plots below, then zoom out to see that the plots are the same when points are clustered together to form a distribution. (If running a live notebook; remove the semicolon so that you see the live output rather than the saved GIF.)
|
239 |
+
|
240 |
+
|
241 |
+
```python
|
242 |
+
pts = rasterize(points).opts(cnorm='eq_hist')
|
243 |
+
|
244 |
+
pts + dynspread(pts);
|
245 |
+
```
|
246 |
+
|
247 |
+
<img src="http://assets.holoviews.org/gifs/guides/user_guide/Large_Data/dynspread.gif"></img>
|
248 |
+
|
249 |
+
Both plots show the same data, and look identical when zoomed out, but when zoomed in enough you should be able to see the individual data points on the right while the ones on the left are barely visible. The dynspread parameters typically need some hand tuning, as the only purpose of such spreading is to make things visible on a particular monitor for a particular observer; the underlying mathematical operations in Datashader do not normally need parameters to be adjusted.
|
250 |
+
|
251 |
+
Dynspread is not usable with connected plots like trajectories or curves, because the spreading amount is measured by the fraction of cells that have neighbors closer than the given spread distance, which is always 100% when datapoints are connected together. For connected plots you can instead use `spread` with a fixed value to expand patterns by `px` in every direction after they are drawn, or (for Datashader 0.14 or later) pass an explicit width like `line_width=1` to the rasterizer (at some cost in performance) to draw fully antialiased lines with the specified width:
|
252 |
+
|
253 |
+
|
254 |
+
```python
|
255 |
+
rasterize(paths).relabel("Rasterized") + \
|
256 |
+
spread(rasterize(paths), px=1).relabel("Spread 1") + \
|
257 |
+
rasterize(paths, line_width=2).relabel("Antialiased line_width 2")
|
258 |
+
```
|
259 |
+
|
260 |
+
# Multidimensional plots
|
261 |
+
|
262 |
+
The above plots show two dimensions of data plotted along *x* and *y*, but Datashader operations can be used with additional dimensions as well. For instance, an extra dimension (here called `k`), can be treated as a category label and used to colorize the points or lines, aggregating the data points separately depending on which category value they have. Compared to a standard overlaid scatterplot that would suffer from overplotting, here the result will be merged mathematically by Datashader, completely avoiding any overplotting issues except any local issues that may arise from spreading when zoomed in:
|
263 |
+
|
264 |
+
|
265 |
+
```python
|
266 |
+
np.random.seed(3)
|
267 |
+
kdims=['d1','d2']
|
268 |
+
num_ks=8
|
269 |
+
|
270 |
+
def rand_gauss2d(value=0, n=100000):
|
271 |
+
"""Return a randomly shaped 2D Gaussian distribution with an associated numeric value"""
|
272 |
+
g = 100*np.random.multivariate_normal(np.random.randn(2), random_cov(), (n,))
|
273 |
+
return np.hstack((g,value*np.ones((g.shape[0],1))))
|
274 |
+
```
|
275 |
+
|
276 |
+
|
277 |
+
```python
|
278 |
+
gaussians = {str(i): hv.Points(rand_gauss2d(i), kdims, "i") for i in range(num_ks)}
|
279 |
+
|
280 |
+
c = dynspread(datashade(hv.NdOverlay(gaussians, kdims='k'), aggregator=ds.by('k', ds.count())))
|
281 |
+
m = dynspread(datashade(hv.NdOverlay(gaussians, kdims='k'), aggregator=ds.by('k', ds.mean("i"))))
|
282 |
+
|
283 |
+
c.opts(width=400) + m.opts(width=400)
|
284 |
+
```
|
285 |
+
|
286 |
+
Above you can see that (as of Datashader 0.11) categorical aggregates can take any reduction function, either `count`ing the datapoints (left) or reporting some other statistic (e.g. the mean value of a column, right). This type of categorical mixing is currently only supported by `shade()` and `datashade()`, not `rasterize()` alone, because it depends on Datashader's custom color mixing code.
|
287 |
+
|
288 |
+
Categorical aggregates are one way to allow separate lines or other shapes to be visually distinctive from one another while avoiding obscuring data due to overplotting:
|
289 |
+
|
290 |
+
|
291 |
+
```python
|
292 |
+
lines = {str(i): hv.Curve(time_series(N=10000, S0=200+np.random.rand())) for i in range(num_ks)}
|
293 |
+
lineoverlay = hv.NdOverlay(lines, kdims='k')
|
294 |
+
datashade(lineoverlay, pixel_ratio=2, line_width=4, aggregator=ds.by('k', ds.count())).opts(width=800)
|
295 |
+
```
|
296 |
+
|
297 |
+
As you can see, overlapping colors yield color mixtures that indicate that the given pixels contain data from multiple curves, which helps users realize where they need to zoom in to see further detail.
|
298 |
+
|
299 |
+
Note that Bokeh only ever sees an image come out of `datashade`, not any of the actual data. As a result, providing legends and keys has to be done separately, though we are hoping to make this process more seamless. For now, you can show a legend by adding a suitable collection of "fake" labeled points (size zero and thus invisible):
|
300 |
+
|
301 |
+
|
302 |
+
```python
|
303 |
+
# definition copied here to ensure independent pan/zoom state for each dynamic plot
|
304 |
+
gaussspread2 = dynspread(datashade(hv.NdOverlay(gaussians, kdims=['k']), aggregator=ds.by('k', ds.count())))
|
305 |
+
|
306 |
+
from datashader.colors import Sets1to3 # default datashade() and shade() color cycle
|
307 |
+
color_key = list(enumerate(Sets1to3[0:num_ks]))
|
308 |
+
color_points = hv.NdOverlay({k: hv.Points([(0,0)], label=str(k)).opts(color=v, size=0) for k, v in color_key})
|
309 |
+
|
310 |
+
(color_points * gaussspread2).opts(width=600)
|
311 |
+
```
|
312 |
+
|
313 |
+
Here the dummy points are at (0,0) for this dataset, but would need to be at another suitable value for data that is in a different range.
|
314 |
+
|
315 |
+
## Working with time series
|
316 |
+
|
317 |
+
HoloViews also makes it possible to datashade large timeseries using the ``datashade`` and ``rasterize`` operations. For smoother lines, datashader implements anti-aliasing if a `line_width` > 0 is set:
|
318 |
+
|
319 |
+
|
320 |
+
```python
|
321 |
+
dates = pd.date_range(start="2014-01-01", end="2016-01-01", freq='1D') # or '1min'
|
322 |
+
curve = hv.Curve((dates, time_series(N=len(dates), sigma = 1)))
|
323 |
+
rasterize(curve, width=800, line_width=3, pixel_ratio=2).opts(width=800, cmap=['lightblue','blue'])
|
324 |
+
```
|
325 |
+
|
326 |
+
Here we're also doubling the resolution in x and y using `pixel_ratio=2`, allowing for more precise rendering of the line shape; higher pixel ratios work well for lines and other shapes, though they can make individual points more difficult to see in points plots.
|
327 |
+
|
328 |
+
HoloViews also supplies some operations that are useful in combination with Datashader timeseries. For instance, you can compute a rolling mean of the results and then show a subset of outlier points, which will then support hover, selection, and other interactive Bokeh features:
|
329 |
+
|
330 |
+
|
331 |
+
```python
|
332 |
+
from holoviews.operation.timeseries import rolling, rolling_outlier_std
|
333 |
+
smoothed = rolling(curve, rolling_window=50)
|
334 |
+
outliers = rolling_outlier_std(curve, rolling_window=50, sigma=2)
|
335 |
+
|
336 |
+
ds_curve = rasterize(curve, line_width=2.5, pixel_ratio=2).opts(cmap=["lightblue","blue"])
|
337 |
+
curvespread = rasterize(smoothed, line_width=6, pixel_ratio=2).opts(cmap=["pink","red"], width=800)
|
338 |
+
|
339 |
+
(ds_curve * curvespread * outliers).opts(
|
340 |
+
opts.Scatter(line_color="black", fill_color="red", size=10, tools=['hover', 'box_select'], width=800))
|
341 |
+
```
|
342 |
+
|
343 |
+
Another option when working with time series is to downsample the data before plotting it. This can be done with `downsample1D`. Algorithms supported are `lttb` (Largest Triangle Three Buckets) and `nth` element. The two algorithm is overlaid on top of the original curve in the example below.
|
344 |
+
|
345 |
+
|
346 |
+
```python
|
347 |
+
from holoviews.operation.downsample import downsample1d
|
348 |
+
|
349 |
+
lttb = downsample1d(curve)
|
350 |
+
nth = downsample1d(curve, algorithm="nth")
|
351 |
+
|
352 |
+
lttb_com = (curve * lttb).opts(width=800, title="lttb comparison")
|
353 |
+
nth_com = (curve * nth).opts(width=800, title="nth comparison")
|
354 |
+
|
355 |
+
(lttb_com + nth_com).cols(1)
|
356 |
+
```
|
357 |
+
|
358 |
+
The result of all these operations can be laid out, overlaid, selected, and sampled just like any other HoloViews element, letting you work naturally with even very large datasets.
|
359 |
+
|
360 |
+
Note that the above plot will look blocky in a static export (such as on anaconda.org), because the exported version is generated without taking the size of the actual plot (using default height and width for Datashader) into account, whereas the live notebook automatically regenerates the plot to match the visible area on the page.
|
361 |
+
|
362 |
+
# Element types supported for Datashading
|
363 |
+
|
364 |
+
Fundamentally, what Datashader does is to rasterize data, i.e., render a representation of it into a regularly gridded rectangular portion of a two-dimensional plane. Datashader natively supports six basic types of rasterization:
|
365 |
+
|
366 |
+
- **points**: zero-dimensional objects aggregated by position alone, each point covering zero area in the plane and thus falling into exactly one grid cell of the resulting array (if the point is within the bounds being aggregated).
|
367 |
+
- **line**: polyline/multiline objects (connected series of line segments), with each segment having a fixed length but either zero width (not antialiased) or a specified width, and crossing each grid cell at most once.
|
368 |
+
- **area**: a region to fill either between the supplied y-values and the origin or between two supplied lines.
|
369 |
+
- **trimesh**: irregularly spaced triangular grid, with each triangle covering a portion of the 2D plane and thus potentially crossing multiple grid cells (thus requiring interpolation/upsampling). Depending on the zoom level, a single pixel can also include multiple triangles, which then becomes similar to the `points` case (requiring aggregation/downsampling of all triangles covered by the pixel).
|
370 |
+
- **raster**: an axis-aligned regularly gridded two-dimensional subregion of the plane, with each grid cell in the source data covering more than one grid cell in the output grid (requiring interpolation/upsampling), or with each grid cell in the output grid including contributions from more than one grid cell in the input grid (requiring aggregation/downsampling).
|
371 |
+
- **quadmesh**: a recti-linear or curvi-linear mesh (like a raster, but allowing nonuniform spacing and coordinate mapping) where each quad can cover one or more cells in the output (requiring upsampling, currently only as nearest neighbor), or with each output grid cell including contributions from more than one input grid cell (requiring aggregation/downsampling).
|
372 |
+
- **polygons**: arbitrary filled shapes in 2D space (bounded by a piecewise linear set of segments), optionally punctuated by similarly bounded internal holes.
|
373 |
+
|
374 |
+
Datashader focuses on implementing those four cases very efficiently, and HoloViews in turn can use them to render a very large range of specific types of data:
|
375 |
+
|
376 |
+
### Supported Elements
|
377 |
+
|
378 |
+
- **points**: [`hv.Nodes`](../reference/elements/bokeh/Graph.ipynb), [`hv.Points`](../reference/elements/bokeh/Points.ipynb), [`hv.Scatter`](../reference/elements/bokeh/Scatter.ipynb)
|
379 |
+
- **line**: [`hv.Contours`](../reference/elements/bokeh/Contours.ipynb), [`hv.Curve`](../reference/elements/bokeh/Curve.ipynb), [`hv.Path`](../reference/elements/bokeh/Path.ipynb), [`hv.Graph`](../reference/elements/bokeh/Graph.ipynb), [`hv.EdgePaths`](../reference/elements/bokeh/Graph.ipynb), [`hv.Spikes`](../reference/elements/bokeh/Spikes.ipynb), [`hv.Segments`](../reference/elements/bokeh/Segments.ipynb)
|
380 |
+
- **area**: [`hv.Area`](../reference/elements/bokeh/Area.ipynb), [`hv.Rectangles`](../reference/elements/bokeh/Rectangles.ipynb), [`hv.Spread`](../reference/elements/bokeh/Spread.ipynb)
|
381 |
+
- **raster**: [`hv.Image`](../reference/elements/bokeh/Image.ipynb), [`hv.HSV`](../reference/elements/bokeh/HSV.ipynb), [`hv.RGB`](../reference/elements/bokeh/RGB.ipynb)
|
382 |
+
- **trimesh**: [`hv.TriMesh`](../reference/elements/bokeh/TriMesh.ipynb)
|
383 |
+
- **quadmesh**: [`hv.QuadMesh`](../reference/elements/bokeh/QuadMesh.ipynb)
|
384 |
+
- **polygons**: [`hv.Polygons`](../reference/elements/bokeh/Polygons.ipynb)
|
385 |
+
|
386 |
+
Other HoloViews elements *could* be supported, but do not currently have a useful datashaded representation:
|
387 |
+
|
388 |
+
### Elements not yet supported
|
389 |
+
|
390 |
+
- **line**: [`hv.Spline`](../reference/elements/bokeh/Spline.ipynb), [`hv.VectorField`](../reference/elements/bokeh/VectorField.ipynb)
|
391 |
+
- **raster**: [`hv.HeatMap`](../reference/elements/bokeh/HeatMap.ipynb), [`hv.Raster`](../reference/elements/bokeh/Raster.ipynb)
|
392 |
+
|
393 |
+
There are also other Elements that are not expected to be useful with datashader because they are isolated annotations, are already summaries or aggregations of other data, have graphical representations that are only meaningful at a certain size, or are text based:
|
394 |
+
|
395 |
+
### Not useful to support
|
396 |
+
|
397 |
+
- datashadable annotations: [`hv.Arrow`](../reference/elements/bokeh/Arrow.ipynb), [`hv.Bounds`](../reference/elements/bokeh/Bounds.ipynb), [`hv.Box`](../reference/elements/bokeh/Box.ipynb), [`hv.Ellipse`](../reference/elements/bokeh/Ellipse.ipynb) (actually do work with datashade currently, but not officially supported because they are not vectorized and thus unlikely to have enough items to be worth datashading)
|
398 |
+
- other annotations: [`hv.Labels`](../reference/elements/bokeh/Labels.ipynb), [`hv.HLine`](../reference/elements/bokeh/HLine.ipynb), [`hv.VLine`](../reference/elements/bokeh/VLine.ipynb), [`hv.Text`](../reference/elements/bokeh/Text.ipynb)
|
399 |
+
- kdes: [`hv.Distribution`](../reference/elements/bokeh/Distribution.ipynb), [`hv.Bivariate`](../reference/elements/bokeh/Bivariate.ipynb) (already aggregated)
|
400 |
+
- categorical/symbolic: [`hv.BoxWhisker`](../reference/elements/bokeh/BoxWhisker.ipynb), [`hv.Bars`](../reference/elements/bokeh/Bars.ipynb), [`hv.ErrorBars`](../reference/elements/bokeh/ErrorBars.ipynb)
|
401 |
+
- tables: [`hv.Table`](../reference/elements/bokeh/Table.ipynb), [`hv.ItemTable`](../reference/elements/bokeh/ItemTable.ipynb)
|
402 |
+
|
403 |
+
Let's make some examples of each supported Element type. First, some dummy data:
|
404 |
+
|
405 |
+
|
406 |
+
```python
|
407 |
+
from bokeh.sampledata.unemployment import data as unemployment
|
408 |
+
from bokeh.sampledata.us_counties import data as counties
|
409 |
+
|
410 |
+
np.random.seed(12)
|
411 |
+
N=50
|
412 |
+
pts = [(10*i/N, np.sin(10*i/N)) for i in range(N)]
|
413 |
+
|
414 |
+
x = y = np.linspace(0, 5, int(np.sqrt(N)))
|
415 |
+
xs,ys = np.meshgrid(x,y)
|
416 |
+
z = np.sin(xs)*np.cos(ys)
|
417 |
+
|
418 |
+
r = 0.5*np.sin(0.1*xs**2+0.05*ys**2)+0.5
|
419 |
+
g = 0.5*np.sin(0.02*xs**2+0.2*ys**2)+0.5
|
420 |
+
b = 0.5*np.sin(0.02*xs**2+0.02*ys**2)+0.5
|
421 |
+
|
422 |
+
n=20
|
423 |
+
coords = np.linspace(-1.5,1.5,n)
|
424 |
+
X,Y = np.meshgrid(coords, coords);
|
425 |
+
Qx = np.cos(Y) - np.cos(X)
|
426 |
+
Qy = np.sin(Y) + np.sin(X)
|
427 |
+
Z = np.sqrt(X**2 + Y**2)
|
428 |
+
|
429 |
+
rect_colors = {True: 'red', False: 'green'}
|
430 |
+
s = np.random.randn(100).cumsum()
|
431 |
+
e = s + np.random.randn(100)
|
432 |
+
```
|
433 |
+
|
434 |
+
Next, some options:
|
435 |
+
|
436 |
+
|
437 |
+
```python
|
438 |
+
hv.output(backend='matplotlib')
|
439 |
+
|
440 |
+
opts.defaults(opts.Layout(vspace=0.1, hspace=0.1, sublabel_format='', fig_size=48))
|
441 |
+
eopts = dict(aspect=1, axiswise=True, xaxis='bare', yaxis='bare', xticks=False, yticks=False)
|
442 |
+
opts2 = dict(filled=True, edge_color='z')
|
443 |
+
rect_opts = opts.Rectangles(lw=0, color=hv.dim('sign').categorize(rect_colors))
|
444 |
+
ds_point_opts = dict(aggregator='any')
|
445 |
+
ds_line_opts = dict(aggregator='any', line_width=1)
|
446 |
+
ResampleOperation2D.width=115
|
447 |
+
ResampleOperation2D.height=115
|
448 |
+
|
449 |
+
ds_opts = {
|
450 |
+
hv.Path: ds_line_opts,
|
451 |
+
hv.Graph: ds_line_opts,
|
452 |
+
hv.Contours: ds_line_opts,
|
453 |
+
hv.EdgePaths: ds_line_opts,
|
454 |
+
hv.Curve: ds_line_opts,
|
455 |
+
hv.Scatter: ds_point_opts,
|
456 |
+
hv.Points: ds_point_opts,
|
457 |
+
hv.Segments: ds_point_opts,
|
458 |
+
hv.Rectangles: dict(aggregator=ds.count_cat('sign'), color_key=rect_colors)
|
459 |
+
}
|
460 |
+
```
|
461 |
+
|
462 |
+
Now, some Elements that support datashading, in categories depending on whether they work best with `spread(rasterize())`, with plain `rasterize()`, or require `datashade()`:
|
463 |
+
|
464 |
+
|
465 |
+
```python
|
466 |
+
tri = hv.TriMesh.from_vertices(hv.Points(np.random.randn(N,3), vdims='z')).opts(**opts2)
|
467 |
+
|
468 |
+
rasterizable = [hv.Curve(pts)]
|
469 |
+
rasterizable += [hv.operation.contours(hv.Image((x,y,z)), levels=10)]
|
470 |
+
rasterizable += [hv.Area(np.random.randn(10000).cumsum())]
|
471 |
+
rasterizable += [hv.Spread((np.arange(10000), np.random.randn(10000).cumsum(), np.random.randn(10000)*10))]
|
472 |
+
rasterizable += [hv.Spikes(np.random.randn(1000))]
|
473 |
+
rasterizable += [hv.Graph(((np.zeros(N//2), np.arange(N//2)),))]
|
474 |
+
rasterizable += [hv.QuadMesh((Qx,Qy,Z))]
|
475 |
+
rasterizable += [hv.Image((x,y,z))]
|
476 |
+
rasterizable += [hv.RGB(np.dstack([r,g,b])), hv.HSV(np.dstack([g,b,r]))]
|
477 |
+
rasterizable += [tri, tri.edgepaths]
|
478 |
+
rasterizable += [hv.Path(counties[(1, 1)], ['lons', 'lats'])]
|
479 |
+
|
480 |
+
polys = hv.Polygons([dict(county, unemployment=unemployment[k])
|
481 |
+
for k, county in counties.items()
|
482 |
+
if county['state'] == 'tx'],
|
483 |
+
['lons', 'lats'], ['unemployment']).opts(color='unemployment')
|
484 |
+
try:
|
485 |
+
import spatialpandas # Needed for datashader polygon support
|
486 |
+
rasterizable += [polys]
|
487 |
+
except: pass
|
488 |
+
|
489 |
+
spreadable = [e(pts) for e in [hv.Scatter]]
|
490 |
+
spreadable += [hv.Points(counties[(1, 1)], ['lons', 'lats'])]
|
491 |
+
spreadable += [hv.Segments((np.arange(100), s, np.arange(100), e))]
|
492 |
+
|
493 |
+
shadeable = [hv.Rectangles((np.arange(100)-0.4, s, np.arange(100)+0.4, e, s>e), vdims='sign').opts(rect_opts)]
|
494 |
+
```
|
495 |
+
|
496 |
+
We can now view these with Datashader via `spread(rasterize())`, `rasterize()`, or `datashade()`:
|
497 |
+
|
498 |
+
|
499 |
+
```python
|
500 |
+
def nop(x,**k): return x
|
501 |
+
def spread2(e, **k): return spread(rasterize(e, **k), px=2).opts(cnorm='eq_hist', padding=0.1)
|
502 |
+
def plot(e, operation=nop):
|
503 |
+
return operation(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {})).opts(**eopts)
|
504 |
+
|
505 |
+
hv.Layout(
|
506 |
+
[plot(e, rasterize) for e in rasterizable] + \
|
507 |
+
[plot(e, spread2) for e in spreadable] + \
|
508 |
+
[plot(e, datashade) for e in shadeable]).cols(6)
|
509 |
+
```
|
510 |
+
|
511 |
+
For comparison, you can see the corresponding non-datashaded plots (as long as you leave N lower than 10000 unless you have a long time to wait!):
|
512 |
+
|
513 |
+
|
514 |
+
```python
|
515 |
+
hv.Layout([plot(e) for e in rasterizable + spreadable + shadeable]).cols(6)
|
516 |
+
```
|
517 |
+
|
518 |
+
The previous two sets of examples use Matplotlib, but if they were switched to Bokeh and you had a live server, they would support dynamic re-rendering on zoom and pan so that you could explore the full range of data available (e.g. even very large raster images, networks, paths, point clouds, or meshes).
|
519 |
+
|
520 |
+
|
521 |
+
```python
|
522 |
+
hv.output(backend='bokeh') # restore bokeh backend in case cells will run out of order
|
523 |
+
```
|
524 |
+
|
525 |
+
# Container types supported for datashading
|
526 |
+
|
527 |
+
In the above examples `datashade()` or `rasterize` was called directly on each Element, but these operations can also be called on Containers, in which case each Element in the Container will be datashaded separately (for all Container types other than a Layout):
|
528 |
+
|
529 |
+
|
530 |
+
```python
|
531 |
+
curves = {'+':hv.Curve(pts), '-':hv.Curve([(x, -1.0*y) for x, y in pts])}
|
532 |
+
rasterize(hv.HoloMap(curves,'sign'), line_width=3)
|
533 |
+
```
|
534 |
+
|
535 |
+
|
536 |
+
```python
|
537 |
+
rasterize(hv.NdLayout(curves,'sign'), line_width=3)
|
538 |
+
```
|
539 |
+
|
540 |
+
|
541 |
+
```python
|
542 |
+
containers = [hv.Overlay(list(curves.values())), hv.NdOverlay(curves), hv.GridSpace(hv.NdOverlay(curves))]
|
543 |
+
hv.Layout([rasterize(e.relabel(e.__class__.name), line_width=3) for e in containers]).cols(3)
|
544 |
+
```
|
545 |
+
|
546 |
+
# Optimizing performance
|
547 |
+
|
548 |
+
Datashader and HoloViews have different design principles that are worth keeping in mind when using them in combination, if you want to ensure good overall performance. By design, Datashader supports only a small number of operations and datatypes, focusing only on what can be implemented very efficiently. HoloViews instead focuses on supporting the typical workflows of Python users, recognizing that the most computationally efficient choice is only going to be faster overall if it also minimizes the time users have to spend getting things working.
|
549 |
+
|
550 |
+
HoloViews thus helps you get something working quickly, but once it is working and you realize that you need to do this often or that it comes up against the limits of your computing hardware, you can consider whether you can get much better performance by considering the following issues and suggestions.
|
551 |
+
|
552 |
+
### Use a Datashader-supported data structure
|
553 |
+
|
554 |
+
HoloViews helpfully tries to convert whatever data you have provided into what Datashader supports, which is good for optimizing your time to an initial solution, but will not always be the fastest approach computationally. If you ensure that you store your data in a format that Datashader understands already, HoloViews can simply pass it down to Datashader without copying or transforming it:
|
555 |
+
|
556 |
+
1. For point, line, and trimesh data, Datashader supports Dask and Pandas dataframes, and so those two data sources will be fastest. Of those two, Dask Dataframes will usually be somewhat faster and also able to make use of distributed computational resources and out-of-core processing.
|
557 |
+
2. For rasters and quadmeshes, Datashader supports xarray objects natively, and so if your data is provided as an xarray already, plotting will be faster.
|
558 |
+
3. For polygons Datashader supports [spatialpandas](https://github.com/holoviz/spatialpandas) DataFrames.
|
559 |
+
|
560 |
+
See the [Datashader docs](http://datashader.org) for examples of dealing with even quite large datasets (in the billions of points) on commodity hardware, including many HoloViews-based examples.
|
561 |
+
|
562 |
+
### Cache initial processing with `precompute=True`
|
563 |
+
|
564 |
+
In the typical case of having datasets much larger than the plot resolution, HoloViews Datashader-based operations that work on the full dataset (`rasterize`, `aggregate`,`regrid`) are computationally expensive; the others are not (`shade`, `spread`, `dynspread`, etc.)
|
565 |
+
|
566 |
+
The expensive operations are all of type `ResamplingOperation`, which has a parameter `precompute` (see `hv.help(hv.operation.datashader.rasterize)`, etc.) Precompute can be used to get faster performance in interactive usage by caching the last set of data used in plotting (*after* any transformations needed) and reusing it when it is requested again. This is particularly useful when your data is not in one of the supported data formats already and needs to be converted. `precompute` is False by default, because it requires using memory to store the cached data, but if you have enough memory, you can enable it so that repeated interactions (such as zooming and panning) will be much faster than the first one. In practice, most Datashader-plots don't need to do extensive precomputing, but enabling it for TriMesh and Polygon plots can greatly speed up interactive usage.
|
567 |
+
|
568 |
+
### Use GPU support
|
569 |
+
|
570 |
+
Many elements now also support aggregation directly on a GPU-based datastructure such as a [cuDF DataFrame](https://github.com/rapidsai/cudf) or an Xarray DataArray backed by a [cupy](https://github.com/cupy/cupy) array. These data structures can be passed directly to the appropriate HoloViews elements just as you would use a Pandas or other Xarray object. For instance, a cuDF can be used on elements like `hv.Points` and `hv.Curve`, while a cupy-backed DataArray raster or quadmesh can be passed to `hv.QuadMesh` elements. When used with Datashader, the GPU implementation can result in 10-100x speedups, as well as avoiding having to transfer the data out of the GPU for plotting (sending only the final rendered plot out of the GPU's memory). To see which HoloViews elements are supported, see the [datashader performance guide](https://datashader.org/user_guide/Performance.html). As of the Datashader 0.11 release, all point, line, area, and quadmesh aggregations are supported when using a GPU backed datastructure, including raster objects like `hv.Image` if first converted to `hv.Quadmesh`.
|
571 |
+
|
572 |
+
### Project data only once
|
573 |
+
|
574 |
+
If you are working with geographic data using [GeoViews](http://geoviews.org) that needs to be projected before display and/or before datashading, GeoViews will have to do this every time you update a plot, which can drown out the performance improvement you get by using Datashader. GeoViews allows you to project the entire dataset at once using `gv.operation.project`, and once you do this you should be able to use Datashader at full speed.
|
575 |
+
|
576 |
+
If you follow these suggestions, the combination of HoloViews and Datashader will allow you to work uniformly with data covering a huge range of sizes. Per session or per plot, you can trade off the ability to export user-manipulable plots against file size and browser compatibility, and allowing you to render even the largest dataset faithfully. HoloViews makes the full power of Datashader available in just a few lines of code, giving you a natural way to work with your data regardless of its size.
|
hvplot_docs/16-Streaming_Data.md
ADDED
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Working with Streaming Data
|
2 |
+
|
3 |
+
"Streaming data" is data that is continuously generated, often by some external source like a remote website, a measuring device, or a simulator. This kind of data is common for financial time series, web server logs, scientific applications, and many other situations. We have seen how to visualize any data output by a callable in the [Live Data](07-Live_Data.ipynb) user guide and we have also seen how to use the HoloViews stream system to push events in the user guide sections [Responding to Events](12-Responding_to_Events.ipynb) and [Custom Interactivity](13-Custom_Interactivity.ipynb).
|
4 |
+
|
5 |
+
This user guide shows a third way of building an interactive plot, using ``DynamicMap`` and streams. Here, instead of pushing plot metadata (such as zoom ranges, user triggered events such as ``Tap`` and so on) to a ``DynamicMap`` callback, the underlying data in the visualized elements are updated directly using a HoloViews ``Stream``.
|
6 |
+
|
7 |
+
In particular, we will show how the HoloViews ``Pipe`` and ``Buffer`` streams can be used to work with streaming data sources without having to fetch or generate the data from inside the ``DynamicMap`` callable. Apart from simply setting element data from outside a ``DynamicMap``, we will also explore ways of working with streaming data coordinated by the separate [``streamz``](http://matthewrocklin.com/blog/work/2017/10/16/streaming-dataframes-1) library from Matt Rocklin, which can make building complex streaming pipelines much simpler.
|
8 |
+
|
9 |
+
As this notebook makes use of the ``streamz`` library, you will need to install it with ``conda install streamz`` or ``pip install streamz``.
|
10 |
+
|
11 |
+
|
12 |
+
```python
|
13 |
+
import time
|
14 |
+
import numpy as np
|
15 |
+
import pandas as pd
|
16 |
+
import holoviews as hv
|
17 |
+
import streamz
|
18 |
+
import streamz.dataframe
|
19 |
+
|
20 |
+
from holoviews import opts
|
21 |
+
from holoviews.streams import Pipe, Buffer
|
22 |
+
|
23 |
+
hv.extension('bokeh')
|
24 |
+
```
|
25 |
+
|
26 |
+
## ``Pipe``
|
27 |
+
|
28 |
+
A ``Pipe`` allows data to be pushed into a DynamicMap callback to change a visualization, just like the streams in the [Responding to Events](./12-Responding_to_Events.ipynb) user guide were used to push changes to metadata that controlled the visualization. A ``Pipe`` can be used to push data of any type and make it available to a ``DynamicMap`` callback. Since all ``Element`` types accept ``data`` of various forms we can use ``Pipe`` to push data directly to the constructor of an ``Element`` through a DynamicMap.
|
29 |
+
|
30 |
+
|
31 |
+
We can take advantage of the fact that most Elements can be instantiated without providing any data, so we declare the the ``Pipe`` with an empty list, declare the ``DynamicMap``, providing the pipe as a stream, which will dynamically update a ``VectorField`` :
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
pipe = Pipe(data=[])
|
36 |
+
vector_dmap = hv.DynamicMap(hv.VectorField, streams=[pipe])
|
37 |
+
vector_dmap.opts(color='Magnitude', xlim=(-1, 1), ylim=(-1, 1))
|
38 |
+
```
|
39 |
+
|
40 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/pipe_vectorfield.gif"></img>
|
41 |
+
|
42 |
+
Having set up this ``VectorField`` tied to a ``Pipe`` we can start pushing data to it varying the orientation of the VectorField:
|
43 |
+
|
44 |
+
|
45 |
+
```python
|
46 |
+
x,y = np.mgrid[-10:11,-10:11] * 0.1
|
47 |
+
sine_rings = np.sin(x**2+y**2)*np.pi+np.pi
|
48 |
+
exp_falloff = 1/np.exp((x**2+y**2)/8)
|
49 |
+
|
50 |
+
for i in np.linspace(0, 1, 25):
|
51 |
+
time.sleep(0.1)
|
52 |
+
pipe.send((x,y,sine_rings*i, exp_falloff))
|
53 |
+
```
|
54 |
+
|
55 |
+
This approach of using an element constructor directly does not allow you to use anything other than the default key and value dimensions. One simple workaround for this limitation is to use ``functools.partial`` as demonstrated in the **Controlling the length section** below.
|
56 |
+
|
57 |
+
Since ``Pipe`` is completely general and the data can be any custom type, it provides a completely general mechanism to stream structured or unstructured data. Due to this generality, ``Pipe`` does not offer some of the more complex features and optimizations available when using the ``Buffer`` stream described in the next section.
|
58 |
+
|
59 |
+
## ``Buffer``
|
60 |
+
|
61 |
+
While ``Pipe`` provides a general solution for piping arbitrary data to ``DynamicMap`` callback, ``Buffer`` on the other hand provides a very powerful means of working with streaming tabular data, defined as pandas dataframes, arrays or dictionaries of columns (as well as StreamingDataFrame, which we will cover later). ``Buffer`` automatically accumulates the last ``N`` rows of the tabular data, where ``N`` is defined by the ``length``.
|
62 |
+
|
63 |
+
The ability to accumulate data allows performing operations on a recent history of data, while plotting backends (such as bokeh) can optimize plot updates by sending just the latest patch. This optimization works only if the ``data`` object held by the ``Buffer`` is identical to the plotted ``Element`` data, otherwise all the data will be updated as normal.
|
64 |
+
|
65 |
+
#### A simple example: Brownian motion
|
66 |
+
|
67 |
+
To initialize a ``Buffer`` we have to provide an example dataset which defines the columns and dtypes of the data we will be streaming. Next we define the ``length`` to keep the last 100 rows of data. If the data is a DataFrame we can specify whether we will also want to use the ``DataFrame`` ``index``. In this case we will simply define that we want to plot a ``DataFrame`` of 'x' and 'y' positions and a 'count' as ``Points`` and ``Curve`` elements:
|
68 |
+
|
69 |
+
|
70 |
+
```python
|
71 |
+
example = pd.DataFrame({'x': [], 'y': [], 'count': []}, columns=['x', 'y', 'count'])
|
72 |
+
dfstream = Buffer(example, length=100, index=False)
|
73 |
+
curve_dmap = hv.DynamicMap(hv.Curve, streams=[dfstream])
|
74 |
+
point_dmap = hv.DynamicMap(hv.Points, streams=[dfstream])
|
75 |
+
```
|
76 |
+
|
77 |
+
After applying some styling we will display an ``Overlay`` of the dynamic ``Curve`` and ``Points``
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
(curve_dmap * point_dmap).opts(
|
82 |
+
opts.Points(color='count', line_color='black', size=5, padding=0.1, xaxis=None, yaxis=None),
|
83 |
+
opts.Curve(line_width=1, color='black'))
|
84 |
+
```
|
85 |
+
|
86 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/brownian.gif"></img>
|
87 |
+
|
88 |
+
Now that we have set up the ``Buffer`` and defined a ``DynamicMap`` to plot the data we can start pushing data to it. We will define a simple function which simulates brownian motion by accumulating x, y positions. We can ``send`` data through the ``hv.streams.Buffer`` directly.
|
89 |
+
|
90 |
+
|
91 |
+
```python
|
92 |
+
def gen_brownian():
|
93 |
+
x, y, count = 0, 0, 0
|
94 |
+
while True:
|
95 |
+
x += np.random.randn()
|
96 |
+
y += np.random.randn()
|
97 |
+
count += 1
|
98 |
+
yield pd.DataFrame([(x, y, count)], columns=['x', 'y', 'count'])
|
99 |
+
|
100 |
+
brownian = gen_brownian()
|
101 |
+
for i in range(200):
|
102 |
+
dfstream.send(next(brownian))
|
103 |
+
```
|
104 |
+
|
105 |
+
Finally we can clear the data on the stream and plot using the ``clear`` method:
|
106 |
+
|
107 |
+
|
108 |
+
```python
|
109 |
+
dfstream.clear()
|
110 |
+
```
|
111 |
+
|
112 |
+
Note that when using the ``Buffer`` stream the view will always follow the current range of the data by default, by setting ``buffer.following=False`` or passing following as an argument to the constructor this behavior may be disabled.
|
113 |
+
|
114 |
+
## Using the Streamz library
|
115 |
+
|
116 |
+
Now that we have discovered what ``Pipe`` and ``Buffer`` can do it's time to show how you can use them together with the ``streamz`` library. Although HoloViews does not depend on ``streamz`` and you can use the streaming functionality without needing to learn about it, the two libraries work well together, allowing you to build pipelines to manage continuous streams of data. Streamz is easy to use for simple tasks, but also supports complex pipelines that involve branching, joining, flow control, feedback and more. Here we will mostly focus on connecting streamz output to ``Pipe`` and then ``Buffer`` so for more details about the streamz API, consult the [streamz documentation](https://streamz.readthedocs.io/en/latest/).
|
117 |
+
|
118 |
+
#### Using ``streamz.Stream`` together with ``Pipe``
|
119 |
+
|
120 |
+
Let's start with a fairly simple example:
|
121 |
+
|
122 |
+
1. Declare a ``streamz.Stream`` and a ``Pipe`` object and connect them into a pipeline into which we can push data.
|
123 |
+
2. Use a ``sliding_window`` of 10, which will first wait for 10 sets of stream updates to accumulate. At that point and for every subsequent update, it will apply ``pd.concat`` to combine the most recent 10 updates into a new dataframe.
|
124 |
+
3. Use the ``sink`` method on the ``streamz.Stream`` to ``send`` the resulting collection of 10 updates to ``Pipe``.
|
125 |
+
4. Declare a ``DynamicMap`` that takes the sliding window of concatenated DataFrames and displays it using a ``Scatter`` Element.
|
126 |
+
5. Color the ``Scatter`` points by their 'count' and set a range, then display:
|
127 |
+
|
128 |
+
|
129 |
+
```python
|
130 |
+
point_source = streamz.Stream()
|
131 |
+
pipe = Pipe(data=pd.DataFrame({'x': [], 'y': [], 'count': []}))
|
132 |
+
point_source.sliding_window(20).map(pd.concat).sink(pipe.send) # Connect streamz to the Pipe
|
133 |
+
scatter_dmap = hv.DynamicMap(hv.Scatter, streams=[pipe])
|
134 |
+
```
|
135 |
+
|
136 |
+
After set up our streaming pipeline we can again display it:
|
137 |
+
|
138 |
+
|
139 |
+
```python
|
140 |
+
scatter_dmap.opts(bgcolor='black', color='count', ylim=(-4, 4), show_legend=False)
|
141 |
+
```
|
142 |
+
|
143 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz1.gif"></img>
|
144 |
+
|
145 |
+
There is now a pipeline, but initially this plot will be empty, because no data has been sent to it. To see the plot update, let's use the ``emit`` method of ``streamz.Stream`` to send small chunks of random pandas ``DataFrame``s to our plot:
|
146 |
+
|
147 |
+
|
148 |
+
```python
|
149 |
+
for i in range(100):
|
150 |
+
df = pd.DataFrame({'x': np.random.rand(100), 'y': np.random.randn(100), 'count': i},
|
151 |
+
columns=['x', 'y', 'count'])
|
152 |
+
point_source.emit(df)
|
153 |
+
```
|
154 |
+
|
155 |
+
#### Using StreamingDataFrame and StreamingSeries
|
156 |
+
|
157 |
+
The streamz library provides ``StreamingDataFrame`` and ``StreamingSeries`` as a powerful way to easily work with live sources of tabular data. This makes it perfectly suited to work with ``Buffer``. With the ``StreamingDataFrame`` we can easily stream data, apply computations such as cumulative and rolling statistics and then visualize the data with HoloViews.
|
158 |
+
|
159 |
+
The ``streamz.dataframe`` module provides a ``Random`` utility that generates a ``StreamingDataFrame`` that emits random data with a certain frequency at a specified interval. The ``example`` attribute lets us see the structure and dtypes of the data we can expect:
|
160 |
+
|
161 |
+
|
162 |
+
```python
|
163 |
+
simple_sdf = streamz.dataframe.Random(freq='10ms', interval='100ms')
|
164 |
+
print(simple_sdf.index)
|
165 |
+
simple_sdf.example.dtypes
|
166 |
+
```
|
167 |
+
|
168 |
+
Since the ``StreamingDataFrame`` provides a pandas-like API, we can specify operations on the data directly. In this example we subtract a fixed offset and then compute the cumulative sum, giving us a randomly drifting timeseries. We can then pass the x-values of this dataframe to the HoloViews ``Buffer`` and supply ``hv.Curve`` as the ``DynamicMap`` callback to stream the data into a HoloViews ``Curve`` (with the default key and value dimensions):
|
169 |
+
|
170 |
+
|
171 |
+
```python
|
172 |
+
sdf = (simple_sdf-0.5).cumsum()
|
173 |
+
hv.DynamicMap(hv.Curve, streams=[Buffer(sdf.x)]).opts(width=500, show_grid=True)
|
174 |
+
```
|
175 |
+
|
176 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz3.gif"></img>
|
177 |
+
|
178 |
+
The ``Random`` StreamingDataFrame will asynchronously emit events, driving the visualization forward, until it is explicitly stopped, which we can do by calling the ``stop`` method.
|
179 |
+
|
180 |
+
|
181 |
+
```python
|
182 |
+
simple_sdf.stop()
|
183 |
+
```
|
184 |
+
|
185 |
+
#### Making use of the ``StreamingDataFrame`` API
|
186 |
+
|
187 |
+
So far we have only computed the cumulative sum, but the ``StreamingDataFrame`` actually has an extensive API that lets us run a broad range of streaming computations on our data. For example, let's apply a rolling mean to our x-values with a window of 500ms and overlay it on top of the 'raw' data:
|
188 |
+
|
189 |
+
|
190 |
+
```python
|
191 |
+
source_df = streamz.dataframe.Random(freq='5ms', interval='100ms')
|
192 |
+
sdf = (source_df-0.5).cumsum()
|
193 |
+
raw_dmap = hv.DynamicMap(hv.Curve, streams=[Buffer(sdf.x)])
|
194 |
+
smooth_dmap = hv.DynamicMap(hv.Curve, streams=[Buffer(sdf.x.rolling('500ms').mean())])
|
195 |
+
|
196 |
+
(raw_dmap.relabel('raw') * smooth_dmap.relabel('smooth')).opts(
|
197 |
+
opts.Curve(width=500, show_grid=True))
|
198 |
+
```
|
199 |
+
|
200 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz4.gif"></img>
|
201 |
+
|
202 |
+
|
203 |
+
```python
|
204 |
+
source_df.stop()
|
205 |
+
```
|
206 |
+
|
207 |
+
#### Customizing elements with ``functools.partial``
|
208 |
+
|
209 |
+
In this notebook we have avoided defining custom functions for ``DynamicMap`` by simply supplying the element class and using the element constructor instead. Although this works well for examples, it often won't generalize to real-life situations, because you don't have an opportunity to use anything other than the default dimensions. One simple way to get around this limitation is to use ``functools.partial``:
|
210 |
+
|
211 |
+
|
212 |
+
|
213 |
+
|
214 |
+
```python
|
215 |
+
from functools import partial
|
216 |
+
```
|
217 |
+
|
218 |
+
Now you can now easily create an inline callable that creates an element with custom key and value dimensions by supplying them to ``partial`` in the form ``partial(hv.Element, kdims=[...], vdims=[...])``. In the next section, we will see an example of this pattern using ``hv.BoxWhisker``.
|
219 |
+
|
220 |
+
#### Controlling the length
|
221 |
+
|
222 |
+
By default the ``Buffer`` accumulates a ``length`` of 1000 samples. In many cases this may be excessive, but we can specify a shorter (or longer) length value to control how much history we accumulate, often depending on the element type.
|
223 |
+
|
224 |
+
In the following example, a custom ``length`` is used together with a ``partial`` wrapping ``hv.BoxWhisker`` in order to display a cumulative sum generated from a stream of random dataframes:
|
225 |
+
|
226 |
+
|
227 |
+
```python
|
228 |
+
multi_source = streamz.dataframe.Random(freq='50ms', interval='500ms')
|
229 |
+
sdf = (multi_source-0.5).cumsum()
|
230 |
+
hv.DynamicMap(hv.Table, streams=[Buffer(sdf.x, length=10)]) +\
|
231 |
+
hv.DynamicMap(partial(hv.BoxWhisker, kdims=[], vdims='x'), streams=[Buffer(sdf.x, length=100)])
|
232 |
+
```
|
233 |
+
|
234 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz5.gif"></img>
|
235 |
+
|
236 |
+
Here the given stream ``sdf`` is being consumed by a table showing a short length (where only the items visible in the table need to be kept), along with a plot computing averages and variances over a longer length (100 items).
|
237 |
+
|
238 |
+
#### Updating multiple cells
|
239 |
+
|
240 |
+
Since a ``StreamingDataFrame`` will emit data until it is stopped, we can subscribe multiple plots across different cells to the same stream. Here, let's add a ``Scatter`` plot of the same data stream as in the preceding cell:
|
241 |
+
|
242 |
+
|
243 |
+
```python
|
244 |
+
hv.DynamicMap(hv.Scatter, streams=[Buffer(sdf.x)]).redim.label(x='value', index='time')
|
245 |
+
```
|
246 |
+
|
247 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz6.gif"></img>
|
248 |
+
|
249 |
+
Here we let the ``Scatter`` elements use the column names from the supplied ``DataFrames`` which are relabelled using the ``redim`` method. Stopping the stream will now stop updates to all three of these DynamicMaps:
|
250 |
+
|
251 |
+
|
252 |
+
```python
|
253 |
+
multi_source.stop()
|
254 |
+
```
|
255 |
+
|
256 |
+
## Operations over streaming data
|
257 |
+
|
258 |
+
As we discovered above, the ``Buffer`` lets us set a ``length``, which defines how many rows we want to accumulate. We can use this to our advantage and apply an operation over this length window. In this example we declare a ``Dataset`` and then apply the ``histogram`` operation to compute a ``Histogram`` over the specified ``length`` window:
|
259 |
+
|
260 |
+
|
261 |
+
```python
|
262 |
+
hist_source = streamz.dataframe.Random(freq='5ms', interval='100ms')
|
263 |
+
sdf = (hist_source-0.5).cumsum()
|
264 |
+
dmap = hv.DynamicMap(hv.Dataset, streams=[Buffer(sdf.x, length=500)])
|
265 |
+
hv.operation.histogram(dmap, dimension='x')
|
266 |
+
```
|
267 |
+
|
268 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz7.gif"></img>
|
269 |
+
|
270 |
+
|
271 |
+
```python
|
272 |
+
hist_source.stop()
|
273 |
+
```
|
274 |
+
|
275 |
+
#### Datashading
|
276 |
+
|
277 |
+
The same approach will also work for the datashader operation letting us datashade the entire ``length`` window even if we make it very large such as 1 million samples:
|
278 |
+
|
279 |
+
|
280 |
+
```python
|
281 |
+
from holoviews.operation.datashader import datashade
|
282 |
+
from bokeh.palettes import Blues8
|
283 |
+
|
284 |
+
large_source = streamz.dataframe.Random(freq='100us', interval='200ms')
|
285 |
+
sdf = (large_source-0.5).cumsum()
|
286 |
+
dmap = hv.DynamicMap(hv.Curve, streams=[Buffer(sdf.x, length=1000000)])
|
287 |
+
datashade(dmap, streams=[hv.streams.PlotSize], cnorm='linear', cmap=list(Blues8)).opts(width=600)
|
288 |
+
```
|
289 |
+
|
290 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz8.gif"></img>
|
291 |
+
|
292 |
+
|
293 |
+
```python
|
294 |
+
large_source.stop()
|
295 |
+
```
|
296 |
+
|
297 |
+
## Asynchronous updates using the tornado ``IOLoop``
|
298 |
+
|
299 |
+
In most cases, instead of pushing updates manually from the same Python process, you'll want the object to update asynchronously as new data arrives. Since both Jupyter and Bokeh server run on [tornado](http://www.tornadoweb.org/en/stable/), we can use the tornado ``IOLoop`` in both cases to define a non-blocking co-routine that can push data to our stream whenever it is ready. The ``PeriodicCallback`` makes this approach very simple, we simply define a function which will be called periodically with a timeout defined in milliseconds. Once we have declared the callback we can call ``start`` to begin emitting events:
|
300 |
+
|
301 |
+
|
302 |
+
```python
|
303 |
+
from tornado.ioloop import PeriodicCallback
|
304 |
+
from tornado import gen
|
305 |
+
|
306 |
+
count = 0
|
307 |
+
buffer = Buffer(np.zeros((0, 2)), length=50)
|
308 |
+
|
309 |
+
@gen.coroutine
|
310 |
+
def f():
|
311 |
+
global count
|
312 |
+
count += 1
|
313 |
+
buffer.send(np.array([[count, np.random.rand()]]))
|
314 |
+
|
315 |
+
cb = PeriodicCallback(f, 100)
|
316 |
+
cb.start()
|
317 |
+
|
318 |
+
hv.DynamicMap(hv.Curve, streams=[buffer]).opts(padding=0.1, width=600)
|
319 |
+
```
|
320 |
+
|
321 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz2.gif"></img>
|
322 |
+
|
323 |
+
Since the callback is non-blocking we can continue working in the notebook and execute other cells. Once we're done we can stop the callback by calling ``cb.stop()``.
|
324 |
+
|
325 |
+
|
326 |
+
```python
|
327 |
+
cb.stop()
|
328 |
+
```
|
329 |
+
|
330 |
+
## Real examples
|
331 |
+
|
332 |
+
Using the ``Pipe`` and ``Buffer`` streams we can create complex streaming plots very easily. In addition to the toy examples we presented in this guide it is worth looking at looking at some of the examples using real, live, streaming data.
|
333 |
+
|
334 |
+
* The [streaming_psutil](http://holoviews.org/gallery/apps/bokeh/streaming_psutil.html) bokeh app is one such example which display CPU and memory information using the ``psutil`` library (install with ``pip install psutil`` or ``conda install psutil``)
|
335 |
+
|
336 |
+
<img class="gif" src="https://assets.holoviews.org/gifs/guides/user_guide/Streaming_Data/streamz9.gif"></img>
|
337 |
+
|
338 |
+
As you can see, streaming data works like streams in HoloViews in general, flexibly handling changes over time under either explicit control or governed by some external data source.
|
hvplot_docs/17-Dashboards.md
ADDED
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Creating interactive dashboards
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import pandas as pd
|
6 |
+
import holoviews as hv
|
7 |
+
|
8 |
+
from bokeh.sampledata import stocks
|
9 |
+
from holoviews.operation.timeseries import rolling, rolling_outlier_std
|
10 |
+
|
11 |
+
hv.extension('bokeh')
|
12 |
+
```
|
13 |
+
|
14 |
+
In the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb) we discovered how to declare a ``DynamicMap`` and control multiple processing steps with the use of custom streams as described in the [Responding to Events](./12-Responding_to_Events.ipynb) guide. A DynamicMap works like a tiny web application, with widgets that select values along a dimension, and a plot that updates. Let's start with a function that loads stock data and see what a DynamicMap can do:
|
15 |
+
|
16 |
+
|
17 |
+
```python
|
18 |
+
def load_symbol(symbol, variable, **kwargs):
|
19 |
+
df = pd.DataFrame(getattr(stocks, symbol))
|
20 |
+
df['date'] = df.date.astype('datetime64[ns]')
|
21 |
+
return hv.Curve(df, ('date', 'Date'), variable).opts(framewise=True)
|
22 |
+
|
23 |
+
stock_symbols = ['AAPL', 'IBM', 'FB', 'GOOG', 'MSFT']
|
24 |
+
variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']
|
25 |
+
dmap = hv.DynamicMap(load_symbol, kdims=['Symbol','Variable'])
|
26 |
+
dmap = dmap.redim.values(Symbol=stock_symbols, Variable=variables)
|
27 |
+
|
28 |
+
dmap.opts(framewise=True)
|
29 |
+
rolling(dmap, rolling_window=2)
|
30 |
+
```
|
31 |
+
|
32 |
+
Here we already have widgets for Symbol and Variable, as those are dimensions in the DynamicMap, but what if we wanted a widget to control the `rolling_window`width value in the HoloViews operation? We could redefine the DynamicMap to include the operation and accept that parameter as another dimension, but in complex cases we would quickly find we need more flexibility in defining widgets and layouts than DynamicMap can give us directly.
|
33 |
+
|
34 |
+
## Building dashboards
|
35 |
+
|
36 |
+
For more flexibility, we can build a full-featured dashboard using the [Panel](https://panel.pyviz.org) library, which is what a DynamicMap is already using internally to generate widgets and layouts. We can easily declare our own custom Panel widgets and link them to HoloViews streams to get dynamic, user controllable analysis workflows.
|
37 |
+
|
38 |
+
Here, let's start with defining various Panel widgets explicitly, choosing a `RadioButtonGroup` for the `symbol` instead of DynamicMaps's default `Select` widget:
|
39 |
+
|
40 |
+
|
41 |
+
```python
|
42 |
+
import panel as pn
|
43 |
+
|
44 |
+
symbol = pn.widgets.RadioButtonGroup(options=stock_symbols)
|
45 |
+
variable = pn.widgets.Select(options=variables)
|
46 |
+
rolling_window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)
|
47 |
+
|
48 |
+
pn.Column(symbol, variable, rolling_window)
|
49 |
+
```
|
50 |
+
|
51 |
+
As you can see, these widgets can be displayed but they aren't yet attached to anything, so they don't do much. We can now use ``pn.bind`` to bind the `symbol` and `variable` widgets to the arguments of the DynamicMap callback function, and provide `rolling_window` to the `rolling` operation argument. (HoloViews operations accept Panel widgets or param Parameter values, and they will then update reactively to changes in those widgets.)
|
52 |
+
|
53 |
+
We can then lay it all out into a simple application that works similarly to the regular DynamicMap display but where we can add our additional widget and control every aspect of the widget configuration and the layout:
|
54 |
+
|
55 |
+
|
56 |
+
```python
|
57 |
+
dmap = hv.DynamicMap(pn.bind(load_symbol, symbol=symbol, variable=variable))
|
58 |
+
smoothed = rolling(dmap, rolling_window=rolling_window)
|
59 |
+
|
60 |
+
app = pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window),
|
61 |
+
smoothed.opts(width=500, framewise=True)).servable()
|
62 |
+
app
|
63 |
+
```
|
64 |
+
|
65 |
+
Here we chose to lay the widgets out into a box to the left of the plot, but we could put the widgets each in different locations, add different plots, etc., to create a full-featured dashboard. See [panel.holoviz.org](https://panel.holoviz.org) for the full set of widgets and layouts supported.
|
66 |
+
|
67 |
+
Now that we have an app, we can launch it in a separate server if we wish (using `app.show()`), run it as an entirely separate process (`panel serve <thisfilename>.ipynb`, to serve the object marked `servable` above), or export it to a static HTML file (sampling the space of parameter values using "embed"):
|
68 |
+
|
69 |
+
|
70 |
+
```python
|
71 |
+
app.save("dashboard.html", embed=True)
|
72 |
+
```
|
73 |
+
|
74 |
+
## Declarative dashboards
|
75 |
+
|
76 |
+
What if we want our analysis code usable both as a dashboard and also in "headless" contexts such as batch jobs or remote execution? Both Panel and HoloViews are built on the [param](https://param.holoviz.org) library, which lets you capture the definitions and allowable values for your widgets in a way that's not attached to any GUI. That way you can declare all of your attributes and allowed values once, presenting a GUI if you want to explore them interactively or else simply provide specific values if you want batch operation.
|
77 |
+
|
78 |
+
With this approach, we declare a ``StockExplorer`` class subclassing ``Parameterized`` and defining three parameters, namely the rolling window, the symbol, and the variable to show for that symbol:
|
79 |
+
|
80 |
+
|
81 |
+
```python
|
82 |
+
import param
|
83 |
+
|
84 |
+
class StockExplorer(param.Parameterized):
|
85 |
+
|
86 |
+
rolling_window = param.Integer(default=10, bounds=(1, 365))
|
87 |
+
symbol = param.ObjectSelector(default='AAPL', objects=stock_symbols)
|
88 |
+
variable = param.ObjectSelector(default='adj_close', objects=variables)
|
89 |
+
|
90 |
+
@param.depends('symbol', 'variable')
|
91 |
+
def load_symbol(self):
|
92 |
+
df = pd.DataFrame(getattr(stocks, self.symbol))
|
93 |
+
df['date'] = df.date.astype('datetime64[ns]')
|
94 |
+
return hv.Curve(df, ('date', 'Date'), self.variable).opts(framewise=True)
|
95 |
+
```
|
96 |
+
|
97 |
+
Here the StockExplorer class will look similar to the Panel code above, defining most of the same information that's in the Panel widgets, but without any dependency on Panel or other GUI libraries; it's simply declaring that this code accepts certain parameter values of the specified types and ranges. These declarations are useful even outside a GUI context, because they allow type and range checking for detecting user errors, but they are also sufficient for creating a GUI later.
|
98 |
+
|
99 |
+
Instead of using `pn.bind` to bind widget values to functions, here we are declaring that each method depends on the specified parameters, which can be expressed independently of whether there is a widget controlling those parameters; it simply declares (in a way that Panel can utilize) that the given method needs re-running when any of the parameters in that list changes.
|
100 |
+
|
101 |
+
Now let's use the `load_symbol` method, which already declares which parameters it depends on, as the callback of a DynamicMap and create widgets out of those parameters to build a little GUI:
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
explorer = StockExplorer()
|
106 |
+
stock_dmap = hv.DynamicMap(explorer.load_symbol)
|
107 |
+
pn.Row(explorer.param, stock_dmap)
|
108 |
+
```
|
109 |
+
|
110 |
+
Here you'll notice that the `rolling_window` widget doesn't do anything, because it's not connected to anything (e.g., nothing `@param.depends` on it). As we saw in the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb), the ``rolling`` and ``rolling_outlier_std`` operations both accept a ``rolling_window`` parameter, so lets provide that to the operations and display the output of those operations. Finally we compose everything into a panel ``Row``:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
# Apply rolling mean
|
115 |
+
smoothed = rolling(stock_dmap, rolling_window=explorer.param.rolling_window)
|
116 |
+
|
117 |
+
# Find outliers
|
118 |
+
outliers = rolling_outlier_std(stock_dmap, rolling_window=explorer.param.rolling_window).opts(
|
119 |
+
color='red', marker='triangle')
|
120 |
+
|
121 |
+
pn.Row(explorer.param, (smoothed * outliers).opts(width=600))
|
122 |
+
```
|
123 |
+
|
124 |
+
## Replacing the output
|
125 |
+
|
126 |
+
Updating plots using a ``DynamicMap`` is a very efficient means of updating a plot since it will only update the data that has changed. In some cases it is either necessary or more convenient to redraw a plot entirely. ``Panel`` makes this easy by annotating a method with any dependencies that should trigger the plot to be redrawn. In the example below we extend the ``StockExplorer`` by adding a ``datashade`` boolean and a view method which will flip between a datashaded and regular view of the plot:
|
127 |
+
|
128 |
+
|
129 |
+
```python
|
130 |
+
from holoviews.operation.datashader import datashade, dynspread
|
131 |
+
|
132 |
+
class AdvancedStockExplorer(StockExplorer):
|
133 |
+
|
134 |
+
datashade = param.Boolean(default=False)
|
135 |
+
|
136 |
+
@param.depends('datashade')
|
137 |
+
def view(self):
|
138 |
+
stocks = hv.DynamicMap(self.load_symbol)
|
139 |
+
|
140 |
+
# Apply rolling mean
|
141 |
+
smoothed = rolling(stocks, rolling_window=self.param.rolling_window)
|
142 |
+
if self.datashade:
|
143 |
+
smoothed = dynspread(datashade(smoothed, aggregator='any')).opts(framewise=True)
|
144 |
+
|
145 |
+
# Find outliers
|
146 |
+
outliers = rolling_outlier_std(stocks, rolling_window=self.param.rolling_window).opts(
|
147 |
+
width=600, color='red', marker='triangle', framewise=True)
|
148 |
+
return (smoothed * outliers)
|
149 |
+
```
|
150 |
+
|
151 |
+
In the previous example we explicitly called the ``view`` method, but to allow ``panel`` to update the plot when the datashade parameter is toggled we instead pass it the actual view method. Whenever the datashade parameter is toggled ``panel`` will call the method and update the plot with whatever is returned:
|
152 |
+
|
153 |
+
|
154 |
+
```python
|
155 |
+
explorer = AdvancedStockExplorer()
|
156 |
+
pn.Row(explorer.param, explorer.view)
|
157 |
+
```
|
158 |
+
|
159 |
+
As you can see using streams we have bound the widgets to the streams letting us easily control the stream values and making it trivial to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) user guide section.
|
hvplot_docs/Annotators.md
ADDED
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import holoviews as hv
|
3 |
+
import numpy as np
|
4 |
+
import panel as pn
|
5 |
+
|
6 |
+
hv.extension('bokeh')
|
7 |
+
```
|
8 |
+
|
9 |
+
HoloViews-generated plots generally convey information from Python _to_ a viewer of the plot, but there are also circumstances where information needs to be collected _from_ the viewer and made available for processing in Python:
|
10 |
+
|
11 |
+
* annotating data with contextual information to aid later interpretation
|
12 |
+
* labeling or tagging data for automated machine-learning or other processing pipelines
|
13 |
+
* indicating regions of interest, outliers, or other semantic information
|
14 |
+
* specifying inputs to a query, command, or simulation
|
15 |
+
* testing sensitivity of analyses to adding, changing, or deleting user-selected data points
|
16 |
+
|
17 |
+
In such cases, it is important to be able to augment, edit, and annotate datasets and to access those values from Python. To perform these actions, HoloViews provides an ``annotate`` helper using [Bokeh's drawing tools](https://docs.bokeh.org/en/latest/docs/reference/models/tools.html#bokeh.models.tools.PointDrawTool) to make it easy to edit HoloViews Elements and add additional information using an associated table. The `annotate` helper:
|
18 |
+
|
19 |
+
* Adds plot tools that allow editing and adding new elements to a plot
|
20 |
+
* Adds table(s) to allow editing the element in a tabular format
|
21 |
+
* Returns a layout of these two components
|
22 |
+
* Makes the edits, annotations, and selections available on a property of the annotate object so that they can be utilized in Python
|
23 |
+
|
24 |
+
## Basics
|
25 |
+
|
26 |
+
Let us start by annotating a small set of Points. To do this, we need two things:
|
27 |
+
|
28 |
+
1. A Points Element to annotate or edit
|
29 |
+
2. An annotator object to collect and store annotations
|
30 |
+
|
31 |
+
The annotator is a callable object with its own state that can be called to return a Layout consisting of the object to be annotated and an Overlay of editable table(s):
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
points = hv.Points([(0.0, 0.0), (1.0, 1.0), (200000.0, 2000000.0)]).opts(size=10, min_height=500)
|
36 |
+
|
37 |
+
annotator = hv.annotate.instance()
|
38 |
+
layout = annotator(hv.element.tiles.OSM() * points, annotations=['Label'], name="Point Annotations")
|
39 |
+
|
40 |
+
print(layout)
|
41 |
+
```
|
42 |
+
|
43 |
+
This layout of a DynamicMap (the user-editable Element data) and an Overlay (the user-editable table) lets a user input the required information:
|
44 |
+
|
45 |
+
|
46 |
+
```python
|
47 |
+
layout
|
48 |
+
```
|
49 |
+
|
50 |
+
Here we have pre-populated the Element with three points. Each of the points has three bits of information that can be edited using the table: the x location, y location, and a "Label", which was initialized to dummy values when we called `annotate` and asked that there be a `Label` column. Try clicking on one of the rows and editing the location or the label to anything you like. As long as Python is running and the new location is in the viewport, you should see the dot move when you edit the location, and any labels you entered should be visible in the table.
|
51 |
+
|
52 |
+
You can also edit the locations graphically using the [PointDraw tool](../reference/streams/bokeh/PointDraw.ipynb) in the toolbar:<img src="https://bokeh.pydata.org/en/latest/_images/PointDraw.png">
|
53 |
+
|
54 |
+
Once you select that tool, you should be able to click and drag any of the existing points and see the location update in the table. Whether you click on the table or the points, the same object should be selected in each, so that you can see how the graphical and tabular representations relate.
|
55 |
+
|
56 |
+
The PointDraw tool also allows us to add completely new points; once the tool is selected, just click on the plot above in locations not already containing a point and you can see a new point and a new table row appear ready for editing. You can also delete points by selecting them in the plot then pressing Backspace or Delete (depending on operating system).
|
57 |
+
|
58 |
+
All the above editing and interaction could have been done if we had simply called `hv.annotate(points, annotations=['Label'])` directly, but instead we first saved an "instance" of the annotator object so that we'd also be able to access the resulting data. So, once we are done collecting data from the user, let's use the saved `annotator` object handle to read out the values (by re-evaluating the following line):
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
annotator.annotated.dframe()
|
63 |
+
```
|
64 |
+
|
65 |
+
You should see that you can access the current set of user-provided or user-modified points and their user-provided labels from within Python, ready for any subsequent processing you need to do.
|
66 |
+
|
67 |
+
We can also access the currently `selected` points, in case we care only about a subset of the points (which will be empty if no points/rows are selected):
|
68 |
+
|
69 |
+
|
70 |
+
```python
|
71 |
+
annotator.selected.dframe()
|
72 |
+
```
|
73 |
+
|
74 |
+
## Configuring the Annotator
|
75 |
+
|
76 |
+
In addition to managing the list of `annotations`, the `annotate` helper exposes a few additional parameters. Remember like most Param-based objects you can get help about `annotate` parameters using the `hv.help` function:
|
77 |
+
|
78 |
+
|
79 |
+
```python
|
80 |
+
hv.help(hv.annotate)
|
81 |
+
```
|
82 |
+
|
83 |
+
### Annotation types
|
84 |
+
|
85 |
+
The default annotation type is a string, to allow you to put in arbitrary information that you later process in Python. If you want to enforce a more specific type, you can specify the annotation-value types explicitly using a dictionary mapping from column name to the type:
|
86 |
+
|
87 |
+
|
88 |
+
```python
|
89 |
+
hv.annotate(points, annotations={'int': int, 'float': float, 'str': str})
|
90 |
+
```
|
91 |
+
|
92 |
+
This example also shows how to collect multiple columns of information for the same data point.
|
93 |
+
|
94 |
+
## Types of Annotators
|
95 |
+
|
96 |
+
Currently only a limited set of Elements may be annotated, which include:
|
97 |
+
|
98 |
+
* ``Points``/``Scatter``
|
99 |
+
* ``Curve``
|
100 |
+
* ``Path``
|
101 |
+
* ``Polygons``
|
102 |
+
* ``Rectangles``
|
103 |
+
|
104 |
+
Adding support for new elements, in most cases, requires adding corresponding drawing/edit tools to Bokeh itself. But if you have data of other types, you may still be able to annotate it by casting it to one of the indicated types, collecting the data, then casting it back.
|
105 |
+
|
106 |
+
## Annotating Curves
|
107 |
+
|
108 |
+
To allow dragging the vertices of the Curve, the ``Curve`` annotator uses the PointDraw tool in the toolbar: <img src="https://bokeh.pydata.org/en/latest/_images/PointDraw.png">
|
109 |
+
The vertices will appear when the tool is selected or a vertex is selected in the table. Unlike most other annotators the Curve annotator only allows editing the vertices and does not allow adding new ones.
|
110 |
+
|
111 |
+
|
112 |
+
```python
|
113 |
+
curve = hv.Curve(np.random.randn(50).cumsum())
|
114 |
+
|
115 |
+
curve_annotator = hv.annotate.instance()
|
116 |
+
|
117 |
+
curve_annotator(curve.opts(width=800, height=400, responsive=False), annotations={'Label': str})
|
118 |
+
```
|
119 |
+
|
120 |
+
To access the data you can make use of the ``annotated`` property on the annotator:
|
121 |
+
|
122 |
+
|
123 |
+
```python
|
124 |
+
curve_annotator.annotated.dframe().head(5)
|
125 |
+
```
|
126 |
+
|
127 |
+
## Annotating Rectangles
|
128 |
+
|
129 |
+
The `Rectangles` annotator behaves very similarly to the Points annotator. It allows adding any number of annotation columns, using Bokeh's `BoxEdit` tool that allows both drawing and moving boxes. To see how to use the BoxEdit tool, refer to the HoloViews [BoxEdit stream reference](../reference/streams/bokeh/BoxEdit.ipynb), but briefly:
|
130 |
+
|
131 |
+
* Select the `BoxEdit` tool in the toolbar: <img src="https://bokeh.pydata.org/en/latest/_images/BoxEdit.png">
|
132 |
+
* Click and drag on an existing Rectangle to move it
|
133 |
+
* Double click to start drawing a new Rectangle at one corner, and double click to complete the rectangle at the opposite corner
|
134 |
+
* Select a rectangle and press the Backspace or Delete key (depending on OS) to delete it
|
135 |
+
* Edit the box coordinates in the table to resize it
|
136 |
+
|
137 |
+
|
138 |
+
```python
|
139 |
+
boxes = hv.Rectangles([(0, 0, 1, 1), (1.5, 1.5, 2.5, 2.5)])
|
140 |
+
|
141 |
+
box_annotator = hv.annotate.instance()
|
142 |
+
|
143 |
+
box_annotator(boxes.opts(width=800, height=400, responsive=False), annotations=['Label'])
|
144 |
+
```
|
145 |
+
|
146 |
+
To access the data we can make use of the ``annotated`` property on the annotator instance:
|
147 |
+
|
148 |
+
|
149 |
+
```python
|
150 |
+
box_annotator.annotated.dframe()
|
151 |
+
```
|
152 |
+
|
153 |
+
### Annotating paths/polygons
|
154 |
+
|
155 |
+
Unlike the Points and Boxes annotators, the Path and Polygon annotators allow annotating not just each individual entity but also the vertices that make up the paths and polygons. For more information about using the editing tools associated with this annotator refer to the HoloViews [PolyDraw](../reference/streams/PolyDraw.ipynb) and [PolyEdit](../reference/streams/PolyEdit.ipynb) stream reference guides, but briefly:
|
156 |
+
|
157 |
+
##### Drawing/Selecting Deleting Paths/Polygons
|
158 |
+
|
159 |
+
- Select the PolyDraw tool in the toolbar: <img src="https://bokeh.pydata.org/en/latest/_images/PolyDraw.png">
|
160 |
+
- Double click to start a new object, single click to add each vertex, and double-click to complete it.
|
161 |
+
- Delete paths/polygons by selecting and pressing Delete key (OSX) or Backspace key (PC)
|
162 |
+
|
163 |
+
##### Editing Paths/Polygons
|
164 |
+
|
165 |
+
- Select the PolyEdit tool in the toolbar: <img src="https://bokeh.pydata.org/en/latest/_images/PolyEdit.png">
|
166 |
+
- Double click a Path/Polygon to start editing
|
167 |
+
- Drag vertices to edit them, delete vertices by selecting them
|
168 |
+
|
169 |
+
To edit and annotate the vertices, use the draw tool or the first table to select a particular path/polygon and then navigate to the Vertices tab.
|
170 |
+
|
171 |
+
|
172 |
+
```python
|
173 |
+
path = hv.Path([hv.Box(0, 0, 1), hv.Ellipse(1, 1, 1)])
|
174 |
+
|
175 |
+
path_annotator = hv.annotate.instance()
|
176 |
+
|
177 |
+
path_annotator(path.opts(width=800, height=400, responsive=False), annotations=['Label'], vertex_annotations=['Value'])
|
178 |
+
```
|
179 |
+
|
180 |
+
To access the data we can make use of the iloc method on `Path` objects to access a particular path, and then access the `.data` or convert it to a dataframe:
|
181 |
+
|
182 |
+
|
183 |
+
```python
|
184 |
+
path_annotator.annotated.iloc[0].dframe()
|
185 |
+
```
|
186 |
+
|
187 |
+
## Composing Annotators
|
188 |
+
|
189 |
+
Often we will want to add some annotations on top of one or more other elements which provide context, e.g. when annotating an image with a set of `Points`. As long as only one annotation layer is required you can pass an overlay of multiple elements to the `annotate` operation and it will automatically associate the annotator with the layer that supports annotation. Note however that this will error if multiple elements that support annotation are provided. Below we will annotate a two-photon microscopy image with a set of Points, e.g. to mark the location of each cell:
|
190 |
+
|
191 |
+
|
192 |
+
```python
|
193 |
+
img = hv.Image(np.load('../assets/twophoton.npz')['Calcium'][..., 0])
|
194 |
+
cells = hv.Points([]).opts(width=500, height=500, responsive=False, padding=0)
|
195 |
+
|
196 |
+
hv.annotate(img * cells, annotations=['Label'], name="Cell Annotator")
|
197 |
+
```
|
198 |
+
|
199 |
+
### Multiple Annotators
|
200 |
+
|
201 |
+
If you want to work with multiple annotators in the same plot, you can recompose and rearrange the components returned by each `annotate` helper manually, but doing so can get confusing. To simplify working with multiple annotators at the same time, the `annotate` helper provides a special classmethod that allows composing multiple annotators and other elements, e.g. making a set of tiles into a combined layout consisting of all the components:
|
202 |
+
|
203 |
+
|
204 |
+
```python
|
205 |
+
point_annotate = hv.annotate.instance()
|
206 |
+
points = hv.Points([(500000, 500000), (1000000, 1000000)]).opts(size=10, color='red', line_color='black')
|
207 |
+
point_layout = point_annotate(points, annotations=['Label'])
|
208 |
+
|
209 |
+
poly_annotate = hv.annotate.instance()
|
210 |
+
poly_layout = poly_annotate(hv.Polygons([]), annotations=['Label'])
|
211 |
+
|
212 |
+
hv.annotate.compose(hv.element.tiles.OSM(), point_layout, poly_layout)
|
213 |
+
```
|
214 |
+
|
215 |
+
## Internals
|
216 |
+
|
217 |
+
The `annotate` function builds on [Param](https://param.holoviz.org) and [Panel](https://panel.holoviz.org), creating and wrapping Panel `Annotator` panes internally. These objects make it easy to include the annotator in Param-based workflows and trigger actions when parameters change and/or update the annotator in response to external events. The Annotator of a `annotate` instance can be accessed using the `annotator` attribute:
|
218 |
+
|
219 |
+
|
220 |
+
```python
|
221 |
+
print(point_annotate.annotator)
|
222 |
+
```
|
223 |
+
|
224 |
+
This object can be included directly in a Panel layout, be used to watch for parameter changes, or updated directly. To see the effect of updating directly, uncomment the line below, execute that cell, and then look at the previous plot of Africa, which should get updated with 10 randomly located blue dots.
|
225 |
+
|
226 |
+
|
227 |
+
```python
|
228 |
+
#point_annotate.annotator.object = hv.Points(np.random.randn(10, 2)*1000000).opts(color='blue')
|
229 |
+
```
|
hvplot_docs/Colormaps.md
ADDED
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
## Using colormaps
|
2 |
+
|
3 |
+
HoloViews supports a wide range of colormaps, each of which allow you to translate numerical data values into visible colors in a plot. Here we will review all the colormaps provided for HoloViews and discuss when and how to use them.
|
4 |
+
|
5 |
+
The [Styling_Plots](Styling_Plots.ipynb) user guide discusses how to specify any of the colormaps discussed here, using the `cmap` style option:
|
6 |
+
|
7 |
+
|
8 |
+
```python
|
9 |
+
import numpy as np
|
10 |
+
import holoviews as hv
|
11 |
+
hv.extension('matplotlib')
|
12 |
+
|
13 |
+
ls = np.linspace(0, 10, 400)
|
14 |
+
x,y = np.meshgrid(ls, ls)
|
15 |
+
img = hv.Image(np.sin(x)*np.cos(y)+0.1*np.random.rand(400,400),
|
16 |
+
bounds=(-20,-20,20,20)).opts(colorbar=True, xaxis=None, yaxis=None)
|
17 |
+
|
18 |
+
hv.Layout([img.relabel(c).opts(cmap=c) for c in ['gray','PiYG','flag','Set1']])
|
19 |
+
```
|
20 |
+
|
21 |
+
As you can see, the colormap you choose can dramatically change how your data appears. A well-chosen colormap can help guide the user to notice the features of the data you want to highlight, while a poorly chosen colormap can completely obscure the data and lead to erroneous conclusions. E.g. the low levels of noise present in this data are very difficult to see in A and B, but they completely dominate the plot in C and are visible only at specific (presumably arbitrary) value levels that correspond to color transitions in D. Thus it is important to choose colormaps very carefully!
|
22 |
+
|
23 |
+
Note that the `cmap` style option used above is applied by the underlying plotting library, not by HoloViews itself. In the above example, Matplotlib uses it as the colormap constructs the image, whereas a Bokeh version of the same plot would provide the colormap to the Bokeh JavaScript code running in the local web browser, which allows the user to control the colormap dynamically in some cases.
|
24 |
+
|
25 |
+
Colormaps can also be used with the [Datashader `shade()` operation](15-Large_Data.ipynb), in which the provided `cmap` is applied by Datashader to create an image *before* passing the image to the plotting library, which enables additional Datashader features but disables client-side features like colorbars and dynamic colormapping on display.
|
26 |
+
|
27 |
+
## Available colormaps
|
28 |
+
|
29 |
+
As outlined in [Styling_Plots](Styling_Plots.ipynb), you can easily make your own custom colormaps, but it's quite difficult to ensure that a custom map is designed well, so it's generally best to choose an existing, well-tested colormap. Here we will show the many different types of colormaps available, discussing each category and how to use that type of map. The ones shown here are those that are available by name, if the corresponding provider has been installed. E.g. those labelled "(bokeh)" will only be available if Bokeh is installed.
|
30 |
+
|
31 |
+
Most of these colormaps will work best on *either* a light or a dark background, but not both. To faithfully and intuitively represent monotonically increasing values, you will generally want a colormap where the lowest values are similar in tone to the page background, and higher values become more perceptually salient compared to the page background. To let you match the colormap to the page, the maps listed below have a variant suffixed with `_r` (not shown), which is the same map but with the reverse order.
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
from math import ceil
|
36 |
+
from holoviews.plotting.util import process_cmap
|
37 |
+
|
38 |
+
colormaps = hv.plotting.list_cmaps()
|
39 |
+
spacing = np.linspace(0, 1, 64)[np.newaxis]
|
40 |
+
opt_kwargs = dict(aspect=6, xaxis=None, yaxis=None, sublabel_format='')
|
41 |
+
|
42 |
+
def filter_cmaps(category):
|
43 |
+
return hv.plotting.util.list_cmaps(records=True,category=category,reverse=False)
|
44 |
+
|
45 |
+
def cmap_examples(category,cols=4):
|
46 |
+
cms = filter_cmaps(category)
|
47 |
+
n = len(cms)*1.0
|
48 |
+
c=ceil(n/cols) if n>cols else cols
|
49 |
+
bars = [hv.Image(spacing, ydensity=1, label="{0} ({1})".format(r.name,r.provider))\
|
50 |
+
.opts(cmap=process_cmap(r.name,provider=r.provider), **opt_kwargs)
|
51 |
+
for r in cms]
|
52 |
+
return hv.Layout(bars).opts(vspace=0.1, hspace=0.1, transpose=(n>cols)).cols(c)
|
53 |
+
```
|
54 |
+
|
55 |
+
### Perceptually uniform sequential colormaps
|
56 |
+
|
57 |
+
Useful for the typical case of having increasing numeric values that you want to distinguish without bias for any specific value. The colormaps in this category are designed to represent similar distances in value space (e.g. a numerical difference from 0.2 to 0.4, or one from 0.4 to 0.6, with similar differences in what we perceive visually).
|
58 |
+
|
59 |
+
For detailed discussions of this important issue, see
|
60 |
+
[Kovesi,](https://arxiv.org/abs/1509.03700)
|
61 |
+
[van der Walt & Smith,](https://bids.github.io/colormap) and
|
62 |
+
[Karpov,](http://inversed.ru/Blog_2.htm) who each argue for different color spaces and criteria for evaluating colormaps and thus develop different types of colormaps. Despite the disagreements over important details, *all* of the maps here will be significantly more uniform than an arbitrary map designed without perceptual criteria, such as those in "Other Sequential" below, and thus these colormaps represent good default choices in most cases.
|
63 |
+
|
64 |
+
When choosing one of these, be sure to consider whether you wish your page background to be distinguishable from a color in the colormap. If your data covers the entire plot, then using the background color is fine, but if you need the background color to show through (e.g. to show missing values), then you should avoid maps that include black (`fire`, `magma`, `inferno`, `gray`, `k*`) on a black page or white (`fire`,`gray`) on a white page.
|
65 |
+
|
66 |
+
|
67 |
+
```python
|
68 |
+
cmap_examples('Uniform Sequential')
|
69 |
+
```
|
70 |
+
|
71 |
+
### Diverging colormaps
|
72 |
+
|
73 |
+
Useful to highlight differences from a neutral central value, which is typically chosen to match the page background (e.g. white or yellow when using a white page, or black when using a black page).
|
74 |
+
|
75 |
+
Most of the diverging maps listed here were *not* developed to match a definition of perceptual uniformity, but those coming from `colorcet` were and should thus be preferred over the rest (which can be obtained by specifying `Uniform Diverging` here).
|
76 |
+
|
77 |
+
Some of these colormaps include both red and green, making them ambiguous for people with the most common types of colorblindness, and should thus be avoided where possible.
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
cmap_examples('Diverging')
|
82 |
+
```
|
83 |
+
|
84 |
+
### Rainbow colormaps
|
85 |
+
|
86 |
+
Rainbow-like colormaps convey value primarily through color rather than luminance. They result in eye-catching plots, but because rainbow colors form a continuous, cyclic spectrum, they can be ambiguous about which values are higher than the others. Most of them are also highly perceptually non-uniform, with pronounced banding that makes some values easily distinguished from their neighbors, and other wide ranges of values nearly indistinguishable (e.g. the greenish colors in the `gist_rainbow` and `jet` colormaps).
|
87 |
+
|
88 |
+
If you do want a rainbow colormap, please consider using one of the three perceptually uniform versions (category `Uniform Rainbow`) included here:
|
89 |
+
|
90 |
+
- `colorwheel` (colorcet): for cyclic values like angles and longitudes that wrap around to the same value at the end of the range (notice that the lowest and highest colors are both blue)
|
91 |
+
- `rainbow` (colorcet): for monotonically and uniformly increasing values (skips purple region to avoid ordering ambiguity)
|
92 |
+
- `isolum` (colorcet): for monotonically and uniformly increasing values, but only uses hue changes, with a constant lightness. Nearly all the other maps are dominated by changes in lightness, which is much more perceptually salient than strict changes in hue as in this map. Useful as a counterpart and illustration of the role of lightness.
|
93 |
+
|
94 |
+
Of course, rainbow colormaps have the disadvantage that they are inherently unsuitable for most colorblind viewers, because they require viewers to distinguish between red and green to determine value.
|
95 |
+
|
96 |
+
|
97 |
+
```python
|
98 |
+
cmap_examples('Rainbow')
|
99 |
+
```
|
100 |
+
|
101 |
+
### Categorical colormaps
|
102 |
+
|
103 |
+
Primarily useful as color cycles rather than colormaps, i.e. as a list of discrete color values, not a continuous range of colors. Will produce discrete banding when used on continuous values, like in a geographic contour plot, but if that effect is desired it's probably better to use `color_levels` with a sequential colormap to be able to control how many levels there are and give them a natural ordering.
|
104 |
+
|
105 |
+
Most of these color sets are constructed by hand, with a relatively small number of distinct colors. If you want a larger number of colors, the `glasbey_` categorical maps from Colorcet are generated using a systematic procedure based on sampling a perceptual space for widely separated colors, which allows large numbers of categories to be distinguished from each other.
|
106 |
+
|
107 |
+
The `glasbey_hv` colors have the useful property that they share the same first 12 colors as the default HoloViews color cycle, which means that if you want the same colors as usual but with more available when needed, you can switch the HoloViews default using `hv.Cycle.default_cycles['default_colors']=colorcet.glasbey_hv`.
|
108 |
+
|
109 |
+
|
110 |
+
```python
|
111 |
+
cmap_examples('Categorical')
|
112 |
+
```
|
113 |
+
|
114 |
+
### Mono Sequential colormaps
|
115 |
+
|
116 |
+
Monotonically increasing values that serve the same purpose as [Uniform Sequential](#Perceptually-uniform-sequential-colormaps) (above), but are not specifically constructed to be perceptually uniform. Useful when you want to fit into a particular visual theme or color scheme, or when you want to color entire plots differently from other entire plots (e.g. to provide a visual "traffic light" indicator for the entire plot, making some plots stand out relative to others). If you just need a single colormap, try to select a Uniform Sequential map instead of these.
|
117 |
+
|
118 |
+
|
119 |
+
```python
|
120 |
+
cmap_examples('Mono Sequential')
|
121 |
+
```
|
122 |
+
|
123 |
+
### Other Sequential colormaps
|
124 |
+
|
125 |
+
Other sequential colormaps are included, but are not meant for general use. Some of these have a very high degree of perceptual non-uniformity, making them highly misleading. E.g. the `hot` (matplotlib) colormap includes pronounced banding (with sharp perceptual discontinuities and long stretches of indistinguishable colors); consider using the perceptually uniform `fire` (colorcet) map instead. Others like `gray` largely duplicate maps in the other categories above, and so can cause confusion. The Uniform Sequential maps, or if necessary Mono Sequential, are generally good alternatives to these.
|
126 |
+
|
127 |
+
|
128 |
+
```python
|
129 |
+
cmap_examples('Other Sequential')
|
130 |
+
```
|
131 |
+
|
132 |
+
### Miscellaneous colormaps
|
133 |
+
|
134 |
+
There are a variety of other colormaps not fitting into the categories above, mostly of limited usefuless. Exceptions include the `flag` and `prism` (matplotlib) colormaps that could be useful for highlighting local changes in value (details), with no information about global changes in value (due to the repeating colors).
|
135 |
+
|
136 |
+
|
137 |
+
```python
|
138 |
+
cmap_examples('Miscellaneous')
|
139 |
+
```
|
140 |
+
|
141 |
+
See the [Styling_Plots](Styling_Plots.ipynb) user guide for how these colormaps can be used to control how your data is plotted.
|
142 |
+
|
143 |
+
## Querying and filtering the list of colormaps
|
144 |
+
|
145 |
+
For most purposes, you can just pick one of the colormaps above for a given plot. However, HoloViews is very often used to build applications and dashboards, many of which include a "colormap" selection widget. Because there are so many colormaps available, most of which are inappropriate for any specific plot, it's useful to be able to pull up a list of all the colormaps that are suitable for the specific type of plot used in the app.
|
146 |
+
|
147 |
+
To allow such filtering, HoloViews stores the following information about each named colormap, matched by substring:
|
148 |
+
|
149 |
+
- **name**: string name for the colormap
|
150 |
+
- **category**: Type of map by intended use or purpose ('[Uniform|Mono|Other ]Sequential', '[Uniform ]Diverging', '[Uniform ]Rainbow', '[Uniform ]Categorical', or 'Miscellaneous')
|
151 |
+
- **provider**: package offering the colormap ('matplotlib', 'bokeh', or 'colorcet')
|
152 |
+
- **source**: original source or creator of the colormaps ('cet', 'colorbrewer', 'd3', 'bids','misc')
|
153 |
+
- **bg**: base/background color expected for the map ('light','dark','medium','any')
|
154 |
+
- **reverse**: whether the colormap name includes `_r` indicating that it is a reverse of a base map (True, False)
|
155 |
+
|
156 |
+
The `hv.plotting.list_cmaps()` function used above can select a subset of the available colormaps by filtering based on the above values:
|
157 |
+
|
158 |
+
```
|
159 |
+
list_cmaps(provider, records, name, category, source, bg, reverse)
|
160 |
+
```
|
161 |
+
|
162 |
+
The examples below should make it clear how to use this function.
|
163 |
+
|
164 |
+
|
165 |
+
```python
|
166 |
+
from holoviews.plotting import list_cmaps
|
167 |
+
def format_list(l):
|
168 |
+
print(' '.join(sorted([k for k in l])))
|
169 |
+
```
|
170 |
+
|
171 |
+
All named colormaps provided by `colorcet`, reversed:
|
172 |
+
|
173 |
+
|
174 |
+
```python
|
175 |
+
format_list(list_cmaps(provider='colorcet',reverse=True))
|
176 |
+
```
|
177 |
+
|
178 |
+
All non-reversed colormaps provided by `matplotlib` and originating from `d3` that have `20` in their names:
|
179 |
+
|
180 |
+
|
181 |
+
```python
|
182 |
+
format_list(list_cmaps(name='20', source='d3', provider='matplotlib', reverse=False))
|
183 |
+
```
|
184 |
+
|
185 |
+
Colormaps provided by Bokeh that are suitable for dark-colored (e.g. black) backgrounds:
|
186 |
+
|
187 |
+
|
188 |
+
```python
|
189 |
+
format_list(list_cmaps(category='Other Sequential', bg='dark'))
|
190 |
+
```
|
191 |
+
|
192 |
+
Notice how some of these have `_r`, because those two natively start with light values and must be reversed to be suitable on a dark background. In this case the results for `bg='light'` are the complementary set of colormaps:
|
193 |
+
|
194 |
+
|
195 |
+
```python
|
196 |
+
format_list(list_cmaps(category='Other Sequential', bg='light'))
|
197 |
+
```
|
198 |
+
|
199 |
+
However, `Diverging` colormaps do not change their background color when reversed, and so requesting a light or dark background gives different maps altogether (depending on their central color):
|
200 |
+
|
201 |
+
|
202 |
+
```python
|
203 |
+
format_list(list_cmaps(category='Diverging', bg='dark'))
|
204 |
+
```
|
205 |
+
|
206 |
+
|
207 |
+
```python
|
208 |
+
format_list(list_cmaps(category='Diverging', bg='light'))
|
209 |
+
```
|
210 |
+
|
211 |
+
Matches are done by substring, so all sequential colormaps suitable for `dark` or `any` backgrounds can be obtained with:
|
212 |
+
|
213 |
+
|
214 |
+
```python
|
215 |
+
format_list(list_cmaps(category='Sequential', bg='a'))
|
216 |
+
```
|
217 |
+
|
218 |
+
In the examples above, `list_cmaps` is returning just the colormap name, but if you want to work with the filter information yourself to do more complex queries, you can ask that it return the full records as namedtuples instead:
|
219 |
+
|
220 |
+
|
221 |
+
```python
|
222 |
+
list_cmaps(category="Uniform Sequential", provider='bokeh', bg='light', records=True)
|
223 |
+
```
|
224 |
+
|
225 |
+
In addition to populating GUI widgets, another way to use this filtering is to systematically evaluate how your plot will look with a variety of different colormaps of the same type:
|
226 |
+
|
227 |
+
|
228 |
+
```python
|
229 |
+
hv.Layout([img.relabel(c).opts(cmap=c, colorbar=False, sublabel_format='')
|
230 |
+
for c in list_cmaps(category='Diverging', bg='light', reverse=False)][:14])\
|
231 |
+
.opts(vspace=0.1, hspace=0.1).cols(7)
|
232 |
+
```
|
233 |
+
|
234 |
+
You could also consider filtering on the actual values in the colormap, perhaps to ensure that the specific background color you are using is not present in the colormap. For this you can use the `hv.plotting.util.process_cmap` function to look up the actual colormap values by name and provider.
|
hvplot_docs/Continuous_Coordinates.md
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Continuous Coordinates
|
2 |
+
|
3 |
+
HoloViews is designed to work with scientific and engineering data, which is often in the form of discrete samples from an underlying continuous system. Imaging data is one clear example: measurements taken at a regular interval over a grid covering a two-dimensional area. Although the measurements are discrete, they approximate a continuous distribution, and HoloViews provides extensive support for working naturally with data of this type.
|
4 |
+
|
5 |
+
## 2D Continuous spaces
|
6 |
+
|
7 |
+
In this user guide we will show the support provided by HoloViews for working with two-dimensional regularly sampled grid data like images, and then in subsequent sections discuss how HoloViews supports one-dimensional, higher-dimensional, and irregularly sampled data with continuous coordinates.
|
8 |
+
|
9 |
+
|
10 |
+
```python
|
11 |
+
import numpy as np
|
12 |
+
import holoviews as hv
|
13 |
+
from holoviews import opts
|
14 |
+
|
15 |
+
hv.extension('bokeh')
|
16 |
+
|
17 |
+
np.set_printoptions(precision=2, linewidth=80)
|
18 |
+
opts.defaults(opts.Layout(shared_axes=False))
|
19 |
+
```
|
20 |
+
|
21 |
+
First, let's consider:
|
22 |
+
|
23 |
+
|||
|
24 |
+
|:--------------:|:----------------|
|
25 |
+
| **``f(x,y)``** | a simple function that accepts a location in a 2D plane specified in millimeters (mm) |
|
26 |
+
| **``region``** | a 1mm×1mm square region of this 2D plane, centered at the origin, and |
|
27 |
+
| **``coords``** | a function returning a square (s×s) grid of (x,y) coordinates regularly sampling the region in the given bounds, at the centers of each grid cell |
|
28 |
+
||||
|
29 |
+
|
30 |
+
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
def f(x,y):
|
35 |
+
return x+y/3.1
|
36 |
+
|
37 |
+
region=(-0.5,-0.5,0.5,0.5)
|
38 |
+
|
39 |
+
def coords(bounds,samples):
|
40 |
+
l,b,r,t=bounds
|
41 |
+
hc=0.5/samples
|
42 |
+
return np.meshgrid(np.linspace(l+hc,r-hc,samples),
|
43 |
+
np.linspace(b+hc,t-hc,samples))
|
44 |
+
```
|
45 |
+
|
46 |
+
Now let's build a Numpy array regularly sampling this function at a density of 5 samples per mm:
|
47 |
+
|
48 |
+
|
49 |
+
```python
|
50 |
+
f5=f(*coords(region,5))
|
51 |
+
f5
|
52 |
+
```
|
53 |
+
|
54 |
+
We can visualize this array (and thus the function ``f``) either using a ``Raster``, which uses the array's own integer-based coordinate system (which we will call "array" coordinates), or an ``Image``, which uses a continuous coordinate system, or as a ``HeatMap`` labelling each value explicitly:
|
55 |
+
|
56 |
+
|
57 |
+
```python
|
58 |
+
r5 = hv.Raster(f5, label="R5")
|
59 |
+
i5 = hv.Image( f5, label="I5", bounds=region)
|
60 |
+
h5 = hv.HeatMap([(x, y, round(f5[4-y,x],2)) for x in range(0,5) for y in range(0,5)], label="H5")
|
61 |
+
|
62 |
+
h5_labels = hv.Labels(h5).opts(padding=0)
|
63 |
+
|
64 |
+
r5 + i5 + h5*h5_labels
|
65 |
+
```
|
66 |
+
|
67 |
+
Both the ``Raster`` and ``Image`` ``Element`` types accept the same input data and show the same arrangement of colors, but a visualization of the ``Raster`` type reveals the underlying raw array indexing, while the ``Image`` type has been labelled with the coordinate system from which we know the data has been sampled. All ``Image`` operations work with this continuous coordinate system instead, while the corresponding operations on a ``Raster`` use raw array indexing.
|
68 |
+
|
69 |
+
For instance, all five of these indexing operations refer to the same element of the underlying Numpy array, i.e. the second item in the first row:
|
70 |
+
|
71 |
+
|
72 |
+
```python
|
73 |
+
"r5[0,1]=%0.2f r5.data[0,1]=%0.2f i5[-0.2,0.4]=%0.2f i5[-0.24,0.37]=%0.2f i5.data[0,1]=%0.2f" % \
|
74 |
+
(r5[1,0], r5.data[0,1], i5[-0.2,0.4], i5[-0.24,0.37], i5.data[0,1])
|
75 |
+
```
|
76 |
+
|
77 |
+
You can see that the ``Raster`` and the underlying ``.data`` elements both use Numpy's raw integer indexing, while the ``Image`` uses floating-point values that are then mapped onto the appropriate array element.
|
78 |
+
|
79 |
+
This diagram should help show the relationships between the ``Raster`` coordinate system in the plot (which ranges from 0 at the top edge to 5 at the bottom), the underlying raw Numpy integer array indexes (labelling each dot in the **Array coordinates** figure), and the underlying **Continuous coordinates**:
|
80 |
+
|
81 |
+
<TABLE style='border:5'>
|
82 |
+
<TR>
|
83 |
+
<TH><CENTER>Array coordinates</CENTER></TH>
|
84 |
+
<TH><CENTER>Continuous coordinates</CENTER></TH>
|
85 |
+
</TR>
|
86 |
+
<TR>
|
87 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/matrix_coords.png"></TD>
|
88 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/sheet_coords_-0.2_0.4.png"></TD>
|
89 |
+
</TR>
|
90 |
+
</TABLE>
|
91 |
+
|
92 |
+
Importantly, although we used a 5×5 array in this example, we could substitute a much larger array with the same continuous coordinate system if we wished, without having to change any of our continuous indexes -- they will still point to the correct location in the continuous space:
|
93 |
+
|
94 |
+
|
95 |
+
```python
|
96 |
+
f10=f(*coords(region,10))
|
97 |
+
f10
|
98 |
+
```
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
r10 = hv.Raster(f10, label="R10")
|
103 |
+
i10 = hv.Image(f10, label="I10", bounds=region)
|
104 |
+
r10+i10
|
105 |
+
```
|
106 |
+
|
107 |
+
The image now has higher resolution, but still visualizes the same underlying continuous function, now evaluated at 100 grid positions instead of 25:
|
108 |
+
|
109 |
+
<TABLE style='border:5'>
|
110 |
+
<TR>
|
111 |
+
<TH><CENTER>Array coordinates</CENTER></TH>
|
112 |
+
<TH><CENTER>Continuous coordinates</CENTER></TH>
|
113 |
+
</TR>
|
114 |
+
<TR>
|
115 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/matrix_coords_hidensity.png"></TD>
|
116 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/sheet_coords_-0.2_0.4.png"></TD>
|
117 |
+
</TR>
|
118 |
+
</TABLE>
|
119 |
+
|
120 |
+
Indexing the exact same coordinates as above now gets very different results:
|
121 |
+
|
122 |
+
|
123 |
+
```python
|
124 |
+
"r10[0,1]=%0.2f r10.data[0,1]=%0.2f i10[-0.2,0.4]=%0.2f i10[-0.24,0.37]=%0.2f i10.data[0,1]=%0.2f" % \
|
125 |
+
(r10[1,0], r10.data[0,1], i10[-0.2,0.4], i10[-0.24,0.37], i10.data[0,1])
|
126 |
+
```
|
127 |
+
|
128 |
+
The array-based indexes used by ``Raster`` and the Numpy array in ``.data`` still return the second item in the first row of the array, but this array element now corresponds to location (-0.35,0.4) in the continuous function, and so the value is different. These indexes thus do *not* refer to the same location in continuous space as they did for the other array density, because raw Numpy-based indexing is *not* independent of density or resolution.
|
129 |
+
|
130 |
+
Luckily, the two continuous coordinates still return very similar values to what they did before, since they always return the value of the array element corresponding to the closest location in continuous space. They now return elements just above and to the right, or just below and to the left, of the earlier location, because the array now has a higher resolution with elements centered at different locations.
|
131 |
+
|
132 |
+
Indexing in continuous coordinates always returns the value closest to the requested value, given the available resolution. Note that in the case of coordinates truly on the boundary between array elements (as for -0.2,0.4), the bounds of each array cell are taken as right exclusive and upper exclusive, and so (-0.2,0.4) returns array index (3,0).
|
133 |
+
|
134 |
+
## Slicing in 2D
|
135 |
+
|
136 |
+
In addition to indexing (looking up a value), slicing (selecting a region) works as expected in continuous space (see the [Indexing and Selecting](10-Indexing_and_Selecting.ipynb) user guide for more explanation). For instance, we can ask for a slice from (-0.275,-0.0125) to (0.025,0.2885) in continuous coordinates:
|
137 |
+
|
138 |
+
|
139 |
+
```python
|
140 |
+
sl10=i10[-0.275:0.025,-0.0125:0.2885]
|
141 |
+
sl10.data
|
142 |
+
```
|
143 |
+
|
144 |
+
|
145 |
+
```python
|
146 |
+
sl10
|
147 |
+
```
|
148 |
+
|
149 |
+
This slice has selected those array elements whose centers are contained within the specified continuous space. To do this, the continuous coordinates are first converted by HoloViews into the floating-point range (5.125,2.250) (2.125,5.250) of array coordinates, and all those elements whose centers are in that range are selected:
|
150 |
+
|
151 |
+
<TABLE style='border:5'>
|
152 |
+
<TR>
|
153 |
+
<TH><CENTER>Array coordinates</CENTER></TH>
|
154 |
+
<TH><CENTER>Continuous coordinates</CENTER></TH>
|
155 |
+
</TR>
|
156 |
+
<TR>
|
157 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/connection_field.png"></TD>
|
158 |
+
<TD><IMG src="http://ioam.github.io/topographica/_images/sheet_coords_-0.275_-0.0125_0.025_0.2885.png"></TD>
|
159 |
+
</TR>
|
160 |
+
</TABLE>
|
161 |
+
|
162 |
+
Slicing also works for ``Raster`` elements, but it results in an object that always reflects the contents of the underlying Numpy array (i.e., always with the upper left corner labelled 0,0):
|
163 |
+
|
164 |
+
|
165 |
+
```python
|
166 |
+
r5[0:3,1:3] + r5[0:3,1:2]
|
167 |
+
```
|
168 |
+
|
169 |
+
Hopefully these examples make it clear that if you are using data that is sampled from some underlying continuous system, you should use the continuous coordinates offered by HoloViews objects like ``Image`` so that your programs can be independent of the resolution or sampling density of that data, and so that your axes and indexes can be expressed naturally, using the actual units of the underlying continuous space. The data will still be stored in the same Numpy array, but now you can treat it consistently like the approximation to continuous values that it is.
|
170 |
+
|
171 |
+
## 1D and nD Continuous coordinates
|
172 |
+
|
173 |
+
All of the above examples use the common case for visualizations of a two-dimensional regularly gridded continuous space, which is implemented in ``holoviews.core.sheetcoords.SheetCoordinateSystem``.
|
174 |
+
|
175 |
+
Similar continuous coordinates and slicing are also supported for ``Chart`` elements, such as ``Curve``s, but using a single index and allowing arbitrary irregular spacing, implemented in ``holoviews.elements.chart.Chart``.
|
176 |
+
|
177 |
+
They also work the same for the n-dimensional coordinates and slicing supported by the [container](Containers) types ``HoloMap``, ``NdLayout``, and ``NdOverlay``, implemented in ``holoviews.core.dimension.Dimensioned`` and again allowing arbitrary irregular spacing.
|
178 |
+
|
179 |
+
``QuadMesh`` elements are similar but allow more general types of mapping between the underlying array and the continuous space, with arbitrary spacing along each of the axes or even over the entire array. See the ``QuadMesh`` element for more details.
|
180 |
+
|
181 |
+
Together, these powerful continuous-coordinate indexing and slicing operations allow you to work naturally and simply in the full *n*-dimensional space that characterizes your data and parameter values.
|
182 |
+
|
183 |
+
## Sampling
|
184 |
+
|
185 |
+
The above examples focus on indexing and slicing, but as described in the [Indexing and Selecting](10-Indexing_and_Selecting.ipynb) user guide there is another related operation supported for continuous spaces, called sampling. Sampling is similar to indexing and slicing, in that all of them can reduce the dimensionality of your data, but sampling is implemented in a general way that applies for any of the 1D, 2D, or nD datatypes. For instance, if we take our 10×10 array from above, we can ask for the value at a given location, which will come back as a ``Table``, i.e. a dictionary with one (key,value) pair:
|
186 |
+
|
187 |
+
|
188 |
+
```python
|
189 |
+
e10=i10.sample(x=-0.275, y=0.2885)
|
190 |
+
e10.opts(height=75)
|
191 |
+
```
|
192 |
+
|
193 |
+
Similarly, if we ask for the value of a given *y* location in continuous space, we will get a ``Curve`` with the array row closest to that *y* value in the ``Image`` 2D array returned as arrays of `x` values and the corresponding *z* value from the image:
|
194 |
+
|
195 |
+
|
196 |
+
```python
|
197 |
+
r10=i10.sample(y=0.2885)
|
198 |
+
r10
|
199 |
+
```
|
200 |
+
|
201 |
+
The same sampling syntax can be used on HoloViews objects with any number of continuous-coordinate dimensions, in each case returning a HoloViews object of the correct dimensionality. This support for working in continuous spaces makes it much more natural to work with HoloViews objects than directly with the underlying raw Numpy arrays, but the raw data always remains available when needed.
|
hvplot_docs/Customization.md
ADDED
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import hvplot
|
3 |
+
import hvplot.pandas # noqa
|
4 |
+
```
|
5 |
+
|
6 |
+
The hvPlot API is closely modeled on the pandas plot API but also diverges in certain cases, either to improve consistency or to provide additional functionality. This section will outline the valid options to control the axes of a plot, to control datashading and to modify the style of a plot. To look these options up interactively you may either use the tab-completion machinery in IPython or the Jupyter notebook, e.g.:
|
7 |
+
|
8 |
+
```python
|
9 |
+
df.hvplot.line(<TAB>
|
10 |
+
```
|
11 |
+
|
12 |
+
OR use the help method:
|
13 |
+
|
14 |
+
```
|
15 |
+
hvplot.help('line')
|
16 |
+
```
|
17 |
+
|
18 |
+
## Generic options
|
19 |
+
|
20 |
+
The generic set of options which may apply to all plot types include:
|
21 |
+
|
22 |
+
clim: tuple
|
23 |
+
Lower and upper bound of the color scale
|
24 |
+
cnorm (default='linear'): str
|
25 |
+
Color scaling which must be one of 'linear', 'log' or 'eq_hist'
|
26 |
+
colorbar (default=False): boolean
|
27 |
+
Enables a colorbar
|
28 |
+
fontscale: number
|
29 |
+
Scales the size of all fonts by the same amount, e.g. fontscale=1.5
|
30 |
+
enlarges all fonts (title, xticks, labels etc.) by 50%
|
31 |
+
fontsize: number or dict
|
32 |
+
Set title, label and legend text to the same fontsize. Finer control
|
33 |
+
by using a dict: {'title': '15pt', 'ylabel': '5px', 'ticks': 20}
|
34 |
+
flip_xaxis/flip_yaxis: boolean
|
35 |
+
Whether to flip the axis left to right or up and down respectively
|
36 |
+
grid (default=False): boolean
|
37 |
+
Whether to show a grid
|
38 |
+
hover : boolean
|
39 |
+
Whether to show hover tooltips, default is True unless datashade is
|
40 |
+
True in which case hover is False by default
|
41 |
+
hover_cols (default=[]): list or str
|
42 |
+
Additional columns to add to the hover tool or 'all' which will
|
43 |
+
includes all columns (including indexes if use_index is True).
|
44 |
+
invert (default=False): boolean
|
45 |
+
Swaps x- and y-axis
|
46 |
+
frame_width/frame_height: int
|
47 |
+
The width and height of the data area of the plot
|
48 |
+
legend (default=True): boolean or str
|
49 |
+
Whether to show a legend, or a legend position
|
50 |
+
('top', 'bottom', 'left', 'right')
|
51 |
+
logx/logy (default=False): boolean
|
52 |
+
Enables logarithmic x- and y-axis respectively
|
53 |
+
logz (default=False): boolean
|
54 |
+
Enables logarithmic colormapping
|
55 |
+
loglog (default=False): boolean
|
56 |
+
Enables logarithmic x- and y-axis
|
57 |
+
max_width/max_height: int
|
58 |
+
The maximum width and height of the plot for responsive modes
|
59 |
+
min_width/min_height: int
|
60 |
+
The minimum width and height of the plot for responsive modes
|
61 |
+
padding: number or tuple
|
62 |
+
Fraction by which to increase auto-ranged extents to make
|
63 |
+
datapoints more visible around borders. Supports tuples to
|
64 |
+
specify different amount of padding for x- and y-axis and
|
65 |
+
tuples of tuples to specify different amounts of padding for
|
66 |
+
upper and lower bounds.
|
67 |
+
responsive: boolean
|
68 |
+
Whether the plot should responsively resize depending on the
|
69 |
+
size of the browser. Responsive mode will only work if at
|
70 |
+
least one dimension of the plot is left undefined, e.g. when
|
71 |
+
width and height or width and aspect are set the plot is set
|
72 |
+
to a fixed size, ignoring any responsive option.
|
73 |
+
rot: number
|
74 |
+
Rotates the axis ticks along the x-axis by the specified
|
75 |
+
number of degrees.
|
76 |
+
shared_axes (default=True): boolean
|
77 |
+
Whether to link axes between plots
|
78 |
+
transforms (default={}): dict
|
79 |
+
A dictionary of HoloViews dim transforms to apply before plotting
|
80 |
+
title (default=''): str
|
81 |
+
Title for the plot
|
82 |
+
tools (default=[]): list
|
83 |
+
List of tool instances or strings (e.g. ['tap', box_select'])
|
84 |
+
xaxis/yaxis: str or None
|
85 |
+
Whether to show the x/y-axis and whether to place it at the
|
86 |
+
'top'/'bottom' and 'left'/'right' respectively.
|
87 |
+
xformatter/yformatter (default=None): str or TickFormatter
|
88 |
+
Formatter for the x-axis and y-axis (accepts printf formatter,
|
89 |
+
e.g. '%.3f', and bokeh TickFormatter)
|
90 |
+
xlabel/ylabel/clabel (default=None): str
|
91 |
+
Axis labels for the x-axis, y-axis, and colorbar
|
92 |
+
xlim/ylim (default=None): tuple or list
|
93 |
+
Plot limits of the x- and y-axis
|
94 |
+
xticks/yticks (default=None): int or list
|
95 |
+
Ticks along x- and y-axis specified as an integer, list of
|
96 |
+
ticks positions, or list of tuples of the tick positions and labels
|
97 |
+
width (default=700)/height (default=300): int
|
98 |
+
The width and height of the plot in pixels
|
99 |
+
attr_labels (default=None): bool
|
100 |
+
Whether to use an xarray object's attributes as labels, defaults to
|
101 |
+
None to allow best effort without throwing a warning. Set to True
|
102 |
+
to see warning if the attrs can't be found, set to False to disable
|
103 |
+
the behavior.
|
104 |
+
sort_date (default=True): bool
|
105 |
+
Whether to sort the x-axis by date before plotting
|
106 |
+
symmetric (default=None): bool
|
107 |
+
Whether the data are symmetric around zero. If left unset, the data
|
108 |
+
will be checked for symmetry as long as the size is less than
|
109 |
+
``check_symmetric_max``.
|
110 |
+
check_symmetric_max (default=1000000):
|
111 |
+
Size above which to stop checking for symmetry by default on the data.
|
112 |
+
|
113 |
+
## Datashading options
|
114 |
+
|
115 |
+
In addition to regular plot options hvplot also exposes options for dealing with large data:
|
116 |
+
|
117 |
+
aggregator (default=None):
|
118 |
+
Aggregator to use when applying rasterize or datashade operation
|
119 |
+
(valid options include 'mean', 'count', 'min', 'max' and more, and
|
120 |
+
datashader reduction objects)
|
121 |
+
dynamic (default=True):
|
122 |
+
Whether to return a dynamic plot which sends updates on widget and
|
123 |
+
zoom/pan events or whether all the data should be embedded
|
124 |
+
(warning: for large groupby operations embedded data can become
|
125 |
+
very large if dynamic=False)
|
126 |
+
datashade (default=False):
|
127 |
+
Whether to apply rasterization and shading using datashader
|
128 |
+
library returning an RGB object
|
129 |
+
dynspread (default=False):
|
130 |
+
Allows plots generated with datashade=True to increase the point
|
131 |
+
size to make sparse regions more visible
|
132 |
+
rasterize (default=False):
|
133 |
+
Whether to apply rasterization using the datashader library
|
134 |
+
returning an aggregated Image
|
135 |
+
x_sampling/y_sampling (default=None):
|
136 |
+
Specifies the smallest allowed sampling interval along the x/y axis.
|
137 |
+
|
138 |
+
## Geographic options
|
139 |
+
|
140 |
+
When dealing with geographic data, there are a number of options that become available. See the [geographic section](Geographic_Data.ipynb) for more information on working with geographic data:
|
141 |
+
|
142 |
+
coastline (default=False):
|
143 |
+
Whether to display a coastline on top of the plot, setting
|
144 |
+
coastline='10m'/'50m'/'110m' specifies a specific scale.
|
145 |
+
crs (default=None):
|
146 |
+
Coordinate reference system of the data specified as Cartopy
|
147 |
+
CRS object, proj.4 string or EPSG code.
|
148 |
+
features (default=None): dict or list
|
149 |
+
A list of features or a dictionary of features and the scale
|
150 |
+
at which to render it. Available features include 'borders',
|
151 |
+
'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.
|
152 |
+
Available scales include '10m'/'50m'/'110m'.
|
153 |
+
geo (default=False):
|
154 |
+
Whether the plot should be treated as geographic (and assume
|
155 |
+
PlateCarree, i.e. lat/lon coordinates).
|
156 |
+
global_extent (default=False):
|
157 |
+
Whether to expand the plot extent to span the whole globe.
|
158 |
+
project (default=False):
|
159 |
+
Whether to project the data before plotting (adds initial
|
160 |
+
overhead but avoids projecting data when plot is dynamically
|
161 |
+
updated).
|
162 |
+
tiles (default=False):
|
163 |
+
Whether to overlay the plot on a tile source. Tiles sources
|
164 |
+
can be selected by name or a tiles object or class can be passed,
|
165 |
+
the default is 'Wikipedia'.
|
166 |
+
|
167 |
+
## Kind options
|
168 |
+
|
169 |
+
Each type of plot may have a number of options to visual attributes specific to that plot type. In general these are provided in the docstring of the plot type, which can be viewed using ``help`` method:
|
170 |
+
|
171 |
+
|
172 |
+
```python
|
173 |
+
hvplot.help('scatter', generic=False, style=False)
|
174 |
+
```
|
175 |
+
|
176 |
+
## Styling options
|
177 |
+
|
178 |
+
Beyond the options specific to each plot type (or ``kind``) it is also possible to customize each component in detail, exposing all the options bokeh exposes. These usually include options to color the line and fill color, alpha and style. To see the full listing we can once again use the ``help`` method:
|
179 |
+
|
180 |
+
|
181 |
+
```python
|
182 |
+
hvplot.help('line', docstring=False, generic=False)
|
183 |
+
```
|
184 |
+
|
185 |
+
In general, the objects returned by hvPlot are regular HoloViews objects, which can be overlaid, laid out, composed and customized like all other HoloViews objects. The [HoloViews](https://holoviews.org) website explains all the functionality available, but what's on this hvPlot website should be enough to get you up and running for typical usage.
|
hvplot_docs/Customizing_Plots.md
ADDED
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Customizing Plots
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import holoviews as hv
|
7 |
+
from holoviews import dim, opts
|
8 |
+
|
9 |
+
hv.extension('bokeh', 'matplotlib')
|
10 |
+
```
|
11 |
+
|
12 |
+
The HoloViews options system allows controlling the various attributes of a plot. While different plotting extensions like bokeh, matplotlib and plotly offer different features and the style options may differ, there are a wide array of options and concepts that are shared across the different extensions. Specifically this guide provides an overview on controlling the various aspects of a plot including titles, axes, legends and colorbars.
|
13 |
+
|
14 |
+
Plots have an overall hierarchy and here we will break down the different components:
|
15 |
+
|
16 |
+
* [**Plot**](#customizing-the-plot): Refers to the overall plot which can consist of one or more axes
|
17 |
+
- [Titles](#title): Using title formatting and providing custom titles
|
18 |
+
- [Background](#background): Setting the plot background color
|
19 |
+
- [Font sizes](#font-sizes): Controlling the font sizes on a plot
|
20 |
+
- [Legends](#legend-customization): Controlling the position and styling of the legend
|
21 |
+
- [Plot hooks](#plot-hooks): Using custom hooks to modify plots
|
22 |
+
* [**Axes**](#customizing-axes): A set of axes provides scales describing the mapping between data and the space on screen
|
23 |
+
- [Types of axes](#types-of-axes):
|
24 |
+
- [Linear axes](#linear-axes)
|
25 |
+
- [Logarithmic axes](#log-axes)
|
26 |
+
- [Datetime axes](#datetime-axes)
|
27 |
+
- [Categorical axes](#categorical-axes)
|
28 |
+
- [Axis position](#axis-position): Positioning and hiding axes
|
29 |
+
- [Inverting axes](#inverting-axes): Flipping the x-/y-axes and inverting an axis
|
30 |
+
- [Axis labels](#axis-labels): Setting axis labels using dimensions and options
|
31 |
+
- [Axis ranges](#axis-ranges): Controlling axes ranges using dimensions, padding and options
|
32 |
+
- [Axis ticks](#axis-ticks): Controlling axis tick locations, labels and formatting
|
33 |
+
- [Twin axes](#twin-axes): Enabling twin axes
|
34 |
+
|
35 |
+
## Customizing the plot
|
36 |
+
|
37 |
+
### Title
|
38 |
+
|
39 |
+
A plot's title is usually constructed using a formatter which takes the group and label along with the plots dimensions into consideration. The default formatter is:
|
40 |
+
|
41 |
+
'{label} {group} {dimensions}'
|
42 |
+
|
43 |
+
where the ``{label}`` and ``{group}`` are inherited from the objects group and label parameters and ``dimensions`` represent the key dimensions in a HoloMap/DynamicMap:
|
44 |
+
|
45 |
+
|
46 |
+
```python
|
47 |
+
hv.HoloMap({i: hv.Curve([1, 2, 3-i], group='Group', label='Label') for i in range(3)}, 'Value')
|
48 |
+
```
|
49 |
+
|
50 |
+
The title formatter may however be overridden with an explicit title, which may include any combination of the three formatter variables:
|
51 |
+
|
52 |
+
|
53 |
+
```python
|
54 |
+
hv.Curve([1, 2, 3]).opts(title="Custom Title")
|
55 |
+
```
|
56 |
+
|
57 |
+
### Background
|
58 |
+
|
59 |
+
Another option which can be controlled at the level of a plot is the background color which may be set using the `bgcolor` option:
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
hv.Curve([1, 2, 3]).opts(bgcolor='lightgray')
|
64 |
+
```
|
65 |
+
|
66 |
+
### Font sizes
|
67 |
+
|
68 |
+
Controlling the font sizes of a plot is very common so HoloViews provides a convenient option to set the ``fontsize``. The ``fontsize`` accepts a dictionary which allows supplying fontsizes for different components of the plot from the title, to the axis labels, ticks and legends. The full list of plot components that can be customized separately include:
|
69 |
+
|
70 |
+
['xlabel', 'ylabel', 'zlabel', 'labels', 'xticks', 'yticks', 'zticks', 'ticks', 'minor_xticks', 'minor_yticks', 'minor_ticks', 'title', 'legend', 'legend_title']
|
71 |
+
|
72 |
+
Let's take a simple example customizing the title, the axis labels and the x/y-ticks separately:
|
73 |
+
|
74 |
+
|
75 |
+
```python
|
76 |
+
hv.Curve([1, 2, 3], label='Title').opts(fontsize={'title': 16, 'labels': 14, 'xticks': 6, 'yticks': 12})
|
77 |
+
```
|
78 |
+
|
79 |
+
### Font scaling
|
80 |
+
|
81 |
+
Instead of control each property individually it is often useful to scale all fonts by a constant factor, e.g. to produce a more legible plot for presentations and posters. The `fontscale` option will affect the title, axis labels, tick labels, and legend:
|
82 |
+
|
83 |
+
|
84 |
+
```python
|
85 |
+
(hv.Curve([1, 2, 3], label='A') * hv.Curve([3, 2, 1], label='B')).opts(fontscale=2, width=500, height=400, title='Title')
|
86 |
+
```
|
87 |
+
|
88 |
+
### Legend customization
|
89 |
+
|
90 |
+
When overlaying plots with different labels, a legend automatically appears to differentiate elements in the overlay. This legend can be customized in several ways:
|
91 |
+
|
92 |
+
- by **position**
|
93 |
+
- by adjusting the legend location within the figure using the `legend_position` option (e.g. `legend_position='bottom_right'`)
|
94 |
+
- by adjusting the legend location *outside* of the figure using the `legend_position` and `legend_offset` parameters (which then positions the legend in *screen* space) (e.g. `legend_position='right', legend_offset=(0, 200)`). **Note**: the `legend_position` option applies to `bokeh` and `matplotlib` backends but the `legend_offset` only applies to `bokeh`.
|
95 |
+
- by **style**
|
96 |
+
- by muting elements with `legend_muted=True` (applies only to the `bokeh` backend)
|
97 |
+
- by putting the legend elements in a column layout with `legend_cols=True` or (`legend_cols=int` in matplotlib)
|
98 |
+
|
99 |
+
These customizations are demonstrated by the examples that follow.
|
100 |
+
|
101 |
+
Moving the legend to the bottom right:
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
overlay = (hv.Curve([1, 2, 3], label='A') * hv.Curve([3, 2, 1], label='B')).opts(width=500, height=400)
|
106 |
+
overlay.opts(legend_position='bottom_right')
|
107 |
+
```
|
108 |
+
|
109 |
+
Moving the legend outside, to the right of the plot:
|
110 |
+
|
111 |
+
|
112 |
+
```python
|
113 |
+
overlay.opts(legend_position='right')
|
114 |
+
```
|
115 |
+
|
116 |
+
Moving the legend outside, to the right of the plot but offset it 200 pixels higher:
|
117 |
+
|
118 |
+
|
119 |
+
```python
|
120 |
+
overlay.opts(width=500, height=400, legend_position='right', legend_offset=(0, 200))
|
121 |
+
```
|
122 |
+
|
123 |
+
Muting the legend and laying the labels out as columns.
|
124 |
+
|
125 |
+
|
126 |
+
```python
|
127 |
+
overlay.opts(legend_muted=True, legend_cols=2)
|
128 |
+
```
|
129 |
+
|
130 |
+
### Plot hooks
|
131 |
+
|
132 |
+
HoloViews does not expose every single option a plotting extension like matplotlib or bokeh provides, therefore it is sometimes necessary to dig deeper to achieve precisely the customizations one might need. One convenient way of doing so is to use plot hooks to modify the plot object directly. The hooks are applied after HoloViews is done with the plot, allowing for detailed manipulations of the backend specific plot object.
|
133 |
+
|
134 |
+
The signature of a hook has two arguments, the HoloViews `plot` object that is rendering the plot and the `element` being rendered. From there the hook can modify the objects in the plot's handles, which provides convenient access to various components of a plot or simply access the ``plot.state`` which corresponds to the plot as a whole, e.g. in this case we define colors for the x- and y-labels of the plot.
|
135 |
+
|
136 |
+
|
137 |
+
```python
|
138 |
+
def hook(plot, element):
|
139 |
+
print('plot.state: ', plot.state)
|
140 |
+
print('plot.handles: ', sorted(plot.handles.keys()))
|
141 |
+
plot.handles['xaxis'].axis_label_text_color = 'red'
|
142 |
+
plot.handles['yaxis'].axis_label_text_color = 'blue'
|
143 |
+
|
144 |
+
hv.Curve([1, 2, 3]).opts(hooks=[hook])
|
145 |
+
```
|
146 |
+
|
147 |
+
## Customizing axes
|
148 |
+
|
149 |
+
Controlling the axis scales is one of the most common changes to make to a plot, so we will provide a quick overview of the four main types of axes and then go into some more detail on how to control the axis labels, ranges, ticks and orientation.
|
150 |
+
|
151 |
+
### Types of axes
|
152 |
+
|
153 |
+
There are four main types of axes supported across plotting backends, standard linear axes, log axes, datetime axes and categorical axes. In most cases HoloViews automatically detects the appropriate axis type to use based on the type of the data, e.g. numeric values use linear/log axes, date(time) values use datetime axes and string or other object types use categorical axes.
|
154 |
+
|
155 |
+
#### Linear axes
|
156 |
+
|
157 |
+
A linear axes is simply the default, as long as the data is numeric HoloViews will automatically use a linear axis on the plot.
|
158 |
+
|
159 |
+
#### Log axes
|
160 |
+
|
161 |
+
When the data is exponential it is often useful to use log axes, which can be enabled using independent ``logx`` and ``logy`` options. This way both semi-log and log-log plots can be achieved:
|
162 |
+
|
163 |
+
|
164 |
+
```python
|
165 |
+
semilogy = hv.Curve(np.logspace(0, 5), label='Semi-log y axes')
|
166 |
+
loglog = hv.Curve((np.logspace(0, 5), np.logspace(0, 5)), label='Log-log axes')
|
167 |
+
|
168 |
+
semilogy.opts(logy=True) + loglog.opts(logx=True, logy=True, shared_axes=False)
|
169 |
+
```
|
170 |
+
|
171 |
+
#### Datetime axes
|
172 |
+
|
173 |
+
All current plotting extensions allow plotting datetime data, if you ensure the dates array is of a valid datetime dtype.
|
174 |
+
|
175 |
+
|
176 |
+
```python
|
177 |
+
from bokeh.sampledata.stocks import GOOG, AAPL
|
178 |
+
|
179 |
+
goog_dates = np.array(GOOG['date'], dtype=np.datetime64)
|
180 |
+
aapl_dates = np.array(AAPL['date'], dtype=np.datetime64)
|
181 |
+
|
182 |
+
goog = hv.Curve((goog_dates, GOOG['adj_close']), 'Date', 'Stock Index', label='Google')
|
183 |
+
aapl = hv.Curve((aapl_dates, AAPL['adj_close']), 'Date', 'Stock Index', label='Apple')
|
184 |
+
|
185 |
+
(goog * aapl).opts(width=600, legend_position='top_left')
|
186 |
+
```
|
187 |
+
|
188 |
+
#### Categorical axes
|
189 |
+
|
190 |
+
While the handling of categorical data handles significantly between plotting extensions the same basic concepts apply. If the data is a string type or other object type it is formatted as a string and each unique category is assigned a tick along the axis. When overlaying elements the categories are combined and overlaid appropriately.
|
191 |
+
|
192 |
+
Whether an axis is categorical also depends on the Element type, e.g. a ``HeatMap`` always has two categorical axes while a ``Bars`` element always has a categorical x-axis. As a simple example let us create a set of points with categories along the x- and y-axes and render them on top of a `HeatMap` of th same data:
|
193 |
+
|
194 |
+
|
195 |
+
```python
|
196 |
+
points = hv.Points([(chr(i+65), chr(j+65), i*j) for i in range(10) for j in range(10)], vdims='z')
|
197 |
+
|
198 |
+
heatmap = hv.HeatMap(points)
|
199 |
+
|
200 |
+
(heatmap * points).opts(
|
201 |
+
opts.HeatMap(toolbar='above', tools=['hover']),
|
202 |
+
opts.Points(tools=['hover'], size=dim('z')*0.3))
|
203 |
+
```
|
204 |
+
|
205 |
+
As a more complex example which does not implicitly assume categorical axes due to the element type we will create a set of random samples indexed by categories from 'A' to 'E' using the ``Scatter`` Element and overlay them. Secondly we compute the mean and standard deviation for each category displayed using a set of ``ErrorBars`` and finally we overlay these two elements with a ``Curve`` representing the mean value . All these Elements respect the categorical index, providing us a view of the distribution of values in each category:
|
206 |
+
|
207 |
+
|
208 |
+
```python
|
209 |
+
overlay = hv.NdOverlay({group: hv.Scatter(([group]*100, np.random.randn(100)*(5-i)-i))
|
210 |
+
for i, group in enumerate(['A', 'B', 'C', 'D', 'E'])})
|
211 |
+
|
212 |
+
errorbars = hv.ErrorBars([(k, el.reduce(function=np.mean), el.reduce(function=np.std))
|
213 |
+
for k, el in overlay.items()])
|
214 |
+
|
215 |
+
curve = hv.Curve(errorbars)
|
216 |
+
|
217 |
+
(errorbars * overlay * curve).opts(
|
218 |
+
opts.ErrorBars(line_width=5), opts.Scatter(jitter=0.2, alpha=0.5, size=6, height=400, width=600))
|
219 |
+
```
|
220 |
+
|
221 |
+
Categorical axes are special in that they support multi-level nesting in some cases. Currently this is only supported for certain element types (BoxWhisker, Violin and Bars) but eventually all chart-like elements will interpret multiple key dimensions as a multi-level categorical hierarchy. To demonstrate this behavior consider the `BoxWhisker` plot below which support two-level nested categories:
|
222 |
+
|
223 |
+
|
224 |
+
```python
|
225 |
+
groups = [chr(65+g) for g in np.random.randint(0, 3, 200)]
|
226 |
+
boxes = hv.BoxWhisker((groups, np.random.randint(0, 5, 200), np.random.randn(200)),
|
227 |
+
['Group', 'Category'], 'Value').sort()
|
228 |
+
|
229 |
+
boxes.opts(width=600)
|
230 |
+
```
|
231 |
+
|
232 |
+
### Axis positions
|
233 |
+
|
234 |
+
Axes may be hidden or moved to a different location using the ``xaxis`` and ``yaxis`` options, which accept `None`, `'right'`/`'bottom'`, `'left'`/`'top'` and `'bare'` as values.
|
235 |
+
|
236 |
+
|
237 |
+
```python
|
238 |
+
np.random.seed(42)
|
239 |
+
ys = np.random.randn(101).cumsum(axis=0)
|
240 |
+
|
241 |
+
curve = hv.Curve(ys, ('x', 'x-label'), ('y', 'y-label'))
|
242 |
+
|
243 |
+
(curve.relabel('No axis').opts(xaxis=None, yaxis=None) +
|
244 |
+
curve.relabel('Bare axis').opts(xaxis='bare') +
|
245 |
+
curve.relabel('Moved axis').opts(xaxis='top', yaxis='right'))
|
246 |
+
```
|
247 |
+
|
248 |
+
### Inverting axes
|
249 |
+
|
250 |
+
Another option to control axes is to invert the x-/y-axes using the ``invert_axes`` options, i.e. turn a vertical plot into a horizontal plot. Secondly each individual axis can be flipped left to right or upside down respectively using the ``invert_xaxis`` and ``invert_yaxis`` options.
|
251 |
+
|
252 |
+
|
253 |
+
```python
|
254 |
+
bars = hv.Bars([('Australia', 10), ('United States', 14), ('United Kingdom', 7)], 'Country')
|
255 |
+
|
256 |
+
(bars.relabel('Invert axes').opts(invert_axes=True, width=400) +
|
257 |
+
bars.relabel('Invert x-axis').opts(invert_xaxis=True) +
|
258 |
+
bars.relabel('Invert y-axis').opts(invert_yaxis=True)).opts(shared_axes=False)
|
259 |
+
```
|
260 |
+
|
261 |
+
### Axis labels
|
262 |
+
|
263 |
+
Ordinarily axis labels are controlled using the dimension label, however explicitly ``xlabel`` and ``ylabel`` options make it possible to override the label at the plot level. Additionally the ``labelled`` option allows specifying which axes should be labelled at all, making it possible to hide axis labels:
|
264 |
+
|
265 |
+
|
266 |
+
```python
|
267 |
+
(curve.relabel('Dimension labels') +
|
268 |
+
curve.relabel("xlabel='Custom x-label'").opts(xlabel='Custom x-label') +
|
269 |
+
curve.relabel('Unlabelled').opts(labelled=[]))
|
270 |
+
```
|
271 |
+
|
272 |
+
### Axis ranges
|
273 |
+
|
274 |
+
The ranges of a plot are ordinarily controlled by computing the data range and combining it with the dimension ``range`` and ``soft_range`` but they may also be padded or explicitly overridden using ``xlim`` and ``ylim`` options.
|
275 |
+
|
276 |
+
#### Dimension ranges
|
277 |
+
|
278 |
+
* **data range**: The data range is computed by min and max of the dimension values
|
279 |
+
* **range**: Hard override of the data range
|
280 |
+
* **soft_range**: Soft override of the data range
|
281 |
+
|
282 |
+
##### **Dimension.range**
|
283 |
+
|
284 |
+
Setting the ``range`` of a Dimension overrides the data ranges, i.e. here we can see that despite the fact the data extends to x=100 the axis is cut off at 90:
|
285 |
+
|
286 |
+
|
287 |
+
```python
|
288 |
+
curve.redim(x=hv.Dimension('x', range=(-10, 90)))
|
289 |
+
```
|
290 |
+
|
291 |
+
##### Dimension.soft_range
|
292 |
+
|
293 |
+
Declaringa ``soft_range`` on the other hand combines the data range and the supplied range, i.e. it will pick whichever extent is wider. Using the same example as above we can see it uses the -10 value supplied in the soft_range but also extends to 100, which is the upper bound of the actual data:
|
294 |
+
|
295 |
+
|
296 |
+
```python
|
297 |
+
curve.redim(x=hv.Dimension('x', soft_range=(-10, 90)))
|
298 |
+
```
|
299 |
+
|
300 |
+
#### Padding
|
301 |
+
|
302 |
+
Applying padding to the ranges is an easy way to ensure that the data is not obscured by the margins. The padding is specified by the fraction by which to increase auto-ranged extents to make datapoints more visible around borders. The default for most elements is `padding=0.1`. The padding considers the width and height of the plot to keep the visual extent of the padding equal. The padding values can be specified with three levels of detail:
|
303 |
+
|
304 |
+
* 1. A single numeric value (e.g. ``padding=0.1``)
|
305 |
+
* 2. A tuple specifying the padding for the x/y(/z) axes respectively (e.g. ``padding=(0, 0.1)``)
|
306 |
+
* 3. A tuple of tuples specifying padding for the lower and upper bound respectively (e.g. ``padding=(0, (0, 0.1))``)
|
307 |
+
|
308 |
+
|
309 |
+
```python
|
310 |
+
(curve.relabel('Pad both axes').opts(padding=0.1) +
|
311 |
+
curve.relabel('Pad y-axis').opts(padding=(0, 0.1)) +
|
312 |
+
curve.relabel('Pad y-axis upper bound').opts(padding=(0, (0, 0.1)))).opts(shared_axes=False)
|
313 |
+
```
|
314 |
+
|
315 |
+
#### xlim/ylim
|
316 |
+
|
317 |
+
The data ranges, dimension ranges and padding combine across plots in an overlay to ensure that all the data is contained in the viewport. In some cases it is more convenient to override the ranges with explicit ``xlim`` and ``ylim`` options which have the highest precedence and will be respected no matter what.
|
318 |
+
|
319 |
+
|
320 |
+
```python
|
321 |
+
curve.relabel('Explicit xlim/ylim').opts(xlim=(-10, 110), ylim=(-14, 6))
|
322 |
+
```
|
323 |
+
|
324 |
+
#### Autoranging
|
325 |
+
|
326 |
+
With the `autorange` keyword, you can ensure the data in the viewport is automatically ranged to maximise the use of the x- or y-axis. To illustrate, here is the same `curve` autoranging on the `y-axis`: note the difference in behavior when zooming into the data:
|
327 |
+
|
328 |
+
|
329 |
+
```python
|
330 |
+
curve.relabel('Autoranging on y').opts(autorange='y')
|
331 |
+
```
|
332 |
+
|
333 |
+
To pin the ends of the ranges you can use the `xlim` and `ylim` options, using a value of `None` to allow autoranging to operate. Here the bottom range of the y-axis is pinned to the value of `-14`:
|
334 |
+
|
335 |
+
|
336 |
+
```python
|
337 |
+
curve.relabel('Autoranging on y with set lower limit').opts(autorange='y', ylim=(-14,None))
|
338 |
+
```
|
339 |
+
|
340 |
+
Autoranging works analogously for the x-axis and also respects the padding setting. In addition, autoranging is triggered when the plotted data is updated dynamically, as is common when building interactive visualizations with operations or `DynamicMap`s.
|
341 |
+
|
342 |
+
### Axis ticks
|
343 |
+
|
344 |
+
Setting tick locations differs a little bit depending on the plotting extension, interactive backends such as bokeh or plotly dynamically update the ticks, which means fixed tick locations may not be appropriate and the formatters have to be applied in Javascript code. Nevertheless most options to control the ticking are consistent across extensions.
|
345 |
+
|
346 |
+
#### Tick locations
|
347 |
+
|
348 |
+
The number and locations of ticks can be set in three main ways:
|
349 |
+
|
350 |
+
* Number of ticks: Declare the number of desired ticks as an integer
|
351 |
+
* List of tick positions: An explicit list defining the list of positions at which to draw a tick
|
352 |
+
* List of tick positions and labels: A list of tuples of the form (position, label)
|
353 |
+
|
354 |
+
|
355 |
+
```python
|
356 |
+
(curve.relabel('N ticks (xticks=10)').opts(xticks=10) +
|
357 |
+
curve.relabel('Listed ticks (xticks=[0, 1, 2])').opts(xticks=[0, 50, 100]) +
|
358 |
+
curve.relabel("Tick labels (xticks=[(0, 'zero'), ...").opts(xticks=[(0, 'zero'), (50, 'fifty'), (100, 'one hundred')]))
|
359 |
+
```
|
360 |
+
|
361 |
+
Lastly each extension will accept the custom Ticker objects the library provides, which can be used to achieve layouts not usually available.
|
362 |
+
|
363 |
+
#### Tick formatters
|
364 |
+
|
365 |
+
Tick formatting works very differently in different backends, however the ``xformatter`` and ``yformatter`` options try to minimize these differences. Tick formatters may be defined in one of three formats:
|
366 |
+
|
367 |
+
* A classic format string such as ``'%d'``, ``'%.3f'`` or ``'%d'`` which may also contain other characters (``'$%.2f'``)
|
368 |
+
* A ``bokeh.models.TickFormatter`` in bokeh and a ``matplotlib.ticker.Formatter`` instance in matplotlib
|
369 |
+
|
370 |
+
Here is a small example demonstrating how to use the string approaches:
|
371 |
+
|
372 |
+
|
373 |
+
```python
|
374 |
+
curve.relabel('Tick formatters').opts(xformatter='%.0f days', yformatter='$%.2f', width=500)
|
375 |
+
```
|
376 |
+
|
377 |
+
#### Tick orientation
|
378 |
+
|
379 |
+
Particularly when dealing with categorical axes it is often useful to control the tick rotation. This can be achieved using the ``xrotation`` and ``yrotation`` options which accept angles in degrees.
|
380 |
+
|
381 |
+
|
382 |
+
```python
|
383 |
+
bars.opts(xrotation=45)
|
384 |
+
```
|
385 |
+
|
386 |
+
### Twin axes
|
387 |
+
*(Available in HoloViews >= 1.17, requires Bokeh >=3.2)*
|
388 |
+
|
389 |
+
HoloViews now supports displaying overlays containing two different value dimensions as twin axes for chart elements. To maintain backwards compatibility, this feature is only enabled by setting the `multi_y=True` option on the overlay.
|
390 |
+
|
391 |
+
To illustrate, here is an overlay containing three curves with two value dimensions ('A' and 'B'). Setting `multi_y=True` then maps these two value dimensions to twin-axes:
|
392 |
+
|
393 |
+
|
394 |
+
```python
|
395 |
+
overlay = hv.Curve([1, 2, 3], vdims=['A']) * hv.Curve([2, 3, 4], vdims=['A']) * hv.Curve([3, 2, 1], vdims=['B'])
|
396 |
+
overlay.opts(multi_y=True)
|
397 |
+
```
|
398 |
+
|
399 |
+
Additional value dimensions do map to additional axes but be aware that support of multi axes beyond twin axes is currently considered experimental.
|
400 |
+
|
401 |
+
The first value dimension is mapped to the left-hand axis and the second value dimension maps to the right axis. Note that the two axes are individually zoomable by hovering over them and using the Bokeh wheelzoom tool.
|
402 |
+
|
403 |
+
|
404 |
+
#### Supported `multi_y` options
|
405 |
+
|
406 |
+
When `multi_y` is enabled, you can set individual axis options on the elements of the overlay.
|
407 |
+
|
408 |
+
In this example, the left axis uses the default options while the right axis is an inverted, autoranged, log axis with a set `ylim`:
|
409 |
+
|
410 |
+
|
411 |
+
```python
|
412 |
+
(hv.Curve([1, 2, 3], vdims=['A'])
|
413 |
+
* hv.Curve([2, 3, 4], vdims=['B']).opts(autorange='y', invert_yaxis=True, logy=True, ylim=(1,10),
|
414 |
+
ylabel='B custom', fontsize={'ylabel':10})
|
415 |
+
).opts(multi_y=True)
|
416 |
+
```
|
417 |
+
|
418 |
+
Supported options for customizing individual axes are `apply_ranges`, `autorange='y'`, `invert_yaxis`, `logy` and `ylim`, `yaxis` as well as the following options for labelling: `labelled`, `ylabel` and the `'ylabel'` setting in `fontsize`.
|
419 |
+
|
420 |
+
Note that as of HoloViews 1.17.0, `multi_y` does not have streaming plot support, extra axis labels are not dynamic and only the `RangeXY` linked stream is aware of additional y-axes.
|
421 |
+
|
422 |
+
### Subcoordinate y-axis
|
423 |
+
*(Available in HoloViews >= 1.18)*
|
424 |
+
|
425 |
+
HoloViews enables you to create overlays where each element has its own distinct y-axis subcoordinate system. To activate this feature, set the `subcoordinate_y` keyword to True for **each** overlay element; the default is False. When using `subcoordinate_y=True`, setting a `label` for each element is required for proper rendering and identification.This will automatically distribute overlay elements along the y-axis.
|
426 |
+
|
427 |
+
For more fine-grained control over y-axis positioning, you can specify a numerical 2-tuple for subcoordinate_y with values ranging from 0 to 1. Additionally, the `subcoordinate_scale` keyword, which defaults to 1, allows you to adjust the vertical scale of each element. This option is only applicable when `subcoordinate_y=True`. For example, setting a single Curve's `subcoordinate_scale` to 2 will result in it overlapping 50% with its adjacent elements.
|
428 |
+
|
429 |
+
|
430 |
+
```python
|
431 |
+
x = np.linspace(0, 10*np.pi)
|
432 |
+
|
433 |
+
curves = [
|
434 |
+
hv.Curve((x + i*np.pi/2, np.sin(x)), label=f'Line {i}').opts(subcoordinate_y=True, subcoordinate_scale=1.2)
|
435 |
+
for i in range(3)
|
436 |
+
]
|
437 |
+
|
438 |
+
hv.Overlay(curves).opts(show_legend=False)
|
439 |
+
```
|
hvplot_docs/Deploying_Bokeh_Apps.md
ADDED
@@ -0,0 +1,553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Deploying Bokeh Apps
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import holoviews as hv
|
7 |
+
hv.extension('bokeh')
|
8 |
+
```
|
9 |
+
|
10 |
+
## Purpose
|
11 |
+
|
12 |
+
HoloViews is an incredibly convenient way of working interactively and exploratively within a notebook or commandline context. However, once you have implemented a polished interactive dashboard or some other complex interactive visualization, you will often want to deploy it outside the notebook to share with others who may not be comfortable with the notebook interface.
|
13 |
+
|
14 |
+
In the simplest case, to visualize some HoloViews container or element `obj`, you can export it to a standalone HTML file for sharing using the `save` function of the Bokeh renderer:
|
15 |
+
|
16 |
+
```
|
17 |
+
hv.save(obj, 'out.html')
|
18 |
+
```
|
19 |
+
|
20 |
+
This command will generate a file `out.html` that you can put on any web server, email directly to colleagues, etc.; it is fully self-contained and does not require any Python server to be installed or running.
|
21 |
+
|
22 |
+
Unfortunately, a static approach like this cannot support any HoloViews object that uses DynamicMap (either directly or via operations that return DynamicMaps like `decimate`, `datashade`, and `rasterize`). Anything with DynamicMap requires a live, running Python server to dynamically select and provide the data for the various parameters that can be selected by the user. Luckily, when you need a live Python process during the visualization, the [Bokeh server](http://bokeh.pydata.org/en/latest/docs/user_guide/server.html) provides a very convenient way of deploying HoloViews plots and interactive dashboards in a scalable and flexible manner. The Bokeh server allows all the usual interactions that HoloViews lets you define and more including:
|
23 |
+
|
24 |
+
* responding to plot events and tool interactions via [Linked Streams](./13-Custom_Interactivity.ipynb)
|
25 |
+
* generating and interacting with plots via the usual widgets that HoloViews supports for HoloMap and DynamicMap objects.
|
26 |
+
* using periodic and timeout events to drive plot updates
|
27 |
+
* combining HoloViews plots with custom Bokeh plots to quickly write highly customized apps.
|
28 |
+
|
29 |
+
## Overview
|
30 |
+
|
31 |
+
In this guide we will cover how we can deploy a Bokeh app from a HoloViews plot in a number of different ways:
|
32 |
+
|
33 |
+
1. Inline from within the Jupyter notebook
|
34 |
+
|
35 |
+
2. Starting a server interactively and open it in a new browser window.
|
36 |
+
|
37 |
+
3. From a standalone script file
|
38 |
+
|
39 |
+
4. Combining HoloViews and Bokeh models to create a more customized app
|
40 |
+
|
41 |
+
If you have read a bit about HoloViews you will know that HoloViews objects are not themselves plots, instead they contain sufficient data and metadata allowing them to be rendered automatically in a notebook context. In other words, when a HoloViews object is evaluated a backend specific ``Renderer`` converts the HoloViews object into Bokeh models, a Matplotlib figure or a Plotly graph. This intermediate representation is then rendered as an image or as HTML with associated Javascript, which is what ends up being displayed.
|
42 |
+
|
43 |
+
## The workflow
|
44 |
+
|
45 |
+
The most convenient way to work with HoloViews is to iteratively improve a visualization in the notebook. Once you have developed a visualization or dashboard that you would like to deploy you can use the ``BokehRenderer`` to export the visualization as illustrated above, or you can deploy it as a Bokeh server app.
|
46 |
+
|
47 |
+
Here we will create a small interactive plot, using [Linked Streams](./13-Custom_Interactivity.ipynb), which mirrors the points selected using box- and lasso-select tools in a second plot and computes some statistics:
|
48 |
+
|
49 |
+
|
50 |
+
```python
|
51 |
+
# Declare some points
|
52 |
+
points = hv.Points(np.random.randn(1000,2 ))
|
53 |
+
|
54 |
+
# Declare points as source of selection stream
|
55 |
+
selection = hv.streams.Selection1D(source=points)
|
56 |
+
|
57 |
+
# Write function that uses the selection indices to slice points and compute stats
|
58 |
+
def selected_info(index):
|
59 |
+
arr = points.array()[index]
|
60 |
+
if index:
|
61 |
+
label = 'Mean x, y: %.3f, %.3f' % tuple(arr.mean(axis=0))
|
62 |
+
else:
|
63 |
+
label = 'No selection'
|
64 |
+
return points.clone(arr, label=label).opts(color='red')
|
65 |
+
|
66 |
+
# Combine points and DynamicMap
|
67 |
+
selected_points = hv.DynamicMap(selected_info, streams=[selection])
|
68 |
+
layout = points.opts(tools=['box_select', 'lasso_select']) + selected_points
|
69 |
+
|
70 |
+
layout
|
71 |
+
```
|
72 |
+
|
73 |
+
<img src='https://assets.holoviews.org/gifs/examples/streams/bokeh/point_selection1d.gif'></img>
|
74 |
+
|
75 |
+
#### Working with the BokehRenderer
|
76 |
+
|
77 |
+
When working with Bokeh server or wanting to manipulate a backend specific plot object you will have to use a HoloViews ``Renderer`` directly to convert the HoloViews object into the backend specific representation. Therefore we will start by getting a hold of a ``BokehRenderer``:
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
renderer = hv.renderer('bokeh')
|
82 |
+
print(renderer)
|
83 |
+
```
|
84 |
+
|
85 |
+
```python
|
86 |
+
BokehRenderer()
|
87 |
+
```
|
88 |
+
|
89 |
+
All ``Renderer`` classes in HoloViews are so called ParameterizedFunctions; they provide both classmethods and instance methods to render an object. You can easily create a new ``Renderer`` instance using the ``.instance`` method:
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
renderer = renderer.instance(mode='server')
|
94 |
+
```
|
95 |
+
|
96 |
+
Renderers can also have different modes. In this case we will instantiate the renderer in ``'server'`` mode, which tells the Renderer to render the HoloViews object to a format that can easily be deployed as a server app. Before going into more detail about deploying server apps we will quickly remind ourselves how the renderer turns HoloViews objects into Bokeh models.
|
97 |
+
|
98 |
+
### Figures
|
99 |
+
|
100 |
+
The BokehRenderer converts the HoloViews object to a HoloViews ``Plot``, which holds the Bokeh models that will be rendered to screen. As a very simple example we can convert a HoloViews ``Image`` to a HoloViews plot:
|
101 |
+
|
102 |
+
|
103 |
+
```python
|
104 |
+
plot = renderer.get_plot(layout)
|
105 |
+
print(plot)
|
106 |
+
```
|
107 |
+
|
108 |
+
```
|
109 |
+
<LayoutPlot LayoutPlot01811>
|
110 |
+
```
|
111 |
+
|
112 |
+
Using the ``state`` attribute on the HoloViews plot we can access the Bokeh ``Column`` model, which we can then work with directly.
|
113 |
+
|
114 |
+
|
115 |
+
```python
|
116 |
+
plot.state
|
117 |
+
```
|
118 |
+
|
119 |
+
**Column**(id='1570', ...)
|
120 |
+
|
121 |
+
In the background this is how HoloViews converts any HoloViews object into Bokeh models, which can then be converted to embeddable or standalone HTML and be rendered in the browser. This conversion is usually done in the background using the ``figure_data`` method:
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
html = renderer._figure_data(plot, 'html')
|
126 |
+
```
|
127 |
+
|
128 |
+
### Bokeh Documents
|
129 |
+
|
130 |
+
In Bokeh the [``Document``](http://bokeh.pydata.org/en/latest/docs/reference/document.html) is the basic unit at which Bokeh models (such as plots, layouts and widgets) are held and serialized. The serialized JSON representation is then sent to BokehJS on the client-side browser. When in ``'server'`` mode the BokehRenderer will automatically return a server Document:
|
131 |
+
|
132 |
+
|
133 |
+
```python
|
134 |
+
renderer(layout)
|
135 |
+
```
|
136 |
+
|
137 |
+
```
|
138 |
+
(<bokeh.document.Document at 0x11afc7590>,
|
139 |
+
{'file-ext': 'html', 'mime_type': u'text/html'})
|
140 |
+
```
|
141 |
+
|
142 |
+
We can also easily use the ``server_doc`` method to get a Bokeh ``Document``, which does not require you to make an instance in 'server' mode.
|
143 |
+
|
144 |
+
|
145 |
+
```python
|
146 |
+
doc = renderer.server_doc(layout)
|
147 |
+
doc.title = 'HoloViews App'
|
148 |
+
```
|
149 |
+
|
150 |
+
In the background however, HoloViews uses the Panel library to render components to a Bokeh model which can be rendered in the notebook, to a file or on a server:
|
151 |
+
|
152 |
+
|
153 |
+
```python
|
154 |
+
import panel as pn
|
155 |
+
|
156 |
+
model = pn.panel(layout).get_root()
|
157 |
+
model
|
158 |
+
```
|
159 |
+
|
160 |
+
For more information on the interaction between Panel and HoloViews see the the [Panel documentation](https://panel.holoviz.org/reference/panes/HoloViews.html).
|
161 |
+
|
162 |
+
## Deploying with ``panel serve``
|
163 |
+
|
164 |
+
Deployment from a script with `panel serve` is one of the most common ways to deploy a Bokeh app. Any ``.py`` or ``.ipynb`` file that attaches a plot to Bokeh's ``curdoc`` can be deployed using ``panel serve``. The easiest way to do this is using wrapping the HoloViews component in Panel using ``pn.panel(hvobj)`` and then calling the ``panel_obj.servable()`` method, which accepts any HoloViews object ensures that the plot is discoverable by Panel and the underlying Bokeh server. See below to see a full standalone script:
|
165 |
+
|
166 |
+
```python
|
167 |
+
import numpy as np
|
168 |
+
import panel as pn
|
169 |
+
import holoviews as hv
|
170 |
+
import holoviews.plotting.bokeh
|
171 |
+
|
172 |
+
points = hv.Points(np.random.randn(1000,2 )).opts(tools=['box_select', 'lasso_select'])
|
173 |
+
selection = hv.streams.Selection1D(source=points)
|
174 |
+
|
175 |
+
def selected_info(index):
|
176 |
+
arr = points.array()[index]
|
177 |
+
if index:
|
178 |
+
label = 'Mean x, y: %.3f, %.3f' % tuple(arr.mean(axis=0))
|
179 |
+
else:
|
180 |
+
label = 'No selection'
|
181 |
+
return points.clone(arr, label=label).opts(color='red')
|
182 |
+
|
183 |
+
layout = points + hv.DynamicMap(selected_info, streams=[selection])
|
184 |
+
|
185 |
+
pn.panel(layout).servable(title='HoloViews App')
|
186 |
+
```
|
187 |
+
|
188 |
+
In just a few steps we can iteratively refine in the notebook to a deployable Panel app. Note also that we can also deploy an app directly from a notebook. By using `.servable()` in a notebook any regular ``.ipynb`` file can be made into a valid Panel/Bokeh app, which can be served with ``panel serve example.ipynb``.
|
189 |
+
|
190 |
+
It is also possible to create a Bokeh `Document` more directly working with the underlying Bokeh representation instead. This in itself is sufficient to make the plot servable using `bokeh serve`:
|
191 |
+
|
192 |
+
|
193 |
+
```python
|
194 |
+
hv.renderer('bokeh').server_doc(layout)
|
195 |
+
```
|
196 |
+
|
197 |
+
In addition to starting a server from a script we can also start up a server interactively, so let's do a quick deep dive into Bokeh ``Application`` and ``Server`` objects and how we can work with them from within HoloViews.
|
198 |
+
|
199 |
+
## Bokeh Server
|
200 |
+
|
201 |
+
To start a Bokeh server directly from a notebook we can also use Panel, specifically we'll use the `panel.serve` function. We'll define a ``DynamicMap`` of a sine ``Curve`` varying by frequency, phase and an offset and then create a server instance using Panel:
|
202 |
+
|
203 |
+
|
204 |
+
```python
|
205 |
+
def sine(frequency, phase, amplitude):
|
206 |
+
xs = np.linspace(0, np.pi*4)
|
207 |
+
return hv.Curve((xs, np.sin(frequency*xs+phase)*amplitude)).opts(width=800)
|
208 |
+
|
209 |
+
ranges = dict(frequency=(1, 5), phase=(-np.pi, np.pi), amplitude=(-2, 2), y=(-2, 2))
|
210 |
+
dmap = hv.DynamicMap(sine, kdims=['frequency', 'phase', 'amplitude']).redim.range(**ranges)
|
211 |
+
|
212 |
+
server = pn.serve(dmap, start=False, show=False)
|
213 |
+
```
|
214 |
+
|
215 |
+
```
|
216 |
+
<bokeh.server.server.Server object at 0x10b3a0510>
|
217 |
+
```
|
218 |
+
|
219 |
+
Next we can define a callback on the IOLoop that will open the server app in a new browser window and actually start the app (and if outside the notebook the IOLoop):
|
220 |
+
|
221 |
+
|
222 |
+
```python
|
223 |
+
server.start()
|
224 |
+
server.show('/')
|
225 |
+
|
226 |
+
# Outside the notebook ioloop needs to be started
|
227 |
+
# from tornado.ioloop import IOLoop
|
228 |
+
# loop = IOLoop.current()
|
229 |
+
# loop.start()
|
230 |
+
```
|
231 |
+
|
232 |
+
After running the cell above you should have noticed a new browser window popping up displaying our plot. Once you are done playing with it you can stop it with:
|
233 |
+
|
234 |
+
|
235 |
+
```python
|
236 |
+
server.stop()
|
237 |
+
```
|
238 |
+
|
239 |
+
We can achieve the equivalent using the `.show` method on a Panel object:
|
240 |
+
|
241 |
+
|
242 |
+
```python
|
243 |
+
server = pn.panel(dmap).show()
|
244 |
+
```
|
245 |
+
|
246 |
+
<img width='80%' src="https://assets.holoviews.org/gifs/guides/user_guide/Deploying_Bokeh_Apps/bokeh_server_new_window.png"></img>
|
247 |
+
|
248 |
+
We will once again stop this Server before continuing:
|
249 |
+
|
250 |
+
|
251 |
+
```python
|
252 |
+
server.stop()
|
253 |
+
```
|
254 |
+
|
255 |
+
## Inlining apps in the notebook
|
256 |
+
|
257 |
+
Instead of displaying our app in a new browser window we can also display an app inline in the notebook simply by using the `.app` method on Panel object. The server app will be killed whenever you rerun or delete the cell that contains the output. Additionally, if your Jupyter Notebook server is not running on the default address or port (``localhost:8888``) supply the websocket origin, which should match the first part of the URL of your notebook:
|
258 |
+
|
259 |
+
|
260 |
+
```python
|
261 |
+
dmap
|
262 |
+
```
|
263 |
+
|
264 |
+
<img width='80%' src='https://assets.holoviews.org/gifs/guides/user_guide/Deploying_Bokeh_Apps/bokeh_server_inline_simple.gif'></img>
|
265 |
+
|
266 |
+
## Periodic callbacks
|
267 |
+
|
268 |
+
One of the most important features of deploying apps is the ability to attach asynchronous, periodic callbacks, which update the plot. The simplest way of achieving this is to attach a ``Counter`` stream on the plot which is incremented on each callback. As a simple demo we'll simply compute a phase offset from the counter value, animating the sine wave:
|
269 |
+
|
270 |
+
|
271 |
+
```python
|
272 |
+
def sine(counter):
|
273 |
+
phase = counter*0.1%np.pi*2
|
274 |
+
xs = np.linspace(0, np.pi*4)
|
275 |
+
return hv.Curve((xs, np.sin(xs+phase))).opts(width=800)
|
276 |
+
|
277 |
+
counter = hv.streams.Counter()
|
278 |
+
hv.DynamicMap(sine, streams=[counter])
|
279 |
+
|
280 |
+
dmap
|
281 |
+
```
|
282 |
+
|
283 |
+
<img width='80%' src='https://assets.holoviews.org/gifs/guides/user_guide/Deploying_Bokeh_Apps/bokeh_server_periodic.gif'></img>
|
284 |
+
|
285 |
+
Once we have created a Panel object we can call the `add_periodic_callback` method to set up a periodic callback. The first argument to the method is the callback and the second argument period specified in milliseconds. As soon as we start this callback you should see the Curve above become animated.
|
286 |
+
|
287 |
+
|
288 |
+
```python
|
289 |
+
def update():
|
290 |
+
counter.event(counter=counter.counter+1)
|
291 |
+
|
292 |
+
cb = pn.state.add_periodic_callback(update, period=200)
|
293 |
+
```
|
294 |
+
|
295 |
+
Once started we can stop and start it at will using the `.stop` and `.start` methods:
|
296 |
+
|
297 |
+
|
298 |
+
```python
|
299 |
+
cb.stop()
|
300 |
+
```
|
301 |
+
|
302 |
+
## Combining Bokeh Application and Flask Application
|
303 |
+
|
304 |
+
While Panel and Bokeh are great ways to create an application often we want to leverage the simplicity of a Flask server. With Flask we can easily embed a HoloViews, Bokeh and Panel application in a regular website. The main idea for getting Bokeh and Flask to work together is to run both apps on ports and then use Flask to pull the Bokeh Serve session with `pull_session` from [bokeh.client.session](https://bokeh.pydata.org/en/latest/docs/reference/client/session.html).
|
305 |
+
|
306 |
+
|
307 |
+
```python
|
308 |
+
def sine(frequency, phase, amplitude):
|
309 |
+
xs = np.linspace(0, np.pi*4)
|
310 |
+
return hv.Curve((xs, np.sin(frequency*xs+phase)*amplitude)).options(width=800)
|
311 |
+
|
312 |
+
ranges = dict(frequency=(1, 5), phase=(-np.pi, np.pi), amplitude=(-2, 2), y=(-2, 2))
|
313 |
+
dmap = hv.DynamicMap(sine, kdims=['frequency', 'phase', 'amplitude']).redim.range(**ranges)
|
314 |
+
|
315 |
+
pn.serve(dmap, websocket_origin='localhost:5000', port=5006, show=False)
|
316 |
+
```
|
317 |
+
|
318 |
+
We run load up our dynamic map into a Bokeh Application with the parameter `allow_websocket_origin=["localhost:5000"]`
|
319 |
+
|
320 |
+
```python
|
321 |
+
from bokeh.client import pull_session
|
322 |
+
from bokeh.embed import server_session
|
323 |
+
from flask import Flask, render_template
|
324 |
+
from flask import send_from_directory
|
325 |
+
|
326 |
+
app = Flask(__name__)
|
327 |
+
|
328 |
+
|
329 |
+
# locally creates a page
|
330 |
+
@app.route('/')
|
331 |
+
def index():
|
332 |
+
with pull_session(url="http://localhost:5006/") as session:
|
333 |
+
# generate a script to load the customized session
|
334 |
+
script = server_session(session_id=session.id, url='http://localhost:5006')
|
335 |
+
# use the script in the rendered page
|
336 |
+
return render_template("embed.html", script=script, template="Flask")
|
337 |
+
|
338 |
+
if __name__ == '__main__':
|
339 |
+
# runs app in debug mode
|
340 |
+
app.run(port=5000, debug=True)
|
341 |
+
```
|
342 |
+
|
343 |
+
Note that in a notebook context we cannot use `pull_session` but this example demonstrates how we can embed the Bokeh server inside a simple flask app.
|
344 |
+
|
345 |
+
This is an example of a basic flask app. To find out more about Flask a tutorial can be found on the [Flask Quickstart Guide](http://flask.pocoo.org/docs/1.0/quickstart/#).
|
346 |
+
|
347 |
+
|
348 |
+
|
349 |
+
Below is an example of a basic Flask App that pulls from the Bokeh Application. The Bokeh Application is using `Server` from Bokeh and `IOLoop` from tornado to run the app.
|
350 |
+
|
351 |
+
```python
|
352 |
+
# holoviews.py
|
353 |
+
|
354 |
+
import holoviews as hv
|
355 |
+
import panel as pn
|
356 |
+
import numpy as np
|
357 |
+
|
358 |
+
hv.extension('bokeh')
|
359 |
+
|
360 |
+
def sine(frequency, phase, amplitude):
|
361 |
+
xs = np.linspace(0, np.pi*4)
|
362 |
+
return hv.Curve((xs, np.sin(frequency*xs+phase)*amplitude)).options(width=800)
|
363 |
+
|
364 |
+
if __name__ == '__main__':
|
365 |
+
ranges = dict(frequency=(1, 5), phase=(-np.pi, np.pi), amplitude=(-2, 2), y=(-2, 2))
|
366 |
+
dmap = hv.DynamicMap(sine, kdims=['frequency', 'phase', 'amplitude']).redim.range(**ranges)
|
367 |
+
pn.serve(dmap, port=5006, allow_websocket_origin=["localhost:5000"], show=False)
|
368 |
+
```
|
369 |
+
|
370 |
+
```python
|
371 |
+
#flaskApp.py
|
372 |
+
|
373 |
+
from bokeh.client import pull_session
|
374 |
+
from bokeh.embed import server_session
|
375 |
+
from flask import Flask, render_template
|
376 |
+
from flask import send_from_directory
|
377 |
+
|
378 |
+
app = Flask(__name__)
|
379 |
+
|
380 |
+
# locally creates a page
|
381 |
+
@app.route('/')
|
382 |
+
def index():
|
383 |
+
with pull_session(url="http://localhost:5006/") as session:
|
384 |
+
# generate a script to load the customized session
|
385 |
+
script = server_session(session_id=session.id, url='http://localhost:5006')
|
386 |
+
# use the script in the rendered page
|
387 |
+
return render_template("embed.html", script=script, template="Flask")
|
388 |
+
|
389 |
+
|
390 |
+
if __name__ == '__main__':
|
391 |
+
# runs app in debug mode
|
392 |
+
app.run(port=5000, debug=True)
|
393 |
+
```
|
394 |
+
|
395 |
+
```html
|
396 |
+
<!-- embed.html -->
|
397 |
+
|
398 |
+
<!doctype html>
|
399 |
+
|
400 |
+
<html lang="en">
|
401 |
+
<head>
|
402 |
+
<meta charset="utf-8">
|
403 |
+
<title>Embedding a Bokeh Server With Flask</title>
|
404 |
+
</head>
|
405 |
+
|
406 |
+
<body>
|
407 |
+
<div>
|
408 |
+
This Bokeh app below served by a Bokeh server that has been embedded
|
409 |
+
in another web app framework. For more information see the section
|
410 |
+
<a target="_blank" href="https://bokeh.pydata.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library">Embedding Bokeh Server as a Library</a>
|
411 |
+
in the User's Guide.
|
412 |
+
</div>
|
413 |
+
{{ script|safe }}
|
414 |
+
</body>
|
415 |
+
</html>
|
416 |
+
```
|
417 |
+
|
418 |
+
If you wish to replicate navigate to the `examples/gallery/apps/flask` directory and follow the these steps:
|
419 |
+
|
420 |
+
* Step One: call `python holoviews_app.py` in the terminal (this will start the Panel/Bokeh server)
|
421 |
+
* Step Two: open a new terminal and call `python flask_app.py` (this will start the Flask application)
|
422 |
+
* Step Three: go to web browser and type `localhost:5000` and the app will appear
|
423 |
+
|
424 |
+
## Combining HoloViews and Panel or Bokeh Plots/Widgets
|
425 |
+
|
426 |
+
While HoloViews provides very convenient ways of creating an app it is not as fully featured as Bokeh itself is. Therefore we often want to extend a HoloViews based app with Panel or Bokeh plots and widgets. Here we will discover to achieve this with both Panel and then the equivalent using pure Bokeh.
|
427 |
+
|
428 |
+
|
429 |
+
```python
|
430 |
+
import holoviews as hv
|
431 |
+
import numpy as np
|
432 |
+
import panel as pn
|
433 |
+
|
434 |
+
# Create the holoviews app again
|
435 |
+
def sine(phase):
|
436 |
+
xs = np.linspace(0, np.pi*4)
|
437 |
+
return hv.Curve((xs, np.sin(xs+phase))).opts(width=800)
|
438 |
+
|
439 |
+
stream = hv.streams.Stream.define('Phase', phase=0.)()
|
440 |
+
dmap = hv.DynamicMap(sine, streams=[stream])
|
441 |
+
|
442 |
+
start, end = 0, np.pi*2
|
443 |
+
slider = pn.widgets.FloatSlider(start=start, end=end, value=start, step=0.2, name="Phase")
|
444 |
+
|
445 |
+
# Create a slider and play buttons
|
446 |
+
def animate_update():
|
447 |
+
year = slider.value + 0.2
|
448 |
+
if year > end:
|
449 |
+
year = start
|
450 |
+
slider.value = year
|
451 |
+
|
452 |
+
def slider_update(event):
|
453 |
+
# Notify the HoloViews stream of the slider update
|
454 |
+
stream.event(phase=event.new)
|
455 |
+
|
456 |
+
slider.param.watch(slider_update, 'value')
|
457 |
+
|
458 |
+
def animate(event):
|
459 |
+
if button.name == '► Play':
|
460 |
+
button.name = '❚❚ Pause'
|
461 |
+
callback.start()
|
462 |
+
else:
|
463 |
+
button.name = '► Play'
|
464 |
+
callback.stop()
|
465 |
+
|
466 |
+
button = pn.widgets.Button(name='► Play', width=60, align='end')
|
467 |
+
button.on_click(animate)
|
468 |
+
callback = pn.state.add_periodic_callback(animate_update, 50, start=False)
|
469 |
+
|
470 |
+
app = pn.Column(
|
471 |
+
dmap,
|
472 |
+
pn.Row(slider, button)
|
473 |
+
)
|
474 |
+
|
475 |
+
app
|
476 |
+
```
|
477 |
+
|
478 |
+
If instead we want to deploy this we could add `.servable` as discussed before or use `pn.serve`. Note however that when using `pn.serve` all sessions will share the same state therefore it is best to
|
479 |
+
wrap the creation of the app in a function which we can then provide to `pn.serve`. For more detail on deploying Panel applications also see the [Panel server deployment guide](https://panel.holoviz.org/user_guide/Server_Deployment.html).
|
480 |
+
|
481 |
+
Now we can reimplement the same example using Bokeh allowing us to compare and contrast the approaches:
|
482 |
+
|
483 |
+
|
484 |
+
```python
|
485 |
+
import numpy as np
|
486 |
+
import holoviews as hv
|
487 |
+
|
488 |
+
from bokeh.io import show, curdoc
|
489 |
+
from bokeh.layouts import layout
|
490 |
+
from bokeh.models import Slider, Button
|
491 |
+
|
492 |
+
renderer = hv.renderer('bokeh').instance(mode='server')
|
493 |
+
|
494 |
+
# Create the holoviews app again
|
495 |
+
def sine(phase):
|
496 |
+
xs = np.linspace(0, np.pi*4)
|
497 |
+
return hv.Curve((xs, np.sin(xs+phase))).opts(width=800)
|
498 |
+
|
499 |
+
stream = hv.streams.Stream.define('Phase', phase=0.)()
|
500 |
+
dmap = hv.DynamicMap(sine, streams=[stream])
|
501 |
+
|
502 |
+
# Define valid function for FunctionHandler
|
503 |
+
# when deploying as script, simply attach to curdoc
|
504 |
+
def modify_doc(doc):
|
505 |
+
# Create HoloViews plot and attach the document
|
506 |
+
hvplot = renderer.get_plot(dmap, doc)
|
507 |
+
|
508 |
+
# Create a slider and play buttons
|
509 |
+
def animate_update():
|
510 |
+
year = slider.value + 0.2
|
511 |
+
if year > end:
|
512 |
+
year = start
|
513 |
+
slider.value = year
|
514 |
+
|
515 |
+
def slider_update(attrname, old, new):
|
516 |
+
# Notify the HoloViews stream of the slider update
|
517 |
+
stream.event(phase=new)
|
518 |
+
|
519 |
+
start, end = 0, np.pi*2
|
520 |
+
slider = Slider(start=start, end=end, value=start, step=0.2, title="Phase")
|
521 |
+
slider.on_change('value', slider_update)
|
522 |
+
|
523 |
+
callback_id = None
|
524 |
+
|
525 |
+
def animate():
|
526 |
+
global callback_id
|
527 |
+
if button.label == '► Play':
|
528 |
+
button.label = '❚❚ Pause'
|
529 |
+
callback_id = doc.add_periodic_callback(animate_update, 50)
|
530 |
+
else:
|
531 |
+
button.label = '► Play'
|
532 |
+
doc.remove_periodic_callback(callback_id)
|
533 |
+
button = Button(label='► Play', width=60)
|
534 |
+
button.on_click(animate)
|
535 |
+
|
536 |
+
# Combine the holoviews plot and widgets in a layout
|
537 |
+
plot = layout([
|
538 |
+
[hvplot.state],
|
539 |
+
[slider, button]], sizing_mode='fixed')
|
540 |
+
|
541 |
+
doc.add_root(plot)
|
542 |
+
return doc
|
543 |
+
|
544 |
+
# To display in the notebook
|
545 |
+
show(modify_doc, notebook_url='localhost:8888')
|
546 |
+
|
547 |
+
# To display in a script
|
548 |
+
# doc = modify_doc(curdoc())
|
549 |
+
```
|
550 |
+
|
551 |
+
<img width='80%' src='https://assets.holoviews.org/gifs/guides/user_guide/Deploying_Bokeh_Apps/bokeh_server_play.gif'></img>
|
552 |
+
|
553 |
+
As you can see depending on your needs you have complete freedom whether to use just HoloViews and deploy your application, combine it Panel or even with pure Bokeh.
|
hvplot_docs/Explorer.md
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import hvplot.pandas # noqa
|
3 |
+
import xarray as xr
|
4 |
+
```
|
5 |
+
|
6 |
+
hvPlot API provides a simple and intuitive way to create plots. However when you are exploring data you don't always know in advance the best way to display it, or even what kind of plot would be best to visualize the data. You will very likely embark in an iterative process that implies choosing a kind of plot, setting various options, running some code, and repeat until you're satisfied with the output and the insights you get. The *Explorer* is a *Graphical User Interface* that allows you to easily generate customized plots, which in practice gives you the possibility to **explore** both your data and hvPlot's extensive API.
|
7 |
+
|
8 |
+
:::{note}
|
9 |
+
The *Explorer* has been added to hvPlot in version <code>0.8.0</code> and improve over the next versions, in particular version <code>0.9.0</code> added support to Xarray input types. We plan to keep on improving the explorer, making it a powerful exploration app, in the meantime please report any issue or feature request <a href='https://github.com/holoviz/hvplot/'>on GitHub</a>.
|
10 |
+
:::
|
11 |
+
|
12 |
+
## Set up
|
13 |
+
|
14 |
+
For an explorer to be displayed in a notebook you need to load the hvPlot extension, which happens automatically when you execute an import like `import hvplot.pandas`. You could also just run `hvplot.extension('bokeh')`. If instead of building Bokeh plots you would rather build Matplotlib or Plotly plots, simply execute once `hvplot.extension('matplotlib')` or `hvplot.extension('matplotlib')` before displaying the explorer.
|
15 |
+
|
16 |
+
## Instantiate
|
17 |
+
|
18 |
+
An explorer can be instantiated in two different ways:
|
19 |
+
|
20 |
+
- via the top-level `explorer()` function: `from hvplot import explorer; explorer(data)`
|
21 |
+
- via the `.explorer()` method available on the `.hvplot` namespace: `data.hvplot.explorer()` (added in version 0.9.0)
|
22 |
+
|
23 |
+
The `explorer` callable accept options to pre-customize the plot, for example `data.hvplot.explorer(title='Penguins', width=200)`.
|
24 |
+
|
25 |
+
## Interface
|
26 |
+
|
27 |
+
The object returned by `explorer()` is a [Panel](https://panel.holoviz.org/) layout that can be displayed in a notebook or served in a web application. This small application includes:
|
28 |
+
|
29 |
+
- right-hand side: a preview of the hvPlot plot and code you are building
|
30 |
+
- left-hand side: the various options that you can set to customize the plot
|
31 |
+
- top part: an Alert section that displays error messages
|
32 |
+
- bottom part: a status bar which includes a *live update* checkbox to disable live updating the preview
|
33 |
+
|
34 |
+
Let's create our first explorer instance.
|
35 |
+
|
36 |
+
|
37 |
+
```python
|
38 |
+
from bokeh.sampledata.penguins import data as df
|
39 |
+
|
40 |
+
df.head(2)
|
41 |
+
```
|
42 |
+
|
43 |
+
|
44 |
+
```python
|
45 |
+
hvexplorer = df.hvplot.explorer()
|
46 |
+
hvexplorer
|
47 |
+
```
|
48 |
+
|
49 |
+
Spend some time browsing the options made available to you. Note however that to be fully interactive the explorer needs to be executed with a live Python kernel, updating the options on the website won't update the plot.
|
50 |
+
|
51 |
+
Before diving more into the explorer's capabilities, we will update the explorer we just created as the default configuration doesn't lead to a very interesting preview for this dataset. We will do so programmatically for the purpose of building this website but you would usually not have to do that, so just assume you've changed a few options directly in the explorer using your mouse and keyboard.
|
52 |
+
|
53 |
+
|
54 |
+
```python
|
55 |
+
hvexplorer.param.update(x='bill_length_mm', y_multi=['bill_depth_mm'], by=['species'])
|
56 |
+
hvexplorer.labels.title = 'Penguins Scatter'
|
57 |
+
```
|
58 |
+
|
59 |
+
### Record the plot state
|
60 |
+
|
61 |
+
Quite often you will want to record the state of a plot you have obtained from the explorer. We even encourage the pattern of creating short-lived explorer instances that allow for quickly building the plots you want, record their state and then remove the instances from your notebook, possibly replacing them by simpler `.hvplot()` plot expressions.
|
62 |
+
|
63 |
+
You can record the state of an explorer instance in multiple ways:
|
64 |
+
|
65 |
+
- *Code* tab: displays a code snippet you can copy/paste in your notebook and that will generate exactly the same plot as previewed in the explorer
|
66 |
+
- `code` parameter: holds the code snippet string
|
67 |
+
- `.plot_code(var_name)` method: similar to the `code` parameter except you can configure the variable name
|
68 |
+
- `.settings()` method: to obtain a dictionary of your customized settings
|
69 |
+
- `.save(filename, **kwargs)` method: to save the plot to file
|
70 |
+
- `.hvplot()` method: to get a handle on the displayed HoloViews plot
|
71 |
+
|
72 |
+
We will explore a few of these approaches. Let's start with printng `code` and validating that is produces a snippet that can be copy/pasted into another cell and executed (using `eval` to simulate that).
|
73 |
+
|
74 |
+
|
75 |
+
```python
|
76 |
+
print(hvexplorer.code)
|
77 |
+
```
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
eval(hvexplorer.code)
|
82 |
+
```
|
83 |
+
|
84 |
+
The dictionary obtained from calling `.settings()` can be directly passed as kwargs to the `.hvplot()` data accessor to re-create the customized plot using the plotting API.
|
85 |
+
|
86 |
+
|
87 |
+
```python
|
88 |
+
settings = hvexplorer.settings()
|
89 |
+
settings
|
90 |
+
```
|
91 |
+
|
92 |
+
Note that for the next line to display a plot `hvplot.pandas` has to be imported, which we did at the beginning of this notebook.
|
93 |
+
|
94 |
+
|
95 |
+
```python
|
96 |
+
df.hvplot(**settings)
|
97 |
+
```
|
98 |
+
|
99 |
+
## Supported data inputs
|
100 |
+
|
101 |
+
The explorer was added in version `0.8.0` with support for Pandas DataFrames. Support for Xarray objects was added in version `0.9.0`.
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
ds = xr.tutorial.open_dataset('air_temperature')
|
106 |
+
|
107 |
+
hvplot.explorer(ds, x='lon', y='lat')
|
108 |
+
```
|
109 |
+
|
110 |
+
## Geographic options
|
111 |
+
|
112 |
+
When `geoviews` is installed, it's also possible to geographically reference the data.
|
113 |
+
|
114 |
+
|
115 |
+
```python
|
116 |
+
hvexplorer = hvplot.explorer(ds, x='lon', y='lat', geo=True)
|
117 |
+
hvexplorer.geographic.param.update(crs='PlateCarree', tiles='CartoDark', global_extent=False)
|
118 |
+
hvexplorer
|
119 |
+
```
|
120 |
+
|
121 |
+
## Conclusion
|
122 |
+
|
123 |
+
The *Explorer* makes it very easy to quickly spin up a small application in a notebook with which you can explore your data, generate the visualization that you want, record it in a simple way, and keep going with your analysis!
|
hvplot_docs/Exporting_and_Archiving.md
ADDED
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Exporting and Archiving
|
2 |
+
|
3 |
+
Most of the other user guides show you how to use HoloViews for interactive, exploratory visualization of your data, while the [Applying Customizations](03-Applying_Customizations.ipynb) user guide shows how to use HoloViews completely non-interactively, generating and rendering images directly to disk using `hv.save`. In this notebook, we show how HoloViews works together with the Jupyter Notebook to establish a fully interactive yet *also* fully reproducible scientific or engineering workflow for generating reports or publications. That is, as you interactively explore your data and build visualizations in the notebook, you can automatically generate and export them as figures that will feed directly into your papers or web pages, along with records of how those figures were generated and even storing the actual data involved so that it can be re-analyzed later.
|
4 |
+
|
5 |
+
|
6 |
+
|
7 |
+
|
8 |
+
```python
|
9 |
+
import holoviews as hv
|
10 |
+
from holoviews import opts
|
11 |
+
from holoviews.operation import contours
|
12 |
+
hv.extension('matplotlib')
|
13 |
+
```
|
14 |
+
|
15 |
+
## Exporting specific files
|
16 |
+
|
17 |
+
During interactive exploration in the Jupyter Notebook, your results are always visible within the notebook itself, but you can explicitly request that any visualization is also exported to an external file on disk:
|
18 |
+
|
19 |
+
|
20 |
+
```python
|
21 |
+
penguins = hv.RGB.load_image('../assets/penguins.png')
|
22 |
+
hv.save(penguins, 'penguin_plot.png', fmt='png')
|
23 |
+
penguins
|
24 |
+
```
|
25 |
+
|
26 |
+
This mechanism can be used to provide a clear link between the steps for generating the figure, and the file on disk. You can now load the exported PNG image back into HoloViews, if you like, using ``hv.RGB.load_image`` although the result would be a bit confusing due to the nested axes.
|
27 |
+
|
28 |
+
The ``fig="png"`` part of the ``hv.save`` function call above specified that the file should be saved in PNG format, which is useful for posting on web pages or editing in raster-based graphics programs. Note that `hv.save` also accepts `HoloMap`s which can be saved to formats such as ``'scrubber'``, ``'widgets'`` or even ``'gif'`` or ``'mp4'`` (if the necessary matplotlib dependencies are available).
|
29 |
+
|
30 |
+
If the file extension is part of the filename, that will automatically be used to set the format. Conversely, if the format is explicitly specified, then the extension does not have to be part of the filename (and any filename extension that is provided will be ignored). Sometimes the two pieces of information are independent: for instance, a filename ending in `.html` can support either the `'widgets'` or `'scrubber'` formats.
|
31 |
+
|
32 |
+
For a publication, you will usually want to select SVG format because this vector format preserves the full resolution of all text and drawing elements. SVG files can be be used in some document preparation programs directly (e.g. [LibreOffice](http://www.libreoffice.org/)), and can easily be converted and manipulated in vector graphics editors such as [Inkscape](https://inkscape.org).
|
33 |
+
|
34 |
+
## Exporting notebooks
|
35 |
+
|
36 |
+
The ``hv.save`` function is useful when you want specific plots saved into specific files. Often, however, a notebook will contain an entire suite of results contained in multiple different cells, and manually specifying these cells and their filenames is error-prone, with a high likelihood of accidentally creating multiple files with the same name or using different names in different notebooks for the same objects.
|
37 |
+
|
38 |
+
To make the exporting process easier for large numbers of outputs, as well as more predictable, HoloViews also offers a powerful automatic notebook exporting facility, creating an archive of all your results. Automatic export is very useful in the common case of having a notebook that contains a series of figures to be used in a report or publication, particularly if you are repeatedly re-running the notebook as you finalize your results, and want the full set of current outputs to be available to an external document preparation system.
|
39 |
+
|
40 |
+
The advantage of using this archival system over simply converting the notebook to a static HTML file with nbconvert is that you can generate a collection of individual file assets in one or more desired file formats.
|
41 |
+
|
42 |
+
To turn on automatic adding of your files to the export archive, run ``hv.archive.auto()``:
|
43 |
+
|
44 |
+
|
45 |
+
```python
|
46 |
+
hv.archive.auto()
|
47 |
+
```
|
48 |
+
|
49 |
+
This object's behavior can be customized extensively; try pressing tab within the parentheses for a list of options, which are described more fully below.
|
50 |
+
|
51 |
+
By default, the output will go into a directory with the same name as your notebook, and the names for each object will be generated from the groups and labels used by HoloViews. Objects that contain HoloMaps are not exported by default, since those are usually rendered as animations that are not suitable for inclusion in publications, but you can change it to ``.auto(holomap='gif')`` if you want those as well.
|
52 |
+
|
53 |
+
### Adding files to an archive
|
54 |
+
|
55 |
+
To see how the auto-exporting works, let's define a few HoloViews objects:
|
56 |
+
|
57 |
+
|
58 |
+
```python
|
59 |
+
penguins[:,:,'R'].relabel("Red") + penguins[:,:,'G'].relabel("Green") + penguins[:,:,'B'].relabel("Blue")
|
60 |
+
```
|
61 |
+
|
62 |
+
|
63 |
+
```python
|
64 |
+
penguins * hv.Arrow(0.15, 0.3, 'Penguin', '>')
|
65 |
+
```
|
66 |
+
|
67 |
+
|
68 |
+
```python
|
69 |
+
cs = contours(penguins[:,:,'R'], levels=[0.10,0.80])
|
70 |
+
overlay = penguins[:, :, 'R'] * cs
|
71 |
+
overlay.opts(
|
72 |
+
opts.Contours(linewidth=1.3, cmap='Autumn'),
|
73 |
+
opts.Image(cmap="gray"))
|
74 |
+
```
|
75 |
+
|
76 |
+
We can now list what has been captured, along with the names that have been generated:
|
77 |
+
|
78 |
+
|
79 |
+
```python
|
80 |
+
hv.archive.contents()
|
81 |
+
```
|
82 |
+
|
83 |
+
Here each object has resulted in two files, one in SVG format and one in Python "pickle" format (which appears as a ``zip`` file with extension ``.hvz`` in the listing). We'll ignore the pickle files for now, focusing on the SVG images.
|
84 |
+
|
85 |
+
The name generation code for these files is heavily customizable, but by default it consists of a list of dimension values and objects:
|
86 |
+
|
87 |
+
``{dimension},{dimension},...{group}-{label},{group}-{label},...``.
|
88 |
+
|
89 |
+
The ``{dimension}`` shows what dimension values are included anywhere in this object, if it contains any high-level ``Dimensioned`` objects like ``HoloMap``, ``NdOverlay``, and ``GridSpace``. Of course, nearly all HoloViews objects have dimensions, such as ``x`` and ``y`` in this case, but those dimensions are not used in the filenames because they are explicitly shown in the plots; only the top-level dimensions are used (those that determine which plot this is, not those that are shown in the plot itself.)
|
90 |
+
|
91 |
+
The ``{group}-{label}`` information lists the names HoloViews uses for default titles and for attribute access for the various objects that make up a given displayed object. E.g. the first SVG image in the list is a ``Layout`` of the three given ``Image`` objects, and the second one is an ``Overlay`` of an ``RGB`` object and an ``Arrow`` object. This information usually helps distinguish one plot from another, because they will typically be plots of objects that have different labels.
|
92 |
+
|
93 |
+
If the generated names are not unique, a numerical suffix will be added to make them unique. A maximum filename length is enforced, which can be set with ``hv.archive.max_filename=``_num_.
|
94 |
+
|
95 |
+
If you prefer a fixed-width filename, you can use a hash for each name instead (or in addition), where ``:.8`` specifies how many characters to keep from the hash:
|
96 |
+
|
97 |
+
|
98 |
+
```python
|
99 |
+
hv.archive.filename_formatter="{SHA:.8}"
|
100 |
+
cs
|
101 |
+
```
|
102 |
+
|
103 |
+
|
104 |
+
```python
|
105 |
+
hv.archive.contents()
|
106 |
+
```
|
107 |
+
|
108 |
+
You can see that the newest files added have the shorter, fixed-width format, though the names are no longer meaningful. If the ``filename_formatter`` had been set from the start, all filenames would have been of this type, which has both practical advantages (short names, all the same length) and disadvantages (no semantic clue about the contents).
|
109 |
+
|
110 |
+
### Generated indexes
|
111 |
+
|
112 |
+
In addition to the files that were added to the archive for each of the cell outputs above, the archive exporter will also add an ``index.html`` file with a static copy of the notebook, with each cell labelled with the filename used to save it once `hv.archive.export()` is called (you can verify this for yourself after this call is executed below). This HTML file acts as a definitive index to your results, showing how they were generated and where they were exported on disk.
|
113 |
+
|
114 |
+
The exporter will also add a cleared, runnable copy of the notebook ``index.ipynb`` (with output deleted), so that you can later regenerate all of the output, with changes if necessary.
|
115 |
+
|
116 |
+
The exported archive will thus be a complete set of your results, along with a record of how they were generated, plus a recipe for regenerating them -- i.e., fully reproducible research! This HTML file and .ipynb file can the be submitted as supplemental materials for a paper, allowing any reader to build on your results, or it can just be kept privately so that future collaborators can start where this research left off.
|
117 |
+
|
118 |
+
### Adding your own data to the archive
|
119 |
+
|
120 |
+
Of course, your results may depend on a lot of external packages, libraries, code files, and so on, which will not automatically be included or listed in the exported archive.
|
121 |
+
|
122 |
+
Luckily, the archive support is very general, and you can add any object to it that you want to be exported along with your output. For instance, you can store arbitrary metadata of your choosing, such as version control information, here as a JSON-format text file:
|
123 |
+
|
124 |
+
|
125 |
+
```python
|
126 |
+
import json
|
127 |
+
hv.archive.add(filename='metadata.json',
|
128 |
+
data=json.dumps({'repository':'git@github.com:holoviz/holoviews.git',
|
129 |
+
'commit':'437e8d69'}), info={'mime_type':'text/json'})
|
130 |
+
```
|
131 |
+
|
132 |
+
The new file can now be seen in the contents listing:
|
133 |
+
|
134 |
+
|
135 |
+
```python
|
136 |
+
hv.archive.contents()
|
137 |
+
```
|
138 |
+
|
139 |
+
You can get a more direct list of filenames using the ``listing`` method:
|
140 |
+
|
141 |
+
|
142 |
+
```python
|
143 |
+
listing = hv.archive.listing()
|
144 |
+
listing
|
145 |
+
```
|
146 |
+
|
147 |
+
In this way, you should be able to automatically generate output files, with customizable filenames, storing any data or metadata you like along with them so that you can keep track of all the important information for reproducing these results later.
|
148 |
+
|
149 |
+
### Controlling the behavior of ``hv.archive``
|
150 |
+
|
151 |
+
The ``hv.archive`` object provides numerous parameters that can be changed. You can e.g.:
|
152 |
+
|
153 |
+
- output the whole directory to a single compressed ZIP or tar archive file (e.g. ``hv.archive.param.update(pack=False, archive_format='zip')`` or ``archive_format='tar'``)
|
154 |
+
|
155 |
+
- generate a new directory or archive every time the notebook is run (``hv.archive.uniq_name=True``); otherwise the old output directory is erased each time
|
156 |
+
|
157 |
+
- choose your own name for the output directory or archive (e.g. ``hv.archive.export_name="{timestamp}"``)
|
158 |
+
|
159 |
+
- change the format of the optional timestamp (e.g. to retain snapshots hourly, ``archive.param.update(export_name="{timestamp}", timestamp_format="%Y_%m_%d-%H")``)
|
160 |
+
|
161 |
+
- select PNG output, at a specified rendering resolution: ``hv.archive.exporters=[hv.renderer('bokeh').instance(size=50)])
|
162 |
+
``
|
163 |
+
|
164 |
+
These options and any others listed above can all be set in the ``hv.archive.auto()`` call at the start, for convenience and to ensure that they apply to all of the files that are added.
|
165 |
+
|
166 |
+
### Writing the archive to disk
|
167 |
+
|
168 |
+
To actually write the files you have stored in the archive to disk, you need to call ``export()`` after any cell that might contain computation-intensive code. Usually it's best to do so as the last or nearly last cell in your notebook, though here we do it earlier because we wanted to show how to use the exported files.
|
169 |
+
|
170 |
+
|
171 |
+
```python
|
172 |
+
hv.archive.export()
|
173 |
+
```
|
174 |
+
|
175 |
+
Shortly after the ``export()`` command has been executed, the output should be available as a directory on disk, by default in the same directory as the notebook file, named with the name of the notebook:
|
176 |
+
|
177 |
+
|
178 |
+
```python
|
179 |
+
import os
|
180 |
+
os.getcwd()
|
181 |
+
if os.path.exists(hv.archive.notebook_name):
|
182 |
+
print('\n'.join(sorted(os.listdir(hv.archive.notebook_name))))
|
183 |
+
```
|
184 |
+
|
185 |
+
For technical reasons to do with how the IPython Notebook interacts with JavaScript, if you use the Jupyter Notebook command ``Run all``, the ``hv.archive.export()`` command is not actually executed when the cell with that call is encountered during the run. Instead, the ``export()`` is queued until after the final cell in the notebook has been executed. This asynchronous execution has several awkward but not serious consequences:
|
186 |
+
|
187 |
+
- It is not possible for the ``export()`` cell to show whether any errors were encountered during exporting, because these will not occur until after the notebook has completed processing. To see any errors, you can run ``hv.archive.last_export_status()`` separately, *after* the ``Run all`` has completed. E.g. just press shift-[Enter] in the following cell, which will tell you whether the previous export was successful.
|
188 |
+
|
189 |
+
- If you use ``Run all``, the directory listing ``os.listdir()`` above will show the results from the *previous* time this notebook was run, since it executes before the export. Again, you can use shift-[Enter] to update the data once complete.
|
190 |
+
|
191 |
+
- The ``Export name:`` in the output of ``hv.archive.export()`` will not always show the actual name of the directory or archive that will be created. In particular, it may say ``{notebook}``, which when saving will actually expand to the name of your Jupyter Notebook.
|
192 |
+
|
193 |
+
|
194 |
+
```python
|
195 |
+
hv.archive.last_export_status()
|
196 |
+
```
|
197 |
+
|
198 |
+
### Accessing your saved data
|
199 |
+
|
200 |
+
By default, HoloViews saves not only your rendered plots (PNG, SVG, etc.), but also the actual HoloViews objects that the plots visualize, which contain all your actual data. The objects are stored in compressed Python pickle files (``.hvz``), which are visible in the directory listings above but have been ignored until now. The plots are what you need for writing a document, but the raw data is is a crucial record to keep as well. For instance, you now can load in the HoloViews object, and manipulate it just as you could when it was originally defined. E.g. we can re-load our ``Levels`` ``Overlay`` file, which has the contours overlaid on top of the image, and easily pull out the underlying ``Image`` object:
|
201 |
+
|
202 |
+
|
203 |
+
```python
|
204 |
+
import os
|
205 |
+
from holoviews.core.io import Unpickler
|
206 |
+
c, a = None,None
|
207 |
+
hvz_file = [f for f in listing if f.endswith('hvz')][0]
|
208 |
+
path = os.path.join(hv.archive.notebook_name, hvz_file)
|
209 |
+
|
210 |
+
if os.path.isfile(path):
|
211 |
+
print('Unpickling {filename}'.format(filename=hvz_file))
|
212 |
+
obj = Unpickler.load(open(path,"rb"))
|
213 |
+
print(obj)
|
214 |
+
else:
|
215 |
+
print('Could not find file {path}'.format(path=path))
|
216 |
+
print('Current directory is {cwd}'.format(cwd=os.getcwd()))
|
217 |
+
print('Containing files and directories: {listing}'.format(listing=os.listdir(os.getcwd())))
|
218 |
+
```
|
219 |
+
|
220 |
+
Given the ``Image``, you can also access the underlying array data, because HoloViews objects are simply containers for your data and associated metadata. This means that years from now, as long as you can still run HoloViews, you can now easily re-load and explore your data, plotting it entirely different ways or running different analyses, even if you no longer have any of the original code you used to generate the data. All you need is HoloViews, which is permanently archived on GitHub and is fully open source and thus should always remain available. Because the data is stored conveniently in the archive alongside the figure that was published, you can see immediately which file corresponds to the data underlying any given plot in your paper, and immediately start working with the data, rather than laboriously trying to reconstruct the data from a saved figure.
|
221 |
+
|
222 |
+
If you do not want the pickle files, you can of course turn them off if you prefer, by changing ``hv.archive.auto()`` to:
|
223 |
+
|
224 |
+
```python
|
225 |
+
hv.archive.auto(exporters=[hv.renderer('matplotlib').instance(holomap=None)])
|
226 |
+
```
|
227 |
+
|
228 |
+
Here, the exporters list has been updated to include the usual default exporters *without* the `Pickler` exporter that would usually be included.
|
229 |
+
|
230 |
+
## Using HoloViews to do reproducible research
|
231 |
+
|
232 |
+
The export options from HoloViews help you establish a feasible workflow for doing reproducible research: starting from interactive exploration, either export specific files with ``hv.save``, or enable ``hv.archive.auto()``, which will store a copy of your notebook and its output ready for inclusion in a document but retaining the complete recipe for reproducing the results later.
|
233 |
+
|
234 |
+
### Why reproducible research matters
|
235 |
+
|
236 |
+
To understand why these capabilities are important, let's consider the process by which scientific results are typically generated and published without HoloViews. Scientists and engineers use a wide variety of data-analysis tools, ranging from GUI-based programs like Excel spreadsheets, mixed GUI/command-line programs like Matlab, or purely scriptable tools like matplotlib or bokeh. The process by which figures are created in any of these tools typically involves copying data from its original source, selecting it, transforming it, choosing portions of it to put into a figure, choosing the various plot options for a subfigure, combining different subfigures into a complete figure, generating a publishable figure file with the full figure, and then inserting that into a report or publication.
|
237 |
+
|
238 |
+
If using GUI tools, often the final figure is the only record of that process, and even just a few weeks or months later a researcher will often be completely unable to say precisely how a given figure was generated. Moreover, this process needs to be repeated whenever new data is collected, which is an error-prone and time-consuming process. The lack of records is a serious problem for building on past work and revisiting the assumptions involved, which greatly slows progress both for individual researchers and for the field as a whole. Graphical environments for capturing and replaying a user's GUI-based workflow have been developed, but these have greatly restricted the process of exploration, because they only support a few of the many analyses required, and thus they have rarely been successful in practice. With GUI tools it is also very difficult to "curate" the sequence of steps involved, i.e., eliminating dead ends, speculative work, and unnecessary steps, with a goal of showing the clear path from incoming data to a final figure.
|
239 |
+
|
240 |
+
In principle, using scriptable or command-line tools offers the promise of capturing the steps involved, in a form that can be curated. In practice, however, the situation is often no better than with GUI tools, because the data is typically taken through many manual steps that culminate in a published figure, and without a laboriously manually created record of what steps are involved, the provenance of a given figure remains unknown. Where reproducible workflows are created in this way, they tend to be "after the fact", as an explicit exercise to accompany a publication, and thus (a) they are rarely done, (b) they are very difficult to do if any of the steps were not recorded originally.
|
241 |
+
|
242 |
+
A Jupyter notebook helps significantly to make the scriptable-tools approach viable, by recording both code and the resulting output, and can thus in principle act as a record for establishing the full provenance of a figure. But because typical plotting libraries require so much plotting-specific code before any plot is visible, the notebook quickly becomes unreadable. To make notebooks readable, researchers then typically move the plotting code for a specific figure to some external file, which then drifts out of sync with the notebook so that the notebook no longer acts as a record of the link between the original data and the resulting figure.
|
243 |
+
|
244 |
+
HoloViews provides the final missing piece in this approach, by allowing researchers to work directly with their data interactively in a notebook, using small amounts of code that focus on the data and analyses rather than plotting code, yet showing the results directly alongside the specification for generating them. This user guide will describe how use a Jupyter notebook with HoloViews to export your results in a way that preserves the information about how those results were generated, providing a clear chain of provenance and making reproducible research practical at last.
|
245 |
+
|
246 |
+
For more information on how HoloViews can help build a reproducible workflow, see our [2015 paper on using HoloViews for reproducible research](http://conference.scipy.org/proceedings/scipy2015/pdfs/jean-luc_stevens.pdf).
|
hvplot_docs/Geographic_Data.md
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import xarray as xr
|
3 |
+
import hvplot.pandas # noqa
|
4 |
+
import hvplot.xarray # noqa
|
5 |
+
import cartopy.crs as ccrs
|
6 |
+
|
7 |
+
from bokeh.sampledata.airport_routes import airports
|
8 |
+
```
|
9 |
+
|
10 |
+
## Installation
|
11 |
+
|
12 |
+
The plot API also has support for geographic data built on top of Cartopy and GeoViews. Both can be installed using conda with:
|
13 |
+
|
14 |
+
conda install geoviews
|
15 |
+
|
16 |
+
or with pip:
|
17 |
+
|
18 |
+
pip install geoviews
|
19 |
+
|
20 |
+
## Usage
|
21 |
+
|
22 |
+
Only certain hvPlot types support geographic coordinates, currently including: 'points', 'polygons', 'paths', 'image', 'quadmesh', 'contour', and 'contourf'. As an initial example, consider a dataframe of all US airports (including military bases overseas):
|
23 |
+
|
24 |
+
|
25 |
+
```python
|
26 |
+
airports.head(3)
|
27 |
+
```
|
28 |
+
|
29 |
+
### Plotting points
|
30 |
+
|
31 |
+
If we want to overlay our data on geographic maps or reproject it into a geographic plot, we can set ``geo=True``, which declares that the data will be plotted in a geographic coordinate system. The default coordinate system is the ``PlateCarree`` projection, i.e., raw longitudes and latitudes. If the data is in another coordinate system, you will need to [declare an explicit ``crs``](#Declaring-a-CRS) as an argument, in which case `geo=True` is assumed. Once hvPlot knows that your data is in geo coordinates, you can use the ``tiles`` option to overlay a the plot on top of map tiles.
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
airports.hvplot.points('Longitude', 'Latitude', geo=True, color='red', alpha=0.2,
|
36 |
+
xlim=(-180, -30), ylim=(0, 72), tiles='ESRI')
|
37 |
+
```
|
38 |
+
|
39 |
+
### Declaring a CRS
|
40 |
+
|
41 |
+
To declare a geographic plot we have to supply a ``cartopy.crs.CRS`` (or coordinate reference system). Coordinate reference systems are described in the [GeoViews documentation](https://geoviews.org/user_guide/Projections.html) and the full list of available CRSs is in the [cartopy documentation](https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html).
|
42 |
+
|
43 |
+
### Geopandas
|
44 |
+
|
45 |
+
Since a GeoPandas ``DataFrame`` is just a Pandas DataFrames with additional geographic information, it inherits the ``.hvplot`` method. We can thus easily load shapefiles and plot them on a map:
|
46 |
+
|
47 |
+
|
48 |
+
```python
|
49 |
+
import geopandas as gpd
|
50 |
+
|
51 |
+
cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
|
52 |
+
|
53 |
+
cities.hvplot(global_extent=True, frame_height=450, tiles=True)
|
54 |
+
```
|
55 |
+
|
56 |
+
The GeoPandas support allows plotting ``GeoDataFrames`` containing ``'Point'``, ``'Polygon'``, ``'LineString'`` and ``'LineRing'`` geometries, but not ones containing a mixture of different geometry types. Calling ``.hvplot`` will automatically figure out the geometry type to plot, but it also possible to call ``.hvplot.points``, ``.hvplot.polygons``, and ``.hvplot.paths`` explicitly.
|
57 |
+
|
58 |
+
To draw multiple GeoDataFrames onto the same plot, use the ``*`` operator:
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
|
63 |
+
|
64 |
+
world.hvplot(geo=True) * cities.hvplot(geo=True, color='orange')
|
65 |
+
```
|
66 |
+
|
67 |
+
It is possible to declare a specific column to use as color with the ``c`` keyword:
|
68 |
+
|
69 |
+
|
70 |
+
```python
|
71 |
+
world.hvplot(geo=True) + world.hvplot(c='continent', geo=True)
|
72 |
+
```
|
73 |
+
|
74 |
+
## Spatialpandas
|
75 |
+
|
76 |
+
Spatialpandas is another powerful library for working with geometries and is optimized for rendering with datashader, making it possible to plot millions of individual geometries very quickly:
|
77 |
+
|
78 |
+
|
79 |
+
```python
|
80 |
+
import spatialpandas as spd
|
81 |
+
|
82 |
+
spd_world = spd.GeoDataFrame(world)
|
83 |
+
|
84 |
+
spd_world.hvplot(datashade=True, project=True, aggregator='count_cat', c='continent', color_key='Category10')
|
85 |
+
```
|
86 |
+
|
87 |
+
### Declaring an output projection
|
88 |
+
|
89 |
+
The ``crs=`` argument specifies the *input* projection, i.e. it declares how to interpret the incoming data values. You can independently choose any *output* projection, i.e. how you want to map the data points onto the screen for display, using the ``projection=`` argument. After loading the same temperature dataset explored in the [Gridded Data](Gridded_Data.ipynb) section, the data can be displayed on an Orthographic projection:
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
air_ds = xr.tutorial.open_dataset('air_temperature').load()
|
94 |
+
|
95 |
+
air_ds.hvplot.quadmesh(
|
96 |
+
'lon', 'lat', 'air', projection=ccrs.Orthographic(-90, 30),
|
97 |
+
global_extent=True, frame_height=540, cmap='viridis',
|
98 |
+
coastline=True
|
99 |
+
)
|
100 |
+
```
|
101 |
+
|
102 |
+
If you don't need to pass any keyword arguments to a given projection and you don't have cartopy.crs (ccrs) imported, you can use the string representation: e.g. ``'LambertConformal'`` instead of ``ccrs.LambertConformal()``. Note that it is case sensitive!
|
103 |
+
|
104 |
+
|
105 |
+
```python
|
106 |
+
air_ds.hvplot.quadmesh(
|
107 |
+
'lon', 'lat', 'air', projection='LambertConformal',
|
108 |
+
)
|
109 |
+
```
|
110 |
+
|
111 |
+
Note that when displaying raster data in a projection other than the one in which the data is stored, it is more accurate to render it as a ``quadmesh`` rather than an ``image``. As you can see above, a QuadMesh will project each original bin or pixel into the correct non-rectangular shape determined by the projection, accurately showing the geographic extent covered by each sample. An Image, on the other hand, will always be rectangularly aligned in the 2D plane, which requires warping and resampling the data in a way that allows efficient display but loses accuracy at the pixel level. Unfortunately, rendering a large QuadMesh using Bokeh can be very slow, but there are two useful alternatives for datasets too large to be practical as native QuadMeshes.
|
112 |
+
|
113 |
+
The first is using the ``rasterize`` or ``datashade`` options to regrid the data before rendering it, i.e., rendering the data on the backend and then sending a more efficient image-based representation to the browser. One thing to note when using these operations is that it may be necessary to project the data **before** rasterizing it, e.g. to address wrapping issues. To do this provide ``project=True``, which will project the data before it is rasterized (this also works for other types and even when not using these operations). Another reason why this is important when rasterizing the data is that if the CRS of the data does not match the displayed projection, all the data will be projected every time you zoom or pan, which can be very slow. Deciding whether to ``project`` is therefore a tradeoff between projecting the raw data ahead of time or accepting the overhead on dynamic zoom and pan actions.
|
114 |
+
|
115 |
+
|
116 |
+
```python
|
117 |
+
rasm = xr.tutorial.open_dataset('rasm').load()
|
118 |
+
|
119 |
+
|
120 |
+
|
121 |
+
rasm.hvplot.quadmesh(
|
122 |
+
'xc', 'yc', crs=ccrs.PlateCarree(), projection=ccrs.PlateCarree(),
|
123 |
+
ylim=(0, 90), cmap='viridis', project=True, geo=True,
|
124 |
+
rasterize=True, coastline=True, frame_width=800, dynamic=False,
|
125 |
+
)
|
126 |
+
```
|
127 |
+
|
128 |
+
Another option that's still relatively slow for larger data but avoids sending large data into your browser is to plot the data using ``contour`` and ``contourf`` visualizations, generating a line or filled contour with a discrete number of levels:
|
129 |
+
|
130 |
+
|
131 |
+
```python
|
132 |
+
rasm.hvplot.contourf(
|
133 |
+
'xc', 'yc', crs=ccrs.PlateCarree(), projection=ccrs.PlateCarree(),
|
134 |
+
ylim=(0, 90), frame_width=800, cmap='viridis', levels=10,
|
135 |
+
coastline=True
|
136 |
+
)
|
137 |
+
```
|
138 |
+
|
139 |
+
As you can see, hvPlot makes it simple to work with geographic data visually. For more complex plot types and additional details, see the [GeoViews](https://geoviews.org) documentation.
|
140 |
+
|
141 |
+
## Geographic options
|
142 |
+
|
143 |
+
The API provides various geo-specific options:
|
144 |
+
|
145 |
+
- ``coastline`` (default=False): Whether to display a coastline on top of the plot, setting ``coastline='10m'/'50m'/'110m'`` specifies a specific scale
|
146 |
+
- ``crs`` (default=None): Coordinate reference system of the data specified as Cartopy CRS object, proj.4 string or EPSG code
|
147 |
+
- ``features`` features (default=None): A list of features or a dictionary of features and the scale at which to render it. Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'. Available scales include '10m'/'50m'/'110m'.
|
148 |
+
- ``geo`` (default=False): Whether the plot should be treated as geographic (and assume PlateCarree, i.e. lat/lon coordinates)
|
149 |
+
- ``global_extent`` (default=False): Whether to expand the plot extent to span the whole globe
|
150 |
+
- ``project`` (default=False): Whether to project the data before plotting (adds initial overhead but avoids projecting data when plot is dynamically updated)
|
151 |
+
- ``tiles`` (default=False): Whether to overlay the plot on a tile source. Tiles sources can be selected by name, the default is 'Wikipedia'.
|
152 |
+
Other options are: 'CartoDark', 'CartoEco', 'CartoLight', 'CartoMidnight', 'EsriImagery', 'EsriNatGeo', 'EsriReference''EsriTerrain', 'EsriUSATopo', 'OSM', 'StamenLabels', 'StamenTerrain', 'StamenTerrainRetina', 'StamenToner', 'StamenTonerBackground', 'StamenWatercolor'. Stamen tile sources require a Stadia account when not running locally; see [stadiamaps.com](https://stadiamaps.com/).
|
hvplot_docs/Geometry_Data.md
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
In addition to the two main types of data, namely tabular/columnar and gridded data HoloViews also provide extensible interfaces to represent path geometry data. Specifically it has three main element types used to representing different types of geometries. In this section we will cover the HoloViews data model for representing different kinds of geometries.
|
2 |
+
|
3 |
+
There are many different ways of representing path geometries but HoloViews' data model is oriented on GEOS geometry definitions and allows faithfully round-tripping data between its element types and GEOS geometry definitions such as ``LinearString``, ``Polygon``, ``MultiLineString`` and ``MultiPolygon`` geometries (even if this is not implemented in HoloViews itself). HoloViews defines a dictionary based format for the geometries but also supports [spatialpandas](https://github.com/holoviz/spatialpandas), which is a highly optimized implementation similar to [geopandas](https://github.com/geopandas/geopandas/) but without the heavy geo-dependencies such as shapely and fiona. [GeoViews](https://geoviews.org/user_guide/Geometries.html) supports both geopandas and raw shapely geometries directly.
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import numpy as np
|
8 |
+
import holoviews as hv
|
9 |
+
from holoviews import opts
|
10 |
+
|
11 |
+
hv.extension('bokeh')
|
12 |
+
```
|
13 |
+
|
14 |
+
## Representing paths
|
15 |
+
|
16 |
+
The ``Path`` element represents a collection of path geometries with optional associated values. Each path geometry may be split into sub-geometries on NaN-values and may be associated with scalar values or array values varying along its length. In analogy to GEOS geometry types a Path is a collection of LineString and MultiLineString geometries with associated values.
|
17 |
+
|
18 |
+
While other formats can be supported through extensible interfaces (e.g. geopandas and shapely objects in GeoViews), natively HoloViews provides support for representing paths as one or more columnar data-structures including arrays, dataframes and dictionaries of column arrays and scalars. A simple path geometry may therefore be drawn using:
|
19 |
+
|
20 |
+
|
21 |
+
```python
|
22 |
+
hv.Path({'x': [1, 2, 3, 4, 5], 'y': [0, 0, 1, 1, 2]}, ['x', 'y'])
|
23 |
+
```
|
24 |
+
|
25 |
+
Here the dictionary of x- and y-coordinates could also be an NumPy array with two columns or a dataframe with 'x' and 'y' columns.
|
26 |
+
|
27 |
+
To draw multiple paths the data-structures can be wrapped in a list. Additionally, it is also possible to associate a value with each path by declaring it as a value dimension:
|
28 |
+
|
29 |
+
|
30 |
+
```python
|
31 |
+
p = hv.Path([{'x': [1, 2, 3, 4, 5], 'y': [0, 0, 1, 1, 2], 'value': 0},
|
32 |
+
{'x': [5, 4, 3, 2, 1], 'y': [2, 2, 1, 1, 0], 'value': 1}], vdims='value').opts(color='value')
|
33 |
+
p
|
34 |
+
```
|
35 |
+
|
36 |
+
#### Multi-geometry
|
37 |
+
|
38 |
+
Splitting the geometries in this way allows assigning separate values to each geometry, however often multiple geometries share the same value in which case it may be desirable to represent them as a multi-geometry by combining the coordinates and separating them by a NaN value:
|
39 |
+
|
40 |
+
|
41 |
+
```python
|
42 |
+
hv.Path([{'x': [1, 2, 3, 4, 5, np.nan, 5, 4, 3, 2, 1],
|
43 |
+
'y': [0, 0, 1, 1, 2, np.nan, 2, 2, 1, 1, 0], 'value': 0}],
|
44 |
+
vdims='value').opts(color='value')
|
45 |
+
```
|
46 |
+
|
47 |
+
This represents a more efficient format particularly when there are very many small geometries with the same value.
|
48 |
+
|
49 |
+
#### Scalar vs. continuously varying value dimensions
|
50 |
+
|
51 |
+
Unlike ``Contours`` which are limited to representing iso-contours or isoclines, i.e. a function of two variables which describes a curve along which the function has a constant value, a ``Path`` element may also have continuously varying values along its path. Below we will declare a path with a value that varies along its path:
|
52 |
+
|
53 |
+
|
54 |
+
```python
|
55 |
+
a, b, delta = 3, 5, np.pi/2.
|
56 |
+
|
57 |
+
vs = np.linspace(0, np.pi*2, 200)
|
58 |
+
xs = np.sin(a * vs + delta)
|
59 |
+
ys = np.sin(b * vs)
|
60 |
+
|
61 |
+
hv.Path([{'x': xs, 'y': ys, 'value': vs}], vdims='value').opts(
|
62 |
+
color='value', cmap='hsv')
|
63 |
+
```
|
64 |
+
|
65 |
+
Note that since not all data formats allow storing scalar values as actual scalars, 1D-arrays matching the length of the coordinates but with only one unique value are also considered scalar. For example the following is a valid ``Contours`` element despite the fact that the value dimension is not a scalar variable:
|
66 |
+
|
67 |
+
|
68 |
+
```python
|
69 |
+
hv.Contours([{'x': xs, 'y': ys, 'value': np.ones(200)}], vdims='value').opts(color='value')
|
70 |
+
```
|
71 |
+
|
72 |
+
## Representing Polygons
|
73 |
+
|
74 |
+
The ``Polygons`` element represents a collection of polygon geometries with associated scalar values. Each polygon geometry may be split into sub-geometries on NaN-values and may be associated with scalar values. In analogy to GEOS geometry types a ``Polygons`` element is a collection of Polygon and MultiPolygon geometries. Polygon geometries are defined as a set of coordinates describing the exterior bounding ring and any number of interior holes.
|
75 |
+
|
76 |
+
In summary ``Polygons`` can be represented in much the same way as ``Paths`` above but have a special reserved key to store the polygon interiors or 'holes'. The holes are stored as a list-of-lists of arrays. This nested format is necessary to unambiguously associate holes with the sub-geometries in a multi-geometry. In the simplest case of a single Polygon geometry the format looks like this:
|
77 |
+
|
78 |
+
|
79 |
+
```python
|
80 |
+
xs = [1, 2, 3]
|
81 |
+
ys = [2, 0, 7]
|
82 |
+
holes = [[[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]]]
|
83 |
+
|
84 |
+
hv.Polygons([{'x': xs, 'y': ys, 'holes': holes}])
|
85 |
+
```
|
86 |
+
|
87 |
+
The 'x' and 'y' coordinates represent the exterior of the Polygon and the list-of-list of holes defines two interior regions inside the polygon.
|
88 |
+
|
89 |
+
In a multi-Polygon arrangement where two Polygon geometries are separated by NaNs, the purpose of the nested format becomes a bit clearer. Here the polygon from above still has the two holes but the second polygon does not have any holes, which we declare with an empty list:
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
xs = [1, 2, 3, np.nan, 6, 7, 3]
|
94 |
+
ys = [2, 0, 7, np.nan, 7, 5, 2]
|
95 |
+
|
96 |
+
holes = [
|
97 |
+
[[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]],
|
98 |
+
[]
|
99 |
+
]
|
100 |
+
|
101 |
+
hv.Polygons([{'x': xs, 'y': ys, 'holes': holes}])
|
102 |
+
```
|
103 |
+
|
104 |
+
If a polygon has no holes at all the 'holes' key may be omitted entirely:
|
105 |
+
|
106 |
+
|
107 |
+
```python
|
108 |
+
hv.Polygons([{'x': xs, 'y': ys, 'holes': holes, 'value': 0},
|
109 |
+
{'x': [4, 6, 6], 'y': [0, 2, 1], 'value': 1},
|
110 |
+
{'x': [-3, -1, -6], 'y': [3, 2, 1], 'value': 3}], vdims='value')
|
111 |
+
```
|
112 |
+
|
113 |
+
## Accessing the data
|
114 |
+
|
115 |
+
To access the underlying data the geometry elements (``Path``/``Contours``/``Polygons``) implement a ``split`` method. By default it simply returns a list of elements, where each contains only one geometry:
|
116 |
+
|
117 |
+
|
118 |
+
```python
|
119 |
+
poly = hv.Polygons([
|
120 |
+
{'x': xs, 'y': ys, 'holes': holes, 'value': 0},
|
121 |
+
{'x': [4, 6, 6], 'y': [0, 2, 1], 'value': 1}
|
122 |
+
], vdims='value')
|
123 |
+
|
124 |
+
hv.Layout(poly.split())
|
125 |
+
```
|
126 |
+
|
127 |
+
Using the ``datatype`` argument the data may instead be returned in the desired format, e.g. 'dictionary', 'array' or 'dataframe'. Here we return the 'dictionary' format:
|
128 |
+
|
129 |
+
|
130 |
+
```python
|
131 |
+
poly.split(datatype='dictionary')
|
132 |
+
```
|
133 |
+
|
134 |
+
Note that this conversion may be lossy if the converted format has no way of representing 'holes' or other data.
|
hvplot_docs/Gridded_Data.md
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
hvPlot provides one API to explore data of many different types. Previous sections have exclusively worked with tabular data stored in pandas (or pandas-like) DataFrames. The other most common type of data are n-dimensional arrays. hvPlot aims to eventually support different array libraries but for now focuses on [xarray](https://xarray.pydata.org/en/stable/). XArray provides a convenient and very powerful wrapper to label the axis and coordinates of multi-dimensional (n-D) arrays. This user guide will cover how to leverage ``xarray`` and ``hvplot`` to visualize and explore data of different dimensionality ranging from simple 1D data, to 2D image-like data, to multi-dimensional cubes of data.
|
2 |
+
|
3 |
+
For these examples we’ll use the North American air temperature dataset:
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import xarray as xr
|
8 |
+
import hvplot.xarray # noqa
|
9 |
+
|
10 |
+
air_ds = xr.tutorial.open_dataset('air_temperature').load()
|
11 |
+
air = air_ds.air
|
12 |
+
air_ds
|
13 |
+
```
|
14 |
+
|
15 |
+
## 1D Plots
|
16 |
+
|
17 |
+
Selecting the data at a particular lat/lon coordinate we get a 1D dataset of air temperatures over time:
|
18 |
+
|
19 |
+
|
20 |
+
```python
|
21 |
+
air1d = air.sel(lat=40, lon=285)
|
22 |
+
air1d.hvplot()
|
23 |
+
```
|
24 |
+
|
25 |
+
Notice how the axes are already appropriately labeled, because xarray stores the metadata required. We can also further subselect the data and use `*` to overlay plots:
|
26 |
+
|
27 |
+
|
28 |
+
```python
|
29 |
+
air1d_sel = air1d.sel(time='2013-01')
|
30 |
+
air1d_sel.hvplot(color='purple') * air1d_sel.hvplot.scatter(marker='o', color='blue', size=15)
|
31 |
+
```
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
air.lat
|
36 |
+
```
|
37 |
+
|
38 |
+
### Selecting multiple
|
39 |
+
|
40 |
+
If we select multiple coordinates along one axis and plot a chart type, the data will automatically be split by the coordinate:
|
41 |
+
|
42 |
+
|
43 |
+
```python
|
44 |
+
air.sel(lat=[20, 40, 60], lon=285).hvplot.line()
|
45 |
+
```
|
46 |
+
|
47 |
+
To plot a different relationship we can explicitly request to display the latitude along the y-axis and use the ``by`` keyword to color each longitude (or 'lon') differently (note that this differs from the ``hue`` keyword xarray uses):
|
48 |
+
|
49 |
+
|
50 |
+
```python
|
51 |
+
air.sel(time='2013-02-01 00:00', lon=[280, 285]).hvplot.line(y='lat', by='lon', legend='top_right')
|
52 |
+
```
|
53 |
+
|
54 |
+
## 2D Plots
|
55 |
+
|
56 |
+
By default the ``DataArray.hvplot()`` method generates an image if the data is two-dimensional.
|
57 |
+
|
58 |
+
|
59 |
+
```python
|
60 |
+
air2d = air.sel(time='2013-06-01 12:00')
|
61 |
+
air2d.hvplot(width=400)
|
62 |
+
```
|
63 |
+
|
64 |
+
Alternatively we can also plot the same data using the ``contour`` and ``contourf`` methods, which provide a ``levels`` argument to control the number of iso-contours to draw:
|
65 |
+
|
66 |
+
|
67 |
+
```python
|
68 |
+
air2d.hvplot.contour(width=400, levels=20) + air2d.hvplot.contourf(width=400, levels=8)
|
69 |
+
```
|
70 |
+
|
71 |
+
## n-D Plots
|
72 |
+
|
73 |
+
If the data has more than two dimensions it will default to a histogram without providing it further hints:
|
74 |
+
|
75 |
+
|
76 |
+
```python
|
77 |
+
air.hvplot()
|
78 |
+
```
|
79 |
+
|
80 |
+
However we can tell it to apply a ``groupby`` along a particular dimension, allowing us to explore the data as images along that dimension with a slider:
|
81 |
+
|
82 |
+
|
83 |
+
```python
|
84 |
+
air.hvplot(groupby='time', width=500)
|
85 |
+
```
|
86 |
+
|
87 |
+
By default, for numeric types you'll get a slider and for non-numeric types you'll get a selector. Use ``widget_type`` and ``widget_location`` to control the look of the widget. To learn more about customizing widget behavior see [Widgets](Widgets.ipynb).
|
88 |
+
|
89 |
+
|
90 |
+
```python
|
91 |
+
air.hvplot(groupby='time', width=600, widget_type='scrubber', widget_location='bottom')
|
92 |
+
```
|
93 |
+
|
94 |
+
If we pick a different, lower dimensional plot type (such as a 'line') it will automatically apply a groupby over the remaining dimensions:
|
95 |
+
|
96 |
+
|
97 |
+
```python
|
98 |
+
air.hvplot.line(width=600)
|
99 |
+
```
|
100 |
+
|
101 |
+
## Statistical plots
|
102 |
+
|
103 |
+
Statistical plots such as histograms, kernel-density estimates, or violin and box-whisker plots aggregate the data across one or more of the coordinate dimensions. For instance, plotting a KDE provides a summary of all the air temperature values but we can, once again, use the ``by`` keyword to view each selected latitude (or 'lat') separately:
|
104 |
+
|
105 |
+
|
106 |
+
```python
|
107 |
+
air.sel(lat=[25, 50, 75]).hvplot.kde('air', by='lat', alpha=0.5)
|
108 |
+
```
|
109 |
+
|
110 |
+
Using the ``by`` keyword we can break down the distribution of the air temperature across one or more variables:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
air.hvplot.violin('air', by='lat', color='lat', cmap='Category20')
|
115 |
+
```
|
116 |
+
|
117 |
+
## Rasterizing
|
118 |
+
|
119 |
+
If you are plotting a large amount of data at once, you can consider using the hvPlot interface to [Datashader](https://datashader.org), which can be enabled simply by setting `rasterize=True`.
|
120 |
+
|
121 |
+
Note that by declaring that the data should not be grouped by another coordinate variable, i.e. by setting `groupby=[]`, we can plot all the datapoints, showing us the spread of air temperatures in the dataset:
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
air.hvplot.scatter('time', groupby=[], rasterize=True) *\
|
126 |
+
air.mean(['lat', 'lon']).hvplot.line('time', color='indianred')
|
127 |
+
```
|
128 |
+
|
129 |
+
Here we also overlaid a non-datashaded line plot of the average temperature at each time. If you enable the appropriate hover tool, the overlaid data supports hovering and zooming even in a static export such as on a web server or in an email, while the raw-data plot has been aggregated spatially before it is sent to the browser, and thus it has only the fixed spatial binning available at that time. If you have a live Python process, the raw data will be aggregated each time you pan or zoom, letting you see the entire dataset regardless of size.
|
hvplot_docs/Installing_and_Configuring.md
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Installing and Configuring Holoviews
|
2 |
+
|
3 |
+
HoloViews can be installed on any platform where [NumPy](http://numpy.org) and Python 3 are available.
|
4 |
+
|
5 |
+
That said, HoloViews is designed to work closely with many other libraries, which can make installation and configuration more complicated. This user guide page describes some of these less-common or not-required options that may be helpful for some users.
|
6 |
+
|
7 |
+
## Other installation options
|
8 |
+
|
9 |
+
The main [installation instructions](http://holoviews.org/#installation) should be sufficient for most users, but you may also want the [Matplotlib](http://matplotlib.org) and [Plotly](https://plot.ly/python/) backends, which are required for some of the examples:
|
10 |
+
|
11 |
+
conda install matplotlib plotly
|
12 |
+
|
13 |
+
HoloViews can also be installed using one of these `pip` commands:
|
14 |
+
|
15 |
+
pip install holoviews
|
16 |
+
pip install 'holoviews[recommended]'
|
17 |
+
pip install 'holoviews[extras]'
|
18 |
+
pip install 'holoviews[all]'
|
19 |
+
|
20 |
+
The first option installs just the bare library and the [NumPy](http://numpy.org) and [Param](https://github.com/holoviz/param) libraries, which is all you need on your system to generate and work with HoloViews objects without visualizing them. The other options install additional libraries that are often useful, with the `recommended` option being similar to the `conda` install command above.
|
21 |
+
|
22 |
+
Between releases, development snapshots are made available as conda packages:
|
23 |
+
|
24 |
+
conda install -c pyviz/label/dev holoviews
|
25 |
+
|
26 |
+
To get the very latest development version you can clone our git
|
27 |
+
repository and put it on the Python path:
|
28 |
+
|
29 |
+
git clone https://github.com/holoviz/holoviews.git
|
30 |
+
cd holoviews
|
31 |
+
pip install -e .
|
32 |
+
|
33 |
+
## JupyterLab configuration
|
34 |
+
|
35 |
+
To work with JupyterLab you will also need the HoloViews JupyterLab
|
36 |
+
extension:
|
37 |
+
|
38 |
+
```
|
39 |
+
conda install -c conda-forge jupyterlab
|
40 |
+
jupyter labextension install @pyviz/jupyterlab_pyviz
|
41 |
+
```
|
42 |
+
|
43 |
+
Once you have installed JupyterLab and the extension launch it with:
|
44 |
+
|
45 |
+
```
|
46 |
+
jupyter-lab
|
47 |
+
```
|
48 |
+
|
49 |
+
## ``hv.config`` settings
|
50 |
+
|
51 |
+
The default HoloViews installation will use the latest defaults and options available, which is appropriate for new users. If you want to work with code written for older HoloViews versions, you can use the top-level ``hv.config`` object to control various backwards-compatibility options:
|
52 |
+
|
53 |
+
* ``future_deprecations``: Enables warnings about future deprecations (introduced in 1.11).
|
54 |
+
* ``warn_options_call``: Warn when using the to-be-deprecated ``__call__`` syntax for specifying options, instead of the recommended ``.opts`` method.
|
55 |
+
|
56 |
+
It is recommended you set ``warn_options_call`` to ``True`` in your holoviews.rc file (see section below).
|
57 |
+
|
58 |
+
It is possible to set the configuration using `hv.config` directly:
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
import holoviews as hv
|
63 |
+
hv.config(future_deprecations=True)
|
64 |
+
```
|
65 |
+
|
66 |
+
However, because in some cases this configuration needs to be declared before the plotting extensions are imported, the recommended way of setting configuration options is:
|
67 |
+
|
68 |
+
|
69 |
+
```python
|
70 |
+
hv.extension('bokeh', config=dict(future_deprecations=True))
|
71 |
+
```
|
72 |
+
|
73 |
+
In addition to backwards-compatibility options, ``hv.config`` holds some global options:
|
74 |
+
|
75 |
+
* ``image_rtol``: The tolerance used to enforce regular sampling for regular, gridded data. Used to validate ``Image`` data.
|
76 |
+
|
77 |
+
This option allows you to set the ``rtol`` parameter of [``Image``](../reference/elements/bokeh/Image.ipynb) elements globally.
|
78 |
+
|
79 |
+
|
80 |
+
## Improved tab-completion
|
81 |
+
|
82 |
+
Both ``Layout`` and ``Overlay`` are designed around convenient tab-completion, with the expectation of upper-case names being listed first. In recent versions of Jupyter/IPython there has been a regression whereby the tab-completion is no longer case-sensitive. This can be fixed with:
|
83 |
+
|
84 |
+
|
85 |
+
```python
|
86 |
+
import holoviews as hv
|
87 |
+
hv.extension(case_sensitive_completion=True)
|
88 |
+
```
|
89 |
+
|
90 |
+
## The holoviews.rc file
|
91 |
+
|
92 |
+
HoloViews searches for the first rc file it finds in the following places (in order):
|
93 |
+
|
94 |
+
1. ``holoviews.rc`` in the parent directory of the top-level ``__init__.py`` file (useful for developers working out of the HoloViews git repo)
|
95 |
+
2. ``~/.holoviews.rc``
|
96 |
+
3. ``~/.config/holoviews/holoviews.rc``
|
97 |
+
|
98 |
+
The rc file location can be overridden via the ``HOLOVIEWSRC`` environment variable.
|
99 |
+
|
100 |
+
The rc file is a Python script, executed as HoloViews is imported. An example rc file to include various options discussed above might look like this:
|
101 |
+
|
102 |
+
```
|
103 |
+
import holoviews as hv
|
104 |
+
hv.config(warn_options_call=True)
|
105 |
+
hv.extension.case_sensitive_completion=True
|
106 |
+
```
|
107 |
+
|
hvplot_docs/Integrations.md
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
np.random.seed(1)
|
5 |
+
```
|
6 |
+
|
7 |
+
## Data sources
|
8 |
+
|
9 |
+
The `.hvplot()` plotting API supports a wide range of data sources. Most frequently, a special import can be executed to register the `.hvplot` accessor on a data type. For instance, importing `hvplot.pandas` registers the `.hvplot` accessor on Pandas `DataFrame` and `Series` objects, allowing to call `df.hvplot.line()`.
|
10 |
+
|
11 |
+
Among the data sources introduced below, Pandas](https://pandas.pydata.org) is the only library that doesn't need to be installed separately as it is a direct dependency of hvPlot.
|
12 |
+
|
13 |
+
:::{note}
|
14 |
+
Supporting so many data sources is hard work! We are aware that the support for some of them isn't as good as we would like. If you encounter any issue please report it <a href='https://github.com/holoviz/hvplot/'>on GitHub</a>, we always welcome Pull Requests too!
|
15 |
+
:::
|
16 |
+
|
17 |
+
### Columnar/tabular
|
18 |
+
|
19 |
+
#### Pandas
|
20 |
+
|
21 |
+
`.hvplot()` supports [Pandas](https://pandas.pydata.org) `DataFrame` and `Series` objects.
|
22 |
+
|
23 |
+
|
24 |
+
```python
|
25 |
+
import hvplot.pandas # noqa
|
26 |
+
import pandas as pd
|
27 |
+
|
28 |
+
df_pandas = pd.DataFrame(np.random.randn(1000, 4), columns=list('ABCD')).cumsum()
|
29 |
+
df_pandas.head(2)
|
30 |
+
```
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
# Pandas DataFrame
|
35 |
+
df_pandas.hvplot.line(height=150)
|
36 |
+
```
|
37 |
+
|
38 |
+
|
39 |
+
```python
|
40 |
+
# Pandas Series
|
41 |
+
s_pandas = df_pandas['A']
|
42 |
+
s_pandas.hvplot.line(height=150)
|
43 |
+
```
|
44 |
+
|
45 |
+
#### [Dask](https://www.dask.org)
|
46 |
+
|
47 |
+
`.hvplot()` supports [Dask](https://www.dask.org) `DataFrame` and `Series` objects.
|
48 |
+
|
49 |
+
|
50 |
+
```python
|
51 |
+
import hvplot.dask # noqa
|
52 |
+
import dask
|
53 |
+
|
54 |
+
df_dask = dask.dataframe.from_pandas(df_pandas, npartitions=2)
|
55 |
+
df_dask
|
56 |
+
```
|
57 |
+
|
58 |
+
|
59 |
+
```python
|
60 |
+
# Dask DataFrame
|
61 |
+
df_dask.hvplot.line(height=150)
|
62 |
+
```
|
63 |
+
|
64 |
+
|
65 |
+
```python
|
66 |
+
# Dask Series
|
67 |
+
s_dask = df_dask['A']
|
68 |
+
s_dask.hvplot.line(height=150)
|
69 |
+
```
|
70 |
+
|
71 |
+
#### GeoPandas
|
72 |
+
|
73 |
+
`.hvplot()` supports [GeoPandas](https://geopandas.org) `GeoDataFrame` objects.
|
74 |
+
|
75 |
+
|
76 |
+
```python
|
77 |
+
import hvplot.pandas # noqa
|
78 |
+
import geopandas as gpd
|
79 |
+
|
80 |
+
p_geometry = gpd.points_from_xy(
|
81 |
+
x=[12.45339, 12.44177, 9.51667, 6.13000],
|
82 |
+
y=[41.90328, 43.93610, 47.13372, 49.61166],
|
83 |
+
crs='EPSG:4326'
|
84 |
+
)
|
85 |
+
p_names = ['Vatican City', 'San Marino', 'Vaduz', 'Luxembourg']
|
86 |
+
gdf = gpd.GeoDataFrame(dict(name=p_names), geometry=p_geometry)
|
87 |
+
gdf.head(2)
|
88 |
+
```
|
89 |
+
|
90 |
+
|
91 |
+
```python
|
92 |
+
# GeoPandas GeoDataFrame
|
93 |
+
gdf.hvplot.points(geo=True, tiles='CartoLight', frame_height=150, data_aspect=0.5)
|
94 |
+
```
|
95 |
+
|
96 |
+
#### Ibis
|
97 |
+
|
98 |
+
[Ibis](https://ibis-project.org/) is the "portable Python dataframe library", it provides a unified interface to many data backends (e.g. DuckDB, SQLite, SnowFlake, Google BigQuery). `.hvplot()` supports [Ibis](https://ibis-project.org/) `Expr` objects.
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
import hvplot.ibis # noqa
|
103 |
+
import ibis
|
104 |
+
|
105 |
+
table = ibis.memtable(df_pandas.reset_index())
|
106 |
+
table
|
107 |
+
```
|
108 |
+
|
109 |
+
|
110 |
+
```python
|
111 |
+
# Ibis Expr
|
112 |
+
table.hvplot.line(x='index', height=150)
|
113 |
+
```
|
114 |
+
|
115 |
+
#### Polars
|
116 |
+
|
117 |
+
:::{note}
|
118 |
+
Added in version `0.9.0`.
|
119 |
+
:::
|
120 |
+
|
121 |
+
:::{important}
|
122 |
+
While other data sources like `Pandas` or `Dask` have built-in support in HoloViews, as of version 1.17.1 this is not yet the case for `Polars`. You can track this [issue](https://github.com/holoviz/holoviews/issues/5939) to follow the evolution of this feature in HoloViews. Internally hvPlot simply selects the columns that contribute to the plot and casts them to a Pandas object using Polars' `.to_pandas()` method.
|
123 |
+
:::
|
124 |
+
|
125 |
+
|
126 |
+
```python
|
127 |
+
import hvplot.polars # noqa
|
128 |
+
import polars
|
129 |
+
|
130 |
+
df_polars = polars.from_pandas(df_pandas)
|
131 |
+
df_polars.head(2)
|
132 |
+
```
|
133 |
+
|
134 |
+
`.hvplot()` supports [Polars](https://www.pola.rs/) `DataFrame`, `LazyFrame` and `Series` objects.
|
135 |
+
|
136 |
+
|
137 |
+
```python
|
138 |
+
# Polars DataFrame
|
139 |
+
df_polars.hvplot.line(y=['A', 'B', 'C', 'D'], height=150)
|
140 |
+
```
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
# Polars LazyFrame
|
145 |
+
df_polars.lazy().hvplot.line(y=['A', 'B', 'C', 'D'], height=150)
|
146 |
+
```
|
147 |
+
|
148 |
+
|
149 |
+
```python
|
150 |
+
# Polars Series
|
151 |
+
df_polars['A'].hvplot.line(height=150)
|
152 |
+
```
|
153 |
+
|
154 |
+
#### Rapids cuDF
|
155 |
+
|
156 |
+
:::{important}
|
157 |
+
[Rapids cuDF](https://docs.rapids.ai/api/cudf) is a Python **GPU** DataFrame library. Neither hvPlot's nor HoloViews' test suites currently run on a GPU part of their CI, as of versions 0.9.0 and 1.17.1, respectively. This is due to the non availability of machines equipped with a GPU on the free CI system we rely on (Github Actions). Therefore it's possible that support for cuDF gets degraded in hvPlot without us noticing it immediately. Please report any issue you might encounter.
|
158 |
+
:::
|
159 |
+
|
160 |
+
`.hvplot()` supports [cuDF](https://docs.rapids.ai/api/cudf) `DataFrame` and `Series` objects.
|
161 |
+
|
162 |
+
#### Fugue
|
163 |
+
|
164 |
+
:::{admonition} Experimental
|
165 |
+
:class: caution
|
166 |
+
[Fugue](https://fugue-tutorials.readthedocs.io/) support, added in version `0.9.0`, is experimental and may change in future versions.
|
167 |
+
:::
|
168 |
+
|
169 |
+
hvPlot adds the `hvplot` plotting extension to FugueSQL.
|
170 |
+
|
171 |
+
|
172 |
+
```python
|
173 |
+
import hvplot.fugue # noqa
|
174 |
+
import fugue
|
175 |
+
|
176 |
+
fugue.api.fugue_sql(
|
177 |
+
"""
|
178 |
+
OUTPUT df_pandas USING hvplot:line(
|
179 |
+
height=150,
|
180 |
+
)
|
181 |
+
"""
|
182 |
+
)
|
183 |
+
```
|
184 |
+
|
185 |
+
### Multidimensional
|
186 |
+
|
187 |
+
#### Xarray
|
188 |
+
|
189 |
+
`.hvplot()` supports [XArray](https://xarray.pydata.org) `Dataset` and `DataArray` labelled multidimensional objects.
|
190 |
+
|
191 |
+
|
192 |
+
```python
|
193 |
+
import hvplot.xarray # noqa
|
194 |
+
import xarray as xr
|
195 |
+
|
196 |
+
ds = xr.Dataset({
|
197 |
+
'A': (['x', 'y'], np.random.randn(100, 100)),
|
198 |
+
'B': (['x', 'y'], np.random.randn(100, 100))},
|
199 |
+
coords={'x': np.arange(100), 'y': np.arange(100)}
|
200 |
+
)
|
201 |
+
ds
|
202 |
+
```
|
203 |
+
|
204 |
+
|
205 |
+
```python
|
206 |
+
# Xarray Dataset
|
207 |
+
ds.hvplot.hist(height=150)
|
208 |
+
```
|
209 |
+
|
210 |
+
|
211 |
+
```python
|
212 |
+
# Xarray DataArray
|
213 |
+
ds['A'].hvplot.image(height=150)
|
214 |
+
```
|
215 |
+
|
216 |
+
### Catalog
|
217 |
+
|
218 |
+
#### Intake
|
219 |
+
|
220 |
+
`.hvplot()` supports [Intake](https://github.com/ContinuumIO/intake) `DataSource` objects.
|
221 |
+
|
222 |
+
### Streaming
|
223 |
+
|
224 |
+
#### Streamz
|
225 |
+
|
226 |
+
`.hvplot()` supports [Streamz](https://streamz.readthedocs.io) `DataFrame`, `DataFrames`, `Series` and `Seriess` objects.
|
227 |
+
|
228 |
+
### Graph
|
229 |
+
|
230 |
+
#### NetworkX
|
231 |
+
|
232 |
+
The hvPlot [NetworkX](https://networkx.github.io) plotting API is meant as a drop-in replacement for the `networkx.draw` methods. The `draw` and other `draw_<>` methods are available in the `hvplot.networkx` module.
|
233 |
+
|
234 |
+
|
235 |
+
```python
|
236 |
+
import hvplot.networkx as hvnx
|
237 |
+
import networkx as nx
|
238 |
+
|
239 |
+
G = nx.petersen_graph()
|
240 |
+
hvnx.draw(G, with_labels=True, height=150)
|
241 |
+
```
|
242 |
+
|
243 |
+
## Plotting extensions
|
244 |
+
|
245 |
+
hvPlot is capable of producing plots with [Bokeh](https://www.bokeh.org) (default, interactive), [Matplotlib](https://matplotlib.org) (static) and [Plotly](https://plotly.com/python/) (interactive). Under the hood, hvPlot delegates plotting to HoloViews which itself calls these plotting libraries. This is why we call hvPlot a high-level plotting library!
|
246 |
+
|
247 |
+
Follow the [Plotting Extensions Guide](Plotting_Extensions.ipynb) for more information.
|
248 |
+
|
249 |
+
:::{note}
|
250 |
+
Similarly to having to support many data sources, supporting three plotting extensions is hard work! We are aware they are not supported equivalently, you will get best support for Bokeh, followed by Matplotlib and finally Plotly. If you encounter any issue with a specific plotting extension please report it <a href='https://github.com/holoviz/hvplot/'>on GitHub</a>, we always welcome Pull Requests too!
|
251 |
+
:::
|
hvplot_docs/Interactive.md
ADDED
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|