Skip to content

Commit

Permalink
Support for row-oriented coordinates in Canvas.line (#694)
Browse files Browse the repository at this point in the history
Support for row-oriented coordinates in Canvas.line by
adding an axis argument to the API
  • Loading branch information
jonmmease authored Jan 31, 2019
1 parent f945be4 commit 1238bfb
Show file tree
Hide file tree
Showing 11 changed files with 1,039 additions and 176 deletions.
1 change: 1 addition & 0 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ environment:
install:
- "SET PATH=%CONDA%;%CONDA%\\Scripts;%PATH%"
- "conda install -y -c pyviz pyctdev && doit ecosystem_setup"
- conda install -y "conda<4.6"
- "doit env_create %CHANNELS% --name=test --python=%PY%"
- "activate test"
- "doit develop_install %CHANNELS%"
Expand Down
124 changes: 114 additions & 10 deletions datashader/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import absolute_import, division, print_function

from numbers import Number

import numpy as np
import pandas as pd
import dask.dataframe as dd
from dask.array import Array
from six import string_types
from xarray import DataArray, Dataset
from collections import OrderedDict

Expand Down Expand Up @@ -160,8 +163,9 @@ def points(self, source, x, y, agg=None):
agg = count_rdn()
return bypixel(source, self, Point(x, y), agg)

def line(self, source, x, y, agg=None):
"""Compute a reduction by pixel, mapping data to pixels as a line.
def line(self, source, x, y, agg=None, axis=0):
"""Compute a reduction by pixel, mapping data to pixels as one or
more lines.
For aggregates that take in extra fields, the interpolated bins will
receive the fields from the previous point. In pseudocode:
Expand All @@ -176,16 +180,117 @@ def line(self, source, x, y, agg=None):
----------
source : pandas.DataFrame, dask.DataFrame, or xarray.DataArray/Dataset
The input datasource.
x, y : str
Column names for the x and y coordinates of each vertex.
x, y : str or number or list or tuple or np.ndarray
Specification of the x and y coordinates of each vertex
* str or number: Column labels in source
* list or tuple: List or tuple of column labels in source
* np.ndarray: When axis=1, a literal array of the
coordinates to be used for every row
agg : Reduction, optional
Reduction to compute. Default is ``any()``.
axis : 0 or 1, default 0
Axis in source to draw lines along
* 0: Draw lines using data from the specified columns across
all rows in source
* 1: Draw one line per row in source using data from the
specified columns
Examples
--------
Define a canvas and a pandas DataFrame with 6 rows
>>> import pandas as pd # doctest: +SKIP
... import numpy as np
... from datashader import Canvas
... import datashader.transfer_functions as tf
... cvs = Canvas()
... df = pd.DataFrame({
... 'A1': [1, 1.5, 2, 2.5, 3, 4],
... 'A2': [1.5, 2, 3, 3.2, 4, 5],
... 'B1': [10, 12, 11, 14, 13, 15],
... 'B2': [11, 9, 10, 7, 8, 12],
... }, dtype='float64')
Aggregate one line across all rows, with coordinates df.A1 by df.B1
>>> agg = cvs.line(df, x='A1', y='B1', axis=0) # doctest: +SKIP
... tf.shade(agg)
Aggregate two lines across all rows. The first with coordinates
df.A1 by df.B1 and the second with coordinates df.A2 by df.B2
>>> agg = cvs.line(df, x=['A1', 'A2'], y=['B1', 'B2'], axis=0) # doctest: +SKIP
... tf.shade(agg)
Aggregate two lines across all rows where the lines share the same
x coordinates. The first line will have coordinates df.A1 by df.B1
and the second will have coordinates df.A1 by df.B2
>>> agg = cvs.line(df, x='A1', y=['B1', 'B2'], axis=0) # doctest: +SKIP
... tf.shade(agg)
Aggregate 6 length-2 lines, one per row, where the ith line has
coordinates [df.A1[i], df.A2[i]] by [df.B1[i], df.B2[i]]
>>> agg = cvs.line(df, x=['A1', 'A2'], y=['B1', 'B2'], axis=1) # doctest: +SKIP
... tf.shade(agg)
Aggregate 6 length-4 lines, one per row, where the x coordinates
of every line are [0, 1, 2, 3] and the y coordinates of the ith line
are [df.A1[i], df.A2[i], df.B1[i], df.B2[i]].
>>> agg = cvs.line(df, # doctest: +SKIP
... x=np.arange(4),
... y=['A1', 'A2', 'B1', 'B2'],
... axis=1)
... tf.shade(agg)
"""
from .glyphs import Line
from .glyphs import (LineAxis0, LinesAxis1, LinesAxis1XConstant,
LinesAxis1YConstant, LineAxis0Multi)
from .reductions import any as any_rdn
if agg is None:
agg = any_rdn()
return bypixel(source, self, Line(x, y), agg)

if axis == 0:
if (isinstance(x, (Number, string_types)) and
isinstance(y, (Number, string_types))):
glyph = LineAxis0(x, y)
elif (isinstance(x, (list, tuple)) and
isinstance(y, (list, tuple))):
glyph = LineAxis0Multi(tuple(x), tuple(y))
elif (isinstance(x, (list, tuple)) and
isinstance(y, (Number, string_types))):
glyph = LineAxis0Multi(tuple(x), (y,) * len(x))
elif (isinstance(x, (Number, string_types)) and
isinstance(y, (list, tuple))):
glyph = LineAxis0Multi((x,) * len(y), tuple(y))
else:
raise ValueError("""
Invalid combination of x and y arguments to Canvas.line when axis=0.
Received:
x: {x}
y: {y}
See docstring for more information on valid usage""".format(
x=repr(x), y=repr(y)))

elif axis == 1:
if isinstance(x, (list, tuple)) and isinstance(y, (list, tuple)):
glyph = LinesAxis1(tuple(x), tuple(y))
elif (isinstance(x, np.ndarray) and
isinstance(y, (list, tuple))):
glyph = LinesAxis1XConstant(x, tuple(y))
elif (isinstance(x, (list, tuple)) and
isinstance(y, np.ndarray)):
glyph = LinesAxis1YConstant(tuple(x), y)
else:
raise ValueError("""
Invalid combination of x and y arguments to Canvas.line when axis=1.
Received:
x: {x}
y: {y}
See docstring for more information on valid usage""".format(
x=repr(x), y=repr(y)))

else:
raise ValueError("""
The axis argument to Canvas.line must be 0 or 1
Received: {axis}""".format(axis=axis))

return bypixel(source, self, glyph, agg)

# TODO re 'untested', below: Consider replacing with e.g. a 3x3
# array in the call to Canvas (plot_height=3,plot_width=3), then
Expand Down Expand Up @@ -541,10 +646,9 @@ def bypixel(source, canvas, glyph, agg):

def _cols_to_keep(columns, glyph, agg):
cols_to_keep = OrderedDict({col: False for col in columns})
cols_to_keep[glyph.x] = True
cols_to_keep[glyph.y] = True
if hasattr(glyph, 'z'):
cols_to_keep[glyph.z[0]] = True
for col in glyph.required_columns():
cols_to_keep[col] = True

if hasattr(agg, 'values'):
for subagg in agg.values:
if subagg.column is not None:
Expand Down
12 changes: 6 additions & 6 deletions datashader/dask.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .core import bypixel
from .compatibility import apply
from .compiler import compile_components
from .glyphs import Glyph, Line
from .glyphs import Glyph, LineAxis0
from .utils import Dispatcher

__all__ = ()
Expand All @@ -31,8 +31,8 @@ def dask_pipeline(df, schema, canvas, glyph, summary):


def shape_bounds_st_and_axis(df, canvas, glyph):
x_range = canvas.x_range or glyph._compute_x_bounds_dask(df)
y_range = canvas.y_range or glyph._compute_y_bounds_dask(df)
x_range = canvas.x_range or glyph.compute_x_bounds_dask(df)
y_range = canvas.y_range or glyph.compute_y_bounds_dask(df)
x_min, x_max, y_min, y_max = bounds = compute(*(x_range + y_range))
x_range, y_range = (x_min, x_max), (y_min, y_max)

Expand Down Expand Up @@ -75,11 +75,11 @@ def chunk(df):
keys2 = [(name, i) for i in range(len(keys))]
dsk = dict((k2, (chunk, k)) for (k2, k) in zip(keys2, keys))
dsk[name] = (apply, finalize, [(combine, keys2)],
dict(coords=axis, dims=[glyph.y, glyph.x]))
dict(coords=axis, dims=[glyph.y_label, glyph.x_label]))
return dsk, name


@glyph_dispatch.register(Line)
@glyph_dispatch.register(LineAxis0)
def line(glyph, df, schema, canvas, summary):
shape, bounds, st, axis = shape_bounds_st_and_axis(df, canvas, glyph)

Expand All @@ -106,5 +106,5 @@ def chunk(df, df2=None):
dsk[(name, i)] = (chunk, (old_name, i - 1), (old_name, i))
keys2 = [(name, i) for i in range(df.npartitions)]
dsk[name] = (apply, finalize, [(combine, keys2)],
dict(coords=axis, dims=[glyph.y, glyph.x]))
dict(coords=axis, dims=[glyph.y_label, glyph.x_label]))
return dsk, name
Loading

0 comments on commit 1238bfb

Please sign in to comment.