Skip to content

Commit

Permalink
Merge eb86493 into fdc61e3
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Dec 8, 2018
2 parents fdc61e3 + eb86493 commit 5485baf
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 43 deletions.
25 changes: 13 additions & 12 deletions examples/reference/elements/bokeh/Path.ipynb
Expand Up @@ -21,14 +21,16 @@
"source": [
"import numpy as np\n",
"import holoviews as hv\n",
"from holoviews import opts\n",
"\n",
"hv.extension('bokeh')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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. For a full description of the path geometry data model see the [Geometry Data User Guide](../user_guide/Geometry_Data.ipynb). \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. ``Path`` supports plotting an individual line or multiple subpaths, which should be supplied as a list. Each path should be defined in a columnar format such as NumPy arrays, DataFrames or dictionaries for each column. For a full description of the path geometry data model see the [Geometry Data User Guide](../user_guide/Geometry_Data.ipynb).\n",
"\n",
"In this example we will create a Lissajous curve, which describe complex harmonic motion:"
]
Expand All @@ -39,14 +41,12 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path (color='black' line_width=4)\n",
"\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)])"
"hv.Path([lissajous(lin, 3, 5, np.pi/2)]).opts(color='black', line_width=4)"
]
},
{
Expand All @@ -63,15 +63,15 @@
"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')"
"hv.Path([lissajous(lin, 3, 5, np.pi/2)], vdims='time').opts(\n",
" cmap='hsv', color='time', line_width=4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"If we do not provide a ``color`` 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:"
]
Expand All @@ -82,15 +82,15 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path (line_width=4)\n",
"angle = np.linspace(0, 2*np.pi, 100)\n",
"baby = list(zip(0.15*np.sin(angle), 0.2*np.cos(angle)-0.2))\n",
"\n",
"adultR = [(0.25, 0.45), (0.35,0.35), (0.25, 0.25), (0.15, 0.35), (0.25, 0.45)]\n",
"adultL = [(-0.3, 0.4), (-0.3, 0.3), (-0.2, 0.3), (-0.2, 0.4),(-0.3, 0.4)]\n",
"scene = hv.RGB.load_image('../assets/penguins.png')\n",
"\n",
"scene * hv.Path([adultL, adultR, baby]) * hv.Path([baby])"
"(scene * hv.Path([adultL, adultR, baby]) * hv.Path([baby])).opts(\n",
" opts.Path(line_width=4))"
]
},
{
Expand All @@ -106,10 +106,11 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path [width=600]\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, :]))"
"paths = hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :]))\n",
"paths2 = hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :]))\n",
"\n",
"(paths * paths2).opts(width=600)"
]
},
{
Expand Down
26 changes: 14 additions & 12 deletions examples/reference/elements/matplotlib/Path.ipynb
Expand Up @@ -21,14 +21,16 @@
"source": [
"import numpy as np\n",
"import holoviews as hv\n",
"from holoviews import opts\n",
"\n",
"hv.extension('matplotlib')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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. For a full description of the path geometry data model see the [Geometry Data User Guide](../user_guide/Geometry_Data.ipynb). \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. ``Path`` supports plotting an individual line or multiple subpaths, which should be supplied as a list. Each path should be defined in a columnar format such as NumPy arrays, DataFrames or dictionaries for each column. For a full description of the path geometry data model see the [Geometry Data User Guide](../user_guide/Geometry_Data.ipynb).\n",
"\n",
"In this example we will create a Lissajous curve, which describe complex harmonic motion:"
]
Expand All @@ -39,14 +41,12 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path (color='black' linewidth=4)\n",
"\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)])"
"hv.Path([lissajous(lin, 3, 5, np.pi/2)]).opts(color='black', linewidth=4)"
]
},
{
Expand All @@ -63,15 +63,15 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path [color_index='time'] (linewidth=4 cmap='hsv')\n",
"hv.Path([lissajous(lin, 3, 5, np.pi/2)], vdims='time')"
"hv.Path([lissajous(lin, 3, 5, np.pi/2)], vdims='time').opts(\n",
" cmap='hsv', color='time', linewidth=4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"If we do not provide a ``color`` 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:"
]
Expand All @@ -82,15 +82,16 @@
"metadata": {},
"outputs": [],
"source": [
"%%opts Path (linewidth=4)\n",
"angle = np.linspace(0, 2*np.pi, 100)\n",
"baby = list(zip(0.15*np.sin(angle), 0.2*np.cos(angle)-0.2))\n",
"\n",
"adultR = [(0.25, 0.45), (0.35,0.35), (0.25, 0.25), (0.15, 0.35), (0.25, 0.45)]\n",
"adultL = [(-0.3, 0.4), (-0.3, 0.3), (-0.2, 0.3), (-0.2, 0.4),(-0.3, 0.4)]\n",
"scene = hv.RGB.load_image('../assets/penguins.png')\n",
"\n",
"scene * hv.Path([adultL, adultR, baby]) * hv.Path([baby])"
"(scene * hv.Path([adultL, adultR]) * hv.Path(baby)).opts(\n",
" opts.Path(linewidth=4)\n",
")"
]
},
{
Expand All @@ -106,10 +107,11 @@
"metadata": {},
"outputs": [],
"source": [
"%%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, :]))"
"paths = hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :]))\n",
"paths2 = hv.Path((np.arange(N), np.random.rand(N, NLINES) + np.arange(NLINES)[np.newaxis, :]))\n",
"\n",
"(paths * paths2).opts(aspect=3, fig_size=300)"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions examples/user_guide/Geometry_Data.ipynb
Expand Up @@ -29,7 +29,7 @@
"\n",
"The ``Path`` element represents a collection of path geometries with optional associated values. Each path geometry may be split into sub-geometries on NaN-values and may be associated with scalar values or array values varying along its length. In analogy to GEOS geometry types a Path is a collection of LineString and MultiLineString geometries with associated values.\n",
"\n",
"While many different formats are accepted in theory, natively HoloViews provides support for representing paths as lists of regular columnar data objects including arrays, dataframes and dictionaries of column arrays and scalars. A simple path geometry may therefore be drawn using:"
"While other formats can be supported through extensible interfaces (e.g. geopandas and shapely objects in GeoViews), natively HoloViews provides support for representing paths as one or more columnar data-structures including arrays, dataframes and dictionaries of column arrays and scalars. A simple path geometry may therefore be drawn using:"
]
},
{
Expand All @@ -38,14 +38,14 @@
"metadata": {},
"outputs": [],
"source": [
"hv.Path([{'x': [1, 2, 3, 4, 5], 'y': [0, 0, 1, 1, 2]}]).options(padding=0.1)"
"hv.Path({'x': [1, 2, 3, 4, 5], 'y': [0, 0, 1, 1, 2]}).options(padding=0.1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here the dictionary of x- and y-coordinates could also be an NumPy array with two columns or a dataframe with 'x' and 'y' columns. Since the format supports lists any number of geometries may be drawn in this way. Additionally, it is also possible to associate a value with each path by declaring it as a value dimension:"
"Here the dictionary of x- and y-coordinates could also be an NumPy array with two columns or a dataframe with 'x' and 'y' columns. To draw multiple paths the data-structures can be wrapped in a list. Additionally, it is also possible to associate a value with each path by declaring it as a value dimension:"
]
},
{
Expand Down
65 changes: 50 additions & 15 deletions holoviews/element/path.py
Expand Up @@ -11,13 +11,13 @@
from ..core import Element2D, Dataset
from ..core.data import MultiInterface
from ..core.dimension import Dimension, asdim
from ..core.util import config, disable_constant
from ..core.util import config, disable_constant, isscalar
from .geom import Geometry


class Path(Geometry):
"""
The Path element represents a collection of path geometries with
The Path element represents one or more of path geometries with
associated values. Each path geometry may be split into
sub-geometries on NaN-values and may be associated with scalar
values or array values varying along its length. In analogy to
Expand All @@ -35,11 +35,17 @@ class Path(Geometry):
[{'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array}, ...]
Alternatively Path also supports a single columnar data-structure
to specify an individual path:
{'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array}
Both scalar values and values continuously varying along the
geometries coordinates a Path may be used to color the geometry
by. Since not all formats allow storing scalar values as actual
scalars arrays which are the same length as the coordinates but
have only one unique value are also considered scalar.
geometries coordinates a Path may be used vary visual properties
of the paths such as the color. Since not all formats allow
storing scalar values as actual scalars, arrays that are the same
length as the coordinates but have only one unique value are also
considered scalar.
The easiest way of accessing the individual geometries is using
the `Path.split` method, which returns each path geometry as a
Expand All @@ -49,19 +55,38 @@ class Path(Geometry):

group = param.String(default="Path", constant=True)

datatype = param.ObjectSelector(default=['multitabular'])
datatype = param.ObjectSelector(default=[
'multitabular', 'dataframe', 'dictionary', 'dask', 'array'])

def __init__(self, data, kdims=None, vdims=None, **params):
if isinstance(data, tuple) and len(data) == 2:
# Add support for (x, ys) where ys defines multiple paths
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])]
if y.ndim > 1:
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, 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)
# Allow unpacking of a list of Path elements
paths = []
for path in data:
if path.interface.multi and isinstance(path.data, list):
paths += path.data
else:
paths.append(path.data)
data = paths

datatype = params.pop('datatype', self.datatype)

# Ensure that a list of tuples of scalars and any other non-list
# type is interpreted as a single path
if (not isinstance(data, list) or
(isinstance(data, list) and not len(data) == 0 and all(
isinstance(d, tuple) and all(isscalar(v) for v in d)
for d in data))):
datatype = [dt for dt in datatype if dt != 'multitabular']
super(Path, self).__init__(data, kdims=kdims, vdims=vdims,
datatype=datatype, **params)


def __getitem__(self, key):
Expand Down Expand Up @@ -149,6 +174,11 @@ class Contours(Path):
[{'x': 1d-array, 'y': 1d-array, 'value': scalar}, ...]
Alternatively Contours also supports a single columnar
data-structure to specify an individual contour:
{'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array}
Since not all formats allow storing scalar values as actual
scalars arrays which are the same length as the coordinates but
have only one unique value are also considered scalar. This is
Expand Down Expand Up @@ -203,7 +233,7 @@ def dimension_values(self, dim, expanded=True, flat=True):

class Polygons(Contours):
"""
The Polygons element represents a collection of polygon geometries
The Polygons element represents one or more polygon geometries
with associated scalar values. Each polygon geometry may be split
into sub-geometries on NaN-values and may be associated with
scalar values. In analogy to GEOS geometry types a Polygons
Expand All @@ -224,6 +254,11 @@ class Polygons(Contours):
[{'x': 1d-array, 'y': 1d-array, 'holes': list-of-lists-of-arrays, 'value': scalar}, ...]
Alternatively Polygons also supports a single columnar
data-structure to specify an individual polygon:
{'x': 1d-array, 'y': 1d-array, 'holes': list-of-lists-of-arrays, 'value': scalar}
The list-of-lists format of the holes corresponds to the potential
for each coordinate array to be split into a multi-geometry
through NaN-separators. Each sub-geometry separated by the NaNs
Expand Down
72 changes: 71 additions & 1 deletion holoviews/tests/element/testpaths.py
Expand Up @@ -2,11 +2,81 @@
Unit tests of Path types.
"""
import numpy as np
from holoviews import Ellipse, Box, Polygons
from holoviews import Dataset, Ellipse, Box, Polygons, Path
from holoviews.core.data.interface import DataError
from holoviews.element.comparison import ComparisonTestCase


class PathTests(ComparisonTestCase):

def test_multi_path_list_constructor(self):
path = Path([[(0, 1), (1, 2)], [(2, 3), (3, 4)]])
self.assertTrue(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, np.nan, 2, 3]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, np.nan, 3, 4]))

def test_multi_path_tuple(self):
path = Path(([0, 1], [[1, 3], [2, 4]]))
self.assertTrue(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, np.nan, 0, 1]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, np.nan, 3, 4]))

def test_multi_path_unpack_single_paths(self):
path = Path([Path([(0, 1), (1, 2)]), Path([(2, 3), (3, 4)])])
self.assertTrue(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, np.nan, 2, 3]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, np.nan, 3, 4]))

def test_multi_path_unpack_multi_paths(self):
path = Path([Path([[(0, 1), (1, 2)]]),
Path([[(2, 3), (3, 4)], [(4, 5), (5, 6)]])])
self.assertTrue(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, np.nan, 2, 3, np.nan, 4, 5]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, np.nan, 3, 4, np.nan, 5, 6]))

def test_single_path_list_constructor(self):
path = Path([(0, 1), (1, 2), (2, 3), (3, 4)])
self.assertFalse(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, 2, 3]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, 3, 4]))

def test_single_path_tuple_constructor(self):
path = Path(([0, 1, 2, 3], [1, 2, 3, 4]))
self.assertFalse(path.interface.multi)
self.assertEqual(path.dimension_values(0), np.array([
0, 1, 2, 3]))
self.assertEqual(path.dimension_values(1), np.array([
1, 2, 3, 4]))

def test_multi_path_list_split(self):
path = Path([[(0, 1), (1, 2)], [(2, 3), (3, 4)]])
subpaths = path.split()
self.assertEqual(len(subpaths), 2)
self.assertEqual(subpaths[0], Path([(0, 1), (1, 2)]))
self.assertEqual(subpaths[1], Path([(2, 3), (3, 4)]))

def test_single_path_split(self):
path = Path(([0, 1, 2, 3], [1, 2, 3, 4]))
self.assertEqual(path, path.split()[0])

def test_dataset_groupby_path(self):
ds = Dataset([(0, 0, 1), (0, 1, 2), (1, 2, 3), (1, 3, 4)], ['group', 'x', 'y'])
subpaths = ds.groupby('group', group_type=Path)
self.assertEqual(len(subpaths), 2)
self.assertEqual(subpaths[0], Path([(0, 1), (1, 2)]))
self.assertEqual(subpaths[1], Path([(2, 3), (3, 4)]))


class PolygonsTests(ComparisonTestCase):

def setUp(self):
Expand Down

0 comments on commit 5485baf

Please sign in to comment.