diff --git a/.travis.yml b/.travis.yml index cd27155d3..df724ad6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,6 +60,11 @@ jobs: - doit env_create $CHANS_DEV --python=$PYTHON_VERSION --name=$PYTHON_VERSION - source activate $PYTHON_VERSION - doit develop_install $CHANS_DEV $OPTS + # Install spatialpandas here because it's python 3 only and requires + # conda-forge for some dependencies + - if [[ "$PYTHON_VERSION" != "2.7" ]]; then + conda install -c pyviz/label/dev -c conda-forge spatialpandas; + fi - doit env_capture script: - doit test_all diff --git a/datashader/__init__.py b/datashader/__init__.py index 433d45f51..aaa30dbc5 100644 --- a/datashader/__init__.py +++ b/datashader/__init__.py @@ -12,7 +12,6 @@ from . import transfer_functions as tf # noqa (API import) from . import data_libraries # noqa (API import) - # Make RaggedArray pandas extension array available for # pandas >= 0.24.0 is installed from pandas import __version__ as pandas_version diff --git a/datashader/core.py b/datashader/core.py index 319d1d161..deea52959 100644 --- a/datashader/core.py +++ b/datashader/core.py @@ -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. @@ -157,7 +169,7 @@ 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 @@ -165,26 +177,51 @@ def points(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 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, MultiPointGeometry 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: + from spatialpandas import GeoDataFrame + from spatialpandas.dask import DaskGeoDataFrame + if isinstance(source, DaskGeoDataFrame): + # Downselect partitions to those that may contain points in viewport + x_range = self.x_range if self.x_range is not None else (None, None) + y_range = self.y_range if self.y_range is not None else (None, None) + source = source.cx_partitions[slice(*x_range), slice(*y_range)] + elif not isinstance(source, GeoDataFrame): + raise ValueError( + "source must be an instance of spatialpandas.GeoDataFrame or \n" + "spatialpandas.dask.DaskGeoDataFrame.\n" + " Received value of type {typ}".format(typ=type(source))) + + glyph = MultiPointGeometry(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. @@ -215,6 +252,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 -------- @@ -284,55 +324,74 @@ def line(self, source, x, y, agg=None, axis=0): """ from .glyphs import (LineAxis0, LinesAxis1, LinesAxis1XConstant, LinesAxis1YConstant, LineAxis0Multi, - LinesAxis1Ragged) + LinesAxis1Ragged, LineAxis1Geometry) 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: + from spatialpandas import GeoDataFrame + from spatialpandas.dask import DaskGeoDataFrame + if isinstance(source, DaskGeoDataFrame): + # Downselect partitions to those that may contain lines in viewport + x_range = self.x_range if self.x_range is not None else (None, None) + y_range = self.y_range if self.y_range is not None else (None, None) + source = source.cx_partitions[slice(*x_range), slice(*y_range)] + elif not isinstance(source, GeoDataFrame): + raise ValueError( + "source must be an instance of spatialpandas.GeoDataFrame or \n" + "spatialpandas.dask.DaskGeoDataFrame.\n" + " Received value of type {typ}".format(typ=type(source))) + + glyph = LineAxis1Geometry(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)) @@ -575,6 +634,72 @@ 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 + -------- + >>> import datashader as ds # doctest: +SKIP + ... import datashader.transfer_functions as tf + ... from spatialpandas.geometry import PolygonArray + ... from spatialpandas import GeoDataFrame + ... import pandas as pd + ... + ... polygons = PolygonArray([ + ... # First Element + ... [[0, 0, 1, 0, 2, 2, -1, 4, 0, 0], # Filled quadrilateral (CCW order) + ... [0.5, 1, 1, 2, 1.5, 1.5, 0.5, 1], # Triangular hole (CW order) + ... [0, 2, 0, 2.5, 0.5, 2.5, 0.5, 2, 0, 2], # Rectangular hole (CW order) + ... [2.5, 3, 3.5, 3, 3.5, 4, 2.5, 3], # Filled triangle + ... ], + ... + ... # Second Element + ... [[3, 0, 3, 2, 4, 2, 4, 0, 3, 0], # Filled rectangle (CCW order) + ... # Rectangular hole (CW order) + ... [3.25, 0.25, 3.75, 0.25, 3.75, 1.75, 3.25, 1.75, 3.25, 0.25], + ... ] + ... ]) + ... + ... df = GeoDataFrame({'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 + from spatialpandas import GeoDataFrame + from spatialpandas.dask import DaskGeoDataFrame + if isinstance(source, DaskGeoDataFrame): + # Downselect partitions to those that may contain polygons in viewport + x_range = self.x_range if self.x_range is not None else (None, None) + y_range = self.y_range if self.y_range is not None else (None, None) + source = source.cx_partitions[slice(*x_range), slice(*y_range)] + elif not isinstance(source, GeoDataFrame): + raise ValueError( + "source must be an instance of spatialpandas.GeoDataFrame or \n" + "spatialpandas.dask.DaskGeoDataFrame.\n" + " Received value of type {typ}".format(typ=type(source))) + + 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 diff --git a/datashader/data_libraries/pandas.py b/datashader/data_libraries/pandas.py index 0f4ba627c..a5aa65307 100644 --- a/datashader/data_libraries/pandas.py +++ b/datashader/data_libraries/pandas.py @@ -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, _GeometryLike from datashader.glyphs.area import _AreaToLineLike from datashader.utils import Dispatcher from collections import OrderedDict @@ -21,6 +21,7 @@ def pandas_pipeline(df, schema, canvas, glyph, summary): @glyph_dispatch.register(_PointLike) +@glyph_dispatch.register(_GeometryLike) @glyph_dispatch.register(_AreaToLineLike) def default(glyph, source, schema, canvas, summary, cuda=False): create, info, append, _, finalize = compile_components(summary, schema, glyph, cuda) diff --git a/datashader/glyphs/__init__.py b/datashader/glyphs/__init__.py index 8abcb83c3..ce3472adc 100644 --- a/datashader/glyphs/__init__.py +++ b/datashader/glyphs/__init__.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from .points import Point # noqa (API import) +from .points import Point, MultiPointGeometry # noqa (API import) from .line import ( # noqa (API import) LineAxis0, LineAxis0Multi, @@ -7,6 +7,7 @@ LinesAxis1XConstant, LinesAxis1YConstant, LinesAxis1Ragged, + LineAxis1Geometry, ) from .area import ( # noqa (API import) AreaToZeroAxis0, @@ -23,6 +24,7 @@ AreaToLineAxis1Ragged, ) from .trimesh import Triangles # noqa (API import) +from .polygon import PolygonGeom # noqa (API import) from .quadmesh import ( # noqa (API import) QuadMeshRectilinear, QuadMeshCurvialinear ) diff --git a/datashader/glyphs/line.py b/datashader/glyphs/line.py index 598560447..560ec9e52 100644 --- a/datashader/glyphs/line.py +++ b/datashader/glyphs/line.py @@ -2,8 +2,8 @@ import numpy as np from toolz import memoize +from datashader.glyphs.points import _PointLike, _GeometryLike from datashader.glyphs.glyph import isnull -from datashader.glyphs.points import _PointLike from datashader.utils import isreal, ngjit from numba import cuda @@ -458,6 +458,53 @@ def extend(aggs, df, vt, bounds, plot_start=True): return extend +class LineAxis1Geometry(_GeometryLike): + + @property + def geom_dtypes(self): + from spatialpandas.geometry import ( + LineDtype, MultiLineDtype, RingDtype, PolygonDtype, + MultiPolygonDtype + ) + return (LineDtype, MultiLineDtype, RingDtype, + PolygonDtype, MultiPolygonDtype) + + @memoize + def _build_extend(self, x_mapper, y_mapper, info, append): + from spatialpandas.geometry import ( + PolygonArray, MultiPolygonArray + ) + expand_aggs_and_cols = self.expand_aggs_and_cols(append) + map_onto_pixel = _build_map_onto_pixel_for_line(x_mapper, y_mapper) + draw_segment = _build_draw_segment( + append, map_onto_pixel, expand_aggs_and_cols + ) + + perform_extend_cpu = _build_extend_line_axis1_geometry( + draw_segment, expand_aggs_and_cols + ) + geometry_name = self.geometry + + def extend(aggs, df, vt, bounds, plot_start=True): + sx, tx, sy, ty = vt + xmin, xmax, ymin, ymax = bounds + aggs_and_cols = aggs + info(df) + geom_array = df[geometry_name].array + # line may be clipped, then mapped to pixels + + if isinstance(geom_array, (PolygonArray, MultiPolygonArray)): + # Convert polygon array to multi line of boundary + geom_array = geom_array.boundary + + perform_extend_cpu( + sx, tx, sy, ty, + xmin, xmax, ymin, ymax, + geom_array, *aggs_and_cols + ) + + return extend + + def _build_map_onto_pixel_for_line(x_mapper, y_mapper): @ngjit def map_onto_pixel(sx, tx, sy, ty, xmin, xmax, ymin, ymax, x, y): @@ -910,3 +957,79 @@ def extend_cpu_numba( segment_start, x0, x1, y0, y1, *aggs_and_cols) return extend_cpu + + +def _build_extend_line_axis1_geometry( + draw_segment, expand_aggs_and_cols +): + def extend_cpu( + sx, tx, sy, ty, + xmin, xmax, ymin, ymax, + geometry, *aggs_and_cols + ): + + values = geometry.buffer_values + missing = geometry.isna() + offsets = geometry.buffer_offsets + + if len(offsets) == 2: + # MultiLineArray + offsets0, offsets1 = offsets + else: + # LineArray + offsets1 = offsets[0] + offsets0 = np.arange(len(offsets1)) + + # Compute indices of potentially intersecting polygons using + # geometry's R-tree + eligible_inds = geometry.sindex.intersects((xmin, ymin, xmax, ymax)) + + extend_cpu_numba( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets0, offsets1, eligible_inds, *aggs_and_cols + ) + + @ngjit + @expand_aggs_and_cols + def extend_cpu_numba( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets0, offsets1, eligible_inds, *aggs_and_cols + ): + for i in eligible_inds: + if missing[i]: + continue + + start0 = offsets0[i] + stop0 = offsets0[i + 1] + + for j in range(start0, stop0): + start1 = offsets1[j] + stop1 = offsets1[j + 1] + + for k in range(start1, stop1 - 2, 2): + x0 = values[k] + if not np.isfinite(x0): + continue + + y0 = values[k + 1] + if not np.isfinite(y0): + continue + + x1 = values[k + 2] + if not np.isfinite(x1): + continue + + y1 = values[k + 3] + if not np.isfinite(y1): + continue + + segment_start = ( + (k == start1) or + not np.isfinite(values[k - 2]) or + not np.isfinite(values[k - 1]) + ) + + draw_segment(i, sx, tx, sy, ty, xmin, xmax, ymin, ymax, + segment_start, x0, x1, y0, y1, *aggs_and_cols) + + return extend_cpu diff --git a/datashader/glyphs/points.py b/datashader/glyphs/points.py index f0a131119..b3434b861 100644 --- a/datashader/glyphs/points.py +++ b/datashader/glyphs/points.py @@ -22,6 +22,60 @@ def values(s): return s.values +class _GeometryLike(Glyph): + def __init__(self, geometry): + self.geometry = geometry + + @property + def ndims(self): + return 1 + + @property + def inputs(self): + return (self.geometry,) + + @property + def geom_dtypes(self): + from spatialpandas.geometry import GeometryDtype + return (GeometryDtype,) + + def validate(self, in_dshape): + if not isinstance(in_dshape[str(self.geometry)], self.geom_dtypes): + raise ValueError( + '{col} must be an array with one of the following types: {typs}'.format( + col=self.geometry, + typs=', '.join(typ.__name__ for typ in self.geom_dtypes) + )) + + @property + def x_label(self): + return 'x' + + @property + def y_label(self): + return 'y' + + def required_columns(self): + return [self.geometry] + + def compute_x_bounds(self, df): + bounds = df[self.geometry].array.total_bounds_x + return self.maybe_expand_bounds(bounds) + + def compute_y_bounds(self, df): + bounds = df[self.geometry].array.total_bounds_y + return self.maybe_expand_bounds(bounds) + + @memoize + def compute_bounds_dask(self, ddf): + total_bounds = ddf[self.geometry].total_bounds + x_extents = (total_bounds[0], total_bounds[2]) + y_extents = (total_bounds[1], total_bounds[3]) + + return (self.maybe_expand_bounds(x_extents), + self.maybe_expand_bounds(y_extents)) + + class _PointLike(Glyph): """Shared methods between Point and Line""" def __init__(self, x, y): @@ -142,3 +196,94 @@ def extend(aggs, df, vt, bounds): ) return extend + + +class MultiPointGeometry(_GeometryLike): + + @property + def geom_dtypes(self): + from spatialpandas.geometry import PointDtype, MultiPointDtype + return PointDtype, MultiPointDtype + + @memoize + def _build_extend(self, x_mapper, y_mapper, info, append): + geometry_name = self.geometry + + @ngjit + @self.expand_aggs_and_cols(append) + def _perform_extend_points( + i, j, sx, tx, sy, ty, xmin, xmax, ymin, ymax, values, *aggs_and_cols + ): + x = values[j] + y = values[j + 1] + # points outside bounds are dropped; remainder + # are mapped onto pixels + if (xmin <= x <= xmax) and (ymin <= y <= ymax): + xx = int(x_mapper(x) * sx + tx) + yy = int(y_mapper(y) * sy + ty) + xi, yi = (xx - 1 if x == xmax else xx, + yy - 1 if y == ymax else yy) + + append(i, xi, yi, *aggs_and_cols) + + @ngjit + @self.expand_aggs_and_cols(append) + def extend_point_cpu( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, eligible_inds, *aggs_and_cols + ): + for i in eligible_inds: + if missing[i] is True: + continue + _perform_extend_points( + i, 2 * i, sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, *aggs_and_cols + ) + + @ngjit + @self.expand_aggs_and_cols(append) + def extend_multipoint_cpu( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets, eligible_inds, *aggs_and_cols + ): + for i in eligible_inds: + if missing[i] is True: + continue + start = offsets[i] + stop = offsets[i + 1] + for j in range(start, stop, 2): + _perform_extend_points( + i, j, sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, *aggs_and_cols + ) + + def extend(aggs, df, vt, bounds): + from spatialpandas.geometry import PointArray + + aggs_and_cols = aggs + info(df) + sx, tx, sy, ty = vt + xmin, xmax, ymin, ymax = bounds + + geometry = df[geometry_name].array + + # Compute indices of potentially intersecting polygons using + # geometry's R-tree + eligible_inds = geometry.sindex.intersects((xmin, ymin, xmax, ymax)) + missing = geometry.isna() + + if isinstance(geometry, PointArray): + values = geometry.flat_values + extend_point_cpu( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, eligible_inds, *aggs_and_cols + ) + else: + values = geometry.buffer_values + offsets = geometry.buffer_offsets[0] + + extend_multipoint_cpu( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets, eligible_inds, *aggs_and_cols + ) + + return extend diff --git a/datashader/glyphs/polygon.py b/datashader/glyphs/polygon.py new file mode 100644 index 000000000..8c42ebbb4 --- /dev/null +++ b/datashader/glyphs/polygon.py @@ -0,0 +1,263 @@ +from toolz import memoize +import numpy as np + +from datashader.glyphs.line import _build_map_onto_pixel_for_line +from datashader.glyphs.points import _GeometryLike +from datashader.utils import ngjit + + +class PolygonGeom(_GeometryLike): + @property + def geom_dtypes(self): + from spatialpandas.geometry import PolygonDtype, MultiPolygonDtype + return PolygonDtype, MultiPolygonDtype + + @memoize + def _build_extend(self, x_mapper, y_mapper, info, append): + expand_aggs_and_cols = self.expand_aggs_and_cols(append) + map_onto_pixel = _build_map_onto_pixel_for_line(x_mapper, y_mapper) + draw_polygon = _build_draw_polygon( + append, map_onto_pixel, x_mapper, y_mapper, expand_aggs_and_cols + ) + + perform_extend_cpu = _build_extend_polygon_geometry( + draw_polygon, expand_aggs_and_cols + ) + geom_name = self.geometry + + def extend(aggs, df, vt, bounds, plot_start=True): + sx, tx, sy, ty = vt + xmin, xmax, ymin, ymax = bounds + aggs_and_cols = aggs + info(df) + geom_array = df[geom_name].array + + perform_extend_cpu( + sx, tx, sy, ty, + xmin, xmax, ymin, ymax, + geom_array, *aggs_and_cols + ) + + return extend + + +def _build_draw_polygon(append, map_onto_pixel, x_mapper, y_mapper, expand_aggs_and_cols): + @ngjit + @expand_aggs_and_cols + def draw_polygon( + i, sx, tx, sy, ty, xmin, xmax, ymin, ymax, + offsets, values, xs, ys, yincreasing, eligible, + *aggs_and_cols + ): + """Draw a polygon using a winding-number scan-line algorithm + """ + # Initialize values of pre-allocated buffers + xs.fill(np.nan) + ys.fill(np.nan) + yincreasing.fill(0) + eligible.fill(1) + + # First pass, compute bounding box of polygon vertices in data coordinates + start_index = offsets[0] + stop_index = offsets[-1] + # num_edges = stop_index - start_index - 2 + poly_xmin = np.min(values[start_index:stop_index:2]) + poly_ymin = np.min(values[start_index + 1:stop_index:2]) + poly_xmax = np.max(values[start_index:stop_index:2]) + poly_ymax = np.max(values[start_index + 1:stop_index:2]) + + # skip polygon if outside viewport + if (poly_xmax < xmin or poly_xmin > xmax + or poly_ymax < ymin or poly_ymin > ymax): + return + + # Compute pixel bounds for polygon + startxi, startyi = map_onto_pixel( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + max(poly_xmin, xmin), max(poly_ymin, ymin) + ) + stopxi, stopyi = map_onto_pixel( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + min(poly_xmax, xmax), min(poly_ymax, ymax) + ) + stopxi += 1 + stopyi += 1 + + # Handle subpixel polygons (pixel width and/or height of polygon is 1) + if (stopxi - startxi) == 1 and (stopyi - startyi) == 1: + append(i, startxi, startyi, *aggs_and_cols) + return + elif (stopxi - startxi) == 1: + for yi in range(min(startyi, stopyi) + 1, max(startyi, stopyi)): + append(i, startxi, yi, *aggs_and_cols) + return + elif (stopyi - startyi) == 1: + for xi in range(min(startxi, stopxi) + 1, max(startxi, stopxi)): + append(i, xi, startyi, *aggs_and_cols) + return + + # Build arrays of edges in canvas coordinates + ei = 0 + for j in range(len(offsets) - 1): + start = offsets[j] + stop = offsets[j + 1] + for k in range(start, stop - 2, 2): + x0 = values[k] + y0 = values[k + 1] + x1 = values[k + 2] + y1 = values[k + 3] + + # Map to canvas coordinates without rounding + x0c = x_mapper(x0) * sx + tx + y0c = y_mapper(y0) * sy + ty + x1c = x_mapper(x1) * sx + tx + y1c = y_mapper(y1) * sy + ty + + if y1c > y0c: + xs[ei, 0] = x0c + ys[ei, 0] = y0c + xs[ei, 1] = x1c + ys[ei, 1] = y1c + yincreasing[ei] = 1 + elif y1c < y0c: + xs[ei, 1] = x0c + ys[ei, 1] = y0c + xs[ei, 0] = x1c + ys[ei, 0] = y1c + yincreasing[ei] = -1 + else: + # Skip horizontal edges + continue + + ei += 1 + + # Perform scan-line algorithm + num_edges = ei + for yi in range(startyi, stopyi): + # All edges eligible at start of new row + eligible.fill(1) + for xi in range(startxi, stopxi): + # Init winding number + winding_number = 0 + for ei in range(num_edges): + if eligible[ei] == 0: + # We've already determined that edge is above, below, or left + # of edge for the current pixel + continue + + # Get edge coordinates. + # Note: y1c > y0c due to how xs/ys were populated + x0c = xs[ei, 0] + x1c = xs[ei, 1] + y0c = ys[ei, 0] + y1c = ys[ei, 1] + + # Reject edges that are above, below, or left of current pixel. + # Note: Edge skipped if lower vertex overlaps, + # but is kept if upper vertex overlaps + if (y0c >= yi or y1c < yi + or (x0c < xi and x1c < xi) + ): + # Edge not eligible for any remaining pixel in this row + eligible[ei] = 0 + continue + + if xi <= x0c and xi <= x1c: + # Edge is fully to the right of the pixel, so we know ray to the + # the right of pixel intersects edge. + winding_number += yincreasing[ei] + else: + # Now check if edge is to the right of pixel using cross product + # A is vector from pixel to first vertex + ax = x0c - xi + ay = y0c - yi + + # B is vector from pixel to second vertex + bx = x1c - xi + by = y1c - yi + + # Compute cross product of B and A + bxa = (bx * ay - by * ax) + + if bxa < 0 or (bxa == 0 and yincreasing[ei]): + # Edge to the right + winding_number += yincreasing[ei] + else: + # Edge to left, not eligible for any remaining pixel in row + eligible[ei] = 0 + continue + + if winding_number != 0: + # If winding number is not zero, point + # is inside polygon + append(i, xi, yi, *aggs_and_cols) + + return draw_polygon + + +def _build_extend_polygon_geometry( + draw_polygon, expand_aggs_and_cols +): + def extend_cpu( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, geometry, *aggs_and_cols + ): + values = geometry.buffer_values + missing = geometry.isna() + offsets = geometry.buffer_offsets + + # Compute indices of potentially intersecting polygons using + # geometry's R-tree + eligible_inds = geometry.sindex.intersects((xmin, ymin, xmax, ymax)) + + if len(offsets) == 3: + # MultiPolygonArray + offsets0, offsets1, offsets2 = offsets + else: + # PolygonArray + offsets1, offsets2 = offsets + offsets0 = np.arange(len(offsets1)) + + extend_cpu_numba( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets0, offsets1, offsets2, eligible_inds, *aggs_and_cols + ) + + @ngjit + @expand_aggs_and_cols + def extend_cpu_numba( + sx, tx, sy, ty, xmin, xmax, ymin, ymax, + values, missing, offsets0, offsets1, offsets2, + eligible_inds, *aggs_and_cols + ): + # Pre-allocate temp arrays + if len(offsets0) > 1: + max_edges = -1 + for i in range(len(offsets0) - 1): + if missing[i]: + continue + start = offsets2[offsets1[offsets0[i]]] + stop = offsets2[offsets1[offsets0[i + 1]]] + max_edges = max(max_edges, (stop - start) // 2) + else: + max_edges = 0 + + xs = np.full((max_edges, 2), np.nan, dtype=np.float32) + ys = np.full((max_edges, 2), np.nan, dtype=np.float32) + yincreasing = np.zeros(max_edges, dtype=np.int8) + + # Initialize array indicating which edges are still eligible for processing + eligible = np.ones(max_edges, dtype=np.int8) + + for i in eligible_inds: + if missing[i]: + continue + + polygon_inds = offsets1[offsets0[i]:offsets0[i + 1] + 1] + for j in range(len(polygon_inds) - 1): + start = polygon_inds[j] + stop = polygon_inds[j + 1] + + draw_polygon(i, sx, tx, sy, ty, xmin, xmax, ymin, ymax, + offsets2[start:stop + 1], values, + xs, ys, yincreasing, eligible, *aggs_and_cols) + + return extend_cpu diff --git a/datashader/tests/test_dask.py b/datashader/tests/test_dask.py index c718debde..1aa34122c 100644 --- a/datashader/tests/test_dask.py +++ b/datashader/tests/test_dask.py @@ -1,8 +1,10 @@ from __future__ import division, absolute_import import os + from dask.context import config import dask.dataframe as dd import numpy as np +from numpy import nan import pandas as pd import xarray as xr @@ -11,6 +13,11 @@ import pytest +try: + import spatialpandas as sp +except ImportError: + sp = None + from datashader.tests.test_pandas import ( assert_eq_xr, assert_eq_ndarray, values ) @@ -39,7 +46,11 @@ _ddf = dd.from_pandas(df_pd, npartitions=2) def dask_DataFrame(*args, **kwargs): - return dd.from_pandas(pd.DataFrame(*args, **kwargs), npartitions=2) + if kwargs.pop("geo", False): + df = sp.GeoDataFrame(*args, **kwargs) + else: + df = pd.DataFrame(*args, **kwargs) + return dd.from_pandas(df, npartitions=2) try: @@ -54,6 +65,7 @@ def dask_DataFrame(*args, **kwargs): ddfs = [_ddf, dask_cudf.from_dask_dataframe(_ddf)] def dask_cudf_DataFrame(*args, **kwargs): + assert not kwargs.pop("geo", False) cdf = cudf.DataFrame.from_pandas( pd.DataFrame(*args, **kwargs), nan_as_null=False ) @@ -342,6 +354,27 @@ def test_log_axis_points(ddf): assert_eq_xr(c_logxy.points(ddf, 'log_x', 'log_y', ds.count('i32')), out) +@pytest.mark.skipif(not sp, reason="spatialpandas not installed") +def test_points_geometry(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0., 2.), 3), 3) + + ddf = dd.from_pandas(sp.GeoDataFrame({ + 'geom': pd.array( + [[0, 0], [0, 1, 1, 1], [0, 2, 1, 2, 2, 2]], dtype='MultiPoint[float64]'), + 'v': [1, 2, 3] + }), npartitions=3) + + cvs = ds.Canvas(plot_width=3, plot_height=3) + agg = cvs.points(ddf, geometry='geom', agg=ds.sum('v')) + sol = np.array([[1, nan, nan], + [2, 2, nan], + [3, 3, 3]], dtype='float64') + out = xr.DataArray(sol, coords=[lincoords, lincoords], + dims=['y', 'x']) + assert_eq_xr(agg, out) + + @pytest.mark.parametrize('DataFrame', DataFrames) def test_line(DataFrame): axis = ds.core.LinearAxis() @@ -365,8 +398,7 @@ def test_line(DataFrame): # # Line tests -@pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +line_manual_range_params = [ # axis1 none constant (dict(data={ 'x0': [4, -4, 4], @@ -375,20 +407,20 @@ def test_line(DataFrame): 'y0': [0, 0, 0], 'y1': [-4, 4, 0], 'y2': [0, 0, 0] - }), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 x constant (dict(data={ 'y0': [0, 0, 0], 'y1': [0, 4, -4], 'y2': [0, 0, 0] - }), np.array([-4, 0, 4]), ['y0', 'y1', 'y2'], 1), + }), dict(x=np.array([-4, 0, 4]), y=['y0', 'y1', 'y2'], axis=1)), # axis0 single (dict(data={ 'x': [4, 0, -4, np.nan, -4, 0, 4, np.nan, 4, 0, -4], 'y': [0, -4, 0, np.nan, 0, 4, 0, np.nan, 0, 0, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -398,7 +430,7 @@ def test_line(DataFrame): 'y0': [0, -4, 0], 'y1': [0, 4, 0], 'y2': [0, 0, 0] - }), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 0), + }), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=0)), # axis0 multi with string (dict(data={ @@ -406,27 +438,38 @@ def test_line(DataFrame): 'y0': [0, -4, 0], 'y1': [0, 4, 0], 'y2': [0, 0, 0] - }), 'x0', ['y0', 'y1', 'y2'], 0), + }), dict(x='x0', y=['y0', 'y1', 'y2'], axis=0)), # axis1 RaggedArray (dict(data={ 'x': [[4, 0, -4], [-4, 0, 4, 4, 0, -4]], 'y': [[0, -4, 0], [0, 4, 0, 0, 0, 0]], - }, dtype='Ragged[int64]'), 'x', 'y', 1), -]) -def test_line_manual_range(DataFrame, df_kwargs, x, y, ax): + }, dtype='Ragged[int64]'), dict(x='x', y='y', axis=1)), +] +if sp: + line_manual_range_params.append( + # geometry + (dict(data={ + 'geom': [[4, 0, 0, -4, -4, 0], + [-4, 0, 0, 4, 4, 0, 4, 0, 0, 0, -4, 0]] + }, dtype='Line[int64]'), dict(geometry='geom')) + ) +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', line_manual_range_params) +def test_line_manual_range(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: - if df_kwargs.get('dtype', '').startswith('Ragged'): + dtype = df_kwargs.get('dtype', '') + if dtype.startswith('Ragged') or dtype.startswith('Line'): pytest.skip("Ragged array not supported with cudf") axis = ds.core.LinearAxis() lincoords = axis.compute_index(axis.compute_scale_and_translate((-3., 3.), 7), 7) - ddf = DataFrame(**df_kwargs) + ddf = DataFrame(geo='geometry' in cvs_kwargs, **df_kwargs) cvs = ds.Canvas(plot_width=7, plot_height=7, x_range=(-3, 3), y_range=(-3, 3)) - agg = cvs.line(ddf, x, y, ds.count(), axis=ax) + agg = cvs.line(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], @@ -441,8 +484,7 @@ def test_line_manual_range(DataFrame, df_kwargs, x, y, ax): assert_eq_xr(agg, out) -@pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +line_autorange_params = [ # axis1 none constant (dict(data={ 'x0': [0, 0, 0], @@ -451,20 +493,20 @@ def test_line_manual_range(DataFrame, df_kwargs, x, y, ax): 'y0': [-4, 4, -4], 'y1': [0, 0, 0], 'y2': [4, -4, 4] - }), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 y constant (dict(data={ 'x0': [0, 0, 0], 'x1': [-4, 0, 4], 'x2': [0, 0, 0], - }), ['x0', 'x1', 'x2'], np.array([-4, 0, 4]), 1), + }), dict(x=['x0', 'x1', 'x2'], y=np.array([-4, 0, 4]), axis=1)), # axis0 single (dict(data={ 'x': [0, -4, 0, np.nan, 0, 0, 0, np.nan, 0, 4, 0], 'y': [-4, 0, 4, np.nan, 4, 0, -4, np.nan, -4, 0, 4], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -474,7 +516,7 @@ def test_line_manual_range(DataFrame, df_kwargs, x, y, ax): 'y0': [-4, 0, 4], 'y1': [4, 0, -4], 'y2': [-4, 0, 4] - }), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 0), + }), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=0)), # axis0 multi with string (dict(data={ @@ -482,29 +524,40 @@ def test_line_manual_range(DataFrame, df_kwargs, x, y, ax): 'x1': [0, 0, 0], 'x2': [0, 4, 0], 'y0': [-4, 0, 4] - }), ['x0', 'x1', 'x2'], 'y0', 0), + }), dict(x=['x0', 'x1', 'x2'], y='y0', axis=0)), # axis1 RaggedArray (dict(data={ 'x': [[0, -4, 0], [0, 0, 0], [0, 4, 0]], 'y': [[-4, 0, 4], [4, 0, -4], [-4, 0, 4]], - }, dtype='Ragged[int64]'), 'x', 'y', 1), - -]) -def test_line_autorange(DataFrame, df_kwargs, x, y, ax): + }, dtype='Ragged[int64]'), dict(x='x', y='y', axis=1)), +] +if sp: + line_autorange_params.append( + # geometry + (dict(data={ + 'geom': [[0, -4, -4, 0, 0, 4], + [0, 4, 0, 0, 0, -4], + [0, -4, 4, 0, 0, 4]] + }, dtype='Line[int64]'), dict(geometry='geom')) + ) +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', line_autorange_params) +def test_line_autorange(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: - if df_kwargs.get('dtype', '').startswith('Ragged'): + dtype = df_kwargs.get('dtype', '') + if dtype.startswith('Ragged') or dtype.startswith('Line'): pytest.skip("Ragged array not supported with cudf") axis = ds.core.LinearAxis() lincoords = axis.compute_index( axis.compute_scale_and_translate((-4., 4.), 9), 9) - ddf = DataFrame(**df_kwargs) + ddf = DataFrame(geo='geometry' in cvs_kwargs, **df_kwargs) cvs = ds.Canvas(plot_width=9, plot_height=9) - agg = cvs.line(ddf, x, y, ds.count(), axis=ax) + agg = cvs.line(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 0, 3, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 0, 0, 0], @@ -599,7 +652,7 @@ def test_auto_range_line(DataFrame): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, np.nan], @@ -608,13 +661,13 @@ def test_auto_range_line(DataFrame): 'y0': [0, np.nan], 'y1': [-4, 4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, np.nan, 2, 4], 'y': [0, -4, 0, np.nan, 4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -622,15 +675,15 @@ def test_auto_range_line(DataFrame): 'x1': [np.nan, 2, 4], 'y0': [0, -4, 0], 'y1': [np.nan, 4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis1 ragged arrays (dict(data={ 'x': pd.array([[-4, -2, 0], [2, 4]]), 'y': pd.array([[0, -4, 0], [4, 0]]) - }, dtype='Ragged[float32]'), 'x', 'y', 1) + }, dtype='Ragged[float32]'), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_fixedrange(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: if df_kwargs.get('dtype', '').startswith('Ragged'): pytest.skip("Ragged array not supported with cudf") @@ -647,7 +700,7 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): ddf = DataFrame(**df_kwargs) - agg = cvs.area(ddf, x, y, ds.count(), axis=ax) + agg = cvs.area(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -662,7 +715,7 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, 0], @@ -671,7 +724,8 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'y0': [0, 0], 'y1': [-4, -4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), + dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 y constant (dict(data={ @@ -679,13 +733,13 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'x1': [-2, 2], 'x2': [0, 4], }, dtype='float32'), - ['x0', 'x1', 'x2'], np.array([0, -4, 0], dtype='float32'), 1), + dict(x=['x0', 'x1', 'x2'], y=np.array([0, -4, 0], dtype='float32'), axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, 0, 2, 4], 'y': [0, -4, 0, 0, -4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -693,22 +747,22 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'x1': [0, 2, 4], 'y0': [0, -4, 0], 'y1': [0, -4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis0 multi, y string (dict(data={ 'x0': [-4, -2, 0], 'x1': [0, 2, 4], 'y0': [0, -4, 0], - }, dtype='float32'), ['x0', 'x1'], 'y0', 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y='y0', axis=0)), # axis1 ragged arrays (dict(data={ 'x': [[-4, -2, 0], [0, 2, 4]], 'y': [[0, -4, 0], [0, -4, 0]] - }, dtype='Ragged[float32]'), 'x', 'y', 1) + }, dtype='Ragged[float32]'), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_autorange(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: if df_kwargs.get('dtype', '').startswith('Ragged'): pytest.skip("Ragged array not supported with cudf") @@ -722,7 +776,7 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): cvs = ds.Canvas(plot_width=13, plot_height=7) ddf = DataFrame(**df_kwargs) - agg = cvs.area(ddf, x, y, ds.count(), axis=ax) + agg = cvs.area(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], @@ -739,7 +793,7 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, np.nan], @@ -748,13 +802,13 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): 'y0': [0, np.nan], 'y1': [-4, 4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, np.nan, 2, 4], 'y': [0, -4, 0, np.nan, 4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -762,15 +816,15 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): 'x1': [np.nan, 2, 4], 'y0': [0, -4, 0], 'y1': [np.nan, 4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis1 ragged arrays (dict(data={ 'x': [[-4, -2, 0], [2, 4]], 'y': [[0, -4, 0], [4, 0]], - }, dtype='Ragged[float32]'), 'x', 'y', 1) + }, dtype='Ragged[float32]'), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: if df_kwargs.get('dtype', '').startswith('Ragged'): pytest.skip("Ragged array not supported with cudf") @@ -785,7 +839,7 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): ddf = DataFrame(**df_kwargs) - agg = cvs.area(ddf, x, y, ds.count(), axis=ax) + agg = cvs.area(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], @@ -802,7 +856,7 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,y_stack,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, 0], @@ -815,7 +869,8 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'y4': [-2, -2], 'y5': [0, 0], }, dtype='float32'), - ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], ['y3', 'y4', 'y5'], 1), + dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], + y_stack=['y3', 'y4', 'y5'], axis=1)), # axis1 y constant (dict(data={ @@ -823,16 +878,15 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'x1': [-2, 2], 'x2': [0, 4], }, dtype='float32'), - ['x0', 'x1', 'x2'], - np.array([0, -4, 0]), - np.array([0, -2, 0], dtype='float32'), 1), + dict(x=['x0', 'x1', 'x2'], y=np.array([0, -4, 0]), + y_stack=np.array([0, -2, 0], dtype='float32'), axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, 0, 2, 4], 'y': [0, -4, 0, 0, -4, 0], 'y_stack': [0, -2, 0, 0, -2, 0], - }), 'x', 'y', 'y_stack', 0), + }), dict(x='x', y='y', y_stack='y_stack', axis=0)), # axis0 multi (dict(data={ @@ -842,7 +896,8 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'y1': [0, -4, 0], 'y2': [0, -2, 0], 'y3': [0, -2, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], ['y2', 'y3'], 0), + }, dtype='float32'), + dict(x=['x0', 'x1'], y=['y0', 'y1'], y_stack=['y2', 'y3'], axis=0)), # axis0 multi, y string (dict(data={ @@ -850,16 +905,16 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'x1': [0, 2, 4], 'y0': [0, -4, 0], 'y2': [0, -2, 0], - }, dtype='float32'), ['x0', 'x1'], 'y0', 'y2', 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y='y0', y_stack='y2', axis=0)), # axis1 ragged arrays (dict(data={ 'x': [[-4, -2, 0], [0, 2, 4]], 'y': [[0, -4, 0], [0, -4, 0]], 'y_stack': [[0, -2, 0], [0, -2, 0]] - }, dtype='Ragged[float32]'), 'x', 'y', 'y_stack', 1) + }, dtype='Ragged[float32]'), dict(x='x', y='y', y_stack='y_stack', axis=1)) ]) -def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): +def test_area_to_line_autorange(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: if df_kwargs.get('dtype', '').startswith('Ragged'): pytest.skip("Ragged array not supported with cudf") @@ -873,7 +928,7 @@ def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): cvs = ds.Canvas(plot_width=13, plot_height=7) ddf = DataFrame(**df_kwargs) - agg = cvs.area(ddf, x, y, ds.count(), axis=ax, y_stack=y_stack) + agg = cvs.area(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], @@ -890,7 +945,7 @@ def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,y_stack,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, np.nan], @@ -903,14 +958,15 @@ def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): 'y5': [0, 0], 'y6': [0, 0] }, dtype='float32'), - ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], ['y4', 'y5', 'y6'], 1), + dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], + y_stack=['y4', 'y5', 'y6'], axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, np.nan, 2, 4], 'y': [0, -4, 0, np.nan, 4, 0], 'y_stack': [0, 0, 0, 0, 0, 0], - }), 'x', 'y', 'y_stack', 0), + }), dict(x='x', y='y', y_stack='y_stack', axis=0)), # axis0 multi (dict(data={ @@ -920,16 +976,17 @@ def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): 'y1': [np.nan, 4, 0], 'y2': [0, 0, 0], 'y3': [0, 0, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], ['y2', 'y3'], 0), + }, dtype='float32'), + dict(x=['x0', 'x1'], y=['y0', 'y1'], y_stack=['y2', 'y3'], axis=0)), # axis1 ragged arrays (dict(data={ 'x': [[-4, -2, 0], [2, 4]], 'y': [[0, -4, 0], [4, 0]], 'y_stack': [[0, 0, 0], [0, 0]], - }, dtype='Ragged[float32]'), 'x', 'y', 'y_stack', 1) + }, dtype='Ragged[float32]'), dict(x='x', y='y', y_stack='y_stack', axis=1)) ]) -def test_area_to_line_autorange_gap(DataFrame, df_kwargs, x, y, y_stack, ax): +def test_area_to_line_autorange_gap(DataFrame, df_kwargs, cvs_kwargs): if DataFrame is dask_cudf_DataFrame: if df_kwargs.get('dtype', '').startswith('Ragged'): pytest.skip("Ragged array not supported with cudf") @@ -946,7 +1003,7 @@ def test_area_to_line_autorange_gap(DataFrame, df_kwargs, x, y, y_stack, ax): # When a line is specified to fill to, this line is not included in # the fill. So we expect the y=0 line to not be filled. - agg = cvs.area(ddf, x, y, ds.count(), y_stack=y_stack, axis=ax) + agg = cvs.area(ddf, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/datashader/tests/test_pandas.py b/datashader/tests/test_pandas.py index be21d117f..3be1c3d04 100644 --- a/datashader/tests/test_pandas.py +++ b/datashader/tests/test_pandas.py @@ -1,6 +1,8 @@ from __future__ import absolute_import from collections import OrderedDict import os +from numpy import nan + import numpy as np import pandas as pd import xarray as xr @@ -31,6 +33,22 @@ else: test_gpu = None + +try: + import spatialpandas as sp + from spatialpandas.geometry import LineDtype +except ImportError: + LineDtype = None + sp = None + + +def pd_DataFrame(*args, **kwargs): + if kwargs.pop("geo", False): + return sp.GeoDataFrame(*args, **kwargs) + else: + return pd.DataFrame(*args, **kwargs) + + try: import cudf import cupy @@ -40,16 +58,18 @@ raise ImportError def cudf_DataFrame(*args, **kwargs): + assert not kwargs.pop("geo", False) return cudf.DataFrame.from_pandas( pd.DataFrame(*args, **kwargs), nan_as_null=False ) df_cuda = cudf_DataFrame(df_pd) dfs = [df_pd, df_cuda] - DataFrames = [pd.DataFrame, cudf_DataFrame] + DataFrames = [pd_DataFrame, cudf_DataFrame] except ImportError: cudf = cupy = None dfs = [df_pd] - DataFrames = [pd.DataFrame] + DataFrames = [pd_DataFrame] + c = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 1), y_range=(0, 1)) c_logx = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 10), @@ -323,6 +343,48 @@ def test_log_axis_points(df): assert_eq_xr(c_logxy.points(df, 'log_x', 'log_y', ds.count('i32')), out) +@pytest.mark.skipif(not sp, reason="spatialpandas not installed") +def test_points_geometry_point(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0., 2.), 3), 3) + + df = sp.GeoDataFrame({ + 'geom': pd.array( + [[0, 0], [0, 1], [1, 1], [0, 2], [1, 2], [2, 2]], dtype='Point[float64]'), + 'v': [1, 2, 2, 3, 3, 3] + }) + + cvs = ds.Canvas(plot_width=3, plot_height=3) + agg = cvs.points(df, geometry='geom', agg=ds.sum('v')) + sol = np.array([[1, nan, nan], + [2, 2, nan], + [3, 3, 3]], dtype='float64') + out = xr.DataArray(sol, coords=[lincoords, lincoords], + dims=['y', 'x']) + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not sp, reason="spatialpandas not installed") +def test_points_geometry_multipoint(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0., 2.), 3), 3) + + df = sp.GeoDataFrame({ + 'geom': pd.array( + [[0, 0], [0, 1, 1, 1], [0, 2, 1, 2, 2, 2]], dtype='MultiPoint[float64]'), + 'v': [1, 2, 3] + }) + + cvs = ds.Canvas(plot_width=3, plot_height=3) + agg = cvs.points(df, geometry='geom', agg=ds.sum('v')) + sol = np.array([[1, nan, nan], + [2, 2, nan], + [3, 3, 3]], dtype='float64') + out = xr.DataArray(sol, coords=[lincoords, lincoords], + dims=['y', 'x']) + assert_eq_xr(agg, out) + + def test_line(): axis = ds.core.LinearAxis() lincoords = axis.compute_index(axis.compute_scale_and_translate((-3., 3.), 7), 7) @@ -688,8 +750,7 @@ def test_bug_570(): # # Line tests -@pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_args,x,y,ax', [ +line_manual_range_params = [ # axis1 none constant ([{ 'x0': [4, -4], @@ -698,27 +759,27 @@ def test_bug_570(): 'y0': [0, 0], 'y1': [-4, 4], 'y2': [0, 0] - }], ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }], dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 x constant ([{ 'y0': [0, 0], 'y1': [-4, 4], 'y2': [0, 0] - }], np.array([-4, 0, 4]), ['y0', 'y1', 'y2'], 1), + }], dict(x=np.array([-4, 0, 4]), y=['y0', 'y1', 'y2'], axis=1)), # axis1 y constant ([{ 'x0': [0, 0], 'x1': [-4, 4], 'x2': [0, 0] - }], ['x0', 'x1', 'x2'], np.array([-4, 0, 4]), 1), + }], dict(x=['x0', 'x1', 'x2'], y=np.array([-4, 0, 4]), axis=1)), # axis0 single ([{ 'x': [0, -4, 0, np.nan, 0, 4, 0], 'y': [-4, 0, 4, np.nan, -4, 0, 4], - }], 'x', 'y', 0), + }], dict(x='x', y='y', axis=0)), # axis0 multi ([{ @@ -726,7 +787,7 @@ def test_bug_570(): 'x1': [0, 4, 0], 'y0': [-4, 0, 4], 'y1': [-4, 0, 4], - }], ['x0', 'x1'], ['y0', 'y1'], 0), + }], dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis0 multi with string ([{ @@ -734,20 +795,35 @@ def test_bug_570(): 'x1': [0, 4, 0], 'y0': [-4, 0, 4], 'y1': [-4, 0, 4], - }], ['x0', 'x1'], 'y0', 0), + }], dict(x=['x0', 'x1'], y='y0', axis=0)), # axis1 ragged arrays ([{ 'x': pd.array([[4, 0], [0, -4, 0, 4]], dtype='Ragged[float32]'), 'y': pd.array([[0, -4], [-4, 0, 4, 0]], dtype='Ragged[float32]') - }], 'x', 'y', 1) -]) -def test_line_manual_range(DataFrame, df_args, x, y, ax): + }], dict(x='x', y='y', axis=1)), +] +if sp: + line_manual_range_params.append( + # geometry + ([{ + 'geom': pd.array( + [[4, 0, 0, -4], [0, -4, -4, 0, 0, 4, 4, 0]], dtype='Line[float32]' + ), + }], dict(geometry='geom')) + ) +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('df_args,cvs_kwargs', line_manual_range_params) +def test_line_manual_range(DataFrame, df_args, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: - if isinstance(getattr(df_args[0].get('x', []), 'dtype', ''), RaggedDtype): + if (isinstance(getattr(df_args[0].get('x', []), 'dtype', ''), RaggedDtype) or + sp and isinstance( + getattr(df_args[0].get('geom', []), 'dtype', ''), LineDtype + ) + ): pytest.skip("cudf DataFrames do not support extension types") - df = DataFrame(*df_args) + df = DataFrame(geo='geometry' in cvs_kwargs, *df_args) axis = ds.core.LinearAxis() lincoords = axis.compute_index( @@ -756,7 +832,7 @@ def test_line_manual_range(DataFrame, df_args, x, y, ax): cvs = ds.Canvas(plot_width=7, plot_height=7, x_range=(-3, 3), y_range=(-3, 3)) - agg = cvs.line(df, x, y, ds.count(), axis=ax) + agg = cvs.line(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], @@ -771,8 +847,7 @@ def test_line_manual_range(DataFrame, df_args, x, y, ax): assert_eq_xr(agg, out) -@pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_args,x,y,ax', [ +line_autorange_params = [ # axis1 none constant ([{ 'x0': [0, 0], @@ -781,20 +856,20 @@ def test_line_manual_range(DataFrame, df_args, x, y, ax): 'y0': [-4, -4], 'y1': [0, 0], 'y2': [4, 4] - }], ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }], dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 y constant ([{ 'x0': [0, 0], 'x1': [-4, 4], 'x2': [0, 0] - }], ['x0', 'x1', 'x2'], np.array([-4, 0, 4]), 1), + }], dict(x=['x0', 'x1', 'x2'], y=np.array([-4, 0, 4]), axis=1)), # axis0 single ([{ 'x': [0, -4, 0, np.nan, 0, 4, 0], 'y': [-4, 0, 4, np.nan, -4, 0, 4], - }], 'x', 'y', 0), + }], dict(x='x', y='y', axis=0)), # axis0 multi ([{ @@ -802,7 +877,7 @@ def test_line_manual_range(DataFrame, df_args, x, y, ax): 'x1': [0, 4, 0], 'y0': [-4, 0, 4], 'y1': [-4, 0, 4], - }], ['x0', 'x1'], ['y0', 'y1'], 0), + }], dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis0 multi with string ([{ @@ -810,20 +885,35 @@ def test_line_manual_range(DataFrame, df_args, x, y, ax): 'x1': [0, 4, 0], 'y0': [-4, 0, 4], 'y1': [-4, 0, 4], - }], ['x0', 'x1'], 'y0', 0), + }], dict(x=['x0', 'x1'], y='y0', axis=0)), # axis1 ragged arrays ([{ 'x': pd.array([[0, -4, 0], [0, 4, 0]], dtype='Ragged[float32]'), 'y': pd.array([[-4, 0, 4], [-4, 0, 4]], dtype='Ragged[float32]') - }], 'x', 'y', 1) -]) -def test_line_autorange(DataFrame, df_args, x, y, ax): + }], dict(x='x', y='y', axis=1)), +] +if sp: + line_autorange_params.append( + # geometry + ([{ + 'geom': pd.array( + [[0, -4, -4, 0, 0, 4], [0, -4, 4, 0, 0, 4]], dtype='Line[float32]' + ), + }], dict(geometry='geom')) + ) +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('df_args,cvs_kwargs', line_autorange_params) +def test_line_autorange(DataFrame, df_args, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: - if isinstance(getattr(df_args[0].get('x', []), 'dtype', ''), RaggedDtype): + if (isinstance(getattr(df_args[0].get('x', []), 'dtype', ''), RaggedDtype) or + sp and isinstance( + getattr(df_args[0].get('geom', []), 'dtype', ''), LineDtype + ) + ): pytest.skip("cudf DataFrames do not support extension types") - df = DataFrame(*df_args) + df = DataFrame(geo='geometry' in cvs_kwargs, *df_args) axis = ds.core.LinearAxis() lincoords = axis.compute_index( @@ -831,7 +921,7 @@ def test_line_autorange(DataFrame, df_args, x, y, ax): cvs = ds.Canvas(plot_width=9, plot_height=9) - agg = cvs.line(df, x, y, ds.count(), axis=ax) + agg = cvs.line(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 0, 2, 0, 0, 0, 0], [0, 0, 0, 1, 0, 1, 0, 0, 0], @@ -956,7 +1046,7 @@ def test_line_autorange_axis1_ragged(): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, np.nan], @@ -965,13 +1055,13 @@ def test_line_autorange_axis1_ragged(): 'y0': [0, np.nan], 'y1': [-4, 4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, np.nan, 2, 4], 'y': [0, -4, 0, np.nan, 4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -979,15 +1069,15 @@ def test_line_autorange_axis1_ragged(): 'x1': [np.nan, 2, 4], 'y0': [0, -4, 0], 'y1': [np.nan, 4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis1 ragged arrays (dict(data={ 'x': pd.array([[-4, -2, 0], [2, 4]], dtype='Ragged[float32]'), 'y': pd.array([[0, -4, 0], [4, 0]], dtype='Ragged[float32]') - }), 'x', 'y', 1) + }), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_fixedrange(DataFrame, df_kwargs, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: if isinstance(getattr(df_kwargs['data'].get('x', []), 'dtype', ''), RaggedDtype): pytest.skip("cudf DataFrames do not support extension types") @@ -1004,7 +1094,7 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): cvs = ds.Canvas(plot_width=9, plot_height=5, x_range=[-3.75, 3.75], y_range=[-2.25, 2.25]) - agg = cvs.area(df, x, y, ds.count(), axis=ax) + agg = cvs.area(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -1019,7 +1109,7 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, 0], @@ -1028,7 +1118,7 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'y0': [0, 0], 'y1': [-4, -4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis1 y constant (dict(data={ @@ -1036,13 +1126,13 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'x1': [-2, 2], 'x2': [0, 4], }, dtype='float32'), - ['x0', 'x1', 'x2'], np.array([0, -4, 0], dtype='float32'), 1), + dict(x=['x0', 'x1', 'x2'], y=np.array([0, -4, 0], dtype='float32'), axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, 0, 2, 4], 'y': [0, -4, 0, 0, -4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -1050,22 +1140,22 @@ def test_area_to_zero_fixedrange(DataFrame, df_kwargs, x, y, ax): 'x1': [0, 2, 4], 'y0': [0, -4, 0], 'y1': [0, -4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis0 multi, y string (dict(data={ 'x0': [-4, -2, 0], 'x1': [0, 2, 4], 'y0': [0, -4, 0], - }, dtype='float32'), ['x0', 'x1'], 'y0', 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y='y0', axis=0)), # axis1 ragged arrays (dict(data={ 'x': pd.array([[-4, -2, 0], [0, 2, 4]], dtype='Ragged[float32]'), 'y': pd.array([[0, -4, 0], [0, -4, 0]], dtype='Ragged[float32]') - }), 'x', 'y', 1) + }), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_autorange(DataFrame, df_kwargs, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: if isinstance(getattr(df_kwargs['data'].get('x', []), 'dtype', ''), RaggedDtype): pytest.skip("cudf DataFrames do not support extension types") @@ -1080,7 +1170,7 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): cvs = ds.Canvas(plot_width=13, plot_height=7) - agg = cvs.area(df, x, y, ds.count(), axis=ax) + agg = cvs.area(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], @@ -1097,7 +1187,7 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, np.nan], @@ -1108,13 +1198,13 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): # 'y0': [0, 1], 'y1': [-4, 4], 'y2': [0, 0] - }, dtype='float32'), ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], 1), + }, dtype='float32'), dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, np.nan, 2, 4], 'y': [0, -4, 0, np.nan, 4, 0], - }), 'x', 'y', 0), + }), dict(x='x', y='y', axis=0)), # axis0 multi (dict(data={ @@ -1122,15 +1212,15 @@ def test_area_to_zero_autorange(DataFrame, df_kwargs, x, y, ax): 'x1': [np.nan, 2, 4], 'y0': [0, -4, 0], 'y1': [np.nan, 4, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], axis=0)), # axis1 ragged arrays (dict(data={ 'x': pd.array([[-4, -2, 0], [2, 4]], dtype='Ragged[float32]'), 'y': pd.array([[0, -4, 0], [4, 0]], dtype='Ragged[float32]') - }), 'x', 'y', 1) + }), dict(x='x', y='y', axis=1)) ]) -def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): +def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: if isinstance(getattr(df_kwargs['data'].get('x', []), 'dtype', ''), RaggedDtype): pytest.skip("cudf DataFrames do not support extension types") @@ -1145,7 +1235,7 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): cvs = ds.Canvas(plot_width=13, plot_height=7) - agg = cvs.area(df, x, y, ds.count(), axis=ax) + agg = cvs.area(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], @@ -1162,7 +1252,7 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): @pytest.mark.parametrize('DataFrame', DataFrames) -@pytest.mark.parametrize('df_kwargs,x,y,y_stack,ax', [ +@pytest.mark.parametrize('df_kwargs,cvs_kwargs', [ # axis1 none constant (dict(data={ 'x0': [-4, 0], @@ -1175,7 +1265,8 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'y4': [-2, -2], 'y5': [0, 0], }, dtype='float32'), - ['x0', 'x1', 'x2'], ['y0', 'y1', 'y2'], ['y3', 'y4', 'y5'], 1), + dict(x=['x0', 'x1', 'x2'], y=['y0', 'y1', 'y2'], + y_stack=['y3', 'y4', 'y5'], axis=1)), # axis1 y constant (dict(data={ @@ -1183,16 +1274,15 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'x1': [-2, 2], 'x2': [0, 4], }, dtype='float32'), - ['x0', 'x1', 'x2'], - np.array([0, -4, 0]), - np.array([0, -2, 0], dtype='float32'), 1), + dict(x=['x0', 'x1', 'x2'], y=np.array([0, -4, 0]), + y_stack=np.array([0, -2, 0], dtype='float32'), axis=1)), # axis0 single (dict(data={ 'x': [-4, -2, 0, 0, 2, 4], 'y': [0, -4, 0, 0, -4, 0], 'y_stack': [0, -2, 0, 0, -2, 0], - }), 'x', 'y', 'y_stack', 0), + }), dict(x='x', y='y', y_stack='y_stack', axis=0)), # axis0 multi (dict(data={ @@ -1202,7 +1292,8 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'y1': [0, -4, 0], 'y2': [0, -2, 0], 'y3': [0, -2, 0], - }, dtype='float32'), ['x0', 'x1'], ['y0', 'y1'], ['y2', 'y3'], 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y=['y0', 'y1'], + y_stack=['y2', 'y3'], axis=0)), # axis0 multi, y string (dict(data={ @@ -1210,16 +1301,16 @@ def test_area_to_zero_autorange_gap(DataFrame, df_kwargs, x, y, ax): 'x1': [0, 2, 4], 'y0': [0, -4, 0], 'y2': [0, -2, 0], - }, dtype='float32'), ['x0', 'x1'], 'y0', 'y2', 0), + }, dtype='float32'), dict(x=['x0', 'x1'], y='y0', y_stack='y2', axis=0)), # axis1 ragged arrays (dict(data={ 'x': pd.array([[-4, -2, 0], [0, 2, 4]], dtype='Ragged[float32]'), 'y': pd.array([[0, -4, 0], [0, -4, 0]], dtype='Ragged[float32]'), 'y_stack': pd.array([[0, -2, 0], [0, -2, 0]], dtype='Ragged[float32]') - }), 'x', 'y', 'y_stack', 1) + }), dict(x='x', y='y', y_stack='y_stack', axis=1)) ]) -def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): +def test_area_to_line_autorange(DataFrame, df_kwargs, cvs_kwargs): if cudf and DataFrame is cudf_DataFrame: if isinstance(getattr(df_kwargs['data'].get('x', []), 'dtype', ''), RaggedDtype): pytest.skip("cudf DataFrames do not support extension types") @@ -1234,7 +1325,7 @@ def test_area_to_line_autorange(DataFrame, df_kwargs, x, y, y_stack, ax): cvs = ds.Canvas(plot_width=13, plot_height=7) - agg = cvs.area(df, x, y, ds.count(), axis=ax, y_stack=y_stack) + agg = cvs.area(df, agg=ds.count(), **cvs_kwargs) sol = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], diff --git a/datashader/tests/test_polygons.py b/datashader/tests/test_polygons.py new file mode 100644 index 000000000..df835473e --- /dev/null +++ b/datashader/tests/test_polygons.py @@ -0,0 +1,292 @@ +import pytest +import pandas as pd +import numpy as np +import xarray as xr +from numpy import nan +import datashader as ds +from datashader.tests.test_pandas import assert_eq_xr +import dask.dataframe as dd + +try: + # Import to register extension arrays + import spatialpandas # noqa (register EAs) + from spatialpandas import GeoDataFrame + from spatialpandas.geometry import MultiPolygonArray +except ImportError: + spatialpandas = None + GeoDataFrame = None + MultiPolygonArray = None + + +def dask_GeoDataFrame(*args, **kwargs): + return dd.from_pandas(GeoDataFrame(*args, **kwargs), npartitions=3) + + +DataFrames = [GeoDataFrame, dask_GeoDataFrame] + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +def test_multipolygon_manual_range(DataFrame): + df = DataFrame({ + 'polygons': pd.Series([[ + [ + [0, 0, 2, 0, 2, 2, 1, 3, 0, 0], + [1, 0.25, 1, 2, 1.75, .25, 0.25, 0.25] + ], [ + [2.5, 1, 4, 1, 4, 2, 2.5, 2, 2.5, 1] + ], + ]], dtype='MultiPolygon[float64]'), + 'v': [1] + }) + + cvs = ds.Canvas(plot_width=16, plot_height=16) + agg = cvs.polygons(df, geometry='polygons', agg=ds.count()) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((0., 4.), 16), 16) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((0., 3.), 16), 16) + + sol = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ], dtype='i4') + + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +def test_multiple_polygons_auto_range(DataFrame): + df = DataFrame({ + 'polygons': pd.Series([[ + [ + [0, 0, 2, 0, 2, 2, 1, 3, 0, 0], + [1, 0.25, 1, 2, 1.75, .25, 0.25, 0.25] + ], [ + [2.5, 1, 4, 1, 4, 2, 2.5, 2, 2.5, 1] + ], + ]], dtype='MultiPolygon[float64]'), + 'v': [1] + }) + + cvs = ds.Canvas(plot_width=16, plot_height=16, + x_range=[-1, 3.5], y_range=[0.1, 2]) + agg = cvs.polygons(df, geometry='polygons', agg=ds.count()) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((-1, 3.5), 16), 16) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((0.1, 2), 16), 16) + + sol = np.array([ + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1] + ], dtype='i4') + + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +def test_no_overlap(DataFrame): + df = DataFrame({ + 'polygons': pd.Series([ + [ + [1, 1, 2, 2, 1, 3, 0, 2, 1, 1], + [0.5, 1.5, 0.5, 2.5, 1.5, 2.5, 1.5, 1.5, 0.5, 1.5] + ], [ + [0.5, 1.5, 1.5, 1.5, 1.5, 2.5, 0.5, 2.5, 0.5, 1.5] + ], [ + [0, 1, 2, 1, 2, 3, 0, 3, 0, 1, 1, 1, 0, 2, 1, 3, 2, 2, 1, 1] + ] + ], dtype='Polygon[float64]'), + }) + + cvs = ds.Canvas(plot_width=16, plot_height=16) + agg = cvs.polygons(df, geometry='polygons', agg=ds.count()) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((0, 2), 16), 16) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((1, 3), 16), 16) + + sol = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ], dtype='i4') + + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +def test_no_overlap_agg(DataFrame): + df = DataFrame({ + 'polygons': pd.Series([ + [[1, 1, 2, 2, 1, 3, 0, 2, 1, 1], + [0.5, 1.5, 0.5, 2.5, 1.5, 2.5, 1.5, 1.5, 0.5, 1.5]], + [[0.5, 1.5, 1.5, 1.5, 1.5, 2.5, 0.5, 2.5, 0.5, 1.5]], + [[0, 1, 2, 1, 2, 3, 0, 3, 0, 1, 1, 1, 0, 2, 1, 3, 2, 2, 1, 1]] + ], dtype='Polygon[float64]'), + 'v': range(3) + }) + + cvs = ds.Canvas(plot_width=16, plot_height=16) + agg = cvs.polygons(df, geometry='polygons', agg=ds.sum('v')) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((0, 2), 16), 16) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((1, 3), 16), 16) + + sol = np.array([ + [nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan], + [nan, 2., 2., 2., 2., 2., 2., 2., 0., 0., 2., 2., 2., 2., 2., 2.], + [nan, 2., 2., 2., 2., 2., 2., 0., 0., 0., 0., 2., 2., 2., 2., 2.], + [nan, 2., 2., 2., 2., 2., 0., 0., 0., 0., 0., 0., 2., 2., 2., 2.], + [nan, 2., 2., 2., 2., 0., 0., 0., 0., 0., 0., 0., 0., 2., 2., 2.], + [nan, 2., 2., 2., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 2., 2.], + [nan, 2., 2., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 2.], + [nan, 2., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [nan, 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [nan, 2., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [nan, 2., 2., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 2.], + [nan, 2., 2., 2., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 2., 2.], + [nan, 2., 2., 2., 2., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2., 2.], + [nan, 2., 2., 2., 2., 2., 0., 0., 0., 0., 0., 0., 2., 2., 2., 2.], + [nan, 2., 2., 2., 2., 2., 2., 0., 0., 0., 0., 2., 2., 2., 2., 2.], + [nan, 2., 2., 2., 2., 2., 2., 2., 0., 0., 2., 2., 2., 2., 2., 2.] + ]) + + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('scale', [4, 100]) +def test_multipolygon_subpixel_vertical(DataFrame, scale): + df = GeoDataFrame({ + 'geometry': MultiPolygonArray([[ + [[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]], + [[2, 0, 3, 0, 3, 1, 2, 1, 2, 0]], + ]]) + }) + + cvs = ds.Canvas( + plot_height=8, plot_width=8, + x_range=(0, 4), + y_range=(-2 * scale, 2 * scale) + ) + agg = cvs.polygons(df, 'geometry', agg=ds.count()) + + sol = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0] + ], dtype=np.int32) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((0, 4), 8), 8) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((-2 * scale, 2 * scale), 8), 8) + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + assert_eq_xr(agg, out) + + +@pytest.mark.skipif(not spatialpandas, reason="spacialpandas not installed") +@pytest.mark.parametrize('DataFrame', DataFrames) +@pytest.mark.parametrize('scale', [4, 100]) +def test_multipolygon_subpixel_horizontal(DataFrame, scale): + df = GeoDataFrame({ + 'geometry': MultiPolygonArray([[ + [[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]], + [[0, 2, 1, 2, 1, 3, 0, 3, 0, 2]], + ]]) + }) + + cvs = ds.Canvas( + plot_height=8, plot_width=8, + x_range=(-2 * scale, 2 * scale), + y_range=(0, 4) + ) + agg = cvs.polygons(df, 'geometry', agg=ds.count()) + + sol = np.array([ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0] + ], dtype=np.int32) + + axis = ds.core.LinearAxis() + lincoords_x = axis.compute_index( + axis.compute_scale_and_translate((-2 * scale, 2 * scale), 8), 8) + lincoords_y = axis.compute_index( + axis.compute_scale_and_translate((0, 4), 8), 8) + out = xr.DataArray(sol, coords=[lincoords_y, lincoords_x], dims=['y', 'x']) + assert_eq_xr(agg, out) diff --git a/datashader/utils.py b/datashader/utils.py index df82a8f93..7a6592319 100644 --- a/datashader/utils.py +++ b/datashader/utils.py @@ -24,6 +24,12 @@ except ImportError: cudf = None +try: + from spatialpandas.geometry import GeometryDtype +except ImportError: + GeometryDtype = type(None) + + ngjit = nb.jit(nopython=True, nogil=True) @@ -408,7 +414,7 @@ def dshape_from_pandas_helper(col): # Pandas stores this as a pytz.tzinfo, but DataShape wants a string tz = str(tz) return datashape.Option(datashape.DateTime(tz=tz)) - elif isinstance(col.dtype, RaggedDtype): + elif isinstance(col.dtype, (RaggedDtype, GeometryDtype)): return col.dtype dshape = datashape.CType.from_numpy_dtype(col.dtype) dshape = datashape.string if dshape == datashape.object_ else dshape diff --git a/setup.py b/setup.py index 3d539e02e..59b89233f 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'nbsmoke ==0.2.8', # test pinning to allow hv.extension 'fastparquet >=0.1.6', # optional dependency 'pandas >=0.24.1', # optional ragged array support + 'holoviews >=1.10.0', ], 'examples': examples, 'examples_extra': examples + [