Spaces:
Runtime error
Runtime error
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 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import ipywidgets as ipw
|
3 |
+
import hvplot.xarray # noqa
|
4 |
+
import hvplot.pandas # noqa
|
5 |
+
import panel as pn
|
6 |
+
import pandas as pd
|
7 |
+
import panel.widgets as pnw
|
8 |
+
import xarray as xr
|
9 |
+
```
|
10 |
+
|
11 |
+
Interactive command-line or notebook interfaces are incredibly powerful tools for quickly doing exploratory analysis, letting you supply arguments to Python methods and functions and see the results immediately. However, this process of exploration can be slow and awkward for large parameter spaces because it requires manually typing each argument value. To further ease exploratory workflows, hvPlot ships with a convenient `.interactive` API, which mirrors the regular API of your favorite data analysis libraries like Pandas, Dask, and xarray but makes it possible to pass in _widgets_ for each argument value, not just a constant. When the widgets are used, the output will dynamically update the full pipeline of method calls so that it works just as if that particular value had been specified in the call being wrapped.
|
12 |
+
|
13 |
+
In this user guide we will explore how to use the .interactive API on xarray and pandas objects:
|
14 |
+
|
15 |
+
|
16 |
+
```python
|
17 |
+
ds = xr.tutorial.load_dataset('air_temperature')
|
18 |
+
ds
|
19 |
+
```
|
20 |
+
|
21 |
+
|
22 |
+
```python
|
23 |
+
from bokeh.sampledata.stocks import IBM
|
24 |
+
|
25 |
+
df = pd.DataFrame(IBM)
|
26 |
+
df['date'] = pd.to_datetime(df.date)
|
27 |
+
```
|
28 |
+
|
29 |
+
## Interactive widgets
|
30 |
+
|
31 |
+
We can supply both regular values, widgets and parameters as arguments to methods on the `.interactive` accessor. Here, we'll use widgets from the [Panel](https://panel.holoviz.org) library. The repr of the resulting object will contain a layout of the widget and a view of the resulting output:
|
32 |
+
|
33 |
+
|
34 |
+
```python
|
35 |
+
slider = pnw.IntSlider(name='time', start=0, end=10)
|
36 |
+
|
37 |
+
ds.air.interactive(width=800).isel(time=slider)
|
38 |
+
```
|
39 |
+
|
40 |
+
You can also use widgets from the [ipywidgets](https://ipywidgets.readthedocs.io) library:
|
41 |
+
|
42 |
+
|
43 |
+
```python
|
44 |
+
slider = ipw.IntSlider(description='time', min=0, max=10)
|
45 |
+
|
46 |
+
ds.air.interactive(width=800).isel(time=slider)
|
47 |
+
```
|
48 |
+
|
49 |
+
Note that this works just as well for DataFrame objects whether they are Pandas, Dask or cuDF dataframes:
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
nrows = pn.widgets.IntSlider(start=1, end=100, value=10)
|
54 |
+
|
55 |
+
df.interactive(width=500).head(nrows)
|
56 |
+
```
|
57 |
+
|
58 |
+
For Panel widgets, we can let .interactive automatically configure the widget, which is particularly convenient when working with `DiscreteSlider` widgets:
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
ds.air.interactive(width=800).sel(time=pnw.DiscreteSlider)
|
63 |
+
```
|
64 |
+
|
65 |
+
## Functions as inputs
|
66 |
+
|
67 |
+
In some cases your starting point for your interactive pipeline may not simply be a DataFrame or xarray Dataset but a function that fetches some data or applies some initial processing on your data. In such a case you can use the `hvplot.bind` function to bind static AND dynamic arguments to your function. Binding dynamic arguments such as a widget or parameter means that whenever the widget/parameter value changes the output of the function will change as well. This makes it possible to construct functions as the input to your interactive pipeline that themselves represent some data pipeline.
|
68 |
+
|
69 |
+
In the example below we will explicitly declare a `Select` widget to select between multiple stock tickers and a function that loads dataframes containing data for each of those stocks. Using the `hvplot.bind` function we then bind the `ticker` select widget to the `ticker` argument of the `stock_df` function and call `.interactive` on the resulting bound function:
|
70 |
+
|
71 |
+
|
72 |
+
```python
|
73 |
+
from bokeh import sampledata
|
74 |
+
|
75 |
+
ticker = pn.widgets.Select(options=['AAPL', 'IBM', 'GOOG', 'MSFT'], name='Ticker')
|
76 |
+
|
77 |
+
def stock_df(ticker):
|
78 |
+
df = pd.DataFrame(getattr(sampledata.stocks, ticker))
|
79 |
+
df['date'] = pd.to_datetime(df.date)
|
80 |
+
return df
|
81 |
+
|
82 |
+
stock_dfi = hvplot.bind(stock_df, ticker).interactive(width=600)
|
83 |
+
|
84 |
+
stock_dfi.head(10)
|
85 |
+
```
|
86 |
+
|
87 |
+
As you can see this `interactive` component behaves just like any other, allowing us to chain `.head` on it and updating when the `ticker` widget changes.
|
88 |
+
|
89 |
+
Just like any other `interactive` component you may also chain it further:
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
ticker = pn.widgets.Select(options=['AAPL', 'IBM', 'GOOG', 'MSFT'], name='Ticker')
|
94 |
+
|
95 |
+
def stock_df(ticker):
|
96 |
+
df = pd.DataFrame(getattr(sampledata.stocks, ticker))
|
97 |
+
df['date'] = pd.to_datetime(df.date)
|
98 |
+
return df
|
99 |
+
|
100 |
+
stock_dfi = hvplot.bind(stock_df, ticker).interactive()
|
101 |
+
|
102 |
+
dt_range = pn.widgets.DateRangeSlider(start=df.date.iloc[-1000], end=df.date.max(), value=(df.date.iloc[-100], df.date.max()))
|
103 |
+
|
104 |
+
stock_dfi[(stock_dfi.date>=dt_range.param.value_start) & (stock_dfi.date<=dt_range.param.value_end)].hvplot(kind='ohlc', grid=True, title=ticker)
|
105 |
+
```
|
106 |
+
|
107 |
+
## Docstrings
|
108 |
+
|
109 |
+
When accessing a method on the `.interactive` accessor it will transparently mirror the docstring of the equivalent method in the underlying library being wrapped:
|
110 |
+
|
111 |
+
|
112 |
+
```python
|
113 |
+
print(ds.air.interactive.isel.__doc__)
|
114 |
+
```
|
115 |
+
|
116 |
+
## Plotting
|
117 |
+
|
118 |
+
One of the most useful aspects of the .interactive API is to feed the output of chained method calls into a plot.
|
119 |
+
|
120 |
+
### Matplotlib
|
121 |
+
|
122 |
+
The output can be almost anything, such as the HTML repr (above) or a matplotlib plot:
|
123 |
+
|
124 |
+
|
125 |
+
```python
|
126 |
+
ds.air.interactive.sel(time=pnw.DiscreteSlider).plot()
|
127 |
+
```
|
128 |
+
|
129 |
+
If we like, we can animate the output with a `Player` widget, and customize the location of the widget using the `loc` keyword argument to `.interactive`:
|
130 |
+
|
131 |
+
|
132 |
+
```python
|
133 |
+
time = pnw.Player(name='time', start=0, end=10, loop_policy='loop', interval=100)
|
134 |
+
|
135 |
+
ds.air.interactive(loc='bottom').isel(time=time).plot()
|
136 |
+
```
|
137 |
+
|
138 |
+
### hvPlot
|
139 |
+
|
140 |
+
We can also make use of the `.hvplot` method to get fully interactive Bokeh-based plots:
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
slider = pnw.FloatSlider(name='quantile', start=0, end=1)
|
145 |
+
|
146 |
+
ds.air.interactive.quantile(slider, dim='time').hvplot(data_aspect=1)
|
147 |
+
```
|
148 |
+
|
149 |
+
You can chain any number of methods, with as many widgets controlling steps in this pipeline as you wish:
|
150 |
+
|
151 |
+
|
152 |
+
```python
|
153 |
+
q = pnw.FloatSlider(name='quantile', start=0, end=1)
|
154 |
+
|
155 |
+
(ds.air.interactive(loc='left')
|
156 |
+
.sel(time=pnw.DiscreteSlider)
|
157 |
+
.quantile(q=q, dim='lon')
|
158 |
+
.hvplot(aspect=1))
|
159 |
+
```
|
160 |
+
|
161 |
+
We can also use a `RangeSlider` to select a slice and compute the mean over that range instead of selecting a specific time:
|
162 |
+
|
163 |
+
|
164 |
+
```python
|
165 |
+
range_slider = pnw.IntRangeSlider
|
166 |
+
|
167 |
+
(ds.air.interactive
|
168 |
+
.isel(time=range_slider)
|
169 |
+
.mean('time')
|
170 |
+
.hvplot())
|
171 |
+
```
|
172 |
+
|
173 |
+
`.interactive` supports arbitrary chains of method calls, including anything that is supported by your data object. For instance, you can even convert your xarray object into a dataframe using `.to_dataframe`, then call pandas methods:
|
174 |
+
|
175 |
+
|
176 |
+
```python
|
177 |
+
ds.air.interactive.sel(lat=pnw.DiscreteSlider).to_dataframe().groupby('time').mean().hvplot('time', 'air')
|
178 |
+
```
|
179 |
+
|
180 |
+
## Operators
|
181 |
+
|
182 |
+
You can further transform your output, if desired, by applying math operators on the interactive object:
|
183 |
+
|
184 |
+
|
185 |
+
```python
|
186 |
+
slider = pnw.IntSlider(name='time', start=0, end=10)
|
187 |
+
baseline = ds.air.mean().item()
|
188 |
+
baseline
|
189 |
+
```
|
190 |
+
|
191 |
+
|
192 |
+
```python
|
193 |
+
ds.air.interactive(width=800).isel(time=slider).mean().item() - baseline
|
194 |
+
```
|
195 |
+
|
196 |
+
You can even do math with a widget:
|
197 |
+
|
198 |
+
|
199 |
+
```python
|
200 |
+
slider = pnw.IntSlider(name='time', start=0, end=10)
|
201 |
+
offset = pnw.IntSlider(name='offset', start=0, end=500)
|
202 |
+
|
203 |
+
ds.air.interactive.isel(time=slider).mean().item() + offset
|
204 |
+
```
|
205 |
+
|
206 |
+
Math operators work with array data as well, such as the time-averaged value of each array value:
|
207 |
+
|
208 |
+
|
209 |
+
```python
|
210 |
+
diff = ds.air.interactive.sel(time=pnw.DiscreteSlider) - ds.air.mean('time')
|
211 |
+
kind = pnw.Select(options=['contour', 'contourf', 'image'])
|
212 |
+
|
213 |
+
diff.hvplot(cmap='RdBu_r', clim=(-20, 20), kind=kind)
|
214 |
+
```
|
215 |
+
|
216 |
+
If you want more control over the layout, you can use any of the features from [Panel](https://panel.holoviz.org). In this case, `interactive.panel()` (or `interactive.output()`) make sure you display the interactive plot only, with the widgets explicitly declared by `interactive.widgets()`:
|
217 |
+
|
218 |
+
|
219 |
+
```python
|
220 |
+
diff = ds.air.interactive.sel(time=pnw.DiscreteSlider) - ds.air.mean('time')
|
221 |
+
kind = pnw.Select(options=['contourf', 'contour', 'image'], value='image')
|
222 |
+
interactive = diff.hvplot(cmap='RdBu_r', clim=(-20, 20), kind=kind)
|
223 |
+
|
224 |
+
pn.Column(
|
225 |
+
pn.Row(
|
226 |
+
pn.panel("https://hvplot.holoviz.org/assets/hvplot-wm.png", width=100),
|
227 |
+
pn.Spacer(width=20),
|
228 |
+
pn.Column(
|
229 |
+
pn.panel("## Select a time and type of plot", width=400),
|
230 |
+
interactive.widgets()
|
231 |
+
),
|
232 |
+
pn.panel("https://panel.holoviz.org/_static/logo_stacked.png", width=100)
|
233 |
+
),
|
234 |
+
interactive.panel()
|
235 |
+
).servable()
|
236 |
+
```
|
237 |
+
|
238 |
+
As you can see, the `.interactive` functionality makes it simple to work interactively with your data, letting you use widgets about as easily as any other method argument! See the Panel or ipwidgets docs for the various widgets and other functionality available.
|
hvplot_docs/Introduction.md
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The PyData ecosystem has a number of core Python data containers that allow users to work with a wide array of datatypes, including:
|
2 |
+
|
3 |
+
* [Pandas](https://pandas.pydata.org): DataFrame, Series (columnar/tabular data)
|
4 |
+
* [Rapids cuDF](https://docs.rapids.ai/api/cudf/stable/): GPU DataFrame, Series (columnar/tabular data)
|
5 |
+
* [Polars](https://www.pola.rs/): Polars is a fast DataFrame library/in-memory query engine (columnar/tabular data)
|
6 |
+
* [Dask](https://www.dask.org): DataFrame, Series (distributed/out of core arrays and columnar data)
|
7 |
+
* [XArray](https://xarray.pydata.org): Dataset, DataArray (labelled multidimensional arrays)
|
8 |
+
* [Streamz](https://streamz.readthedocs.io): DataFrame(s), Series(s) (streaming columnar data)
|
9 |
+
* [Intake](https://github.com/ContinuumIO/intake): DataSource (data catalogues)
|
10 |
+
* [GeoPandas](https://geopandas.org): GeoDataFrame (geometry data)
|
11 |
+
* [NetworkX](https://networkx.github.io/documentation/stable/): Graph (network graphs)
|
12 |
+
|
13 |
+
Many of these libraries have the concept of a high-level plotting API that lets a user generate common plot types very easily. The native plotting APIs are generally built on [Matplotlib](https://matplotlib.org), which provides a solid foundation, but means that users miss out the benefits of modern, interactive plotting libraries for the web like [Bokeh](https://bokeh.pydata.org) and [HoloViews](https://holoviews.org).
|
14 |
+
|
15 |
+
**hvPlot** provides a high-level plotting API built on HoloViews that provides a general and consistent API for plotting data in all the formats mentioned above.
|
16 |
+
|
17 |
+
As a first simple illustration of using hvPlot, let's create a small set of random data in Pandas to explore:
|
18 |
+
|
19 |
+
|
20 |
+
```python
|
21 |
+
import numpy as np
|
22 |
+
import pandas as pd
|
23 |
+
|
24 |
+
index = pd.date_range('1/1/2000', periods=1000)
|
25 |
+
df = pd.DataFrame(np.random.randn(1000, 4), index=index, columns=list('ABCD')).cumsum()
|
26 |
+
|
27 |
+
df.head()
|
28 |
+
```
|
29 |
+
|
30 |
+
## Pandas default .plot()
|
31 |
+
|
32 |
+
Pandas provides Matplotlib-based plotting by default, using the `.plot()` method:
|
33 |
+
|
34 |
+
|
35 |
+
```python
|
36 |
+
%matplotlib inline
|
37 |
+
|
38 |
+
df.plot();
|
39 |
+
```
|
40 |
+
|
41 |
+
The result is a PNG image that displays easily, but is otherwise static.
|
42 |
+
|
43 |
+
## Switching Pandas backend
|
44 |
+
|
45 |
+
To allow using hvPlot directly with Pandas we have to import `hvplot.pandas` and swap the Pandas backend with:
|
46 |
+
|
47 |
+
|
48 |
+
```python
|
49 |
+
import hvplot.pandas # noqa
|
50 |
+
|
51 |
+
pd.options.plotting.backend = 'holoviews'
|
52 |
+
```
|
53 |
+
|
54 |
+
**NOTE:** This requires a recent version of pandas (later than 0.25.0), see the [Pandas API](Pandas_API.ipynb) for more details.
|
55 |
+
|
56 |
+
|
57 |
+
```python
|
58 |
+
df.plot()
|
59 |
+
```
|
60 |
+
|
61 |
+
## .hvplot()
|
62 |
+
|
63 |
+
If we instead change `%matplotlib inline` to `import hvplot.pandas` and use the ``df.hvplot`` method, it will now display an interactively explorable [Bokeh](https://bokeh.pydata.org) plot with panning, zooming, hovering, and clickable/selectable legends:
|
64 |
+
|
65 |
+
|
66 |
+
```python
|
67 |
+
df.hvplot()
|
68 |
+
```
|
69 |
+
|
70 |
+
This interactive plot makes it much easier to explore the properties of the data, without having to write code to select ranges, columns, or data values manually. Note that while pandas, dask and xarray all use the `.hvplot` method, `intake` uses hvPlot as its main plotting API, which means that is available using `.plot()`.
|
71 |
+
|
72 |
+
## hvPlot native API
|
73 |
+
|
74 |
+
For the plot above, hvPlot dynamically added the Pandas `.hvplot()` method, so that you can use the same syntax as with the Pandas default plotting. If you prefer to be more explicit, you can instead work directly with hvPlot objects:
|
75 |
+
|
76 |
+
|
77 |
+
```python
|
78 |
+
from hvplot import hvPlot
|
79 |
+
hvplot.extension('bokeh')
|
80 |
+
|
81 |
+
plot = hvPlot(df)
|
82 |
+
plot(y=['A', 'B', 'C', 'D'])
|
83 |
+
```
|
84 |
+
|
85 |
+
## Switching the plotting extension to Matplotlib or Plotly
|
86 |
+
|
87 |
+
While the default plotting extension of hvPlot is [Bokeh](https://bokeh.pydata.org), it is possible to load either Matplotlib or Plotly with `.extension()` and later switch from a plotting library to another with `.output()`. More information about working with multiple plotting backends can be found in the [plotting extensions guide](Plotting_Extensions.ipynb).
|
88 |
+
|
89 |
+
|
90 |
+
```python
|
91 |
+
hvplot.extension('matplotlib')
|
92 |
+
|
93 |
+
df.hvplot(rot=30)
|
94 |
+
```
|
95 |
+
|
96 |
+
## Getting help
|
97 |
+
|
98 |
+
When working inside IPython or the Jupyter notebook hvplot methods will automatically complete valid keywords, e.g. pressing tab after declaring the plot type will provide all valid keywords and the docstring:
|
99 |
+
|
100 |
+
```python
|
101 |
+
df.hvplot.line(<TAB>
|
102 |
+
```
|
103 |
+
|
104 |
+
Outside an interactive environment ``hvplot.help`` will bring up information providing the ``kind`` of plot, e.g.:
|
105 |
+
|
106 |
+
```python
|
107 |
+
hvplot.help('line')
|
108 |
+
```
|
109 |
+
|
110 |
+
For more detail on the available options see the [Customization](Customization.ipynb) user guide.
|
111 |
+
|
112 |
+
|
113 |
+
## Next steps
|
114 |
+
|
115 |
+
Now that you can see how hvPlot is used, let's jump straight in and discover some of the more powerful things we can do with it in the [Plotting](Plotting.ipynb) section.
|
hvplot_docs/Linked_Brushing.md
ADDED
The diff for this file is too large to render.
See raw diff
|
|
hvplot_docs/Linking_Plots.md
ADDED
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import numpy as np
|
3 |
+
import holoviews as hv
|
4 |
+
from holoviews import opts
|
5 |
+
|
6 |
+
hv.extension('bokeh')
|
7 |
+
```
|
8 |
+
|
9 |
+
When working with the bokeh backend in HoloViews complex interactivity can be achieved using very little code, whether that is shared axes, which zoom and pan together or shared datasources, which allow for linked cross-filtering. Separately it is possible to create custom interactions by attaching LinkedStreams to a plot and thereby triggering events on interactions with the plot. The Streams based interactivity affords a lot of flexibility to declare custom interactivity on a plot, however it always requires a live Python kernel to be connected either via the notebook or bokeh server. The ``Link`` classes described in this user guide however allow declaring interactions which do not require a live server, opening up the possibility of declaring complex interactions in a plot that can be exported to a static HTML file.
|
10 |
+
|
11 |
+
## What is a ``Link``?
|
12 |
+
|
13 |
+
A ``Link`` defines some connection between a source and target object in their visualization. It is quite similar to a ``Stream`` as it allows defining callbacks in response to some change or event on the source object, however, unlike a Stream, it does not transfer data between the browser and a Python process. Instead a ``Link`` directly causes some action to occur on the ``target``, for JS based backends this usually means that a corresponding JS callback will effect some change on the target in response to a change on the source.
|
14 |
+
|
15 |
+
One of the simplest examples of a ``Link`` is the ``DataLink`` which links the data from two sources as long as they match in length, e.g. below we create two elements with data of the same length. By declaring a ``DataLink`` between the two we can ensure they are linked and can be selected together:
|
16 |
+
|
17 |
+
|
18 |
+
```python
|
19 |
+
from holoviews.plotting.links import DataLink
|
20 |
+
|
21 |
+
scatter1 = hv.Scatter(np.arange(100))
|
22 |
+
scatter2 = hv.Scatter(np.arange(100)[::-1], 'x2', 'y2')
|
23 |
+
|
24 |
+
dlink = DataLink(scatter1, scatter2)
|
25 |
+
|
26 |
+
(scatter1 + scatter2).opts(
|
27 |
+
opts.Scatter(tools=['box_select', 'lasso_select']))
|
28 |
+
```
|
29 |
+
|
30 |
+
If we want to display the elements subsequently without linking them we can call the ``unlink`` method:
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
dlink.unlink()
|
35 |
+
|
36 |
+
(scatter1 + scatter2)
|
37 |
+
```
|
38 |
+
|
39 |
+
Another example of a link is the ``RangeToolLink`` which adds a RangeTool to the ``source`` plot which is linked to the axis range on the ``target`` plot. In this way the source plot can be used as an overview of the full data while the target plot provides a more detailed view of a subset of the data:
|
40 |
+
|
41 |
+
|
42 |
+
```python
|
43 |
+
from holoviews.plotting.links import RangeToolLink
|
44 |
+
|
45 |
+
data = np.random.randn(1000).cumsum()
|
46 |
+
|
47 |
+
source = hv.Curve(data).opts(width=800, height=125, axiswise=True, default_tools=[])
|
48 |
+
target = hv.Curve(data).opts(width=800, labelled=['y'], toolbar=None)
|
49 |
+
|
50 |
+
rtlink = RangeToolLink(source, target)
|
51 |
+
|
52 |
+
(target + source).opts(merge_tools=False).cols(1)
|
53 |
+
```
|
54 |
+
|
55 |
+
## Advanced: Writing a ``Link``
|
56 |
+
|
57 |
+
A ``Link`` consists of two components the ``Link`` itself and a ``LinkCallback`` which provides the actual implementation behind the ``Link``. In order to demonstrate writing a ``Link`` we'll start with a fairly straightforward example, linking an ``HLine`` or ``VLine`` to the mean value of a selection on a ``Scatter`` element. To express this we declare a ``MeanLineLink`` class subclassing from the ``Link`` baseclass and declare ``ClassSelector`` parameters for the ``source`` and ``target`` with the appropriate types to perform some basic validation. Additionally we declare a ``column`` parameter to specify which column to compute the mean on.
|
58 |
+
|
59 |
+
|
60 |
+
```python
|
61 |
+
import param
|
62 |
+
from holoviews.plotting.links import Link
|
63 |
+
|
64 |
+
class MeanLineLink(Link):
|
65 |
+
|
66 |
+
column = param.String(default='x', doc="""
|
67 |
+
The column to compute the mean on.""")
|
68 |
+
|
69 |
+
_requires_target = True
|
70 |
+
```
|
71 |
+
|
72 |
+
Now we have the ``Link`` class we need to write the implementation in the form of a ``LinkCallback``, which in the case of bokeh will be translated into a [``CustomJS`` callback](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#userguide-interaction-jscallbacks). A ``LinkCallback`` should declare the ``source_model`` we want to listen to events on and a ``target_model``, declaring which model should be altered in response. To find out which models we can attach the ``Link`` to we can create a ``Plot`` instance and look at the ``plot.handles``, e.g. here we create a ``ScatterPlot`` and can see it has a 'cds', which represents the ``ColumnDataSource``.
|
73 |
+
|
74 |
+
|
75 |
+
```python
|
76 |
+
renderer = hv.renderer('bokeh')
|
77 |
+
|
78 |
+
plot = renderer.get_plot(hv.Scatter([]))
|
79 |
+
|
80 |
+
plot.handles.keys()
|
81 |
+
```
|
82 |
+
|
83 |
+
In this case we are interested in the 'cds' handle, but we still have to tell it which events should trigger the callback. Bokeh callbacks can be grouped into two types, model property changes and events. For more detail on these two types of callbacks see the [Bokeh user guide](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#userguide-interaction-jscallbacks).
|
84 |
+
|
85 |
+
For this example we want to respond to changes to the ``ColumnDataSource.selected`` property. We can declare this in the ``on_source_changes`` class attribute on our callback. So now that we have declared which model we want to listen to events on and which events we want to listen to, we have to declare the model on the target we want to change in response.
|
86 |
+
|
87 |
+
We can once again look at the handles on the plot corresponding to the ``HLine`` element:
|
88 |
+
|
89 |
+
|
90 |
+
```python
|
91 |
+
plot = renderer.get_plot(hv.HLine(0))
|
92 |
+
plot.handles.keys()
|
93 |
+
```
|
94 |
+
|
95 |
+
We now want to change the ``glyph``, which defines the position of the ``HLine``, so we declare the ``target_model`` as ``'glyph'``. Having defined both the source and target model and the events we can finally start writing the JS callback that should be triggered. To declare it we simply define the ``source_code`` class attribute. To understand how to write this code we need to understand how the source and target models, we have declared, can be referenced from within the callback.
|
96 |
+
|
97 |
+
The ``source_model`` will be made available by prefixing it with ``source_``, while the target model is made available with the prefix ``target_``. This means that the ``ColumnDataSource`` on the ``source`` can be referenced as ``source_source``, while the glyph on the target can be referenced as ``target_glyph``.
|
98 |
+
|
99 |
+
Finally, any parameters other than the ``source`` and ``target`` on the ``Link`` will also be made available inside the callback, which means we can reference the appropriate ``column`` in the ``ColumnDataSource`` to compute the mean value along a particular axis.
|
100 |
+
|
101 |
+
Once we know how to reference the bokeh models and ``Link`` parameters we can access their properties to compute the mean value of the current selection on the source ``ColumnDataSource`` and set the ``target_glyph.position`` to that value.
|
102 |
+
|
103 |
+
A ``LinkCallback`` may also define a validate method to validate that the Link parameters and plots are compatible, e.g. in this case we can validate that the ``column`` is actually present in the source_plot ``ColumnDataSource``.
|
104 |
+
|
105 |
+
|
106 |
+
```python
|
107 |
+
from holoviews.plotting.bokeh import LinkCallback
|
108 |
+
|
109 |
+
class MeanLineCallback(LinkCallback):
|
110 |
+
|
111 |
+
source_model = 'selected'
|
112 |
+
source_handles = ['cds']
|
113 |
+
on_source_changes = ['indices']
|
114 |
+
|
115 |
+
target_model = 'glyph'
|
116 |
+
|
117 |
+
source_code = """
|
118 |
+
var inds = source_selected.indices
|
119 |
+
var d = source_cds.data
|
120 |
+
var vm = 0
|
121 |
+
if (inds.length == 0)
|
122 |
+
return
|
123 |
+
for (var i = 0; i < inds.length; i++)
|
124 |
+
vm += d[column][inds[i]]
|
125 |
+
vm /= inds.length
|
126 |
+
target_glyph.location = vm
|
127 |
+
"""
|
128 |
+
|
129 |
+
def validate(self):
|
130 |
+
assert self.link.column in self.source_plot.handles['cds'].data
|
131 |
+
```
|
132 |
+
|
133 |
+
Finally we need to register the ``MeanLineLinkCallback`` with the ``MeanLineLink`` using the ``register_callback`` classmethod:
|
134 |
+
|
135 |
+
|
136 |
+
```python
|
137 |
+
MeanLineLink.register_callback('bokeh', MeanLineCallback)
|
138 |
+
```
|
139 |
+
|
140 |
+
Now the newly declared Link is ready to use, we'll create a ``Scatter`` element along with an ``HLine`` and ``VLine`` element and link each one:
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
options = opts.Scatter(
|
145 |
+
selection_fill_color='firebrick', alpha=0.4, line_color='black', size=8,
|
146 |
+
tools=['lasso_select', 'box_select'], width=500, height=500,
|
147 |
+
active_tools=['lasso_select']
|
148 |
+
)
|
149 |
+
scatter = hv.Scatter(np.random.randn(500, 2)).opts(options)
|
150 |
+
vline = hv.VLine(scatter['x'].mean()).opts(color='black')
|
151 |
+
hline = hv.HLine(scatter['y'].mean()).opts(color='black')
|
152 |
+
|
153 |
+
MeanLineLink(scatter, vline, column='x')
|
154 |
+
MeanLineLink(scatter, hline, column='y')
|
155 |
+
|
156 |
+
scatter * hline * vline
|
157 |
+
```
|
158 |
+
|
159 |
+
Using the 'box_select' and 'lasso_select' tools will now update the position of the HLine and VLine.
|
hvplot_docs/NetworkX.md
ADDED
@@ -0,0 +1,552 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The hvPlot NetworkX plotting API is meant as a drop-in replacement for the ``networkx.draw`` methods. In most cases the existing code will work as is or with minor modifications, returning a HoloViews object rendering an interactive bokeh plot, equivalent to the matplotlib plot the standard API constructs. First let us import the plotting interface and give it the canonical name ``hvnx``:
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import hvplot.networkx as hvnx
|
6 |
+
|
7 |
+
import networkx as nx
|
8 |
+
import holoviews as hv
|
9 |
+
```
|
10 |
+
|
11 |
+
In this user guide we will follow along with many of the examples in the [NetworkX tutorial](https://networkx.github.io/documentation/stable/tutorial.html#drawing-graphs) on drawing graphs.
|
12 |
+
|
13 |
+
The ``hxnx`` namespace provides all the same plotting functions as ``nx``, this means in most cases one can simply be swapped for the other. This also includes most keywords used to customize the plots. The main difference is in the way multiple plots are composited, like all other hvPlot APIs the networkX functions returns HoloViews objects which can be composited using ``+`` and ``*`` operations:
|
14 |
+
|
15 |
+
|
16 |
+
```python
|
17 |
+
G = nx.petersen_graph()
|
18 |
+
|
19 |
+
spring = hvnx.draw(G, with_labels=True)
|
20 |
+
shell = hvnx.draw_shell(G, nlist=[range(5, 10), range(5)], with_labels=True, font_weight='bold')
|
21 |
+
|
22 |
+
spring + shell
|
23 |
+
```
|
24 |
+
|
25 |
+
|
26 |
+
```python
|
27 |
+
H = nx.triangular_lattice_graph(1, 20)
|
28 |
+
hvnx.draw_planar(H, node_color='green', edge_color='brown')
|
29 |
+
```
|
30 |
+
|
31 |
+
The most common ``layout`` functions have dedicated drawing methods such as the ``draw_shell`` function above, which automatically computes the node positions.
|
32 |
+
|
33 |
+
However layout algorithms are not necessarily deterministic, so if we want to plot and overlay subsets of either the nodes or edges using the ``nodelist`` and ``edgelist`` keywords the node positions should be computed ahead of time and passed in explicitly:
|
34 |
+
|
35 |
+
|
36 |
+
```python
|
37 |
+
pos = nx.layout.spring_layout(G)
|
38 |
+
|
39 |
+
hvnx.draw(G, pos, nodelist=[0, 1, 2, 3, 4], node_color='blue') *\
|
40 |
+
hvnx.draw_networkx_nodes(G, pos, nodelist=[5, 6, 7, 8, 9], node_color='green')
|
41 |
+
```
|
42 |
+
|
43 |
+
The ``hvnx`` namespace also makes ``save`` and ``show utilities available to save the plot to HTML or PNG files or display it in a separate browser window when working in a standard Python interpreter.
|
44 |
+
|
45 |
+
|
46 |
+
```python
|
47 |
+
G = nx.dodecahedral_graph()
|
48 |
+
|
49 |
+
shells = [[2, 3, 4, 5, 6], [8, 1, 0, 19, 18, 17, 16, 15, 14, 7], [9, 10, 11, 12, 13]]
|
50 |
+
shell = hvnx.draw_shell(G, nlist=shells)
|
51 |
+
|
52 |
+
pos = nx.nx_agraph.graphviz_layout(G)
|
53 |
+
graphviz = hvnx.draw(G, pos=pos)
|
54 |
+
|
55 |
+
layout = shell + graphviz
|
56 |
+
|
57 |
+
hvnx.save(layout, 'graph_layout.png')
|
58 |
+
```
|
59 |
+
|
60 |
+
#### Styling Graphs
|
61 |
+
|
62 |
+
The full set of options which are inherited from networkx's API are listed in the ``hxnx.draw()`` docstring. Using these the more common styling of nodes and edges can easily be altered through the common set of options that are inherited from networkx. In addition common HoloViews options to control the size of the plots, axes and styling are also supported. Finally, some ``layout`` functions also accept special keyword arguments such as the ``nlist`` argument for the shell layout which specifies the shells.
|
63 |
+
|
64 |
+
|
65 |
+
```python
|
66 |
+
options = {
|
67 |
+
'node_color': 'black',
|
68 |
+
'node_size': 100,
|
69 |
+
'edge_width': 3,
|
70 |
+
'width': 300,
|
71 |
+
'height': 300
|
72 |
+
}
|
73 |
+
|
74 |
+
random = hvnx.draw_random(G, **options)
|
75 |
+
circular = hvnx.draw_circular(G, **options)
|
76 |
+
spectral = hvnx.draw_spectral(G, **options)
|
77 |
+
shell = hvnx.draw_shell(G, nlist=[range(5,10), range(5)], **options)
|
78 |
+
|
79 |
+
(random + circular + spectral + shell).cols(2)
|
80 |
+
```
|
81 |
+
|
82 |
+
In addition to being able to set scalar style values hvPlot also supports the HoloViews concept of [style mapping](https://holoviews.org/user_guide/Style_Mapping.html#styling-mapping), which uses so called ``dim`` transforms to map attributes of the graph nodes and edges to vary the visual attributes of the plot. For example we might construct a graph with edge weights and node sizes as attributes. The plotting function will extract these attributes which means they can be used to scale visual properties of the plot such as the ``edge_width``, ``edge_color`` or ``node_size``:
|
83 |
+
|
84 |
+
|
85 |
+
```python
|
86 |
+
G = nx.Graph()
|
87 |
+
|
88 |
+
G.add_edge('a', 'b', weight=0.6)
|
89 |
+
G.add_edge('a', 'c', weight=0.2)
|
90 |
+
G.add_edge('c', 'd', weight=0.1)
|
91 |
+
G.add_edge('c', 'e', weight=0.7)
|
92 |
+
G.add_edge('c', 'f', weight=0.9)
|
93 |
+
G.add_edge('a', 'd', weight=0.3)
|
94 |
+
|
95 |
+
G.add_node('a', size=20)
|
96 |
+
G.add_node('b', size=10)
|
97 |
+
G.add_node('c', size=12)
|
98 |
+
G.add_node('d', size=5)
|
99 |
+
G.add_node('e', size=8)
|
100 |
+
G.add_node('f', size=3)
|
101 |
+
|
102 |
+
pos = nx.spring_layout(G) # positions for all nodes
|
103 |
+
|
104 |
+
hvnx.draw(G, pos, edge_color='weight', edge_cmap='viridis',
|
105 |
+
edge_width=hv.dim('weight')*10, node_size=hv.dim('size')*20)
|
106 |
+
```
|
107 |
+
|
108 |
+
The full set of options that are supported can be accessed on the ``hvnx.draw`` function (note this does not include some bokeh specific option to control the styling of selection, nonselection and hover nodes and edges which may also be supplied and follow a pattern like ``hover_node_fill_color`` or ``selection_edge_line_alpha``).
|
109 |
+
|
110 |
+
For reference here is the docstring listing the main supported option:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
print(hvnx.draw.__doc__)
|
115 |
+
```
|
116 |
+
|
117 |
+
The main difference to the networkx.draw API are a few options which are not supported (such as `font_weight` and `arrowsize`) and the renaming of `width` (which controls the edge line width) to ``edge_width`` since `width` and `height` are reserved for defining the screen dimensions of the plot.
|
118 |
+
|
119 |
+
## Examples
|
120 |
+
|
121 |
+
To demonstrate that the API works almost identically this section reproduces various examples from the NetworkX documentation.
|
122 |
+
|
123 |
+
### Plot properties
|
124 |
+
|
125 |
+
Compute some network properties for the lollipop graph.
|
126 |
+
|
127 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/basic/plot_properties.html
|
128 |
+
|
129 |
+
|
130 |
+
```python
|
131 |
+
# Copyright (C) 2004-2018 by
|
132 |
+
# Aric Hagberg <hagberg@lanl.gov>
|
133 |
+
# Dan Schult <dschult@colgate.edu>
|
134 |
+
# Pieter Swart <swart@lanl.gov>
|
135 |
+
# All rights reserved.
|
136 |
+
# BSD license.
|
137 |
+
|
138 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
139 |
+
|
140 |
+
G = nx.lollipop_graph(4, 6)
|
141 |
+
|
142 |
+
pathlengths = []
|
143 |
+
|
144 |
+
print("source vertex {target:length, }")
|
145 |
+
for v in G.nodes():
|
146 |
+
spl = dict(nx.single_source_shortest_path_length(G, v))
|
147 |
+
print('{} {} '.format(v, spl))
|
148 |
+
for p in spl:
|
149 |
+
pathlengths.append(spl[p])
|
150 |
+
|
151 |
+
print('')
|
152 |
+
print("average shortest path length %s" % (sum(pathlengths) / len(pathlengths)))
|
153 |
+
|
154 |
+
# histogram of path lengths
|
155 |
+
dist = {}
|
156 |
+
for p in pathlengths:
|
157 |
+
if p in dist:
|
158 |
+
dist[p] += 1
|
159 |
+
else:
|
160 |
+
dist[p] = 1
|
161 |
+
|
162 |
+
print('')
|
163 |
+
print("length #paths")
|
164 |
+
verts = dist.keys()
|
165 |
+
for d in sorted(verts):
|
166 |
+
print('%s %d' % (d, dist[d]))
|
167 |
+
|
168 |
+
print("radius: %d" % nx.radius(G))
|
169 |
+
print("diameter: %d" % nx.diameter(G))
|
170 |
+
print("eccentricity: %s" % nx.eccentricity(G))
|
171 |
+
print("center: %s" % nx.center(G))
|
172 |
+
print("periphery: %s" % nx.periphery(G))
|
173 |
+
print("density: %s" % nx.density(G))
|
174 |
+
|
175 |
+
hvnx.draw(G, with_labels=True)
|
176 |
+
```
|
177 |
+
|
178 |
+
### Simple Path
|
179 |
+
|
180 |
+
Draw a graph with hvPlot.
|
181 |
+
|
182 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_simple_path.html
|
183 |
+
|
184 |
+
|
185 |
+
```python
|
186 |
+
G = nx.path_graph(8)
|
187 |
+
hvnx.draw(G)
|
188 |
+
```
|
189 |
+
|
190 |
+
### Node colormap
|
191 |
+
|
192 |
+
Draw a graph with hvPlot, color by degree.
|
193 |
+
|
194 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_node_colormap.html
|
195 |
+
|
196 |
+
|
197 |
+
```python
|
198 |
+
# Author: Aric Hagberg (hagberg@lanl.gov)
|
199 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
200 |
+
|
201 |
+
G = nx.cycle_graph(24)
|
202 |
+
pos = nx.spring_layout(G, iterations=200)
|
203 |
+
|
204 |
+
# Preferred API
|
205 |
+
# hvnx.draw(G, pos, node_color='index', node_size=500, cmap='Blues')
|
206 |
+
|
207 |
+
# Original code
|
208 |
+
hvnx.draw(G, pos, node_color=range(24), node_size=500, cmap='Blues')
|
209 |
+
```
|
210 |
+
|
211 |
+
### Edge Colormap
|
212 |
+
|
213 |
+
Draw a graph with hvPlot, color edges.
|
214 |
+
|
215 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_edge_colormap.html
|
216 |
+
|
217 |
+
|
218 |
+
```python
|
219 |
+
# Author: Aric Hagberg (hagberg@lanl.gov)
|
220 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
221 |
+
|
222 |
+
G = nx.star_graph(20)
|
223 |
+
pos = nx.spring_layout(G)
|
224 |
+
colors = range(20)
|
225 |
+
hvnx.draw(G, pos, node_color='#A0CBE2', edge_color=colors,
|
226 |
+
edge_width=4, edge_cmap='Blues', with_labels=False)
|
227 |
+
```
|
228 |
+
|
229 |
+
### House With Colors
|
230 |
+
|
231 |
+
Draw a graph with hvPlot.
|
232 |
+
|
233 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_house_with_colors.html
|
234 |
+
|
235 |
+
|
236 |
+
```python
|
237 |
+
# Author: Aric Hagberg (hagberg@lanl.gov)
|
238 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
239 |
+
|
240 |
+
G = nx.house_graph()
|
241 |
+
# explicitly set positions
|
242 |
+
pos = {0: (0, 0),
|
243 |
+
1: (1, 0),
|
244 |
+
2: (0, 1),
|
245 |
+
3: (1, 1),
|
246 |
+
4: (0.5, 2.0)}
|
247 |
+
|
248 |
+
hvnx.draw_networkx_nodes(G, pos, node_size=2000, nodelist=[4], padding=0.2) *\
|
249 |
+
hvnx.draw_networkx_nodes(G, pos, node_size=3000, nodelist=[0, 1, 2, 3], node_color='black') *\
|
250 |
+
hvnx.draw_networkx_edges(G, pos, alpha=0.5, width=6, xaxis=None, yaxis=None)
|
251 |
+
```
|
252 |
+
|
253 |
+
### Circular Tree
|
254 |
+
|
255 |
+
URL: https://networkx.org/documentation/stable/auto_examples/graphviz_layout/plot_circular_tree.html
|
256 |
+
|
257 |
+
|
258 |
+
```python
|
259 |
+
try:
|
260 |
+
import pygraphviz # noqa
|
261 |
+
from networkx.drawing.nx_agraph import graphviz_layout
|
262 |
+
except ImportError:
|
263 |
+
try:
|
264 |
+
import pydot # noqa
|
265 |
+
from networkx.drawing.nx_pydot import graphviz_layout
|
266 |
+
except ImportError:
|
267 |
+
raise ImportError("This example needs Graphviz and either "
|
268 |
+
"PyGraphviz or pydot")
|
269 |
+
|
270 |
+
G = nx.balanced_tree(3, 5)
|
271 |
+
pos = graphviz_layout(G, prog='twopi', args='')
|
272 |
+
hvnx.draw(G, pos, node_size=20, alpha=0.5, node_color="blue", with_labels=False, width=600, height=600)
|
273 |
+
```
|
274 |
+
|
275 |
+
### Spectral Embedding
|
276 |
+
|
277 |
+
The spectral layout positions the nodes of the graph based on the eigenvectors of the graph Laplacian L=D−A, where A is the adjacency matrix and D is the degree matrix of the graph. By default, the spectral layout will embed the graph in two dimensions (you can embed your graph in other dimensions using the dim argument to either draw_spectral() or spectral_layout()).
|
278 |
+
|
279 |
+
When the edges of the graph represent similarity between the incident nodes, the spectral embedding will place highly similar nodes closer to one another than nodes which are less similar.
|
280 |
+
|
281 |
+
This is particularly striking when you spectrally embed a grid graph. In the full grid graph, the nodes in the center of the graph are pulled apart more than nodes on the periphery. As you remove internal nodes, this effect increases.
|
282 |
+
|
283 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_spectral_grid.html
|
284 |
+
|
285 |
+
|
286 |
+
```python
|
287 |
+
options = {
|
288 |
+
'node_size': 100,
|
289 |
+
'width': 250, 'height': 250
|
290 |
+
}
|
291 |
+
|
292 |
+
G = nx.grid_2d_graph(6, 6)
|
293 |
+
spectral1 = hvnx.draw_spectral(G, **options)
|
294 |
+
|
295 |
+
G.remove_edge((2, 2), (2, 3))
|
296 |
+
spectral2 = hvnx.draw_spectral(G, **options)
|
297 |
+
|
298 |
+
G.remove_edge((3, 2), (3, 3))
|
299 |
+
spectral3 = hvnx.draw_spectral(G, **options)
|
300 |
+
|
301 |
+
G.remove_edge((2, 2), (3, 2))
|
302 |
+
spectral4 = hvnx.draw_spectral(G, **options)
|
303 |
+
|
304 |
+
G.remove_edge((2, 3), (3, 3))
|
305 |
+
spectral5 = hvnx.draw_spectral(G, **options)
|
306 |
+
|
307 |
+
G.remove_edge((1, 2), (1, 3))
|
308 |
+
spectral6 = hvnx.draw_spectral(G, **options)
|
309 |
+
|
310 |
+
G.remove_edge((4, 2), (4, 3))
|
311 |
+
spectral7 = hvnx.draw_spectral(G, **options)
|
312 |
+
|
313 |
+
(hv.Empty() + spectral1 + hv.Empty() +
|
314 |
+
spectral2 + spectral3 + spectral4 +
|
315 |
+
spectral5 + spectral6 + spectral7).cols(3)
|
316 |
+
```
|
317 |
+
|
318 |
+
### Plot four grids
|
319 |
+
|
320 |
+
Draw a graph with hvPlot.
|
321 |
+
|
322 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_four_grids.html
|
323 |
+
|
324 |
+
|
325 |
+
```python
|
326 |
+
# Author: Aric Hagberg (hagberg@lanl.gov)
|
327 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
328 |
+
|
329 |
+
# Copyright (C) 2004-2018
|
330 |
+
# Aric Hagberg <hagberg@lanl.gov>
|
331 |
+
# Dan Schult <dschult@colgate.edu>
|
332 |
+
# Pieter Swart <swart@lanl.gov>
|
333 |
+
# All rights reserved.
|
334 |
+
# BSD license.
|
335 |
+
|
336 |
+
G = nx.grid_2d_graph(4, 4) # 4x4 grid
|
337 |
+
|
338 |
+
pos = nx.spring_layout(G, iterations=100)
|
339 |
+
|
340 |
+
g1 = hvnx.draw(G, pos, font_size=8)
|
341 |
+
|
342 |
+
g2 = hvnx.draw(G, pos, node_color='black', node_size=0, with_labels=False)
|
343 |
+
|
344 |
+
g3 = hvnx.draw(G, pos, node_color='green', node_size=250, with_labels=False, edge_width=6)
|
345 |
+
|
346 |
+
H = G.to_directed()
|
347 |
+
g4 = hvnx.draw(H, pos, node_color='blue', node_size=20, with_labels=False)
|
348 |
+
|
349 |
+
(g1 + g2 + g3 + g4).cols(2)
|
350 |
+
```
|
351 |
+
|
352 |
+
### Ego Graph
|
353 |
+
|
354 |
+
Example using the NetworkX ego_graph() function to return the main egonet of the largest hub in a Barabási-Albert network.
|
355 |
+
|
356 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_ego_graph.html
|
357 |
+
|
358 |
+
|
359 |
+
```python
|
360 |
+
# Author: Drew Conway (drew.conway@nyu.edu)
|
361 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
362 |
+
|
363 |
+
from operator import itemgetter
|
364 |
+
|
365 |
+
# Create a BA model graph
|
366 |
+
n = 1000
|
367 |
+
m = 2
|
368 |
+
G = nx.generators.barabasi_albert_graph(n, m)
|
369 |
+
# find node with largest degree
|
370 |
+
node_and_degree = G.degree()
|
371 |
+
(largest_hub, degree) = sorted(node_and_degree, key=itemgetter(1))[-1]
|
372 |
+
# Create ego graph of main hub
|
373 |
+
hub_ego = nx.ego_graph(G, largest_hub)
|
374 |
+
# Draw graph
|
375 |
+
pos = nx.spring_layout(hub_ego)
|
376 |
+
g = hvnx.draw(hub_ego, pos, node_color='blue', node_size=50, with_labels=False)
|
377 |
+
# Draw ego as large and red
|
378 |
+
gnodes = hvnx.draw_networkx_nodes(hub_ego, pos, nodelist=[largest_hub], node_size=300, node_color='red')
|
379 |
+
|
380 |
+
g * gnodes
|
381 |
+
```
|
382 |
+
|
383 |
+
### Random Geometric Graph
|
384 |
+
|
385 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_random_geometric_graph.html
|
386 |
+
|
387 |
+
|
388 |
+
```python
|
389 |
+
G = nx.random_geometric_graph(200, 0.125)
|
390 |
+
# position is stored as node attribute data for random_geometric_graph
|
391 |
+
pos = nx.get_node_attributes(G, 'pos')
|
392 |
+
|
393 |
+
# find node near center (0.5,0.5)
|
394 |
+
dmin = 1
|
395 |
+
ncenter = 0
|
396 |
+
for n in pos:
|
397 |
+
x, y = pos[n]
|
398 |
+
d = (x - 0.5)**2 + (y - 0.5)**2
|
399 |
+
if d < dmin:
|
400 |
+
ncenter = n
|
401 |
+
dmin = d
|
402 |
+
|
403 |
+
# color by path length from node near center
|
404 |
+
p = nx.single_source_shortest_path_length(G, ncenter)
|
405 |
+
|
406 |
+
hvnx.draw_networkx_edges(G, pos, nodelist=[ncenter], alpha=0.4, width=600, height=600) *\
|
407 |
+
hvnx.draw_networkx_nodes(G, pos, nodelist=list(p.keys()),
|
408 |
+
node_size=80,
|
409 |
+
node_color=list(p.values()),
|
410 |
+
cmap='Reds_r')
|
411 |
+
```
|
412 |
+
|
413 |
+
### Weighted Graph
|
414 |
+
|
415 |
+
An example using Graph as a weighted network.
|
416 |
+
|
417 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_weighted_graph.html
|
418 |
+
|
419 |
+
|
420 |
+
```python
|
421 |
+
# Author: Aric Hagberg (hagberg@lanl.gov)
|
422 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
423 |
+
|
424 |
+
import networkx as nx
|
425 |
+
|
426 |
+
G = nx.Graph()
|
427 |
+
|
428 |
+
G.add_edge('a', 'b', weight=0.6)
|
429 |
+
G.add_edge('a', 'c', weight=0.2)
|
430 |
+
G.add_edge('c', 'd', weight=0.1)
|
431 |
+
G.add_edge('c', 'e', weight=0.7)
|
432 |
+
G.add_edge('c', 'f', weight=0.9)
|
433 |
+
G.add_edge('a', 'd', weight=0.3)
|
434 |
+
|
435 |
+
elarge = [(u, v) for (u, v, attr) in G.edges(data=True) if attr['weight'] > 0.5]
|
436 |
+
esmall = [(u, v) for (u, v, attr) in G.edges(data=True) if attr['weight'] <= 0.5]
|
437 |
+
|
438 |
+
pos = nx.spring_layout(G) # positions for all nodes
|
439 |
+
|
440 |
+
# nodes
|
441 |
+
nodes = hvnx.draw_networkx_nodes(G, pos, node_size=700)
|
442 |
+
|
443 |
+
# edges
|
444 |
+
edges1 = hvnx.draw_networkx_edges(
|
445 |
+
G, pos, edgelist=elarge, edge_width=6)
|
446 |
+
edges2 = hvnx.draw_networkx_edges(
|
447 |
+
G, pos, edgelist=esmall, edge_width=6, alpha=0.5, edge_color='blue', style='dashed')
|
448 |
+
labels = hvnx.draw_networkx_labels(G, pos, font_size=20, font_family='sans-serif')
|
449 |
+
|
450 |
+
edges1 * edges2 * nodes * labels
|
451 |
+
```
|
452 |
+
|
453 |
+
### Directed Graph
|
454 |
+
|
455 |
+
Draw a graph with directed edges using a colormap and different node sizes.
|
456 |
+
|
457 |
+
Edges have different colors and alphas (opacity). Drawn using matplotlib.
|
458 |
+
|
459 |
+
URL: https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html
|
460 |
+
|
461 |
+
|
462 |
+
```python
|
463 |
+
# Author: Rodrigo Dorantes-Gilardi (rodgdor@gmail.com)
|
464 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
465 |
+
|
466 |
+
G = nx.generators.directed.random_k_out_graph(10, 3, 0.5)
|
467 |
+
pos = nx.layout.spring_layout(G)
|
468 |
+
|
469 |
+
node_sizes = [3 + 10 * i for i in range(len(G))]
|
470 |
+
M = G.number_of_edges()
|
471 |
+
edge_colors = range(2, M + 2)
|
472 |
+
edge_alphas = [(5 + i) / (M + 4) for i in range(M)]
|
473 |
+
|
474 |
+
nodes = hvnx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='blue')
|
475 |
+
edges = hvnx.draw_networkx_edges(G, pos, node_size=node_sizes, arrowstyle='->',
|
476 |
+
arrowsize=10, edge_color=edge_colors,
|
477 |
+
edge_cmap='Blues', edge_width=2, colorbar=True)
|
478 |
+
|
479 |
+
nodes * edges
|
480 |
+
```
|
481 |
+
|
482 |
+
### Giant Component
|
483 |
+
|
484 |
+
This example illustrates the sudden appearance of a giant connected component in a binomial random graph.
|
485 |
+
|
486 |
+
https://networkx.org/documentation/stable/auto_examples/graphviz_layout/plot_giant_component.html
|
487 |
+
|
488 |
+
|
489 |
+
```python
|
490 |
+
# Copyright (C) 2006-2018
|
491 |
+
# Aric Hagberg <hagberg@lanl.gov>
|
492 |
+
# Dan Schult <dschult@colgate.edu>
|
493 |
+
# Pieter Swart <swart@lanl.gov>
|
494 |
+
# All rights reserved.
|
495 |
+
# BSD license.
|
496 |
+
|
497 |
+
# Adapted by Philipp Rudiger <prudiger@anaconda.com>
|
498 |
+
|
499 |
+
import math
|
500 |
+
|
501 |
+
try:
|
502 |
+
import pygraphviz # noqa
|
503 |
+
from networkx.drawing.nx_agraph import graphviz_layout
|
504 |
+
layout = graphviz_layout
|
505 |
+
except ImportError:
|
506 |
+
try:
|
507 |
+
import pydot # noqa
|
508 |
+
from networkx.drawing.nx_pydot import graphviz_layout
|
509 |
+
layout = graphviz_layout
|
510 |
+
except ImportError:
|
511 |
+
print("PyGraphviz and pydot not found;\n"
|
512 |
+
"drawing with spring layout;\n"
|
513 |
+
"will be slow.")
|
514 |
+
layout = nx.spring_layout
|
515 |
+
|
516 |
+
n = 150 # 150 nodes
|
517 |
+
# p value at which giant component (of size log(n) nodes) is expected
|
518 |
+
p_giant = 1.0 / (n - 1)
|
519 |
+
# p value at which graph is expected to become completely connected
|
520 |
+
p_conn = math.log(n) / float(n)
|
521 |
+
|
522 |
+
# the following range of p values should be close to the threshold
|
523 |
+
pvals = [0.003, 0.006, 0.008, 0.015]
|
524 |
+
|
525 |
+
region = 220 # for pylab 2x2 subplot layout
|
526 |
+
plots = []
|
527 |
+
for p in pvals:
|
528 |
+
G = nx.binomial_graph(n, p)
|
529 |
+
pos = layout(G)
|
530 |
+
region += 1
|
531 |
+
g = hvnx.draw(G, pos, with_labels=False, node_size=15)
|
532 |
+
# identify largest connected component
|
533 |
+
Gcc = sorted([G.subgraph(c) for c in nx.connected_components(G)], key=len, reverse=True)
|
534 |
+
G0 = Gcc[0]
|
535 |
+
edges = hvnx.draw_networkx_edges(
|
536 |
+
G0, pos, with_labels=False, edge_color='red', edge_width=6.0)
|
537 |
+
|
538 |
+
# show other connected components
|
539 |
+
other_edges = []
|
540 |
+
for Gi in Gcc[1:]:
|
541 |
+
if len(Gi) > 1:
|
542 |
+
edge = hvnx.draw_networkx_edges(Gi, pos,
|
543 |
+
with_labels=False,
|
544 |
+
edge_color='red',
|
545 |
+
alpha=0.3,
|
546 |
+
edge_width=5.0
|
547 |
+
)
|
548 |
+
other_edges.append(edge)
|
549 |
+
plots.append((g*edges*hv.Overlay(other_edges)).relabel("p = %6.3f" % (p)))
|
550 |
+
|
551 |
+
hv.Layout(plots).cols(2)
|
552 |
+
```
|
hvplot_docs/Network_Graphs.md
ADDED
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import numpy as np
|
3 |
+
import pandas as pd
|
4 |
+
import holoviews as hv
|
5 |
+
import networkx as nx
|
6 |
+
from holoviews import opts
|
7 |
+
|
8 |
+
hv.extension('bokeh')
|
9 |
+
|
10 |
+
defaults = dict(width=400, height=400)
|
11 |
+
hv.opts.defaults(
|
12 |
+
opts.EdgePaths(**defaults), opts.Graph(**defaults), opts.Nodes(**defaults))
|
13 |
+
```
|
14 |
+
|
15 |
+
Visualizing and working with network graphs is a common problem in many different disciplines. HoloViews provides the ability to represent and visualize graphs very simply and easily with facilities for interactively exploring the nodes and edges of the graph, especially using the bokeh plotting interface.
|
16 |
+
|
17 |
+
The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes. By default the element will automatically compute concrete ``x`` and ``y`` positions for the nodes and represent them using a ``Nodes`` element, which is stored on the Graph. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.
|
18 |
+
|
19 |
+
To summarize a ``Graph`` consists of three different components:
|
20 |
+
|
21 |
+
* The ``Graph`` itself holds the abstract edges stored as a table of node indices.
|
22 |
+
* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.
|
23 |
+
* The ``EdgePaths`` can optionally be supplied to declare explicit node paths.
|
24 |
+
|
25 |
+
#### A simple Graph
|
26 |
+
|
27 |
+
Let's start by declaring a very simple graph connecting one node to all others. If we simply supply the abstract connectivity of the ``Graph``, it will automatically compute a layout for the nodes using the ``layout_nodes`` operation, which defaults to a circular layout:
|
28 |
+
|
29 |
+
|
30 |
+
```python
|
31 |
+
# Declare abstract edges
|
32 |
+
N = 8
|
33 |
+
node_indices = np.arange(N, dtype=np.int32)
|
34 |
+
source = np.zeros(N, dtype=np.int32)
|
35 |
+
target = node_indices
|
36 |
+
|
37 |
+
|
38 |
+
simple_graph = hv.Graph(((source, target),))
|
39 |
+
simple_graph
|
40 |
+
```
|
41 |
+
|
42 |
+
#### Accessing the nodes and edges
|
43 |
+
|
44 |
+
We can easily access the ``Nodes`` and ``EdgePaths`` on the ``Graph`` element using the corresponding properties:
|
45 |
+
|
46 |
+
|
47 |
+
```python
|
48 |
+
simple_graph.nodes + simple_graph.edgepaths
|
49 |
+
```
|
50 |
+
|
51 |
+
#### Displaying directed graphs
|
52 |
+
|
53 |
+
When specifying the graph edges the source and target node are listed in order, if the graph is actually a directed graph this may used to indicate the directionality of the graph. By setting ``directed=True`` as a plot option it is possible to indicate the directionality of each edge using an arrow:
|
54 |
+
|
55 |
+
|
56 |
+
```python
|
57 |
+
simple_graph.relabel('Directed Graph').opts(directed=True, node_size=5, arrowhead_length=0.05)
|
58 |
+
```
|
59 |
+
|
60 |
+
The length of the arrows can be set as an fraction of the overall graph extent using the ``arrowhead_length`` option.
|
61 |
+
|
62 |
+
#### Supplying explicit paths
|
63 |
+
|
64 |
+
Next we will extend this example by supplying explicit edges:
|
65 |
+
|
66 |
+
|
67 |
+
```python
|
68 |
+
def bezier(start, end, control, steps=np.linspace(0, 1, 100)):
|
69 |
+
return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end
|
70 |
+
|
71 |
+
x, y = simple_graph.nodes.array([0, 1]).T
|
72 |
+
|
73 |
+
paths = []
|
74 |
+
for node_index in node_indices:
|
75 |
+
ex, ey = x[node_index], y[node_index]
|
76 |
+
paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))
|
77 |
+
|
78 |
+
bezier_graph = hv.Graph(((source, target), (x, y, node_indices), paths))
|
79 |
+
bezier_graph
|
80 |
+
```
|
81 |
+
|
82 |
+
## Interactive features
|
83 |
+
|
84 |
+
#### Hover and selection policies
|
85 |
+
|
86 |
+
Thanks to Bokeh we can reveal more about the graph by hovering over the nodes and edges. The ``Graph`` element provides an ``inspection_policy`` and a ``selection_policy``, which define whether hovering and selection highlight edges associated with the selected node or nodes associated with the selected edge, these policies can be toggled by setting the policy to ``'nodes'`` (the default) and ``'edges'``.
|
87 |
+
|
88 |
+
|
89 |
+
```python
|
90 |
+
bezier_graph.relabel('Edge Inspection').opts(inspection_policy='edges')
|
91 |
+
```
|
92 |
+
|
93 |
+
In addition to changing the policy we can also change the colors used when hovering and selecting nodes:
|
94 |
+
|
95 |
+
|
96 |
+
```python
|
97 |
+
bezier_graph.opts(
|
98 |
+
opts.Graph(inspection_policy='nodes', tools=['hover', 'box_select'],
|
99 |
+
edge_hover_line_color='green', node_hover_fill_color='red'))
|
100 |
+
```
|
101 |
+
|
102 |
+
#### Additional information
|
103 |
+
|
104 |
+
We can also associate additional information with the nodes and edges of a graph. By constructing the ``Nodes`` explicitly we can declare additional value dimensions, which are revealed when hovering and/or can be mapped to the color by setting the ``color`` to the dimension name ('Weight'). We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself, which we can map to various style options, e.g. by setting the ``edge_color`` and ``edge_line_width``.
|
105 |
+
|
106 |
+
|
107 |
+
```python
|
108 |
+
node_labels = ['Output']+['Input']*(N-1)
|
109 |
+
np.random.seed(7)
|
110 |
+
edge_labels = np.random.rand(8)
|
111 |
+
|
112 |
+
nodes = hv.Nodes((x, y, node_indices, node_labels), vdims='Type')
|
113 |
+
graph = hv.Graph(((source, target, edge_labels), nodes, paths), vdims='Weight')
|
114 |
+
|
115 |
+
(graph + graph.opts(inspection_policy='edges', clone=True)).opts(
|
116 |
+
opts.Graph(node_color='Type', edge_color='Weight', cmap='Set1',
|
117 |
+
edge_cmap='viridis', edge_line_width=hv.dim('Weight')*10))
|
118 |
+
```
|
119 |
+
|
120 |
+
If you want to supply additional node information without specifying explicit node positions you may pass in a ``Dataset`` object consisting of various value dimensions.
|
121 |
+
|
122 |
+
|
123 |
+
```python
|
124 |
+
node_info = hv.Dataset(node_labels, vdims='Label')
|
125 |
+
hv.Graph(((source, target), node_info)).opts(node_color='Label', cmap='Set1')
|
126 |
+
```
|
127 |
+
|
128 |
+
## Working with NetworkX
|
129 |
+
|
130 |
+
NetworkX is a very useful library when working with network graphs and the Graph Element provides ways of importing a NetworkX Graph directly. Here we will load the Karate Club graph and use the ``circular_layout`` function provided by NetworkX to lay it out:
|
131 |
+
|
132 |
+
|
133 |
+
```python
|
134 |
+
G = nx.karate_club_graph()
|
135 |
+
hv.Graph.from_networkx(G, nx.layout.circular_layout).opts(tools=['hover'])
|
136 |
+
```
|
137 |
+
|
138 |
+
It is also possible to pass arguments to the NetworkX layout function as keywords to ``hv.Graph.from_networkx``, e.g. we can override the k-value of the Fruchteran Reingold layout
|
139 |
+
|
140 |
+
|
141 |
+
```python
|
142 |
+
hv.Graph.from_networkx(G, nx.layout.fruchterman_reingold_layout, k=1)
|
143 |
+
```
|
144 |
+
|
145 |
+
Finally if we want to layout a Graph after it has already been constructed, the ``layout_nodes`` operation may be used, which also allows applying the ``weight`` argument to graphs which have not been constructed with networkx:
|
146 |
+
|
147 |
+
|
148 |
+
```python
|
149 |
+
from holoviews.element.graphs import layout_nodes
|
150 |
+
|
151 |
+
graph = hv.Graph([
|
152 |
+
('a', 'b', 3),
|
153 |
+
('a', 'c', 0.2),
|
154 |
+
('c', 'd', 0.1),
|
155 |
+
('c', 'e', 0.7),
|
156 |
+
('c', 'f', 5),
|
157 |
+
('a', 'd', 0.3)
|
158 |
+
], vdims='weight')
|
159 |
+
|
160 |
+
layout_nodes(graph, layout=nx.layout.fruchterman_reingold_layout, kwargs={'weight': 'weight'})
|
161 |
+
```
|
162 |
+
|
163 |
+
## Adding labels
|
164 |
+
|
165 |
+
If the ``Graph`` we have constructed has additional metadata we can easily use those as labels, we simply get a handle on the nodes, cast them to hv.Labels and then overlay them:
|
166 |
+
|
167 |
+
|
168 |
+
```python
|
169 |
+
graph = hv.Graph.from_networkx(G, nx.layout.fruchterman_reingold_layout)
|
170 |
+
labels = hv.Labels(graph.nodes, ['x', 'y'], 'club')
|
171 |
+
|
172 |
+
(graph * labels.opts(text_font_size='8pt', text_color='white', bgcolor='gray'))
|
173 |
+
```
|
174 |
+
|
175 |
+
## Animating graphs
|
176 |
+
|
177 |
+
Like all other elements ``Graph`` can be updated in a ``HoloMap`` or ``DynamicMap``. Here we animate how the Fruchterman-Reingold force-directed algorithm lays out the nodes in real time.
|
178 |
+
|
179 |
+
|
180 |
+
```python
|
181 |
+
hv.HoloMap({i: hv.Graph.from_networkx(G, nx.spring_layout, iterations=i, seed=10) for i in range(5, 30, 5)},
|
182 |
+
kdims='Iterations')
|
183 |
+
```
|
184 |
+
|
185 |
+
## Real world graphs
|
186 |
+
|
187 |
+
As a final example let's look at a slightly larger graph. We will load a dataset of a Facebook network consisting a number of friendship groups identified by their ``'circle'``. We will load the edge and node data using pandas and then color each node by their friendship group using many of the things we learned above.
|
188 |
+
|
189 |
+
|
190 |
+
```python
|
191 |
+
kwargs = dict(width=800, height=800, xaxis=None, yaxis=None)
|
192 |
+
opts.defaults(opts.Nodes(**kwargs), opts.Graph(**kwargs))
|
193 |
+
|
194 |
+
colors = ['#000000']+hv.Cycle('Category20').values
|
195 |
+
edges_df = pd.read_csv('../assets/fb_edges.csv')
|
196 |
+
fb_nodes = hv.Nodes(pd.read_csv('../assets/fb_nodes.csv')).sort()
|
197 |
+
fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')
|
198 |
+
|
199 |
+
fb_graph.opts(cmap=colors, node_size=10, edge_line_width=1,
|
200 |
+
node_line_color='gray', node_color='circle')
|
201 |
+
```
|
202 |
+
|
203 |
+
## Bundling graphs
|
204 |
+
|
205 |
+
The datashader library provides algorithms for bundling the edges of a graph and HoloViews provides convenient wrappers around the libraries. Note that these operations need ``scikit-image`` which you can install using:
|
206 |
+
|
207 |
+
```
|
208 |
+
conda install scikit-image
|
209 |
+
```
|
210 |
+
|
211 |
+
or
|
212 |
+
|
213 |
+
```
|
214 |
+
pip install scikit-image
|
215 |
+
```
|
216 |
+
|
217 |
+
|
218 |
+
```python
|
219 |
+
from holoviews.operation.datashader import datashade, bundle_graph
|
220 |
+
bundled = bundle_graph(fb_graph)
|
221 |
+
bundled
|
222 |
+
```
|
223 |
+
|
224 |
+
## Datashading graphs
|
225 |
+
|
226 |
+
For graphs with a large number of edges we can datashade the paths and display the nodes separately. This loses some of the interactive features but will let you visualize quite large graphs:
|
227 |
+
|
228 |
+
|
229 |
+
```python
|
230 |
+
(datashade(bundled, normalization='linear', width=800, height=800) * bundled.nodes).opts(
|
231 |
+
opts.Nodes(color='circle', size=10, width=1000, cmap=colors, legend_position='right'))
|
232 |
+
```
|
233 |
+
|
234 |
+
### Applying selections
|
235 |
+
|
236 |
+
Alternatively we can select the nodes and edges by an attribute that resides on either. In this case we will select the nodes and edges for a particular circle and then overlay just the selected part of the graph on the datashaded plot. Note that selections on the ``Graph`` itself will select all nodes that connect to one of the selected nodes. In this way a smaller subgraph can be highlighted and the larger graph can be datashaded.
|
237 |
+
|
238 |
+
|
239 |
+
```python
|
240 |
+
datashade(bundle_graph(fb_graph), normalization='linear', width=800, height=800) *\
|
241 |
+
bundled.select(circle='circle15').opts(node_fill_color='white')
|
242 |
+
```
|
243 |
+
|
244 |
+
To select just nodes that are in 'circle15' set the ``selection_mode='nodes'`` overriding the default of 'edges':
|
245 |
+
|
246 |
+
|
247 |
+
```python
|
248 |
+
bundled.select(circle='circle15', selection_mode='nodes')
|
249 |
+
```
|
hvplot_docs/Notebook_Magics.md
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Notebook Magics
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import pandas as pd
|
6 |
+
import numpy as np
|
7 |
+
import holoviews as hv
|
8 |
+
from holoviews import opts
|
9 |
+
hv.extension('bokeh', 'matplotlib')
|
10 |
+
```
|
11 |
+
|
12 |
+
The [Applying Customizations](03-Applying_Customizations.ipynb) user guide describes the currently *recommended* way to customize your visualizations in HoloViews. Those mechanisms use standard Python syntax but they are not the only way to apply options as there is a much older approach for working with HoloViews that is specific to notebooks.
|
13 |
+
|
14 |
+
From the start, HoloViews aimed to enable rapid exploration of data in Jupyter Notebooks. For this reason, when you load the HoloViews extension in a notebook, you also get a set of IPython magics. IPython magics use a syntax that is not standard Python and the HoloViews magics only apply in the notebook environment (and not the IPython terminal for instance).
|
15 |
+
|
16 |
+
The advantages of the notebook magics are:
|
17 |
+
|
18 |
+
* They allow tab-completion in the notebook environment (but so do the more recent option builders and `hv.output` mechanisms).
|
19 |
+
* They allow very concise expression of options and settings.
|
20 |
+
|
21 |
+
|
22 |
+
Unfortunately, they also have some serious disadvantages:
|
23 |
+
|
24 |
+
* They are not Python syntax which makes it difficult to use code written with magics in notebooks anywhere else. For instance, it makes it harder to use such code with [bokeh server](https://bokeh.pydata.org/en/latest/docs/reference/server.html) or [panel](http://panel.pyviz.org/).
|
25 |
+
* They have their own special syntax which is very concise but also rather mysterious.
|
26 |
+
|
27 |
+
|
28 |
+
These disadvantages means the magics can be bewildering to anyone unfamiliar with the IPython specific syntax and HoloViews itself, and are no longer recommended for these reasons. This user guide documents these magics to allow people to understand older notebooks using HoloViews and to help people update these old notebooks to use the recommended Python API.
|
29 |
+
|
30 |
+
## Line and cell magics
|
31 |
+
|
32 |
+
There are two types of magic supported in Jupyter notebooks called *line magics* and *cell magics* respectively. Both typically appear at the top of code cells prefixed by `%` (line magics) or `%%` (cell magics).
|
33 |
+
|
34 |
+
* **line magics**: These can appear anywhere in a code cell and effect global changes to the current notebook session. HoloViews has the `%opts` and `%output` line magics.
|
35 |
+
* **cell magics**: These have to appear at the top of the cell and are used to modify how that cell is executed. HoloViews has the `%%opts` and `%%output` cell magics.
|
36 |
+
|
37 |
+
|
38 |
+
|
39 |
+
## The %opts and %%opts magics
|
40 |
+
|
41 |
+
These two magics are now served by `opts.defaults` and the `.opts` method respectively, as described in the [Applying Customizations](03-Applying_Customizations.ipynb) user guide.
|
42 |
+
|
43 |
+
* *The ``%opts`` line magic*: IPython specific syntax applied globally *[string format]*
|
44 |
+
* *The ``%%opts`` cell magic*: IPython specific syntax applies to displayed object *[string format]*
|
45 |
+
|
46 |
+
These magics have their own syntax that separates between *style*, *plot* and *norm* options described towards the end of the [Applying Customizations](03-Applying_Customizations.ipynb) user guide. The definition of the syntax is as follows:
|
47 |
+
|
48 |
+
```
|
49 |
+
[[spec] [normalization] [plotting options] [style options]]+
|
50 |
+
|
51 |
+
spec: A dotted type.group.label specification
|
52 |
+
(e.g. Curve,Sinusoid.Squared)
|
53 |
+
|
54 |
+
normalization: List of normalization options delimited by braces.
|
55 |
+
One of | -axiswise | -framewise | +axiswise | +framewise |
|
56 |
+
E.g. { +axiswise +framewise }
|
57 |
+
|
58 |
+
plotting options: List of plotting option keywords delimited by
|
59 |
+
square brackets. E.g. [show_title=False]
|
60 |
+
|
61 |
+
style options: List of style option keywords delimited by
|
62 |
+
parentheses. E.g. (lw=10 marker='+')
|
63 |
+
```
|
64 |
+
|
65 |
+
In other words, you have a list of spec strings (for instance `Curve` or `Curve.Sinusoid`) followed by keywords in either parentheses, square brackets or braces to represent the style, plot and normalization options respectively.
|
66 |
+
|
67 |
+
## Example `%%opts` cell magic
|
68 |
+
|
69 |
+
Here is the example from the [Customization](../getting_started/2-Customization.ipynb) section of the 'Getting Started' customized using the `%%opts` cell magic:
|
70 |
+
|
71 |
+
|
72 |
+
```python
|
73 |
+
%%opts Curve [height=200 width=900 xaxis=None tools=['hover']]
|
74 |
+
%%opts Curve (color='red' line_width=1.5)
|
75 |
+
%%opts Spikes [height=150 width=900 yaxis=None] (color='grey' line_width=0.25)
|
76 |
+
|
77 |
+
spike_train = pd.read_csv('../assets/spike_train.csv.gz')
|
78 |
+
curve = hv.Curve( spike_train, 'milliseconds', vdims='Hertz')
|
79 |
+
spikes = hv.Spikes(spike_train, 'milliseconds', vdims=[])
|
80 |
+
layout = (curve+spikes).cols(1)
|
81 |
+
layout
|
82 |
+
```
|
83 |
+
|
84 |
+
This `layout` object is now customized in a way that will persist, just like it would using the recommended `.opts` method together with option builders. It is worth noting that instead of just using element names, you can specify the group and label (e.g `Curve.Sinusoid.Squared`) to condition on that metadata, just the way you can using the option builders.
|
85 |
+
|
86 |
+
|
87 |
+
## Example `%opts` line magic
|
88 |
+
|
89 |
+
Here is how you could use the `%opts` line magic instead of `opts.default` as detailed in the [Applying Customizations](03-Applying_Customizations.ipynb) user guide:
|
90 |
+
|
91 |
+
|
92 |
+
|
93 |
+
|
94 |
+
```python
|
95 |
+
%opts HeatMap (cmap='Summer') [colorbar=True, toolbar='above']
|
96 |
+
```
|
97 |
+
|
98 |
+
Now all `HeatMap` elements will use the 'Summer' colormap, showing a colorbar with the Bokeh toolbar at the top:
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
data = [(chr(65+i), chr(97+j), i*j) for i in range(5) for j in range(5) if i!=j]
|
103 |
+
hv.HeatMap(data).sort()
|
104 |
+
```
|
105 |
+
|
106 |
+
## The `%output` line magic
|
107 |
+
|
108 |
+
The `%output` line magic has been fairly directly replaced by the `hv.output` utility. Here is an example of the `%output` line magic:
|
109 |
+
|
110 |
+
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
%output backend='matplotlib', fig='svg'
|
115 |
+
```
|
116 |
+
|
117 |
+
This ensures the following `Path` (and all subsequent `Path` objects) are rendered as SVG with matplotlib:
|
118 |
+
|
119 |
+
|
120 |
+
```python
|
121 |
+
lin = np.linspace(0, np.pi*2, 200)
|
122 |
+
|
123 |
+
def lissajous(t, a, b, delta):
|
124 |
+
return (np.sin(a * t + delta), np.sin(b * t), t)
|
125 |
+
|
126 |
+
path = hv.Path([lissajous(lin, 3, 5, np.pi/2)])
|
127 |
+
path.opts(opts.Path(linewidth=2, color='red', linestyle='dotted'))
|
128 |
+
```
|
129 |
+
|
130 |
+
For the purposes of this notebook, let us switch the plotting extension back to bokeh:
|
131 |
+
|
132 |
+
|
133 |
+
```python
|
134 |
+
%output backend='bokeh'
|
135 |
+
```
|
136 |
+
|
137 |
+
Note that the `%output` magic accepts the same set of output settings as the `hv.output` utility.
|
138 |
+
|
139 |
+
## The `%%output` cell magic
|
140 |
+
|
141 |
+
If we want to *temporarily* switch to matplotlib with some custom output settings, we can use the `%%output` cell magic in an example combines the `%%output` and `%%opts` cell magics in the same cell:
|
142 |
+
|
143 |
+
|
144 |
+
|
145 |
+
```python
|
146 |
+
%%output backend='matplotlib' fig='svg' size=50
|
147 |
+
%%opts Path (linewidth=3 color='blue')
|
148 |
+
lin = np.linspace(0, np.pi*2, 200)
|
149 |
+
|
150 |
+
def lissajous(t, a, b, delta):
|
151 |
+
return (np.sin(a * t + delta), np.sin(b * t), t)
|
152 |
+
|
153 |
+
hv.Path([lissajous(lin, 3, 5, np.pi/2)])
|
154 |
+
```
|
155 |
+
|
156 |
+
The recommended approach would now be to pass the `path` object to the `hv.output` utility as detailed in the [Applying Customizations](03-Applying_Customizations.ipynb) user guide. The magic processes the same set of output settings as the `hv.output` utility.
|
hvplot_docs/Pandas_API.md
ADDED
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
If hvplot and pandas are both installed, then we can use the pandas.options.plotting.backend to control the output of `pd.DataFrame.plot` and `pd.Series.plot`. This notebook is meant to recreate the [pandas visualization docs](https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html).
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import pandas as pd
|
7 |
+
|
8 |
+
pd.options.plotting.backend = 'holoviews'
|
9 |
+
```
|
10 |
+
|
11 |
+
## Basic Plotting: `plot`
|
12 |
+
|
13 |
+
The plot method on Series and DataFrame is just a simple wrapper around `hvplot()`:
|
14 |
+
|
15 |
+
|
16 |
+
```python
|
17 |
+
ts = pd.Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000))
|
18 |
+
ts = ts.cumsum()
|
19 |
+
ts.plot()
|
20 |
+
```
|
21 |
+
|
22 |
+
If the index consists of dates, they will be sorted (as long as `sort_date=True`) and formatted the x-axis nicely as per above.
|
23 |
+
|
24 |
+
On DataFrame, `plot()` is a convenience to plot all of the columns with labels:
|
25 |
+
|
26 |
+
|
27 |
+
```python
|
28 |
+
df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index, columns=list('ABCD'))
|
29 |
+
df = df.cumsum()
|
30 |
+
df.plot()
|
31 |
+
```
|
32 |
+
|
33 |
+
You can plot one column versus another using the *x* and *y* keywords in `plot()`:
|
34 |
+
|
35 |
+
|
36 |
+
```python
|
37 |
+
df3 = pd.DataFrame(np.random.randn(1000, 2), columns=['B', 'C']).cumsum()
|
38 |
+
df3['A'] = pd.Series(list(range(len(df))))
|
39 |
+
df3.plot(x='A', y='B')
|
40 |
+
```
|
41 |
+
|
42 |
+
**Note** For more formatting and styling options, see [formatting](#plot-formatting) below.
|
43 |
+
|
44 |
+
## Other Plots
|
45 |
+
|
46 |
+
Plotting methods allow for a handful of plot styles other than the default `line` plot. These methods can be provided as the `kind` keyword argument to `plot()`. These include:
|
47 |
+
|
48 |
+
* `bar` or `barh` for bar plots
|
49 |
+
* `hist` for histogram
|
50 |
+
* `box` for boxplot
|
51 |
+
* `kde` or `density` for density plots
|
52 |
+
* `area` for area plots
|
53 |
+
* `scatter` for scatter plots
|
54 |
+
* `hexbin` for hexagonal bin plots
|
55 |
+
* ~~`pie` for pie plots~~
|
56 |
+
|
57 |
+
For example, a bar plot can be created the following way:
|
58 |
+
|
59 |
+
|
60 |
+
```python
|
61 |
+
df.iloc[5].plot(kind='bar')
|
62 |
+
```
|
63 |
+
|
64 |
+
You can also create these other plots using the methods DataFrame.plot.<kind> instead of providing the kind keyword argument. This makes it easier to discover plot methods and the specific arguments they use:
|
65 |
+
|
66 |
+
In addition to these `kind`s, there are the `DataFrame.hist()`, and `DataFrame.boxplot()` methods, which use a separate interface.
|
67 |
+
|
68 |
+
Finally, there are several plotting functions in `hvplot.plotting` that take a `Series` or `DataFrame` as an argument. These include:
|
69 |
+
|
70 |
+
* `Scatter Matrix`
|
71 |
+
* `Andrews Curves`
|
72 |
+
* `Parallel Coordinates`
|
73 |
+
* `Lag Plot`
|
74 |
+
* ~~`Autocorrelation Plot`~~
|
75 |
+
* ~~`Bootstrap Plot`~~
|
76 |
+
* ~~`RadViz`~~
|
77 |
+
|
78 |
+
Plots may also be adorned with errorbars or tables.
|
79 |
+
|
80 |
+
### Bar plots
|
81 |
+
|
82 |
+
For labeled, non-time series data, you may wish to produce a bar plot:
|
83 |
+
|
84 |
+
|
85 |
+
|
86 |
+
|
87 |
+
```python
|
88 |
+
import holoviews as hv
|
89 |
+
```
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
df.iloc[5].plot.bar() * hv.HLine(0).opts(color='k')
|
94 |
+
```
|
95 |
+
|
96 |
+
Calling a DataFrame’s `plot.bar()` method produces a multiple bar plot:
|
97 |
+
|
98 |
+
|
99 |
+
```python
|
100 |
+
df2 = pd.DataFrame(np.random.rand(10, 4), columns=['a', 'b', 'c', 'd'])
|
101 |
+
df2.plot.bar()
|
102 |
+
```
|
103 |
+
|
104 |
+
To produce a stacked bar plot, pass `stacked=True`:
|
105 |
+
|
106 |
+
|
107 |
+
```python
|
108 |
+
df2.plot.bar(stacked=True)
|
109 |
+
```
|
110 |
+
|
111 |
+
To get horizontal bar plots, use the `barh` method:
|
112 |
+
|
113 |
+
|
114 |
+
```python
|
115 |
+
df2.plot.barh(stacked=True)
|
116 |
+
```
|
117 |
+
|
118 |
+
### Histograms
|
119 |
+
|
120 |
+
Histogram can be drawn by using the `DataFrame.plot.hist()` and `Series.plot.hist()` methods.
|
121 |
+
|
122 |
+
|
123 |
+
```python
|
124 |
+
df4 = pd.DataFrame({'a': np.random.randn(1000) + 1, 'b': np.random.randn(1000),
|
125 |
+
'c': np.random.randn(1000) - 1}, columns=['a', 'b', 'c'])
|
126 |
+
|
127 |
+
df4.plot.hist(alpha=0.5)
|
128 |
+
```
|
129 |
+
|
130 |
+
Using the matplotlib backend, histograms can be stacked using `stacked=True`; **stacking is not yet supported in the holoviews backend**. Bin size can be changed by `bins` keyword.
|
131 |
+
|
132 |
+
|
133 |
+
```python
|
134 |
+
# Stacked not supported
|
135 |
+
df4.plot.hist(stacked=True, bins=20)
|
136 |
+
```
|
137 |
+
|
138 |
+
You can pass other keywords supported by matplotlib hist. For example, horizontal and cumulative histogram can be drawn by `invert=True` and `cumulative=True`.
|
139 |
+
|
140 |
+
|
141 |
+
```python
|
142 |
+
df4['a'].plot.hist(invert=True, cumulative=True)
|
143 |
+
```
|
144 |
+
|
145 |
+
The existing interface `DataFrame.hist` to plot histogram still can be used.
|
146 |
+
|
147 |
+
|
148 |
+
```python
|
149 |
+
df['A'].diff().hist()
|
150 |
+
```
|
151 |
+
|
152 |
+
`DataFrame.hist()` plots the histograms of the columns on multiple subplots:
|
153 |
+
|
154 |
+
|
155 |
+
```python
|
156 |
+
df.diff().hist(color='k', alpha=0.5, bins=50, subplots=True, width=300).cols(2)
|
157 |
+
```
|
158 |
+
|
159 |
+
The by keyword can be specified to plot grouped histograms:
|
160 |
+
|
161 |
+
|
162 |
+
```python
|
163 |
+
# by does not support arrays, instead the array should be added as a column
|
164 |
+
data = pd.Series(np.random.randn(1000))
|
165 |
+
data = pd.DataFrame({'data': data, 'by_column': np.random.randint(0, 4, 1000)})
|
166 |
+
data.hist(by='by_column', width=300, subplots=True).cols(2)
|
167 |
+
```
|
168 |
+
|
169 |
+
### Box Plots
|
170 |
+
|
171 |
+
Boxplot can be drawn calling `Series.plot.box()` and `DataFrame.plot.box()`, or `DataFrame.boxplot()` to visualize the distribution of values within each column.
|
172 |
+
|
173 |
+
For instance, here is a boxplot representing five trials of 10 observations of a uniform random variable on [0,1).
|
174 |
+
|
175 |
+
|
176 |
+
|
177 |
+
```python
|
178 |
+
df = pd.DataFrame(np.random.rand(10, 5), columns=['A', 'B', 'C', 'D', 'E'])
|
179 |
+
df.plot.box()
|
180 |
+
```
|
181 |
+
|
182 |
+
Using the matplotlib backend, boxplot can be colorized by passing `color` keyword. You can pass a `dict` whose keys are `boxes`, `whiskers`, `medians` and `caps`.
|
183 |
+
|
184 |
+
**This behavior is not supported in the holoviews backend.**
|
185 |
+
|
186 |
+
You can pass other keywords supported by holoviews boxplot. For example, horizontal and custom-positioned boxplot can be drawn by `invert=True` and positions keywords.
|
187 |
+
|
188 |
+
|
189 |
+
```python
|
190 |
+
# positions not supported
|
191 |
+
df.plot.box(invert=True, positions=[1, 4, 5, 6, 8])
|
192 |
+
```
|
193 |
+
|
194 |
+
See the boxplot method and the matplotlib boxplot documentation for more.
|
195 |
+
|
196 |
+
The existing interface `DataFrame.boxplot` to plot boxplot still can be used.
|
197 |
+
|
198 |
+
|
199 |
+
```python
|
200 |
+
df = pd.DataFrame(np.random.rand(10, 5))
|
201 |
+
df.boxplot()
|
202 |
+
```
|
203 |
+
|
204 |
+
You can create a stratified boxplot using the `groupby` keyword argument to create groupings. For instance,
|
205 |
+
|
206 |
+
|
207 |
+
```python
|
208 |
+
df = pd.DataFrame(np.random.rand(10,2), columns=['Col1', 'Col2'])
|
209 |
+
|
210 |
+
df['X'] = pd.Series(['A','A','A','A','A','B','B','B','B','B'])
|
211 |
+
|
212 |
+
df.boxplot(col='X')
|
213 |
+
```
|
214 |
+
|
215 |
+
You can also pass a subset of columns to plot, as well as group by multiple columns:
|
216 |
+
|
217 |
+
|
218 |
+
```python
|
219 |
+
df = pd.DataFrame(np.random.rand(10,3), columns=['Col1', 'Col2', 'Col3'])
|
220 |
+
|
221 |
+
df['X'] = pd.Series(['A','A','A','A','A','B','B','B','B','B'])
|
222 |
+
df['Y'] = pd.Series(['A','B','A','B','A','B','A','B','A','B'])
|
223 |
+
|
224 |
+
df.boxplot(y=['Col1','Col2'], col='X', row='Y')
|
225 |
+
```
|
226 |
+
|
227 |
+
|
228 |
+
```python
|
229 |
+
df_box = pd.DataFrame(np.random.randn(50, 2))
|
230 |
+
df_box['g'] = np.random.choice(['A', 'B'], size=50)
|
231 |
+
df_box.loc[df_box['g'] == 'B', 1] += 3
|
232 |
+
df_box.boxplot(row='g')
|
233 |
+
```
|
234 |
+
|
235 |
+
For more control over the ordering of the levels, we can perform a groupby on the data before plotting.
|
236 |
+
|
237 |
+
|
238 |
+
```python
|
239 |
+
df_box.groupby('g').boxplot()
|
240 |
+
```
|
241 |
+
|
242 |
+
### Area Plot
|
243 |
+
|
244 |
+
You can create area plots with `Series.plot.area()` and `DataFrame.plot.area()`. Area plots are *not* stacked by default. To produce stacked area plot, each column must be either all positive or all negative values.
|
245 |
+
|
246 |
+
When input data contains *NaN*, it will be automatically filled by 0. If you want to drop or fill by different values, use `dataframe.dropna()` or `dataframe.fillna()` before calling plot.
|
247 |
+
|
248 |
+
|
249 |
+
```python
|
250 |
+
df = pd.DataFrame(np.random.rand(10, 4), columns=['a', 'b', 'c', 'd'])
|
251 |
+
df.plot.area(alpha=0.5)
|
252 |
+
```
|
253 |
+
|
254 |
+
To produce an stacked plot, pass `stacked=True`.
|
255 |
+
|
256 |
+
|
257 |
+
```python
|
258 |
+
df.plot.area(stacked=True)
|
259 |
+
```
|
260 |
+
|
261 |
+
### Scatter Plot
|
262 |
+
|
263 |
+
Scatter plot can be drawn by using the `DataFrame.plot.scatter()` method. Scatter plot require that x and y be specified using the `x` and `y` keywords.
|
264 |
+
|
265 |
+
|
266 |
+
```python
|
267 |
+
df = pd.DataFrame(np.random.rand(50, 4), columns=['a', 'b', 'c', 'd'])
|
268 |
+
df.plot.scatter(x='a', y='b')
|
269 |
+
```
|
270 |
+
|
271 |
+
To plot multiple column groups in a single axes, repeat `plot` method and then use the overlay operator (`*`) to combine the plots. It is recommended to specify color and label keywords to distinguish each groups.
|
272 |
+
|
273 |
+
|
274 |
+
```python
|
275 |
+
plot_1 = df.plot.scatter(x='a', y='b', color='DarkBlue', label='Group 1')
|
276 |
+
plot_2 = df.plot.scatter(x='c', y='d', color='DarkGreen', label='Group 2')
|
277 |
+
|
278 |
+
plot_1 * plot_2
|
279 |
+
```
|
280 |
+
|
281 |
+
The keyword `c` may be given as the name of a column to provide colors for each point:
|
282 |
+
|
283 |
+
|
284 |
+
```python
|
285 |
+
df.plot.scatter(x='a', y='b', c='c', s=50)
|
286 |
+
```
|
287 |
+
|
288 |
+
You can pass other keywords supported by matplotlib `scatter`. The example below shows a bubble chart using a column of the `DataFrame` as the bubble size.
|
289 |
+
|
290 |
+
|
291 |
+
```python
|
292 |
+
df.plot.scatter(x='a', y='b', s=df['c']*500)
|
293 |
+
```
|
294 |
+
|
295 |
+
The same effect can be accomplished using the `scale` option.
|
296 |
+
|
297 |
+
|
298 |
+
```python
|
299 |
+
df.plot.scatter(x='a', y='b', s='c', scale=25)
|
300 |
+
```
|
301 |
+
|
302 |
+
### Hexagonal Bin Plot
|
303 |
+
|
304 |
+
|
305 |
+
You can create hexagonal bin plots with `DataFrame.plot.hexbin()`. Hexbin plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually.
|
306 |
+
|
307 |
+
|
308 |
+
```python
|
309 |
+
df = pd.DataFrame(np.random.randn(1000, 2), columns=['a', 'b'])
|
310 |
+
df['b'] = df['b'] + np.arange(1000)
|
311 |
+
|
312 |
+
df.plot.hexbin(x='a', y='b', gridsize=25, width=500, height=400)
|
313 |
+
```
|
314 |
+
|
315 |
+
A useful keyword argument is `gridsize`; it controls the number of hexagons in the x-direction, and defaults to 100. A larger `gridsize` means more, smaller bins.
|
316 |
+
|
317 |
+
By default, a histogram of the counts around each `(x, y)` point is computed. You can specify alternative aggregations by passing values to the `C` and `reduce_C_function` arguments. `C` specifies the value at each `(x, y)` point and `reduce_C_function` is a function of one argument that reduces all the values in a bin to a single number (e.g. `mean`, `max`, `sum`, `std`). In this example the positions are given by columns `a` and `b`, while the value is given by column z. The bins are aggregated with numpy’s `max` function.
|
318 |
+
|
319 |
+
|
320 |
+
```python
|
321 |
+
df = pd.DataFrame(np.random.randn(1000, 2), columns=['a', 'b'])
|
322 |
+
df['b'] = df['b'] = df['b'] + np.arange(1000)
|
323 |
+
df['z'] = np.random.uniform(0, 3, 1000)
|
324 |
+
|
325 |
+
df.plot.hexbin(x='a', y='b', C='z', reduce_function=np.max, gridsize=25, width=500, height=400)
|
326 |
+
```
|
327 |
+
|
328 |
+
### Density Plot
|
329 |
+
|
330 |
+
You can create density plots using the `Series.plot.kde()` and `DataFrame.plot.kde()` methods.
|
331 |
+
|
332 |
+
|
333 |
+
```python
|
334 |
+
ser = pd.Series(np.random.randn(1000))
|
335 |
+
|
336 |
+
ser.plot.kde()
|
337 |
+
```
|
338 |
+
|
339 |
+
### Pie plot
|
340 |
+
|
341 |
+
**NOT SUPPORTED**
|
342 |
+
|
343 |
+
|
344 |
+
## Plotting Tools
|
345 |
+
|
346 |
+
These functions can be imported from `hvplot.plotting` and take a `Series` or `DataFrame` as an argument.
|
347 |
+
|
348 |
+
### Scatter Matrix Plot
|
349 |
+
|
350 |
+
You can create a scatter plot matrix using the `scatter_matrix` function:
|
351 |
+
|
352 |
+
|
353 |
+
```python
|
354 |
+
import pandas as pd, numpy as np
|
355 |
+
```
|
356 |
+
|
357 |
+
|
358 |
+
```python
|
359 |
+
from hvplot import scatter_matrix
|
360 |
+
|
361 |
+
df = pd.DataFrame(np.random.randn(1000, 4), columns=['a', 'b', 'c', 'd'])
|
362 |
+
|
363 |
+
scatter_matrix(df, alpha=0.2, diagonal='kde')
|
364 |
+
```
|
365 |
+
|
366 |
+
Since scatter matrix plots may generate a large number of points (the one above renders 120,000 points!), you may want to take advantage of the power provided by [Datashader](https://datashader.org/) to rasterize the off-diagonal plots into a fixed-resolution representation:
|
367 |
+
|
368 |
+
|
369 |
+
```python
|
370 |
+
df = pd.DataFrame(np.random.randn(10000, 4), columns=['a', 'b', 'c', 'd'])
|
371 |
+
|
372 |
+
scatter_matrix(df, rasterize=True, dynspread=True)
|
373 |
+
```
|
374 |
+
|
375 |
+
### Andrews Curves
|
376 |
+
|
377 |
+
Andrews curves allow one to plot multivariate data as a large number of curves that are created using the attributes of samples as coefficients for Fourier series. By coloring these curves differently for each class it is possible to visualize data clustering. Curves belonging to samples of the same class will usually be closer together and form larger structures.
|
378 |
+
|
379 |
+
|
380 |
+
|
381 |
+
|
382 |
+
```python
|
383 |
+
from bokeh.sampledata import iris
|
384 |
+
from hvplot import andrews_curves
|
385 |
+
|
386 |
+
data = iris.flowers
|
387 |
+
|
388 |
+
andrews_curves(data, 'species')
|
389 |
+
```
|
390 |
+
|
391 |
+
### Parallel Coordinates
|
392 |
+
|
393 |
+
Parallel coordinates is a plotting technique for plotting multivariate data. It allows one to see clusters in data and to estimate other statistics visually. Using parallel coordinates points are represented as connected line segments. Each vertical line represents one attribute. One set of connected line segments represents one data point. Points that tend to cluster will appear closer together.
|
394 |
+
|
395 |
+
|
396 |
+
```python
|
397 |
+
from hvplot import parallel_coordinates
|
398 |
+
|
399 |
+
parallel_coordinates(data, 'species')
|
400 |
+
```
|
401 |
+
|
402 |
+
### Lag Plot
|
403 |
+
|
404 |
+
Lag plots are used to check if a data set or time series is random. Random data should not exhibit any structure in the lag plot. Non-random structure implies that the underlying data are not random.
|
405 |
+
|
406 |
+
|
407 |
+
```python
|
408 |
+
from hvplot import lag_plot
|
409 |
+
|
410 |
+
data = pd.Series(0.1 * np.random.rand(1000) +
|
411 |
+
0.9 * np.sin(np.linspace(-99 * np.pi, 99 * np.pi, num=1000)))
|
412 |
+
|
413 |
+
lag_plot(data, width=400, height=400)
|
414 |
+
```
|
415 |
+
|
416 |
+
|
417 |
+
### Autocorrelation Plot
|
418 |
+
|
419 |
+
**NOT SUPPORTED**
|
420 |
+
|
421 |
+
### Bootstrap Plot
|
422 |
+
|
423 |
+
**NOT SUPPORTED**
|
424 |
+
|
425 |
+
### RadViz
|
426 |
+
|
427 |
+
**NOT SUPPORTED**
|
428 |
+
|
429 |
+
|
430 |
+
|
431 |
+
|
432 |
+
## Plot Formatting
|
433 |
+
|
434 |
+
### General plot style arguments
|
435 |
+
|
436 |
+
Most plotting methods have a set of keyword arguments that control the layout and formatting of the returned plot:
|
437 |
+
|
438 |
+
|
439 |
+
```python
|
440 |
+
ts.plot(c='k', line_dash='dashed', label='Series')
|
441 |
+
```
|
442 |
+
|
443 |
+
For each kind of plot (e.g. *line*, *bar*, *scatter*) any additional arguments keywords are passed along to the corresponding holoviews object (`hv.Curve`, `hv.Bar`, `hv.Scatter`). These can be used to control additional styling, beyond what pandas provides.
|
444 |
+
|
445 |
+
### Controlling the Legend
|
446 |
+
|
447 |
+
You may set the `legend` argument to `False` to hide the legend, which is shown by default. You can also control the placement of the legend using the same `legend` argument set to one of: 'top_right', 'top_left', 'bottom_left', 'bottom_right', 'right', 'left', 'top', or 'bottom'.
|
448 |
+
|
449 |
+
|
450 |
+
```python
|
451 |
+
df = pd.DataFrame(np.random.randn(1000, 4),
|
452 |
+
index=ts.index, columns=list('ABCD'))
|
453 |
+
df = df.cumsum()
|
454 |
+
|
455 |
+
df.plot(legend=False)
|
456 |
+
```
|
457 |
+
|
458 |
+
|
459 |
+
```python
|
460 |
+
df.plot(legend='top_left')
|
461 |
+
```
|
462 |
+
|
463 |
+
### Scales
|
464 |
+
|
465 |
+
You may pass `logy` to get a log-scale Y axis, similarly use `logx` to get a log-scale on the X axis or `logz` to get a log-scale on the color axis.
|
466 |
+
|
467 |
+
|
468 |
+
```python
|
469 |
+
ts = pd.Series(np.random.randn(1000),
|
470 |
+
index=pd.date_range('1/1/2000', periods=1000))
|
471 |
+
|
472 |
+
ts = np.exp(ts.cumsum())
|
473 |
+
|
474 |
+
ts.plot(logy=True)
|
475 |
+
```
|
476 |
+
|
477 |
+
|
478 |
+
```python
|
479 |
+
df = pd.DataFrame(np.random.randn(1000, 2), columns=['a', 'b'])
|
480 |
+
df['b'] = df['b'] + np.arange(1000)
|
481 |
+
df.plot.hexbin(x='a', y='b', gridsize=25, width=500, height=400, logz=True)
|
482 |
+
```
|
483 |
+
|
484 |
+
### Plotting on a Secondary Y-axis
|
485 |
+
|
486 |
+
**NOT SUPPORTED**
|
487 |
+
|
488 |
+
### Suppressing tick resolution adjustment
|
489 |
+
pandas includes automatic tick resolution adjustment for regular frequency time-series data. For limited cases where pandas cannot infer the frequency information (e.g., in an externally created twinx), you can choose to suppress this behavior for alignment purposes.
|
490 |
+
|
491 |
+
Here is the default behavior, notice how the x-axis tick labeling is performed:
|
492 |
+
|
493 |
+
|
494 |
+
```python
|
495 |
+
df = pd.DataFrame(np.random.randn(1000, 4),
|
496 |
+
index=ts.index, columns=list('ABCD'))
|
497 |
+
df = df.cumsum()
|
498 |
+
df.A.plot()
|
499 |
+
```
|
500 |
+
|
501 |
+
Formatters can be set using format strings, by declaring bokeh TickFormatters, or using custom functions. See [HoloViews Tick Docs](https://holoviews.org/user_guide/Customizing_Plots.html#Axis-ticks) for more information.
|
502 |
+
|
503 |
+
|
504 |
+
```python
|
505 |
+
from bokeh.models.formatters import DatetimeTickFormatter
|
506 |
+
|
507 |
+
formatter = DatetimeTickFormatter(months='%b %Y')
|
508 |
+
```
|
509 |
+
|
510 |
+
|
511 |
+
```python
|
512 |
+
df.A.plot(yformatter='$%.2f', xformatter=formatter)
|
513 |
+
```
|
514 |
+
|
515 |
+
### Subplots
|
516 |
+
|
517 |
+
Each `Series` in a `DataFrame` can be plotted on a different axis with the `subplots` keyword:
|
518 |
+
|
519 |
+
|
520 |
+
```python
|
521 |
+
df = pd.DataFrame(np.random.randn(1000, 4),
|
522 |
+
index=ts.index, columns=list('ABCD'))
|
523 |
+
df = df.cumsum()
|
524 |
+
df.plot(subplots=True, height=150).cols(1)
|
525 |
+
```
|
526 |
+
|
527 |
+
### Controlling layout and targeting multiple axes
|
528 |
+
|
529 |
+
The layout of subplots can be specified using `.cols(n)`.
|
530 |
+
|
531 |
+
|
532 |
+
```python
|
533 |
+
df.plot(subplots=True, shared_axes=False, width=150).cols(3)
|
534 |
+
```
|
535 |
+
|
536 |
+
### Plotting with error bars
|
537 |
+
|
538 |
+
Plotting with error bars is *not* supported in `DataFrame.plot()` and `Series.plot()`. To add errorbars, users should fall back to using hvplot directly.
|
539 |
+
|
540 |
+
|
541 |
+
```python
|
542 |
+
import hvplot.pandas # noqa
|
543 |
+
```
|
544 |
+
|
545 |
+
|
546 |
+
```python
|
547 |
+
df3 = pd.DataFrame({'data1': [3, 2, 4, 3, 2, 4, 3, 2],
|
548 |
+
'data2': [6, 5, 7, 5, 4, 5, 6, 5]})
|
549 |
+
mean_std = pd.DataFrame({'mean': df3.mean(), 'std': df3.std()})
|
550 |
+
mean_std
|
551 |
+
```
|
552 |
+
|
553 |
+
|
554 |
+
```python
|
555 |
+
(mean_std.plot.bar(y='mean', alpha=0.7) * \
|
556 |
+
mean_std.hvplot.errorbars(x='index', y='mean', yerr1='std')
|
557 |
+
).opts(show_legend=False).redim.range(mean=(0, 6.5))
|
558 |
+
```
|
559 |
+
|
560 |
+
### Plotting tables
|
561 |
+
|
562 |
+
The matplotlib backend includes support for the `table` keyword in `DataFrame.plot()` and `Series.plot()`. This same effect can be accomplished with holoviews by using `hvplot.table` and creating a layout of the resulting plots.
|
563 |
+
|
564 |
+
|
565 |
+
```python
|
566 |
+
df = pd.DataFrame(np.random.rand(5, 3), columns=['a', 'b', 'c'])
|
567 |
+
|
568 |
+
(df.plot(xaxis=False, legend='top_right') + \
|
569 |
+
df.T.hvplot.table()).cols(1)
|
570 |
+
```
|
571 |
+
|
572 |
+
### Colormaps
|
573 |
+
|
574 |
+
A potential issue when plotting a large number of columns is that it can be difficult to distinguish some series due to repetition in the default colors. To remedy this, DataFrame plotting supports the use of the `colormap` argument, which accepts either a colormap or a string that is a name of a colormap.
|
575 |
+
|
576 |
+
To use the cubehelix colormap, we can pass `colormap='cubehelix'`.
|
577 |
+
|
578 |
+
|
579 |
+
```python
|
580 |
+
df = pd.DataFrame(np.random.randn(1000, 10), index=ts.index)
|
581 |
+
df = df.cumsum()
|
582 |
+
df.plot(colormap='cubehelix')
|
583 |
+
```
|
584 |
+
|
585 |
+
Colormaps can also be used with other plot types, like bar charts:
|
586 |
+
|
587 |
+
|
588 |
+
```python
|
589 |
+
dd = pd.DataFrame(np.random.randn(10, 10)).applymap(abs)
|
590 |
+
dd = dd.cumsum()
|
591 |
+
dd.plot.bar(colormap='Greens')
|
592 |
+
```
|
593 |
+
|
594 |
+
Parallel coordinates charts:
|
595 |
+
|
596 |
+
|
597 |
+
```python
|
598 |
+
from bokeh.sampledata import iris
|
599 |
+
from hvplot import parallel_coordinates, andrews_curves
|
600 |
+
|
601 |
+
data = iris.flowers
|
602 |
+
parallel_coordinates(data, 'species', colormap='gist_rainbow')
|
603 |
+
```
|
604 |
+
|
605 |
+
Andrews curves charts:
|
606 |
+
|
607 |
+
|
608 |
+
```python
|
609 |
+
andrews_curves(data, 'species', colormap='winter')
|
610 |
+
```
|
611 |
+
|
612 |
+
## Plotting directly with holoviews
|
613 |
+
|
614 |
+
In some situations it may still be preferable or necessary to prepare plots directly with hvplot or holoviews, for instance when a certain type of plot or customization is not (yet) supported by pandas. `Series` and `DataFrame` objects behave like arrays and can therefore be passed directly to holoviews functions without explicit casts.
|
615 |
+
|
616 |
+
|
617 |
+
```python
|
618 |
+
import holoviews as hv
|
619 |
+
|
620 |
+
price = pd.Series(np.random.randn(150).cumsum(),
|
621 |
+
index=pd.date_range('2000-1-1', periods=150, freq='B'), name='price')
|
622 |
+
ma = price.rolling(20).mean()
|
623 |
+
mstd = price.rolling(20).std()
|
624 |
+
|
625 |
+
price.plot(c='k') * ma.plot(c='b', label='mean') * \
|
626 |
+
hv.Area((mstd.index, ma - 2 * mstd, ma + 2 * mstd),
|
627 |
+
vdims=['y', 'y2']).opts(color='b', alpha=0.2)
|
628 |
+
```
|
hvplot_docs/Plots_and_Renderers.md
ADDED
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
import holoviews as hv
|
3 |
+
import numpy as np
|
4 |
+
```
|
5 |
+
|
6 |
+
HoloViews ordinarily hides the plotting machinery from the user. This allows for very quick iteration over different visualizations to explore a dataset, however it is often important to customize the precise details of a plot. HoloViews makes it very easy to customize existing plots, or even create completely novel plots. This manual will provide a general overview of the plotting system.
|
7 |
+
|
8 |
+
The separation of the data from the precise details of the visualization is one of the core principles of the HoloViews. [``Elements``](http://holoviews.org/reference/index.html#elements) provide thin wrappers around chunks of actual data, while [containers](http://holoviews.org/reference/index.html#containers) allow composing these Elements into overlays, layouts, grids and animations/widgets. Each Element or container type has a corresponding plotting class, which renders a visual representation of the data for a particular backend. While the precise details of the implementation differ between backends to accommodate the vastly different APIs plotting backends provide, many of the high-level details are shared across backends.
|
9 |
+
|
10 |
+
# The Store object
|
11 |
+
|
12 |
+
The association between an Element or container and the backend specific plotting class is held on the global ``Store`` object. The ``Store`` object holds a ``registry`` of plot objects for each backend. We can view the registry for each backend by accessing ``Store.registry`` directly:
|
13 |
+
|
14 |
+
|
15 |
+
```python
|
16 |
+
import holoviews.plotting.mpl
|
17 |
+
list(hv.Store.registry['matplotlib'].items())[0:5]
|
18 |
+
```
|
19 |
+
|
20 |
+
The Store object provides a global registry not only for the plots themselves but also creates an entry in the OptionsTree for that particular backend. This allows options for that backend to be validated and enables setting plot, style and normalization options via the [Options system](03-Applying_Customizations.ipynb) system. We can view the ``OptionsTree`` object by requesting it from the store. We'll make a copy with just the first few entries so we can view the structure of the tree:
|
21 |
+
|
22 |
+
|
23 |
+
```python
|
24 |
+
opts = hv.Store.options(backend='matplotlib')
|
25 |
+
hv.core.options.OptionTree(opts.items()[0:10], groups=['plot', 'style', 'norm'])
|
26 |
+
```
|
27 |
+
|
28 |
+
# Saving and rendering
|
29 |
+
|
30 |
+
The easiest entry points to rendering a HoloViews object either to the backend specific representation (e.g. a matplotlib figure) or directly to file are the ``hv.render`` and ``hv.save`` functions. Both are shortcuts for using an actual ``Renderer`` object, which will be introduced in the next section. To start with we will create a simple object, a ``Scatter`` element:
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
curve = hv.Curve(range(10))
|
35 |
+
curve
|
36 |
+
```
|
37 |
+
|
38 |
+
This abstract and declarative representation of a ``Curve`` can be turned into an actual plot object, defaulting to the currently selected (or default) backend:
|
39 |
+
|
40 |
+
|
41 |
+
```python
|
42 |
+
fig = hv.render(curve)
|
43 |
+
print(type(fig))
|
44 |
+
```
|
45 |
+
|
46 |
+
By providing an explicit ``backend`` keyword the plot can be rendered using a different backend.
|
47 |
+
|
48 |
+
|
49 |
+
```python
|
50 |
+
p = hv.render(curve, backend='bokeh')
|
51 |
+
print(type(p))
|
52 |
+
```
|
53 |
+
|
54 |
+
This can often be useful to customize a plot in more detail by tweaking styling in ways that are not directly supported by HoloViews. An alternative to ``hv.render`` in case you do not want to fully switch to the underlying plotting API are ``hooks``, which are a plot option on all elements. These allow defining hooks which modify a plot after it has been rendered in the backend but before it is displayed.
|
55 |
+
|
56 |
+
A ``hook`` is given the HoloViews plot instance and the currently rendered element and thereby provides access to the rendered plot object to apply any customizations and changes which are not exposed by HoloViews:
|
57 |
+
|
58 |
+
|
59 |
+
```python
|
60 |
+
def hook(plot, element):
|
61 |
+
# Allows accessing the backends figure object
|
62 |
+
plot.state
|
63 |
+
|
64 |
+
# The handles contain common plot objects
|
65 |
+
plot.handles
|
66 |
+
|
67 |
+
curve = curve.opts(hooks=[hook])
|
68 |
+
```
|
69 |
+
|
70 |
+
Hooks allow for extensive tweaking of objects before they are finished rendering, without having to entirely abandon HoloViews' API and render the backend's plot object manually.
|
71 |
+
|
72 |
+
In much the same way the ``hv.save`` function allows exporting plots straight to a file, by default inferring the format from the file extension:
|
73 |
+
|
74 |
+
|
75 |
+
```python
|
76 |
+
hv.save(curve, 'curve.png')
|
77 |
+
```
|
78 |
+
|
79 |
+
Just like ``hv.render`` the ``hv.save`` function also allows specifying an explicit backend.
|
80 |
+
|
81 |
+
|
82 |
+
```python
|
83 |
+
hv.save(curve, 'curve.html', backend='bokeh')
|
84 |
+
```
|
85 |
+
|
86 |
+
Additionally for ambiguous file extensions such as HTML it may be necessary to specify an explicit fmt to override the default, e.g. in the case of 'html' output the widgets will default to ``fmt='widgets'``, which may be changed to scrubber widgets using ``fmt='scrubber'``.
|
87 |
+
|
88 |
+
# Renderers
|
89 |
+
|
90 |
+
HoloViews provides a general ``Renderer`` baseclass, which defines a general interface to render the output from different backends to a number of standard output formats such as ``png``, ``html`` or ``svg``. The ``__call__`` method on the Renderer automatically looks up and instantiates the registered plotting classes for an object it is passed and then returns the output in the requested format. To make this a bit clearer we'll break this down step by step. First we'll get a handle on the ``MPLRenderer`` and create an object to render.
|
91 |
+
|
92 |
+
Renderers aren't registered with the Store until the corresponding backends have been imported. Loading the notebook extension with ``hv.notebook_extension('matplotlib')`` is one way of loading a backend and registering a renderer. Another is to simply import the corresponding plotting module as we did above.
|
93 |
+
|
94 |
+
|
95 |
+
```python
|
96 |
+
hv.Store.renderers
|
97 |
+
```
|
98 |
+
|
99 |
+
This is one way to access a Renderer, another is to instantiate a Renderer instance directly, allowing you to override some of the default plot options.
|
100 |
+
|
101 |
+
|
102 |
+
```python
|
103 |
+
renderer = hv.plotting.mpl.MPLRenderer.instance(dpi=120)
|
104 |
+
```
|
105 |
+
|
106 |
+
The recommended way to get a handle on a renderer is to use the ``hv.renderer`` function which will also handle imports for you:
|
107 |
+
|
108 |
+
|
109 |
+
```python
|
110 |
+
hv.renderer('matplotlib')
|
111 |
+
```
|
112 |
+
|
113 |
+
# Working with a Renderer
|
114 |
+
|
115 |
+
A ``Renderer`` in HoloViews is responsible for instantiating a HoloViews plotting class. It does this by looking up the plotting class in the ``Store.registry``:
|
116 |
+
|
117 |
+
|
118 |
+
```python
|
119 |
+
hv.Store.registry['matplotlib'][hv.Curve]
|
120 |
+
```
|
121 |
+
|
122 |
+
If we create a ``Curve`` we can instantiate a plotting class from it using the ``Renderer.get_plot`` method:
|
123 |
+
|
124 |
+
|
125 |
+
```python
|
126 |
+
curve = hv.Curve(range(10))
|
127 |
+
curve_plot = renderer.get_plot(curve)
|
128 |
+
curve_plot
|
129 |
+
```
|
130 |
+
|
131 |
+
We will revisit how to work with ``Plot`` instances later. For now all we need to know is that they are responsible for translating the HoloViews object (like the ``Curve``) into a backend specific plotting object, accessible on ``Plot.state``:
|
132 |
+
|
133 |
+
|
134 |
+
```python
|
135 |
+
print(type(curve_plot.state), curve_plot.state)
|
136 |
+
curve_plot.state
|
137 |
+
```
|
138 |
+
|
139 |
+
In case of the matplotlib backend this is a ``Figure`` object. However the ``Renderer`` ignores the specific representation of the plot, instead providing a unified interface to translating it into a representation that can displayed, i.e. either an image format or an HTML representation.
|
140 |
+
|
141 |
+
In this way we can convert the curve directly to its ``png`` representation by calling the ``Renderer`` with the object and the format:
|
142 |
+
|
143 |
+
|
144 |
+
```python
|
145 |
+
from IPython.display import display_png
|
146 |
+
png, info = renderer(curve, fmt='png')
|
147 |
+
print(info)
|
148 |
+
display_png(png, raw=True)
|
149 |
+
```
|
150 |
+
|
151 |
+
<div class="alert alert-success">
|
152 |
+
Tip: To find more information about any HoloViews object use <code>hv.help</code> to print a detailed docstring.
|
153 |
+
</div>
|
154 |
+
|
155 |
+
The valid figure display formats can be seen in the docstring of the Renderer or directly on the parameter:
|
156 |
+
|
157 |
+
|
158 |
+
```python
|
159 |
+
renderer.param['fig'].objects
|
160 |
+
```
|
161 |
+
|
162 |
+
Note that to export PNG files using the [bokeh renderer](./Plotting_with_Bokeh.ipynb) you may need to install some additional dependencies [detailed here](https://bokeh.pydata.org/en/latest/docs/user_guide/export.html). If you are using conda, it is currently sufficient to run ``conda install selenium phantomjs pillow``.
|
163 |
+
|
164 |
+
In this way we can easily render the plot in different formats:
|
165 |
+
|
166 |
+
|
167 |
+
```python
|
168 |
+
from IPython.display import display_svg
|
169 |
+
svg, info = renderer(curve, fmt='svg')
|
170 |
+
print(info)
|
171 |
+
display_svg(svg, raw=True)
|
172 |
+
```
|
173 |
+
|
174 |
+
We could save these byte string representations ourselves but the ``Renderer`` provides a convenient ``save`` method to do so. Simply supply the object the filename and the format, which doubles as the file extension:
|
175 |
+
|
176 |
+
|
177 |
+
```python
|
178 |
+
renderer.save(curve, '/tmp/test', fmt='png')
|
179 |
+
```
|
180 |
+
|
181 |
+
Another convenient way to render the object is to wrap it in HTML, which we can do with the ``html`` method:
|
182 |
+
|
183 |
+
|
184 |
+
```python
|
185 |
+
from IPython.display import display_html
|
186 |
+
html = renderer.html(curve)
|
187 |
+
display_html(html, raw=True)
|
188 |
+
```
|
189 |
+
|
190 |
+
Rendering plots containing ``HoloMap`` and ``DynamicMap`` objects will automatically generate a Panel HoloViews pane which can be rendered in the notebook, saved or rendered as a server app:
|
191 |
+
|
192 |
+
|
193 |
+
```python
|
194 |
+
holomap = hv.HoloMap({i: hv.Image(np.random.rand(10, 10)) for i in range(3)})
|
195 |
+
widget = renderer.get_widget(holomap, 'widgets')
|
196 |
+
widget
|
197 |
+
```
|
198 |
+
|
199 |
+
However most of the time it is more convenient to let the Renderer export the widget HTML, again via a convenient method, which will export a HTML document with all the required JS and CSS dependencies:
|
200 |
+
|
201 |
+
|
202 |
+
```python
|
203 |
+
html = renderer.static_html(holomap)
|
204 |
+
```
|
205 |
+
|
206 |
+
This covers the basics of working with HoloViews renderers. This API is consistent across plotting backends, whether matplotlib, bokeh or plotly.
|
207 |
+
|
208 |
+
# Plots
|
209 |
+
|
210 |
+
Above we saw how the Renderer looks up the appropriate plotting class but so far we haven't seen how the plotting actually works. Since HoloViews already nests the data into semantically meaningful components, which define the rough layout of the plots on the page, the plotting classes follow roughly the same hierarchy. To review this hierarchy have a look at the [nesting diagram](./06-Building_Composite_Objects.ipynb#Nesting-hierarchy-) in the Building Composite objects guide.
|
211 |
+
|
212 |
+
The Layout and GridSpace plotting classes set up the figure and axes appropriately and then instantiate the subplots for all the objects that are contained within. For this purpose we will create a relatively complex object, a ``Layout`` of ``HoloMap``s containing ``Overlay``s containing ``Elements``. We'll instantiate the matching plotting hierarchy and then inspect it.
|
213 |
+
|
214 |
+
|
215 |
+
```python
|
216 |
+
hmap1 = hv.HoloMap({i: hv.Image(np.random.rand(10, 10)) * hv.Ellipse(0, 0, 0.2*i) for i in range(5)})
|
217 |
+
element = hv.Curve((range(10), np.random.rand(10)))
|
218 |
+
layout = hmap1 + element
|
219 |
+
```
|
220 |
+
|
221 |
+
We can see the hierarchy in the object's repr:
|
222 |
+
|
223 |
+
|
224 |
+
```python
|
225 |
+
print( repr(layout) )
|
226 |
+
```
|
227 |
+
|
228 |
+
Now that we've created the object we can again use the ``MPLRenderer`` to instantiate the plot:
|
229 |
+
|
230 |
+
|
231 |
+
```python
|
232 |
+
layout_plot = renderer.get_plot(layout)
|
233 |
+
layout_plot
|
234 |
+
```
|
235 |
+
|
236 |
+
During instantiation the LayoutPlot expanded each object and created subplots. We can access them via a row, column based index and thereby view the first plot.
|
237 |
+
|
238 |
+
|
239 |
+
```python
|
240 |
+
adjoint_plot = layout_plot.subplots[0, 0]
|
241 |
+
adjoint_plot
|
242 |
+
```
|
243 |
+
|
244 |
+
This plotting layer handles plots adjoined to the plot. They are indexed by their position in the AdjointLayout which may include 'top', 'right' and 'main':
|
245 |
+
|
246 |
+
|
247 |
+
```python
|
248 |
+
overlay_plot = adjoint_plot.subplots['main']
|
249 |
+
overlay_plot
|
250 |
+
```
|
251 |
+
|
252 |
+
Now we've drilled all the way down to the OverlayPlot level, we see as expected that this contains two further subplots, one for the ``Image`` and one for ``Text`` Element.
|
253 |
+
|
254 |
+
|
255 |
+
```python
|
256 |
+
overlay_plot.subplots.keys()
|
257 |
+
```
|
258 |
+
|
259 |
+
Now you might have noticed that the HoloMap seems to have disappeared from the hierarchy. This is because updating a particular plot is handled by the ``ElementPlots`` itself. With that knowledge we can now have a look at the actual plotting API.
|
260 |
+
|
261 |
+
### Traversing plots
|
262 |
+
|
263 |
+
When working with such deeply nested plots accessing leafs can be a lot of effort, therefore the plots also provide a ``traverse`` method letting you specify the types of plots you want to access:
|
264 |
+
|
265 |
+
|
266 |
+
```python
|
267 |
+
layout_plot.traverse(specs=[hv.plotting.mpl.CurvePlot])
|
268 |
+
```
|
269 |
+
|
270 |
+
# Plotting API
|
271 |
+
|
272 |
+
There a few methods shared by all plotting classes, which allow the renderer to easily create, update and render a plot. The three most important methods and attributes are:
|
273 |
+
|
274 |
+
* ``Plot.__init__`` - The constructor already handles a lot of the processing in a plot, it sets up all the subplots if there are any, computes ranges across the object to normalize the display, sets the options that were specified and instantiates the figure, axes, model graphs, or canvas objects depending on the backend.
|
275 |
+
* ``Plot.initialize_plot`` - This method draws the initial frame to the appropriate figure, axis or canvas, setting up the various artists (matplotlib) or glyphs (bokeh).
|
276 |
+
* ``Plot.update`` - This method updates an already instantiated plot with the data corresponding to the supplied key. This key should match the key in the HoloMap.
|
277 |
+
|
278 |
+
### Initializing
|
279 |
+
|
280 |
+
The Renderer and the widgets use these three methods to instantiate and update a plot to render both static frames and animations or widgets as defined by the ``HoloMap`` or ``DynamicMap``. Above we already instantiated a plot, now we initialize it, thereby drawing the first (or rather last frame).
|
281 |
+
|
282 |
+
|
283 |
+
```python
|
284 |
+
fig = layout_plot.initialize_plot()
|
285 |
+
fig
|
286 |
+
```
|
287 |
+
|
288 |
+
### Updating
|
289 |
+
|
290 |
+
We can see ``initialize_plot`` has rendered the last frame with the key ``4``. We can update the figure with another key simply by calling the ``Plot.update`` method with the corresponding key.
|
291 |
+
|
292 |
+
|
293 |
+
```python
|
294 |
+
layout_plot.update(0)
|
295 |
+
```
|
296 |
+
|
297 |
+
|
298 |
+
```python
|
299 |
+
plot = hv.plotting.mpl.RasterPlot(holomap)
|
300 |
+
plot
|
301 |
+
```
|
302 |
+
|
303 |
+
Internally each level of the plotting hierarchy updates all the objects below it, all the way down to the ElementPlots, which handle updating the plotting data.
|
304 |
+
|
305 |
+
### Dynamic plot updates
|
306 |
+
|
307 |
+
Since DynamicMaps may be updated based on stream events they don't work via quite the same API. Each stream automatically captures the plots it is attached to and whenever it receives an event it will update the plot.
|
308 |
+
|
309 |
+
|
310 |
+
```python
|
311 |
+
dmap = hv.DynamicMap(lambda x: hv.Points(np.arange(x)), kdims=[], streams=[hv.streams.PointerX(x=10)])
|
312 |
+
plot = renderer.get_plot(dmap)
|
313 |
+
plot.initialize_plot()
|
314 |
+
```
|
315 |
+
|
316 |
+
Internally the update to the stream value will automatically trigger the plot to update, here we will disable this by setting ``trigger=False`` and explicitly calling ``refresh`` on the plot:
|
317 |
+
|
318 |
+
|
319 |
+
```python
|
320 |
+
dmap.event(x=20,)
|
321 |
+
plot.refresh()
|
322 |
+
plot.state
|
323 |
+
```
|
324 |
+
|
325 |
+
### Plotting handles
|
326 |
+
|
327 |
+
In addition to accessing the overall state of the plot each plotting class usually keeps direct handles for important plotting elements on the ``handles`` attribute:
|
328 |
+
|
329 |
+
|
330 |
+
```python
|
331 |
+
plot.handles
|
332 |
+
```
|
333 |
+
|
334 |
+
Here we can see how the ``PointPlot`` keeps track of the artist, which is a matplotlib ``PathCollection``, the axis, the figure and the plot title. In addition all matplotlib plots also keep track of a list of any additional plotting elements which should be considered in the bounding box calculation.
|
335 |
+
|
336 |
+
This is only the top-level API, which is used by the Renderer to render the plot, animation or widget. Each backend has internal APIs to create and update the various plot components.
|
337 |
+
|
338 |
+
### Using the matplotlib renderer outside of a Jupyter notebook
|
339 |
+
|
340 |
+
The matplotlib renderer can be used outside of a notebook by using the `show` method. If running in a python script, this will cause the usual matplotlib window to appear. This is done as follows:
|
341 |
+
|
342 |
+
mr = hv.renderer('matplotlib')
|
343 |
+
mr.show(curve)
|
hvplot_docs/Plotting.md
ADDED
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
As we discovered in the [Introduction](Introduction.ipynb), HoloViews allows plotting a variety of data types. Here we will use the sample data module and load the pandas and dask hvPlot API:
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import hvplot.pandas # noqa
|
7 |
+
import hvplot.dask # noqa
|
8 |
+
```
|
9 |
+
|
10 |
+
As we learned the hvPlot API closely mirrors the [Pandas plotting API](https://pandas.pydata.org/pandas-docs/stable/visualization.html), but instead of generating static images when used in a notebook, it uses HoloViews to generate either static or dynamically streaming Bokeh plots. Static plots can be used in any context, while streaming plots require a live [Jupyter notebook](https://jupyter.org), a deployed [Bokeh Server app](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html), or a deployed [Panel](https://panel.holoviz.org) app.
|
11 |
+
|
12 |
+
HoloViews provides an extensive, very rich set of objects along with a powerful set of operations to apply, as you can find out in the [HoloViews User Guide](https://holoviews.org/user_guide/index.html). But here we will focus on the most essential mechanisms needed to make your data visualizable, without having to worry about the mechanics going on behind the scenes.
|
13 |
+
|
14 |
+
We will be focusing on two different datasets:
|
15 |
+
|
16 |
+
- A small CSV file of US crime data, broken down by state
|
17 |
+
- A larger Parquet-format file of airline flight data
|
18 |
+
|
19 |
+
The ``hvplot.sample_data`` module makes these datasets Intake data catalogue, which we can load either using pandas:
|
20 |
+
|
21 |
+
|
22 |
+
```python
|
23 |
+
from hvplot.sample_data import us_crime, airline_flights
|
24 |
+
|
25 |
+
crime = us_crime.read()
|
26 |
+
print(type(crime))
|
27 |
+
crime.head()
|
28 |
+
```
|
29 |
+
|
30 |
+
Or using dask as a ``dask.DataFrame``:
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
flights = airline_flights.to_dask().persist()
|
35 |
+
print(type(flights))
|
36 |
+
flights.head()
|
37 |
+
```
|
38 |
+
|
39 |
+
## The plot interface
|
40 |
+
|
41 |
+
The ``dask.dataframe.DataFrame.hvplot``, ``pandas.DataFrame.hvplot`` and ``intake.DataSource.plot`` interfaces (and Series equivalents) from HvPlot provide a powerful high-level API to generate complex plots. The ``.hvplot`` API can be called directly or used as a namespace to generate specific plot types.
|
42 |
+
|
43 |
+
### The plot method
|
44 |
+
|
45 |
+
The most explicit way to use the plotting API is to specify the names of columns to plot on the ``x``- and ``y``-axis respectively:
|
46 |
+
|
47 |
+
|
48 |
+
```python
|
49 |
+
crime.hvplot.line(x='Year', y='Violent Crime rate')
|
50 |
+
```
|
51 |
+
|
52 |
+
As you'll see in more detail below, you can choose which kind of plot you want to use for the data:
|
53 |
+
|
54 |
+
|
55 |
+
```python
|
56 |
+
crime.hvplot(x='Year', y='Violent Crime rate', kind='scatter')
|
57 |
+
```
|
58 |
+
|
59 |
+
To group the data by one or more additional columns, specify an additional ``by`` variable. As an example here we will plot the departure delay ('depdelay') as a function of 'distance', grouping the data by the 'carrier'. There are many available carriers, so we will select only two of them so that the plot is readable:
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
flight_subset = flights[flights.carrier.isin(['OH', 'F9'])]
|
64 |
+
flight_subset.hvplot(x='distance', y='depdelay', by='carrier', kind='scatter', alpha=0.2, persist=True)
|
65 |
+
```
|
66 |
+
|
67 |
+
Here we have specified the `x` axis explicitly, which can be omitted if the Pandas index column is already the desired x axis. Similarly, here we specified the `y` axis; by default all of the non-index columns would be plotted (which would be a lot of data in this case). If you don't specify the 'y' axis, it will have a default label named 'value', but you can then provide a y axis label explicitly using the ``value_label`` option.
|
68 |
+
|
69 |
+
Putting all of this together we will plot violent crime, robbery, and burglary rates on the y-axis, specifying 'Year' as the x, and relabel the y-axis to display the 'Rate'.
|
70 |
+
|
71 |
+
|
72 |
+
```python
|
73 |
+
crime.hvplot(x='Year', y=['Violent Crime rate', 'Robbery rate', 'Burglary rate'],
|
74 |
+
value_label='Rate (per 100k people)')
|
75 |
+
```
|
76 |
+
|
77 |
+
### The hvplot namespace
|
78 |
+
|
79 |
+
Instead of using the ``kind`` argument to the plot call, we can use the ``hvplot`` namespace, which lets us easily discover the range of plot types that are supported. Use tab completion to explore the available plot types:
|
80 |
+
|
81 |
+
```python
|
82 |
+
crime.hvplot.<TAB>
|
83 |
+
```
|
84 |
+
|
85 |
+
Plot types available include:
|
86 |
+
|
87 |
+
* <a href="#area">``.area()``</a>: Plots a area chart similar to a line chart except for filling the area under the curve and optionally stacking
|
88 |
+
* <a href="#bars">``.bar()``</a>: Plots a bar chart that can be stacked or grouped
|
89 |
+
* <a href="#bivariate">``.bivariate()``</a>: Plots 2D density of a set of points
|
90 |
+
* <a href="#box-whisker-plots">``.box()``</a>: Plots a box-whisker chart comparing the distribution of one or more variables
|
91 |
+
* <a href="#heatmap">``.heatmap()``</a>: Plots a heatmap to visualizing a variable across two independent dimensions
|
92 |
+
* <a href="#hexbins">``.hexbins()``</a>: Plots hex bins
|
93 |
+
* <a href="#histogram">``.hist()``</a>: Plots the distribution of one or histograms as a set of bins
|
94 |
+
* <a href="#kde-density">``.kde()``</a>: Plots the kernel density estimate of one or more variables.
|
95 |
+
* <a href="#the-plot-method">``.line()``</a>: Plots a line chart (such as for a time series)
|
96 |
+
* <a href="#scatter">``.scatter()``</a>: Plots a scatter chart comparing two variables
|
97 |
+
* <a href="#step">``.step()``</a>: Plots a step chart akin to a line plot
|
98 |
+
* <a href="#tables">``.table()``</a>: Generates a SlickGrid DataTable
|
99 |
+
* <a href="#groupby">``.violin()``</a>: Plots a violin plot comparing the distribution of one or more variables using the kernel density estimate
|
100 |
+
|
101 |
+
#### Area
|
102 |
+
|
103 |
+
Like most other plot types the ``area`` chart supports the three ways of defining a plot outlined above. An area chart is most useful when plotting multiple variables in a stacked chart. This can be achieve by specifying ``x``, ``y``, and ``by`` columns or using the ``columns`` and ``index``/``use_index`` (equivalent to ``x``) options:
|
104 |
+
|
105 |
+
|
106 |
+
```python
|
107 |
+
crime.hvplot.area(x='Year', y=['Robbery', 'Aggravated assault'])
|
108 |
+
```
|
109 |
+
|
110 |
+
We can also explicitly set ``stacked`` to False and define an ``alpha`` value to compare the values directly:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
crime.hvplot.area(x='Year', y=['Aggravated assault', 'Robbery'], stacked=False, alpha=0.4)
|
115 |
+
```
|
116 |
+
|
117 |
+
Another use for an area plot is to visualize the spread of a value. For instance using the flights dataset we may want to see the spread in mean delay values across carriers. For that purpose we compute the mean delay by day and carrier and then the min/max mean delay for across all carriers. Since the output of ``hvplot`` is just a regular holoviews object, we can use the overlay operator (\*) to place the plots on top of each other.
|
118 |
+
|
119 |
+
|
120 |
+
```python
|
121 |
+
delay_min_max = flights.groupby(['day', 'carrier'])['carrier_delay'].mean().groupby('day').agg({'min': np.min, 'max': np.max})
|
122 |
+
delay_mean = flights.groupby('day')['carrier_delay'].mean()
|
123 |
+
|
124 |
+
delay_min_max.hvplot.area(x='day', y='min', y2='max', alpha=0.2) * delay_mean.hvplot()
|
125 |
+
```
|
126 |
+
|
127 |
+
#### Bars
|
128 |
+
|
129 |
+
In the simplest case we can use ``.hvplot.bar`` to plot ``x`` against ``y``. We'll use ``rot=90`` to rotate the tick labels on the x-axis making the years easier to read:
|
130 |
+
|
131 |
+
|
132 |
+
```python
|
133 |
+
crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90)
|
134 |
+
```
|
135 |
+
|
136 |
+
If we want to compare multiple columns instead we can set ``y`` to a list of columns. Using the ``stacked`` option we can then compare the column values more easily:
|
137 |
+
|
138 |
+
|
139 |
+
```python
|
140 |
+
crime.hvplot.bar(x='Year', y=['Violent crime total', 'Property crime total'],
|
141 |
+
stacked=True, rot=90, width=800, legend='top_left')
|
142 |
+
```
|
143 |
+
|
144 |
+
#### Scatter
|
145 |
+
|
146 |
+
The scatter plot supports many of the same features as the other chart types we have seen so far but can also be colored by another variable using the ``c`` option.
|
147 |
+
|
148 |
+
|
149 |
+
```python
|
150 |
+
crime.hvplot.scatter(x='Violent Crime rate', y='Burglary rate', c='Year')
|
151 |
+
```
|
152 |
+
|
153 |
+
Anytime that color is being used to represent a dimension, the ``cmap`` option can be used to control the colormap that is used to represent that dimension. Additionally, the colorbar can be disabled using ``colorbar=False``.
|
154 |
+
|
155 |
+
#### Step
|
156 |
+
|
157 |
+
A step chart is very similar to a line chart but instead of linearly interpolating between samples the step chart visualizes discrete steps. The point at which to step can be controlled via the ``where`` keyword allowing `'pre'`, `'mid'` (default) and `'post'` values:
|
158 |
+
|
159 |
+
|
160 |
+
```python
|
161 |
+
crime.hvplot.step(x='Year', y=['Robbery', 'Aggravated assault'])
|
162 |
+
```
|
163 |
+
|
164 |
+
#### HexBins
|
165 |
+
|
166 |
+
You can create hexagonal bin plots with the ``hexbin`` method. Hexbin plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually. Since these data are not regularly distributed, we'll use the ``logz`` option to map z-axis (color) to a log scale colorbar.
|
167 |
+
|
168 |
+
|
169 |
+
```python
|
170 |
+
flights.hvplot.hexbin(x='airtime', y='arrdelay', width=600, height=500, logz=True)
|
171 |
+
```
|
172 |
+
|
173 |
+
#### Bivariate
|
174 |
+
|
175 |
+
You can create a 2D density plot with the ``bivariate`` method. Bivariate plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually.
|
176 |
+
|
177 |
+
|
178 |
+
```python
|
179 |
+
crime.hvplot.bivariate(x='Violent Crime rate', y='Burglary rate', width=600, height=500)
|
180 |
+
```
|
181 |
+
|
182 |
+
#### HeatMap
|
183 |
+
|
184 |
+
A ``HeatMap`` lets us view the relationship between three variables, so we specify the 'x' and 'y' variables and an additional 'C' variable. Additionally we can define a ``reduce_function`` that computes the values for each bin from the samples that fall into it. Here we plot the 'depdelay' (i.e. departure delay) for each day of the month and carrier in the dataset:
|
185 |
+
|
186 |
+
|
187 |
+
```python
|
188 |
+
flights.compute().hvplot.heatmap(x='day', y='carrier', C='depdelay', reduce_function=np.mean, colorbar=True)
|
189 |
+
```
|
190 |
+
|
191 |
+
#### Tables
|
192 |
+
|
193 |
+
Unlike all other plot types, a table only supports one signature: either all columns are plotted, or a subset of columns can be selected by defining the ``columns`` explicitly:
|
194 |
+
|
195 |
+
|
196 |
+
```python
|
197 |
+
crime.hvplot.table(columns=['Year', 'Population', 'Violent Crime rate'], width=400)
|
198 |
+
```
|
199 |
+
|
200 |
+
### Distributions
|
201 |
+
|
202 |
+
Plotting distributions differs slightly from other plots since they plot only one variable in the simple case rather than plotting two or more variables against each other. Therefore when plotting these plot types no ``index`` or ``x`` value needs to be supplied. Instead:
|
203 |
+
|
204 |
+
1. Declare a single ``y`` variable, e.g. ``source.plot.hist(variable)``, or
|
205 |
+
2. Declare a ``y`` variable and ``by`` variable, e.g. ``source.plot.hist(variable, by='Group')``, or
|
206 |
+
3. Declare columns or plot all columns, e.g. ``source.plot.hist()`` or ``source.plot.hist(columns=['A', 'B', 'C'])``
|
207 |
+
|
208 |
+
#### Histogram
|
209 |
+
|
210 |
+
The Histogram is the simplest example of a distribution; often we simply plot the distribution of a single variable, in this case the 'Violent Crime rate'. Additionally we can define a range over which to compute the histogram and the number of bins using the ``bin_range`` and ``bins`` arguments respectively:
|
211 |
+
|
212 |
+
|
213 |
+
```python
|
214 |
+
crime.hvplot.hist(y='Violent Crime rate')
|
215 |
+
```
|
216 |
+
|
217 |
+
Or we can plot the distribution of multiple columns:
|
218 |
+
|
219 |
+
|
220 |
+
```python
|
221 |
+
columns = ['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
222 |
+
crime.hvplot.hist(y=columns, bins=50, alpha=0.5, legend='top', height=400)
|
223 |
+
```
|
224 |
+
|
225 |
+
We can also group the data by another variable. Here we'll use ``subplots`` to split each carrier out into its own plot:
|
226 |
+
|
227 |
+
|
228 |
+
```python
|
229 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
230 |
+
flight_subset.hvplot.hist('depdelay', by='carrier', bins=20, bin_range=(-20, 100), width=300, subplots=True)
|
231 |
+
```
|
232 |
+
|
233 |
+
#### KDE (density)
|
234 |
+
|
235 |
+
You can also create density plots using ``hvplot.kde()`` or ``hvplot.density()``:
|
236 |
+
|
237 |
+
|
238 |
+
```python
|
239 |
+
crime.hvplot.kde(y='Violent Crime rate')
|
240 |
+
```
|
241 |
+
|
242 |
+
Comparing the distribution of multiple columns is also possible:
|
243 |
+
|
244 |
+
|
245 |
+
```python
|
246 |
+
columns=['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
247 |
+
crime.hvplot.kde(y=columns, alpha=0.5, value_label='Rate', legend='top_right')
|
248 |
+
```
|
249 |
+
|
250 |
+
The ``hvplot.kde`` also supports the ``by`` keyword:
|
251 |
+
|
252 |
+
|
253 |
+
```python
|
254 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
255 |
+
flight_subset.hvplot.kde('depdelay', by='carrier', xlim=(-20, 70), width=300, subplots=True)
|
256 |
+
```
|
257 |
+
|
258 |
+
#### Box-Whisker Plots
|
259 |
+
|
260 |
+
Just like the other distribution-based plot types, the box-whisker plot supports plotting a single column:
|
261 |
+
|
262 |
+
|
263 |
+
```python
|
264 |
+
crime.hvplot.box(y='Violent Crime rate')
|
265 |
+
```
|
266 |
+
|
267 |
+
It also supports multiple columns and the same options as seen previously (``legend``, ``invert``, ``value_label``):
|
268 |
+
|
269 |
+
|
270 |
+
```python
|
271 |
+
columns=['Burglary rate', 'Larceny-theft rate', 'Motor vehicle theft rate',
|
272 |
+
'Property crime rate', 'Violent Crime rate']
|
273 |
+
crime.hvplot.box(y=columns, group_label='Crime', legend=False, value_label='Rate (per 100k)', invert=True)
|
274 |
+
```
|
275 |
+
|
276 |
+
Lastly, it also supports using the ``by`` keyword to split the data into multiple subsets:
|
277 |
+
|
278 |
+
|
279 |
+
```python
|
280 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
281 |
+
flight_subset.hvplot.box('depdelay', by='carrier', ylim=(-10, 70))
|
282 |
+
```
|
283 |
+
|
284 |
+
## Composing Plots
|
285 |
+
|
286 |
+
One of the core strengths of HoloViews is the ease of composing
|
287 |
+
different plots. Individual plots can be composed using the ``*`` and
|
288 |
+
``+`` operators, which overlay and compose plots into layouts
|
289 |
+
respectively. For more information on composing objects, see the
|
290 |
+
HoloViews [User Guide](https://holoviews.org/user_guide/Composing_Elements.html).
|
291 |
+
|
292 |
+
By using these operators we can combine multiple plots into composite plots. A simple example is overlaying two plot types:
|
293 |
+
|
294 |
+
|
295 |
+
```python
|
296 |
+
crime.hvplot(x='Year', y='Violent Crime rate') * crime.hvplot.scatter(x='Year', y='Violent Crime rate', c='k')
|
297 |
+
```
|
298 |
+
|
299 |
+
We can also lay out different plots and tables together:
|
300 |
+
|
301 |
+
|
302 |
+
```python
|
303 |
+
(crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90, width=550) +
|
304 |
+
crime.hvplot.table(['Year', 'Population', 'Violent Crime rate'], width=420))
|
305 |
+
```
|
306 |
+
|
307 |
+
## Large data
|
308 |
+
|
309 |
+
The previous examples summarized the fairly large airline dataset using statistical plot types that aggregate the data into a feasible subset for plotting. We can instead aggregate the data directly into the viewable image using [datashader](https://datashader.org), which provides a rendering of the entire set of raw data available (as far as the resolution of the screen allows). Here we plot the 'airtime' against the 'distance':
|
310 |
+
|
311 |
+
|
312 |
+
```python
|
313 |
+
flights.hvplot.scatter(x='distance', y='airtime', datashade=True)
|
314 |
+
```
|
315 |
+
|
316 |
+
## Groupby
|
317 |
+
|
318 |
+
Thanks to the ability of HoloViews to explore a parameter space with a set of widgets we can apply a groupby along a particular column or dimension. For example we can view the distribution of departure delays by carrier grouped by day, allowing the user to choose which day to display:
|
319 |
+
|
320 |
+
|
321 |
+
```python
|
322 |
+
flights.hvplot.violin(y='depdelay', by='carrier', groupby='dayofweek', ylim=(-20, 60), height=500)
|
323 |
+
```
|
324 |
+
|
325 |
+
This user guide merely provided an overview over the available plot types; to see a detailed description on how to customize plots see the [Customization](Customization.ipynb) user guide.
|
hvplot_docs/Plotting_Extensions.md
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```python
|
2 |
+
from hvplot.sample_data import us_crime
|
3 |
+
```
|
4 |
+
|
5 |
+
hvPlot allows to generate plots with three different plotting extensions: [Bokeh](https://docs.bokeh.org), [Matplotlib](https://matplotlib.org/) and [Plotly](https://plotly.com/). Support for Maplotlib and Plotly was added in version 0.8.0, before which Bokeh was the only option available and is as such kept as the default plotting extension.
|
6 |
+
|
7 |
+
## Loading plotting extensions
|
8 |
+
|
9 |
+
Importing hvPlot accessors as shown throughout the documentation (e.g. `import hvplot.pandas`, `import hvplot.xarray`, ...) loads by default the Bokeh plotting extension. The `extension` function can be used after this first import to additionally load the Matplotlib and Plotly plotting extensions, or just one of them. The currently active extension is the first one passed to `extension()`.
|
10 |
+
|
11 |
+
|
12 |
+
```python
|
13 |
+
import hvplot.pandas # This import automatically loads the Bokeh extension.
|
14 |
+
```
|
15 |
+
|
16 |
+
|
17 |
+
```python
|
18 |
+
hvplot.extension('matplotlib', 'plotly') # This call loads the Matplotlib (the active one) and Plotly extensions.
|
19 |
+
```
|
20 |
+
|
21 |
+
|
22 |
+
```python
|
23 |
+
us_crime.hvplot(x='Year', y='Violent Crime rate')
|
24 |
+
```
|
25 |
+
|
26 |
+
## Switching the plotting extension
|
27 |
+
|
28 |
+
The `output` function allows to switch the plotting extension by setting the `backend` parameter. It must be called **after** importing an accessor (e.g. `import hvplot.pandas`) and calling to `extension`:
|
29 |
+
|
30 |
+
|
31 |
+
```python
|
32 |
+
hvplot.output(backend='plotly')
|
33 |
+
|
34 |
+
us_crime.hvplot(x='Year', y='Violent Crime rate')
|
35 |
+
```
|
36 |
+
|
37 |
+
<div class="alert alert-warning" role="alert">
|
38 |
+
The <code>extension</code> function could also be used to switch the plotting extension. You should however prefer <code>output</code> as any call to <code>extension</code> actually internally loads code in the output of the cell where it is executed, which can significantly increase the notebook size if <code>extension</code> is called in multiple cells.
|
39 |
+
</div>
|
40 |
+
|
41 |
+
## Plot styling options
|
42 |
+
|
43 |
+
On top of the parameters part of `.hvplot()`'s API, a plot can be further customized by providing detailed styling options. By default, i.e. without calling `hvplot.extension()`, the accepted styling options are those of Bokeh, such as `line_dash='dashed'`. When another extension is set with either `extension` or `output` the styling options of that extension are expected, e.g. `linestyle='dashed'` for Matplotlib.
|
44 |
+
|
45 |
+
|
46 |
+
```python
|
47 |
+
hvplot.output(backend='bokeh')
|
48 |
+
us_crime.hvplot(x='Year', y='Violent Crime rate', line_dash='dashed')
|
49 |
+
```
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
hvplot.output(backend='matplotlib')
|
54 |
+
us_crime.hvplot(x='Year', y='Violent Crime rate', linestyle='dashed')
|
55 |
+
```
|
56 |
+
|
57 |
+
It is also possible to generates plots with either Matplotlib or Plotly that are constructed with Bokeh options. This can be used for instance to create plots previously generated with Bokeh with another backend, without having to change any parameter. The only change required is to declare Bokeh as the compatible extension with `hvplot.extension('matplotlib', compatibility='bokeh')`, preferably at the beginning of the notebook.
|
58 |
+
|
59 |
+
|
60 |
+
```python
|
61 |
+
hvplot.extension('matplotlib', compatibility='bokeh')
|
62 |
+
violent_crime = us_crime.hvplot(x='Year', y='Violent Crime rate', line_dash='dashed')
|
63 |
+
violent_crime
|
64 |
+
```
|
65 |
+
|
66 |
+
|
67 |
+
```python
|
68 |
+
violent_crime.opts.info()
|
69 |
+
```
|
70 |
+
|
71 |
+
You can see that `line_dash='dashed'` has been internally converted to `linestyle='dashed'` that is a valid option for the Matplotlib plotting extension. Note that support for all Bokeh options is currently incomplete and will continue to be improved while equivalent support for Plotly and Matplotlib is planned for a future release
|
72 |
+
|
73 |
+
## Obtaining the underlying figure object
|
74 |
+
|
75 |
+
In some cases it can be convenient to construct a plot with hvPlot and then get a handle on the figure object of the underlying plotting library to further customize the plot or to embed it in some more complex application. The `render` function allows to get a handle on the figure object. The following examples show that it's possible to use the API of Bokeh, Matplotlib or Plotly to update the title of the `violent_crime` plot.
|
76 |
+
|
77 |
+
|
78 |
+
```python
|
79 |
+
violent_crime = us_crime.hvplot(x='Year', y='Violent Crime rate')
|
80 |
+
```
|
81 |
+
|
82 |
+
|
83 |
+
```python
|
84 |
+
from bokeh.io import show
|
85 |
+
|
86 |
+
bk_fig = hvplot.render(violent_crime, backend='bokeh')
|
87 |
+
bk_fig.title = 'Violent crime'
|
88 |
+
show(bk_fig)
|
89 |
+
```
|
90 |
+
|
91 |
+
|
92 |
+
```python
|
93 |
+
%matplotlib inline
|
94 |
+
mpl_fig = hvplot.render(violent_crime, backend='matplotlib')
|
95 |
+
axes = mpl_fig.get_axes()
|
96 |
+
axes[0].set_title('Violent crime')
|
97 |
+
mpl_fig
|
98 |
+
```
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
from plotly.graph_objects import Figure
|
103 |
+
|
104 |
+
plotly_fig = hvplot.render(violent_crime, backend='plotly')
|
105 |
+
fig = Figure(plotly_fig).update_layout(title='Violent crime')
|
106 |
+
fig
|
107 |
+
```
|
108 |
+
|
109 |
+
As demonstrated on this page hvPlot offers you the ability to choose your favorite plotting extension among those supported by [HoloViews](https://holoviews.org/), on which hvPlot is built.
|
hvplot_docs/Plotting_with_Bokeh.md
ADDED
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Plotting with Bokeh
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import numpy as np
|
6 |
+
import pandas as pd
|
7 |
+
import holoviews as hv
|
8 |
+
from holoviews import dim, opts
|
9 |
+
|
10 |
+
hv.extension('bokeh')
|
11 |
+
```
|
12 |
+
|
13 |
+
One of the major design principles of HoloViews is that the declaration of data is completely independent from the plotting implementation. Bokeh provides a powerful platform to generate interactive plots using HTML5 canvas and WebGL, and is ideally suited towards interactive exploration of data. By combining the ease of generating interactive, high-dimensional visualizations with the interactive widgets and fast rendering provided by Bokeh, HoloViews becomes even more powerful.
|
14 |
+
|
15 |
+
This user guide will cover various interactive features that bokeh provides which is not covered by the more general user guides, including interactive tools, linked axes and brushing and more. The general principles behind customizing plots and styling the visual elements of a plot are covered in the [Style Mapping](04-Style_Mapping.ipynb) and [Customizing Plots](Customizing_Plots.ipynb) user guides.
|
16 |
+
|
17 |
+
## Working with bokeh directly
|
18 |
+
|
19 |
+
When HoloViews outputs bokeh plots it creates and manipulates bokeh models in the background. If at any time you need access to the underlying Bokeh representation of an object you can use the ``hv.render`` function to convert it. For example let us convert a HoloViews ``Image`` to a bokeh Figure, which will let us access and modify every aspect of the plot:
|
20 |
+
|
21 |
+
|
22 |
+
```python
|
23 |
+
img = hv.Image(np.random.rand(10, 10))
|
24 |
+
|
25 |
+
fig = hv.render(img)
|
26 |
+
|
27 |
+
print('Figure: ', fig)
|
28 |
+
print('Renderers: ', fig.renderers[-1].glyph)
|
29 |
+
```
|
30 |
+
|
31 |
+
## Exporting static files
|
32 |
+
|
33 |
+
Bokeh supports static export to png's using ``Selenium``, ``PhantomJS`` and ``pillow``, to install the required dependencies run:
|
34 |
+
|
35 |
+
```
|
36 |
+
conda install selenium phantomjs pillow
|
37 |
+
```
|
38 |
+
|
39 |
+
alternatively install PhantomJS from npm using:
|
40 |
+
|
41 |
+
```
|
42 |
+
npm install -g phantomjs-prebuilt
|
43 |
+
```
|
44 |
+
|
45 |
+
To switch to png output persistently you can run:
|
46 |
+
|
47 |
+
```
|
48 |
+
hv.output(fig='png')
|
49 |
+
```
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
violin = hv.Violin(np.random.randn(100))
|
54 |
+
|
55 |
+
hv.output(violin, fig='png')
|
56 |
+
```
|
57 |
+
|
58 |
+
The exported png can also be saved to disk using the ``save`` function by changing the file extension from ``.html``, which exports an interactive plot, ``.png``:
|
59 |
+
|
60 |
+
```python
|
61 |
+
hv.save(violin, 'violin.png')
|
62 |
+
```
|
63 |
+
|
64 |
+
## Element Style options
|
65 |
+
|
66 |
+
One of the major benefits of bokeh is that it was designed from the ground up with consistency in mind, therefore most style options are a combination of the fill, line, and text style options listed below:
|
67 |
+
|
68 |
+
|
69 |
+
```python
|
70 |
+
from holoviews.plotting.bokeh.styles import (line_properties, fill_properties, text_properties)
|
71 |
+
print("""
|
72 |
+
Line properties: %s\n
|
73 |
+
Fill properties: %s\n
|
74 |
+
Text properties: %s
|
75 |
+
""" % (line_properties, fill_properties, text_properties))
|
76 |
+
```
|
77 |
+
|
78 |
+
Note also that most of these options support vectorized style mapping as described in the [Style Mapping user guide](04-Style_Mapping.ipynb). Here's an example of HoloViews Elements using a Bokeh backend, with bokeh's style options:
|
79 |
+
|
80 |
+
|
81 |
+
```python
|
82 |
+
curve_opts = opts.Curve(line_width=10, line_color='indianred', line_dash='dotted', line_alpha=0.5)
|
83 |
+
point_opts = opts.Points(fill_color='#00AA00', fill_alpha=0.5, line_width=1, line_color='black', size=5)
|
84 |
+
text_opts = opts.Text(text_align='center', text_baseline='middle', text_color='gray', text_font='Arial')
|
85 |
+
|
86 |
+
xs = np.linspace(0, np.pi*4, 100)
|
87 |
+
data = (xs, np.sin(xs))
|
88 |
+
|
89 |
+
(hv.Curve(data) + hv.Points(data) + hv.Text(6, 0, 'Here is some text')).opts(
|
90 |
+
curve_opts, point_opts, text_opts)
|
91 |
+
```
|
92 |
+
|
93 |
+
Notice that because the first two plots use the same underlying data, they become linked, such that zooming or panning one of the plots makes the corresponding change on the other.
|
94 |
+
|
95 |
+
## Setting Backend Opts
|
96 |
+
|
97 |
+
HoloViews does not expose every single option from Bokeh.
|
98 |
+
|
99 |
+
Instead, HoloViews allow users to attach [hooks](Customizing_Plots.ipynb#plot-hooks) to modify the plot object directly--but writing these hooks could be cumbersome, especially if it's only used for a single line of update.
|
100 |
+
|
101 |
+
Fortunately, HoloViews allows `backend_opts` for the Bokeh backend to configure options by declaring a dictionary with accessor specification for updating the plot components.
|
102 |
+
|
103 |
+
For example, here's how to make the toolbar auto-hide.
|
104 |
+
|
105 |
+
|
106 |
+
```python
|
107 |
+
hv.Curve(data).opts(
|
108 |
+
backend_opts={"plot.toolbar.autohide": True}
|
109 |
+
)
|
110 |
+
```
|
111 |
+
|
112 |
+
The following is the equivalent, as a hook.
|
113 |
+
|
114 |
+
|
115 |
+
```python
|
116 |
+
def hook(hv_plot, element):
|
117 |
+
toolbar = hv_plot.handles['plot'].toolbar
|
118 |
+
toolbar.autohide = True
|
119 |
+
|
120 |
+
hv.Curve(data).opts(hooks=[hook])
|
121 |
+
```
|
122 |
+
|
123 |
+
Notice how much more concise it is with `backend_opts`!
|
124 |
+
|
125 |
+
With knowledge of the attributes of Bokeh, it's possible to configure many other plot components besides `toolbar`. Some examples include `legend`, `colorbar`, `xaxis`, `yaxis`, and much, much more.
|
126 |
+
|
127 |
+
If you're unsure, simply input your best guess and it'll try to provide a list of suggestions if there's an issue.
|
128 |
+
|
129 |
+
## Sizing Elements
|
130 |
+
|
131 |
+
In the bokeh backend the sizing of plots and specifically layouts of plots is determined in an inside-out or compositional manner. Each subplot can be sized independently and it will fill the allocated space. The sizing is determined by the combination of width, height, aspect and responsive options. In this section we will discover the different approaches to setting plot dimensions and aspects.
|
132 |
+
|
133 |
+
### Width and height
|
134 |
+
|
135 |
+
The most straightforward approach to specifying the size of a plot is using a width and height specified in pixels. This is the default behavior and makes it easy to achieve precise alignment between plots but does not allow for keeping a constant aspect or responsive resizing. In particular, the specified size includes the axes and any legends or colorbars.
|
136 |
+
|
137 |
+
|
138 |
+
```python
|
139 |
+
points_a = hv.Points(data)
|
140 |
+
points_b = hv.Points(data)
|
141 |
+
|
142 |
+
points_a.opts(width=300, height=300) + points_b.opts(width=600, height=300)
|
143 |
+
```
|
144 |
+
|
145 |
+
### Frame width and height
|
146 |
+
|
147 |
+
The frame width on the other hand provides precise control over the inner dimensions of a plot, it ensures the actual plot frame matches the specified dimensions exactly. This makes it possible to achieve a precise aspect ratio between the axis scales, without worrying about the size of axes, colorbars, titles and legends, e.g. below we can see two plots defined using explicit ``frame_width`` and ``frame_height`` share the same dimensions despite the fact that one has a colorbar.
|
148 |
+
|
149 |
+
|
150 |
+
```python
|
151 |
+
xs = ys = np.arange(10)
|
152 |
+
yy, xx = np.meshgrid(xs, ys)
|
153 |
+
zz = xx*yy
|
154 |
+
|
155 |
+
img = hv.Image(np.random.rand(100, 100))
|
156 |
+
|
157 |
+
points_a.opts(frame_width=200, frame_height=200) +\
|
158 |
+
img.opts(frame_width=200, frame_height=200, colorbar=True, axiswise=True)
|
159 |
+
```
|
160 |
+
|
161 |
+
### Aspect
|
162 |
+
|
163 |
+
The ``aspect`` and ``data_aspect`` options provide control over the scaling of the plot dimensions and the axis limits.
|
164 |
+
|
165 |
+
#### ``aspect``:
|
166 |
+
|
167 |
+
The ``aspect`` specifies the ratio between the width and height dimensions of the plot. If specified this options takes absolute precedence over the dimensions of the plot but has no effect on the plot's axis limits. It supports the following options:
|
168 |
+
|
169 |
+
* **``float``** : A numeric value will scale the ratio of plot width to plot height
|
170 |
+
* **``"equal"``** : Sets aspect of the axis scaling to be equal, equivalent to **``data_aspect=1``**
|
171 |
+
* **``"square"``** : Ensures the plot dimensions are square.
|
172 |
+
|
173 |
+
#### ``data_aspect``:
|
174 |
+
|
175 |
+
The ``data_aspect`` specifies the scaling between the x- and y-axis ranges. If specified this option will scale both the plot ranges and dimensions unless explicit ``aspect``, ``width`` or ``height`` value overrides the plot dimensions:
|
176 |
+
|
177 |
+
* **``float``** : Sets ratio between the units on the x-scale and the y-scale
|
178 |
+
|
179 |
+
|
180 |
+
```python
|
181 |
+
xs = np.linspace(0, 10)
|
182 |
+
ys = np.linspace(0, 5)
|
183 |
+
|
184 |
+
img = hv.Image((xs, ys, xs[:, np.newaxis]*np.sin(ys*4)))
|
185 |
+
|
186 |
+
(img.options(aspect='equal').relabel('aspect=\'equal\'') +
|
187 |
+
img.options(aspect='square', colorbar=True, frame_width=300).relabel('aspect=\'square\'') +
|
188 |
+
img.options(aspect=2).relabel('aspect=2') +
|
189 |
+
img.options(data_aspect=2, frame_width=300).relabel('data_aspect=2')).cols(2)
|
190 |
+
```
|
191 |
+
|
192 |
+
## Responsive
|
193 |
+
|
194 |
+
Since bokeh plots are rendered within a browser window which can be resized dynamically it supports responsive sizing modes allowing the plot to rescale when the window it is placed in is changed. If enabled, the behavior of ``responsive`` modes depends on whether an aspect or width/height option is set. Specifically responsive mode will only work if at least one dimension of the plot is left undefined, e.g. when width and height or width and aspect are set the plot is set to a fixed size, ignoring any ``responsive`` option. This leaves four different ``responsive`` modes:
|
195 |
+
|
196 |
+
* **``scale_both``**: If neither a width or a height are defined but a fixed aspect is defined both axes will be scaled up to the maximum size of the container. Scaling ensures that the aspect ratio of the plot is maintained.
|
197 |
+
* **``stretch_both``**: If neither a width, height or aspect are defined the plot will stretch to fill all available space.
|
198 |
+
* **``stretch_width``**: If a height but neither a width or aspect are defined the plot will stretch to fill all available horizontal space.
|
199 |
+
* **``stretch_height``**: If a width but neither a height or aspect are defined the plot will stretch to fill all available vertical space.
|
200 |
+
|
201 |
+
**Note**: In the notebook stretching and scaling the height does not increase the size of a cell.
|
202 |
+
|
203 |
+
As a simple example let us declare a plot that has a fixed height but stretches to fit all available horizontal space:
|
204 |
+
|
205 |
+
|
206 |
+
```python
|
207 |
+
hv.Points(data).opts(height=200, responsive=True, title='stretch width')
|
208 |
+
```
|
209 |
+
|
210 |
+
Similarly if we declare a fixed ``aspect`` or ``data_aspect`` responsive modes will try to fill all available space but avoid distorting the specified aspect:
|
211 |
+
|
212 |
+
|
213 |
+
```python
|
214 |
+
img.opts(data_aspect=0.5, responsive=True, title='scale both')
|
215 |
+
```
|
216 |
+
|
217 |
+
## Alignment
|
218 |
+
|
219 |
+
The alignment of a plot in a row or column can be controlled using the ``align`` option. It controls both the vertical alignment in a row and the horizontal alignment in a column and can be set to one of `'start'`, `'center'` or `'end'` (where `'start'` is the default).
|
220 |
+
|
221 |
+
#### Vertical
|
222 |
+
|
223 |
+
|
224 |
+
```python
|
225 |
+
points = hv.Points(data).opts(axiswise=True)
|
226 |
+
img = hv.Image((xs, ys, xs[:, np.newaxis]*np.sin(ys*4)))
|
227 |
+
|
228 |
+
img + points.opts(height=200, align='end')
|
229 |
+
```
|
230 |
+
|
231 |
+
#### Horizontal
|
232 |
+
|
233 |
+
|
234 |
+
```python
|
235 |
+
(img.opts(axiswise=True, width=200, align='center') + points).cols(1)
|
236 |
+
```
|
237 |
+
|
238 |
+
## Grid lines
|
239 |
+
|
240 |
+
Grid lines can be controlled through the combination of ``show_grid`` and ``gridstyle`` parameters. The ``gridstyle`` allows specifying a number of options including:
|
241 |
+
|
242 |
+
* ``grid_line_color``
|
243 |
+
* ``grid_line_alpha``
|
244 |
+
* ``grid_line_dash``
|
245 |
+
* ``grid_line_width``
|
246 |
+
* ``grid_bounds``
|
247 |
+
* ``grid_band``
|
248 |
+
|
249 |
+
These options may also be applied to minor grid lines by prepending the ``'minor_'`` prefix and may be applied to a specific axis by replacing ``'grid_`` with ``'xgrid_'`` or ``'ygrid_'``. Here we combine some of these options to generate a complex grid pattern:
|
250 |
+
|
251 |
+
|
252 |
+
```python
|
253 |
+
grid_style = {'grid_line_color': 'black', 'grid_line_width': 1.5, 'ygrid_bounds': (0.3, 0.7),
|
254 |
+
'minor_xgrid_line_color': 'lightgray', 'xgrid_line_dash': [4, 4]}
|
255 |
+
|
256 |
+
hv.Points(np.random.rand(10, 2)).opts(gridstyle=grid_style, show_grid=True, size=5, width=600)
|
257 |
+
```
|
258 |
+
|
259 |
+
## Containers
|
260 |
+
|
261 |
+
The bokeh plotting extension also supports a number of additional features relating to container components.
|
262 |
+
|
263 |
+
### Tabs
|
264 |
+
|
265 |
+
Using bokeh, both ``(Nd)Overlay`` and ``(Nd)Layout`` types may be displayed inside a ``tabs`` widget. This may be enabled via a plot option ``tabs``, and may even be nested inside a Layout.
|
266 |
+
|
267 |
+
|
268 |
+
```python
|
269 |
+
x,y = np.mgrid[-50:51, -50:51] * 0.1
|
270 |
+
|
271 |
+
img = hv.Image(np.sin(x**2+y**2), bounds=(-1,-1,1,1))
|
272 |
+
(img.relabel('Image') * img.sample(x=0).relabel('Cross-section')).opts(tabs=True)
|
273 |
+
```
|
274 |
+
|
275 |
+
Another reason to use ``tabs`` is that some Layout combinations may not be able to be displayed directly using HoloViews. For example, it is not currently possible to display a ``GridSpace`` as part of a ``Layout`` in any backend, and this combination will automatically switch to a ``tab`` representation for the bokeh backend.
|
276 |
+
|
277 |
+
### Interactive Legends
|
278 |
+
|
279 |
+
|
280 |
+
|
281 |
+
When using ``NdOverlay`` and ``Overlay`` containers each element will get a legend entry, which can be used to interactively toggle the visibility of the element. In this example we will create a number of ``Histogram`` elements each with a different mean. By setting a ``muted_fill_alpha`` we can define the style of the element when it is de-selected using the legend, simply try tapping on each legend entry to see the effect:
|
282 |
+
|
283 |
+
|
284 |
+
```python
|
285 |
+
hv.NdOverlay({i: hv.Histogram(np.histogram(np.random.randn(100)+i*2)) for i in range(5)}).opts(
|
286 |
+
'Histogram', width=600, alpha=0.8, muted_fill_alpha=0.1)
|
287 |
+
```
|
288 |
+
|
289 |
+
The other ``muted_`` options can be used to define other aspects of the Histogram style when it is unselected.
|
290 |
+
|
291 |
+
If you have multiple plots in a ``Layout`` with the same legend label, muting one of them will automatically mute all of them.
|
292 |
+
|
293 |
+
|
294 |
+
```python
|
295 |
+
overlay1 = hv.Curve([0, 0], label="A") * hv.Curve([1, 1], label="B")
|
296 |
+
overlay2 = hv.Curve([2, 2], label="A") * hv.Curve([3, 3], label="B")
|
297 |
+
layout = overlay1 + overlay2
|
298 |
+
layout
|
299 |
+
```
|
300 |
+
|
301 |
+
If you want to turn off this behavior, use ``.opts(sync_legends=False)``
|
302 |
+
|
303 |
+
|
304 |
+
```python
|
305 |
+
layout.opts(sync_legends=False)
|
306 |
+
```
|
307 |
+
|
308 |
+
If you want to control the number of legend shown in the ``Layout`` or the position of them ``show_legends`` and ``legend_position`` can be used.
|
309 |
+
|
310 |
+
|
311 |
+
```python
|
312 |
+
layout.opts(sync_legends=True, show_legends=0, legend_position="top_left")
|
313 |
+
```
|
314 |
+
|
315 |
+
### Marginals
|
316 |
+
|
317 |
+
|
318 |
+
The Bokeh backend also supports marginal plots to generate adjoined plots. The most convenient way to build an AdjointLayout is with the ``.hist()`` method.
|
319 |
+
|
320 |
+
|
321 |
+
```python
|
322 |
+
points = hv.Points(np.random.randn(500,2))
|
323 |
+
points.hist(num_bins=51, dimension=['x','y'])
|
324 |
+
```
|
325 |
+
|
326 |
+
When the histogram represents a quantity that is mapped to a value dimension with a corresponding colormap, it will automatically share the colormap, making it useful as a colorbar for that dimension as well as a histogram.
|
327 |
+
|
328 |
+
|
329 |
+
```python
|
330 |
+
img.hist(num_bins=100, dimension=['x', 'y'], weight_dimension='z', mean_weighted=True) +\
|
331 |
+
img.hist(dimension='z')
|
332 |
+
```
|
333 |
+
|
334 |
+
## Tools
|
335 |
+
|
336 |
+
Bokeh provides a range of tools to interact with a plot and HoloViews adds a number of tools by default but also makes it easy to add additional tools. The ``default_tools`` define the list of tools that are added automatically and usually a user would override only the ``tools`` option to add additional tools. By default the ``default_tools`` include:
|
337 |
+
|
338 |
+
['save', 'pan', 'wheel_zoom', 'box_zoom', 'reset']
|
339 |
+
|
340 |
+
#### Toolbar
|
341 |
+
|
342 |
+
The bokeh toolbar is added automatically and will be placed to the right for a single plot and on the top for a layout. Additionally for layouts of plots it will automatically merge the toolbars to avoid crowding the plot. However both behaviors can be customized, the toolbar can be hidden or moved to a different location on a plot by setting the ``toolbar`` option to one of:
|
343 |
+
|
344 |
+
['above', 'below', 'left', 'right', 'disable', None]
|
345 |
+
|
346 |
+
Secondly a layout or grid plot supports the ``merge_tools`` option which can be used to maintain one toolbar per plot:
|
347 |
+
|
348 |
+
|
349 |
+
```python
|
350 |
+
(hv.Curve([1, 2, 3]).opts(toolbar='above') + hv.Curve([1, 2, 3]).opts(toolbar=None)).opts(merge_tools=False)
|
351 |
+
```
|
352 |
+
|
353 |
+
#### Hover tools
|
354 |
+
|
355 |
+
Some Elements allow revealing additional data by hovering over the data. To enable the hover tool, simply supply ``'hover'`` as a list to the ``tools`` plot option. By default the tool will display information for all the dimensions specified on the element:
|
356 |
+
|
357 |
+
|
358 |
+
```python
|
359 |
+
error = np.random.rand(100, 3)
|
360 |
+
heatmap_data = {(chr(65+i), chr(97+j)):i*j for i in range(5) for j in range(5) if i!=j}
|
361 |
+
data = [np.random.normal() for i in range(10000)]
|
362 |
+
hist = np.histogram(data, 20)
|
363 |
+
|
364 |
+
points = hv.Points(error)
|
365 |
+
heatmap = hv.HeatMap(heatmap_data).sort()
|
366 |
+
histogram = hv.Histogram(hist)
|
367 |
+
image = hv.Image(np.random.rand(50,50))
|
368 |
+
|
369 |
+
(points + heatmap + histogram + image).opts(
|
370 |
+
opts.Points(tools=['hover'], size=5), opts.HeatMap(tools=['hover']),
|
371 |
+
opts.Image(tools=['hover']), opts.Histogram(tools=['hover']),
|
372 |
+
opts.Layout(shared_axes=False)).cols(2)
|
373 |
+
```
|
374 |
+
|
375 |
+
Additionally, you can provide `'vline'`, the equivalent of passing `HoverTool(mode='vline')`, or `'hline'` to set the hit-testing behavior
|
376 |
+
|
377 |
+
|
378 |
+
```python
|
379 |
+
error = np.random.rand(100, 3)
|
380 |
+
heatmap_data = {(chr(65+i), chr(97+j)):i*j for i in range(5) for j in range(5) if i!=j}
|
381 |
+
data = [np.random.normal() for i in range(10000)]
|
382 |
+
hist = np.histogram(data, 20)
|
383 |
+
|
384 |
+
points = hv.Points(error)
|
385 |
+
heatmap = hv.HeatMap(heatmap_data).sort()
|
386 |
+
histogram = hv.Histogram(hist)
|
387 |
+
image = hv.Image(np.random.rand(50,50))
|
388 |
+
|
389 |
+
(points + heatmap + histogram + image).opts(
|
390 |
+
opts.Points(tools=['hline'], size=5), opts.HeatMap(tools=['hover']),
|
391 |
+
opts.Image(tools=['vline']), opts.Histogram(tools=['hover']),
|
392 |
+
opts.Layout(shared_axes=False)).cols(2)
|
393 |
+
```
|
394 |
+
|
395 |
+
It is also possible to explicitly declare the columns to display by manually constructing a `HoverTool` and declaring the tooltips as a list of tuples of the labels and a specification of the dimension name and how to display it (for a complete reference see the [bokeh user guide](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#hovertool)).
|
396 |
+
|
397 |
+
|
398 |
+
```python
|
399 |
+
from bokeh.models import HoverTool
|
400 |
+
from bokeh.sampledata.periodic_table import elements
|
401 |
+
|
402 |
+
points = hv.Points(
|
403 |
+
elements, ['electronegativity', 'density'],
|
404 |
+
['name', 'symbol', 'metal', 'CPK', 'atomic radius']
|
405 |
+
).sort('metal')
|
406 |
+
|
407 |
+
tooltips = [
|
408 |
+
('Name', '@name'),
|
409 |
+
('Symbol', '@symbol'),
|
410 |
+
('CPK', '$color[hex, swatch]:CPK')
|
411 |
+
]
|
412 |
+
hover = HoverTool(tooltips=tooltips)
|
413 |
+
|
414 |
+
points.opts(
|
415 |
+
tools=[hover], color='metal', cmap='Category20',
|
416 |
+
line_color='black', size=dim('atomic radius')/10,
|
417 |
+
width=600, height=400, show_grid=True,
|
418 |
+
title='Chemical Elements by Type (scaled by atomic radius)')
|
419 |
+
```
|
420 |
+
|
421 |
+
#### Selection tools
|
422 |
+
|
423 |
+
Bokeh provides a number of tools for selecting data points including ``tap``, ``box_select``, ``lasso_select`` and ``poly_select``. To distinguish between selected and unselected data points we can also control the color and alpha of the ``selection`` and ``nonselection`` points. You can try out any of these selection tools and see how the plot is affected:
|
424 |
+
|
425 |
+
|
426 |
+
```python
|
427 |
+
hv.Points(error).opts(
|
428 |
+
color='blue', nonselection_color='red', size=10, tools=['box_select', 'lasso_select', 'tap'])
|
429 |
+
```
|
430 |
+
|
431 |
+
#### Selection tool with shared axes and linked brushing
|
432 |
+
|
433 |
+
When dealing with complex multi-variate data it is often useful to explore interactions between variables across plots. HoloViews will automatically link the data sources of plots in a Layout if they draw from the same data, allowing for both linked axes and brushing.
|
434 |
+
|
435 |
+
We'll see what this looks like in practice using a small dataset of macro-economic data:
|
436 |
+
|
437 |
+
|
438 |
+
```python
|
439 |
+
macro_df = pd.read_csv('http://assets.holoviews.org/macro.csv', sep='\t')
|
440 |
+
```
|
441 |
+
|
442 |
+
By creating two ``Points`` Elements, which both draw their data from the same pandas DataFrame, the two plots become automatically linked. Note that the [Linking Plots user guide](Linking_Plots.ipynb) provides a more explicit way to declare two elements as being linked without having to share the same underlying datastructure. The automated linking behavior can be toggled with the ``shared_datasource`` plot option on a ``Layout`` or ``GridSpace``. You can try selecting data in one plot, and see how the corresponding data (those on the same rows of the DataFrame, even if the plots show different data, will be highlighted in each.
|
443 |
+
|
444 |
+
|
445 |
+
```python
|
446 |
+
(hv.Scatter(macro_df, 'year', 'gdp') + hv.Scatter(macro_df, 'gdp', 'unem')).opts(
|
447 |
+
opts.Scatter(tools=['box_select', 'lasso_select']), opts.Layout(shared_axes=True, shared_datasource=True))
|
448 |
+
```
|
449 |
+
|
450 |
+
A gridmatrix is a clear use case for linked plotting. This operation plots any combination of numeric variables against each other, in a grid, and selecting datapoints in any plot will highlight them in all of them. Such linking can thus reveal how values in a particular range (e.g. very large outliers along one dimension) relate to each of the other dimensions.
|
451 |
+
|
452 |
+
|
453 |
+
```python
|
454 |
+
table = hv.Dataset(macro_df, kdims=['year', 'country'])
|
455 |
+
matrix = hv.operation.gridmatrix(table.groupby('country'))
|
456 |
+
|
457 |
+
matrix.select(country=['West Germany', 'United Kingdom', 'United States']).opts(
|
458 |
+
opts.Scatter(tools=['box_select', 'lasso_select', 'hover'], border=0))
|
459 |
+
```
|
460 |
+
|
461 |
+
#### Drawing Tools
|
462 |
+
|
463 |
+
Another commonly useful set of tools are the drawing tools which are integrated with the linked streams introduced in the [Custom Interactivity guide](13-Custom_Interactivity.ipynb). These tools allow drawing and annotating a plot and accessing the annotation back in Python. The available drawing tools include:
|
464 |
+
|
465 |
+
* [PointDraw](../reference/streams/bokeh/PointDraw.ipynb): The ``PointDraw`` stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting points.
|
466 |
+
* [BoxEdit](../reference/streams/bokeh/BoxEdit.ipynb): The ``BoxEdit`` stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting boxes.
|
467 |
+
* [FreehandDraw](../reference/streams/bokeh/FreehandDraw.ipynb): The ``FreehandDraw`` stream adds a bokeh tool to the source plot, which allows freehand drawing on the plot canvas
|
468 |
+
* [PolyDraw](../reference/streams/bokeh/PolyDraw.ipynb): The ``PolyDraw`` stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting polygons and paths.
|
469 |
+
* [PolyEdit](../reference/streams/bokeh/PolyEdit.ipynb): The ``PolyEdit`` stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting vertices on polygons and paths.
|
470 |
+
|
471 |
+
Each of the reference notebooks explains the tools in more detail but to get an of how these tools work see the ``FreehandDraw`` example below, which allows drawing on the canvas and accessing the drawn data from Python:
|
472 |
+
|
473 |
+
|
474 |
+
```python
|
475 |
+
path = hv.Path([])
|
476 |
+
freehand = hv.streams.FreehandDraw(source=path, num_objects=3)
|
477 |
+
|
478 |
+
path.opts(
|
479 |
+
opts.Path(active_tools=['freehand_draw'], height=400, line_width=10, width=400))
|
480 |
+
```
|
481 |
+
|
482 |
+
To access the data from Python, you can access the ``element`` property on the stream, which lets us access each drawn line drawn as a separate ``Path`` element:
|
483 |
+
|
484 |
+
|
485 |
+
```python
|
486 |
+
freehand.element.split()
|
487 |
+
```
|
488 |
+
|
489 |
+
The [Reference Gallery](http://holoviews.org/reference/index.html) shows examples of all the Elements supported for Bokeh, in a format that can be compared with the corresponding matplotlib versions.
|
490 |
+
|
491 |
+
## Theming
|
492 |
+
|
493 |
+
Bokeh supports theming via the [Theme object](https://bokeh.pydata.org/en/latest/docs/reference/themes.html) object which can also be using in HoloViews. Applying a Bokeh theme is useful when you need to set detailed aesthetic options not directly exposed via the HoloViews style options.
|
494 |
+
|
495 |
+
To apply a Bokeh theme, you will need to create a ``Theme`` object:
|
496 |
+
|
497 |
+
|
498 |
+
```python
|
499 |
+
from bokeh.themes.theme import Theme
|
500 |
+
|
501 |
+
theme = Theme(
|
502 |
+
json={
|
503 |
+
'attrs' : {
|
504 |
+
'Figure' : {
|
505 |
+
'background_fill_color': '#2F2F2F',
|
506 |
+
'border_fill_color': '#2F2F2F',
|
507 |
+
'outline_line_color': '#444444',
|
508 |
+
},
|
509 |
+
'Grid': {
|
510 |
+
'grid_line_dash': [6, 4],
|
511 |
+
'grid_line_alpha': .3,
|
512 |
+
},
|
513 |
+
|
514 |
+
'Axis': {
|
515 |
+
'major_label_text_color': 'white',
|
516 |
+
'axis_label_text_color': 'white',
|
517 |
+
'major_tick_line_color': 'white',
|
518 |
+
'minor_tick_line_color': 'white',
|
519 |
+
'axis_line_color': "white"
|
520 |
+
}
|
521 |
+
}
|
522 |
+
})
|
523 |
+
```
|
524 |
+
|
525 |
+
Instead of supplying a JSON object, you can also create a Bokeh ``Theme`` object from a [YAML file](https://bokeh.pydata.org/en/latest/docs/reference/themes.html). Once the ``Theme`` object is created, you can apply it by setting it on the ``theme`` parameter of the current Bokeh renderer:
|
526 |
+
|
527 |
+
|
528 |
+
```python
|
529 |
+
hv.renderer('bokeh').theme = theme
|
530 |
+
```
|
531 |
+
|
532 |
+
The theme will then be applied to subsequent plots:
|
533 |
+
|
534 |
+
|
535 |
+
```python
|
536 |
+
xs = np.linspace(0, np.pi*4, 100)
|
537 |
+
hv.Curve((xs, np.sin(xs)), label='foo').opts(bgcolor='grey')
|
538 |
+
```
|
539 |
+
|
540 |
+
You may also supply a name from Bokeh's built-in-themes:
|
541 |
+
|
542 |
+
|
543 |
+
```python
|
544 |
+
hv.renderer('bokeh').theme = 'light_minimal'
|
545 |
+
xs = np.linspace(0, np.pi*4, 100)
|
546 |
+
hv.Curve((xs, np.sin(xs)), label='foo')
|
547 |
+
```
|
548 |
+
|
549 |
+
To disable theming, you can set the ``theme`` parameter on the Bokeh renderer to ``None``.
|
hvplot_docs/Plotting_with_Matplotlib.md
ADDED
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page demonstrates the use of the **Matplotlib** plotting backend, the equivalent page demonstrating the Plotly backend may be found [here](Plotting_with_Plotly.ipynb)
|
2 |
+
|
3 |
+
As we discovered in the [Introduction](Introduction.ipynb), HoloViews allows plotting a variety of data types. Here we will use the sample data module and load the pandas and dask hvPlot API:
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import numpy as np
|
8 |
+
import hvplot.pandas # noqa
|
9 |
+
import hvplot.dask # noqa
|
10 |
+
|
11 |
+
hvplot.extension('matplotlib')
|
12 |
+
```
|
13 |
+
|
14 |
+
As we learned the hvPlot API closely mirrors the [Pandas plotting API](https://pandas.pydata.org/pandas-docs/stable/visualization.html), but instead of generating static images when used in a notebook, it uses HoloViews to generate either static or dynamically streaming Bokeh plots. Static plots can be used in any context, while streaming plots require a live [Jupyter notebook](https://jupyter.org), a deployed [Bokeh Server app](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html), or a deployed [Panel](https://panel.pyviz.org) app.
|
15 |
+
|
16 |
+
HoloViews provides an extensive, very rich set of objects along with a powerful set of operations to apply, as you can find out in the [HoloViews User Guide](https://holoviews.org/user_guide/index.html). But here we will focus on the most essential mechanisms needed to make your data visualizable, without having to worry about the mechanics going on behind the scenes.
|
17 |
+
|
18 |
+
We will be focusing on two different datasets:
|
19 |
+
|
20 |
+
- A small CSV file of US crime data, broken down by state
|
21 |
+
- A larger Parquet-format file of airline flight data
|
22 |
+
|
23 |
+
The ``hvplot.sample_data`` module makes these datasets Intake data catalogue, which we can load either using pandas:
|
24 |
+
|
25 |
+
|
26 |
+
```python
|
27 |
+
from hvplot.sample_data import us_crime, airline_flights
|
28 |
+
|
29 |
+
crime = us_crime.read()
|
30 |
+
print(type(crime))
|
31 |
+
crime.head()
|
32 |
+
```
|
33 |
+
|
34 |
+
Or using dask as a ``dask.DataFrame``:
|
35 |
+
|
36 |
+
|
37 |
+
```python
|
38 |
+
flights = airline_flights.to_dask().persist()
|
39 |
+
print(type(flights))
|
40 |
+
flights.head()
|
41 |
+
```
|
42 |
+
|
43 |
+
## The plot interface
|
44 |
+
|
45 |
+
The ``dask.dataframe.DataFrame.hvplot``, ``pandas.DataFrame.hvplot`` and ``intake.DataSource.plot`` interfaces (and Series equivalents) from HvPlot provide a powerful high-level API to generate complex plots. The ``.hvplot`` API can be called directly or used as a namespace to generate specific plot types.
|
46 |
+
|
47 |
+
### The plot method
|
48 |
+
|
49 |
+
The most explicit way to use the plotting API is to specify the names of columns to plot on the ``x``- and ``y``-axis respectively:
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
crime.hvplot.line(x='Year', y='Violent Crime rate')
|
54 |
+
```
|
55 |
+
|
56 |
+
As you'll see in more detail below, you can choose which kind of plot you want to use for the data:
|
57 |
+
|
58 |
+
|
59 |
+
```python
|
60 |
+
crime.hvplot(x='Year', y='Violent Crime rate', kind='scatter')
|
61 |
+
```
|
62 |
+
|
63 |
+
To group the data by one or more additional columns, specify an additional ``by`` variable. As an example here we will plot the departure delay ('depdelay') as a function of 'distance', grouping the data by the 'carrier'. There are many available carriers, so we will select only two of them so that the plot is readable:
|
64 |
+
|
65 |
+
|
66 |
+
```python
|
67 |
+
flight_subset = flights[flights.carrier.isin(['OH', 'F9'])]
|
68 |
+
flight_subset.hvplot(x='distance', y='depdelay', by='carrier', kind='scatter', alpha=0.2, persist=True)
|
69 |
+
```
|
70 |
+
|
71 |
+
Here we have specified the `x` axis explicitly, which can be omitted if the Pandas index column is already the desired x axis. Similarly, here we specified the `y` axis; by default all of the non-index columns would be plotted (which would be a lot of data in this case). If you don't specify the 'y' axis, it will have a default label named 'value', but you can then provide a y axis label explicitly using the ``value_label`` option.
|
72 |
+
|
73 |
+
Putting all of this together we will plot violent crime, robbery, and burglary rates on the y-axis, specifying 'Year' as the x, and relabel the y-axis to display the 'Rate'.
|
74 |
+
|
75 |
+
|
76 |
+
```python
|
77 |
+
crime.hvplot(x='Year', y=['Violent Crime rate', 'Robbery rate', 'Burglary rate'],
|
78 |
+
value_label='Rate (per 100k people)')
|
79 |
+
```
|
80 |
+
|
81 |
+
### The hvplot namespace
|
82 |
+
|
83 |
+
Instead of using the ``kind`` argument to the plot call, we can use the ``hvplot`` namespace, which lets us easily discover the range of plot types that are supported. Use tab completion to explore the available plot types:
|
84 |
+
|
85 |
+
```python
|
86 |
+
crime.hvplot.<TAB>
|
87 |
+
```
|
88 |
+
|
89 |
+
Plot types available include:
|
90 |
+
|
91 |
+
* <a href="#area">``.area()``</a>: Plots a area chart similar to a line chart except for filling the area under the curve and optionally stacking
|
92 |
+
* <a href="#bars">``.bar()``</a>: Plots a bar chart that can be stacked or grouped
|
93 |
+
* <a href="#bivariate">``.bivariate()``</a>: Plots 2D density of a set of points
|
94 |
+
* <a href="#box-whisker-plots">``.box()``</a>: Plots a box-whisker chart comparing the distribution of one or more variables
|
95 |
+
* <a href="#heatmap">``.heatmap()``</a>: Plots a heatmap to visualizing a variable across two independent dimensions
|
96 |
+
* <a href="#hexbins">``.hexbins()``</a>: Plots hex bins
|
97 |
+
* <a href="#histogram">``.hist()``</a>: Plots the distribution of one or histograms as a set of bins
|
98 |
+
* <a href="#kde-density">``.kde()``</a>: Plots the kernel density estimate of one or more variables.
|
99 |
+
* <a href="#the-plot-method">``.line()``</a>: Plots a line chart (such as for a time series)
|
100 |
+
* <a href="#scatter">``.scatter()``</a>: Plots a scatter chart comparing two variables
|
101 |
+
* <a href="#step">``.step()``</a>: Plots a step chart akin to a line plot
|
102 |
+
* <a href="#tables">``.table()``</a>: Generates a SlickGrid DataTable
|
103 |
+
* <a href="#groupby">``.violin()``</a>: Plots a violin plot comparing the distribution of one or more variables using the kernel density estimate
|
104 |
+
|
105 |
+
#### Area
|
106 |
+
|
107 |
+
Like most other plot types the ``area`` chart supports the three ways of defining a plot outlined above. An area chart is most useful when plotting multiple variables in a stacked chart. This can be achieve by specifying ``x``, ``y``, and ``by`` columns or using the ``columns`` and ``index``/``use_index`` (equivalent to ``x``) options:
|
108 |
+
|
109 |
+
|
110 |
+
```python
|
111 |
+
crime.hvplot.area(x='Year', y=['Robbery', 'Aggravated assault'])
|
112 |
+
```
|
113 |
+
|
114 |
+
We can also explicitly set ``stacked`` to False and define an ``alpha`` value to compare the values directly:
|
115 |
+
|
116 |
+
|
117 |
+
```python
|
118 |
+
crime.hvplot.area(x='Year', y=['Aggravated assault', 'Robbery'], stacked=False, alpha=0.4)
|
119 |
+
```
|
120 |
+
|
121 |
+
Another use for an area plot is to visualize the spread of a value. For instance using the flights dataset we may want to see the spread in mean delay values across carriers. For that purpose we compute the mean delay by day and carrier and then the min/max mean delay for across all carriers. Since the output of ``hvplot`` is just a regular holoviews object, we can use the overlay operator (\*) to place the plots on top of each other.
|
122 |
+
|
123 |
+
|
124 |
+
```python
|
125 |
+
delay_min_max = flights.groupby(['day', 'carrier'])['carrier_delay'].mean().groupby('day').agg({'min': np.min, 'max': np.max})
|
126 |
+
delay_mean = flights.groupby('day')['carrier_delay'].mean()
|
127 |
+
|
128 |
+
delay_min_max.hvplot.area(x='day', y='min', y2='max', alpha=0.2) * delay_mean.hvplot()
|
129 |
+
```
|
130 |
+
|
131 |
+
#### Bars
|
132 |
+
|
133 |
+
In the simplest case we can use ``.hvplot.bar`` to plot ``x`` against ``y``. We'll use ``rot=90`` to rotate the tick labels on the x-axis making the years easier to read:
|
134 |
+
|
135 |
+
|
136 |
+
```python
|
137 |
+
crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90, width=900)
|
138 |
+
```
|
139 |
+
|
140 |
+
If we want to compare multiple columns instead we can set ``y`` to a list of columns. Using the ``stacked`` option we can then compare the column values more easily:
|
141 |
+
|
142 |
+
|
143 |
+
```python
|
144 |
+
crime.hvplot.bar(x='Year', y=['Violent crime total', 'Property crime total'],
|
145 |
+
stacked=True, rot=90, width=900, legend='top_left')
|
146 |
+
```
|
147 |
+
|
148 |
+
#### Scatter
|
149 |
+
|
150 |
+
The scatter plot supports many of the same features as the other chart types we have seen so far but can also be colored by another variable using the ``c`` option.
|
151 |
+
|
152 |
+
|
153 |
+
```python
|
154 |
+
crime.hvplot.scatter(x='Violent Crime rate', y='Burglary rate', c='Year')
|
155 |
+
```
|
156 |
+
|
157 |
+
Anytime that color is being used to represent a dimension, the ``cmap`` option can be used to control the colormap that is used to represent that dimension. Additionally, the colorbar can be disabled using ``colorbar=False``.
|
158 |
+
|
159 |
+
#### Step
|
160 |
+
|
161 |
+
A step chart is very similar to a line chart but instead of linearly interpolating between samples the step chart visualizes discrete steps. The point at which to step can be controlled via the ``where`` keyword allowing `'pre'`, `'mid'` (default) and `'post'` values:
|
162 |
+
|
163 |
+
|
164 |
+
```python
|
165 |
+
crime.hvplot.step(x='Year', y=['Robbery', 'Aggravated assault'])
|
166 |
+
```
|
167 |
+
|
168 |
+
#### HexBins
|
169 |
+
|
170 |
+
You can create hexagonal bin plots with the ``hexbin`` method. Hexbin plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually. Since these data are not regularly distributed, we'll use the ``logz`` option to map z-axis (color) to a log scale colorbar.
|
171 |
+
|
172 |
+
|
173 |
+
```python
|
174 |
+
# flights.hvplot.hexbin(x='airtime', y='arrdelay', width=600, height=500, logz=True);
|
175 |
+
```
|
176 |
+
|
177 |
+
<div class="alert alert-warning" role="alert">
|
178 |
+
Output suppressed as this is <a href='https://github.com/holoviz/hvplot/pull/653#issuecomment-964056881'>not currently supported</a> with the Matplotlib backend and doesn't display any plot.
|
179 |
+
</div>
|
180 |
+
|
181 |
+
#### Bivariate
|
182 |
+
|
183 |
+
You can create a 2D density plot with the ``bivariate`` method. Bivariate plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually.
|
184 |
+
|
185 |
+
|
186 |
+
```python
|
187 |
+
crime.hvplot.bivariate(x='Violent Crime rate', y='Burglary rate', width=600, height=500)
|
188 |
+
```
|
189 |
+
|
190 |
+
#### HeatMap
|
191 |
+
|
192 |
+
A ``HeatMap`` lets us view the relationship between three variables, so we specify the 'x' and 'y' variables and an additional 'C' variable. Additionally we can define a ``reduce_function`` that computes the values for each bin from the samples that fall into it. Here we plot the 'depdelay' (i.e. departure delay) for each day of the month and carrier in the dataset:
|
193 |
+
|
194 |
+
|
195 |
+
```python
|
196 |
+
flights.compute().hvplot.heatmap(x='day', y='carrier', C='depdelay', reduce_function=np.mean).opts(show_values=False)
|
197 |
+
```
|
198 |
+
|
199 |
+
#### Tables
|
200 |
+
|
201 |
+
Unlike all other plot types, a table only supports one signature: either all columns are plotted, or a subset of columns can be selected by defining the ``columns`` explicitly:
|
202 |
+
|
203 |
+
|
204 |
+
```python
|
205 |
+
crime.hvplot.table(columns=['Year', 'Population', 'Violent Crime rate'], width=400)
|
206 |
+
```
|
207 |
+
|
208 |
+
### Distributions
|
209 |
+
|
210 |
+
Plotting distributions differs slightly from other plots since they plot only one variable in the simple case rather than plotting two or more variables against each other. Therefore when plotting these plot types no ``index`` or ``x`` value needs to be supplied. Instead:
|
211 |
+
|
212 |
+
1. Declare a single ``y`` variable, e.g. ``source.plot.hist(variable)``, or
|
213 |
+
2. Declare a ``y`` variable and ``by`` variable, e.g. ``source.plot.hist(variable, by='Group')``, or
|
214 |
+
3. Declare columns or plot all columns, e.g. ``source.plot.hist()`` or ``source.plot.hist(columns=['A', 'B', 'C'])``
|
215 |
+
|
216 |
+
#### Histogram
|
217 |
+
|
218 |
+
The Histogram is the simplest example of a distribution; often we simply plot the distribution of a single variable, in this case the 'Violent Crime rate'. Additionally we can define a range over which to compute the histogram and the number of bins using the ``bin_range`` and ``bins`` arguments respectively:
|
219 |
+
|
220 |
+
|
221 |
+
```python
|
222 |
+
crime.hvplot.hist(y='Violent Crime rate')
|
223 |
+
```
|
224 |
+
|
225 |
+
Or we can plot the distribution of multiple columns:
|
226 |
+
|
227 |
+
|
228 |
+
```python
|
229 |
+
columns = ['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
230 |
+
crime.hvplot.hist(y=columns, bins=50, alpha=0.5, legend='top', height=400)
|
231 |
+
```
|
232 |
+
|
233 |
+
We can also group the data by another variable. Here we'll use ``subplots`` to split each carrier out into its own plot:
|
234 |
+
|
235 |
+
|
236 |
+
```python
|
237 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
238 |
+
# flight_subset.hvplot.hist('depdelay', by='carrier', bins=20, bin_range=(-20, 100), width=300, subplots=True);
|
239 |
+
```
|
240 |
+
|
241 |
+
<div class="alert alert-warning" role="alert">
|
242 |
+
Output suppressed as this is <a href='https://github.com/holoviz/holoviews/issues/5111'>not currently supported</a> with the Matplotlib backend and raises an error. Tip: execute <code>flight_subset.compute()</code> before plotting to avoid the error.
|
243 |
+
</div>
|
244 |
+
|
245 |
+
#### KDE (density)
|
246 |
+
|
247 |
+
You can also create density plots using ``hvplot.kde()`` or ``hvplot.density()``:
|
248 |
+
|
249 |
+
|
250 |
+
```python
|
251 |
+
crime.hvplot.kde(y='Violent Crime rate')
|
252 |
+
```
|
253 |
+
|
254 |
+
Comparing the distribution of multiple columns is also possible:
|
255 |
+
|
256 |
+
|
257 |
+
```python
|
258 |
+
columns=['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
259 |
+
crime.hvplot.kde(y=columns, alpha=0.5, value_label='Rate', legend='top_right')
|
260 |
+
```
|
261 |
+
|
262 |
+
The ``hvplot.kde`` also supports the ``by`` keyword:
|
263 |
+
|
264 |
+
|
265 |
+
```python
|
266 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
267 |
+
flight_subset.hvplot.kde('depdelay', by='carrier', xlim=(-20, 70), width=300, subplots=True)
|
268 |
+
```
|
269 |
+
|
270 |
+
#### Box-Whisker Plots
|
271 |
+
|
272 |
+
Just like the other distribution-based plot types, the box-whisker plot supports plotting a single column:
|
273 |
+
|
274 |
+
|
275 |
+
```python
|
276 |
+
crime.hvplot.box(y='Violent Crime rate')
|
277 |
+
```
|
278 |
+
|
279 |
+
It also supports multiple columns and the same options as seen previously (``legend``, ``invert``, ``value_label``):
|
280 |
+
|
281 |
+
|
282 |
+
```python
|
283 |
+
columns=['Burglary rate', 'Larceny-theft rate', 'Motor vehicle theft rate',
|
284 |
+
'Property crime rate', 'Violent Crime rate']
|
285 |
+
crime.hvplot.box(y=columns, group_label='Crime', legend=False, value_label='Rate (per 100k)', invert=True)
|
286 |
+
```
|
287 |
+
|
288 |
+
Lastly, it also supports using the ``by`` keyword to split the data into multiple subsets:
|
289 |
+
|
290 |
+
|
291 |
+
```python
|
292 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
293 |
+
# flight_subset.hvplot.box('depdelay', by='carrier', ylim=(-10, 70));
|
294 |
+
```
|
295 |
+
|
296 |
+
<div class="alert alert-warning" role="alert">
|
297 |
+
Output suppressed as this is <a href='https://github.com/holoviz/holoviews/issues/5120'>not currently supported</a> with the Matplotlib backend and displays an empty plot.
|
298 |
+
</div>
|
299 |
+
|
300 |
+
## Composing Plots
|
301 |
+
|
302 |
+
One of the core strengths of HoloViews is the ease of composing
|
303 |
+
different plots. Individual plots can be composed using the ``*`` and
|
304 |
+
``+`` operators, which overlay and compose plots into layouts
|
305 |
+
respectively. For more information on composing objects, see the
|
306 |
+
HoloViews [User Guide](https://holoviews.org/user_guide/Composing_Elements.html).
|
307 |
+
|
308 |
+
By using these operators we can combine multiple plots into composite plots. A simple example is overlaying two plot types:
|
309 |
+
|
310 |
+
|
311 |
+
```python
|
312 |
+
crime.hvplot(x='Year', y='Violent Crime rate') * crime.hvplot.scatter(x='Year', y='Violent Crime rate', c='k')
|
313 |
+
```
|
314 |
+
|
315 |
+
We can also lay out different plots and tables together:
|
316 |
+
|
317 |
+
|
318 |
+
```python
|
319 |
+
(crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90, width=550) +
|
320 |
+
crime.hvplot.table(['Year', 'Population', 'Violent Crime rate'], width=420))
|
321 |
+
```
|
322 |
+
|
323 |
+
## Large data
|
324 |
+
|
325 |
+
The previous examples summarized the fairly large airline dataset using statistical plot types that aggregate the data into a feasible subset for plotting. We can instead aggregate the data directly into the viewable image using [datashader](https://datashader.org), which provides a rendering of the entire set of raw data available (as far as the resolution of the screen allows). Here we plot the 'airtime' against the 'distance':
|
326 |
+
|
327 |
+
|
328 |
+
```python
|
329 |
+
flights.hvplot.scatter(x='distance', y='airtime', datashade=True)
|
330 |
+
```
|
331 |
+
|
332 |
+
## Groupby
|
333 |
+
|
334 |
+
Thanks to the ability of HoloViews to explore a parameter space with a set of widgets we can apply a groupby along a particular column or dimension. For example we can view the distribution of departure delays by carrier grouped by day, allowing the user to choose which day to display:
|
335 |
+
|
336 |
+
|
337 |
+
```python
|
338 |
+
flights.hvplot.violin(y='depdelay', by='carrier', groupby='dayofweek', ylim=(-20, 60), height=500)
|
339 |
+
```
|
340 |
+
|
341 |
+
This user guide merely provided an overview over the available plot types; to see a detailed description on how to customize plots see the [Customization](Customization.ipynb) user guide.
|
hvplot_docs/Plotting_with_Plotly.md
ADDED
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page demonstrates the use of the **Plotly** plotting backend, the equivalent page demonstrating the Matplotlib backend may be found [here](Plotting_with_Matplotlib.ipynb)
|
2 |
+
|
3 |
+
As we discovered in the [Introduction](Introduction.ipynb), HoloViews allows plotting a variety of data types. Here we will use the sample data module and load the pandas and dask hvPlot API:
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import numpy as np
|
8 |
+
import hvplot.pandas # noqa
|
9 |
+
import hvplot.dask # noqa
|
10 |
+
```
|
11 |
+
|
12 |
+
|
13 |
+
```python
|
14 |
+
hvplot.extension('plotly')
|
15 |
+
```
|
16 |
+
|
17 |
+
As we learned the hvPlot API closely mirrors the [Pandas plotting API](https://pandas.pydata.org/pandas-docs/stable/visualization.html), but instead of generating static images when used in a notebook, it uses HoloViews to generate either static or dynamically streaming Bokeh plots. Static plots can be used in any context, while streaming plots require a live [Jupyter notebook](https://jupyter.org), a deployed [Bokeh Server app](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html), or a deployed [Panel](https://panel.holoviz.org) app.
|
18 |
+
|
19 |
+
HoloViews provides an extensive, very rich set of objects along with a powerful set of operations to apply, as you can find out in the [HoloViews User Guide](https://holoviews.org/user_guide/index.html). But here we will focus on the most essential mechanisms needed to make your data visualizable, without having to worry about the mechanics going on behind the scenes.
|
20 |
+
|
21 |
+
We will be focusing on two different datasets:
|
22 |
+
|
23 |
+
- A small CSV file of US crime data, broken down by state
|
24 |
+
- A larger Parquet-format file of airline flight data
|
25 |
+
|
26 |
+
The ``hvplot.sample_data`` module makes these datasets Intake data catalogue, which we can load either using pandas:
|
27 |
+
|
28 |
+
|
29 |
+
```python
|
30 |
+
from hvplot.sample_data import us_crime, airline_flights
|
31 |
+
|
32 |
+
crime = us_crime.read()
|
33 |
+
print(type(crime))
|
34 |
+
crime.head()
|
35 |
+
```
|
36 |
+
|
37 |
+
Or using dask as a ``dask.DataFrame``:
|
38 |
+
|
39 |
+
|
40 |
+
```python
|
41 |
+
flights = airline_flights.to_dask().persist()
|
42 |
+
print(type(flights))
|
43 |
+
flights.head()
|
44 |
+
```
|
45 |
+
|
46 |
+
## The plot interface
|
47 |
+
|
48 |
+
The ``dask.dataframe.DataFrame.hvplot``, ``pandas.DataFrame.hvplot`` and ``intake.DataSource.plot`` interfaces (and Series equivalents) from HvPlot provide a powerful high-level API to generate complex plots. The ``.hvplot`` API can be called directly or used as a namespace to generate specific plot types.
|
49 |
+
|
50 |
+
### The plot method
|
51 |
+
|
52 |
+
The most explicit way to use the plotting API is to specify the names of columns to plot on the ``x``- and ``y``-axis respectively:
|
53 |
+
|
54 |
+
|
55 |
+
```python
|
56 |
+
crime.hvplot.line(x='Year', y='Violent Crime rate')
|
57 |
+
```
|
58 |
+
|
59 |
+
As you'll see in more detail below, you can choose which kind of plot you want to use for the data:
|
60 |
+
|
61 |
+
|
62 |
+
```python
|
63 |
+
crime.hvplot(x='Year', y='Violent Crime rate', kind='scatter')
|
64 |
+
```
|
65 |
+
|
66 |
+
To group the data by one or more additional columns, specify an additional ``by`` variable. As an example here we will plot the departure delay ('depdelay') as a function of 'distance', grouping the data by the 'carrier'. There are many available carriers, so we will select only two of them so that the plot is readable:
|
67 |
+
|
68 |
+
|
69 |
+
```python
|
70 |
+
flight_subset = flights[flights.carrier.isin(['OH', 'F9'])]
|
71 |
+
flight_subset.hvplot(x='distance', y='depdelay', by='carrier', kind='scatter', alpha=0.2, persist=True)
|
72 |
+
```
|
73 |
+
|
74 |
+
Here we have specified the `x` axis explicitly, which can be omitted if the Pandas index column is already the desired x axis. Similarly, here we specified the `y` axis; by default all of the non-index columns would be plotted (which would be a lot of data in this case). If you don't specify the 'y' axis, it will have a default label named 'value', but you can then provide a y axis label explicitly using the ``value_label`` option.
|
75 |
+
|
76 |
+
Putting all of this together we will plot violent crime, robbery, and burglary rates on the y-axis, specifying 'Year' as the x, and relabel the y-axis to display the 'Rate'.
|
77 |
+
|
78 |
+
|
79 |
+
```python
|
80 |
+
crime.hvplot(x='Year', y=['Violent Crime rate', 'Robbery rate', 'Burglary rate'],
|
81 |
+
value_label='Rate (per 100k people)')
|
82 |
+
```
|
83 |
+
|
84 |
+
### The hvplot namespace
|
85 |
+
|
86 |
+
Instead of using the ``kind`` argument to the plot call, we can use the ``hvplot`` namespace, which lets us easily discover the range of plot types that are supported. Use tab completion to explore the available plot types:
|
87 |
+
|
88 |
+
```python
|
89 |
+
crime.hvplot.<TAB>
|
90 |
+
```
|
91 |
+
|
92 |
+
Plot types available include:
|
93 |
+
|
94 |
+
* <a href="#area">``.area()``</a>: Plots a area chart similar to a line chart except for filling the area under the curve and optionally stacking
|
95 |
+
* <a href="#bars">``.bar()``</a>: Plots a bar chart that can be stacked or grouped
|
96 |
+
* <a href="#bivariate">``.bivariate()``</a>: Plots 2D density of a set of points
|
97 |
+
* <a href="#box-whisker-plots">``.box()``</a>: Plots a box-whisker chart comparing the distribution of one or more variables
|
98 |
+
* <a href="#heatmap">``.heatmap()``</a>: Plots a heatmap to visualizing a variable across two independent dimensions
|
99 |
+
* <a href="#hexbins">``.hexbins()``</a>: Plots hex bins
|
100 |
+
* <a href="#histogram">``.hist()``</a>: Plots the distribution of one or histograms as a set of bins
|
101 |
+
* <a href="#kde-density">``.kde()``</a>: Plots the kernel density estimate of one or more variables.
|
102 |
+
* <a href="#the-plot-method">``.line()``</a>: Plots a line chart (such as for a time series)
|
103 |
+
* <a href="#scatter">``.scatter()``</a>: Plots a scatter chart comparing two variables
|
104 |
+
* <a href="#step">``.step()``</a>: Plots a step chart akin to a line plot
|
105 |
+
* <a href="#tables">``.table()``</a>: Generates a SlickGrid DataTable
|
106 |
+
* <a href="#groupby">``.violin()``</a>: Plots a violin plot comparing the distribution of one or more variables using the kernel density estimate
|
107 |
+
|
108 |
+
#### Area
|
109 |
+
|
110 |
+
Like most other plot types the ``area`` chart supports the three ways of defining a plot outlined above. An area chart is most useful when plotting multiple variables in a stacked chart. This can be achieve by specifying ``x``, ``y``, and ``by`` columns or using the ``columns`` and ``index``/``use_index`` (equivalent to ``x``) options:
|
111 |
+
|
112 |
+
|
113 |
+
```python
|
114 |
+
crime.hvplot.area(x='Year', y=['Robbery', 'Aggravated assault'])
|
115 |
+
```
|
116 |
+
|
117 |
+
We can also explicitly set ``stacked`` to False and define an ``alpha`` value to compare the values directly:
|
118 |
+
|
119 |
+
|
120 |
+
```python
|
121 |
+
crime.hvplot.area(x='Year', y=['Aggravated assault', 'Robbery'], stacked=False, alpha=0.4)
|
122 |
+
```
|
123 |
+
|
124 |
+
Another use for an area plot is to visualize the spread of a value. For instance using the flights dataset we may want to see the spread in mean delay values across carriers. For that purpose we compute the mean delay by day and carrier and then the min/max mean delay for across all carriers. Since the output of ``hvplot`` is just a regular holoviews object, we can use the overlay operator (\*) to place the plots on top of each other.
|
125 |
+
|
126 |
+
|
127 |
+
```python
|
128 |
+
delay_min_max = flights.groupby(['day', 'carrier'])['carrier_delay'].mean().groupby('day').agg({'min': np.min, 'max': np.max})
|
129 |
+
delay_mean = flights.groupby('day')['carrier_delay'].mean()
|
130 |
+
|
131 |
+
delay_min_max.hvplot.area(x='day', y='min', y2='max', alpha=0.2) * delay_mean.hvplot()
|
132 |
+
```
|
133 |
+
|
134 |
+
#### Bars
|
135 |
+
|
136 |
+
In the simplest case we can use ``.hvplot.bar`` to plot ``x`` against ``y``. We'll use ``rot=90`` to rotate the tick labels on the x-axis making the years easier to read:
|
137 |
+
|
138 |
+
|
139 |
+
```python
|
140 |
+
crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90)
|
141 |
+
```
|
142 |
+
|
143 |
+
If we want to compare multiple columns instead we can set ``y`` to a list of columns. Using the ``stacked`` option we can then compare the column values more easily:
|
144 |
+
|
145 |
+
|
146 |
+
```python
|
147 |
+
crime.hvplot.bar(x='Year', y=['Violent crime total', 'Property crime total'],
|
148 |
+
stacked=True, rot=90, width=800, legend='top_left')
|
149 |
+
```
|
150 |
+
|
151 |
+
#### Scatter
|
152 |
+
|
153 |
+
The scatter plot supports many of the same features as the other chart types we have seen so far but can also be colored by another variable using the ``c`` option.
|
154 |
+
|
155 |
+
|
156 |
+
```python
|
157 |
+
crime.hvplot.scatter(x='Violent Crime rate', y='Burglary rate', c='Year')
|
158 |
+
```
|
159 |
+
|
160 |
+
Anytime that color is being used to represent a dimension, the ``cmap`` option can be used to control the colormap that is used to represent that dimension. Additionally, the colorbar can be disabled using ``colorbar=False``.
|
161 |
+
|
162 |
+
#### Step
|
163 |
+
|
164 |
+
A step chart is very similar to a line chart but instead of linearly interpolating between samples the step chart visualizes discrete steps. The point at which to step can be controlled via the ``where`` keyword allowing `'pre'`, `'mid'` (default) and `'post'` values:
|
165 |
+
|
166 |
+
|
167 |
+
```python
|
168 |
+
crime.hvplot.step(x='Year', y=['Robbery', 'Aggravated assault'])
|
169 |
+
```
|
170 |
+
|
171 |
+
#### HexBins
|
172 |
+
|
173 |
+
You can create hexagonal bin plots with the ``hexbin`` method. Hexbin plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually. Since these data are not regularly distributed, we'll use the ``logz`` option to map z-axis (color) to a log scale colorbar.
|
174 |
+
|
175 |
+
|
176 |
+
```python
|
177 |
+
# flights.hvplot.hexbin(x='airtime', y='arrdelay', width=600, height=500, logz=True)
|
178 |
+
```
|
179 |
+
|
180 |
+
<div class="alert alert-warning" role="alert">
|
181 |
+
HexBins plots <a href='https://github.com/holoviz/holoviews/issues/5219'>not yet supported</a> with the Plotly backend.
|
182 |
+
</div>
|
183 |
+
|
184 |
+
#### Bivariate
|
185 |
+
|
186 |
+
You can create a 2D density plot with the ``bivariate`` method. Bivariate plots can be a useful alternative to scatter plots if your data are too dense to plot each point individually.
|
187 |
+
|
188 |
+
|
189 |
+
```python
|
190 |
+
# crime.hvplot.bivariate(x='Violent Crime rate', y='Burglary rate', width=600, height=500)
|
191 |
+
```
|
192 |
+
|
193 |
+
<div class="alert alert-warning" role="alert">
|
194 |
+
Commented as this is <a href='https://github.com/holoviz/holoviews/issues/5220'>not currently supported</a> with the Plotly backend and raises an error.
|
195 |
+
</div>
|
196 |
+
|
197 |
+
#### HeatMap
|
198 |
+
|
199 |
+
A ``HeatMap`` lets us view the relationship between three variables, so we specify the 'x' and 'y' variables and an additional 'C' variable. Additionally we can define a ``reduce_function`` that computes the values for each bin from the samples that fall into it. Here we plot the 'depdelay' (i.e. departure delay) for each day of the month and carrier in the dataset:
|
200 |
+
|
201 |
+
|
202 |
+
```python
|
203 |
+
flights.compute().hvplot.heatmap(x='day', y='carrier', C='depdelay', reduce_function=np.mean, colorbar=True)
|
204 |
+
```
|
205 |
+
|
206 |
+
#### Tables
|
207 |
+
|
208 |
+
Unlike all other plot types, a table only supports one signature: either all columns are plotted, or a subset of columns can be selected by defining the ``columns`` explicitly:
|
209 |
+
|
210 |
+
|
211 |
+
```python
|
212 |
+
crime.hvplot.table(columns=['Year', 'Population', 'Violent Crime rate'], width=400)
|
213 |
+
```
|
214 |
+
|
215 |
+
### Distributions
|
216 |
+
|
217 |
+
Plotting distributions differs slightly from other plots since they plot only one variable in the simple case rather than plotting two or more variables against each other. Therefore when plotting these plot types no ``index`` or ``x`` value needs to be supplied. Instead:
|
218 |
+
|
219 |
+
1. Declare a single ``y`` variable, e.g. ``source.plot.hist(variable)``, or
|
220 |
+
2. Declare a ``y`` variable and ``by`` variable, e.g. ``source.plot.hist(variable, by='Group')``, or
|
221 |
+
3. Declare columns or plot all columns, e.g. ``source.plot.hist()`` or ``source.plot.hist(columns=['A', 'B', 'C'])``
|
222 |
+
|
223 |
+
#### Histogram
|
224 |
+
|
225 |
+
The Histogram is the simplest example of a distribution; often we simply plot the distribution of a single variable, in this case the 'Violent Crime rate'. Additionally we can define a range over which to compute the histogram and the number of bins using the ``bin_range`` and ``bins`` arguments respectively:
|
226 |
+
|
227 |
+
|
228 |
+
```python
|
229 |
+
crime.hvplot.hist(y='Violent Crime rate')
|
230 |
+
```
|
231 |
+
|
232 |
+
Or we can plot the distribution of multiple columns:
|
233 |
+
|
234 |
+
|
235 |
+
```python
|
236 |
+
columns = ['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
237 |
+
crime.hvplot.hist(y=columns, bins=50, alpha=0.5, legend='top', height=400)
|
238 |
+
```
|
239 |
+
|
240 |
+
We can also group the data by another variable. Here we'll use ``subplots`` to split each carrier out into its own plot:
|
241 |
+
|
242 |
+
|
243 |
+
```python
|
244 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
245 |
+
flight_subset.hvplot.hist('depdelay', by='carrier', bins=20, bin_range=(-20, 100), width=300, subplots=True)
|
246 |
+
```
|
247 |
+
|
248 |
+
#### KDE (density)
|
249 |
+
|
250 |
+
You can also create density plots using ``hvplot.kde()`` or ``hvplot.density()``:
|
251 |
+
|
252 |
+
|
253 |
+
```python
|
254 |
+
crime.hvplot.kde(y='Violent Crime rate')
|
255 |
+
```
|
256 |
+
|
257 |
+
Comparing the distribution of multiple columns is also possible:
|
258 |
+
|
259 |
+
|
260 |
+
```python
|
261 |
+
columns=['Violent Crime rate', 'Property crime rate', 'Burglary rate']
|
262 |
+
crime.hvplot.kde(y=columns, alpha=0.5, value_label='Rate', legend='top_right')
|
263 |
+
```
|
264 |
+
|
265 |
+
The ``hvplot.kde`` also supports the ``by`` keyword:
|
266 |
+
|
267 |
+
|
268 |
+
```python
|
269 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
270 |
+
flight_subset.hvplot.kde('depdelay', by='carrier', xlim=(-20, 70), width=300, subplots=True)
|
271 |
+
```
|
272 |
+
|
273 |
+
#### Box-Whisker Plots
|
274 |
+
|
275 |
+
Just like the other distribution-based plot types, the box-whisker plot supports plotting a single column:
|
276 |
+
|
277 |
+
|
278 |
+
```python
|
279 |
+
crime.hvplot.box(y='Violent Crime rate')
|
280 |
+
```
|
281 |
+
|
282 |
+
It also supports multiple columns and the same options as seen previously (``legend``, ``invert``, ``value_label``):
|
283 |
+
|
284 |
+
|
285 |
+
```python
|
286 |
+
columns=['Burglary rate', 'Larceny-theft rate', 'Motor vehicle theft rate',
|
287 |
+
'Property crime rate', 'Violent Crime rate']
|
288 |
+
crime.hvplot.box(y=columns, group_label='Crime', legend=False, value_label='Rate (per 100k)', invert=True)
|
289 |
+
```
|
290 |
+
|
291 |
+
Lastly, it also supports using the ``by`` keyword to split the data into multiple subsets:
|
292 |
+
|
293 |
+
|
294 |
+
```python
|
295 |
+
flight_subset = flights[flights.carrier.isin(['AA', 'US', 'OH'])]
|
296 |
+
flight_subset.hvplot.box('depdelay', by='carrier', ylim=(-10, 70))
|
297 |
+
```
|
298 |
+
|
299 |
+
## Composing Plots
|
300 |
+
|
301 |
+
One of the core strengths of HoloViews is the ease of composing
|
302 |
+
different plots. Individual plots can be composed using the ``*`` and
|
303 |
+
``+`` operators, which overlay and compose plots into layouts
|
304 |
+
respectively. For more information on composing objects, see the
|
305 |
+
HoloViews [User Guide](https://holoviews.org/user_guide/Composing_Elements.html).
|
306 |
+
|
307 |
+
By using these operators we can combine multiple plots into composite plots. A simple example is overlaying two plot types:
|
308 |
+
|
309 |
+
|
310 |
+
```python
|
311 |
+
crime.hvplot(x='Year', y='Violent Crime rate') * crime.hvplot.scatter(x='Year', y='Violent Crime rate', c='k')
|
312 |
+
```
|
313 |
+
|
314 |
+
We can also lay out different plots and tables together:
|
315 |
+
|
316 |
+
|
317 |
+
```python
|
318 |
+
(crime.hvplot.bar(x='Year', y='Violent Crime rate', rot=90, width=550) +
|
319 |
+
crime.hvplot.table(['Year', 'Population', 'Violent Crime rate'], width=420))
|
320 |
+
```
|
321 |
+
|
322 |
+
## Large data
|
323 |
+
|
324 |
+
The previous examples summarized the fairly large airline dataset using statistical plot types that aggregate the data into a feasible subset for plotting. We can instead aggregate the data directly into the viewable image using [datashader](https://datashader.org), which provides a rendering of the entire set of raw data available (as far as the resolution of the screen allows). Here we plot the 'airtime' against the 'distance':
|
325 |
+
|
326 |
+
|
327 |
+
```python
|
328 |
+
flights.hvplot.scatter(x='distance', y='airtime', datashade=True)
|
329 |
+
```
|
330 |
+
|
331 |
+
## Groupby
|
332 |
+
|
333 |
+
Thanks to the ability of HoloViews to explore a parameter space with a set of widgets we can apply a groupby along a particular column or dimension. For example we can view the distribution of departure delays by carrier grouped by day, allowing the user to choose which day to display:
|
334 |
+
|
335 |
+
|
336 |
+
```python
|
337 |
+
flights.hvplot.violin(y='depdelay', by='carrier', groupby='dayofweek', ylim=(-20, 60), height=500)
|
338 |
+
```
|
339 |
+
|
340 |
+
This user guide merely provided an overview over the available plot types; to see a detailed description on how to customize plots see the [Customization](Customization.ipynb) user guide.
|
hvplot_docs/Statistical_Plots.md
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
In addition to the plots available via the plot interface, hvPlot makes a number of more sophisticated, statistical plots available that are modelled on ``pandas.plotting``. To explore these, we will load the iris and stocks datasets from Bokeh:
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import pandas as pd
|
6 |
+
import hvplot.pandas # noqa
|
7 |
+
|
8 |
+
from bokeh.sampledata import iris, stocks
|
9 |
+
|
10 |
+
iris = iris.flowers
|
11 |
+
```
|
12 |
+
|
13 |
+
### Scatter Matrix
|
14 |
+
|
15 |
+
When working with multi-dimensional data, it is often difficult to understand the relationship between all the different variables. A ``scatter_matrix`` makes it possible to visualize all of the pairwise relationships in a compact format. ``hvplot.scatter_matrix`` is closely modelled on ``pandas.plotting.scatter_matrix``:
|
16 |
+
|
17 |
+
|
18 |
+
```python
|
19 |
+
hvplot.scatter_matrix(iris, c="species")
|
20 |
+
```
|
21 |
+
|
22 |
+
Compared to a static Seaborn/Matplotlib-based plot, here it is easy to explore the data interactively thanks to Bokeh's linked zooming, linked panning, and linked brushing (using the ``box_select`` and ``lasso_select`` tools).
|
23 |
+
|
24 |
+
### Parallel Coordinates
|
25 |
+
|
26 |
+
Parallel coordinate plots provide another way of visualizing multi-variate data. ``hvplot.parallel_coordinates`` provides a simple API to create such a plot, modelled on the API of `pandas.plotting.parallel_coordinates()`:
|
27 |
+
|
28 |
+
|
29 |
+
```python
|
30 |
+
hvplot.parallel_coordinates(iris, "species")
|
31 |
+
```
|
32 |
+
|
33 |
+
The plot quickly clarifies the relationship between different variables, highlighting the difference of the "setosa" species in the petal width and length dimensions.
|
34 |
+
|
35 |
+
### Andrews Curves
|
36 |
+
|
37 |
+
|
38 |
+
Another similar approach is to visualize the dimensions using Andrews curves, which are constructed by generating a Fourier series from the features of each observation, visualizing the aggregate differences between classes. The ``hvplot.andrews_curves()`` function provides a simple API to generate Andrews curves from a datafrom, closely matching the API of ``pandas.plotting.andrews_curves()``:
|
39 |
+
|
40 |
+
|
41 |
+
```python
|
42 |
+
hvplot.andrews_curves(iris, "species")
|
43 |
+
```
|
44 |
+
|
45 |
+
Once again we can see the significant difference of the setosa species. However, unlike the parallel coordinate plot, the Andrews plot does not give any real quantitative insight into the features that drive those differences.
|
46 |
+
|
47 |
+
### Lag Plot
|
48 |
+
|
49 |
+
Lastly, for the analysis of time series hvplot offers a so called lag plot, implemented by the ``hvplot.lag_plot()`` function, modelled on the matching ``pandas.plotting.lag_plot()`` function.
|
50 |
+
|
51 |
+
As an example we will compare the closing stock prices of Apple and IBM from 2000-2013 using a lag of 365 days:
|
52 |
+
|
53 |
+
|
54 |
+
```python
|
55 |
+
index = pd.DatetimeIndex(stocks.AAPL['date'])
|
56 |
+
stock_df = pd.DataFrame({'IBM': stocks.IBM['close'], 'AAPL': stocks.AAPL['close']}, index=index)
|
57 |
+
|
58 |
+
hvplot.lag_plot(stock_df, lag=365, alpha=0.3)
|
59 |
+
```
|
60 |
+
|
61 |
+
Using this plot it becomes apparent that Apple was significantly more volatile over the analyzed time scale. In other words, its price at a particular point in time sometimes differed significantly from the price 365 days in the past. This also becomes visible in a simple line chart of the same data:
|
62 |
+
|
63 |
+
|
64 |
+
```python
|
65 |
+
stock_df.hvplot.line()
|
66 |
+
```
|
67 |
+
|
68 |
+
These plot types can help you make sense of complex datasets. See [holoviews.org](https://holoviews.org) for many other plots and tools that can be used alongside those from hvPlot for other purposes.
|
hvplot_docs/Streaming.md
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
hvPlot supports [streamz](https://github.com/mrocklin/streamz) DataFrame and Series objects, automatically generating streaming plots in a Jupyter notebook or deployed as a [Bokeh Server app](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html).
|
2 |
+
|
3 |
+
All hvPlot methods on streamz objects return HoloViews `DynamicMap` objects that update the plot whenever `streamz` triggers an event. For more information on `DynamicMap` and HoloViews dynamic plotting support, see the [HoloViews User Guide](https://holoviews.org/user_guide); here we will focus on using the simple, high-level hvPlot API rather than on the details of how events and data flow behind the scenes.
|
4 |
+
|
5 |
+
**All plots generated by the streamz plotting interface dynamically stream data from Python into the web browser. The web version for this page includes *screen captures* of the streaming visualizations, not live streaming data.**
|
6 |
+
|
7 |
+
As for any of the data backends, we start by patching the streamz library with the plotting API:
|
8 |
+
|
9 |
+
|
10 |
+
```python
|
11 |
+
import hvplot.streamz # noqa
|
12 |
+
```
|
13 |
+
|
14 |
+
## Basic plotting
|
15 |
+
|
16 |
+
Throughout this section we will be using the ``Random`` object from streamz, which provides an easy way of generating a DataFrame of random streaming data but which could be substituted with any streamz ``DataFrame`` or ``Series`` driven by a live, external data source instead. To stop the ``Random`` stream you can call ``df.stop()`` at any point.
|
17 |
+
|
18 |
+
|
19 |
+
```python
|
20 |
+
from streamz.dataframe import Random
|
21 |
+
df = Random(interval='200ms', freq='50ms')
|
22 |
+
```
|
23 |
+
|
24 |
+
<img src='https://assets.holoviews.org/hvplot/gifs/df_streamz.gif' width=400px></img>
|
25 |
+
|
26 |
+
The plot method on Series and DataFrame is a simple wrapper around a
|
27 |
+
line plot, which will plot all columns:
|
28 |
+
|
29 |
+
|
30 |
+
```python
|
31 |
+
df.hvplot()
|
32 |
+
```
|
33 |
+
|
34 |
+
<center><img src='https://assets.holoviews.org/hvplot/gifs/df_plot.gif' width=700px></img></center>
|
35 |
+
|
36 |
+
The plot method can also be called on a Series, plotting a specific column:
|
37 |
+
|
38 |
+
|
39 |
+
```python
|
40 |
+
df.z.cumsum().hvplot()
|
41 |
+
```
|
42 |
+
|
43 |
+
<center><img src='https://assets.holoviews.org/hvplot/gifs/df_curve.gif' width=700px></img></center>
|
44 |
+
|
45 |
+
Actually, *all* the functionality described in the [Plotting](Plotting.ipynb) user guide should work just the same as it does for a regular (non-streaming) dask or pandas DataFrame or Series, but will now update as new data appears.
|
46 |
+
|
47 |
+
## Controlling the backlog
|
48 |
+
|
49 |
+
The main difference when using a streaming DataFrame is that a only a certain amount of data will be buffered and displayed. The number of rows of data that will be buffered can be controlled by the ``backlog`` parameter. Here, let's buffer and display 10 rows of our streaming DataFrame as a table:
|
50 |
+
|
51 |
+
|
52 |
+
```python
|
53 |
+
df.hvplot.table(width=400, backlog=10)
|
54 |
+
```
|
55 |
+
|
56 |
+
<center><img src='https://assets.holoviews.org/hvplot/gifs/df_table.gif' width=400px></img></center>
|
57 |
+
|
58 |
+
This of course only has an effect when we are directly streaming data point by point. When we have an aggregate DataFrame, the plot will continuously accumulate updates:
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
df.groupby('y').sum().hvplot.bar(x='y')
|
63 |
+
```
|
64 |
+
|
65 |
+
<center><img src='https://assets.holoviews.org/hvplot/gifs/df_bars.gif' width=700px></img></center>
|
66 |
+
|
67 |
+
## Composing Plots
|
68 |
+
|
69 |
+
hvPlot is a convenient API for generating HoloViews objects. One of the core strengths of HoloViews objects is the ease with which they can be composed, which works with streaming plots just as with static ones. Individual plots can be composed using the ``*`` and ``+`` operators, which overlay and compose plots into layouts respectively. For more information on composing objects see the HoloViews [User Guide](https://holoviews.org/user_guide/Composing_Elements.html).
|
70 |
+
|
71 |
+
By using these operators we can combine multiple plots into composite Overlay and Layout objects, and lay them out in two columns using the ``Layout.cols`` method:
|
72 |
+
|
73 |
+
|
74 |
+
```python
|
75 |
+
(df.hvplot.line(width=400, backlog=100) * df.hvplot.scatter(width=400, backlog=100) +
|
76 |
+
df.groupby('y').sum().hvplot.bar('y', 'x', width=400) +
|
77 |
+
df.hvplot.box(width=400) + df.x.hvplot.kde(width=400, shared_axes=False)).cols(2)
|
78 |
+
```
|
79 |
+
|
80 |
+
<center><img src='https://assets.holoviews.org/hvplot/gifs/df_composite.gif' width=700px></img></center>
|
81 |
+
|
82 |
+
## Deployment as Bokeh apps
|
83 |
+
|
84 |
+
HoloViews objects automatically render themselves in Jupyter notebook cells, but when deploying a bokeh app the plot has to be rendered explicitly. Deploying as a Bokeh Server app allows you to share live, dynamically updated visualizations like those for streaming data, backed by a running Python process.
|
85 |
+
|
86 |
+
The following example describes how to set up a streaming DataFrame, declare some plots, compose them, set up a callback to update the plot and finally convert the composite plot to a bokeh Document, which can be served from a script using ``bokeh serve`` on the commandline.
|
87 |
+
|
88 |
+
```python
|
89 |
+
|
90 |
+
import numpy as np
|
91 |
+
import pandas as pd
|
92 |
+
import hvplot.streamz
|
93 |
+
import holoviews as hv
|
94 |
+
|
95 |
+
from streamz import Stream
|
96 |
+
from streamz.dataframe import DataFrame
|
97 |
+
|
98 |
+
renderer = hv.renderer('bokeh')
|
99 |
+
|
100 |
+
# Set up streaming DataFrame
|
101 |
+
stream = Stream()
|
102 |
+
index = pd.DatetimeIndex([])
|
103 |
+
example = pd.DataFrame({'x': [], 'y': [], 'z': []},
|
104 |
+
columns=['x', 'y', 'z'], index=[])
|
105 |
+
df = DataFrame(stream, example=example)
|
106 |
+
cumulative = df.cumsum()[['x', 'z']]
|
107 |
+
|
108 |
+
# Declare plots
|
109 |
+
line = cumulative.hvplot.line(width=400)
|
110 |
+
scatter = cumulative.hvplot.scatter(width=400)
|
111 |
+
bars = df.groupby('y').sum().hvplot.bar(width=400)
|
112 |
+
box = df.hvplot.box(width=400)
|
113 |
+
kde = df.x.hvplot.kde(width=400)
|
114 |
+
|
115 |
+
# Compose plots
|
116 |
+
layout = (line * scatter + bars + box + kde).cols(2)
|
117 |
+
|
118 |
+
# Set up callback with streaming data
|
119 |
+
def emit():
|
120 |
+
now = pd.datetime.now()
|
121 |
+
delta = np.timedelta64(500, 'ms')
|
122 |
+
index = pd.date_range(np.datetime64(now)-delta, now, freq='100ms')
|
123 |
+
df = pd.DataFrame({'x': np.random.randn(len(index)),
|
124 |
+
'y': np.random.randint(0, 10, len(index)),
|
125 |
+
'z': np.random.randn(len(index))},
|
126 |
+
columns=['x', 'y', 'z'], index=index)
|
127 |
+
stream.emit(df)
|
128 |
+
|
129 |
+
# Render layout to bokeh server Document and attach callback
|
130 |
+
doc = renderer.server_doc(layout)
|
131 |
+
doc.title = 'Streamz HoloViews based Plotting API Bokeh App Demo'
|
132 |
+
doc.add_periodic_callback(emit, 500)
|
133 |
+
```
|
134 |
+
|
135 |
+
For more details on deploying Bokeh apps see the HoloViews [User Guide](https://holoviews.org/user_guide/Deploying_Bokeh_Apps.html).
|
136 |
+
|
137 |
+
## Using HoloViews directly
|
138 |
+
|
139 |
+
HoloViews itself includes first class support for streamz DataFrame and Series; for more details see the [Streaming Data section](https://holoviews.org/user_guide/Streaming_Data.html) in the HoloViews documentation.
|
hvplot_docs/Subplots.md
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
## Subplots
|
2 |
+
|
3 |
+
When plotting multiple columns, hvPlot will overlay the plots onto one axis by default so that they can be compared easily in a compact format:
|
4 |
+
|
5 |
+
|
6 |
+
```python
|
7 |
+
import xarray as xr
|
8 |
+
import hvplot.pandas # noqa
|
9 |
+
import hvplot.xarray # noqa
|
10 |
+
|
11 |
+
from hvplot.sample_data import airline_flights, us_crime
|
12 |
+
|
13 |
+
us_crime.hvplot(x='Year', y=['Burglary rate', 'Violent Crime rate', 'Robbery rate'], value_label='Rate')
|
14 |
+
```
|
15 |
+
|
16 |
+
If you wish, you can instead set `subplots=True` to split each column into its own separate plot:
|
17 |
+
|
18 |
+
|
19 |
+
```python
|
20 |
+
us_crime.hvplot(x='Year', y=['Burglary rate', 'Violent Crime rate', 'Robbery rate'],
|
21 |
+
value_label='Rate', subplots=True, width=300, height=200)
|
22 |
+
```
|
23 |
+
|
24 |
+
By default, the subplots will have linked, normalized axes, to facilitate comparing the numerical values across plots (try panning or zooming in any one of the plots, to see how the others change.)
|
25 |
+
|
26 |
+
However, if the data covers widely different numerical ranges, you can specify `shared_axes=False` to give each plot its own range:
|
27 |
+
|
28 |
+
|
29 |
+
```python
|
30 |
+
us_crime.hvplot(x='Year', y=['Robbery', 'Robbery rate', 'Burglary', 'Burglary rate'],
|
31 |
+
width=350, height=300, subplots=True, shared_axes=False).cols(2)
|
32 |
+
```
|
33 |
+
|
34 |
+
(Notice the very different y axis ranges between the plots.) Here we also specified `.cols(2)` to allow up to two plots per line, wrapping the rest onto subsequent rows.
|
35 |
+
|
36 |
+
You can use the `subplots=True` (and `shared_axes` if desired) arguments when using the ``by`` keyword as well, if you want to group the data along a dimension:
|
37 |
+
|
38 |
+
|
39 |
+
```python
|
40 |
+
flights = airline_flights.read()
|
41 |
+
|
42 |
+
flight_subset = flights[flights.carrier.isin(['OH', 'F9', 'US'])].sample(2000)
|
43 |
+
|
44 |
+
flight_subset.hvplot.scatter(x='arrdelay', y='depdelay', by='carrier',
|
45 |
+
subplots=True, width=250, height=250, alpha=0.1)
|
46 |
+
```
|
47 |
+
|
48 |
+
## Grids
|
49 |
+
|
50 |
+
|
51 |
+
`subplot=True` lays out plots sequentially, breaking lines if requested with the `.cols()` method but otherwise formatting each plot independently. You can instead arrange multidimensional data into an explicit 1D row of plots or a 2D grid of plots with shared axes, to allow easier comparisons across large numbers of plots. To make a row or grid plot, just specify the ``col`` keyword and add a ``row`` keyword if you want a grid:
|
52 |
+
|
53 |
+
|
54 |
+
```python
|
55 |
+
flight_subset.sort_values('dayofweek').hvplot.scatter(x='arrdelay', y='depdelay',
|
56 |
+
row='dayofweek', col='carrier', alpha=0.2)
|
57 |
+
```
|
58 |
+
|
59 |
+
(Just declaring `row` to get a single column is not currently supported.) Here you can see that compared to the subplot versions above, the axis ticks and labels are shared to save space and make comparisons easier.
|
60 |
+
|
61 |
+
If you do not require an x axis and y axis for each plot at all, you can disable it with the ``xaxis`` and ``yaxis`` options:
|
62 |
+
|
63 |
+
|
64 |
+
```python
|
65 |
+
air_ds = xr.tutorial.open_dataset('air_temperature').load()
|
66 |
+
|
67 |
+
air_ds.air.isel(time=slice(0, 5)).hvplot(col='time', xaxis=False, yaxis=False, colorbar=False)
|
68 |
+
```
|
69 |
+
|
70 |
+
Using subplots and grids in this way is supported throughout the hvPlot API, making it simple to determine how you want your data laid out and overlaid.
|
hvplot_docs/Timeseries_Data.md
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Almost all the other sections in the user guide mention timeseries. This section demonstrates the special functionality that hvPlot provides specifically for dealing with time.
|
2 |
+
|
3 |
+
|
4 |
+
```python
|
5 |
+
import hvplot.pandas # noqa
|
6 |
+
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature as sst
|
7 |
+
|
8 |
+
sst.hvplot()
|
9 |
+
```
|
10 |
+
|
11 |
+
By default, the index will be used as the x-axis when plotting tabular data. If the index is composed of datetimes, and they are not in chronological order, hvPlot will try to sort them before plotting (unless you set ``sort_date=False``).
|
12 |
+
|
13 |
+
|
14 |
+
```python
|
15 |
+
scrampled = sst.sample(frac=1)
|
16 |
+
scrampled.hvplot()
|
17 |
+
```
|
18 |
+
|
19 |
+
### Tickers
|
20 |
+
|
21 |
+
The datetime tickers will be set to a default that is meant to fit in the allotted space. If you'd rather use a different format, then you can declare an explicit date ticker according to the rules on [Bokeh DatetimeTickFormatter](https://bokeh.pydata.org/en/latest/docs/reference/models/formatters.html#bokeh.models.formatters.DatetimeTickFormatter).
|
22 |
+
|
23 |
+
|
24 |
+
```python
|
25 |
+
from bokeh.models.formatters import DatetimeTickFormatter
|
26 |
+
|
27 |
+
formatter = DatetimeTickFormatter(months='%b %Y')
|
28 |
+
sst.hvplot(xformatter=formatter)
|
29 |
+
```
|
30 |
+
|
31 |
+
### Auto-range
|
32 |
+
|
33 |
+
*(Available with HoloViews >= 1.16)*
|
34 |
+
|
35 |
+
Automatic ranging, aka auto-ranging, on the data in x or y is supported, making it easy to scale the given axes and fit the entire visible curve after a zoom or pan. Try zooming in on the plot and panning around, the y range nicely adapt to fit the curve.
|
36 |
+
|
37 |
+
|
38 |
+
```python
|
39 |
+
sst.hvplot(autorange="y")
|
40 |
+
```
|
41 |
+
|
42 |
+
### Pandas datetime features
|
43 |
+
|
44 |
+
hvPlot takes advantage of datetime features to make it trivial to produce plots that are aggregated on some feature of the date. For instance in the case of temperature data, it might be interesting to examine the monthly temperature distribution. We can easily do that by setting ``by='index.month'``.
|
45 |
+
|
46 |
+
|
47 |
+
```python
|
48 |
+
sst.hvplot.violin(by='index.month')
|
49 |
+
```
|
50 |
+
|
51 |
+
We can also use these datetime features as the ``x`` and ``y``. Here we'll look at the mean temperature at each hour of the day for each month in our dataset.
|
52 |
+
|
53 |
+
|
54 |
+
```python
|
55 |
+
sst.hvplot.heatmap(x='index.hour', y='index.month', C='temperature', cmap='reds')
|
56 |
+
```
|
57 |
+
|
58 |
+
Combining this with the information from the [section on widgets](Widgets.ipynb), we can even use the datetime features to produce a plot that steps through each month in the data.
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
sst.hvplot(groupby=['index.year', 'index.month'], widget_type='scrubber', widget_location='bottom')
|
63 |
+
```
|
64 |
+
|
65 |
+
### Xarray datetime features
|
66 |
+
|
67 |
+
The same datetime features can be used with xarray data as well, although for now, the functionality is only supported for non-gridded output.
|
68 |
+
|
69 |
+
|
70 |
+
```python
|
71 |
+
import xarray as xr
|
72 |
+
import hvplot.xarray # noqa
|
73 |
+
|
74 |
+
air_ds = xr.tutorial.open_dataset('air_temperature').load()
|
75 |
+
air_ds
|
76 |
+
```
|
77 |
+
|
78 |
+
Similar to how we did for sea surface temperature above, we can get the distribution of air temperature by month.
|
79 |
+
|
80 |
+
|
81 |
+
```python
|
82 |
+
air_ds.hvplot.violin(y='air', by='time.month')
|
83 |
+
```
|
84 |
+
|
85 |
+
Once we reduce the dimensionality (by taking the mean over 'lat' and 'lon'), we can groupby various datetime features.
|
86 |
+
|
87 |
+
|
88 |
+
```python
|
89 |
+
air_ds.mean(dim=['lat', 'lon']).hvplot(by='time.hour', groupby=['time.year', 'time.month'])
|
90 |
+
```
|
91 |
+
|
92 |
+
Note that xarray supports grouping and aggregation using a similar syntax. To learn more about timeseries in xarray, see the [xarray timeseries docs](https://xarray.pydata.org/en/stable/time-series.html).
|
93 |
+
|
94 |
+
### Downsample time series
|
95 |
+
|
96 |
+
*(Available with HoloViews >= 1.16)*
|
97 |
+
|
98 |
+
An option when working with large time series is to downsample the data before plotting it. This can be done with `downsample=True`, which applies the `lttb` (Largest Triangle Three Buckets) algorithm to the data.
|
99 |
+
|
100 |
+
|
101 |
+
```python
|
102 |
+
sst.hvplot(label="original") * sst.hvplot(downsample=True, label="downsampled")
|
103 |
+
```
|
hvplot_docs/Viewing.md
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
hvPlot is written to work well inside a Jupyter notebook, from the interactive Python command prompt, or inside a Python batch script. In this user guide we will discover how to use hvPlot to view plots in each of these cases and how to save the plots to a separate file.
|
2 |
+
|
3 |
+
## Using hvPlot in different contexts
|
4 |
+
|
5 |
+
### Notebook
|
6 |
+
|
7 |
+
In a Jupyter notebook, hvPlot will return HoloViews objects that display themselves, as long as the plotting library has been loaded. The easiest way of loading the plotting library is to import one of the plotting accessors such as pandas' one with `hvplot.pandas`:
|
8 |
+
|
9 |
+
|
10 |
+
```python
|
11 |
+
import hvplot.pandas # noqa
|
12 |
+
```
|
13 |
+
|
14 |
+
The above import statement triggers the loading of the default plotting library, i.e. Bokeh.
|
15 |
+
|
16 |
+
Here we will load some of the sample data and then compose the HoloViews objects into a layout:
|
17 |
+
|
18 |
+
|
19 |
+
```python
|
20 |
+
from hvplot.sample_data import airline_flights, us_crime
|
21 |
+
|
22 |
+
violent_crime = us_crime.hvplot(x='Year', y='Violent Crime rate', width=400)
|
23 |
+
burglaries = us_crime.hvplot(x='Year', y='Burglary rate', width=400)
|
24 |
+
|
25 |
+
violent_crime + burglaries
|
26 |
+
```
|
27 |
+
|
28 |
+
### Using Panel
|
29 |
+
|
30 |
+
To display the object from inside a function it's recommended that you use [Panel](https://panel.holoviz.org). Panel can also be used to create more complicated layouts, to add additional interactivity, and to deploy servers.
|
31 |
+
|
32 |
+
|
33 |
+
```python
|
34 |
+
import panel as pn
|
35 |
+
|
36 |
+
pane = pn.panel(violent_crime)
|
37 |
+
pane
|
38 |
+
```
|
39 |
+
|
40 |
+
This ``pane`` can be updated with another HoloViews object replacing the plot:
|
41 |
+
|
42 |
+
|
43 |
+
```python
|
44 |
+
pane.object = burglaries
|
45 |
+
```
|
46 |
+
|
47 |
+
Or the object can be updated inplace:
|
48 |
+
|
49 |
+
|
50 |
+
```python
|
51 |
+
pane.object *= violent_crime
|
52 |
+
```
|
53 |
+
|
54 |
+
### Python Command Prompt & Scripts
|
55 |
+
|
56 |
+
When working outside the notebook we can instead use the ``hvplot.show`` function, which will open the plot in a new browser window:
|
57 |
+
|
58 |
+
<img src="../assets/console.png" style="display: table; margin: 0 auto;"></img>
|
59 |
+
|
60 |
+
For static plots this will simply save a temporary file and open it, however for dynamic and [datashaded](https://datashader.org) plots it will automatically launch a Bokeh server, enabling all the dynamic features.
|
61 |
+
|
62 |
+
<img src="../assets/console_server.gif" style="display: table; margin: 0 auto;"></img>
|
63 |
+
|
64 |
+
## Saving plots
|
65 |
+
|
66 |
+
When looking at any Bokeh plot in a web browser, you can use the toolbar's "Save" tool to export the plot as a PNG (try it on one of the plots above!).
|
67 |
+
|
68 |
+
hvPlot also provides a convenient ``save`` function to export HoloViews objects to a file. By default it will save the plot as HTML:
|
69 |
+
|
70 |
+
|
71 |
+
```python
|
72 |
+
plot = airline_flights.hvplot.hexbin(x='airtime', y='arrdelay', colorbar=True, width=600, height=500, logz=True)
|
73 |
+
|
74 |
+
hvplot.save(plot, 'test.html')
|
75 |
+
```
|
76 |
+
|
77 |
+
By default, the HTML file generated will depend on loading JavaScript code for BokehJS from the online CDN repository, to reduce the file size. If you need to work in an airgapped or no-network environment, you can declare that `INLINE` resources should be used instead of `CDN`:
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
from bokeh.resources import INLINE
|
82 |
+
hvplot.save(plot, 'test.html', resources=INLINE)
|
83 |
+
```
|
84 |
+
|
85 |
+
Finally, if a 'png' file extension is specified, the exported plot will be rendered as a PNG, which currently requires Selenium and PhantomJS to be installed:
|
86 |
+
|
87 |
+
|
88 |
+
```python
|
89 |
+
hvplot.save(plot, 'test.png')
|
90 |
+
```
|
hvplot_docs/Widgets.md
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
As we have seen in previous sections, hvPlot bakes in interactivity by automatically creating widgets when using ``groupby``. These widgets can be refined using [Panel](https://panel.holoviz.org). Panel allows you to customize the interactivity of your hvPlot output and provides more fine-grained control over the layout.
|
2 |
+
|
3 |
+
<div class="alert alert-warning" role="alert">
|
4 |
+
When viewing on a static website, the widgets will be inoperable. To explore this functionality fully, download the notebook and run it!
|
5 |
+
</div>
|
6 |
+
|
7 |
+
|
8 |
+
```python
|
9 |
+
import panel as pn
|
10 |
+
import hvplot.pandas # noqa
|
11 |
+
|
12 |
+
from bokeh.sampledata.iris import flowers
|
13 |
+
|
14 |
+
```
|
15 |
+
|
16 |
+
When ``groupby`` is used, the default widget is selected for the data type of the column. In this case since 'species' is composed of strings, the widget an instance of the class ``pn.widgets.Select``.
|
17 |
+
|
18 |
+
|
19 |
+
```python
|
20 |
+
flowers.hvplot.bivariate(x='sepal_width', y='sepal_length', width=600,
|
21 |
+
groupby='species')
|
22 |
+
```
|
23 |
+
|
24 |
+
### Customizing Widgets
|
25 |
+
|
26 |
+
We can change where the widget is shown using the ``widget_location`` option.
|
27 |
+
|
28 |
+
|
29 |
+
```python
|
30 |
+
flowers.hvplot.bivariate(x='sepal_width', y='sepal_length', width=600,
|
31 |
+
groupby='species', widget_location='left_top')
|
32 |
+
```
|
33 |
+
|
34 |
+
We can also change what the class of the widget is, using the ``widgets`` dict. For instance if we want to use a slider instead of a selector we can specify that.
|
35 |
+
|
36 |
+
|
37 |
+
```python
|
38 |
+
flowers.hvplot.bivariate(x='sepal_width', y='sepal_length', width=600,
|
39 |
+
groupby='species', widgets={'species': pn.widgets.DiscreteSlider})
|
40 |
+
```
|
41 |
+
|
42 |
+
### Using widgets as arguments
|
43 |
+
|
44 |
+
So far we have only been dealing with widgets that are produced when using the ``groupby`` key word. But panel provides many other ways of expanding the interactivity of hvplot objects. For instance we might want to allow the user to select which fields to plot on the ``x`` and ``y`` axes. Or even what ``kind`` of plot to produce.
|
45 |
+
|
46 |
+
|
47 |
+
```python
|
48 |
+
x = pn.widgets.Select(name='x', options=['sepal_width', 'petal_width'])
|
49 |
+
y = pn.widgets.Select(name='y', options=['sepal_length', 'petal_length'])
|
50 |
+
kind = pn.widgets.Select(name='kind', value='scatter', options=['bivariate', 'scatter'])
|
51 |
+
|
52 |
+
plot = flowers.hvplot(x=x, y=y, kind=kind, colorbar=False, width=600)
|
53 |
+
pn.Row(pn.WidgetBox(x, y, kind), plot)
|
54 |
+
```
|
55 |
+
|
56 |
+
### Using functions
|
57 |
+
|
58 |
+
In addition to using widgets directly as arguments, we can also use functions that have been decorated with ``pn.depends``
|
59 |
+
|
60 |
+
|
61 |
+
```python
|
62 |
+
x = pn.widgets.Select(name='x', options=['sepal_width', 'petal_width'])
|
63 |
+
y = pn.widgets.Select(name='y', options=['sepal_length', 'petal_length'])
|
64 |
+
kind = pn.widgets.Select(name='kind', value='scatter', options=['bivariate', 'scatter'])
|
65 |
+
by_species = pn.widgets.Checkbox(name='By species')
|
66 |
+
color = pn.widgets.ColorPicker(value='#ff0000')
|
67 |
+
|
68 |
+
@pn.depends(by_species, color)
|
69 |
+
def by_species_fn(by_species, color):
|
70 |
+
return 'species' if by_species else color
|
71 |
+
|
72 |
+
plot = flowers.hvplot(x=x, y=y, kind=kind, c=by_species_fn, colorbar=False, width=600, legend='top_right')
|
73 |
+
|
74 |
+
pn.Row(pn.WidgetBox(x, y, kind, color, by_species), plot)
|
75 |
+
```
|
76 |
+
|
77 |
+
We can keep even add a callback to disable the color options when 'bivariate' is selected. After running the cell below, try changing 'kind' above and notice how the color and 'By species' areas turn grey to indicate that they are disabled.
|
78 |
+
|
79 |
+
|
80 |
+
```python
|
81 |
+
def update(event):
|
82 |
+
if kind.value == 'bivariate':
|
83 |
+
color.disabled = True
|
84 |
+
by_species.disabled = True
|
85 |
+
else:
|
86 |
+
color.disabled = False
|
87 |
+
by_species.disabled = False
|
88 |
+
|
89 |
+
kind.param.watch(update, 'value');
|
90 |
+
```
|
91 |
+
|
92 |
+
To learn more about Panel and how to use it with output from hvPlot, see the [Panel docs on the HoloViews pane](https://panel.pyviz.org/reference/panes/HoloViews.html). To learn more about available widgets, see the Widgets' section of the [Panel Reference Gallery](https://panel.pyviz.org/reference/index.html).
|
hvplot_docs/plot.html
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en" >
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8">
|
5 |
+
<title>plot</title>
|
6 |
+
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.holoviz.org/panel/1.3.6/dist/images/apple-touch-icon.png">
|
7 |
+
<link rel="icon" type="image/png" sizes="32x32" href="https://cdn.holoviz.org/panel/1.3.6/dist/images/favicon.ico"> <style>
|
8 |
+
html, body {
|
9 |
+
display: flow-root;
|
10 |
+
box-sizing: border-box;
|
11 |
+
height: 100%;
|
12 |
+
margin: 0;
|
13 |
+
padding: 0;
|
14 |
+
}
|
15 |
+
</style>
|
16 |
+
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.3.2.min.js"></script>
|
17 |
+
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.2.min.js"></script>
|
18 |
+
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.2.min.js"></script>
|
19 |
+
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.2.min.js"></script>
|
20 |
+
<script type="text/javascript" src="https://cdn.holoviz.org/panel/1.3.6/dist/panel.min.js"></script>
|
21 |
+
|
22 |
+
<script type="text/javascript">
|
23 |
+
Bokeh.set_log_level("info");
|
24 |
+
</script> </head>
|
25 |
+
<body>
|
26 |
+
<div id="bc2d23bf-3023-4b2e-9825-bb0daf9fdc45" data-root-id="p1579" style="display: contents;"></div>
|
27 |
+
|
28 |
+
<script type="application/json" id="p1678">
|
29 |
+
{"8d93a4b1-0241-4312-97a2-9d7e8cf602d5":{"version":"3.3.2","title":"Bokeh Application","roots":[{"type":"object","name":"Row","id":"p1579","attributes":{"name":"Row01837","tags":["embedded"],"stylesheets":["\n:host(.pn-loading.pn-arc):before, .pn-loading.pn-arc:before {\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IG5vbmU7IGRpc3BsYXk6IGJsb2NrOyBzaGFwZS1yZW5kZXJpbmc6IGF1dG87IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjYzNjM2MzIiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij4gICAgPGFuaW1hdGVUcmFuc2Zvcm0gYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIiB0eXBlPSJyb3RhdGUiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBkdXI9IjFzIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBrZXlUaW1lcz0iMDsxIj48L2FuaW1hdGVUcmFuc2Zvcm0+ICA8L2NpcmNsZT48L3N2Zz4=\");\n background-size: auto calc(min(50%, 400px));\n}",{"type":"object","name":"ImportedStyleSheet","id":"p1669","attributes":{"url":"https://cdn.holoviz.org/panel/1.3.6/dist/css/loading.css"}},{"type":"object","name":"ImportedStyleSheet","id":"p1670","attributes":{"url":"https://cdn.holoviz.org/panel/1.3.6/dist/css/listpanel.css"}},{"type":"object","name":"ImportedStyleSheet","id":"p1667","attributes":{"url":"https://cdn.holoviz.org/panel/1.3.6/dist/bundled/theme/default.css"}},{"type":"object","name":"ImportedStyleSheet","id":"p1668","attributes":{"url":"https://cdn.holoviz.org/panel/1.3.6/dist/bundled/theme/native.css"}}],"min_height":400,"margin":0,"sizing_mode":"stretch_both","align":"start","children":[{"type":"object","name":"Figure","id":"p1593","attributes":{"width":null,"height":null,"min_height":400,"margin":[5,10],"sizing_mode":"stretch_both","align":"start","x_range":{"type":"object","name":"Range1d","id":"p1580","attributes":{"tags":[[["bill_length_mm","bill_length_mm",null]],[]],"start":29.35,"end":62.35,"reset_start":29.35,"reset_end":62.35}},"y_range":{"type":"object","name":"Range1d","id":"p1581","attributes":{"tags":[[["bill_depth_mm","bill_depth_mm",null]],{"type":"map","entries":[["invert_yaxis",false],["autorange",false]]}],"start":12.26,"end":22.34,"reset_start":12.26,"reset_end":22.34}},"x_scale":{"type":"object","name":"LinearScale","id":"p1603"},"y_scale":{"type":"object","name":"LinearScale","id":"p1604"},"title":{"type":"object","name":"Title","id":"p1596","attributes":{"text":"Penguins Scatter","text_color":"black","text_font_size":"12pt"}},"renderers":[{"type":"object","name":"GlyphRenderer","id":"p1633","attributes":{"name":"Adelie","data_source":{"type":"object","name":"ColumnDataSource","id":"p1624","attributes":{"selected":{"type":"object","name":"Selection","id":"p1625","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1626"},"data":{"type":"map","entries":[["bill_length_mm",{"type":"ndarray","array":{"type":"bytes","data":"zczMzMyMQ0AAAAAAAMBDQGZmZmZmJkRAAAAAAAAA+H+amZmZmVlCQGZmZmZmpkNAMzMzMzNzQ0CamZmZmZlDQM3MzMzMDEFAAAAAAAAARUBmZmZmZuZCQGZmZmZm5kJAzczMzMyMREDNzMzMzExDQM3MzMzMTEFAzczMzMxMQkCamZmZmVlDQAAAAAAAQEVAMzMzMzMzQUAAAAAAAABHQGZmZmZm5kJAmpmZmZnZQkAzMzMzM/NBQJqZmZmZGUNAZmZmZmZmQ0BmZmZmZqZBQM3MzMzMTERAAAAAAABAREAzMzMzM/NCQAAAAAAAQERAAAAAAADAQ0CamZmZmZlCQAAAAAAAwENAMzMzMzNzREAzMzMzMzNCQJqZmZmZmUNAZmZmZmZmQ0CamZmZmRlFQM3MzMzMzEJAZmZmZmbmQ0AAAAAAAEBCQGZmZmZmZkRAAAAAAAAAQkDNzMzMzAxGQAAAAAAAgEJAzczMzMzMQ0DNzMzMzIxEQAAAAAAAwEJAAAAAAAAAQkBmZmZmZiZFQM3MzMzMzENAzczMzMwMREAAAAAAAIBBQAAAAAAAAEVAAAAAAABAQUAzMzMzM7NEQAAAAAAAgENAzczMzMxMREAAAAAAAEBCQM3MzMzMzEJAmpmZmZnZQUBmZmZmZqZEQM3MzMzMzEJAzczMzMyMREAzMzMzMzNCQM3MzMzMzERAAAAAAADAQUDNzMzMzIxEQDMzMzMz80FAZmZmZmbmREAAAAAAAMBAQJqZmZmZ2UNAzczMzMzMQ0BmZmZmZuZGQAAAAAAAwEFAZmZmZmZmRUAzMzMzM3NEQJqZmZmZmUJAmpmZmZkZQkDNzMzMzAxFQM3MzMzMTEFAMzMzMzNzRUCamZmZmVlCQM3MzMzMjEFAZmZmZmamQkBmZmZmZqZEQGZmZmZmJkJAMzMzMzNzQkBmZmZmZiZDQDMzMzMzc0NAmpmZmZnZQUDNzMzMzIxEQAAAAAAAAEFAzczMzMzMQ0CamZmZmRlCQGZmZmZmZkRAzczMzMwMQ0BmZmZmZiZEQM3MzMzMjEBAmpmZmZmZRUAAAAAAAIBBQAAAAAAAgERAmpmZmZnZQkBmZmZmZuZCQDMzMzMz80JAmpmZmZnZQ0DNzMzMzExDQJqZmZmZGUNAzczMzMwMQ0CamZmZmZlFQM3MzMzMDENAzczMzMzMRkCamZmZmdlDQJqZmZmZGUVAzczMzMzMQ0CamZmZmVlFQM3MzMzMTENAZmZmZmamQkCamZmZmdlBQM3MzMzMjERAmpmZmZkZQkCamZmZmdlCQJqZmZmZGURAMzMzMzOzRECamZmZmZlBQM3MzMzMTERAZmZmZmZmQ0AAAAAAAMBEQAAAAAAAgENAzczMzMwMRkAAAAAAAEBDQM3MzMzMjEVAZmZmZmZmQkAAAAAAAMBCQM3MzMzMDENAzczMzMyMREDNzMzMzMxBQJqZmZmZGURAAAAAAACAQkCamZmZmdlDQJqZmZmZGURAzczMzMxMREDNzMzMzAxAQJqZmZmZWURAZmZmZmamQkAAAAAAAIBDQJqZmZmZmUNAzczMzMxMQkAAAAAAAABCQGZmZmZm5kJAAAAAAAAAQkAAAAAAAMBEQA=="},"shape":[152],"dtype":"float64","order":"little"}],["bill_depth_mm",{"type":"ndarray","array":{"type":"bytes","data":"MzMzMzOzMkBmZmZmZmYxQAAAAAAAADJAAAAAAAAA+H/NzMzMzEwzQJqZmZmZmTRAzczMzMzMMUCamZmZmZkzQJqZmZmZGTJAMzMzMzMzNECamZmZmRkxQM3MzMzMTDFAmpmZmZmZMUAzMzMzMzM1QJqZmZmZGTVAzczMzMzMMUAAAAAAAAAzQDMzMzMzszRAZmZmZmZmMkAAAAAAAIA1QM3MzMzMTDJAMzMzMzOzMkAzMzMzMzMzQJqZmZmZGTJAMzMzMzMzMUBmZmZmZuYyQJqZmZmZmTJAZmZmZmbmMUCamZmZmZkyQGZmZmZm5jJAMzMzMzOzMECamZmZmRkyQM3MzMzMzDFAZmZmZmbmMkAAAAAAAAAxQJqZmZmZGTVAAAAAAAAANEAAAAAAAIAyQM3MzMzMTDNAmpmZmZkZM0AAAAAAAAAyQGZmZmZmZjJAAAAAAACAMkAzMzMzM7MzQGZmZmZm5jBAzczMzMzMMkAAAAAAAAAzQGZmZmZm5jJAZmZmZmbmMUAzMzMzMzM1QDMzMzMzszFAZmZmZmbmMkBmZmZmZuYxQAAAAAAAgDNAmpmZmZkZMkCamZmZmZkyQAAAAAAAgDFAzczMzMzMMkCamZmZmZkwQJqZmZmZGTNAZmZmZmbmMECamZmZmRk1QAAAAAAAADFAMzMzMzMzMkCamZmZmRkxQAAAAAAAADJAMzMzMzMzMECamZmZmRkzQJqZmZmZmTBAZmZmZmZmM0AAAAAAAAAzQGZmZmZmZjJAMzMzMzMzMUBmZmZmZuYyQAAAAAAAgDFAAAAAAACAMkDNzMzMzMwwQGZmZmZmZjNAmpmZmZkZMECamZmZmRkzQDMzMzMzMzFAmpmZmZmZMUDNzMzMzMwyQGZmZmZmZjNAzczMzMzMMUDNzMzMzEw0QAAAAAAAgDNAmpmZmZmZMkAzMzMzMzMzQM3MzMzMzDJAAAAAAAAAMkCamZmZmRkyQJqZmZmZGTFAmpmZmZkZMkDNzMzMzEwxQGZmZmZm5jJAmpmZmZmZMkAAAAAAAIAyQJqZmZmZGTBAAAAAAACAMkBmZmZmZuYxQAAAAAAAADRAAAAAAAAAMEAAAAAAAAA0QJqZmZmZmTJAZmZmZmbmMkAzMzMzMzMxQAAAAAAAADRAAAAAAAAAMUAAAAAAAAAzQAAAAAAAgDBAzczMzMxMNEAzMzMzM7MxQAAAAAAAgDNAMzMzMzOzNEDNzMzMzEwyQAAAAAAAADFAAAAAAACANEAAAAAAAAAxQJqZmZmZmTJAMzMzMzMzMUDNzMzMzMwzQAAAAAAAADFAAAAAAACAMkDNzMzMzMwvQAAAAAAAADNAmpmZmZmZMUDNzMzMzEwyQJqZmZmZGTFAAAAAAAAAMkBmZmZmZuYxQDMzMzMzMzNAAAAAAACAMkAAAAAAAIAyQJqZmZmZmTFAAAAAAACAMUAAAAAAAIAxQJqZmZmZGTRAAAAAAACAMEBmZmZmZuYxQJqZmZmZGTFAMzMzMzMzMUAAAAAAAAAvQAAAAAAAADFAzczMzMzMMEAzMzMzM7MyQJqZmZmZmTJAZmZmZmZmMkDNzMzMzMwxQJqZmZmZGTJAmpmZmZkZMUAAAAAAAIAyQA=="},"shape":[152],"dtype":"float64","order":"little"}],["species",["Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie","Adelie"]]]}}},"view":{"type":"object","name":"CDSView","id":"p1634","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1635"}}},"glyph":{"type":"object","name":"Scatter","id":"p1630","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#30a2da"},"fill_color":{"type":"value","value":"#30a2da"},"hatch_color":{"type":"value","value":"#30a2da"}}},"selection_glyph":{"type":"object","name":"Scatter","id":"p1638","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"angle":{"type":"value","value":0.0},"line_color":{"type":"value","value":"#30a2da"},"line_alpha":{"type":"value","value":1},"line_width":{"type":"value","value":1},"line_join":{"type":"value","value":"bevel"},"line_cap":{"type":"value","value":"butt"},"line_dash":{"type":"value","value":[]},"line_dash_offset":{"type":"value","value":0},"fill_color":{"type":"value","value":"#30a2da"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#30a2da"},"hatch_alpha":{"type":"value","value":1.0},"hatch_scale":{"type":"value","value":12.0},"hatch_pattern":{"type":"value","value":null},"hatch_weight":{"type":"value","value":1.0},"marker":{"type":"value","value":"circle"}}},"nonselection_glyph":{"type":"object","name":"Scatter","id":"p1631","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#30a2da"},"line_alpha":{"type":"value","value":1},"fill_color":{"type":"value","value":"#30a2da"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#30a2da"},"hatch_alpha":{"type":"value","value":0.1}}},"muted_glyph":{"type":"object","name":"Scatter","id":"p1632","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#30a2da"},"line_alpha":{"type":"value","value":0.2},"fill_color":{"type":"value","value":"#30a2da"},"fill_alpha":{"type":"value","value":0.2},"hatch_color":{"type":"value","value":"#30a2da"},"hatch_alpha":{"type":"value","value":0.2}}}}},{"type":"object","name":"GlyphRenderer","id":"p1648","attributes":{"name":"Chinstrap","data_source":{"type":"object","name":"ColumnDataSource","id":"p1639","attributes":{"selected":{"type":"object","name":"Selection","id":"p1640","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1641"},"data":{"type":"map","entries":[["bill_length_mm",{"type":"ndarray","array":{"type":"bytes","data":"AAAAAABAR0AAAAAAAABJQGZmZmZmpklAMzMzMzOzRkCamZmZmVlKQJqZmZmZmUZAzczMzMwMR0BmZmZmZqZJQAAAAAAAAEdAZmZmZmamSUDNzMzMzExHQJqZmZmZ2UlAAAAAAACAR0AAAAAAAABKQDMzMzMz80ZAAAAAAABASUBmZmZmZiZJQAAAAAAAAE1AMzMzMzMzR0CamZmZmZlIQDMzMzMzM0VAAAAAAABASECamZmZmZlFQM3MzMzMTElAmpmZmZlZR0AAAAAAAABKQAAAAAAAQElAAAAAAADASEAzMzMzMzNHQGZmZmZmZkpAMzMzMzNzRECamZmZmRlLQAAAAAAAQEVAAAAAAACASUCamZmZmdlIQAAAAAAAwEdAzczMzMzMR0AAAAAAAABKQDMzMzMzc0dAAAAAAADASkAAAAAAAIBIQJqZmZmZGUdAMzMzMzNzSUAAAAAAAMBGQDMzMzMzc0lAZmZmZmZmSUDNzMzMzAxJQAAAAAAAgEhAAAAAAADASUBmZmZmZuZIQM3MzMzMDEhAMzMzMzOzSUCamZmZmdlGQJqZmZmZWUlAAAAAAABARUCamZmZmRlKQJqZmZmZmUZAZmZmZmamSECamZmZmRlJQM3MzMzMzEZAMzMzMzPzSUBmZmZmZmZHQJqZmZmZ2UZAZmZmZmbmS0AAAAAAAMBFQM3MzMzMzEhAZmZmZmZmSUCamZmZmRlJQA=="},"shape":[68],"dtype":"float64","order":"little"}],["bill_depth_mm",{"type":"ndarray","array":{"type":"bytes","data":"ZmZmZmbmMUAAAAAAAIAzQDMzMzMzMzNAMzMzMzOzMkDNzMzMzMwzQM3MzMzMzDFAMzMzMzMzMkAzMzMzMzMyQGZmZmZm5jJAZmZmZmbmM0DNzMzMzMwxQM3MzMzMTDRAzczMzMxMMUCamZmZmRkyQJqZmZmZGTFAmpmZmZmZM0AAAAAAAAA0QM3MzMzMzDFAmpmZmZmZMkAzMzMzMzMyQM3MzMzMTDFAAAAAAACAMUCamZmZmZkwQGZmZmZmZjNAZmZmZmbmMUAAAAAAAAAzQGZmZmZmZjJAAAAAAAAAM0DNzMzMzMwxQAAAAAAAADRAmpmZmZmZMEDNzMzMzMw0QDMzMzMzszBAzczMzMzMMkCamZmZmZkyQM3MzMzMzDBAzczMzMxMMkAzMzMzM7M0QJqZmZmZmTBAZmZmZmbmM0AAAAAAAIAzQAAAAAAAgDFAmpmZmZkZM0AAAAAAAAAxQGZmZmZm5jFAAAAAAACAMkBmZmZmZuYxQJqZmZmZmTNAMzMzMzOzMkDNzMzMzEwxQGZmZmZmZjBAAAAAAAAAM0DNzMzMzEwxQDMzMzMzszNAzczMzMxMMUDNzMzMzMwyQJqZmZmZmTBAZmZmZmbmM0DNzMzMzMwyQGZmZmZmZjNAAAAAAACAM0AAAAAAAIAwQAAAAAAAADFAzczMzMzMM0CamZmZmRkyQDMzMzMzMzJAAAAAAAAAM0AzMzMzM7MyQA=="},"shape":[68],"dtype":"float64","order":"little"}],["species",["Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap","Chinstrap"]]]}}},"view":{"type":"object","name":"CDSView","id":"p1649","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1650"}}},"glyph":{"type":"object","name":"Scatter","id":"p1645","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#fc4f30"},"fill_color":{"type":"value","value":"#fc4f30"},"hatch_color":{"type":"value","value":"#fc4f30"}}},"selection_glyph":{"type":"object","name":"Scatter","id":"p1652","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"angle":{"type":"value","value":0.0},"line_color":{"type":"value","value":"#fc4f30"},"line_alpha":{"type":"value","value":1},"line_width":{"type":"value","value":1},"line_join":{"type":"value","value":"bevel"},"line_cap":{"type":"value","value":"butt"},"line_dash":{"type":"value","value":[]},"line_dash_offset":{"type":"value","value":0},"fill_color":{"type":"value","value":"#fc4f30"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#fc4f30"},"hatch_alpha":{"type":"value","value":1.0},"hatch_scale":{"type":"value","value":12.0},"hatch_pattern":{"type":"value","value":null},"hatch_weight":{"type":"value","value":1.0},"marker":{"type":"value","value":"circle"}}},"nonselection_glyph":{"type":"object","name":"Scatter","id":"p1646","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#fc4f30"},"line_alpha":{"type":"value","value":1},"fill_color":{"type":"value","value":"#fc4f30"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#fc4f30"},"hatch_alpha":{"type":"value","value":0.1}}},"muted_glyph":{"type":"object","name":"Scatter","id":"p1647","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#fc4f30"},"line_alpha":{"type":"value","value":0.2},"fill_color":{"type":"value","value":"#fc4f30"},"fill_alpha":{"type":"value","value":0.2},"hatch_color":{"type":"value","value":"#fc4f30"},"hatch_alpha":{"type":"value","value":0.2}}}}},{"type":"object","name":"GlyphRenderer","id":"p1662","attributes":{"name":"Gentoo","data_source":{"type":"object","name":"ColumnDataSource","id":"p1653","attributes":{"selected":{"type":"object","name":"Selection","id":"p1654","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1655"},"data":{"type":"map","entries":[["bill_length_mm",{"type":"ndarray","array":{"type":"bytes","data":"zczMzMwMR0AAAAAAAABJQJqZmZmZWUhAAAAAAAAASUDNzMzMzMxHQAAAAAAAQEdAMzMzMzOzRkCamZmZmVlHQGZmZmZmpkVAZmZmZmZmR0AzMzMzM3NEQAAAAAAAgEhAAAAAAADARkAzMzMzMzNIQGZmZmZm5kZAZmZmZmamSEAAAAAAAABFQJqZmZmZmUhAmpmZmZkZR0CamZmZmVlIQJqZmZmZGUlAzczMzMyMRkAAAAAAAEBHQGZmZmZmJkdAMzMzMzNzRUDNzMzMzAxHQAAAAAAAQEZAZmZmZmbmR0CamZmZmRlIQAAAAAAAAElAZmZmZmamR0BmZmZmZmZFQM3MzMzMjEZAzczMzMzMTUDNzMzMzIxIQDMzMzMzM0hAzczMzMxMRUAzMzMzMzNGQAAAAAAAAEZAmpmZmZlZSECamZmZmVlFQM3MzMzMzEhAZmZmZmamRkDNzMzMzMxIQAAAAAAAQElAzczMzMzMRUAAAAAAAMBGQAAAAAAAQElAMzMzMzNzRkCamZmZmZlGQM3MzMzMTEdAAAAAAABASEDNzMzMzIxGQM3MzMzMDElAAAAAAABAR0AAAAAAAIBGQGZmZmZm5kVAAAAAAADARkCamZmZmZlFQDMzMzMzM0lAZmZmZmamRkCamZmZmRlHQJqZmZmZ2UZAZmZmZmYmS0BmZmZmZuZGQGZmZmZm5khAmpmZmZkZR0AAAAAAAMBIQAAAAAAAwEVAmpmZmZlZSUCamZmZmdlHQDMzMzMzM0dAmpmZmZkZSEAAAAAAAEBHQDMzMzMzM0dAzczMzMxMSEAAAAAAAMBHQM3MzMzMjElAmpmZmZmZRkCamZmZmZlGQM3MzMzMjEhAAAAAAABASkAzMzMzM7NHQAAAAAAAAElAMzMzMzNzRkBmZmZmZmZJQDMzMzMzs0VAZmZmZmamSUAAAAAAAMBHQM3MzMzMDEpAAAAAAADAR0CamZmZmRlKQAAAAAAAwEZAAAAAAADASEAAAAAAAEBGQGZmZmZmZklAMzMzMzOzSEAzMzMzM3NHQDMzMzMzM0hAzczMzMyMSUAAAAAAAEBIQDMzMzMz80tAmpmZmZmZR0DNzMzMzIxIQGZmZmZmpkdAZmZmZmZmR0CamZmZmdlEQDMzMzMzs0pAZmZmZmamRUDNzMzMzAxIQAAAAAAAQElAZmZmZmbmSEAAAAAAAMBFQAAAAAAAwElAmpmZmZkZR0DNzMzMzIxLQAAAAAAAQEZAZmZmZmZmSECamZmZmZlHQAAAAAAAAPh/ZmZmZmZmR0AzMzMzMzNJQJqZmZmZmUZAMzMzMzPzSEA="},"shape":[124],"dtype":"float64","order":"little"}],["bill_depth_mm",{"type":"ndarray","array":{"type":"bytes","data":"ZmZmZmZmKkDNzMzMzEwwQDMzMzMzMyxAZmZmZmZmLkAAAAAAAAAtQAAAAAAAACtAMzMzMzMzLUCamZmZmZkuQM3MzMzMzCpAzczMzMzMLkBmZmZmZmYrQJqZmZmZGTBAZmZmZmZmK0AzMzMzMzMtQDMzMzMzMy1AZmZmZmZmL0AAAAAAAAArQGZmZmZmZi5AAAAAAAAALUAzMzMzMzMuQJqZmZmZmSxAAAAAAAAALUAAAAAAAAAtQJqZmZmZmS9AMzMzMzMzKkAzMzMzMzMuQJqZmZmZmSxAAAAAAAAALkCamZmZmZksQJqZmZmZmS5AmpmZmZmZLkBmZmZmZmYsQAAAAAAAAC1AAAAAAAAAMUCamZmZmZktQM3MzMzMTDBAZmZmZmZmK0DNzMzMzEwxQDMzMzMzMytAZmZmZmZmL0BmZmZmZmYrQAAAAAAAADBAZmZmZmZmK0AAAAAAAAAuQM3MzMzMzC9AzczMzMzMK0DNzMzMzMwrQM3MzMzMzC9AmpmZmZmZKkCamZmZmZkvQGZmZmZmZixAMzMzMzMzLEDNzMzMzMwsQAAAAAAAAC5AzczMzMzMLEDNzMzMzMwuQM3MzMzMzCtAAAAAAAAALkAAAAAAAAAtQJqZmZmZmS5AmpmZmZmZK0DNzMzMzMwtQM3MzMzMzCtAZmZmZmZmL0BmZmZmZmYsQM3MzMzMzDBAzczMzMzMLEAzMzMzMzMwQGZmZmZmZixAAAAAAAAALkAAAAAAAAAuQDMzMzMzMy9AMzMzMzMzL0CamZmZmZktQAAAAAAAAC5AAAAAAAAAMEBmZmZmZmYsQM3MzMzMTDBAmpmZmZmZK0BmZmZmZmYwQAAAAAAAAC1AMzMzMzMzL0AzMzMzMzMtQM3MzMzMzC9AmpmZmZmZK0DNzMzMzEwxQM3MzMzMzCxAZmZmZmZmLEAAAAAAAAAsQAAAAAAAADFAAAAAAAAALkCamZmZmRkxQAAAAAAAAC1AmpmZmZkZMEBmZmZmZmYtQGZmZmZmZi9AmpmZmZmZL0AzMzMzMzMtQM3MzMzMzCxAAAAAAACAMEAAAAAAAAAuQAAAAAAAADFAAAAAAAAAL0AAAAAAAAAuQJqZmZmZmStAmpmZmZkZMEBmZmZmZmYtQJqZmZmZmS9AAAAAAAAALEAzMzMzMzMuQGZmZmZmZi5AzczMzMzML0BmZmZmZmYuQM3MzMzMTDBAMzMzMzMzLEAAAAAAAAAwQGZmZmZmZi9AMzMzMzMzMEBmZmZmZmYrQAAAAAAAAPh/mpmZmZmZLEBmZmZmZmYvQJqZmZmZmS1AmpmZmZkZMEA="},"shape":[124],"dtype":"float64","order":"little"}],["species",["Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo","Gentoo"]]]}}},"view":{"type":"object","name":"CDSView","id":"p1663","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1664"}}},"glyph":{"type":"object","name":"Scatter","id":"p1659","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#e5ae38"},"fill_color":{"type":"value","value":"#e5ae38"},"hatch_color":{"type":"value","value":"#e5ae38"}}},"selection_glyph":{"type":"object","name":"Scatter","id":"p1666","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"angle":{"type":"value","value":0.0},"line_color":{"type":"value","value":"#e5ae38"},"line_alpha":{"type":"value","value":1},"line_width":{"type":"value","value":1},"line_join":{"type":"value","value":"bevel"},"line_cap":{"type":"value","value":"butt"},"line_dash":{"type":"value","value":[]},"line_dash_offset":{"type":"value","value":0},"fill_color":{"type":"value","value":"#e5ae38"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#e5ae38"},"hatch_alpha":{"type":"value","value":1.0},"hatch_scale":{"type":"value","value":12.0},"hatch_pattern":{"type":"value","value":null},"hatch_weight":{"type":"value","value":1.0},"marker":{"type":"value","value":"circle"}}},"nonselection_glyph":{"type":"object","name":"Scatter","id":"p1660","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#e5ae38"},"line_alpha":{"type":"value","value":1},"fill_color":{"type":"value","value":"#e5ae38"},"fill_alpha":{"type":"value","value":1},"hatch_color":{"type":"value","value":"#e5ae38"},"hatch_alpha":{"type":"value","value":0.1}}},"muted_glyph":{"type":"object","name":"Scatter","id":"p1661","attributes":{"tags":["apply_ranges"],"x":{"type":"field","field":"bill_length_mm"},"y":{"type":"field","field":"bill_depth_mm"},"size":{"type":"value","value":5.477225575051661},"line_color":{"type":"value","value":"#e5ae38"},"line_alpha":{"type":"value","value":0.2},"fill_color":{"type":"value","value":"#e5ae38"},"fill_alpha":{"type":"value","value":0.2},"hatch_color":{"type":"value","value":"#e5ae38"},"hatch_alpha":{"type":"value","value":0.2}}}}}],"toolbar":{"type":"object","name":"Toolbar","id":"p1602","attributes":{"tools":[{"type":"object","name":"WheelZoomTool","id":"p1585","attributes":{"tags":["hv_created"],"renderers":"auto","zoom_together":"none"}},{"type":"object","name":"HoverTool","id":"p1586","attributes":{"tags":["hv_created"],"renderers":[{"id":"p1633"},{"id":"p1648"},{"id":"p1662"}],"tooltips":[["species","@{species}"],["bill_length_mm","@{bill_length_mm}"],["bill_depth_mm","@{bill_depth_mm}"]]}},{"type":"object","name":"SaveTool","id":"p1615"},{"type":"object","name":"PanTool","id":"p1616"},{"type":"object","name":"BoxZoomTool","id":"p1617","attributes":{"overlay":{"type":"object","name":"BoxAnnotation","id":"p1618","attributes":{"syncable":false,"level":"overlay","visible":false,"left":{"type":"number","value":"nan"},"right":{"type":"number","value":"nan"},"top":{"type":"number","value":"nan"},"bottom":{"type":"number","value":"nan"},"left_units":"canvas","right_units":"canvas","top_units":"canvas","bottom_units":"canvas","line_color":"black","line_alpha":1.0,"line_width":2,"line_dash":[4,4],"fill_color":"lightgrey","fill_alpha":0.5}}}},{"type":"object","name":"ResetTool","id":"p1623"}],"active_drag":{"id":"p1616"},"active_scroll":{"id":"p1585"}}},"left":[{"type":"object","name":"LinearAxis","id":"p1610","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1611","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1612"},"axis_label":"bill_depth_mm","major_label_policy":{"type":"object","name":"AllLabels","id":"p1613"}}}],"right":[{"type":"object","name":"Legend","id":"p1636","attributes":{"location":[0,0],"title":"species","click_policy":"mute","items":[{"type":"object","name":"LegendItem","id":"p1637","attributes":{"label":{"type":"value","value":"Adelie"},"renderers":[{"id":"p1633"}]}},{"type":"object","name":"LegendItem","id":"p1651","attributes":{"label":{"type":"value","value":"Chinstrap"},"renderers":[{"id":"p1648"}]}},{"type":"object","name":"LegendItem","id":"p1665","attributes":{"label":{"type":"value","value":"Gentoo"},"renderers":[{"id":"p1662"}]}}]}}],"below":[{"type":"object","name":"LinearAxis","id":"p1605","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1606","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1607"},"axis_label":"bill_length_mm","major_label_policy":{"type":"object","name":"AllLabels","id":"p1608"}}}],"center":[{"type":"object","name":"Grid","id":"p1609","attributes":{"axis":{"id":"p1605"},"grid_line_color":null}},{"type":"object","name":"Grid","id":"p1614","attributes":{"dimension":1,"axis":{"id":"p1610"},"grid_line_color":null}}],"min_border_top":10,"min_border_bottom":10,"min_border_left":10,"min_border_right":10,"output_backend":"webgl"}}]}}],"defs":[{"type":"model","name":"ReactiveHTML1"},{"type":"model","name":"FlexBox1","properties":[{"name":"align_content","kind":"Any","default":"flex-start"},{"name":"align_items","kind":"Any","default":"flex-start"},{"name":"flex_direction","kind":"Any","default":"row"},{"name":"flex_wrap","kind":"Any","default":"wrap"},{"name":"justify_content","kind":"Any","default":"flex-start"}]},{"type":"model","name":"FloatPanel1","properties":[{"name":"config","kind":"Any","default":{"type":"map"}},{"name":"contained","kind":"Any","default":true},{"name":"position","kind":"Any","default":"right-top"},{"name":"offsetx","kind":"Any","default":null},{"name":"offsety","kind":"Any","default":null},{"name":"theme","kind":"Any","default":"primary"},{"name":"status","kind":"Any","default":"normalized"}]},{"type":"model","name":"GridStack1","properties":[{"name":"mode","kind":"Any","default":"warn"},{"name":"ncols","kind":"Any","default":null},{"name":"nrows","kind":"Any","default":null},{"name":"allow_resize","kind":"Any","default":true},{"name":"allow_drag","kind":"Any","default":true},{"name":"state","kind":"Any","default":[]}]},{"type":"model","name":"drag1","properties":[{"name":"slider_width","kind":"Any","default":5},{"name":"slider_color","kind":"Any","default":"black"},{"name":"value","kind":"Any","default":50}]},{"type":"model","name":"click1","properties":[{"name":"terminal_output","kind":"Any","default":""},{"name":"debug_name","kind":"Any","default":""},{"name":"clears","kind":"Any","default":0}]},{"type":"model","name":"copy_to_clipboard1","properties":[{"name":"fill","kind":"Any","default":"none"},{"name":"value","kind":"Any","default":null}]},{"type":"model","name":"FastWrapper1","properties":[{"name":"object","kind":"Any","default":null},{"name":"style","kind":"Any","default":null}]},{"type":"model","name":"NotificationAreaBase1","properties":[{"name":"js_events","kind":"Any","default":{"type":"map"}},{"name":"position","kind":"Any","default":"bottom-right"},{"name":"_clear","kind":"Any","default":0}]},{"type":"model","name":"NotificationArea1","properties":[{"name":"js_events","kind":"Any","default":{"type":"map"}},{"name":"notifications","kind":"Any","default":[]},{"name":"position","kind":"Any","default":"bottom-right"},{"name":"_clear","kind":"Any","default":0},{"name":"types","kind":"Any","default":[{"type":"map","entries":[["type","warning"],["background","#ffc107"],["icon",{"type":"map","entries":[["className","fas fa-exclamation-triangle"],["tagName","i"],["color","white"]]}]]},{"type":"map","entries":[["type","info"],["background","#007bff"],["icon",{"type":"map","entries":[["className","fas fa-info-circle"],["tagName","i"],["color","white"]]}]]}]}]},{"type":"model","name":"Notification","properties":[{"name":"background","kind":"Any","default":null},{"name":"duration","kind":"Any","default":3000},{"name":"icon","kind":"Any","default":null},{"name":"message","kind":"Any","default":""},{"name":"notification_type","kind":"Any","default":null},{"name":"_destroyed","kind":"Any","default":false}]},{"type":"model","name":"TemplateActions1","properties":[{"name":"open_modal","kind":"Any","default":0},{"name":"close_modal","kind":"Any","default":0}]},{"type":"model","name":"BootstrapTemplateActions1","properties":[{"name":"open_modal","kind":"Any","default":0},{"name":"close_modal","kind":"Any","default":0}]},{"type":"model","name":"MaterialTemplateActions1","properties":[{"name":"open_modal","kind":"Any","default":0},{"name":"close_modal","kind":"Any","default":0}]}]}}
|
30 |
+
</script>
|
31 |
+
<script type="text/javascript">
|
32 |
+
(function() {
|
33 |
+
const fn = function() {
|
34 |
+
Bokeh.safely(function() {
|
35 |
+
(function(root) {
|
36 |
+
function embed_document(root) {
|
37 |
+
const docs_json = document.getElementById('p1678').textContent;
|
38 |
+
const render_items = [{"docid":"8d93a4b1-0241-4312-97a2-9d7e8cf602d5","roots":{"p1579":"bc2d23bf-3023-4b2e-9825-bb0daf9fdc45"},"root_ids":["p1579"]}];
|
39 |
+
root.Bokeh.embed.embed_items(docs_json, render_items);
|
40 |
+
}
|
41 |
+
if (root.Bokeh !== undefined) {
|
42 |
+
embed_document(root);
|
43 |
+
} else {
|
44 |
+
let attempts = 0;
|
45 |
+
const timer = setInterval(function(root) {
|
46 |
+
if (root.Bokeh !== undefined) {
|
47 |
+
clearInterval(timer);
|
48 |
+
embed_document(root);
|
49 |
+
} else {
|
50 |
+
attempts++;
|
51 |
+
if (attempts > 100) {
|
52 |
+
clearInterval(timer);
|
53 |
+
console.log("Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing");
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}, 10, root)
|
57 |
+
}
|
58 |
+
})(window);
|
59 |
+
});
|
60 |
+
};
|
61 |
+
if (document.readyState != "loading") fn();
|
62 |
+
else document.addEventListener("DOMContentLoaded", fn);
|
63 |
+
})();
|
64 |
+
</script>
|
65 |
+
</body>
|
66 |
+
</html>
|