Skip to content

Add Polygon support #819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion datashader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .pipeline import Pipeline # noqa (API import)
from . import transfer_functions as tf # noqa (API import)
from . import data_libraries # noqa (API import)

from . import geom # noqa (API import)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module name geom could be confusing here, as we have both spatial and geo already. The contents of geo.py should probably be moved into the other two, so let's ignore that one. spatial is full of functions for working with aggregate arrays, whereas geom is full of functions for doing the aggregations. geom vs. spatial doesn't convey that distinction, to me. I can't immediately think of a better name, though.


# Make RaggedArray pandas extension array available for
# pandas >= 0.24.0 is installed
Expand Down
177 changes: 134 additions & 43 deletions datashader/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ def validate(self, range):
_axis_lookup = {'linear': LinearAxis(), 'log': LogAxis()}


def validate_xy_or_geometry(glyph, x, y, geometry):
if (geometry is None and (x is None or y is None) or
geometry is not None and (x is not None or y is not None)):
raise ValueError("""
{glyph} coordinates may be specified by providing both the x and y arguments, or by
providing the geometry argument. Received:
x: {x}
y: {y}
geometry: {geometry}
""".format(glyph=glyph, x=repr(x), y=repr(y), geometry=repr(geometry)))


class Canvas(object):
"""An abstract canvas representing the space in which to bin.

Expand All @@ -157,34 +169,46 @@ def __init__(self, plot_width=600, plot_height=600,
self.x_axis = _axis_lookup[x_axis_type]
self.y_axis = _axis_lookup[y_axis_type]

def points(self, source, x, y, agg=None):
def points(self, source, x=None, y=None, agg=None, geometry=None):
"""Compute a reduction by pixel, mapping data to pixels as points.

Parameters
----------
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 point.
Column names for the x and y coordinates of each point. If provided,
the geometry argument may not also be provided.
agg : Reduction, optional
Reduction to compute. Default is ``count()``.
geometry: str
Column name of a PointsArray of the coordinates of each point. If provided,
the x and y arguments may not also be provided.
"""
from .glyphs import Point
from .glyphs import Point, PointGeom
from .reductions import count as count_rdn

validate_xy_or_geometry('Point', x, y, geometry)

if agg is None:
agg = count_rdn()

if (isinstance(source, SpatialPointsFrame) and
source.spatial is not None and
source.spatial.x == x and source.spatial.y == y and
self.x_range is not None and self.y_range is not None):
# Handle down-selecting of SpatialPointsFrame
if geometry is None:
if (isinstance(source, SpatialPointsFrame) and
source.spatial is not None and
source.spatial.x == x and source.spatial.y == y and
self.x_range is not None and self.y_range is not None):

source = source.spatial_query(
x_range=self.x_range, y_range=self.y_range)
source = source.spatial_query(
x_range=self.x_range, y_range=self.y_range)
glyph = Point(x, y)
else:
glyph = PointGeom(geometry)

return bypixel(source, self, Point(x, y), agg)
return bypixel(source, self, glyph, agg)

def line(self, source, x, y, agg=None, axis=0):
def line(self, source, x=None, y=None, agg=None, axis=0, geometry=None):
"""Compute a reduction by pixel, mapping data to pixels as one or
more lines.

Expand Down Expand Up @@ -215,6 +239,9 @@ def line(self, source, x, y, agg=None, axis=0):
all rows in source
* 1: Draw one line per row in source using data from the
specified columns
geometry : str
Column name of a LinesArray of the coordinates of each line. If provided,
the x and y arguments may not also be provided.

Examples
--------
Expand Down Expand Up @@ -284,55 +311,61 @@ def line(self, source, x, y, agg=None, axis=0):
"""
from .glyphs import (LineAxis0, LinesAxis1, LinesAxis1XConstant,
LinesAxis1YConstant, LineAxis0Multi,
LinesAxis1Ragged)
LinesAxis1Ragged, LineAxis1Geom)
from .reductions import any as any_rdn

validate_xy_or_geometry('Line', x, y, geometry)

if agg is None:
agg = any_rdn()

# Broadcast column specifications to handle cases where
# x is a list and y is a string or vice versa
orig_x, orig_y = x, y
x, y = _broadcast_column_specifications(x, y)
if geometry is not None:
glyph = LineAxis1Geom(geometry)
else:
# Broadcast column specifications to handle cases where
# x is a list and y is a string or vice versa
orig_x, orig_y = x, y
x, y = _broadcast_column_specifications(x, y)

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))
else:
raise ValueError("""
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))
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(orig_x), y=repr(orig_y)))
x=repr(orig_x), y=repr(orig_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)
elif (isinstance(x, (Number, string_types)) and
isinstance(y, (Number, string_types))):
glyph = LinesAxis1Ragged(x, y)
else:
raise ValueError("""
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)
elif (isinstance(x, (Number, string_types)) and
isinstance(y, (Number, string_types))):
glyph = LinesAxis1Ragged(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(orig_x), y=repr(orig_y)))
x=repr(orig_x), y=repr(orig_y)))

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

Expand Down Expand Up @@ -575,6 +608,64 @@ def area(self, source, x, y, agg=None, axis=0, y_stack=None):

return bypixel(source, self, glyph, agg)

def polygons(self, source, geometry, agg=None):
"""Compute a reduction by pixel, mapping data to pixels as one or
more filled polygons.

Parameters
----------
source : xarray.DataArray or Dataset
The input datasource.
geometry : str
Column name of a PolygonsArray of the coordinates of each line.
agg : Reduction, optional
Reduction to compute. Default is ``any()``.

Returns
-------
data : xarray.DataArray

Examples
--------
>>> from math import inf # doctest: +SKIP
... import datashader as ds
... import datashader.transfer_functions as tf
... from datashader.geom import PolygonsArray
... import pandas as pd
...
... polygons = PolygonsArray([
... # ## First Element
... # Filled quadrilateral (CCW order)
... [0, 0, 1, 0, 2, 2, -1, 4, 0, 0,
... # Triangular hole (CW order)
... -inf, -inf, 0.5, 1, 1, 2, 1.5, 1.5, 0.5, 1,
... # Rectangular hole (CW order)
... -inf, -inf, 0, 2, 0, 2.5, 0.5, 2.5, 0.5, 2, 0, 2,
... # Filled triangle
... inf, inf, 2.5, 3, 3.5, 3, 3.5, 4, 2.5, 3,
... ],
...
... # ## Second Element
... # Filled rectangle (CCW order)
... [3, 0, 3, 2, 4, 2, 4, 0, 3, 0,
... # Rectangular hole (CW order)
... -inf, -inf, 3.25, 0.25, 3.75, 0.25, 3.75, 1.75, 3.25, 1.75, 3.25, 0.25,
... ]
... ])
...
... df = pd.DataFrame({'polygons': polygons, 'v': range(len(polygons))})
...
... cvs = ds.Canvas()
... agg = cvs.polygons(df, geometry='polygons', agg=ds.sum('v'))
... tf.shade(agg)
"""
from .glyphs import PolygonGeom
from .reductions import any as any_rdn
if agg is None:
agg = any_rdn()
glyph = PolygonGeom(geometry)
return bypixel(source, self, glyph, agg)

def quadmesh(self, source, x=None, y=None, agg=None):
"""Samples a recti- or curvi-linear quadmesh by canvas size and bounds.
Parameters
Expand Down
3 changes: 2 additions & 1 deletion datashader/data_libraries/pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from datashader.core import bypixel
from datashader.compiler import compile_components
from datashader.glyphs.points import _PointLike
from datashader.glyphs.points import _PointLike, _GeomLike
from datashader.glyphs.area import _AreaToLineLike
from datashader.utils import Dispatcher
from collections import OrderedDict
Expand All @@ -21,6 +21,7 @@ def pandas_pipeline(df, schema, canvas, glyph, summary):


@glyph_dispatch.register(_PointLike)
@glyph_dispatch.register(_GeomLike)
@glyph_dispatch.register(_AreaToLineLike)
def default(glyph, source, schema, canvas, summary, cuda=False):
create, info, append, _, finalize = compile_components(summary, schema, glyph, cuda)
Expand Down
Loading