{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "

Tutorial 02. Annotating your Data

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preliminaries\n", "\n", "In this Jupyter notebook, we will be using the [NumPy](http://numpy.org), [Pandas](http://pandas.org), and [HoloViews](http://holoviews.org) libraries to work with our data, using the standard abbreviations ``np``, ``pd``, and ``hv``. In order to show how HoloViews works, we'll focus on the native HoloViews API and not the simplified Pandas-style HVPlot interface, but you can use either one as you prefer.\n", "\n", "
\n", "\n", "\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import holoviews as hv\n", "hv.extension('bokeh')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "``hv.extension('bokeh')`` loads the [Bokeh](bokeh.pydata.org) plotting support in HoloViews; ``hv.extension('matplotlib')`` would use the [matplotlib](http://matplotlib.org) support instead. If ``pyviz`` is installed correctly as described at [pyviz.org](http://pyviz.org), you should see a HoloViews logo and a small Bokeh logo when you run the cell above." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Annotating data to make it visualizable\n", "\n", "By default, Python will typically display your data in a numerical form. For instance, let's say you have a simple Pandas dataframe containing 41 samples of a quadratic function ``y=100-x^2``, at different values of ``x``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xs = np.arange(-10, 10.5, 0.5)\n", "ys = 100-xs**2\n", "\n", "df = pd.DataFrame(dict(x=xs, y=ys))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This dataframe will display as a table of numbers:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's important to be able to access this numerical representation, but it's awkward to work with, hard to take in at a glance, and even this tiny dataset won't fit on a single screen in table form. In many cases, a graphical representation would be much more natural to work with, but visualizing anything but the simplest data typically requires writing a complex series of plotting commands that are distracting when exploring data. HoloViews provides an alternative approach: instead of having plotting as a separate task, annotate your data to make it instantly visualizable on demand in whatever context it ends up. You can then switch between graphical and numerical representations whenever you wish.\n", "\n", "In this case, we know that the data is a set of samples from a continuous function of x, and we can capture that information by declaring a HoloViews object of type [``Curve``](http://holoviews.org/reference/elements/bokeh/Curve.html):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "simple_curve = hv.Curve(df,'x','y')\n", "print(simple_curve)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "``:Curve [x] (y)`` is HoloViews's shorthand for saying that the data in ``df`` is a set of samples from a continuous function ``y`` of one independent variable ``x``, and ``simple_curve`` simply pairs your dataframe ``df`` with this semantic declaration.\n", "\n", "Once we've captured this crucial bit of metadata, HoloViews now knows enough about this object to represent it graphically, as it will do by default in a Jupyter notebook:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "simple_curve" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This Bokeh plot is much more convenient to examine than a column of numbers, because it conveys the entire set of data in a compact, easily appreciated, interactively explorable format. HoloViews knew that a continuous curve like this is the right representation for what would otherwise be just a table of numbers, because we explicitly declared the element type as ``hv.Curve``. Crucially, ``simple_curve`` itself is not a plot, it's just a simple wrapper around your data that happens to have a convenient graphical representation. The full dataframe will always be available as ``simple_curve.data``, for any numerical computations you would like to do:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "simple_curve.data.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, with HoloViews you don't have to select between plotting your data and working with it numerically. Any HoloViews object will let you do *both* conveniently; you can simply choose whatever representation is the most appropriate way to approach the task you are doing. This approach is very different from a traditional plotting program, where the objects you create (e.g. a Matplotlib figure or a native Bokeh plot) are a dead end from an analysis perspective, useful only for plotting. This tutorial will show you how to create visualizable ``Elements`` like the ``Curve`` object above, and some of the ways you can annotate them to make sure the visual representation accurately conveys the properties of your data.\n", "\n", "\n", "## HoloViews Elements\n", "\n", "Elements are HoloViews' most basic, core primitives. Elements allow you to give a dataset a semantically meaningful visual representation, while preserving the raw data you supplied and allowing various analysis operations to be applied to it. Nearly every Element can be constructed in the same way as you saw for ``Curve`` above (apart from ``Histogram`` and certain annotations):\n", "\n", "```\n", "hv.Element(data, kdims=None, vdims=None, **kwargs)\n", "```\n", "\n", "This standard signature consists of the same five types of information:\n", "\n", "- **``Element``**: any of the dozens of element types shown in the [reference gallery](http://holoviews.org/reference/index.html).\n", "- **``data``**: your data in one of a number of formats described below, such as tabular dataframes or multidimensional gridded Xarray or Numpy arrays.\n", "- **``kdims``**: \"key dimension(s)\", also called independent variables or index dimensions in other contexts---the values for which your data was measured.\n", "- **``vdims``**: \"value dimension(s)\", also called dependent variables or measurements---what was measured or recorded for each value of the key dimensions. \n", "- **``kwargs``**: optional keyword arguments specific to that ``Element`` type (rarely needed).\n", "\n", "For ``simple_curve``, we selected column `x` as the key dimension and `y` as the value dimension, corresponding to how we generated the data (``y`` as a function of ``x``). Other element types might expect multiple key dimensions as a list, or allow multiple value dimensions (to show multiple data points for the same keys). We supplied a Pandas dataframe for the ``data``, but we could just as well have supplied the original Numpy arrays ``xs`` and ``ys``, Python lists, or a Python dictionary:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.Curve((xs,ys)) + hv.Curve((list(xs),list(ys))) + hv.Curve({'x':xs,'y':ys})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(We will discuss the ``+`` operator below, but here it just lets us put several HoloViews objects side by side). Each of these data formats would be represented in slightly different textual forms by Python, but as you can see, the graphical representation is the same for all of them because they all represent the same datapoints on a continuous curve.\n", "\n", "Of course, maybe you don't want to treat these samples as a curve; perhaps for your purposes you want to see the individual data points, or maybe you want to draw attention to the area under the curve. You can provide this information to HoloViews by selecting the appropriate ``Element``, such as [``Scatter``](http://holoviews.org/reference/elements/bokeh/Scatter.html), [``Area``](http://holoviews.org/reference/elements/bokeh/Area.html), or [``Bars``]((http://holoviews.org/reference/elements/bokeh/Bars.html). Try creating some of those elements here as an exercise!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Instead of hv.Curve(df,'x','y'), try hv.Area or hv.Scatter to choose how your dataframe appears" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Annotating an element\n", "\n", "Wrapping your data (``df`` or ``(xs,ys)`` here) as a HoloViews element is sufficient to make it visualizable, but there are many other aspects of the data that we can capture to convey more about its meaning to HoloViews. For instance, we might want to specify what the x-axis and y-axis actually correspond to, in the real world. Perhaps this parabola is the trajectory of a ball thrown into the air, in which case we could declare the object as:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "trajectory = hv.Curve(df, ('x','Horizontal distance'), ('y','Height'))\n", "trajectory" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here `x` is a short and convenient [tab-completeable](http://pandas.pydata.org/pandas-docs/stable/dsintro.html#dataframe-column-attribute-access-and-ipython-completion) Python identifier for the dimension that must match the column name when using dataframes, while `Horizontal distance` is a human-readable label conveying more about what it means than the original \"`x`\".\n", "\n", "Even though the additional information we provided is a *description of the data*, not *parameters of a plotting object*, HoloViews is designed to reveal the properties of your data accurately, and so the axes now update to show what these dimensions represent. We'll look more at [``Dimension``s](http://holoviews.org/_modules/holoviews/core/dimension.html) like `x` below, including how to add units or other additional semantic information." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Take a look at trajectory.vdims" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Casting between elements\n", "\n", "The type of an element is a declaration of important facts about your data, which gives HoloViews the appropriate hint required to generate a suitable visual representation from it. For instance, calling it a ``Curve`` is a declaration from the user that the data consists of samples from a *continuous* function, which is why HoloViews plots it as a connected object. If we convert to an ``hv.Scatter`` object instead, the same set of data will show up as separated points, because \"Scatter\" does not make an assumption that the data is meant to be continuous:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.Scatter(simple_curve) + hv.Area(simple_curve)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Casting the same data between different Element types in this way is often useful as a way to see your data differently, particularly if you are not certain of a single best way to interpret the data. Casting preserves your declared metadata as much as possible, propagating your declarations from the original object to the new one." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# How do you predict the representation for hv.Scatter(trajectory) will differ from\n", "# hv.Scatter(simple_curve) above? Try it!\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Also try casting the trajectory to an area then back to a curve.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Gridded data\n", "\n", "Most HoloViews elements fall into one of two categories, depending on whether they accept ``tabular`` or ``gridded`` data. The ``Curve`` above was constructed from columns of x- and y-values, while an Image can be constructed from a 2D [NumPy](http://www.numpy.org/) array:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 10, 500)\n", "y = np.linspace(0, 10, 500)\n", "xx, yy = np.meshgrid(x, y)\n", "\n", "arr = np.sin(xx)*np.cos(yy)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As above, we know that this data was sampled from a continuous function, but this time it's a function of *two* key dimensions, so we declare it as an [``hv.Image``](http://holoviews.org/reference/elements/bokeh/Image.html) object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "image = hv.Image(arr)\n", "image" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A very commonly useful method on all types of elements is the ``.hist`` method, which adjoins a plot displaying the distribution of values:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "image.hist()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, the default names for the dimensions of an ``Image`` are `x` and `y` for the two key dimensions and `z` for the value dimension (shown as color). Overriding those names is easy:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.Image(arr, ['xaxis','yaxis'], 'h').hist()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Try visualizing different two-dimensional arrays.\n", "# You can try a new function entirely or simple modifications of the existing one\n", "# E.g., explore the effect of squaring and cubing the arguments to sine and cosine\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Working with dimensions in dataframes\n", "\n", "In each case above, we've been plotting all of the data provided to an object. A Pandas [DataFrame](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html) very often has many columns of data available, more than will fit into a single ``Element``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "economic_data = pd.read_csv('../data/macro.csv')\n", "economic_data.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here ``country`` and ``year`` are possible key dimensions, determining what measurements were taken, and the rest of the columns are possible value dimensions, i.e., the results of those measurements. Notice that the dataframe itself makes no distinction between the two types of dimension; this is information that a human must supply to map the values into something meaningfully visualizable.\n", "\n", "As an example, let's build an element that helps us understand how the percentage growth in US GDP varies over time. As our dataframe contains GDP growth data for lots of countries, let us select the United States from the table and create a [``Curve``](http://holoviews.org/reference/elements/bokeh/Curve.html) element from it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "US_data = economic_data[economic_data['country'] == 'United States'] # Select data for the US only\n", "US_data.tail()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "growth_curve = hv.Curve(US_data, 'year', 'growth')\n", "growth_curve" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Plot the unemployment (unem) by year\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we want to clarify that 'growth' is 'GDP growth', we could do so in the original call using ``hv.Curve(US_data, 'year', ('growth','GDP growth'))``, which is a convenient shortcut for creating a ``Dimension`` object with a label. We can also supply dimension labels after the fact using the ``redim`` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdp_growth = growth_curve.redim.label(growth='GDP growth', year='Year')\n", "gdp_growth" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here the ``redim`` method associates a dimension label with each of the two key dimensions, creating and returning a new element called ``gdp_growth`` (you can check for yourself that ``growth_curve`` is unchanged).\n", "\n", "The ``redim`` utility lets you easily change other [dimension parameters](), and as an example let's give our GDP growth dimension the appropriate unit:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gdp_growth.redim.unit(growth='%')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Use redim.unit to give the year dimension a better unit \n", "# For instance, relabel to 'Time' then give the unit as 'years'\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Statistical elements\n", "\n", "So far we have mostly considered elements that map data values directly onto the screen, whether a curve where the x- and y-coordinates are drawn on the screen or an Image where the luminance of each pixel is drawn on the screen. Many other types of visualizations instead summarize the data, typically by computing statistics from it.\n", "\n", "One of the most common such summaries is a ``Histogram``. To compute a standard histogram we can use the ``np.histogram`` function and wrap the return value in a Histogram element:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.Histogram(np.histogram(economic_data.growth, normed=True), kdims='Growth')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively, we can let HoloViews compute the histogram using [kernel density estimation](http://holoviews.org/reference/elements/bokeh/Distribution.html) by declaring a ``Distribution`` element, which accepts data along with the key dimension (a column of a dataframe, in this case) whose distribution we want to visualize:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.Distribution(economic_data, 'growth')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another option for summarizing a dataset is using a box plot, which we can declare using a ``BoxWhisker`` element, specifying the ``'country'`` as a kdim and ``growth`` as the vdim, giving us an overview of the distribution of growth percentages for each country:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.BoxWhisker(economic_data, 'country', 'growth')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Composing elements together\n", "\n", "As you saw above, we very often want to combine multiple elements into a single plot, both to save space and to show how things are related. In this section, we introduce the two composition operators ``+`` and ``*``, which build [``Layout``](http://holoviews.org/reference/containers/bokeh/Layout.html) and [``Overlay``](http://holoviews.org/reference/containers/bokeh/Overlay.html) objects.\n", "\n", "### Layouts\n", "\n", "Layouts are useful for grouping related data side by side:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "layout = trajectory + hv.Scatter(trajectory) + hv.Area(trajectory) + hv.Spikes(trajectory)\n", "layout.cols(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Putting items together like this saves space, but even more importantly it acts as a declaration to HoloViews that these objects are related to each other. HoloViews thus ensures that all of them will share the same axis ranges for all dimensions that match, and zooming or panning on any one of them will make the corresponding change to the others (try it!). The [next section](03_Customizing_Visual_Appearance.ipynb) of this tutorial will describe how to change or override this behavior when appropriate.\n", "\n", "If we look closely, we can see that the result of applying the ``+`` operator is an [``hv.Layout``](http://holoviews.org/reference/containers/bokeh/Layout.html) object (with a hint that a two-column layout is desired):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(layout)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let us build a new layout by selecting elements from ``layout``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "layout.Curve.I + layout.Spikes.I" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that a ``Layout`` lets us pick component elements via two levels of tab-completable attribute access. Note that by default the type of the element defines the first level of access and the second level of access uses Roman numerals by default (because Python identifiers cannot start with numbers).\n", "\n", "These two levels correspond to another type of semantic declaration that applies to the elements directly (rather than their dimensions), called ``group`` and ``label``. Specifically, ``group`` allows you to declare what kind of thing this object is, while ``label`` allows you to label which specific object it is. What you put in those declarations, if anything, will form the title of the plot:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cannonball = trajectory.relabel('Cannonball', group='Trajectory')\n", "integral = hv.Area(cannonball).relabel('Filled')\n", "labelled_layout = cannonball + integral\n", "labelled_layout " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Try out the tab-completion of labelled_layout to build a new layout swapping the position of these elements\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Optional: Try using two levels of dictionary-style access to grab the cannonball trajectory\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Overlays\n", "\n", "Layout places objects side by side, allowing it to collect (almost!) any HoloViews objects that you want to indicate are related. Another operator ``*`` allows you to overlay elements into a single plot, if they live in the same space (with matching dimensions, and preferably with similar ranges over those dimensions). The result of ``*`` is an [``Overlay``](http://holoviews.org/reference/containers/bokeh/Overlay.html):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "trajectory * hv.Spikes(trajectory)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The indexing system of [``Overlay``](http://holoviews.org/reference/containers/bokeh/Overlay.html) is identical to that of [``Layout``](http://holoviews.org/reference/containers/bokeh/Layout.html)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Make an overlay of the Spikes object from layout on top of the filled trajectory area of labelled_layout\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One thing that is specific to Overlays is the use of color cycles to automatically differentiate between elements of the same type and ``group``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tennis_ball = cannonball.clone((xs, 0.5*np.array(ys)), label='Tennis Ball')\n", "cannonball + tennis_ball + (cannonball * tennis_ball)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we use the ``clone`` method to make a shallower tennis-ball trajectory: the ``clone`` method create a new object that preserves semantic metadata while allowing overrides (in this case we override the input data and the ``label``).\n", "\n", "As you can see, HoloViews can determine that the two overlaid curves will be distinguished by color, and so it also provides a legend so that the mapping from color to data is clear. In an Overlay, the title will contain whatever is shared between the elements in the plot, and for the third plot here, the title is just the group name \"Trajectory\" because the two labels differ. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Optional Exercise: \n", "# 1. Create a thrown_ball curve with half the height of tennis_ball by cloning it and assigning the label 'Thrown ball'\n", "# 2. Add thrown_ball to the overlay\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Slicing and selecting\n", "\n", "HoloViews elements can easily be sliced using array-style syntax or using the ``.select`` method. The following example shows how we can slice the ``cannonball`` trajectory into its ascending and descending components:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ascending = cannonball[-10.0:0.5].relabel('ascending')\n", "descending = cannonball.select(x=(0,None)).relabel('descending')\n", "ascending * descending" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As before, the color automatically cycles when overlaying two elements of the same type, HoloViews adds a legend when the different components are labelled, and the plot's title is \"Trajectory\" because the group name is all that is common to these elements. \n", "\n", "Note that the slicing in HoloViews is done in the continuous space of the dimension and not in the integer space of individual data samples. In this instance, the slice is over the ``Horizontal distance`` dimension and we can see that the slicing semantics follow the usual Python convention of an inclusive lower bound and an exclusive upper bound.\n", "\n", "This example also illustrates why we typically use simple identifiers for dimension names and reserve longer descriptions for the dimension labels: certain methods such as the ``select`` method shown above accept dimension names as keywords." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Onwards\n", "\n", "In later tutorials, we will see how elements and the principles of composition extend to *containers*, which makes exploring more complex data quick, easy, and interactive. Before we examine the container types, the [next tutorial](./03_Customizing_Visual_Appearance.ipynb) will look at how to customize the appearance of elements, change the plotting extension, and specify output formats.\n", "\n", "For a quick demonstration related to what we will be covering, hit the kernel restart button (⟳) in the toolbar for this notebook, change ``hv.extension('bokeh')`` to ``hv.extension('matplotlib')`` in the first cell, and rerun the notebook!" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }