diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6ef15490..17bcab317c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +Version 1.9.0 +============= + +Changes affecting backwards compatibility: + +- The contours operation no longer overlays the contours on top of + the supplied Image by default and returns a single + Contours/Polygons rather than an NdOverlay of them + ([\#1991](https://github.com/ioam/holoviews/pull/1991)) + + Version 1.8.4 ============= diff --git a/doc/Tutorials/Introduction.ipynb b/doc/Tutorials/Introduction.ipynb index 30a70d2061..a9518add57 100644 --- a/doc/Tutorials/Introduction.ipynb +++ b/doc/Tutorials/Introduction.ipynb @@ -640,10 +640,9 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Contours.Red (color=Palette('Reds')) Contours.Green (color=Palette('Greens')) Contours.Blue (color=Palette('Blues'))\n", - "data = {lvl:(contours(chans.RedChannel.Macaw, levels=[lvl], group='Red') +\\\n", - " contours(chans.Channel.Green, levels=[lvl], group='Green') +\\\n", - " contours(chans.Channel.Blue, levels=[lvl], group='Blue'))\n", + "data = {lvl:(contours(chans.RedChannel.Macaw, levels=[lvl]).opts(style=dict(cmap='Reds')) +\\\n", + " contours(chans.Channel.Green, levels=[lvl]).opts(style=dict(cmap='Greens')) +\\\n", + " contours(chans.Channel.Blue, levels=[lvl]).opts(style=dict(cmap='Blues')))\n", " for lvl in np.linspace(0.1,0.9,9)}\n", "levels = hv.HoloMap(data, kdims=['Levels']).collate()\n", "levels" @@ -662,8 +661,8 @@ "metadata": {}, "outputs": [], "source": [ - "green05 = levels.Overlay.Green[0.5]\n", - "green05 + green05.Channel + green05.Channel.Green.sample(y=0.0)" + "green05 = levels.Contours.Green\n", + "green05 + chans.Channel.Green + chans.Channel.Green.sample(y=0.0)" ] }, { diff --git a/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb b/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb index 56d7193650..5633990fd9 100644 --- a/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb +++ b/examples/gallery/demos/bokeh/texas_choropleth_example.ipynb @@ -38,20 +38,11 @@ "from bokeh.sampledata.us_counties import data as counties\n", "from bokeh.sampledata.unemployment import data as unemployment\n", "\n", - "counties = {\n", - " code: county for code, county in counties.items() if county[\"state\"] == \"tx\"\n", - "}\n", + "counties = [dict(county, Unemployment=unemployment[cid])\n", + " for cid, county in counties.items()\n", + " if county[\"state\"] == \"tx\"]\n", "\n", - "county_xs = [county[\"lons\"] for county in counties.values()]\n", - "county_ys = [county[\"lats\"] for county in counties.values()]\n", - "\n", - "county_names = [county['name'] for county in counties.values()]\n", - "county_rates = [unemployment[county_id] for county_id in counties]\n", - "\n", - "county_polys = {name: hv.Polygons((xs, ys), level=rate, vdims=['Unemployment'])\n", - " for name, xs, ys, rate in zip(county_names, county_xs, county_ys, county_rates)}\n", - "\n", - "choropleth = hv.NdOverlay(county_polys, kdims=['County'])" + "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'])" ] }, { @@ -68,10 +59,11 @@ "outputs": [], "source": [ "plot_opts = dict(logz=True, tools=['hover'], xaxis=None, yaxis=None,\n", - " show_grid=False, show_frame=False, width=500, height=500)\n", + " show_grid=False, show_frame=False, width=500, height=500,\n", + " color_index='Unemployment', colorbar=True, toolbar='above')\n", "style = dict(line_color='white')\n", "\n", - "choropleth({'Polygons': {'style': style, 'plot': plot_opts}})" + "choropleth.opts(style=style, plot=plot_opts)" ] } ], diff --git a/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb b/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb index 8fb4e9d2c7..12f7c3cc96 100644 --- a/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb +++ b/examples/gallery/demos/matplotlib/texas_choropleth_example.ipynb @@ -39,20 +39,11 @@ "from bokeh.sampledata.us_counties import data as counties\n", "from bokeh.sampledata.unemployment import data as unemployment\n", "\n", - "counties = {\n", - " code: county for code, county in counties.items() if county[\"state\"] == \"tx\"\n", - "}\n", + "counties = [dict(county, Unemployment=unemployment[cid])\n", + " for cid, county in counties.items()\n", + " if county[\"state\"] == \"tx\"]\n", "\n", - "county_xs = [county[\"lons\"] for county in counties.values()]\n", - "county_ys = [county[\"lats\"] for county in counties.values()]\n", - "\n", - "county_names = [county['name'] for county in counties.values()]\n", - "county_rates = [unemployment[county_id] for county_id in counties]\n", - "\n", - "county_polys = {name: hv.Polygons((xs, ys), level=rate, vdims=['Unemployment'])\n", - " for name, xs, ys, rate in zip(county_names, county_xs, county_ys, county_rates)}\n", - "\n", - "choropleth = hv.NdOverlay(county_polys, kdims=['County'])" + "choropleth = hv.Polygons(counties, ['lons', 'lats'], [('detailed name', 'County'), 'Unemployment'])" ] }, { @@ -69,10 +60,11 @@ "outputs": [], "source": [ "plot_opts = dict(logz=True, xaxis=None, yaxis=None,\n", - " show_grid=False, show_frame=False, fig_size=200, bgcolor='white')\n", + " show_grid=False, show_frame=False, colorbar=True,\n", + " fig_size=200, color_index='Unemployment')\n", "style = dict(edgecolor='white')\n", "\n", - "choropleth({'Polygons': {'style': style, 'plot': plot_opts}})" + "choropleth.opts(style=style, plot=plot_opts)" ] } ], diff --git a/examples/getting_started/4-Gridded_Datasets.ipynb b/examples/getting_started/4-Gridded_Datasets.ipynb index a7f3e372dd..03907ecaba 100644 --- a/examples/getting_started/4-Gridded_Datasets.ipynb +++ b/examples/getting_started/4-Gridded_Datasets.ipynb @@ -158,7 +158,7 @@ "outputs": [], "source": [ "ROIs = data['ROIs']\n", - "roi_bounds = hv.NdOverlay({i: hv.Bounds(tuple(roi)) for i, roi in enumerate(ROIs)})\n", + "roi_bounds = hv.Path([hv.Bounds(tuple(roi)) for roi in ROIs])\n", "print(ROIs.shape)" ] }, @@ -176,7 +176,7 @@ "outputs": [], "source": [ "%%opts Image [width=400 height=400 xaxis=None yaxis=None] \n", - "%%opts Bounds (color='white') Text (text_color='white' text_font_size='8pt')\n", + "%%opts Path (color='white') Text (text_color='white' text_font_size='8pt')\n", "\n", "opts = dict(halign='left', valign='bottom')\n", "roi_text = hv.NdOverlay({i: hv.Text(roi[0], roi[1], str(i), **opts) for i, roi in enumerate(ROIs)})\n", diff --git a/examples/reference/elements/bokeh/Contours.ipynb b/examples/reference/elements/bokeh/Contours.ipynb index 63a4325903..718dffa8f0 100644 --- a/examples/reference/elements/bokeh/Contours.ipynb +++ b/examples/reference/elements/bokeh/Contours.ipynb @@ -28,7 +28,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Contours`` object is similar to ``Path`` object except it may be associated with a numeric value (the ``level``), which can be used to apply colormapping the ``Contours``. To see the effect of this we can create a number of ``Contours`` with varying shapes and ``level`` values. In this case we will create a number of concentric rings with increasing radii and level values and colormap the ``Contours`` with the viridis colormap:" + "A ``Contours`` object is similar to a ``Path`` element but allows each individual path to be associated with one or more scalar values declared as value dimensions (``vdims``), which can be used to apply colormapping the ``Contours``. Just like the ``Path`` element ``Contours`` will accept a list of arrays, dataframes, a dictionaries of columns (or any of the other literal formats including tuples of columns and lists of tuples). In order to efficiently represent the scalar values associated with each path the dictionary format is preferable since it can store the scalar values without expanding them into a whole column.\n", + "\n", + "To see the effect we will create a number of concentric rings with increasing radii and define a colormap to apply color the circles: " ] }, { @@ -37,20 +39,18 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Contours (cmap='viridis')\n", - "\n", - "def circle(radius, x=0, y=0):\n", + "def circle(radius):\n", " angles = np.linspace(0, 2*np.pi, 100)\n", - " return np.array(list(zip(x+radius*np.sin(angles), y+radius*np.cos(angles))))\n", + " return {'x': radius*np.sin(angles), 'y': radius*np.cos(angles), 'radius': radius}\n", "\n", - "hv.Overlay([hv.Contours([circle(i+0.05)], level=i) for i in np.linspace(0, 1, 10)])" + "hv.Contours([circle(i) for i in np.linspace(0, 1, 10)], vdims=['radius'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Often ``Contours`` will be directly computed from an underlying ``Image``, which is made easy using the ``contours`` operation. The operation accepts an ``Image`` type as input and will compute an ``NdOverlay`` containing a ``Contours`` Element for each of the specified ``levels``. We will declare an ``Image`` of sine rings\n", + "Often ``Contours`` will be directly computed from an underlying ``Image``, which is made easy using the ``contours`` operation. The operation accepts an ``Image`` type as input and will return ``Contours`` containing iso-contours for each of the specified ``levels``. We will declare an ``Image`` of sine rings\n", "and then compute ``Contours`` at 5 levels spaced linearly over the range of values in the Image:" ] }, @@ -60,12 +60,10 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Contours [show_legend=False colorbar=True width=325] (cmap='fire')\n", + "%%opts Contours [colorbar=True width=325 tools=['hover']] (cmap='fire')\n", "x,y = np.mgrid[-50:51, -50:51] * 0.05\n", "img = hv.Image(np.sin(x**2+y**3))\n", - "\n", - "z0, z1 = img.range('z')\n", - "img + hv.operation.contours(img, levels=np.linspace(z0, z1, 5), overlaid=False)" + "img + hv.operation.contours(img, levels=5)" ] } ], diff --git a/examples/reference/elements/bokeh/Path.ipynb b/examples/reference/elements/bokeh/Path.ipynb index 09e602e693..db3bc8b609 100644 --- a/examples/reference/elements/bokeh/Path.ipynb +++ b/examples/reference/elements/bokeh/Path.ipynb @@ -28,8 +28,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Path`` object is actually a collection of lines, which are all plotted with the same style. Unlike ``Curve`` where the y-axis is the dependent variable, a ``Path`` consists of lines connecting arbitrary points in two-dimensional space as is not expected to be a function.\n", - "\n" + "A ``Path`` object is actually a collection of lines, unlike ``Curve`` where the y-axis is the dependent variable, a ``Path`` consists of lines connecting arbitrary points in two-dimensional space. The individual subpaths should be supplied as a list and will be stored as NumPy arrays, DataFrames or dictionaries for each column, i.e. any of the formats accepted by columnar data formats.\n", + "\n", + "In this example we will create a Lissajous curve, which describe complex harmonic motion:" ] }, { @@ -39,19 +40,38 @@ "outputs": [], "source": [ "%%opts Path (color='black' line_width=4)\n", - "lin = np.linspace(-np.pi,np.pi,300)\n", "\n", - "def lissajous(t, a,b, delta):\n", - " return (np.sin(a * t + delta), np.sin(b * t))\n", + "lin = np.linspace(0, np.pi*2, 200)\n", + "\n", + "def lissajous(t, a, b, delta):\n", + " return (np.sin(a * t + delta), np.sin(b * t), t)\n", + "\n", + "hv.Path([lissajous(lin, 3, 5, np.pi/2)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "\n", - "hv.Path(lissajous(np.linspace(-np.pi, np.pi, 1000),3,5,np.pi/2))" + "If you looked carefully the ``lissajous`` function actually returns three columns, respectively for the x, y columns and a third column describing the point in time. By declaring a value dimension for that third column we can also color the Path by time. Since the value is cyclical we will also use a cyclic colormap (``'hsv'``) to represent this variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Path [color_index='time'] (line_width=4 cmap='hsv')\n", + "hv.Path([lissajous(lin, 3, 5, np.pi/2)], vdims=['time'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Unlike ``Curve`` as single ``Path`` element can contain multiple lines that are disconnected from each other which will all be plotted in the same style. Only by overlaying multiple ``Path`` objects do you iterate through the defined color cycle (or any other style options that have been defined). A ``Path`` is often useful to draw arbitrary annotations on top of an existing plot.\n", + "If we do not provide a ``color_index`` overlaid ``Path`` elements will cycle colors just like other elements do unlike ``Curve`` a single ``Path`` element can contain multiple lines that are disconnected from each other. A ``Path`` can therefore often useful to draw arbitrary annotations on top of an existing plot.\n", "\n", "A ``Path`` Element accepts multiple formats for specifying the paths, the simplest of which is passing a list of ``Nx2`` arrays of the x- and y-coordinates, alternative we can pass lists of coordinates. In this example we will create some coordinates representing rectangles and ellipses annotating an ``RGB`` image:" ] diff --git a/examples/reference/elements/bokeh/Polygons.ipynb b/examples/reference/elements/bokeh/Polygons.ipynb index 3addbee98b..8f16229e6a 100644 --- a/examples/reference/elements/bokeh/Polygons.ipynb +++ b/examples/reference/elements/bokeh/Polygons.ipynb @@ -28,9 +28,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Polygons`` object is similar to a ``Contours`` object except that each supplied path is closed and filled. Just like ``Contours``, an optional ``level`` value may be supplied; the Polygons will then be colored according to the supplied ``cmap``. Non-finite values such as ``np.NaN`` or ``np.inf`` will default to the supplied ``facecolor``.\n", + "A ``Polygons`` represents a contiguous filled area in a 2D space as a list of paths. Just like the ``Contours`` element additional scalar value dimensions maybe may be supplied, which can be used to color the ``Polygons`` with the defined ``cmap``. Like other ``Path`` types it accepts a list of arrays, dataframes, a dictionary of columns (or any of the other literal formats including tuples of columns and lists of tuples).\n", "\n", - "Polygons with values can be used to build heatmaps with arbitrary shapes." + "In order to efficiently represent the scalar values associated with each path the dictionary format is preferable since it can store the scalar values without expanding them into a whole column. Additionally it allows passing multiple columns as a single array by specifying the dimension names as a tuple.\n", + "\n", + "In this example we will create a list of random polygons each with an associated ``level`` value. Polygons will default to using the first value dimension as the ``color_index`` but for clarity we will define the ``color_index`` explicitly:" ] }, { @@ -39,19 +41,21 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Polygons (cmap='hot' line_color='black' line_width=2)\n", - "np.random.seed(35)\n", - "hv.Polygons([np.random.rand(4,2)], level=0.5) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=1.0) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=1.5) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=2.0)" + "%%opts Polygons [color_index='level'] (line_color='black' line_width=1)\n", + "np.random.seed(1)\n", + "\n", + "def rectangle(x=0, y=0, width=.05, height=.05):\n", + " return np.array([(x,y), (x+width, y), (x+width, y+height), (x, y+height)])\n", + "\n", + "hv.Polygons([{('x', 'y'): rectangle(x, y), 'level': z}\n", + " for x, y, z in np.random.rand(100, 3)], vdims=['level']).redim.range(x=(-.1,1.1), y=(-0.1, 1.1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "``Polygons`` without a value are useful as annotation, but also allow us to draw arbitrary shapes." + "``Polygons`` is a very versatile element which may be used to draw custom annotations, choropleth maps (as can be seen in the [texas_unemploment example](../../../gallery/demos/bokeh/texas_choropleth_example.ipynb)) among many other examples. We can also use some of the other path based annotations to quickly generate polygons, including ``Box``, ``Bounds`` and ``Ellipse`` elements. In the simple case we can simply pass a list of these elements:" ] }, { @@ -60,12 +64,24 @@ "metadata": {}, "outputs": [], "source": [ - "def rectangle(x=0, y=0, width=1, height=1):\n", - " return np.array([(x,y), (x+width, y), (x+width, y+height), (x, y+height)])\n", - "\n", - "(hv.Polygons([rectangle(width=2), rectangle(x=6, width=2)]).opts(style={'fill_color': '#a50d0d'})\n", - "* hv.Polygons([rectangle(x=2, height=2), rectangle(x=5, height=2)]).opts(style={'fill_color': '#ffcc00'})\n", - "* hv.Polygons([rectangle(x=3, height=2, width=2)]).opts(style={'fill_color': 'cyan'}))" + "hv.Polygons([hv.Box(i, i, i) for i in range(1, 10)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively we can use the ``array`` method to return the x/y-coordinates of the annotations and define additional z-values by declaring a dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Polygons([{('x', 'y'): hv.Box(0, 0, i).array(), 'z': i} for i in range(1, 10)[::-1]], vdims=['z']) +\\\n", + "hv.Polygons([{('x', 'y'): hv.Ellipse(0, 0, (i, i)).array(), 'z': i} for i in range(1, 10)[::-1]], vdims=['z'])" ] } ], diff --git a/examples/reference/elements/matplotlib/Contours.ipynb b/examples/reference/elements/matplotlib/Contours.ipynb index b79a9ce508..b2a65429f4 100644 --- a/examples/reference/elements/matplotlib/Contours.ipynb +++ b/examples/reference/elements/matplotlib/Contours.ipynb @@ -28,7 +28,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Contours`` object is similar to ``Path`` object except it may be associated with a numeric value (the ``level``), which can be used to apply colormapping the ``Contours``. To see the effect of this we can create a number of ``Contours`` with varying shapes and ``level`` values. In this case we will create a number of concentric rings with increasing radii and level values and colormap the ``Contours`` with the viridis colormap:" + "A ``Contours`` object is similar to a ``Path`` element but allows each individual path to be associated with one or more scalar values declared as value dimensions (``vdims``), which can be used to apply colormapping the ``Contours``. Just like the ``Path`` element ``Contours`` will accept a list of arrays, dataframes, a dictionaries of columns (or any of the other literal formats including tuples of columns and lists of tuples). In order to efficiently represent the scalar values associated with each path the dictionary format is preferable since it can store the scalar values without expanding them into a whole column.\n", + "\n", + "To see the effect we will create a number of concentric rings with increasing radii and define a colormap to apply color the circles: " ] }, { @@ -37,20 +39,18 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Contours (cmap='viridis')\n", - "\n", - "def circle(radius, x=0, y=0):\n", - " angles = np.linspace(0, 2*np.pi, 100)\n", - " return np.array(list(zip(x+radius*np.sin(angles), y+radius*np.cos(angles))))\n", + "def circle(radius):\n", + " angles = np.linspace(0, 2*np.pi, 50)\n", + " return {'x': radius*np.sin(angles), 'y': radius*np.cos(angles), 'radius': radius}\n", "\n", - "hv.Overlay([hv.Contours([circle(i+0.05)], level=i) for i in np.linspace(0, 1, 10)])" + "hv.Contours([circle(i) for i in np.linspace(0, 1, 10)], vdims=['radius'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Often ``Contours`` will be directly computed from an underlying ``Image``, which is made easy using the ``contours`` operation. The operation accepts an ``Image`` type as input and will compute an ``NdOverlay`` containing a ``Contours`` Element for each of the specified ``levels``. We will declare an ``Image`` of sine rings\n", + "Often ``Contours`` will be directly computed from an underlying ``Image``, which is made easy using the ``contours`` operation. The operation accepts an ``Image`` type as input and will return ``Contours`` containing iso-contours for each of the specified ``levels``. We will declare an ``Image`` of sine rings\n", "and then compute ``Contours`` at 5 levels spaced linearly over the range of values in the Image:" ] }, @@ -60,12 +60,12 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Contours [show_legend=False colorbar=True width=325] (cmap='fire')\n", + "%%opts Contours [colorbar=True] (cmap='fire')\n", "x,y = np.mgrid[-50:51, -50:51] * 0.05\n", "img = hv.Image(np.sin(x**2+y**3))\n", "\n", "z0, z1 = img.range('z')\n", - "img + hv.operation.contours(img, levels=np.linspace(z0, z1, 5), overlaid=False)" + "img + hv.operation.contours(img, levels=5)" ] } ], diff --git a/examples/reference/elements/matplotlib/Path.ipynb b/examples/reference/elements/matplotlib/Path.ipynb index 7867475cd1..b538f24079 100644 --- a/examples/reference/elements/matplotlib/Path.ipynb +++ b/examples/reference/elements/matplotlib/Path.ipynb @@ -28,8 +28,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Path`` object is actually a collection of lines, which are all plotted with the same style. Unlike ``Curve`` where the y-axis is the dependent variable, a ``Path`` consists of lines connecting arbitrary points in two-dimensional space as is not expected to be a function.\n", - "\n" + "A ``Path`` object is actually a collection of lines, unlike ``Curve`` where the y-axis is the dependent variable, a ``Path`` consists of lines connecting arbitrary points in two-dimensional space. The individual subpaths should be supplied as a list and will be stored as NumPy arrays, DataFrames or dictionaries for each column, i.e. any of the formats accepted by columnar data formats.\n", + "\n", + "In this example we will create a Lissajous curve, which describe complex harmonic motion:" ] }, { @@ -39,19 +40,38 @@ "outputs": [], "source": [ "%%opts Path (color='black' linewidth=4)\n", - "lin = np.linspace(-np.pi,np.pi,300)\n", "\n", - "def lissajous(t, a,b, delta):\n", - " return (np.sin(a * t + delta), np.sin(b * t))\n", + "lin = np.linspace(0, np.pi*2, 200)\n", + "\n", + "def lissajous(t, a, b, delta):\n", + " return (np.sin(a * t + delta), np.sin(b * t), t)\n", + "\n", + "hv.Path([lissajous(lin, 3, 5, np.pi/2)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "\n", - "hv.Path(lissajous(np.linspace(-np.pi, np.pi, 1000),3,5,np.pi/2))" + "If you looked carefully the ``lissajous`` function actually returns three columns, respectively for the x, y columns and a third column describing the point in time. By declaring a value dimension for that third column we can also color the Path by time. Since the value is cyclical we will also use a cyclic colormap (``'hsv'``) to represent this variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%opts Path [color_index='time'] (linewidth=4 cmap='hsv')\n", + "hv.Path([lissajous(lin, 3, 5, np.pi/2)], vdims=['time'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Unlike ``Curve`` as single ``Path`` element can contain multiple lines that are disconnected from each other which will all be plotted in the same style. Only by overlaying multiple ``Path`` objects do you iterate through the defined color cycle (or any other style options that have been defined). A ``Path`` is often useful to draw arbitrary annotations on top of an existing plot.\n", + "If we do not provide a ``color_index`` overlaid ``Path`` elements will cycle colors just like other elements do unlike ``Curve`` a single ``Path`` element can contain multiple lines that are disconnected from each other. A ``Path`` can therefore often useful to draw arbitrary annotations on top of an existing plot.\n", "\n", "A ``Path`` Element accepts multiple formats for specifying the paths, the simplest of which is passing a list of ``Nx2`` arrays of the x- and y-coordinates, alternative we can pass lists of coordinates. In this example we will create some coordinates representing rectangles and ellipses annotating an ``RGB`` image:" ] @@ -86,7 +106,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Path [fig_size=400 aspect=3]\n", + "%%opts Path [aspect=3 fig_size=300]\n", "N, NLINES = 100, 10\n", "hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :])) *\\\n", "hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :]))" diff --git a/examples/reference/elements/matplotlib/Polygons.ipynb b/examples/reference/elements/matplotlib/Polygons.ipynb index 62a3ec6935..7aea3feac9 100644 --- a/examples/reference/elements/matplotlib/Polygons.ipynb +++ b/examples/reference/elements/matplotlib/Polygons.ipynb @@ -28,9 +28,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Polygons`` object is similar to a ``Contours`` object except that each supplied path is closed and filled. Just like ``Contours``, an optional ``level`` value may be supplied; the Polygons will then be colored according to the supplied ``cmap``. Non-finite values such as ``np.NaN`` or ``np.inf`` will default to the supplied ``facecolor``.\n", + "A ``Polygons`` represents a contiguous filled area in a 2D space as a list of paths. Just like the ``Contours`` element additional scalar value dimensions maybe may be supplied, which can be used to color the ``Polygons`` with the defined ``cmap``. Like other ``Path`` types it accepts a list of arrays, dataframes, a dictionary of columns (or any of the other literal formats including tuples of columns and lists of tuples).\n", "\n", - "Polygons with values can be used to build heatmaps with arbitrary shapes." + "In order to efficiently represent the scalar values associated with each path the dictionary format is preferable since it can store the scalar values without expanding them into a whole column. Additionally it allows passing multiple columns as a single array by specifying the dimension names as a tuple.\n", + "\n", + "In this example we will create a list of random polygons each with an associated ``level`` value. Polygons will default to using the first value dimension as the ``color_index`` but for clarity we will define the ``color_index`` explicitly:" ] }, { @@ -39,19 +41,20 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Polygons (cmap='hot' edgecolor='black' linewidth=2)\n", - "np.random.seed(35)\n", - "hv.Polygons([np.random.rand(4,2)], level=0.5) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=1.0) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=1.5) *\\\n", - "hv.Polygons([np.random.rand(4,2)], level=2.0)" + "%%opts Polygons [color_index='level'] (linewidth=1)\n", + "\n", + "def rectangle(x=0, y=0, width=.05, height=.05):\n", + " return np.array([(x,y), (x+width, y), (x+width, y+height), (x, y+height)])\n", + "\n", + "hv.Polygons([{('x', 'y'): rectangle(x, y), 'level': z}\n", + " for x, y, z in np.random.rand(100, 3)], vdims=['level']).redim.range(x=(-.1,1.1), y=(-0.1, 1.1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "``Polygons`` without a value are useful as annotation, but also allow us to draw arbitrary shapes." + "``Polygons`` is a very versatile element which may be used to draw custom annotations, choropleth maps (as can be seen in the [texas_unemploment example](../../../gallery/demos/bokeh/texas_choropleth_example.ipynb)) among many other examples. We can also use some of the other path based annotations to quickly generate polygons, including ``Box``, ``Bounds`` and ``Ellipse`` elements. In the simple case we can simply pass a list of these elements:" ] }, { @@ -60,12 +63,24 @@ "metadata": {}, "outputs": [], "source": [ - "def rectangle(x=0, y=0, width=1, height=1):\n", - " return np.array([(x,y), (x+width, y), (x+width, y+height), (x, y+height)])\n", - "\n", - "(hv.Polygons([rectangle(width=2), rectangle(x=6, width=2)]).opts(style={'facecolor': '#a50d0d'})\n", - "* hv.Polygons([rectangle(x=2, height=2), rectangle(x=5, height=2)]).opts(style={'facecolor': '#ffcc00'})\n", - "* hv.Polygons([rectangle(x=3, height=2, width=2)]).opts(style={'facecolor': 'cyan'}))" + "hv.Polygons([hv.Box(i, i, i) for i in range(10)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively we can use the ``array`` method to return the x/y-coordinates of the annotations and define additional z-values by declaring a dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Polygons([{('x', 'y'): hv.Box(0, 0, i).array(), 'z': i} for i in range(10)[::-1]], vdims=['z']) +\\\n", + "hv.Polygons([{('x', 'y'): hv.Ellipse(0, 0, (i, i)).array(), 'z': i} for i in range(10)[::-1]], vdims=['z'])" ] } ], diff --git a/examples/user_guide/Exporting_and_Archiving.ipynb b/examples/user_guide/Exporting_and_Archiving.ipynb index 0a1fb5e965..305429875c 100644 --- a/examples/user_guide/Exporting_and_Archiving.ipynb +++ b/examples/user_guide/Exporting_and_Archiving.ipynb @@ -155,7 +155,7 @@ "source": [ "%%opts Contours (linewidth=1.3) Image (cmap=\"gray\")\n", "cs = contours(penguins[:,:,'R'], levels=[0.10,0.80])\n", - "cs" + "penguins[:, :, 'R'] * cs" ] }, { diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index ff03be3028..5a5c5f3b87 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -78,6 +78,13 @@ def validate(cls, dataset): raise ValueError("Supplied data does not match specified " "dimensions, expected at least %s columns." % ndims) + + @classmethod + def isscalar(cls, dataset, dim): + idx = dataset.get_dimension_index(dim) + return len(np.unique(dataset.data[:, idx])) == 1 + + @classmethod def array(cls, dataset, dimensions): if dimensions: diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 8350f8d9b8..9d386f5045 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -30,7 +30,9 @@ class DictInterface(Interface): @classmethod def dimension_type(cls, dataset, dim): name = dataset.get_dimension(dim, strict=True).name - return dataset.data[name].dtype.type + values = dataset.data[name] + return type(values) if np.isscalar(values) else values.dtype.type + @classmethod def init(cls, eltype, data, kdims, vdims): @@ -67,7 +69,8 @@ def init(cls, eltype, data, kdims, vdims): elif not any(isinstance(data, tuple(t for t in interface.types if t is not None)) for interface in cls.interfaces.values()): data = {k: v for k, v in zip(dimensions, zip(*data))} - elif isinstance(data, dict) and not all(d in data for d in dimensions): + elif (isinstance(data, dict) and not any(d in data or any(d in k for k in data + if isinstance(k, tuple)) for d in dimensions)): dict_data = sorted(data.items()) dict_data = zip(*((util.wrap_tuple(k)+util.wrap_tuple(v)) for k, v in dict_data)) @@ -76,8 +79,17 @@ def init(cls, eltype, data, kdims, vdims): if not isinstance(data, cls.types): raise ValueError("DictInterface interface couldn't convert data.""") elif isinstance(data, dict): - unpacked = [(d, vals if np.isscalar(vals) else np.asarray(vals)) - for d, vals in data.items()] + unpacked = [] + for d, vals in data.items(): + if isinstance(d, tuple): + vals = np.asarray(vals) + if not vals.ndim == 2 and vals.shape[1] == len(d): + raise ValueError("Values for %s dimensions did not have " + "the expected shape.") + for i, sd in enumerate(d): + unpacked.append((sd, vals[:, i])) + else: + unpacked.append((d, vals if np.isscalar(vals) else np.asarray(vals))) if not cls.expanded([d[1] for d in unpacked if not np.isscalar(d[1])]): raise ValueError('DictInterface expects data to be of uniform shape.') if isinstance(data, odict_types): @@ -111,13 +123,20 @@ def unpack_scalar(cls, dataset, data): if len(data[key]) == 1 and key in dataset.vdims: return data[key][0] + @classmethod + def isscalar(cls, dataset, dim): + name = dataset.get_dimension(dim, strict=True).name + values = dataset.data[name] + return np.isscalar(values) or len(np.unique(values)) == 1 + @classmethod def shape(cls, dataset): return cls.length(dataset), len(dataset.data), @classmethod def length(cls, dataset): - return max([len(vals) for vals in dataset.data.values() if not np.isscalar(vals)]) + lengths = [len(vals) for vals in dataset.data.values() if not np.isscalar(vals)] + return max(lengths) if lengths else 1 @classmethod def array(cls, dataset, dimensions): diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index ee57a3942f..83e2e990f5 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -81,6 +81,11 @@ def init(cls, eltype, data, kdims, vdims): return data, {'kdims':kdims, 'vdims':vdims}, {} + @classmethod + def isscalar(cls, dataset, dim): + return np.unique(cls.values(dataset, dim, expanded=False)) == 1 + + @classmethod def validate(cls, dataset): Interface.validate(dataset) diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index c4aabc598a..554b79dba2 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -86,8 +86,12 @@ class Interface(param.Parameterized): datatype = None + # Denotes whether the interface expects gridded data gridded = False + # Denotes whether the interface expects ragged data + multi = False + @classmethod def register(cls, interface): cls.interfaces[interface.datatype] = interface @@ -182,6 +186,11 @@ def expanded(cls, arrays): return not any(array.shape not in [arrays[0].shape, (1,)] for array in arrays[1:]) + @classmethod + def isscalar(cls, dataset, dim): + return cls.values(dataset, dim, expanded=False) == 1 + + @classmethod def select_mask(cls, dataset, selection): """ diff --git a/holoviews/core/data/multipath.py b/holoviews/core/data/multipath.py index cbc4a8776e..59fbf94c88 100644 --- a/holoviews/core/data/multipath.py +++ b/holoviews/core/data/multipath.py @@ -20,7 +20,9 @@ class MultiInterface(Interface): datatype = 'multitabular' - subtypes = ['dataframe', 'dictionary', 'array', 'dask'] + subtypes = ['dictionary', 'dataframe', 'array', 'dask'] + + multi = True @classmethod def init(cls, eltype, data, kdims, vdims): @@ -49,17 +51,13 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def validate(cls, dataset): - # Ensure that auxilliary key dimensions on each subpaths are scalar - if dataset.ndims <= 2: + if not dataset.data: return ds = cls._inner_dataset_template(dataset) for d in dataset.data: ds.data = d - for dim in dataset.kdims[2:]: - if len(ds.dimension_values(dim, expanded=False)) > 1: - raise ValueError("'%s' key dimension value must have a constant value on each subpath, " - "for paths with value for each coordinate of the array declare a " - "value dimension instead." % dim) + ds.interface.validate(ds) + @classmethod def _inner_dataset_template(cls, dataset): @@ -100,6 +98,20 @@ def range(cls, dataset, dim): ranges.append(ds.interface.range(ds, dim)) return max_range(ranges) + + @classmethod + def isscalar(cls, dataset, dim): + """ + Tests if dimension is scalar in each subpath. + """ + ds = cls._inner_dataset_template(dataset) + isscalar = [] + for d in dataset.data: + ds.data = d + isscalar.append(ds.interface.isscalar(ds, dim)) + return all(isscalar) + + @classmethod def select(cls, dataset, selection_mask=None, **selection): """ @@ -194,29 +206,47 @@ def values(cls, dataset, dimension, expanded, flat): didx = dataset.get_dimension_index(dimension) for d in dataset.data: ds.data = d - expand = expanded if didx>1 and dimension in dataset.kdims else True - dvals = ds.interface.values(ds, dimension, expand, flat) - values.append(dvals) - if expanded: + dvals = ds.interface.values(ds, dimension, expanded, flat) + if not len(dvals): + continue + elif expanded: + values.append(dvals) values.append([np.NaN]) - elif not expand and len(dvals): - values[-1] = dvals[0] + else: + values.append(dvals) if not values: return np.array() elif expanded: return np.concatenate(values[:-1]) else: - return np.array(values) + return np.concatenate(values) @classmethod - def split(cls, dataset, start, end): + def split(cls, dataset, start, end, datatype, **kwargs): """ Splits a multi-interface Dataset into regular Datasets using regular tabular interfaces. """ objs = [] - for d in dataset.data[start: end]: - objs.append(dataset.clone(d, datatype=cls.subtypes)) + if datatype is None: + for d in dataset.data[start: end]: + objs.append(dataset.clone(d, datatype=cls.subtypes)) + return objs + ds = cls._inner_dataset_template(dataset) + for d in dataset.data: + ds.data = d + if datatype == 'array': + obj = ds.array(**kwargs) + elif datatype == 'dataframe': + obj = ds.dframe(**kwargs) + elif datatype == 'columns': + if ds.interface.datatype == 'dictionary': + obj = dict(d) + else: + obj = ds.columns(**kwargs) + else: + raise ValueError("%s datatype not support" % datatype) + objs.append(obj) return objs diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 2252d15942..e587b0ea58 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -80,6 +80,12 @@ def init(cls, eltype, data, kdims, vdims): data = pd.DataFrame(data, columns=columns) return data, {'kdims':kdims, 'vdims':vdims}, {} + + @classmethod + def isscalar(cls, dataset, dim): + name = dataset.get_dimension(dim, strict=True).name + return len(dataset.data[name].unique()) == 1 + @classmethod def validate(cls, dataset): diff --git a/holoviews/element/comparison.py b/holoviews/element/comparison.py index 1c1165ccf2..f090b1d73a 100644 --- a/holoviews/element/comparison.py +++ b/holoviews/element/comparison.py @@ -464,7 +464,7 @@ def compare_text(cls, el1, el2, msg='Text'): @classmethod def compare_paths(cls, el1, el2, msg='Path'): - cls.compare_dimensioned(el1, el2) + cls.compare_dataset(el1, el2, msg) paths1 = el1.split() paths2 = el2.split() @@ -475,14 +475,10 @@ def compare_paths(cls, el1, el2, msg='Path'): @classmethod def compare_contours(cls, el1, el2, msg='Contours'): - if el1.level != el2.level: - raise cls.failureException("Contour levels are mismatched") cls.compare_paths(el1, el2, msg=msg) @classmethod def compare_polygons(cls, el1, el2, msg='Polygons'): - if el1.level != el2.level: - raise cls.failureException("Polygon levels are mismatched") cls.compare_paths(el1, el2, msg=msg) @classmethod diff --git a/holoviews/element/path.py b/holoviews/element/path.py index a252d31d3c..243b897b88 100644 --- a/holoviews/element/path.py +++ b/holoviews/element/path.py @@ -45,15 +45,15 @@ class Path(Dataset, Element2D): datatype = param.ObjectSelector(default=['multitabular']) def __init__(self, data, kdims=None, vdims=None, **params): - if isinstance(data, tuple): + if isinstance(data, tuple) and len(data) == 2: x, y = map(np.asarray, data) if y.ndim == 1: y = np.atleast_2d(y).T if len(x) != y.shape[0]: raise ValueError("Path x and y values must be the same length.") data = [np.column_stack((x, y[:, i])) for i in range(y.shape[1])] - elif isinstance(data, list) and all(isinstance(path, tuple) for path in data): - data = [np.column_stack(path) for path in data] + elif isinstance(data, list) and all(isinstance(path, Path) for path in data): + data = [p for path in data for p in path.data] super(Path, self).__init__(data, kdims=kdims, vdims=vdims, **params) def __setstate__(self, state): @@ -98,15 +98,25 @@ def collapse_data(cls, data_list, function=None, kdims=None, **kwargs): raise Exception("Path types are not uniformly sampled and" "therefore cannot be collapsed with a function.") - def split(self, start=None, end=None, paths=None): + def split(self, start=None, end=None, datatype=None, **kwargs): """ The split method allows splitting a Path type into a list of subpaths of the same type. A start and/or end may be supplied to select a subset of paths. """ - if not issubclass(self.interface, MultiInterface): - return [self] - return self.interface.split(self, start, end) + if not self.interface.multi: + if datatype == 'array': + obj = self.array(**kwargs) + elif datatype == 'dataframe': + obj = self.dframe(**kwargs) + elif datatype == 'columns': + obj = self.columns(**kwargs) + elif datatype is None: + obj = self + else: + raise ValueError("%s datatype not support" % datatype) + return [obj] + return self.interface.split(self, start, end, datatype, **kwargs) class Contours(Path): @@ -129,6 +139,9 @@ class Contours(Path): def __init__(self, data, kdims=None, vdims=None, **params): data = [] if data is None else data if params.get('level') is not None: + self.warning("The level parameter on %s elements is deprecated, " + "supply the value dimension(s) as columns in the data.", + type(self).__name__) vdims = vdims or [self._level_vdim] params['vdims'] = [] else: @@ -137,6 +150,10 @@ def __init__(self, data, kdims=None, vdims=None, **params): if params.get('level') is not None: self.vdims = [d if isinstance(d, Dimension) else Dimension(d) for d in vdims] + else: + all_scalar = all(self.interface.isscalar(self, vdim) for vdim in self.vdims) + if not all_scalar: + raise ValueError("All value dimensions on a Contours element must be scalar") def dimension_values(self, dim, expanded=True, flat=True): dimension = self.get_dimension(dim, strict=True) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 0209da19cf..08166edc29 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -191,8 +191,8 @@ def get_agg_data(cls, obj, category=None): dims = obj.dimensions()[:2] if isinstance(obj, Path): glyph = 'line' - for p in obj.split(): - paths.append(PandasInterface.as_dframe(p)) + for p in obj.split(datatype='dataframe'): + paths.append(p) elif isinstance(obj, CompositeOverlay): element = None for key, el in obj.data.items(): diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py index a98274b6c3..5f70052075 100644 --- a/holoviews/operation/element.py +++ b/holoviews/operation/element.py @@ -406,7 +406,7 @@ class contours(Operation): output_type = Overlay - levels = param.NumericTuple(default=(0.5,), doc=""" + levels = param.ClassSelector(default=10, class_=(list, int), doc=""" A list of scalar values used to specify the contour levels.""") group = param.String(default='Level', doc=""" @@ -415,22 +415,17 @@ class contours(Operation): filled = param.Boolean(default=False, doc=""" Whether to generate filled contours""") - overlaid = param.Boolean(default=True, doc=""" + overlaid = param.Boolean(default=False, doc=""" Whether to overlay the contour on the supplied Element.""") def _process(self, element, key=None): try: - from matplotlib import pyplot as plt + from matplotlib.contour import QuadContourSet + from matplotlib.axes import Axes + from matplotlib.figure import Figure except ImportError: raise ImportError("contours operation requires matplotlib.") - figure_handle = plt.figure() extent = element.range(0) + element.range(1)[::-1] - if self.p.filled: - contour_fn = plt.contourf - contour_type = Polygons - else: - contour_fn = plt.contour - contour_type = Contours if type(element) is Raster: data = [np.flipud(element.data)] @@ -440,19 +435,35 @@ def _process(self, element, key=None): data = (element.dimension_values(0, False), element.dimension_values(1, False), element.data[2]) - contour_set = contour_fn(*data, extent=extent, - levels=self.p.levels) - contours = NdOverlay(None, kdims=['Levels']) - for level, cset in zip(self.p.levels, contour_set.collections): - paths = [] - for path in cset.get_paths(): - paths.extend(np.split(path.vertices, np.where(path.codes==1)[0][1:])) - contours[level] = contour_type(paths, level=level, group=self.p.group, - label=element.label, kdims=element.kdims, - vdims=element.vdims) + if isinstance(self.p.levels, int): + levels = self.p.levels+1 if self.p.filled else self.p.levels + zmin, zmax = element.range(2) + levels = np.linspace(zmin, zmax, levels) + else: + levels = self.p.levels + + xdim, ydim = element.dimensions('key', label=True) + fig = Figure() + ax = Axes(fig, [0, 0, 1, 1]) + contour_set = QuadContourSet(ax, *data, filled=self.p.filled, extent=extent, levels=levels) + if self.p.filled: + contour_type = Polygons + levels = np.convolve(levels, np.ones((2,))/2, mode='valid') + else: + contour_type = Contours + vdims = element.vdims[:1] - plt.close(figure_handle) + paths = [] + for level, cset in zip(levels, contour_set.collections): + for path in cset.get_paths(): + if path.codes is None: + subpaths = [path.vertices] + else: + subpaths = np.split(path.vertices, np.where(path.codes==1)[0][1:]) + for p in subpaths: + paths.append({(xdim, ydim): p, element.vdims[0].name: level}) + contours = contour_type(paths, label=element.label, kdims=element.kdims, vdims=vdims) if self.p.overlaid: contours = element * contours return contours diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index c16f28214e..801a9d75f4 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -149,14 +149,15 @@ def colormap_generator(palette): options.VectorField = Options('style', color='black') # Paths -options.Contours = Options('style', color=Cycle()) if not config.style_17: options.Contours = Options('plot', show_legend=True) -options.Path = Options('style', color=Cycle()) +options.Contours = Options('style', color=Cycle(), cmap='viridis') +options.Path = Options('style', color=Cycle(), cmap='viridis') options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') -options.Polygons = Options('style', color=Cycle(), line_color='black') +options.Polygons = Options('style', color=Cycle(), line_color='black', + cmap='viridis') # Rasters options.Image = Options('style', cmap=dflt_cmap) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index b633d442ea..e70be8452b 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -12,11 +12,11 @@ from ...core.options import abbreviated_exception, SkipRendering from ...core.util import basestring, dimension_sanitizer from .chart import ColorbarPlot, PointPlot -from .element import CompositeElementPlot, line_properties, fill_properties, property_prefixes +from .element import CompositeElementPlot, LegendPlot, line_properties, fill_properties, property_prefixes from .util import mpl_to_bokeh, bokeh_version -class GraphPlot(CompositeElementPlot, ColorbarPlot): +class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" @@ -115,10 +115,10 @@ def get_data(self, element, ranges, style): end = np.array([node_indices.get(y, nan_node) for y in end], dtype=np.int32) path_data = dict(start=start, end=end) if element._edgepaths and not self.static_source: - edges = element._split_edgepaths.split() + edges = element._split_edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims) if len(edges) == len(start): - path_data['xs'] = [path.dimension_values(xidx) for path in edges] - path_data['ys'] = [path.dimension_values(yidx) for path in edges] + path_data['xs'] = [path[:, 0] for path in edges] + path_data['ys'] = [path[:, 1] for path in edges] else: self.warning('Graph edge paths do not match the number of abstract edges ' 'and will be skipped') diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index 805202b73a..7f3a6fe0ee 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -6,25 +6,32 @@ from bokeh.models import HoverTool, FactorRange from ...core import util -from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties +from .element import ColorbarPlot, line_properties, fill_properties from .util import expand_batched_style -class PathPlot(ElementPlot): +class PathPlot(ColorbarPlot): + + color_index = param.ClassSelector(default=None, class_=(util.basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") - style_opts = line_properties + style_opts = line_properties + ['cmap'] _plot_methods = dict(single='multi_line', batched='multi_line') _mapping = dict(xs='xs', ys='ys') _batched_style_opts = line_properties def _hover_opts(self, element): + cdim = element.get_dimension(self.color_index) if self.batched: - dims = list(self.hmap.last.kdims) + dims = list(self.hmap.last.kdims)+self.hmap.last.last.vdims else: dims = list(self.overlay_dims.keys())+self.hmap.last.vdims + if cdim not in dims and cdim is not None: + dims.append(cdim) return dims, {} @@ -42,33 +49,33 @@ def _get_hover_data(self, data, element): def get_data(self, element, ranges, style): - if self.static_source: - data = {} - else: - xidx, yidx = (1, 0) if self.invert_axes else (0, 1) - paths = [p.array(p.kdims[:2]) for p in element.split()] - xs, ys = ([path[:, idx] for path in paths] for idx in [xidx, yidx]) - data = dict(xs=xs, ys=ys) + cdim = element.get_dimension(self.color_index) + if cdim: cidx = element.get_dimension_index(cdim) + inds = (1, 0) if self.invert_axes else (0, 1) + mapping = dict(self._mapping) + if not cdim: + if self.static_source: + data = {} + else: + paths = element.split(datatype='array', dimensions=element.kdims) + xs, ys = ([path[:, idx] for path in paths] for idx in inds) + data = dict(xs=xs, ys=ys) + return data, mapping, style + + dim_name = util.dimension_sanitizer(cdim.name) + if not self.static_source: + paths, cvals = [], [] + for path in element.split(datatype='array'): + splits = [0]+list(np.where(np.diff(path[:, cidx])!=0)[0]+1) + for (s1, s2) in zip(splits[:-1], splits[1:]): + cvals.append(path[s1, cidx]) + paths.append(path[s1:s2+1, :2]) + xs, ys = ([path[:, idx] for path in paths] for idx in inds) + data = dict(xs=xs, ys=ys, **{dim_name: np.array(cvals)}) + cmapper = self._get_colormapper(cdim, element, ranges, style) + mapping['line_color'] = {'field': dim_name, 'transform': cmapper} self._get_hover_data(data, element) - return data, dict(self._mapping), style - - - def _categorize_data(self, data, cols, dims): - """ - Transforms non-string or integer types in datasource if the - axis to be plotted on is categorical. Accepts the column data - source data, the columns corresponding to the axes and the - dimensions for each axis, changing the data inplace. - """ - if self.invert_axes: - cols = cols[::-1] - dims = dims[:2][::-1] - ranges = [self.handles['%s_range' % ax] for ax in 'xy'] - for i, col in enumerate(cols): - column = data[col] - if (isinstance(ranges[i], FactorRange) and - (isinstance(column, list) or column.dtype.kind not in 'SU')): - data[col] = [[dims[i].pprint_value(v) for v in vals] for vals in column] + return data, mapping, style def get_batched_data(self, element, ranges=None): @@ -98,63 +105,77 @@ def get_batched_data(self, element, ranges=None): return data, elmapping, style + +class ContourPlot(PathPlot): -class ContourPlot(ColorbarPlot, PathPlot): - - style_opts = line_properties + ['cmap'] - - def get_data(self, element, ranges, style): - data, mapping, style = super(ContourPlot, self).get_data(element, ranges, style) - ncontours = len(list(data.values())[0]) - if element.vdims and element.level is not None: - cdim = element.vdims[0] - dim_name = util.dimension_sanitizer(cdim.name) - if 'cmap' in style or any(isinstance(t, HoverTool) for t in self.state.tools): - data[dim_name] = np.full(ncontours, float(element.level)) - if 'cmap' in style: - cmapper = self._get_colormapper(cdim, element, ranges, style) - mapping['line_color'] = {'field': dim_name, 'transform': cmapper} - return data, mapping, style - - -class PolygonPlot(ColorbarPlot, PathPlot): - - style_opts = ['cmap', 'palette'] + line_properties + fill_properties - _plot_methods = dict(single='patches', batched='patches') - _style_opts = ['color', 'cmap', 'palette'] + line_properties + fill_properties - _batched_style_opts = line_properties + fill_properties + color_index = param.ClassSelector(default=0, class_=(util.basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") + _color_style = 'line_color' + def _hover_opts(self, element): if self.batched: - dims = list(self.hmap.last.kdims) + dims = list(self.hmap.last.kdims)+self.hmap.last.last.vdims else: - dims = list(self.overlay_dims.keys()) - dims += element.vdims + dims = list(self.overlay_dims.keys())+self.hmap.last.vdims return dims, {} + + def _get_hover_data(self, data, element): + """ + Initializes hover data based on Element dimension values. + If empty initializes with no data. + """ + if not any(isinstance(t, HoverTool) for t in self.state.tools) or self.static_source: + return + + for d in element.vdims: + dim = util.dimension_sanitizer(d.name) + if dim not in data: + data[dim] = element.dimension_values(d, expanded=False) + elif isinstance(data[dim], np.ndarray) and data[dim].dtype.kind == 'M': + data[dim+'_dt_strings'] = [d.pprint_value(v) for v in data[dim]] + + for k, v in self.overlay_dims.items(): + dim = util.dimension_sanitizer(k.name) + if dim not in data: + data[dim] = [v for _ in range(len(list(data.values())[0]))] def get_data(self, element, ranges, style): + paths = element.split(datatype='array', dimensions=element.kdims) if self.static_source: - data = {} + data = dict() else: - paths = [p.array(p.kdims[:2]) for p in element.split()] - xs = [path[:, 0] for path in paths] - ys = [path[:, 1] for path in paths] - data = dict(xs=ys, ys=xs) if self.invert_axes else dict(xs=xs, ys=ys) - + inds = (1, 0) if self.invert_axes else (0, 1) + xs, ys = ([path[:, idx] for path in paths] for idx in inds) + data = dict(xs=xs, ys=ys) mapping = dict(self._mapping) - if element.vdims and element.level is not None: - cdim = element.vdims[0] - dim_name = util.dimension_sanitizer(cdim.name) - data[dim_name] = [element.level for _ in range(len(xs))] - cmapper = self._get_colormapper(cdim, element, ranges, style) - mapping['fill_color'] = {'field': dim_name, - 'transform': cmapper} - - if any(isinstance(t, HoverTool) for t in self.state.tools) and not self.static_source: - dim_name = util.dimension_sanitizer(element.vdims[0].name) - for k, v in self.overlay_dims.items(): - dim = util.dimension_sanitizer(k.name) - data[dim] = [v for _ in range(len(xs))] - data[dim_name] = [element.level for _ in range(len(xs))] + if None not in [element.level, self.color_index] and element.vdims: + cdim = element.vdims[0] + else: + cidx = self.color_index+2 if isinstance(self.color_index, int) else self.color_index + cdim = element.get_dimension(cidx) + if cdim is None: + return data, mapping, style + + ncontours = len(paths) + dim_name = util.dimension_sanitizer(cdim.name) + if element.level is not None: + values = np.full(ncontours, float(element.level)) + else: + values = element.dimension_values(cdim, expanded=False) + data[dim_name] = values + factors = list(np.unique(values)) if values.dtype.kind in 'SUO' else None + cmapper = self._get_colormapper(cdim, element, ranges, style, factors) + mapping[self._color_style] = {'field': dim_name, 'transform': cmapper} + self._get_hover_data(data, element) return data, mapping, style + + +class PolygonPlot(ContourPlot): + + style_opts = ['cmap'] + line_properties + fill_properties + _plot_methods = dict(single='patches', batched='patches') + _batched_style_opts = line_properties + fill_properties + _color_style = 'fill_color' diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 0633e133b1..fe547e480f 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -238,9 +238,9 @@ def grid_selector(grid): options.Text = Options('style', fontsize=13) options.Arrow = Options('style', color='k', linewidth=2, fontsize=13) # Paths -options.Contours = Options('style', color=Cycle()) +options.Contours = Options('style', color=Cycle(), cmap='viridis') options.Contours = Options('plot', show_legend=True) -options.Path = Options('style', color=Cycle()) +options.Path = Options('style', color=Cycle(), cmap='viridis') if config.style_17: options.Box = Options('style', color=Cycle()) @@ -250,7 +250,8 @@ def grid_selector(grid): options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') - options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black') + options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black', + cmap='viridis') # Interface options.TimeSeries = Options('style', color=Cycle()) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 682305edfb..93415f587c 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -205,7 +205,7 @@ def _finalize_axis(self, key, element=None, title=None, dimensions=None, ranges= self._set_aspect(axis, self.aspect) if not subplots and not self.drawn: - self._finalize_artist(key) + self._finalize_artist(element) for hook in self.finalize_hooks: try: @@ -590,7 +590,7 @@ def _adjust_cbar(self, cbar, label, dim): cbar.set_ticklabels(labels) - def _finalize_artist(self, key): + def _finalize_artist(self, element): artist = self.handles.get('artist', None) if artist and self.colorbar: self._draw_colorbar() @@ -781,9 +781,9 @@ def __init__(self, overlay, ranges=None, **params): super(OverlayPlot, self).__init__(overlay, ranges=ranges, **params) - def _finalize_artist(self, key): + def _finalize_artist(self, element): for subplot in self.subplots.values(): - subplot._finalize_artist(key) + subplot._finalize_artist(element) def _adjust_legend(self, overlay, axis): """ diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 8732f557ee..ac9b176800 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -45,7 +45,7 @@ def get_data(self, element, ranges, style): dims = element.nodes.dimensions() self._compute_styles(element.nodes, ranges, style) - paths = element.edgepaths.data + paths = element.edgepaths.split(datatype='array', dimensions=element.edgepaths.kdims) if self.invert_axes: paths = [p[:, ::-1] for p in paths] return {'nodes': (pxs, pys), 'edges': paths}, style, {'dimensions': dims} diff --git a/holoviews/plotting/mpl/path.py b/holoviews/plotting/mpl/path.py index 5e17dc4d7d..8781a769e4 100644 --- a/holoviews/plotting/mpl/path.py +++ b/holoviews/plotting/mpl/path.py @@ -1,23 +1,45 @@ from matplotlib.patches import Polygon -from matplotlib.collections import PatchCollection, LineCollection +from matplotlib.collections import PolyCollection, LineCollection import numpy as np import param +from ...core import util from .element import ElementPlot, ColorbarPlot -class PathPlot(ElementPlot): +class PathPlot(ColorbarPlot): - aspect = param.Parameter(default='equal', doc=""" + aspect = param.Parameter(default='square', doc=""" PathPlots axes usually define single space so aspect of Paths follows aspect in data coordinates by default.""") + + color_index = param.ClassSelector(default=None, class_=(util.basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") - style_opts = ['alpha', 'color', 'linestyle', 'linewidth', 'visible'] + style_opts = ['alpha', 'color', 'linestyle', 'linewidth', 'visible', 'cmap'] + + def _finalize_artist(self, element): + if self.colorbar: + self._draw_colorbar(element.get_dimension(self.color_index)) def get_data(self, element, ranges, style): - paths = [p.array(p.kdims[:2]) for p in element.split()] - if self.invert_axes: - paths = [p[:, ::-1] for p in paths] + cdim = element.get_dimension(self.color_index) + if cdim: cidx = element.get_dimension_index(cdim) + if not cdim: + paths = element.split(datatype='array', dimensions=element.kdims) + if self.invert_axes: + paths = [p[:, ::-1] for p in paths] + return (paths,), style, {} + paths, cvals = [], [] + for path in element.split(datatype='array'): + splits = [0]+list(np.where(np.diff(path[:, cidx])!=0)[0]+1) + for (s1, s2) in zip(splits[:-1], splits[1:]): + cvals.append(path[s1, cidx]) + paths.append(path[s1:s2+1, :2]) + self._norm_kwargs(element, ranges, style, cdim) + style['array'] = np.array(cvals) + style['clim'] = style.pop('vmin', None), style.pop('vmax', None) return (paths,), style, {} def init_artists(self, ax, plot_args, plot_kwargs): @@ -29,34 +51,50 @@ def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] data, style, axis_kwargs = self.get_data(element, ranges, style) artist.set_paths(data[0]) + if 'array' in style: + artist.set_array(style['array']) + artist.set_clim(style['clim']) + if 'norm' in style: + artist.set_norm(style['norm']) artist.set_visible(style.get('visible', True)) return axis_kwargs -class ContourPlot(PathPlot, ColorbarPlot): +class ContourPlot(PathPlot): - style_opts = PathPlot.style_opts + ['cmap'] + color_index = param.ClassSelector(default=0, class_=(util.basestring, int), + allow_None=True, doc=""" + Index of the dimension from which the color will the drawn""") - def get_data(self, element, ranges, style): - args, style, axis_kwargs = super(ContourPlot, self).get_data(element, ranges, style) - value = element.level - if element.vdims and value is not None and np.isfinite(value) and 'cmap' in style: - self._norm_kwargs(element, ranges, style, element.vdims[0]) - style['clim'] = style.pop('vmin'), style.pop('vmax') - style['array'] = np.array([value]*len(args[0])) - return args, style, axis_kwargs + def _finalize_artist(self, element): + if self.colorbar: + cidx = self.color_index+2 if isinstance(self.color_index, int) else self.color_index + cdim = element.get_dimension(cidx) + self._draw_colorbar(cdim) + def get_data(self, element, ranges, style): + if None not in [element.level, self.color_index]: + cdim = element.vdims[0] + else: + cidx = self.color_index+2 if isinstance(self.color_index, int) else self.color_index + cdim = element.get_dimension(cidx) + paths = element.split(datatype='array', dimensions=element.kdims) + if self.invert_axes: + paths = [p[:, ::-1] for p in paths] - def update_handles(self, key, axis, element, ranges, style): - artist = self.handles['artist'] - axis_kwargs = super(ContourPlot, self).update_handles(key, axis, element, ranges, style) - if 'array' in style: - artist.set_array(style['array']) - artist.set_clim(style['clim']) - return axis_kwargs + if cdim is None: + return (paths,), style, {} + if element.level is not None: + style['array'] = np.full(len(paths), element.level) + else: + style['array'] = element.dimension_values(cdim, expanded=False) + self._norm_kwargs(element, ranges, style, cdim) + style['clim'] = style.pop('vmin'), style.pop('vmax') + return (paths,), style, {} -class PolygonPlot(ColorbarPlot): + +class PolygonPlot(ContourPlot): """ PolygonPlot draws the polygon paths in the supplied Polygons object. If the Polygon has an associated value the color of @@ -71,43 +109,7 @@ class PolygonPlot(ColorbarPlot): style_opts = ['alpha', 'cmap', 'facecolor', 'edgecolor', 'linewidth', 'hatch', 'linestyle', 'joinstyle', 'fill', 'capstyle'] - - def get_data(self, element, ranges, style): - value = element.level - vdim = element.vdims[0] if element.vdims else None - polys = [] - paths = [p.array(p.kdims[:2]) for p in element.split()] - for segments in paths: - if segments.shape[0]: - if self.invert_axes: - segments = segments[:, ::-1] - polys.append(Polygon(segments)) - - if None not in [value, vdim] and np.isfinite(value): - self._norm_kwargs(element, ranges, style, vdim) - style['clim'] = style.pop('vmin'), style.pop('vmax') - style['array'] = np.array([value]*len(polys)) - return (polys,), style, {} - def init_artists(self, ax, plot_args, plot_kwargs): - collection = PatchCollection(*plot_args, **plot_kwargs) - ax.add_collection(collection) - if self.colorbar: - self._draw_colorbar() - return {'artist': collection, 'polys': plot_args[0]} - - - def update_handles(self, key, axis, element, ranges, style): - value = element.level - vdim = element.vdims[0] if element.vdims else None - collection = self.handles['artist'] - paths = [p.array(p.kdims[:2]) for p in element.split()] - if any(not np.array_equal(data, poly.get_xy()) for data, poly in - zip(paths, self.handles['polys'])): - return super(PolygonPlot, self).update_handles(key, axis, element, ranges, style) - elif None not in [value, vdim] and np.isfinite(value): - self._norm_kwargs(element, ranges, style, vdim) - collection.set_array(np.array([value]*len(element.data))) - collection.set_clim((style['vmin'], style['vmax'])) - if 'norm' in style: - collection.norm = style['norm'] + polys = PolyCollection(*plot_args, **plot_kwargs) + ax.add_collection(polys) + return {'artist': polys} diff --git a/tests/testcomparisonpath.py b/tests/testcomparisonpath.py index c52711350e..928c9b7d08 100644 --- a/tests/testcomparisonpath.py +++ b/tests/testcomparisonpath.py @@ -40,7 +40,7 @@ def test_paths_unequal(self): try: self.assertEqual(self.path1, self.path2) except AssertionError as e: - if not str(e).startswith("Path data not almost equal to 6 decimals"): + if not str(e).startswith("Path not almost equal to 6 decimals"): raise self.failureException("Path mismatch error not raised.") def test_contours_equal(self): @@ -50,14 +50,14 @@ def test_contours_unequal(self): try: self.assertEqual(self.contours1, self.contours2) except AssertionError as e: - if not str(e).startswith("Contours data not almost equal to 6 decimals"): + if not str(e).startswith("Contours not almost equal to 6 decimals"): raise self.failureException("Contours mismatch error not raised.") def test_contour_levels_unequal(self): try: self.assertEqual(self.contours1, self.contours3) except AssertionError as e: - if not str(e).startswith("Contour levels are mismatched"): + if not str(e).startswith("Contours not almost equal to 6 decimals"): raise self.failureException("Contour level are mismatch error not raised.") @@ -68,7 +68,7 @@ def test_bounds_unequal(self): try: self.assertEqual(self.bounds1, self.bounds2) except AssertionError as e: - if not str(e).startswith("Bounds data not almost equal to 6 decimals"): + if not str(e).startswith("Bounds not almost equal to 6 decimals"): raise self.failureException("Bounds mismatch error not raised.") def test_boxs_equal(self): @@ -78,7 +78,7 @@ def test_boxs_unequal(self): try: self.assertEqual(self.box1, self.box2) except AssertionError as e: - if not str(e).startswith("Box data not almost equal to 6 decimals"): + if not str(e).startswith("Box not almost equal to 6 decimals"): raise self.failureException("Box mismatch error not raised.") def test_ellipses_equal(self): @@ -88,5 +88,5 @@ def test_ellipses_unequal(self): try: self.assertEqual(self.ellipse1, self.ellipse2) except AssertionError as e: - if not str(e).startswith("Ellipse data not almost equal to 6 decimals"): + if not str(e).startswith("Ellipse not almost equal to 6 decimals"): raise self.failureException("Ellipse mismatch error not raised.") diff --git a/tests/testmultiinterface.py b/tests/testmultiinterface.py index 8934875a80..25f83ff4b7 100644 --- a/tests/testmultiinterface.py +++ b/tests/testmultiinterface.py @@ -78,7 +78,7 @@ def test_multi_array_values(self): def test_multi_array_values_coordinates_nonexpanded(self): arrays = [np.column_stack([np.arange(i, i+2), np.arange(i, i+2)]) for i in range(2)] mds = Path(arrays, kdims=['x', 'y'], datatype=['multitabular']) - self.assertEqual(mds.dimension_values(0, expanded=False), np.array([[0., 1], [1, 2]])) + self.assertEqual(mds.dimension_values(0, expanded=False), np.array([0., 1, 1, 2])) def test_multi_array_values_coordinates_nonexpanded_constant_kdim(self): arrays = [np.column_stack([np.arange(i, i+2), np.arange(i, i+2), np.ones(2)*i]) for i in range(2)] @@ -101,15 +101,6 @@ def test_multi_mixed_interface_raises(self): def test_multi_mixed_dims_raises(self): arrays = [{'x': range(10), 'y' if j else 'z': range(10)} for i in range(2) for j in range(2)] - error = "None of the available storage backends were able to support the supplied data format." + error = "Following dimensions not found in data: \['y'\]" with self.assertRaisesRegexp(ValueError, error): mds = Path(arrays, kdims=['x', 'y'], datatype=['multitabular']) - - def test_multi_nonconstant_kdims_raises(self): - arrays = [{'x': range(10), 'y': range(10), 'z': range(10)} - for i in range(2)] - error = ("z' key dimension value must have a constant value on each subpath, " - "for paths with value for each coordinate of the array declare a " - "value dimension instead.") - with self.assertRaisesRegexp(ValueError, error): - mds = Path(arrays, kdims=['x', 'y', 'z'], datatype=['multitabular']) diff --git a/tests/testoperation.py b/tests/testoperation.py index fe36511fa2..fe5aae3cf4 100644 --- a/tests/testoperation.py +++ b/tests/testoperation.py @@ -56,21 +56,20 @@ def test_image_gradient(self): @attr(optional=1) # Requires matplotlib def test_image_contours(self): img = Image(np.array([[0, 1, 0], [3, 4, 5.], [6, 7, 8]])) - op_contours = contours(img) - ndoverlay = NdOverlay(None, kdims=['Levels']) - ndoverlay[0.5] = Contours([[(-0.5, 0.416667), (-0.25, 0.5)], [(0.25, 0.5), (0.5, 0.45)]], - group='Level', level=0.5, vdims=img.vdims) - self.assertEqual(op_contours, img*ndoverlay) + op_contours = contours(img, levels=[0.5]) + contour = Contours([[(-0.5, 0.416667, 0.5), (-0.25, 0.5, 0.5)], + [(0.25, 0.5, 0.5), (0.5, 0.45, 0.5)]], + vdims=img.vdims) + self.assertEqual(op_contours, contour) @attr(optional=1) # Requires matplotlib def test_image_contours_filled(self): img = Image(np.array([[0, 1, 0], [3, 4, 5.], [6, 7, 8]])) op_contours = contours(img, filled=True, levels=[2, 2.5]) - ndoverlay = NdOverlay(None, kdims=['Levels']) - data = [[(0., 0.333333), (0.5, 0.3), (0.5, 0.25), (0., 0.25), - (-0.5, 0.08333333), (-0.5, 0.16666667), (0., 0.33333333)]] - ndoverlay[0.5] = Polygons(data, group='Level', level=2, vdims=img.vdims) - self.assertEqual(op_contours, img*ndoverlay) + data = [[(0., 0.333333, 2.25), (0.5, 0.3, 2.25), (0.5, 0.25, 2.25), (0., 0.25, 2.25), + (-0.5, 0.08333333, 2.25), (-0.5, 0.16666667, 2.25), (0., 0.33333333, 2.25)]] + polys = Polygons(data, vdims=img.vdims) + self.assertEqual(op_contours, polys) def test_points_histogram(self): points = Points([float(i) for i in range(10)])