diff --git a/datashader/core.py b/datashader/core.py index dd811fb71..b919aaa76 100644 --- a/datashader/core.py +++ b/datashader/core.py @@ -54,15 +54,13 @@ def compute_scale_and_translate(self, range, n): ---------- range : tuple A tuple representing the range ``[min, max]`` along the axis, in - data space. min is inclusive and max is exclusive. + data space. Both min and max are inclusive. n : int The number of bins along the axis. Returns ------- s, t : floats - Parameters represe - """ start, end = map(self.mapper, range) s = n/(end - start) diff --git a/datashader/dask.py b/datashader/dask.py index a94e1e174..776f45fd9 100644 --- a/datashader/dask.py +++ b/datashader/dask.py @@ -29,6 +29,7 @@ def shape_bounds_st_and_axis(df, canvas, glyph): y_range = canvas.y_range or glyph._compute_y_bounds_dask(df) x_min, x_max, y_min, y_max = bounds = compute(*(x_range + y_range)) x_range, y_range = (x_min, x_max), (y_min, y_max) + width = canvas.plot_width height = canvas.plot_height diff --git a/datashader/glyphs.py b/datashader/glyphs.py index fb9cb1f4e..bb1a3c517 100644 --- a/datashader/glyphs.py +++ b/datashader/glyphs.py @@ -77,6 +77,8 @@ class Point(_PointLike): """A point, with center at ``x`` and ``y``. Points map each record to a single bin. + Points falling exactly on the upper bounds are treated as a special case, + mapping into the previous bin rather than being cropped off. Parameters ---------- @@ -92,14 +94,20 @@ def _build_extend(self, x_mapper, y_mapper, info, append): def _extend(vt, bounds, xs, ys, *aggs_and_cols): sx, tx, sy, ty = vt xmin, xmax, ymin, ymax = bounds + + def map_onto_pixel(x, y): + xx = int(x_mapper(x) * sx + tx) + yy = int(y_mapper(y) * sy + ty) + # Points falling on upper bound are mapped into previous bin + return (xx - 1 if x == xmax else xx, + yy - 1 if y == ymax else yy) + for i in range(xs.shape[0]): x = xs[i] y = ys[i] - if (xmin <= x < xmax) and (ymin <= y < ymax): - append(i, - int(x_mapper(x) * sx + tx), - int(y_mapper(y) * sy + ty), - *aggs_and_cols) + if (xmin <= x <= xmax) and (ymin <= y <= ymax): + xi, yi = map_onto_pixel(x, y) + append(i, xi, yi, *aggs_and_cols) def extend(aggs, df, vt, bounds): xs = df[x_name].values @@ -127,16 +135,10 @@ def _build_extend(self, x_mapper, y_mapper, info, append): y_name = self.y def extend(aggs, df, vt, bounds, plot_start=True): - # Scale/transform float bounds to integer space and adjust for - # exclusive upper bounds - xmin, xmax, ymin, ymax = map_onto_pixel(vt, *bounds) - mapped_bounds = (xmin, xmax - 1, ymin, ymax - 1) - xs = df[x_name].values ys = df[y_name].values cols = aggs + info(df) - - extend_line(vt, bounds, mapped_bounds, xs, ys, plot_start, *cols) + extend_line(vt, bounds, xs, ys, plot_start, *cols) return extend @@ -169,13 +171,15 @@ def _compute_outcode(x, y, xmin, xmax, ymin, ymax): def _build_map_onto_pixel(x_mapper, y_mapper): @ngjit - def map_onto_pixel(vt, x0, x1, y0, y1): + def map_onto_pixel(vt, bounds, x, y): """Map points onto pixel grid""" sx, tx, sy, ty = vt - return (int(x_mapper(x0) * sx + tx), - int(x_mapper(x1) * sx + tx), - int(y_mapper(y0) * sy + ty), - int(y_mapper(y1) * sy + ty)) + _, xmax, _, ymax = bounds + xx = int(x_mapper(x) * sx + tx) + yy = int(y_mapper(y) * sy + ty) + # Points falling on upper bound are mapped into previous bin + return (xx - 1 if x == xmax else xx, + yy - 1 if y == ymax else yy) return map_onto_pixel @@ -234,9 +238,9 @@ def draw_line(x0i, y0i, x1i, y1i, i, plot_start, clipped, *aggs_and_cols): def _build_extend_line(draw_line, map_onto_pixel): @ngjit - def extend_line(vt, bounds, mapped_bounds, xs, ys, plot_start, *aggs_and_cols): + def extend_line(vt, bounds, xs, ys, plot_start, *aggs_and_cols): """Aggregate along a line formed by ``xs`` and ``ys``""" - xmin, xmax, ymin, ymax = mapped_bounds + xmin, xmax, ymin, ymax = bounds nrows = xs.shape[0] i = 0 while i < nrows - 1: @@ -252,11 +256,9 @@ def extend_line(vt, bounds, mapped_bounds, xs, ys, plot_start, *aggs_and_cols): i += 1 continue - x0i, x1i, y0i, y1i = map_onto_pixel(vt, x0, x1, y0, y1) - # Use Cohen-Sutherland to clip the segment to a bounding box - outcode0 = _compute_outcode(x0i, y0i, xmin, xmax, ymin, ymax) - outcode1 = _compute_outcode(x1i, y1i, xmin, xmax, ymin, ymax) + outcode0 = _compute_outcode(x0, y0, xmin, xmax, ymin, ymax) + outcode1 = _compute_outcode(x1, y1, xmin, xmax, ymin, ymax) accept = False clipped = False @@ -268,34 +270,34 @@ def extend_line(vt, bounds, mapped_bounds, xs, ys, plot_start, *aggs_and_cols): elif outcode0 & outcode1: plot_start = True break + + clipped = True + outcode_out = outcode0 if outcode0 else outcode1 + if outcode_out & TOP: + x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0) + y = ymax + elif outcode_out & BOTTOM: + x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0) + y = ymin + elif outcode_out & RIGHT: + y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0) + x = xmax + elif outcode_out & LEFT: + y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0) + x = xmin + + if outcode_out == outcode0: + x0, y0 = x, y + outcode0 = _compute_outcode(x0, y0, xmin, xmax, ymin, ymax) + # If x0 is clipped, we need to plot the new start + plot_start = True else: - clipped = True - outcode_out = outcode0 if outcode0 else outcode1 - if outcode_out & TOP: - x = x0i + int((x1i - x0i) * (ymax - y0i) / (y1i - y0i)) - y = ymax - elif outcode_out & BOTTOM: - x = x0i + int((x1i - x0i) * (ymin - y0i) / (y1i - y0i)) - y = ymin - elif outcode_out & RIGHT: - y = y0i + int((y1i - y0i) * (xmax - x0i) / (x1i - x0i)) - x = xmax - elif outcode_out & LEFT: - y = y0i + int((y1i - y0i) * (xmin - x0i) / (x1i - x0i)) - x = xmin - - if outcode_out == outcode0: - x0i, y0i = x, y - outcode0 = _compute_outcode(x0i, y0i, xmin, xmax, - ymin, ymax) - # If x0i is clipped, we need to plot the new start - plot_start = True - else: - x1i, y1i = x, y - outcode1 = _compute_outcode(x1i, y1i, xmin, xmax, - ymin, ymax) + x1, y1 = x, y + outcode1 = _compute_outcode(x1, y1, xmin, xmax, ymin, ymax) if accept: + x0i, y0i = map_onto_pixel(vt, bounds, x0, y0) + x1i, y1i = map_onto_pixel(vt, bounds, x1, y1) draw_line(x0i, y0i, x1i, y1i, i, plot_start, clipped, *aggs_and_cols) plot_start = False i += 1 diff --git a/datashader/pandas.py b/datashader/pandas.py index e13457ab9..5eb9335e4 100644 --- a/datashader/pandas.py +++ b/datashader/pandas.py @@ -17,6 +17,7 @@ def pandas_pipeline(df, schema, canvas, glyph, summary): x_range = canvas.x_range or glyph._compute_x_bounds(df[glyph.x].values) y_range = canvas.y_range or glyph._compute_y_bounds(df[glyph.y].values) + width = canvas.plot_width height = canvas.plot_height diff --git a/datashader/tests/test_dask.py b/datashader/tests/test_dask.py index b3fddc562..461327c4b 100644 --- a/datashader/tests/test_dask.py +++ b/datashader/tests/test_dask.py @@ -7,6 +7,8 @@ import datashader as ds +import pytest + set_options(get=get_sync) df = pd.DataFrame({'x': np.array(([0.] * 10 + [1] * 10)), @@ -25,15 +27,17 @@ ddf = dd.from_pandas(df, npartitions=3) -c = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 2), y_range=(0, 2)) -c_logx = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 11), - y_range=(0, 2), x_axis_type='log') -c_logy = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 2), - y_range=(1, 11), y_axis_type='log') -c_logxy = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 11), - y_range=(1, 11), x_axis_type='log', y_axis_type='log') - -coords = [np.arange(2, dtype='f8')+0.5, np.arange(2, dtype='f8')+0.5] +c = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 1), y_range=(0, 1)) +c_logx = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 10), + y_range=(0, 1), x_axis_type='log') +c_logy = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 1), + y_range=(1, 10), y_axis_type='log') +c_logxy = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 10), + y_range=(1, 10), x_axis_type='log', y_axis_type='log') + +axis = ds.core.LinearAxis() +lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) +coords = [lincoords, lincoords] dims = ['y', 'x'] @@ -41,6 +45,13 @@ def assert_eq(agg, b): assert agg.equals(b) +def floats(n): + """Returns contiguous list of floats from initial point""" + while True: + yield n + n = n + np.spacing(n) + + def test_count(): out = xr.DataArray(np.array([[5, 5], [5, 5]], dtype='i4'), coords=coords, dims=dims) @@ -56,12 +67,12 @@ def test_count(): def test_any(): out = xr.DataArray(np.array([[True, True], [True, True]]), coords=coords, dims=dims) - assert_eq(c.points(df, 'x', 'y', ds.any('i64')), out) - assert_eq(c.points(df, 'x', 'y', ds.any('f64')), out) - assert_eq(c.points(df, 'x', 'y', ds.any()), out) + assert_eq(c.points(ddf, 'x', 'y', ds.any('i64')), out) + assert_eq(c.points(ddf, 'x', 'y', ds.any('f64')), out) + assert_eq(c.points(ddf, 'x', 'y', ds.any()), out) out = xr.DataArray(np.array([[True, True], [True, False]]), coords=coords, dims=dims) - assert_eq(c.points(df, 'x', 'y', ds.any('empty_bin')), out) + assert_eq(c.points(ddf, 'x', 'y', ds.any('empty_bin')), out) def test_sum(): @@ -151,18 +162,98 @@ def test_multiple_aggregates(): assert_eq(agg.i32_count, f(np.array([[5, 5], [5, 5]], dtype='i4'))) +def test_auto_range_points(): + n = 10 + data = np.arange(n, dtype='i4') + df = pd.DataFrame({'time': np.arange(n), + 'x': data, + 'y': data}) + ddf = dd.from_pandas(df, npartitions=3) + + cvs = ds.Canvas(plot_width=n, plot_height=n) + agg = cvs.points(ddf, 'x', 'y', ds.count('time')) + sol = np.zeros((n, n), int) + np.fill_diagonal(sol, 1) + np.testing.assert_equal(agg.data, sol) + + cvs = ds.Canvas(plot_width=n+1, plot_height=n+1) + agg = cvs.points(ddf, 'x', 'y', ds.count('time')) + sol = np.zeros((n+1, n+1), int) + np.fill_diagonal(sol, 1) + sol[5, 5] = 0 + np.testing.assert_equal(agg.data, sol) + + n = 4 + data = np.arange(n, dtype='i4') + df = pd.DataFrame({'time': np.arange(n), + 'x': data, + 'y': data}) + ddf = dd.from_pandas(df, npartitions=3) + + cvs = ds.Canvas(plot_width=2*n, plot_height=2*n) + agg = cvs.points(ddf, 'x', 'y', ds.count('time')) + sol = np.zeros((2*n, 2*n), int) + np.fill_diagonal(sol, 1) + sol[[range(1, 4, 2)]] = 0 + sol[[range(4, 8, 2)]] = 0 + np.testing.assert_equal(agg.data, sol) + + cvs = ds.Canvas(plot_width=2*n+1, plot_height=2*n+1) + agg = cvs.points(ddf, 'x', 'y', ds.count('time')) + sol = np.zeros((2*n+1, 2*n+1), int) + sol[0, 0] = 1 + sol[3, 3] = 1 + sol[6, 6] = 1 + sol[8, 8] = 1 + np.testing.assert_equal(agg.data, sol) + + +def test_uniform_points(): + n = 101 + df = pd.DataFrame({'time': np.ones(2*n, dtype='i4'), + 'x': np.concatenate((np.arange(n, dtype='f8'), + np.arange(n, dtype='f8'))), + 'y': np.concatenate(([0.] * n, [1.] * n))}) + + cvs = ds.Canvas(plot_width=10, plot_height=2, y_range=(0, 1)) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.array([[10] * 9 + [11], [10] * 9 + [11]], dtype='i4') + np.testing.assert_equal(agg.data, sol) + + +@pytest.mark.parametrize('high', [9, 10, 99, 100]) +@pytest.mark.parametrize('low', [0]) +def test_uniform_diagonal_points(low, high): + bounds = (low, high) + x_range, y_range = bounds, bounds + + width = x_range[1] - x_range[0] + height = y_range[1] - y_range[0] + n = width * height + df = pd.DataFrame({'time': np.ones(n, dtype='i4'), + 'x': np.array([np.arange(*x_range, dtype='f8')] * width).flatten(), + 'y': np.array([np.arange(*y_range, dtype='f8')] * height).flatten()}) + + cvs = ds.Canvas(plot_width=2, plot_height=2, x_range=x_range, y_range=y_range) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + + diagonal = agg.data.diagonal(0) + assert sum(diagonal) == n + assert abs(bounds[1] - bounds[0]) % 2 == abs(diagonal[1] / high - diagonal[0] / high) + + def test_log_axis_points(): - # Upper bound for scale/index of x-axis - start, end = map(np.log10, (1, 11)) - s = 2/(end - start) - t = -start * s - px = np.arange(2)+0.5 - logcoords = 10**((px-t)/s) + axis = ds.core.LogAxis() + logcoords = axis.compute_index(axis.compute_scale_and_translate((1, 10), 2), 2) + + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) + sol = np.array([[5, 5], [5, 5]], dtype='i4') - out = xr.DataArray(sol, coords=[np.array([0.5, 1.5]), logcoords], + out = xr.DataArray(sol, coords=[lincoords, logcoords], dims=['y', 'log_x']) assert_eq(c_logx.points(ddf, 'log_x', 'y', ds.count('i32')), out) - out = xr.DataArray(sol, coords=[logcoords, np.array([0.5, 1.5])], + out = xr.DataArray(sol, coords=[logcoords, lincoords], dims=['log_y', 'x']) assert_eq(c_logy.points(ddf, 'x', 'log_y', ds.count('i32')), out) out = xr.DataArray(sol, coords=[logcoords, logcoords], @@ -171,11 +262,14 @@ def test_log_axis_points(): def test_line(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((-3., 3.), 7), 7) + df = pd.DataFrame({'x': [4, 0, -4, -3, -2, -1.9, 0, 10, 10, 0, 4], 'y': [0, -4, 0, 1, 2, 2.1, 4, 20, 30, 4, 0]}) ddf = dd.from_pandas(df, npartitions=3) cvs = ds.Canvas(plot_width=7, plot_height=7, - x_range=(-3, 4), y_range=(-3, 4)) + x_range=(-3, 3), y_range=(-3, 3)) agg = cvs.line(ddf, 'x', 'y', ds.count()) sol = np.array([[0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], @@ -184,25 +278,44 @@ def test_line(): [1, 0, 0, 0, 0, 0, 1], [0, 2, 0, 0, 0, 1, 0], [0, 0, 1, 0, 1, 0, 0]], dtype='i4') - out = xr.DataArray(sol, coords=[np.arange(-3., 4.)+0.5, np.arange(-3., 4.)+0.5], + out = xr.DataArray(sol, coords=[lincoords, lincoords], dims=['y', 'x']) assert_eq(agg, out) def test_log_axis_line(): - # Upper bound for scale/index of x-axis - start, end = map(np.log10, (1, 11)) - s = 2/(end - start) - t = -start * s - px = np.arange(2)+0.5 - logcoords = 10**((px-t)/s) + axis = ds.core.LogAxis() + logcoords = axis.compute_index(axis.compute_scale_and_translate((1, 10), 2), 2) + + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) + sol = np.array([[5, 5], [5, 5]], dtype='i4') - out = xr.DataArray(sol, coords=[np.array([0.5, 1.5]), logcoords], + out = xr.DataArray(sol, coords=[lincoords, logcoords], dims=['y', 'log_x']) assert_eq(c_logx.line(ddf, 'log_x', 'y', ds.count('i32')), out) - out = xr.DataArray(sol, coords=[logcoords, np.array([0.5, 1.5])], + out = xr.DataArray(sol, coords=[logcoords, lincoords], dims=['log_y', 'x']) assert_eq(c_logy.line(ddf, 'x', 'log_y', ds.count('i32')), out) out = xr.DataArray(sol, coords=[logcoords, logcoords], dims=['log_y', 'log_x']) assert_eq(c_logxy.line(ddf, 'log_x', 'log_y', ds.count('i32')), out) + + +def test_auto_range_line(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((-10., 10.), 5), 5) + + df = pd.DataFrame({'x': [-10, 0, 10, 0, -10], + 'y': [ 0, 10, 0, -10, 0]}) + ddf = dd.from_pandas(df, npartitions=3) + cvs = ds.Canvas(plot_width=5, plot_height=5) + agg = cvs.line(ddf, 'x', 'y', ds.count()) + sol = np.array([[0, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [2, 0, 0, 0, 1], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 0]], dtype='i4') + out = xr.DataArray(sol, coords=[lincoords, lincoords], + dims=['y', 'x']) + assert_eq(agg, out) diff --git a/datashader/tests/test_glyphs.py b/datashader/tests/test_glyphs.py index 087e6d3ea..0eb97665c 100644 --- a/datashader/tests/test_glyphs.py +++ b/datashader/tests/test_glyphs.py @@ -37,8 +37,6 @@ def new_agg(): bounds = (-3, 1, -3, 1) vt = (1., 3., 1., 3.) -xmin, xmax, ymin, ymax = map_onto_pixel(vt, *bounds) -mbounds = (xmin, xmax - 1, ymin, ymax - 1) def test_draw_line(): @@ -133,23 +131,23 @@ def test_extend_lines(): [0, 1, 0, 1, 0], [0, 0, 0, 0, 0]]) agg = new_agg() - extend_line(vt, bounds, mbounds, xs, ys, False, agg) + extend_line(vt, bounds, xs, ys, False, agg) np.testing.assert_equal(agg, out) # plot_start = True out[2, 3] += 1 agg = new_agg() - extend_line(vt, bounds, mbounds, xs, ys, True, agg) + extend_line(vt, bounds, xs, ys, True, agg) np.testing.assert_equal(agg, out) xs = np.array([2, 1, 0, -1, -4, -1, -100, -1, 2]) ys = np.array([-1, -2, -3, -4, -1, 2, 100, 2, -1]) out = np.array([[0, 1, 0, 1, 0], - [1, 0, 0, 0, 0], + [1, 0, 0, 1, 0], [0, 0, 0, 0, 0], - [1, 0, 0, 0, 0], + [1, 1, 0, 1, 0], [0, 0, 0, 0, 0]]) agg = new_agg() - extend_line(vt, bounds, mbounds, xs, ys, True, agg) + extend_line(vt, bounds, xs, ys, True, agg) np.testing.assert_equal(agg, out) @@ -157,7 +155,7 @@ def test_extend_lines_all_out_of_bounds(): xs = np.array([-100, -200, -100]) ys = np.array([0, 0, 1]) agg = new_agg() - extend_line(vt, bounds, mbounds, xs, ys, True, agg) + extend_line(vt, bounds, xs, ys, True, agg) assert agg.sum() == 0 @@ -165,6 +163,6 @@ def test_extend_lines_nan(): xs = np.array([-3, -2, np.nan, 0, 1]) ys = np.array([-3, -2, np.nan, 0, 1]) agg = new_agg() - extend_line(vt, bounds, mbounds, xs, ys, True, agg) - out = np.diag([1, 1, 0, 1, 0]) + extend_line(vt, bounds, xs, ys, True, agg) + out = np.diag([1, 1, 0, 2, 0]) np.testing.assert_equal(agg, out) diff --git a/datashader/tests/test_pandas.py b/datashader/tests/test_pandas.py index 413e2d4ff..313114639 100644 --- a/datashader/tests/test_pandas.py +++ b/datashader/tests/test_pandas.py @@ -4,6 +4,7 @@ import datashader as ds +import pytest df = pd.DataFrame({'x': np.array(([0.] * 10 + [1] * 10)), @@ -20,15 +21,17 @@ df.f32[2] = np.nan df.f64[2] = np.nan -c = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 2), y_range=(0, 2)) -c_logx = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 11), - y_range=(0, 2), x_axis_type='log') -c_logy = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 2), - y_range=(1, 11), y_axis_type='log') -c_logxy = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 11), - y_range=(1, 11), x_axis_type='log', y_axis_type='log') - -coords = [np.arange(2, dtype='f8')+0.5, np.arange(2, dtype='f8')+0.5] +c = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 1), y_range=(0, 1)) +c_logx = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 10), + y_range=(0, 1), x_axis_type='log') +c_logy = ds.Canvas(plot_width=2, plot_height=2, x_range=(0, 1), + y_range=(1, 10), y_axis_type='log') +c_logxy = ds.Canvas(plot_width=2, plot_height=2, x_range=(1, 10), + y_range=(1, 10), x_axis_type='log', y_axis_type='log') + +axis = ds.core.LinearAxis() +lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) +coords = [lincoords, lincoords] dims = ['y', 'x'] @@ -36,6 +39,13 @@ def assert_eq(agg, b): assert agg.equals(b) +def floats(n): + """Returns contiguous list of floats from initial point""" + while True: + yield n + n = n + np.spacing(n) + + def test_count(): out = xr.DataArray(np.array([[5, 5], [5, 5]], dtype='i4'), coords=coords, dims=dims) @@ -146,18 +156,96 @@ def test_multiple_aggregates(): assert_eq(agg.i32_count, f(np.array([[5, 5], [5, 5]], dtype='i4'))) +def test_auto_range_points(): + n = 10 + data = np.arange(n, dtype='i4') + df = pd.DataFrame({'time': np.arange(n), + 'x': data, + 'y': data}) + + cvs = ds.Canvas(plot_width=n, plot_height=n) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.zeros((n, n), int) + np.fill_diagonal(sol, 1) + np.testing.assert_equal(agg.data, sol) + + cvs = ds.Canvas(plot_width=n+1, plot_height=n+1) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.zeros((n+1, n+1), int) + np.fill_diagonal(sol, 1) + sol[5, 5] = 0 + np.testing.assert_equal(agg.data, sol) + + n = 4 + data = np.arange(n, dtype='i4') + df = pd.DataFrame({'time': np.arange(n), + 'x': data, + 'y': data}) + + cvs = ds.Canvas(plot_width=2*n, plot_height=2*n) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.zeros((2*n, 2*n), int) + np.fill_diagonal(sol, 1) + sol[[range(1, 4, 2)]] = 0 + sol[[range(4, 8, 2)]] = 0 + np.testing.assert_equal(agg.data, sol) + + cvs = ds.Canvas(plot_width=2*n+1, plot_height=2*n+1) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.zeros((2*n+1, 2*n+1), int) + sol[0, 0] = 1 + sol[3, 3] = 1 + sol[6, 6] = 1 + sol[8, 8] = 1 + np.testing.assert_equal(agg.data, sol) + + +def test_uniform_points(): + n = 101 + df = pd.DataFrame({'time': np.ones(2*n, dtype='i4'), + 'x': np.concatenate((np.arange(n, dtype='f8'), + np.arange(n, dtype='f8'))), + 'y': np.concatenate(([0.] * n, [1.] * n))}) + + cvs = ds.Canvas(plot_width=10, plot_height=2, y_range=(0, 1)) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + sol = np.array([[10] * 9 + [11], [10] * 9 + [11]], dtype='i4') + np.testing.assert_equal(agg.data, sol) + + +@pytest.mark.parametrize('high', [9, 10, 99, 100]) +@pytest.mark.parametrize('low', [0]) +def test_uniform_diagonal_points(low, high): + bounds = (low, high) + x_range, y_range = bounds, bounds + + width = x_range[1] - x_range[0] + height = y_range[1] - y_range[0] + n = width * height + df = pd.DataFrame({'time': np.ones(n, dtype='i4'), + 'x': np.array([np.arange(*x_range, dtype='f8')] * width).flatten(), + 'y': np.array([np.arange(*y_range, dtype='f8')] * height).flatten()}) + + cvs = ds.Canvas(plot_width=2, plot_height=2, x_range=x_range, y_range=y_range) + agg = cvs.points(df, 'x', 'y', ds.count('time')) + + diagonal = agg.data.diagonal(0) + assert sum(diagonal) == n + assert abs(bounds[1] - bounds[0]) % 2 == abs(diagonal[1] / high - diagonal[0] / high) + + def test_log_axis_points(): - # Upper bound for scale/index of x-axis - start, end = map(np.log10, (1, 11)) - s = 2/(end - start) - t = -start * s - px = np.arange(2)+0.5 - logcoords = 10**((px-t)/s) + axis = ds.core.LogAxis() + logcoords = axis.compute_index(axis.compute_scale_and_translate((1, 10), 2), 2) + + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) + sol = np.array([[5, 5], [5, 5]], dtype='i4') - out = xr.DataArray(sol, coords=[np.array([0.5, 1.5]), logcoords], + out = xr.DataArray(sol, coords=[lincoords, logcoords], dims=['y', 'log_x']) assert_eq(c_logx.points(df, 'log_x', 'y', ds.count('i32')), out) - out = xr.DataArray(sol, coords=[logcoords, np.array([0.5, 1.5])], + out = xr.DataArray(sol, coords=[logcoords, lincoords], dims=['log_y', 'x']) assert_eq(c_logy.points(df, 'x', 'log_y', ds.count('i32')), out) out = xr.DataArray(sol, coords=[logcoords, logcoords], @@ -166,10 +254,13 @@ def test_log_axis_points(): def test_line(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((-3., 3.), 7), 7) + df = pd.DataFrame({'x': [4, 0, -4, -3, -2, -1.9, 0, 10, 10, 0, 4], 'y': [0, -4, 0, 1, 2, 2.1, 4, 20, 30, 4, 0]}) cvs = ds.Canvas(plot_width=7, plot_height=7, - x_range=(-3, 4), y_range=(-3, 4)) + x_range=(-3, 3), y_range=(-3, 3)) agg = cvs.line(df, 'x', 'y', ds.count()) sol = np.array([[0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], @@ -178,25 +269,43 @@ def test_line(): [1, 0, 0, 0, 0, 0, 1], [0, 2, 0, 0, 0, 1, 0], [0, 0, 1, 0, 1, 0, 0]], dtype='i4') - out = xr.DataArray(sol, coords=[np.arange(-3., 4.)+0.5, np.arange(-3., 4.)+0.5], + out = xr.DataArray(sol, coords=[lincoords, lincoords], dims=['y', 'x']) assert_eq(agg, out) def test_log_axis_line(): - # Upper bound for scale/index of x-axis - start, end = map(np.log10, (1, 11)) - s = 2/(end - start) - t = -start * s - px = np.arange(2)+0.5 - logcoords = 10**((px-t)/s) + axis = ds.core.LogAxis() + logcoords = axis.compute_index(axis.compute_scale_and_translate((1, 10), 2), 2) + + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((0, 1), 2), 2) + sol = np.array([[5, 5], [5, 5]], dtype='i4') - out = xr.DataArray(sol, coords=[np.array([0.5, 1.5]), logcoords], + out = xr.DataArray(sol, coords=[lincoords, logcoords], dims=['y', 'log_x']) assert_eq(c_logx.line(df, 'log_x', 'y', ds.count('i32')), out) - out = xr.DataArray(sol, coords=[logcoords, np.array([0.5, 1.5])], + out = xr.DataArray(sol, coords=[logcoords, lincoords], dims=['log_y', 'x']) assert_eq(c_logy.line(df, 'x', 'log_y', ds.count('i32')), out) out = xr.DataArray(sol, coords=[logcoords, logcoords], dims=['log_y', 'log_x']) assert_eq(c_logxy.line(df, 'log_x', 'log_y', ds.count('i32')), out) + + +def test_auto_range_line(): + axis = ds.core.LinearAxis() + lincoords = axis.compute_index(axis.compute_scale_and_translate((-10., 10.), 5), 5) + + df = pd.DataFrame({'x': [-10, 0, 10, 0, -10], + 'y': [ 0, 10, 0, -10, 0]}) + cvs = ds.Canvas(plot_width=5, plot_height=5) + agg = cvs.line(df, 'x', 'y', ds.count()) + sol = np.array([[0, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [2, 0, 0, 0, 1], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 0]], dtype='i4') + out = xr.DataArray(sol, coords=[lincoords, lincoords], + dims=['y', 'x']) + assert_eq(agg, out)