-
-
Notifications
You must be signed in to change notification settings - Fork 372
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
Add Polygon support #819
Closed
+1,656
−208
Closed
Add Polygon support #819
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b38e7cf
Added Polygon support
jonmmease 4c6dd32
math.isfinite not availabe on Python 2. Use numpy.isfinite
jonmmease 1a644c5
Allow cvs.points to input PolygonsArray
jonmmease d7826ff
Python 2
jonmmease 90c4d1d
Python 2
jonmmease e429a5e
Update datashader/core.py
jonmmease File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
Added Polygon support
- New PolygonsArray/LinesArray/PointsArray extension arrays - New Canvas.polygons() method to rasterize polygons - Updates to Canvas.points and Canvas.lines to accept new geometry arrays.
commit b38e7cf21b033602973c2cd0d8ae21e70c88e16c
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .lines import Lines, LinesArray, LinesDtype # noqa (API import) | ||
from .points import Points, PointsArray, PointsDtype # noqa (API import) | ||
from .polygons import Polygons, PolygonsArray, PolygonsDtype # noqa (API import) | ||
from .base import Geom, GeomArray, GeomDtype # noqa (API import) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
from __future__ import absolute_import | ||
from math import isfinite, inf | ||
import re | ||
from functools import total_ordering | ||
import numpy as np | ||
from pandas.core.dtypes.dtypes import register_extension_dtype | ||
|
||
from datashader.datatypes import _RaggedElement, RaggedDtype, RaggedArray | ||
from datashader.utils import ngjit | ||
|
||
|
||
@total_ordering | ||
class Geom(_RaggedElement): | ||
def __repr__(self): | ||
data = [(x, y) for x, y in zip(self.xs, self.ys)] | ||
return "{typ}({data})".format(typ=self.__class__.__name__, data=data) | ||
|
||
@classmethod | ||
def _shapely_to_array_parts(cls, shape): | ||
raise NotImplementedError() | ||
|
||
@classmethod | ||
def from_shapely(cls, shape): | ||
shape_parts = cls._shapely_to_array_parts(shape) | ||
return cls(np.concatenate(shape_parts)) | ||
|
||
@property | ||
def xs(self): | ||
return self.array[0::2] | ||
|
||
@property | ||
def ys(self): | ||
return self.array[1::2] | ||
|
||
@property | ||
def bounds(self): | ||
return bounds_interleaved(self.array) | ||
|
||
@property | ||
def bounds_x(self): | ||
return bounds_interleaved_1d(self.array, 0) | ||
|
||
@property | ||
def bounds_y(self): | ||
return bounds_interleaved_1d(self.array, 1) | ||
|
||
@property | ||
def length(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def area(self): | ||
raise NotImplementedError() | ||
|
||
|
||
@register_extension_dtype | ||
class GeomDtype(RaggedDtype): | ||
_type_name = "Geom" | ||
_subtype_re = re.compile(r"^geom\[(?P<subtype>\w+)\]$") | ||
|
||
@classmethod | ||
def construct_array_type(cls): | ||
return GeomArray | ||
|
||
|
||
class GeomArray(RaggedArray): | ||
_element_type = Geom | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(GeomArray, self).__init__(*args, **kwargs) | ||
# Validate that there are an even number of elements in each Geom element | ||
if (any(self.start_indices % 2) or | ||
len(self) and (len(self.flat_array) - self.start_indices[-1]) % 2 > 0): | ||
raise ValueError("There must be an even number of elements in each row") | ||
|
||
@property | ||
def _dtype_class(self): | ||
return GeomDtype | ||
|
||
@property | ||
def xs(self): | ||
start_indices = self.start_indices // 2 | ||
flat_array = self.flat_array[0::2] | ||
return RaggedArray({"start_indices": start_indices, "flat_array": flat_array}) | ||
|
||
@property | ||
def ys(self): | ||
start_indices = self.start_indices // 2 | ||
flat_array = self.flat_array[1::2] | ||
return RaggedArray({"start_indices": start_indices, "flat_array": flat_array}) | ||
|
||
def to_geopandas(self): | ||
from geopandas.array import from_shapely | ||
return from_shapely([el.to_shapely() for el in self]) | ||
|
||
@classmethod | ||
def from_geopandas(cls, ga): | ||
line_parts = [ | ||
cls._element_type._shapely_to_array_parts(shape) for shape in ga | ||
] | ||
line_lengths = [ | ||
sum([len(part) for part in parts]) | ||
for parts in line_parts | ||
] | ||
flat_array = np.concatenate( | ||
[part for parts in line_parts for part in parts] | ||
) | ||
start_indices = np.concatenate( | ||
[[0], line_lengths[:-1]] | ||
).astype('uint').cumsum() | ||
return cls({ | ||
'start_indices': start_indices, 'flat_array': flat_array | ||
}) | ||
|
||
@property | ||
def bounds(self): | ||
return bounds_interleaved(self.flat_array) | ||
|
||
@property | ||
def bounds_x(self): | ||
return bounds_interleaved_1d(self.flat_array, 0) | ||
|
||
@property | ||
def bounds_y(self): | ||
return bounds_interleaved_1d(self.flat_array, 1) | ||
|
||
@property | ||
def length(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def area(self): | ||
raise NotImplementedError() | ||
|
||
|
||
@ngjit | ||
def _geom_map(start_indices, flat_array, result, fn): | ||
n = len(start_indices) | ||
for i in range(n): | ||
start = start_indices[i] | ||
stop = start_indices[i + 1] if i < n - 1 else len(flat_array) | ||
result[i] = fn(flat_array[start:stop]) | ||
|
||
|
||
@ngjit | ||
def bounds_interleaved(values): | ||
""" | ||
compute bounds | ||
""" | ||
xmin = inf | ||
ymin = inf | ||
xmax = -inf | ||
ymax = -inf | ||
|
||
for i in range(0, len(values), 2): | ||
x = values[i] | ||
if isfinite(x): | ||
xmin = min(xmin, x) | ||
xmax = max(xmax, x) | ||
|
||
y = values[i + 1] | ||
if isfinite(y): | ||
ymin = min(ymin, y) | ||
ymax = max(ymax, y) | ||
|
||
return (xmin, ymin, xmax, ymax) | ||
|
||
|
||
@ngjit | ||
def bounds_interleaved_1d(values, offset): | ||
""" | ||
compute bounds | ||
""" | ||
vmin = inf | ||
vmax = -inf | ||
|
||
for i in range(0, len(values), 2): | ||
v = values[i + offset] | ||
if isfinite(v): | ||
vmin = min(vmin, v) | ||
vmax = max(vmax, v) | ||
|
||
return (vmin, vmax) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from __future__ import absolute_import, division | ||
import re | ||
from math import sqrt, isfinite | ||
from functools import total_ordering | ||
import numpy as np | ||
|
||
from pandas.core.dtypes.dtypes import register_extension_dtype | ||
|
||
from datashader.geom.base import Geom, GeomDtype, GeomArray, _geom_map | ||
from datashader.utils import ngjit | ||
|
||
|
||
try: | ||
# See if we can register extension type with dask >= 1.1.0 | ||
from dask.dataframe.extensions import make_array_nonempty | ||
except ImportError: | ||
make_array_nonempty = None | ||
|
||
|
||
@total_ordering | ||
class Lines(Geom): | ||
@classmethod | ||
def _shapely_to_array_parts(cls, shape): | ||
import shapely.geometry as sg | ||
if isinstance(shape, (sg.LineString, sg.LinearRing)): | ||
# Single line | ||
return [np.asarray(shape.ctypes)] | ||
elif isinstance(shape, sg.MultiLineString): | ||
shape = list(shape) | ||
line_parts = [np.asarray(shape[0].ctypes)] | ||
line_separator = np.array([np.inf, np.inf]) | ||
for line in shape[1:]: | ||
line_parts.append(line_separator) | ||
line_parts.append(np.asarray(line.ctypes)) | ||
return line_parts | ||
else: | ||
raise ValueError(""" | ||
Received invalid value of type {typ}. Must be an instance of LineString, | ||
MultiLineString, or LinearRing""".format(typ=type(shape).__name__)) | ||
|
||
def to_shapely(self): | ||
import shapely.geometry as sg | ||
line_breaks = np.concatenate( | ||
[[-2], np.nonzero(~np.isfinite(self.array))[0][0::2], [len(self.array)]] | ||
) | ||
line_arrays = [self.array[start + 2:stop] | ||
for start, stop in zip(line_breaks[:-1], line_breaks[1:])] | ||
|
||
lines = [sg.LineString(line_array.reshape(len(line_array) // 2, 2)) | ||
for line_array in line_arrays] | ||
|
||
if len(lines) == 1: | ||
return lines[0] | ||
else: | ||
return sg.MultiLineString(lines) | ||
|
||
@property | ||
def length(self): | ||
return compute_length(self.array) | ||
|
||
@property | ||
def area(self): | ||
return 0.0 | ||
|
||
|
||
@register_extension_dtype | ||
class LinesDtype(GeomDtype): | ||
_type_name = "Lines" | ||
_subtype_re = re.compile(r"^lines\[(?P<subtype>\w+)\]$") | ||
|
||
@classmethod | ||
def construct_array_type(cls): | ||
return LinesArray | ||
|
||
|
||
class LinesArray(GeomArray): | ||
_element_type = Lines | ||
|
||
@property | ||
def _dtype_class(self): | ||
return LinesDtype | ||
|
||
@property | ||
def length(self): | ||
result = np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
_geom_map(self.start_indices, self.flat_array, result, compute_length) | ||
return result | ||
|
||
@property | ||
def area(self): | ||
return np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
|
||
|
||
@ngjit | ||
def compute_length(values): | ||
total_len = 0.0 | ||
x0 = values[0] | ||
y0 = values[1] | ||
for i in range(2, len(values), 2): | ||
x1 = values[i] | ||
y1 = values[i+1] | ||
|
||
if isfinite(x0) and isfinite(y0) and isfinite(x1) and isfinite(y1): | ||
total_len += sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) | ||
|
||
x0 = x1 | ||
y0 = y1 | ||
|
||
return total_len | ||
|
||
|
||
def lines_array_non_empty(dtype): | ||
return LinesArray([[1, 0, 1, 1], [1, 2, 0, 0]], dtype=dtype) | ||
|
||
|
||
if make_array_nonempty: | ||
make_array_nonempty.register(LinesDtype)(lines_array_non_empty) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import re | ||
from functools import total_ordering | ||
import numpy as np | ||
from pandas.core.dtypes.dtypes import register_extension_dtype | ||
|
||
from datashader.geom.base import Geom, GeomDtype, GeomArray | ||
|
||
|
||
try: | ||
# See if we can register extension type with dask >= 1.1.0 | ||
from dask.dataframe.extensions import make_array_nonempty | ||
except ImportError: | ||
make_array_nonempty = None | ||
|
||
|
||
@total_ordering | ||
class Points(Geom): | ||
@classmethod | ||
def _shapely_to_array_parts(cls, shape): | ||
import shapely.geometry as sg | ||
if isinstance(shape, (sg.Point, sg.MultiPoint)): | ||
# Single line | ||
return [np.asarray(shape.ctypes)] | ||
else: | ||
raise ValueError(""" | ||
Received invalid value of type {typ}. Must be an instance of Point, | ||
or MultiPoint""".format(typ=type(shape).__name__)) | ||
|
||
def to_shapely(self): | ||
import shapely.geometry as sg | ||
if len(self.array) == 2: | ||
return sg.Point(self.array) | ||
else: | ||
return sg.MultiPoint(self.array.reshape(len(self.array) // 2, 2)) | ||
|
||
@property | ||
def length(self): | ||
return 0.0 | ||
|
||
@property | ||
def area(self): | ||
return 0.0 | ||
|
||
|
||
@register_extension_dtype | ||
class PointsDtype(GeomDtype): | ||
_type_name = "Points" | ||
_subtype_re = re.compile(r"^points\[(?P<subtype>\w+)\]$") | ||
|
||
@classmethod | ||
def construct_array_type(cls): | ||
return PointsArray | ||
|
||
|
||
class PointsArray(GeomArray): | ||
_element_type = Points | ||
|
||
@property | ||
def _dtype_class(self): | ||
return PointsDtype | ||
|
||
@property | ||
def length(self): | ||
return np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
|
||
@property | ||
def area(self): | ||
return np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
|
||
|
||
def points_array_non_empty(dtype): | ||
return PointsArray([[1, 0, 0, 0], [1, 2, 0, 0]], dtype=dtype) | ||
|
||
|
||
if make_array_nonempty: | ||
make_array_nonempty.register(PointsDtype)(points_array_non_empty) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
from __future__ import absolute_import | ||
import re | ||
from math import isfinite | ||
from functools import total_ordering | ||
|
||
import numpy as np | ||
from pandas.core.dtypes.dtypes import register_extension_dtype | ||
|
||
from datashader.geom.base import Geom, GeomDtype, GeomArray, _geom_map | ||
from datashader.geom.lines import compute_length | ||
from datashader.utils import ngjit | ||
|
||
|
||
try: | ||
# See if we can register extension type with dask >= 1.1.0 | ||
from dask.dataframe.extensions import make_array_nonempty | ||
except ImportError: | ||
make_array_nonempty = None | ||
|
||
|
||
@total_ordering | ||
class Polygons(Geom): | ||
@staticmethod | ||
def _polygon_to_array_parts(polygon): | ||
import shapely.geometry as sg | ||
shape = sg.polygon.orient(polygon) | ||
exterior = np.asarray(shape.exterior.ctypes) | ||
polygon_parts = [exterior] | ||
hole_separator = np.array([-np.inf, -np.inf]) | ||
for ring in shape.interiors: | ||
interior = np.asarray(ring.ctypes) | ||
polygon_parts.append(hole_separator) | ||
polygon_parts.append(interior) | ||
return polygon_parts | ||
|
||
@classmethod | ||
def _shapely_to_array_parts(cls, shape): | ||
import shapely.geometry as sg | ||
if isinstance(shape, sg.Polygon): | ||
# Single polygon | ||
return Polygons._polygon_to_array_parts(shape) | ||
elif isinstance(shape, sg.MultiPolygon): | ||
shape = list(shape) | ||
polygon_parts = Polygons._polygon_to_array_parts(shape[0]) | ||
polygon_separator = np.array([np.inf, np.inf]) | ||
for polygon in shape[1:]: | ||
polygon_parts.append(polygon_separator) | ||
polygon_parts.extend(Polygons._polygon_to_array_parts(polygon)) | ||
return polygon_parts | ||
else: | ||
raise ValueError(""" | ||
Received invalid value of type {typ}. Must be an instance of | ||
shapely.geometry.Polygon or shapely.geometry.MultiPolygon""" | ||
.format(typ=type(shape).__name__)) | ||
|
||
def to_shapely(self): | ||
import shapely.geometry as sg | ||
ring_breaks = np.concatenate( | ||
[[-2], np.nonzero(~np.isfinite(self.array))[0][0::2], [len(self.array)]] | ||
) | ||
polygon_breaks = set(np.concatenate( | ||
[[-2], np.nonzero(np.isposinf(self.array))[0][0::2], [len(self.array)]] | ||
)) | ||
|
||
# Build rings for both outer and holds | ||
rings = [] | ||
for start, stop in zip(ring_breaks[:-1], ring_breaks[1:]): | ||
ring_array = self.array[start + 2: stop] | ||
ring_pairs = ring_array.reshape(len(ring_array) // 2, 2) | ||
rings.append(sg.LinearRing(ring_pairs)) | ||
|
||
# Build polygons | ||
polygons = [] | ||
outer = None | ||
holes = [] | ||
for ring, start in zip(rings, ring_breaks[:-1]): | ||
if start in polygon_breaks: | ||
if outer: | ||
# This is the first ring in a new polygon, construct shapely polygon | ||
# with already collected rings | ||
polygons.append(sg.Polygon(outer, holes)) | ||
|
||
# Start collecting new polygon | ||
outer = ring | ||
holes = [] | ||
else: | ||
# Ring is a hole | ||
holes.append(ring) | ||
|
||
# Build final polygon | ||
polygons.append(sg.Polygon(outer, holes)) | ||
|
||
if len(polygons) == 1: | ||
return polygons[0] | ||
else: | ||
return sg.MultiPolygon(polygons) | ||
|
||
@property | ||
def length(self): | ||
return compute_length(self.array) | ||
|
||
@property | ||
def area(self): | ||
return compute_area(self.array) | ||
|
||
|
||
@register_extension_dtype | ||
class PolygonsDtype(GeomDtype): | ||
_type_name = "Polygons" | ||
_subtype_re = re.compile(r"^polygons\[(?P<subtype>\w+)\]$") | ||
|
||
@classmethod | ||
def construct_array_type(cls): | ||
return PolygonsArray | ||
|
||
|
||
class PolygonsArray(GeomArray): | ||
_element_type = Polygons | ||
|
||
@property | ||
def _dtype_class(self): | ||
return PolygonsDtype | ||
|
||
@property | ||
def length(self): | ||
result = np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
_geom_map(self.start_indices, self.flat_array, result, compute_length) | ||
return result | ||
|
||
@property | ||
def area(self): | ||
result = np.zeros(self.start_indices.shape, dtype=self.flat_array.dtype) | ||
_geom_map(self.start_indices, self.flat_array, result, compute_area) | ||
return result | ||
|
||
|
||
@ngjit | ||
def compute_area(values): | ||
area = 0.0 | ||
if len(values) < 6: | ||
# A degenerate polygon | ||
return 0.0 | ||
polygon_start = 0 | ||
for k in range(0, len(values) - 4, 2): | ||
i, j = k + 2, k + 4 | ||
ix = values[i] | ||
jy = values[j + 1] | ||
ky = values[k + 1] | ||
if not isfinite(values[j]): | ||
# last vertex not finite, polygon traversal finished, add wraparound term | ||
polygon_stop = j | ||
firstx = values[polygon_start] | ||
secondy = values[polygon_start + 3] | ||
lasty = values[polygon_stop - 3] | ||
area += firstx * (secondy - lasty) | ||
elif not isfinite(values[i]): | ||
# middle vertex not finite, but last vertex is. | ||
# We're going to start a new polygon | ||
polygon_start = j | ||
elif isfinite(ix) and isfinite(jy) and isfinite(ky): | ||
area += ix * (jy - ky) | ||
|
||
# wrap-around term for final polygon | ||
firstx = values[polygon_start] | ||
secondy = values[polygon_start + 3] | ||
lasty = values[len(values) - 3] | ||
area += firstx * (secondy - lasty) | ||
|
||
return area / 2.0 | ||
|
||
|
||
def polygons_array_non_empty(dtype): | ||
return PolygonsArray([[1, 0, 0, 0, 2, 2], [1, 2, 0, 0, 2, 2]], dtype=dtype) | ||
|
||
|
||
if make_array_nonempty: | ||
make_array_nonempty.register(PolygonsDtype)(polygons_array_non_empty) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
from math import inf, isfinite, nan | ||
|
||
from toolz import memoize | ||
import numpy as np | ||
|
||
from datashader.glyphs.line import _build_map_onto_pixel_for_line | ||
from datashader.glyphs.points import _GeomLike | ||
from datashader.utils import ngjit | ||
|
||
|
||
class PolygonGeom(_GeomLike): | ||
@property | ||
def geom_dtype(self): | ||
from datashader.geom import PolygonsDtype | ||
return PolygonsDtype | ||
|
||
@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_segment = _build_draw_polygon( | ||
append, map_onto_pixel, x_mapper, y_mapper, expand_aggs_and_cols | ||
) | ||
|
||
perform_extend_cpu = _build_extend_polygon_geom( | ||
draw_segment, 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 | ||
# line may be clipped, then mapped to pixels | ||
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, | ||
start_index, stop_index, flat, 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(nan) | ||
ys.fill(nan) | ||
yincreasing.fill(0) | ||
eligible.fill(1) | ||
|
||
# First pass, compute bounding box in data coordinates and count number of edges | ||
num_edges = 0 | ||
poly_xmin = inf | ||
poly_ymin = inf | ||
poly_xmax = -inf | ||
poly_ymax = -inf | ||
for j in range(start_index, stop_index - 2, 2): | ||
x = flat[j] | ||
y = flat[j + 1] | ||
if isfinite(x) and isfinite(y): | ||
poly_xmin = min(poly_xmin, x) | ||
poly_ymin = min(poly_ymin, y) | ||
poly_xmax = max(poly_xmax, x) | ||
poly_ymax = max(poly_ymax, y) | ||
if isfinite(flat[j + 2]) and isfinite(flat[j + 3]): | ||
# Valid edge | ||
num_edges += 1 | ||
|
||
# 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 or height of polygon is 1) | ||
if (stopxi - startxi) == 1 or (stopyi - startyi) == 1: | ||
for yi in range(startyi, stopyi): | ||
for xi in range(startxi, stopxi): | ||
append(i, xi, yi, *aggs_and_cols) | ||
return | ||
|
||
# Build arrays of edges in canvas coordinates | ||
ei = 0 | ||
for j in range(start_index, stop_index - 2, 2): | ||
x0 = flat[j] | ||
y0 = flat[j + 1] | ||
x1 = flat[j + 2] | ||
y1 = flat[j + 3] | ||
if (isfinite(x0) and isfinite(y0) and | ||
isfinite(y0) and isfinite(y1)): | ||
# 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 | ||
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_geom( | ||
draw_polygon, expand_aggs_and_cols | ||
): | ||
def extend_cpu( | ||
sx, tx, sy, ty, xmin, xmax, ymin, ymax, geom_array, *aggs_and_cols | ||
): | ||
start_i = geom_array.start_indices | ||
flat = geom_array.flat_array | ||
|
||
extend_cpu_numba( | ||
sx, tx, sy, ty, xmin, xmax, ymin, ymax, start_i, flat, *aggs_and_cols | ||
) | ||
|
||
@ngjit | ||
@expand_aggs_and_cols | ||
def extend_cpu_numba( | ||
sx, tx, sy, ty, xmin, xmax, ymin, ymax, start_i, flat, *aggs_and_cols | ||
): | ||
nrows = len(start_i) | ||
flat_len = len(flat) | ||
|
||
# Pre-allocate temp arrays | ||
if len(start_i) > 1: | ||
max_coordinates = max(np.diff(start_i)) | ||
else: | ||
max_coordinates = 0 | ||
# Handle case where last polygon is the longest and divide by 2 to get max | ||
# number of edges | ||
max_edges = int(max(max_coordinates, len(flat) - start_i[-1]) // 2) | ||
|
||
xs = np.full((max_edges, 2), nan, dtype=np.float32) | ||
ys = np.full((max_edges, 2), 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 range(nrows): | ||
# Get x index range | ||
start_index = start_i[i] | ||
stop_index = (start_i[i + 1] if i < nrows - 1 else flat_len) | ||
draw_polygon(i, sx, tx, sy, ty, xmin, xmax, ymin, ymax, | ||
int(start_index), int(stop_index), flat, | ||
xs, ys, yincreasing, eligible, *aggs_and_cols) | ||
|
||
return extend_cpu |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from math import inf | ||
import numpy as np | ||
from datashader.geom import ( | ||
Points, PointsArray, Lines, LinesArray, Polygons, PolygonsArray | ||
) | ||
|
||
unit_square_cw = np.array([1, 1, 1, 2, 2, 2, 2, 1, 1, 1], dtype='float64') | ||
large_square_ccw = np.array([0, 0, 3, 0, 3, 3, 0, 3, 0, 0], dtype='float64') | ||
hole_sep = np.array([-inf, -inf]) | ||
fill_sep = np.array([inf, inf]) | ||
|
||
|
||
def test_points(): | ||
points = Points(unit_square_cw) | ||
assert points.length == 0.0 | ||
assert points.area == 0.0 | ||
|
||
|
||
def test_points_array(): | ||
points = PointsArray([ | ||
unit_square_cw, | ||
large_square_ccw, | ||
np.concatenate([large_square_ccw, hole_sep, unit_square_cw]) | ||
]) | ||
|
||
np.testing.assert_equal(points.length, [0.0, 0.0, 0.0]) | ||
np.testing.assert_equal(points.area, [0.0, 0.0, 0.0]) | ||
|
||
|
||
def test_lines(): | ||
lines = Lines(unit_square_cw) | ||
assert lines.length == 4.0 | ||
assert lines.area == 0.0 | ||
|
||
|
||
def test_lines_array(): | ||
lines = LinesArray([ | ||
unit_square_cw, | ||
large_square_ccw, | ||
np.concatenate([large_square_ccw, hole_sep, unit_square_cw]) | ||
]) | ||
|
||
np.testing.assert_equal(lines.length, [4.0, 12.0, 16.0]) | ||
np.testing.assert_equal(lines.area, [0.0, 0.0, 0.0]) | ||
|
||
|
||
def test_polygons(): | ||
polygons = Polygons(np.concatenate([large_square_ccw, hole_sep, unit_square_cw])) | ||
assert polygons.length == 16.0 | ||
assert polygons.area == 8.0 | ||
|
||
|
||
def test_polygons_array(): | ||
polygons = PolygonsArray([ | ||
large_square_ccw, | ||
np.concatenate([large_square_ccw, hole_sep, unit_square_cw]), | ||
unit_square_cw | ||
]) | ||
np.testing.assert_equal(polygons.length, [12.0, 16.0, 4.0]) | ||
np.testing.assert_equal(polygons.area, [9.0, 8.0, -1.0]) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import pytest | ||
import pandas as pd | ||
import numpy as np | ||
import xarray as xr | ||
from math import inf, nan | ||
import datashader as ds | ||
from datashader.tests.test_pandas import assert_eq_xr | ||
import dask.dataframe as dd | ||
|
||
|
||
def dask_DataFrame(*args, **kwargs): | ||
return dd.from_pandas(pd.DataFrame(*args, **kwargs), npartitions=3) | ||
|
||
|
||
DataFrames = [pd.DataFrame, dask_DataFrame] | ||
|
||
|
||
@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, | ||
-inf, -inf, 1, 0.25, 1, 2, 1.75, .25, 0.25, 0.25, | ||
inf, inf, 2.5, 1, 4, 1, 4, 2, 2.5, 2, 2.5, 1 | ||
], | ||
], dtype='Polygons[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.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, | ||
-inf, -inf, 1, 0.25, 1, 2, 1.75, .25, 0.25, 0.25, | ||
inf, inf, 2.5, 1, 4, 1, 4, 2, 2.5, 2, 2.5, 1 | ||
], | ||
], dtype='Polygons[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.parametrize('DataFrame', DataFrames) | ||
def test_no_overlap(DataFrame): | ||
df = DataFrame({ | ||
'polygons': pd.Series([ | ||
[1, 1, 2, 2, 1, 3, 0, 2, 1, 1, | ||
-inf, -inf, | ||
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='Polygons[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.parametrize('DataFrame', DataFrames) | ||
def test_no_overlap_agg(DataFrame): | ||
df = DataFrame({ | ||
'polygons': pd.Series([ | ||
[1, 1, 2, 2, 1, 3, 0, 2, 1, 1, | ||
-inf, -inf, | ||
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='Polygons[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) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 bothspatial
andgeo
already. The contents ofgeo.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, whereasgeom
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.