From 0d7d6acfbb35220996c900ab47d6ce5f6bcb0913 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 12 Aug 2019 11:50:37 -0400 Subject: [PATCH 01/23] Add rectilinear quadmesh glyph and test --- datashader/__init__.py | 1 + datashader/core.py | 81 ++++++++++++++++++- datashader/glyphs/__init__.py | 3 + datashader/glyphs/quadmesh.py | 123 +++++++++++++++++++++++++++++ datashader/pandas.py | 2 +- datashader/resampling.py | 24 ++++++ datashader/tests/test_quadmesh.py | 127 ++++++++++++++++++++++++++++++ datashader/utils.py | 8 ++ datashader/xarray.py | 18 +++++ 9 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 datashader/glyphs/quadmesh.py create mode 100644 datashader/tests/test_quadmesh.py create mode 100644 datashader/xarray.py diff --git a/datashader/__init__.py b/datashader/__init__.py index da620d0a9..d328f8c3a 100644 --- a/datashader/__init__.py +++ b/datashader/__init__.py @@ -12,6 +12,7 @@ from . import transfer_functions as tf # noqa (API import) from . import pandas # noqa (build backend dispatch) +from . import xarray # noqa (build backend dispatch) try: from . import dask # noqa (build backend dispatch) except ImportError: diff --git a/datashader/core.py b/datashader/core.py index 13e9d16ea..e13d69257 100644 --- a/datashader/core.py +++ b/datashader/core.py @@ -11,7 +11,8 @@ from collections import OrderedDict from datashader.spatial.points import SpatialPointsFrame -from .utils import Dispatcher, ngjit, calc_res, calc_bbox, orient_array, compute_coords +from .utils import Dispatcher, ngjit, calc_res, calc_bbox, orient_array, \ + compute_coords, dshape_from_xarray_dataset from .utils import get_indices, dshape_from_pandas, dshape_from_dask from .utils import Expr # noqa (API import) from .resampling import resample_2d, resample_2d_distributed @@ -564,6 +565,75 @@ def area(self, source, x, y, agg=None, axis=0, y_stack=None): 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 + ---------- + source : xarray.DataArray or Dataset + The input datasource. + x, y : str + Column names for the x and y coordinates of each point. + agg : Reduction, optional + Reduction to compute. Default is ``mean()``. + Returns + ------- + data : xarray.DataArray + """ + from .glyphs import QuadMeshRectilinear, QuadMeshCurvialinear + + # Determine reduction operation + from .reductions import mean as mean_rnd + + if isinstance(source, Dataset): + if agg is None or agg.column is None: + name = list(source.data_vars)[0] + else: + name = agg.column + # Keep as dataset so that source[agg.column] works + source = source[[name]] + elif isinstance(source, DataArray): + # Make dataset so that source[agg.column] works + name = source.name + source = source.to_dataset() + else: + raise ValueError("Invalid input type") + + if agg is None: + agg = mean_rnd(name) + + if x is None and y is None: + y, x = source.dims + elif not x or not y: + raise ValueError("Either specify both x and y coordinates" + "or allow them to be inferred.") + yarr, xarr = source[y], source[x] + + if (yarr.ndim > 1 or xarr.ndim > 1) and xarr.dims != yarr.dims: + raise ValueError("Ensure that x- and y-coordinate arrays " + "share the same dimensions. x-coordinates " + "are indexed by %s dims while " + "y-coordinates are indexed by %s dims." % + (xarr.dims, yarr.dims)) + + if (name is not None + and agg.column is not None + and agg.column != name): + raise ValueError('DataArray name %r does not match ' + 'supplied reduction %s.' % + (source.name, agg)) + + if xarr.ndim == 1: + glyph = QuadMeshRectilinear(x, y, name) + elif xarr.ndim == 2: + glyph = QuadMeshCurvialinear(x, y, name) + else: + raise ValueError("""\ +x- and y-coordinate arrays must have 1 or 2 dimensions. + Received arrays with dimensions: {dims}""".format( + dims=list(xarr.dims))) + + return bypixel(source, self, glyph, agg) + # TODO re 'untested', below: Consider replacing with e.g. a 3x3 # array in the call to Canvas (plot_height=3,plot_width=3), then # show the output as a numpy array that has a compact @@ -910,11 +980,13 @@ def bypixel(source, canvas, glyph, agg): glyph : Glyph agg : Reduction """ - if isinstance(source, DataArray): + + # Convert 1D xarray DataArrays and DataSets into Dask DataFrames + if isinstance(source, DataArray) and source.ndim == 1: if not source.name: source.name = 'value' source = source.reset_coords() - if isinstance(source, Dataset): + if isinstance(source, Dataset) and len(source.dims) == 1: columns = list(source.coords.keys()) + list(source.data_vars.keys()) cols_to_keep = _cols_to_keep(columns, glyph, agg) source = source.drop([col for col in columns if col not in cols_to_keep]) @@ -931,6 +1003,9 @@ def bypixel(source, canvas, glyph, agg): dshape = dshape_from_pandas(source) elif isinstance(source, dd.DataFrame): dshape = dshape_from_dask(source) + elif isinstance(source, Dataset): + # Multi-dimensional Dataset + dshape = dshape_from_xarray_dataset(source) else: raise ValueError("source must be a pandas or dask DataFrame") schema = dshape.measure diff --git a/datashader/glyphs/__init__.py b/datashader/glyphs/__init__.py index 7c2c550a0..8abcb83c3 100644 --- a/datashader/glyphs/__init__.py +++ b/datashader/glyphs/__init__.py @@ -23,4 +23,7 @@ AreaToLineAxis1Ragged, ) from .trimesh import Triangles # noqa (API import) +from .quadmesh import ( # noqa (API import) + QuadMeshRectilinear, QuadMeshCurvialinear +) from .glyph import Glyph # noqa (API import) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py new file mode 100644 index 000000000..c1e0de397 --- /dev/null +++ b/datashader/glyphs/quadmesh.py @@ -0,0 +1,123 @@ +from toolz import memoize + +from datashader.glyphs.glyph import Glyph +from datashader.resampling import infer_interval_breaks +from datashader.utils import isreal, ngjit + + +class _QuadMeshLike(Glyph): + def __init__(self, x, y, name): + self.x = x + self.y = y + self.name = name + + @property + def ndims(self): + return 2 + + @property + def inputs(self): + return (self.x, self.y, self.name) + + def validate(self, in_dshape): + if not isreal(in_dshape.measure[str(self.x)]): + raise ValueError('x must be real') + elif not isreal(in_dshape.measure[str(self.y)]): + raise ValueError('y must be real') + elif not isreal(in_dshape.measure[str(self.name)]): + raise ValueError('aggregate value must be real') + + @property + def x_label(self): + return self.x + + @property + def y_label(self): + return self.y + + +class QuadMeshRectilinear(_QuadMeshLike): + def _compute_bounds_from_1d_centers(self, xr_ds, dim): + vals = xr_ds[dim].values + + # Assume dimension is sorted in ascending or descending order + v0, v1, v_nm1, v_n = [ + vals[i] for i in [0, 1, -2, -1] + ] + + # Check if we should swap order + if v_n < v0: + v0, v1, v_nm1, v_n = v_n, v_nm1, v1, v0 + + bounds = (v0 - 0.5 * (v1 - v0), v_n + 0.5 * (v_n - v_nm1)) + return self.maybe_expand_bounds(bounds) + + def compute_x_bounds(self, xr_ds): + return self._compute_bounds_from_1d_centers(xr_ds, self.x) + + def compute_y_bounds(self, xr_ds): + return self._compute_bounds_from_1d_centers(xr_ds, self.y) + + @memoize + def _build_extend(self, x_mapper, y_mapper, info, append): + x_name = self.x + y_name = self.y + + @ngjit + def _extend(vt, bounds, xs, ys, *aggs_and_cols): + sx, tx, sy, ty = vt + xmin, xmax, ymin, ymax = bounds + + # Compute max valid x and y index + xmaxi = int(round(x_mapper(xmax) * sx + tx)) + ymaxi = int(round(y_mapper(ymax) * sy + ty)) + + for i in range(len(xs) - 1): + for j in range(len(ys) - 1): + x0, x1 = max(xs[i], xmin), min(xs[i + 1], xmax) + y0, y1 = max(ys[j], ymin), min(ys[j + 1], ymax) + + # Makes sure x0 <= x1 and y0 <= y1 + if x0 > x1: + x0, x1 = x1, x0 + if y0 > y1: + y0, y1 = y1, y0 + + # check whether we can skip quad. To avoid overlapping + # quads, skip if upper bound equals viewport lower bound. + if x1 <= xmin or x0 > xmax or y1 <= ymin or y0 > ymax: + continue + + # Map onto pixels and clip to viewport + x0i = max(int(x_mapper(x0) * sx + tx), 0) + x1i = min(int(x_mapper(x1) * sx + tx), xmaxi) + y0i = max(int(y_mapper(y0) * sy + ty), 0) + y1i = min(int(y_mapper(y1) * sy + ty), ymaxi) + + # Make sure single pixel quads are represented + if x0i == x1i and x1i < ymaxi: + x1i += 1 + + if y0i == y1i and y1i < ymaxi: + y1i += 1 + + # x1i and y1i are not included in the iteration. this + # serves to avoid overlapping quads and it avoids the need + # for special handling of quads that end on exactly on the + # upper bound. + for xi in range(x0i, x1i): + for yi in range(y0i, y1i): + append(j, i, xi, yi, *aggs_and_cols) + + def extend(aggs, xr_ds, vt, bounds): + # Convert from bin centers to interval edges + xs = infer_interval_breaks(xr_ds[x_name].values) + ys = infer_interval_breaks(xr_ds[y_name].values) + cols = aggs + info(xr_ds.transpose(y_name, x_name)) + _extend(vt, bounds, xs, ys, *cols) + + return extend + + +class QuadMeshCurvialinear(_QuadMeshLike): + pass diff --git a/datashader/pandas.py b/datashader/pandas.py index fc0c2a036..921b0df8b 100644 --- a/datashader/pandas.py +++ b/datashader/pandas.py @@ -21,7 +21,7 @@ def pandas_pipeline(df, schema, canvas, glyph, summary): @glyph_dispatch.register(_PointLike) @glyph_dispatch.register(_AreaToLineLike) -def pointlike(glyph, df, schema, canvas, summary): +def default(glyph, df, schema, canvas, summary): create, info, append, _, finalize = compile_components(summary, schema, glyph) x_mapper = canvas.x_axis.mapper y_mapper = canvas.y_axis.mapper diff --git a/datashader/resampling.py b/datashader/resampling.py index 9e0baa02a..bc526f7d4 100644 --- a/datashader/resampling.py +++ b/datashader/resampling.py @@ -26,6 +26,8 @@ from __future__ import absolute_import, division, print_function +import sys +import datetime as dt from itertools import groupby from math import floor, ceil @@ -952,3 +954,25 @@ def _downsample_2d_std_var(src, mask, use_mask, method, fill_value, DS_MODE: _downsample_2d_mode, DS_STD: _downsample_2d_std_var, DS_VAR: _downsample_2d_std_var} + + +def infer_interval_breaks(coord, axis=0): + """ + >>> infer_interval_breaks(np.arange(5)) + array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) + >>> infer_interval_breaks([[0, 1], [3, 4]], axis=1) + array([[-0.5, 0.5, 1.5], + [ 2.5, 3.5, 4.5]]) + """ + coord = np.asarray(coord) + if sys.version_info.major == 2 and len(coord) and isinstance(coord[0], (dt.datetime, dt.date)): + # np.diff does not work on datetimes in python 2 + coord = coord.astype('datetime64') + if len(coord) == 0: + return np.array([], dtype=coord.dtype) + deltas = 0.5 * np.diff(coord, axis=axis) + first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis) + last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis) + trim_last = tuple(slice(None, -1) if n == axis else slice(None) + for n in range(coord.ndim)) + return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) diff --git a/datashader/tests/test_quadmesh.py b/datashader/tests/test_quadmesh.py new file mode 100644 index 000000000..b3b90ef2e --- /dev/null +++ b/datashader/tests/test_quadmesh.py @@ -0,0 +1,127 @@ +import numpy as np +from numpy import nan +import xarray as xr +import datashader as ds + + +def test_rect_quadmesh_autorange(): + c = ds.Canvas(plot_width=8, plot_height=4) + da = xr.DataArray( + [[1, 2, 3, 4], + [5, 6, 7, 8]], + coords=[('b', [1, 2]), + ('a', [1, 2, 3, 4])], + name='Z') + + y_coords = np.linspace(0.75, 2.25, 4) + x_coords = np.linspace(0.75, 4.25, 8) + out = xr.DataArray(np.array( + [[1., 1., 2., 2., 3., 3., 4., 4.], + [1., 1., 2., 2., 3., 3., 4., 4.], + [5., 5., 6., 6., 7., 7., 8., 8.], + [5., 5., 6., 6., 7., 7., 8., 8.]], + dtype='i4'), + coords=[('b', y_coords), + ('a', x_coords)] + ) + + res = c.quadmesh(da, x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + # Check transpose gives same answer + res = c.quadmesh(da.transpose('a', 'b'), x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_rect_quadmesh_autorange_reversed(): + c = ds.Canvas(plot_width=8, plot_height=4) + da = xr.DataArray( + [[1, 2, 3, 4], + [5, 6, 7, 8]], + coords=[('b', [-1, -2]), + ('a', [-1, -2, -3, -4])], + name='Z') + + y_coords = np.linspace(-2.25, -0.75, 4) + x_coords = np.linspace(-4.25, -0.75, 8) + out = xr.DataArray(np.array( + [[8., 8., 7., 7., 6., 6., 5., 5.], + [8., 8., 7., 7., 6., 6., 5., 5.], + [4., 4., 3., 3., 2., 2., 1., 1.], + [4., 4., 3., 3., 2., 2., 1., 1.]], + dtype='i4'), + coords=[('b', y_coords), + ('a', x_coords)] + ) + + res = c.quadmesh(da, x='a', y='b', agg=ds.sum('Z')) + print(res) + assert res.equals(out) + + # Check transpose gives same answer + res = c.quadmesh(da.transpose('a', 'b'), x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_rect_quadmesh_manual_range(): + c = ds.Canvas(plot_width=8, plot_height=4, + x_range=[1, 3], + y_range=[-1, 3]) + + da = xr.DataArray( + [[1, 2, 3, 4], + [5, 6, 7, 8]], + coords=[('b', [1, 2]), + ('a', [1, 2, 3, 4])], + name='Z') + + y_coords = np.linspace(-0.5, 2.5, 4) + x_coords = np.linspace(1.125, 2.875, 8) + out = xr.DataArray(np.array( + [[nan, nan, nan, nan, nan, nan, nan, nan], + [1., 1., 2., 2., 2., 2., 3., 3.], + [5., 5., 6., 6., 6., 6., 7., 7.], + [nan, nan, nan, nan, nan, nan, nan, nan]], + dtype='f4'), + coords=[('b', y_coords), + ('a', x_coords)] + ) + + res = c.quadmesh(da, x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + # Check transpose gives same answer + res = c.quadmesh(da.transpose('a', 'b'), x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_subpixel_quads_represented(): + c = ds.Canvas(plot_width=8, plot_height=4, + x_range=[0, 16], + y_range=[0, 8]) + + da = xr.DataArray( + [[1, 2, 3, 4], + [5, 6, 7, 8]], + coords=[('b', [1, 2]), + ('a', [1, 2, 3, 4])], + name='Z') + + y_coords = np.linspace(1, 7, 4) + x_coords = np.linspace(1, 15, 8) + out = xr.DataArray(np.array( + [[14., 22., nan, nan, nan, nan, nan, nan], + [nan, nan, nan, nan, nan, nan, nan, nan], + [nan, nan, nan, nan, nan, nan, nan, nan], + [nan, nan, nan, nan, nan, nan, nan, nan]], + dtype='f4'), + coords=[('b', y_coords), + ('a', x_coords)] + ) + + res = c.quadmesh(da, x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) + + # Check transpose gives same answer + res = c.quadmesh(da.transpose('a', 'b'), x='a', y='b', agg=ds.sum('Z')) + assert res.equals(out) diff --git a/datashader/utils.py b/datashader/utils.py index fb660b17b..f125a648c 100644 --- a/datashader/utils.py +++ b/datashader/utils.py @@ -394,6 +394,14 @@ def dshape_from_dask(df): return datashape.var * dshape_from_pandas(df.head()).measure +def dshape_from_xarray_dataset(xr_ds): + """Return a datashape.DataShape object given a xarray Dataset.""" + return datashape.var * datashape.Record([ + (k, dshape_from_pandas_helper(xr_ds[k])) + for k in list(xr_ds.data_vars) + list(xr_ds.coords) + ]) + + def dataframe_from_multiple_sequences(x_values, y_values): """ Converts a set of multiple sequences (eg: time series), stored as a 2 dimensional diff --git a/datashader/xarray.py b/datashader/xarray.py new file mode 100644 index 000000000..cd5a6e7bc --- /dev/null +++ b/datashader/xarray.py @@ -0,0 +1,18 @@ +from datashader.compiler import compile_components +from datashader.glyphs.quadmesh import _QuadMeshLike +from datashader.pandas import default +from .core import bypixel +import xarray as xr +from .utils import Dispatcher + + +glyph_dispatch = Dispatcher() + + +@bypixel.pipeline.register(xr.Dataset) +def xarray_pipeline(df, schema, canvas, glyph, summary): + return glyph_dispatch(glyph, df, schema, canvas, summary) + + +# Default to default pandas implementation +glyph_dispatch.register(_QuadMeshLike)(default) From eecca81db3213d372d561c51267b0515cd47e9a6 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 11:50:19 -0400 Subject: [PATCH 02/23] flake --- datashader/xarray.py | 1 - 1 file changed, 1 deletion(-) diff --git a/datashader/xarray.py b/datashader/xarray.py index cd5a6e7bc..a5818b4fe 100644 --- a/datashader/xarray.py +++ b/datashader/xarray.py @@ -1,4 +1,3 @@ -from datashader.compiler import compile_components from datashader.glyphs.quadmesh import _QuadMeshLike from datashader.pandas import default from .core import bypixel From 43ff6570740295fa8f9bec8f1ed5f0066e82285f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 12:10:31 -0400 Subject: [PATCH 03/23] py2 absolute_import --- datashader/xarray.py | 1 + 1 file changed, 1 insertion(+) diff --git a/datashader/xarray.py b/datashader/xarray.py index a5818b4fe..d80dd5b37 100644 --- a/datashader/xarray.py +++ b/datashader/xarray.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from datashader.glyphs.quadmesh import _QuadMeshLike from datashader.pandas import default from .core import bypixel From 0334bf437810bf1d498ad4acff07caa1d4845202 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 12:23:59 -0400 Subject: [PATCH 04/23] py2 absolute_import --- datashader/tests/test_bokeh_ext.py | 3 ++- datashader/tests/test_bundling.py | 1 + datashader/tests/test_colors.py | 1 + datashader/tests/test_compatibility.py | 1 + datashader/tests/test_composite.py | 1 + datashader/tests/test_dask.py | 2 +- datashader/tests/test_datatypes.py | 1 + datashader/tests/test_geo.py | 1 + datashader/tests/test_glyphs.py | 1 + datashader/tests/test_layout.py | 1 + datashader/tests/test_pandas.py | 1 + datashader/tests/test_pipeline.py | 1 + datashader/tests/test_quadmesh.py | 1 + datashader/tests/test_raster.py | 1 + datashader/tests/test_spatial.py | 1 + datashader/tests/test_tiles.py | 1 + datashader/tests/test_transfer_functions.py | 2 +- datashader/tests/test_utils.py | 1 + datashader/tests/test_xarray.py | 1 + 19 files changed, 20 insertions(+), 3 deletions(-) diff --git a/datashader/tests/test_bokeh_ext.py b/datashader/tests/test_bokeh_ext.py index 3134b54cb..44fc2a2d8 100644 --- a/datashader/tests/test_bokeh_ext.py +++ b/datashader/tests/test_bokeh_ext.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np import pandas as pd import datashader as ds @@ -46,7 +47,7 @@ def test_interactive_image_update(): # Ensure bokeh Document is instantiated img._repr_html_() - assert isinstance(img.doc, Document) + assert isinstance(img.doc, Document) # Ensure image is updated img.update_image({'xmin': 0.5, 'xmax': 1, 'ymin': 0.5, 'ymax': 1, 'w': 1, 'h': 1}) diff --git a/datashader/tests/test_bundling.py b/datashader/tests/test_bundling.py index ab6bf104f..f5a86f6ac 100644 --- a/datashader/tests/test_bundling.py +++ b/datashader/tests/test_bundling.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import pytest skimage = pytest.importorskip("skimage") diff --git a/datashader/tests/test_colors.py b/datashader/tests/test_colors.py index 8431aa5ad..6d608255e 100644 --- a/datashader/tests/test_colors.py +++ b/datashader/tests/test_colors.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from datashader.colors import rgb, hex_to_rgb import pytest diff --git a/datashader/tests/test_compatibility.py b/datashader/tests/test_compatibility.py index 233fe6fbc..059af6e06 100644 --- a/datashader/tests/test_compatibility.py +++ b/datashader/tests/test_compatibility.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from pytest import raises from datashader.compatibility import apply, _exec diff --git a/datashader/tests/test_composite.py b/datashader/tests/test_composite.py index 6c27daff1..5f3217b9e 100644 --- a/datashader/tests/test_composite.py +++ b/datashader/tests/test_composite.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np from datashader.composite import add, saturate, over, source diff --git a/datashader/tests/test_dask.py b/datashader/tests/test_dask.py index 791acc18c..86079c45d 100644 --- a/datashader/tests/test_dask.py +++ b/datashader/tests/test_dask.py @@ -1,4 +1,4 @@ -from __future__ import division +from __future__ import division, absolute_import from dask.context import config import dask.dataframe as dd import numpy as np diff --git a/datashader/tests/test_datatypes.py b/datashader/tests/test_datatypes.py index 859c0d35c..aee0cc9f2 100644 --- a/datashader/tests/test_datatypes.py +++ b/datashader/tests/test_datatypes.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import pytest import numpy as np import pandas as pd diff --git a/datashader/tests/test_geo.py b/datashader/tests/test_geo.py index e99339378..acb5cfd2f 100644 --- a/datashader/tests/test_geo.py +++ b/datashader/tests/test_geo.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import datashader as ds import xarray as xr import numpy as np diff --git a/datashader/tests/test_glyphs.py b/datashader/tests/test_glyphs.py index fbdeb158b..317605021 100644 --- a/datashader/tests/test_glyphs.py +++ b/datashader/tests/test_glyphs.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from datashape import dshape import pandas as pd import numpy as np diff --git a/datashader/tests/test_layout.py b/datashader/tests/test_layout.py index f8f5e6b4c..f5d4d2f27 100644 --- a/datashader/tests/test_layout.py +++ b/datashader/tests/test_layout.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import pytest skimage = pytest.importorskip("skimage") diff --git a/datashader/tests/test_pandas.py b/datashader/tests/test_pandas.py index 0ff336500..2d610bfcd 100644 --- a/datashader/tests/test_pandas.py +++ b/datashader/tests/test_pandas.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np import pandas as pd import xarray as xr diff --git a/datashader/tests/test_pipeline.py b/datashader/tests/test_pipeline.py index 084f0226a..364500dbc 100644 --- a/datashader/tests/test_pipeline.py +++ b/datashader/tests/test_pipeline.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np import pandas as pd import datashader as ds diff --git a/datashader/tests/test_quadmesh.py b/datashader/tests/test_quadmesh.py index b3b90ef2e..76191cfa3 100644 --- a/datashader/tests/test_quadmesh.py +++ b/datashader/tests/test_quadmesh.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np from numpy import nan import xarray as xr diff --git a/datashader/tests/test_raster.py b/datashader/tests/test_raster.py index 0f951146c..7129e3a26 100644 --- a/datashader/tests/test_raster.py +++ b/datashader/tests/test_raster.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import pytest rasterio = pytest.importorskip("rasterio") diff --git a/datashader/tests/test_spatial.py b/datashader/tests/test_spatial.py index ef9fdb614..3cf48eed7 100644 --- a/datashader/tests/test_spatial.py +++ b/datashader/tests/test_spatial.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import os import pytest import numpy as np diff --git a/datashader/tests/test_tiles.py b/datashader/tests/test_tiles.py index c822a4d1e..b9c5eac1f 100644 --- a/datashader/tests/test_tiles.py +++ b/datashader/tests/test_tiles.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import datashader as ds import datashader.transfer_functions as tf diff --git a/datashader/tests/test_transfer_functions.py b/datashader/tests/test_transfer_functions.py index 988c6a213..322521696 100644 --- a/datashader/tests/test_transfer_functions.py +++ b/datashader/tests/test_transfer_functions.py @@ -1,4 +1,4 @@ -from __future__ import division +from __future__ import division, absolute_import from io import BytesIO diff --git a/datashader/tests/test_utils.py b/datashader/tests/test_utils.py index 28dc3469a..5fe53c36e 100644 --- a/datashader/tests/test_utils.py +++ b/datashader/tests/test_utils.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from datashape import dshape from datashader.utils import Dispatcher, isreal diff --git a/datashader/tests/test_xarray.py b/datashader/tests/test_xarray.py index ce24d22f9..2855b0dc8 100644 --- a/datashader/tests/test_xarray.py +++ b/datashader/tests/test_xarray.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import numpy as np import xarray as xr From 711c40d8846d40bc8241f11a6977cd1b0f75ee81 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 12:40:38 -0400 Subject: [PATCH 05/23] py2 absolute_import --- datashader/geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datashader/geo.py b/datashader/geo.py index 24373ddbf..fcf8076d6 100644 --- a/datashader/geo.py +++ b/datashader/geo.py @@ -3,7 +3,7 @@ """ -from __future__ import division +from __future__ import division, absolute_import import numpy as np import datashader.transfer_functions as tf From c0d1dd43673874544b93a05b74ba5d0682e8f497 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 14:43:24 -0400 Subject: [PATCH 06/23] get default dimensions from DataArray --- datashader/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datashader/core.py b/datashader/core.py index e13d69257..1cd5902a1 100644 --- a/datashader/core.py +++ b/datashader/core.py @@ -602,7 +602,7 @@ def quadmesh(self, source, x=None, y=None, agg=None): agg = mean_rnd(name) if x is None and y is None: - y, x = source.dims + y, x = source[name].dims elif not x or not y: raise ValueError("Either specify both x and y coordinates" "or allow them to be inferred.") From 6e8dcb4504a37d98a6a4a92fb12f07e792942a3e Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 13 Aug 2019 20:27:30 -0400 Subject: [PATCH 07/23] Pull masking and int conversion outside of _extend function --- datashader/glyphs/quadmesh.py | 61 +++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index c1e0de397..6aad463db 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -65,40 +65,22 @@ def _build_extend(self, x_mapper, y_mapper, info, append): @ngjit def _extend(vt, bounds, xs, ys, *aggs_and_cols): - sx, tx, sy, ty = vt - xmin, xmax, ymin, ymax = bounds - - # Compute max valid x and y index - xmaxi = int(round(x_mapper(xmax) * sx + tx)) - ymaxi = int(round(y_mapper(ymax) * sy + ty)) - for i in range(len(xs) - 1): for j in range(len(ys) - 1): - x0, x1 = max(xs[i], xmin), min(xs[i + 1], xmax) - y0, y1 = max(ys[j], ymin), min(ys[j + 1], ymax) + x0i, x1i = xs[i], xs[i + 1] + y0i, y1i = ys[j], ys[j + 1] # Makes sure x0 <= x1 and y0 <= y1 - if x0 > x1: - x0, x1 = x1, x0 - if y0 > y1: - y0, y1 = y1, y0 - - # check whether we can skip quad. To avoid overlapping - # quads, skip if upper bound equals viewport lower bound. - if x1 <= xmin or x0 > xmax or y1 <= ymin or y0 > ymax: - continue - - # Map onto pixels and clip to viewport - x0i = max(int(x_mapper(x0) * sx + tx), 0) - x1i = min(int(x_mapper(x1) * sx + tx), xmaxi) - y0i = max(int(y_mapper(y0) * sy + ty), 0) - y1i = min(int(y_mapper(y1) * sy + ty), ymaxi) + if x0i > x1i: + x0i, x1i = x1i, x0i + if y0i > y1i: + y0i, y1i = y1i, y0i # Make sure single pixel quads are represented - if x0i == x1i and x1i < ymaxi: + if x0i == x1i: x1i += 1 - if y0i == y1i and y1i < ymaxi: + if y0i == y1i: y1i += 1 # x1i and y1i are not included in the iteration. this @@ -113,8 +95,31 @@ def extend(aggs, xr_ds, vt, bounds): # Convert from bin centers to interval edges xs = infer_interval_breaks(xr_ds[x_name].values) ys = infer_interval_breaks(xr_ds[y_name].values) - cols = aggs + info(xr_ds.transpose(y_name, x_name)) - _extend(vt, bounds, xs, ys, *cols) + + x0, x1, y0, y1 = bounds + xspan = x1 - x0 + yspan = y1 - y0 + xscaled = (x_mapper(xs) - x0) / xspan + yscaled = (y_mapper(ys) - y0) / yspan + + xmask = np.where((xscaled >= 0) & (xscaled <= 1)) + ymask = np.where((yscaled >= 0) & (yscaled <= 1)) + xm0, xm1 = max(xmask[0].min() - 1, 0), xmask[0].max() + 1 + ym0, ym1 = max(ymask[0].min() - 1, 0), ymask[0].max() + 1 + + plot_height, plot_width = aggs[0].shape[:2] + + # Downselect xs and ys and convert to int + xs = (xscaled[xm0:xm1 + 1] * plot_width).astype(int).clip(0, plot_width) + ys = (yscaled[ym0:ym1 + 1] * plot_height).astype(int).clip(0, plot_width) + + # For each of aggs and cols, down select to valid range + cols_full = info(xr_ds.transpose(y_name, x_name)) + cols = tuple([c[ym0:ym1, xm0:xm1] for c in cols_full]) + + aggs_and_cols = aggs + cols + + _extend(vt, bounds, xs, ys, *aggs_and_cols) return extend From 2afa24ea0ada22fd0592e0306429ab2d8564f63a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 14 Aug 2019 05:35:31 -0400 Subject: [PATCH 08/23] Add inline mean comment --- datashader/glyphs/quadmesh.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 6aad463db..f3fa91706 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -1,4 +1,5 @@ from toolz import memoize +import numpy as np from datashader.glyphs.glyph import Glyph from datashader.resampling import infer_interval_breaks @@ -91,6 +92,17 @@ def _extend(vt, bounds, xs, ys, *aggs_and_cols): for yi in range(y0i, y1i): append(j, i, xi, yi, *aggs_and_cols) + # # Inline mean aggregation + # # + # if np.isnan(aggs_and_cols[0][yi, xi]): + # aggs_and_cols[0][yi, xi] = aggs_and_cols[2][j, i] + # else: + # aggs_and_cols[0][yi, xi] += aggs_and_cols[2][j, i] + # + # # + # if not np.isnan(aggs_and_cols[2][j, i]): + # aggs_and_cols[1][yi, xi] += 1 + def extend(aggs, xr_ds, vt, bounds): # Convert from bin centers to interval edges xs = infer_interval_breaks(xr_ds[x_name].values) From 8edeb54e6d2562c05748f304e240df2d1b1093b4 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 14 Aug 2019 05:39:26 -0400 Subject: [PATCH 09/23] Pull x calculations outside of inner loop --- datashader/glyphs/quadmesh.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index f3fa91706..29ab9ed8a 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -67,20 +67,25 @@ def _build_extend(self, x_mapper, y_mapper, info, append): @ngjit def _extend(vt, bounds, xs, ys, *aggs_and_cols): for i in range(len(xs) - 1): + x0i, x1i = xs[i], xs[i + 1] + + # Makes sure x0 <= x1 + if x0i > x1i: + x0i, x1i = x1i, x0i + + # Make sure single pixel quads are represented + if x0i == x1i: + x1i += 1 + for j in range(len(ys) - 1): - x0i, x1i = xs[i], xs[i + 1] + y0i, y1i = ys[j], ys[j + 1] - # Makes sure x0 <= x1 and y0 <= y1 - if x0i > x1i: - x0i, x1i = x1i, x0i + # Makes y0 <= y1 if y0i > y1i: y0i, y1i = y1i, y0i # Make sure single pixel quads are represented - if x0i == x1i: - x1i += 1 - if y0i == y1i: y1i += 1 From f4c6843915ee379cd7f18ea7fcec4376b9cdea91 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 14:41:21 -0400 Subject: [PATCH 10/23] Fix clipping error --- datashader/glyphs/quadmesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 29ab9ed8a..1ece3d24d 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -128,7 +128,7 @@ def extend(aggs, xr_ds, vt, bounds): # Downselect xs and ys and convert to int xs = (xscaled[xm0:xm1 + 1] * plot_width).astype(int).clip(0, plot_width) - ys = (yscaled[ym0:ym1 + 1] * plot_height).astype(int).clip(0, plot_width) + ys = (yscaled[ym0:ym1 + 1] * plot_height).astype(int).clip(0, plot_height) # For each of aggs and cols, down select to valid range cols_full = info(xr_ds.transpose(y_name, x_name)) From 87cf7e3f333744ea21690816da497bec1ab93da7 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 15:09:32 -0400 Subject: [PATCH 11/23] Specify explicit coordinates ordering The ordering of x then y makes it possible to pass the output xarray into a HoloViews Image container with the dimensions being transposed --- datashader/pandas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/datashader/pandas.py b/datashader/pandas.py index 921b0df8b..c3e6ddf3a 100644 --- a/datashader/pandas.py +++ b/datashader/pandas.py @@ -7,6 +7,7 @@ from .glyphs.points import _PointLike from .glyphs.area import _AreaToLineLike from .utils import Dispatcher +from collections import OrderedDict __all__ = () @@ -43,5 +44,6 @@ def default(glyph, df, schema, canvas, summary): extend(bases, df, x_st + y_st, x_range + y_range) return finalize(bases, - coords=[y_axis, x_axis], + coords=OrderedDict([(glyph.x_label, x_axis), + (glyph.y_label, y_axis)]), dims=[glyph.y_label, glyph.x_label]) From 223f4d014d5f5103a1f57224d0f4eb957784b09a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 17:15:54 -0400 Subject: [PATCH 12/23] Fix count_cat finalize method now that coords is an OrderedDict --- datashader/reductions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/datashader/reductions.py b/datashader/reductions.py index de0a4703c..1487fc49e 100644 --- a/datashader/reductions.py +++ b/datashader/reductions.py @@ -310,7 +310,9 @@ def _build_finalize(self, dshape): def finalize(bases, **kwargs): dims = kwargs['dims'] + [self.column] - coords = kwargs['coords'] + [cats] + + coords = kwargs['coords'] + coords[self.column] = cats return xr.DataArray(bases[0], dims=dims, coords=coords) return finalize From a0c1c1b18295f31cda25487942c1968034b6c07c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 17:36:09 -0400 Subject: [PATCH 13/23] Fix dask construction of DataArray to use OrderedDict for coords --- datashader/dask.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datashader/dask.py b/datashader/dask.py index b57cbdc9f..853fee6b8 100644 --- a/datashader/dask.py +++ b/datashader/dask.py @@ -3,6 +3,7 @@ import dask import pandas as pd import dask.dataframe as dd +from collections import OrderedDict from dask.base import tokenize, compute from .core import bypixel @@ -51,7 +52,7 @@ def shape_bounds_st_and_axis(df, canvas, glyph): x_axis = canvas.x_axis.compute_index(x_st, width) y_axis = canvas.y_axis.compute_index(y_st, height) - axis = [y_axis, x_axis] + axis = OrderedDict([(glyph.x_label, x_axis), (glyph.y_label, y_axis)]) return shape, bounds, st, axis From 4425e525c7f6fe4f206d9fcbd24752ca6a968801 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 17:39:30 -0400 Subject: [PATCH 14/23] Remove print --- datashader/reductions.py | 38 +++++++++++++++---------------- datashader/tests/test_quadmesh.py | 1 - 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/datashader/reductions.py b/datashader/reductions.py index 1487fc49e..e16ca325f 100644 --- a/datashader/reductions.py +++ b/datashader/reductions.py @@ -389,24 +389,24 @@ def _finalize(bases, **kwargs): class first(Reduction): """First value encountered in ``column``. - Useful for categorical data where an actual value must always be returned, + Useful for categorical data where an actual value must always be returned, not an average or other numerical calculation. - + Currently only supported for rasters, externally to this class. Parameters ---------- column : str - Name of the column to aggregate over. If the data type is floating point, + Name of the column to aggregate over. If the data type is floating point, ``NaN`` values in the column are skipped. """ _dshape = dshape(Option(ct.float64)) - @staticmethod + @staticmethod def _append(x, y, agg): raise NotImplementedError("first is currently implemented only for rasters") - - @staticmethod + + @staticmethod def _create(shape): raise NotImplementedError("first is currently implemented only for rasters") @@ -423,24 +423,24 @@ def _finalize(bases, **kwargs): class last(Reduction): """Last value encountered in ``column``. - Useful for categorical data where an actual value must always be returned, + Useful for categorical data where an actual value must always be returned, not an average or other numerical calculation. - + Currently only supported for rasters, externally to this class. Parameters ---------- column : str - Name of the column to aggregate over. If the data type is floating point, + Name of the column to aggregate over. If the data type is floating point, ``NaN`` values in the column are skipped. """ _dshape = dshape(Option(ct.float64)) - @staticmethod + @staticmethod def _append(x, y, agg): raise NotImplementedError("last is currently implemented only for rasters") - - @staticmethod + + @staticmethod def _create(shape): raise NotImplementedError("last is currently implemented only for rasters") @@ -457,9 +457,9 @@ def _finalize(bases, **kwargs): class mode(Reduction): """Mode (most common value) of all the values encountered in ``column``. - Useful for categorical data where an actual value must always be returned, + Useful for categorical data where an actual value must always be returned, not an average or other numerical calculation. - + Currently only supported for rasters, externally to this class. Implementing it for other glyph types would be difficult due to potentially unbounded data storage requirements to store indefinite point or line @@ -468,16 +468,16 @@ class mode(Reduction): Parameters ---------- column : str - Name of the column to aggregate over. If the data type is floating point, + Name of the column to aggregate over. If the data type is floating point, ``NaN`` values in the column are skipped. """ _dshape = dshape(Option(ct.float64)) - @staticmethod + @staticmethod def _append(x, y, agg): raise NotImplementedError("mode is currently implemented only for rasters") - - @staticmethod + + @staticmethod def _create(shape): raise NotImplementedError("mode is currently implemented only for rasters") @@ -531,4 +531,4 @@ def inputs(self): if isinstance(_v,type) and (issubclass(_v,Reduction) or _v is summary) and _v not in [Reduction, OptionalFieldReduction, FloatingReduction, m2]])) - + diff --git a/datashader/tests/test_quadmesh.py b/datashader/tests/test_quadmesh.py index 76191cfa3..62bf648d1 100644 --- a/datashader/tests/test_quadmesh.py +++ b/datashader/tests/test_quadmesh.py @@ -56,7 +56,6 @@ def test_rect_quadmesh_autorange_reversed(): ) res = c.quadmesh(da, x='a', y='b', agg=ds.sum('Z')) - print(res) assert res.equals(out) # Check transpose gives same answer From eaa410faf9c33179cfffb648a83667f94bf89a79 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 15 Aug 2019 18:06:05 -0400 Subject: [PATCH 15/23] Use expand_aggs_and_cols decorator for quadmesh rendering --- datashader/glyphs/quadmesh.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 1ece3d24d..0040969ec 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -65,11 +65,12 @@ def _build_extend(self, x_mapper, y_mapper, info, append): y_name = self.y @ngjit + @self.expand_aggs_and_cols(append) def _extend(vt, bounds, xs, ys, *aggs_and_cols): for i in range(len(xs) - 1): x0i, x1i = xs[i], xs[i + 1] - # Makes sure x0 <= x1 + # Make sure x0 <= x1 if x0i > x1i: x0i, x1i = x1i, x0i @@ -81,7 +82,7 @@ def _extend(vt, bounds, xs, ys, *aggs_and_cols): y0i, y1i = ys[j], ys[j + 1] - # Makes y0 <= y1 + # Make sure y0 <= y1 if y0i > y1i: y0i, y1i = y1i, y0i @@ -97,17 +98,6 @@ def _extend(vt, bounds, xs, ys, *aggs_and_cols): for yi in range(y0i, y1i): append(j, i, xi, yi, *aggs_and_cols) - # # Inline mean aggregation - # # - # if np.isnan(aggs_and_cols[0][yi, xi]): - # aggs_and_cols[0][yi, xi] = aggs_and_cols[2][j, i] - # else: - # aggs_and_cols[0][yi, xi] += aggs_and_cols[2][j, i] - # - # # - # if not np.isnan(aggs_and_cols[2][j, i]): - # aggs_and_cols[1][yi, xi] += 1 - def extend(aggs, xr_ds, vt, bounds): # Convert from bin centers to interval edges xs = infer_interval_breaks(xr_ds[x_name].values) From 0523dd213b98bcd3d69519958edb773ce3798642 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 08:29:47 -0400 Subject: [PATCH 16/23] Add initial Curvilinear implementation --- datashader/glyphs/glyph.py | 16 +++++ datashader/glyphs/quadmesh.py | 132 +++++++++++++++++++++++++++++++--- 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/datashader/glyphs/glyph.py b/datashader/glyphs/glyph.py index 324b0700f..4427eff2b 100644 --- a/datashader/glyphs/glyph.py +++ b/datashader/glyphs/glyph.py @@ -62,6 +62,22 @@ def _compute_y_bounds(ys): return minval, maxval + @staticmethod + @ngjit + def _compute_bounds_2d(vals): + minval = np.inf + maxval = -np.inf + for i in range(vals.shape[0]): + for j in range(vals.shape[1]): + v = vals[i][j] + if not np.isnan(v): + if v < minval: + minval = v + if v > maxval: + maxval = v + + return minval, maxval + def expand_aggs_and_cols(self, append): """ Create a decorator that can be used on functions that accept diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 0040969ec..c366a572d 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -66,7 +66,7 @@ def _build_extend(self, x_mapper, y_mapper, info, append): @ngjit @self.expand_aggs_and_cols(append) - def _extend(vt, bounds, xs, ys, *aggs_and_cols): + def _extend(xs, ys, *aggs_and_cols): for i in range(len(xs) - 1): x0i, x1i = xs[i], xs[i + 1] @@ -100,14 +100,14 @@ def _extend(vt, bounds, xs, ys, *aggs_and_cols): def extend(aggs, xr_ds, vt, bounds): # Convert from bin centers to interval edges - xs = infer_interval_breaks(xr_ds[x_name].values) - ys = infer_interval_breaks(xr_ds[y_name].values) + x_breaks = infer_interval_breaks(xr_ds[x_name].values) + y_breaks = infer_interval_breaks(xr_ds[y_name].values) x0, x1, y0, y1 = bounds xspan = x1 - x0 yspan = y1 - y0 - xscaled = (x_mapper(xs) - x0) / xspan - yscaled = (y_mapper(ys) - y0) / yspan + xscaled = (x_mapper(x_breaks) - x0) / xspan + yscaled = (y_mapper(y_breaks) - y0) / yspan xmask = np.where((xscaled >= 0) & (xscaled <= 1)) ymask = np.where((yscaled >= 0) & (yscaled <= 1)) @@ -120,16 +120,132 @@ def extend(aggs, xr_ds, vt, bounds): xs = (xscaled[xm0:xm1 + 1] * plot_width).astype(int).clip(0, plot_width) ys = (yscaled[ym0:ym1 + 1] * plot_height).astype(int).clip(0, plot_height) - # For each of aggs and cols, down select to valid range + # For input "column", down select to valid range cols_full = info(xr_ds.transpose(y_name, x_name)) cols = tuple([c[ym0:ym1, xm0:xm1] for c in cols_full]) aggs_and_cols = aggs + cols - _extend(vt, bounds, xs, ys, *aggs_and_cols) + _extend(xs, ys, *aggs_and_cols) return extend class QuadMeshCurvialinear(_QuadMeshLike): - pass + def compute_x_bounds(self, xr_ds): + xs = xr_ds[self.x].values + xs = infer_interval_breaks(xs, axis=1) + xs = infer_interval_breaks(xs, axis=0) + bounds = Glyph._compute_bounds_2d(xs) + return self.maybe_expand_bounds(bounds) + + def compute_y_bounds(self, xr_ds): + ys = xr_ds[self.y].values + ys = infer_interval_breaks(ys, axis=1) + ys = infer_interval_breaks(ys, axis=0) + bounds = Glyph._compute_bounds_2d(ys) + return self.maybe_expand_bounds(bounds) + + @memoize + def _build_extend(self, x_mapper, y_mapper, info, append): + x_name = self.x + y_name = self.y + + @ngjit + @self.expand_aggs_and_cols(append) + def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): + y_len, x_len, = xs.shape + + for i in range(x_len - 1): + for j in range(y_len - 1): + + # Extract quad vertices + x1 = xs[j, i] + x2 = xs[j, i + 1] + x3 = xs[j + 1, i + 1] + x4 = xs[j + 1, i] + + y1 = ys[j, i] + y2 = ys[j, i + 1] + y3 = ys[j + 1, i + 1] + y4 = ys[j + 1, i] + + # Compute the rectilinear bounding box around the quad + xmin = max(min(x1, x2, x3, x4), 0) + xmax = min(max(x1, x2, x3, x4), plot_width - 1) + ymin = max(min(y1, y2, y3, y4), 0) + ymax = min(max(y1, y2, y3, y4), plot_height - 1) + + # Make sure single pixel quads are represented + if xmin == xmax: + xmax += 1 + + if ymin == ymax: + ymax += 1 + + in_quad = [] + for xi in range(xmin, xmax): + for yi in range(ymin, ymax): + if point_in_quad( + x1, x2, x3, x4, y1, y2, y3, y4, xi, yi): + append(j, i, xi, yi, *aggs_and_cols) + in_quad.append((xi, y1)) + + def extend(aggs, xr_ds, vt, bounds): + # Convert from bin centers to interval edges + x_breaks = xr_ds[x_name].values + x_breaks = infer_interval_breaks(x_breaks, axis=1) + x_breaks = infer_interval_breaks(x_breaks, axis=0) + + y_breaks = xr_ds[y_name].values + y_breaks = infer_interval_breaks(y_breaks, axis=1) + y_breaks = infer_interval_breaks(y_breaks, axis=0) + + # Scale x and y vertices into integer canvas coordinates + x0, x1, y0, y1 = bounds + xspan = x1 - x0 + yspan = y1 - y0 + xscaled = (x_mapper(x_breaks) - x0) / xspan + yscaled = (y_mapper(y_breaks) - y0) / yspan + + plot_height, plot_width = aggs[0].shape[:2] + + xs = (xscaled * plot_width).astype(int) + ys = (yscaled * plot_height).astype(int) + + # Question: Should we try to compute a slice of xs and ys that + # eliminates rows and columns of quads that are all outside the + # viewport? + + aggs_and_cols = aggs + info(xr_ds) + _extend(plot_height, plot_width, xs, ys, *aggs_and_cols) + + return extend + + +@ngjit +def tri_area(x1, y1, x2, y2, x3, y3): + return abs((x1 * (y2 - y3) + + x2 * (y3 - y1) + + x3 * (y1 - y2)) / 2.0) + + +@ngjit +def point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x, y): + quad_area = (tri_area(x1, y1, x2, y2, x3, y3) + + tri_area(x1, y1, x4, y4, x3, y3)) + + area_1 = tri_area(x, y, x1, y1, x2, y2) + area_2 = tri_area(x, y, x2, y2, x3, y3) + area_3 = tri_area(x, y, x3, y3, x4, y4) + area_4 = tri_area(x, y, x1, y1, x4, y4) + + return quad_area == (area_1 + area_2 + area_3 + area_4) + + +@ngjit +def pixel_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0): + return (point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0) | + point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0+1, y0) | + point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0+1) | + point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0+1, y0+1)) From 2d1d1c0c099d7f11390f772385cca672921b5330 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 12:18:28 -0400 Subject: [PATCH 17/23] Switch to raycasting inclusion test --- datashader/glyphs/quadmesh.py | 156 +++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index c366a572d..1e3c5c685 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -159,22 +159,45 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): for i in range(x_len - 1): for j in range(y_len - 1): - # Extract quad vertices - x1 = xs[j, i] - x2 = xs[j, i + 1] - x3 = xs[j + 1, i + 1] - x4 = xs[j + 1, i] - - y1 = ys[j, i] - y2 = ys[j, i + 1] - y3 = ys[j + 1, i + 1] - y4 = ys[j + 1, i] - - # Compute the rectilinear bounding box around the quad - xmin = max(min(x1, x2, x3, x4), 0) - xmax = min(max(x1, x2, x3, x4), plot_width - 1) - ymin = max(min(y1, y2, y3, y4), 0) - ymax = min(max(y1, y2, y3, y4), plot_height - 1) + # make array of quad x any vertices + xverts = np.array([ + xs[j, i], + xs[j, i + 1], + xs[j + 1, i + 1], + xs[j + 1, i], + xs[j, i]] + ) + + yverts = np.array([ + ys[j, i], + ys[j, i + 1], + ys[j + 1, i + 1], + ys[j + 1, i], + ys[j, i], + ]) + + # Compute the rectilinear bounding box around the quad and + # skip quad if there is no chance for it to intersect + # viewport + xmin = min(xverts) + if xmin >= plot_width: + continue + xmin = max(xmin, 0) + + xmax = max(xverts) + if xmax < 0: + continue + xmax = min(xmax, plot_width - 1) + + ymin = min(yverts) + if ymin >= plot_height: + continue + ymin = max(ymin, 0) + + ymax = max(yverts) + if ymax < 0: + continue + ymax = min(ymax, plot_height - 1) # Make sure single pixel quads are represented if xmin == xmax: @@ -183,13 +206,70 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): if ymin == ymax: ymax += 1 - in_quad = [] - for xi in range(xmin, xmax): - for yi in range(ymin, ymax): - if point_in_quad( - x1, x2, x3, x4, y1, y2, y3, y4, xi, yi): + # make array holding whether each edge is increasing + # vertically (+1), decreasing vertically (-1), + # or horizontal (0). + yincreasing = np.zeros(4, dtype=np.int8) + yincreasing[yverts[1:] > yverts[:-1]] = 1 + yincreasing[yverts[1:] < yverts[:-1]] = -1 + + # Init array that will hold mask of whether edges are + # eligible for intersection tests + eligible = np.ones(4, dtype=np.int8) + + # Init array that will hold a mask of whether edges + # intersect the ray to the right of test point + intersect = np.zeros(4, dtype=np.int8) + + for yi in range(ymin, ymax): + eligible.fill(True) + for xi in range(xmin, xmax): + intersect.fill(False) + # Test edges + for edge_i in range(4): + # Skip if we already know edge is ineligible + if not eligible[edge_i]: + continue + + # Check if edge is fully to left of point. If + # so, we don't need to consider it again for + # this row. + if ((xverts[edge_i] < xi) and + (xverts[edge_i + 1] < xi)): + eligible[edge_i] = False + continue + + # Check if edge is fully above or below point. + # If so, we don't need to consider it again + # for this row. + if ((yverts[edge_i] > yi) == + (yverts[edge_i + 1] > yi)): + + eligible[edge_i] = False + continue + + # Now check if edge is to the right of point. + # A is vector from point to first vertex + ax = xverts[edge_i] - xi + ay = yverts[edge_i] - yi + + # B is vector from point to second vertex + bx = xverts[edge_i + 1] - xi + by = yverts[edge_i + 1] - yi + + # Compute cross product of B and A + bxa = (bx*ay - by*ax) + + # If cross product has same sign as yincreasing + # then edge intersects to the right + intersect[edge_i] = ( + bxa * yincreasing[edge_i] < 0 + ) + + if intersect.sum() % 2 == 1: + # If odd number of intersections, point + # is inside quad append(j, i, xi, yi, *aggs_and_cols) - in_quad.append((xi, y1)) def extend(aggs, xr_ds, vt, bounds): # Convert from bin centers to interval edges @@ -224,28 +304,28 @@ def extend(aggs, xr_ds, vt, bounds): @ngjit -def tri_area(x1, y1, x2, y2, x3, y3): - return abs((x1 * (y2 - y3) + - x2 * (y3 - y1) + - x3 * (y1 - y2)) / 2.0) +def tri_area(x0, y0, x1, y1, x2, y2): + return abs((x0 * (y1 - y2) + + x1 * (y2 - y0) + + x2 * (y0 - y1)) / 2.0) @ngjit -def point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x, y): - quad_area = (tri_area(x1, y1, x2, y2, x3, y3) + - tri_area(x1, y1, x4, y4, x3, y3)) +def point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y): + quad_area = (tri_area(x0, y0, x1, y1, x2, y2) + + tri_area(x0, y0, x3, y3, x2, y2)) - area_1 = tri_area(x, y, x1, y1, x2, y2) - area_2 = tri_area(x, y, x2, y2, x3, y3) - area_3 = tri_area(x, y, x3, y3, x4, y4) - area_4 = tri_area(x, y, x1, y1, x4, y4) + area_1 = tri_area(x, y, x0, y0, x1, y1) + area_2 = tri_area(x, y, x1, y1, x2, y2) + area_3 = tri_area(x, y, x2, y2, x3, y3) + area_4 = tri_area(x, y, x0, y0, x3, y3) return quad_area == (area_1 + area_2 + area_3 + area_4) @ngjit -def pixel_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0): - return (point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0) | - point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0+1, y0) | - point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0, y0+1) | - point_in_quad(x1, x2, x3, x4, y1, y2, y3, y4, x0+1, y0+1)) +def pixel_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y): + return (point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y) | + point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x + 1, y) | + point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y + 1) | + point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x + 1, y + 1)) From 0095ab5f70a1d7d99ae2196f856ae932e14ffdbf Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 13:03:29 -0400 Subject: [PATCH 18/23] Speedup by initializing numpy arrays outside main loop --- datashader/glyphs/quadmesh.py | 71 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 1e3c5c685..6340fe0a9 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -156,25 +156,40 @@ def _build_extend(self, x_mapper, y_mapper, info, append): def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): y_len, x_len, = xs.shape + # For performance, we initialize all arrays once before the loop + + # xverts/yverts arrays + xverts = np.zeros(5, dtype=np.int32) + yverts = np.zeros(5, dtype=np.int32) + + # Array holding whether each edge is increasing + # vertically (+1), decreasing vertically (-1), + # or horizontal (0). + yincreasing = np.zeros(4, dtype=np.int8) + + # Array that will hold mask of whether edges are + # eligible for intersection tests + eligible = np.ones(4, dtype=np.int8) + + # Array that will hold a mask of whether edges + # intersect the ray to the right of test point + intersect = np.zeros(4, dtype=np.int8) + for i in range(x_len - 1): for j in range(y_len - 1): # make array of quad x any vertices - xverts = np.array([ - xs[j, i], - xs[j, i + 1], - xs[j + 1, i + 1], - xs[j + 1, i], - xs[j, i]] - ) - - yverts = np.array([ - ys[j, i], - ys[j, i + 1], - ys[j + 1, i + 1], - ys[j + 1, i], - ys[j, i], - ]) + xverts[0] = xs[j, i] + xverts[1] = xs[j, i + 1] + xverts[2] = xs[j + 1, i + 1] + xverts[3] = xs[j + 1, i] + xverts[4] = xs[j, i] + + yverts[0] = ys[j, i] + yverts[1] = ys[j, i + 1] + yverts[2] = ys[j + 1, i + 1] + yverts[3] = ys[j + 1, i] + yverts[4] = ys[j, i] # Compute the rectilinear bounding box around the quad and # skip quad if there is no chance for it to intersect @@ -206,25 +221,17 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): if ymin == ymax: ymax += 1 - # make array holding whether each edge is increasing - # vertically (+1), decreasing vertically (-1), + # make yincreasing an array holding whether each edge is + # increasing vertically (+1), decreasing vertically (-1), # or horizontal (0). - yincreasing = np.zeros(4, dtype=np.int8) + yincreasing.fill(0) yincreasing[yverts[1:] > yverts[:-1]] = 1 yincreasing[yverts[1:] < yverts[:-1]] = -1 - # Init array that will hold mask of whether edges are - # eligible for intersection tests - eligible = np.ones(4, dtype=np.int8) - - # Init array that will hold a mask of whether edges - # intersect the ray to the right of test point - intersect = np.zeros(4, dtype=np.int8) - for yi in range(ymin, ymax): - eligible.fill(True) + eligible.fill(1) for xi in range(xmin, xmax): - intersect.fill(False) + intersect.fill(0) # Test edges for edge_i in range(4): # Skip if we already know edge is ineligible @@ -236,7 +243,7 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): # this row. if ((xverts[edge_i] < xi) and (xverts[edge_i + 1] < xi)): - eligible[edge_i] = False + eligible[edge_i] = 0 continue # Check if edge is fully above or below point. @@ -245,7 +252,7 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): if ((yverts[edge_i] > yi) == (yverts[edge_i + 1] > yi)): - eligible[edge_i] = False + eligible[edge_i] = 0 continue # Now check if edge is to the right of point. @@ -293,10 +300,6 @@ def extend(aggs, xr_ds, vt, bounds): xs = (xscaled * plot_width).astype(int) ys = (yscaled * plot_height).astype(int) - # Question: Should we try to compute a slice of xs and ys that - # eliminates rows and columns of quads that are all outside the - # viewport? - aggs_and_cols = aggs + info(xr_ds) _extend(plot_height, plot_width, xs, ys, *aggs_and_cols) From 51029f885fb1ec7109a18028125942724a003a0a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 15:25:41 -0400 Subject: [PATCH 19/23] Don't skip rendering last row/col in canvas --- datashader/glyphs/quadmesh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 6340fe0a9..622b37026 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -202,7 +202,7 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): xmax = max(xverts) if xmax < 0: continue - xmax = min(xmax, plot_width - 1) + xmax = min(xmax, plot_width) ymin = min(yverts) if ymin >= plot_height: @@ -212,13 +212,13 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): ymax = max(yverts) if ymax < 0: continue - ymax = min(ymax, plot_height - 1) + ymax = min(ymax, plot_height) # Make sure single pixel quads are represented - if xmin == xmax: + if xmin == xmax and xmax < plot_width: xmax += 1 - if ymin == ymax: + if ymin == ymax and ymax < plot_height: ymax += 1 # make yincreasing an array holding whether each edge is From d5005b26e37580adf06e01cd60748f79668f6f19 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 15:27:35 -0400 Subject: [PATCH 20/23] Transpose Dataset dimensions to match coordinate dimensions --- datashader/glyphs/quadmesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 622b37026..3ca8c0cf3 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -300,7 +300,8 @@ def extend(aggs, xr_ds, vt, bounds): xs = (xscaled * plot_width).astype(int) ys = (yscaled * plot_height).astype(int) - aggs_and_cols = aggs + info(xr_ds) + coord_dims = xr_ds.coords[x_name].dims + aggs_and_cols = aggs + info(xr_ds.transpose(*coord_dims)) _extend(plot_height, plot_width, xs, ys, *aggs_and_cols) return extend From fa57e482e30a386f3e50d16fe9ad285e4e9cf062 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 17:51:29 -0400 Subject: [PATCH 21/23] Fix subpixel rendering to always display single pixel quads --- datashader/glyphs/quadmesh.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 3ca8c0cf3..7174baf83 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -214,12 +214,19 @@ def _extend(plot_height, plot_width, xs, ys, *aggs_and_cols): continue ymax = min(ymax, plot_height) - # Make sure single pixel quads are represented - if xmin == xmax and xmax < plot_width: - xmax += 1 + # Handle subpixel quads + if xmin == xmax or ymin == ymax: + # If either dimension is a single pixel, then render it + if xmin == xmax and xmax < plot_width: + xmax += 1 + if ymin == ymax and ymax < plot_height: + ymax += 1 + + for yi in range(ymin, ymax): + for xi in range(xmin, xmax): + append(j, i, xi, yi, *aggs_and_cols) - if ymin == ymax and ymax < plot_height: - ymax += 1 + continue # make yincreasing an array holding whether each edge is # increasing vertically (+1), decreasing vertically (-1), From 5a3372a0890b750f253f92729a120e46ace9de6a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 17:53:34 -0400 Subject: [PATCH 22/23] Curvilinear Tests --- datashader/tests/test_quadmesh.py | 169 ++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/datashader/tests/test_quadmesh.py b/datashader/tests/test_quadmesh.py index 62bf648d1..7243d7baf 100644 --- a/datashader/tests/test_quadmesh.py +++ b/datashader/tests/test_quadmesh.py @@ -3,6 +3,7 @@ from numpy import nan import xarray as xr import datashader as ds +from collections import OrderedDict def test_rect_quadmesh_autorange(): @@ -125,3 +126,171 @@ def test_subpixel_quads_represented(): # Check transpose gives same answer res = c.quadmesh(da.transpose('a', 'b'), x='a', y='b', agg=ds.sum('Z')) assert res.equals(out) + + +def test_curve_quadmesh_rect_autorange(): + c = ds.Canvas(plot_width=8, plot_height=4) + Qx = np.array( + [[1, 2], + [1, 2]] + ) + Qy = np.array( + [[1, 1], + [2, 2]] + ) + Z = np.arange(4, dtype='int32').reshape(2, 2) + da = xr.DataArray( + Z, + coords={'Qx': (['Y', 'X'], Qx), + 'Qy': (['Y', 'X'], Qy)}, + dims=['Y', 'X'], + name='Z', + ) + + y_coords = np.linspace(0.75, 2.25, 4) + x_coords = np.linspace(0.625, 2.375, 8) + out = xr.DataArray(np.array( + [[0., 0., 0., 0., 1., 1., 1., 1.], + [0., 0., 0., 0., 1., 1., 1., 1.], + [2., 2., 2., 2., 3., 3., 3., 3.], + [2., 2., 2., 2., 3., 3., 3., 3.]], + ), + coords=[('Qy', y_coords), + ('Qx', x_coords)] + ) + + res = c.quadmesh(da, x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + res = c.quadmesh(da.transpose('X', 'Y'), x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_curve_quadmesh_autorange(): + c = ds.Canvas(plot_width=4, plot_height=8) + Qx = np.array( + [[1, 2], + [1, 2]] + ) + Qy = np.array( + [[1, 1], + [4, 2]] + ) + Z = np.arange(4, dtype='int32').reshape(2, 2) + da = xr.DataArray( + Z, + coords={'Qx': (['Y', 'X'], Qx), + 'Qy': (['Y', 'X'], Qy)}, + dims=['Y', 'X'], + name='Z', + ) + + x_coords = np.linspace(0.75, 2.25, 4) + y_coords = np.linspace(-0.5, 6.5, 8) + out = xr.DataArray(np.array( + [[nan, nan, nan, nan], + [0., 0., nan, nan], + [0., 0., 1., 1.], + [0., 0., 3., 3.], + [2., 2., 3., nan], + [2., 2., nan, nan], + [2., 2., nan, nan], + [2., nan, nan, nan]] + ), + coords=OrderedDict([ + ('Qx', x_coords), + ('Qy', y_coords)]), + dims=['Qy', 'Qx'] + ) + + res = c.quadmesh(da, x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + res = c.quadmesh(da.transpose('X', 'Y'), x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_curve_quadmesh_manual_range(): + c = ds.Canvas(plot_width=4, plot_height=8, x_range=[1, 2], y_range=[1, 3]) + Qx = np.array( + [[1, 2], + [1, 2]] + ) + Qy = np.array( + [[1, 1], + [4, 2]] + ) + Z = np.arange(4, dtype='int32').reshape(2, 2) + da = xr.DataArray( + Z, + coords={'Qx': (['Y', 'X'], Qx), + 'Qy': (['Y', 'X'], Qy)}, + dims=['Y', 'X'], + name='Z', + ) + + x_coords = np.linspace(1.125, 1.875, 4) + y_coords = np.linspace(1.125, 2.875, 8) + out = xr.DataArray(np.array( + [[0., 0., 1., 1.], + [0., 0., 1., 1.], + [0., 0., 1., 1.], + [0., 0., 1., 3.], + [0., 0., 3., 3.], + [0., 2., 3., 3.], + [2., 2., 3., 3.], + [2., 2., 3., 3.]] + ), + coords=OrderedDict([ + ('Qx', x_coords), + ('Qy', y_coords)]), + dims=['Qy', 'Qx'] + ) + + res = c.quadmesh(da, x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + res = c.quadmesh(da.transpose('X', 'Y'), x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + +def test_curve_quadmesh_manual_range_subpixel(): + c = ds.Canvas(plot_width=3, plot_height=5, + x_range=[-150, 150], y_range=[-250, 250]) + Qx = np.array( + [[1, 2], + [1, 2]] + ) + Qy = np.array( + [[1, 1], + [4, 2]] + ) + Z = np.arange(4, dtype='int32').reshape(2, 2) + da = xr.DataArray( + Z, + coords={'Qx': (['Y', 'X'], Qx), + 'Qy': (['Y', 'X'], Qy)}, + dims=['Y', 'X'], + name='Z', + ) + + x_coords = np.linspace(-100, 100, 3) + y_coords = np.linspace(-200, 200, 5) + out = xr.DataArray(np.array( + [[nan, nan, nan], + [nan, nan, nan], + [nan, 6., nan], + [nan, nan, nan], + [nan, nan, nan]] + ), + coords=OrderedDict([ + ('Qx', x_coords), + ('Qy', y_coords)]), + dims=['Qy', 'Qx'] + ) + + res = c.quadmesh(da, x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) + + res = c.quadmesh(da.transpose('X', 'Y'), x='Qx', y='Qy', agg=ds.sum('Z')) + assert res.equals(out) From 33faae1c5421edecc1ea062e2801bd73be55ef65 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 16 Aug 2019 18:46:55 -0400 Subject: [PATCH 23/23] Remove unused quadmesh utility functions --- datashader/glyphs/quadmesh.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/datashader/glyphs/quadmesh.py b/datashader/glyphs/quadmesh.py index 7174baf83..098704eca 100644 --- a/datashader/glyphs/quadmesh.py +++ b/datashader/glyphs/quadmesh.py @@ -312,31 +312,3 @@ def extend(aggs, xr_ds, vt, bounds): _extend(plot_height, plot_width, xs, ys, *aggs_and_cols) return extend - - -@ngjit -def tri_area(x0, y0, x1, y1, x2, y2): - return abs((x0 * (y1 - y2) + - x1 * (y2 - y0) + - x2 * (y0 - y1)) / 2.0) - - -@ngjit -def point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y): - quad_area = (tri_area(x0, y0, x1, y1, x2, y2) + - tri_area(x0, y0, x3, y3, x2, y2)) - - area_1 = tri_area(x, y, x0, y0, x1, y1) - area_2 = tri_area(x, y, x1, y1, x2, y2) - area_3 = tri_area(x, y, x2, y2, x3, y3) - area_4 = tri_area(x, y, x0, y0, x3, y3) - - return quad_area == (area_1 + area_2 + area_3 + area_4) - - -@ngjit -def pixel_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y): - return (point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y) | - point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x + 1, y) | - point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x, y + 1) | - point_in_quad(x0, x1, x2, x3, y0, y1, y2, y3, x + 1, y + 1))