Styling Plots

In [1]:
import numpy as np
import holoviews as hv
hv.extension('bokeh', 'matplotlib')

Since HoloViews supports a number of rendering backends, some of the styling options will differ between them. However, some fundamental concepts such as color cycles, colormapping, setting titles, and controlling legends are shared across backends. In this guide we will review how to apply these common styling options to plots. Once you know how to use these, you should be able to see how to use the options for specific backends covered in the Plotting with bokeh and Plotting with matplotlib user guides.

Cycles and Palettes

Frequently we want to plot multiple subsets of data, which is made easy by using Overlay and NdOverlay objects. When overlaying multiple elements of the same type they will need to be distinguished visually, and HoloViews provides two mechanisms for styling the different subsets automatically in those cases:

  • Cycle : A Cycle defines a list of discrete styles
  • Palette : A Palette defines a continuous color space which will be sampled discretely

Cycle

A Cycle can be applied to any of the style options on an element. By default, most elements define a Cycle on the color property. Here we will create a overlay of three Points objects using the default cycles, then display it using the default cycles along with a copy where we changed the dot color and size using a custom Cycle :

In [2]:
points = (
    hv.Points(np.random.randn(50, 2)      ) *
    hv.Points(np.random.randn(50, 2) + 1  ) *
    hv.Points(np.random.randn(50, 2) * 0.5)
)

color_cycle = hv.Cycle(['red', 'green', 'blue'])
points + points.options({'Points': {'color': color_cycle, 'size': 5}})
Out[2]:

Here color has been changed to cycle over the three provided colors, while size has been specified as a constant (though a cycle like hv.Cycle([2,5,10]) could just as easily have been used for the size as well).

Defaults

In addition to defining custom color cycles by explicitly defining a list of colors, Cycle also defines a list of default Cycles generated from bokeh Palettes and matplotlib colormaps:

In [3]:
def format_list(l):
    print(' '.join(sorted([k for k in l if not k.endswith('_r')])))

format_list(hv.Cycle.default_cycles.keys())
Accent Blues BrBG BuGn BuPu Category10 Category20 Category20b Category20c Colorblind Dark2 GnBu Greens OrRd Oranges PRGn Paired Pastel1 Pastel2 PiYG PuBu PuBuGn PuOr PuRd Purples RdBu RdGy RdPu RdYlBu RdYlGn Reds Set1 Set2 Set3 Spectral YlGn YlGnBu YlOrBr YlOrRd default_colors tab10 tab20 tab20b tab20c

(Here some of these Cycles have a reversed variant ending in _r that is not shown.)

To use one of these default Cycles simply construct the Cycle with the corresponding key:

In [4]:
xs = np.linspace(0, np.pi*2)
curves = hv.Overlay([hv.Curve(np.sin(xs+p)) for p in np.linspace(0, np.pi, 10)])

curves.options({'Curve': {'color': hv.Cycle('Category20'), 'width': 600}})
Out[4]:

Markers and sizes

The above examples focus on color Cycles, but Cycles may be used to define any style option. Here let's use them to cycle over a number of marker styles and sizes, which will be expanded by cycling over each item independently. In this case we are cycling over three Cycles, resulting in the following style combinations:

  1. {'color': '#30a2da', 'marker': 'x', 'size': 10}
  2. {'color': '#fc4f30', 'marker': '^', 'size': 5}
  3. {'color': '#e5ae38', 'marker': '+', 'size': 10}
In [5]:
color = hv.Cycle(['#30a2da', '#fc4f30', '#e5ae38'])
markers = hv.Cycle(['x', '^', '+'])
sizes = hv.Cycle([10, 5])
points.options({'Points': {'marker': markers, 'size': sizes}})
Out[5]:

Palettes

Palettes are similar to cycles, but treat a set of colors as a continuous colorspace to be sampled at regularly spaced intervals. Again they are made automatically available from existing colormaps (with _r versions also available):

In [6]:
format_list(hv.Palette.colormaps.keys())
Accent Blues BrBG BuGn BuPu CMRmap Category10 Category20 Category20b Category20c Colorblind Dark2 GnBu Greens Greys Inferno Magma OrRd Oranges PRGn Paired Pastel1 Pastel2 PiYG Plasma PuBu PuBuGn PuOr PuRd Purples RdBu RdGy RdPu RdYlBu RdYlGn Reds Set1 Set2 Set3 Spectral Viridis Wistia YlGn YlGnBu YlOrBr YlOrRd afmhot autumn binary bone brg bwr cool coolwarm copper cubehelix flag gist_earth gist_gray gist_heat gist_ncar gist_rainbow gist_stern gist_yarg gnuplot gnuplot2 gray grayscale hot hsv jet ocean pink prism rainbow seismic spring summer tab10 tab20 tab20b tab20c terrain winter

(Here each colormap X has a corresponding version X_r with the values reversed; the _r variants are suppressed above.)

As a simple example we will create a Palette from the Spectral colormap and apply it to an Overlay of 6 Ellipses. Comparing it to the Spectral Cycle we can immediately see that the Palette covers the entire color space spanned by the Spectral colormap, while the Cycle instead uses the first 6 colors of the Spectral colormap:

In [7]:
ellipses = hv.Overlay([hv.Ellipse(0, 0, s) for s in range(6)])

ellipses.relabel('Palette').options({'Ellipse': dict(color=hv.Palette('Spectral'), line_width=5)}) +\
ellipses.relabel('Cycle'  ).options({'Ellipse': dict(color=hv.Cycle(  'Spectral'), line_width=5)})
Out[7]:

Thus if you want to have have a discrete set of distinguishable colors starting from a list of colors that vary slowly and continuously, you should usually supply it as a Palette, not a Cycle. Conversely, you should use a Cycle when you want to iterate through a specific list of colors, in order, without skipping around the list like a Palette will.

Colormapping

Color cycles and styles are useful for categorical plots and when overlaying multiple subsets, but when we want to map data values to a color it is better to use HoloViews' facilities for color mapping. Certain image-like types will apply colormapping automatically; e.g. for Image , QuadMesh or HeatMap types the first value dimension is automatically mapped to the color. In other cases the values to colormap can be declared by providing a color style option that specifies which dimension to map into the color value.

Named colormaps

HoloViews accepts colormaps specified either as an explicit list of hex or HTML colors, as a Matplotlib colormap object, or as the name of a bokeh, matplotlib, and colorcet palettes/colormap (which are available when the respective library is imported). The named colormaps available are listed here (suppressing the _r versions) and illustrated in detail in the separate Colormaps user guide:

In [8]:
format_list(hv.plotting.list_cmaps())
Accent Blues BrBG BuGn BuPu CMRmap Category10 Category20 Category20b Category20c Colorblind Dark2 GnBu Greens Greys Inferno Magma OrRd Oranges PRGn Paired Pastel1 Pastel2 PiYG Plasma PuBu PuBuGn PuOr PuRd Purples RdBu RdGy RdPu RdYlBu RdYlGn Reds Set1 Set2 Set3 Spectral Viridis Wistia YlGn YlGnBu YlOrBr YlOrRd afmhot autumn bgy bgyw binary bjy bkr bky blues bmw bmy bone brg bwr cividis colorwheel cool coolwarm copper cubehelix dimgray fire flag gist_earth gist_gray gist_heat gist_ncar gist_rainbow gist_stern gist_yarg gnuplot gnuplot2 gray gwv hot hsv inferno isolum jet kb kbc kg kgy kr magma nipy_spectral ocean pink plasma prism rainbow seismic spring summer tab10 tab20 tab20b tab20c terrain twilight twilight_shifted viridis winter

To use one of these colormaps simply refer to it by name with the cmap style option:

In [9]:
ls = np.linspace(0, 10, 400)
xx, yy = np.meshgrid(ls, ls)
bounds=(-1,-1,1,1)   # Coordinate system: (left, bottom, right, top)
img = hv.Image(np.sin(xx)*np.cos(yy), bounds=bounds).options(colorbar=True, width=400)

img.options(cmap='PiYG') + img.options(cmap='PiYG_r')
Out[9]:
Custom colormaps

You can make your own custom colormaps by providing a list of hex colors:

In [10]:
img.options(cmap=['#0000ff', '#8888ff', '#ffffff', '#ff8888', '#ff0000'], colorbar=True, width=400)
Out[10]:
Discrete color levels

Lastly, existing colormaps can be made discrete by defining an integer number of color_levels :

In [11]:
img.options(cmap='PiYG', color_levels=5) + img.options(cmap='PiYG', color_levels=11) 
Out[11]:
Custom color intervals

In addition to a simple integer defining the number of discrete levels, the color_levels option also allows defining a set of custom intervals. This can be useful for defining a fixed scale, such as the Saffir-Simpson hurricane wind scale. Below we declare the color levels along with a list of colors, declaring the scale. Note that the levels define the intervals to map each color to, so if there are N colors we have to define N+1 levels.

Having defined the scale we can generate a theoretical hurricane path with wind speed values and use the color_levels and cmap to supply the custom color scale:

In [12]:
levels = [0, 38, 73, 95, 110, 130, 156, 999]  
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']

path = [
    (-75.1, 23.1, 0),   (-76.2, 23.8, 0),   (-76.9, 25.4, 0),   (-78.4, 26.1, 39),  (-79.6, 26.2, 39),
    (-80.3, 25.9, 39),  (-82.0, 25.1, 74),  (-83.3, 24.6, 74),  (-84.7, 24.4, 96),  (-85.9, 24.8, 111),
    (-87.7, 25.7, 111), (-89.2, 27.2, 131), (-89.6, 29.3, 156), (-89.6, 30.2, 156), (-89.1, 32.6, 131),
    (-88.0, 35.6, 111), (-85.3, 38.6, 96)
]

hv.Path([path], vdims='Wind Speed').options(
    color='Wind Speed', color_levels=levels, cmap=colors, line_width=8, colorbar=True, width=450
)
Out[12]:

Setting color ranges

For an image-like element, color ranges are determined by the range of the z value dimension, and they can thus be controlled using the .redim.range method with z . As an example, let's set some values in the image array to NaN and then set the range to clip the data at 0 and 0.9. By declaring the clipping_colors option we can control what colors are used for NaN values and for values above and below the defined range:

In [13]:
clipping = {'min': 'red', 'max': 'green', 'NaN': 'gray'}
opts = dict(cmap='Blues', colorbar=True, width=300, height=230, axiswise=True)

arr = np.sin(xx)*np.cos(yy)
arr[:190, :127] = np.NaN

original = hv.Image(arr, bounds=bounds).options(**opts)
colored  = original.options(clipping_colors=clipping)
clipped  = colored.redim.range(z=(0, 0.9))

original + colored + clipped
Out[13]:

By default (left plot above), the min and max values in the array map to the first color (white) and last color (dark blue) in the colormap, and NaNs are 'transparent' (an RGBA tuple of (0, 0, 0, 0)), revealing the underlying plot background. When the specified clipping_colors are supplied (middle plot above), NaN values are now colored gray, but the plot is otherwise the same because the autoranging still ensures that no value is mapped outside the available color range. Finally, when the z range is reduced (right plot above), the color range is mapped from a different range of numerical z values, and some values now fall outside the range and are thus clipped to red or green as specified.

Other options

  • logz : Enable logarithmic color scale (e.g. logz=True )
  • symmetric : Ensures that the color scale is centered on zero (e.g. symmetric=True )

Colormapping

As mentioned above, when plotting elements that do not automatically map colors to certain dimensions (e.g. an Image), we can use color options to do so explicitly. This allows colormapping both continuously valued and categorical values.

Continuous values

If we provide a continuous value for the color style option along with a continuous colormap, we can also enable a colorbar :

In [14]:
polygons = hv.Polygons([{('x', 'y'): hv.Ellipse(0, 0, (i, i)).array(), 'z': i} for i in range(1, 10)[::-1]], vdims='z')

polygons.options(color='z', colorbar=True, width=380)
Out[14]:

Categorical values

Conversely, when mapping a categorical value into a set of colors, we automatically get a legend (which can be disabled using the show_legend option):

In [15]:
categorical_points = hv.Points((np.random.rand(100), 
                                np.random.rand(100), 
                                np.random.choice(list('ABCD'), 100)), vdims='Category')

categorical_points.sort('Category').options(color='Category', cmap='Category20', size=5)
Out[15]:

Explicit color mapping

Instead of using a listed colormap, you can provide an explicit mapping from category to color. Here we will map the categories 'A', 'B', 'C' and 'D' to specific colors:

In [16]:
explicit_mapping = {'A': 'blue', 'B': 'red', 'C': 'green', 'D': 'purple'}

categorical_points.sort('Category').options(color='Category', cmap=explicit_mapping, size=5)
Out[16]:

Controlling axes

A number of options to control the axes are shared across backend. Here we provide a quick overview of linked plots for the same data displayed differently by applying log axes, disabling axes, rotating ticks, specifying the number of ticks, and supplying an explicit list of ticks.

In [17]:
points = hv.Points(np.exp(xs)) 
axes_opts = [('Plain', {}),
             ('Log', {'logy': True}),
             ('None', {'yaxis': None}),
             ('Rotate', {'xrotation': 90}),
             ('N Ticks', {'xticks': 3}),
             ('List Ticks', {'xticks': [0, 20, 50, 90]})]

hv.Layout([points.relabel(group=group).options(**opts)
           for group, opts in axes_opts]).cols(3)
Out[17]:

Tick formatters

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:

  • A classic format string such as '%d' , '%.3f' or '%d' which may also contain other characters ( '$%.2f' )
  • A function which will be compiled to JS using flexx (if installed) when using bokeh
  • A bokeh.models.TickFormatter in bokeh and a matplotlib.ticker.Formatter instance in matplotlib

Here is a small example demonstrating how to use the string format and function approaches:

In [18]:
def formatter(value):
    return str(value) + ' %'

points.options(xformatter=formatter, yformatter='$%.2f')
Out[18]: