{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Dimensioned Containers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So far we've seen how to [wrap data in elements](Annotating_Data.ipynb) and [compose](Composing_Data.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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import holoviews as hv\n",
    "hv.notebook_extension('bokeh')\n",
    "%opts Curve (line_width=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Declaring n-dimensional collections\n",
    "\n",
    "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.\n",
    "\n",
    "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.\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fm_modulation(f_carrier=220, f_mod=220, mod_index=1, length=0.1, sampleRate=2000):\n",
    "    sampleInc = 1.0/sampleRate\n",
    "    x = np.arange(0,length, sampleInc)\n",
    "    y = np.sin(2*np.pi*f_carrier*x + mod_index*np.sin(2*np.pi*f_mod*x))\n",
    "    return hv.Curve((x, y), 'Time', 'Amplitude')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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](./06-Live_Data.ipynb).\n",
    "\n",
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts Curve [width=600]\n",
    "f_carrier = np.linspace(20, 60, 3)\n",
    "f_mod = np.linspace(20, 100, 5)\n",
    "\n",
    "curve_dict = {(fc, fm): fm_modulation(fc, fm) for fc in f_carrier for fm in f_mod}\n",
    "\n",
    "kdims = [hv.Dimension(('f_carrier', 'Carrier frequency'), default=40),\n",
    "         hv.Dimension(('f_mod', 'Modulation frequency'), default=60)]\n",
    "holomap = hv.HoloMap(curve_dict, kdims=kdims)\n",
    "holomap"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Casting between n-dimensional containers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:\n",
    "\n",
    "* NdOverlay - An n-dimensional container which overlays the elements\n",
    "* NdLayout  - An n-dimensional container which displays the data in separate plot axes and adds titles for each value\n",
    "* GridSpace - A 1D or 2D container which lays out up to two dimensions on a grid.\n",
    "\n",
    "Since all these classes share the same baseclass we can trivially cast between them, e.g. we can cast the ``HoloMap`` to a ``GridSpace``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts GridSpace [plot_size=75] Curve [width=100]\n",
    "grid = hv.GridSpace(holomap)\n",
    "grid"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Similarly we can select just a few values and lay the data out in an ``NdLayout``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts Curve [width=500 height=200]\n",
    "hv.NdLayout(grid[20, 20:81]).cols(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Faceting by dimension"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts NdOverlay [width=600 legend_position='right']\n",
    "holomap.overlay('f_carrier')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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](./05-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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts Curve [width=200 height=200]\n",
    "holomap.overlay('f_carrier').grid('f_mod')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This ability to facet data in different ways becomes incredibly useful when working with [tabular](./07-Tabular_Datasets.ipynb) and [gridded](./07-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](./13-Data_Pipelines.ipynb), which visualize themselves and allow you to build complex interactive dashboards."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Methods"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Selecting by value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%opts Curve [width=400]\n",
    "holomap[20, 40] + holomap.select(f_carrier=20, f_mod=40)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more detail on indexing both Elements and containers see the [Indexing and Selecting user guide](./09-Indexing_and_Selecting_Data.ipynb)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Collapsing an n-dimensional container"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(grid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since this simply represents a multi-dimensional parameter space we can collapse this datastructure into a single table containing columns for each of the dimensions we have declared:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ds = hv.Dataset(grid.table())\n",
    "ds.data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Reindexing containers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "holomap.kdims"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the ``reindex`` method we can easily change the order of dimensions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "reindexed = holomap.reindex(['f_mod', 'f_carrier'])\n",
    "reindexed.kdims"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Add dimensions to a container"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_holomap = holomap.add_dimension('New dimension', dim_pos=0, dim_val=1)\n",
    "new_holomap.kdims"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see the 'New dimension' was added at position zero."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
