import datetime as dt

from unittest import SkipTest, skipIf

import numpy as np
import pandas as pd
import pytest

from holoviews import (
    Dimension, Curve, Points, Image, Dataset, RGB, Path, Graph, TriMesh,
    QuadMesh, NdOverlay, Contours, Spikes, Spread, Area, Rectangles,
    Segments, Polygons, Nodes
)
from holoviews.streams import Tap
from holoviews.element.comparison import ComparisonTestCase
from numpy import nan
from packaging.version import Version

try:
    import datashader as ds
    import dask.dataframe as dd
    import xarray as xr
    from holoviews.operation.datashader import (
        aggregate, regrid, ds_version, stack, directly_connect_edges,
        shade, spread, rasterize, datashade, AggregationOperation,
        inspect, inspect_points, inspect_polygons
    )
except ImportError:
    raise SkipTest('Datashader not available')

try:
    import cudf
    import cupy
except ImportError:
    cudf = None

try:
    import spatialpandas
except ImportError:
    spatialpandas = None

spatialpandas_skip = skipIf(spatialpandas is None, "SpatialPandas not available")
cudf_skip = skipIf(cudf is None, "cuDF not available")


import logging

numba_logger = logging.getLogger('numba')
numba_logger.setLevel(logging.WARNING)

AggregationOperation.vdim_prefix = ''

class DatashaderAggregateTests(ComparisonTestCase):
    """
    Tests for datashader aggregation
    """

    def test_aggregate_points(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        self.assertEqual(img, expected)

    def test_aggregate_points_count_column(self):
        points = Points([(0.2, 0.3, np.NaN), (0.4, 0.7, 22), (0, 0.99,np.NaN)], vdims='z')
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2, aggregator=ds.count('z'))
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[0, 0], [1, 0]]),
                         vdims=[Dimension('z Count', nodata=0)])
        self.assertEqual(img, expected)

    @cudf_skip
    def test_aggregate_points_cudf(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)], datatype=['cuDF'])
        self.assertIsInstance(points.data, cudf.DataFrame)
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        self.assertIsInstance(img.data.Count.data, cupy.ndarray)
        self.assertEqual(img, expected)

    def test_aggregate_zero_range_points(self):
        p = Points([(0, 0), (1, 1)])
        agg = rasterize(p, x_range=(0, 0), y_range=(0, 1), expand=False, dynamic=False,
                        width=2, height=2)
        img = Image(([], [0.25, 0.75], np.zeros((2, 0))), bounds=(0, 0, 0, 1),
                    xdensity=1, vdims=[Dimension('Count', nodata=0)])
        self.assertEqual(agg, img)

    def test_aggregate_points_target(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(points, dynamic=False,  target=expected)
        self.assertEqual(img, expected)

    def test_aggregate_points_sampling(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        x_sampling=0.5, y_sampling=0.5)
        self.assertEqual(img, expected)

    def test_aggregate_points_categorical(self):
        points = Points([(0.2, 0.3, 'A'), (0.4, 0.7, 'B'), (0, 0.99, 'C')], vdims='z')
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2, aggregator=ds.count_cat('z'))
        xs, ys = [0.25, 0.75], [0.25, 0.75]
        expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims=Dimension('z Count', nodata=0)),
                              'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0)),
                              'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0))},
                             kdims=['z'])
        self.assertEqual(img, expected)

    def test_aggregate_points_categorical_zero_range(self):
        points = Points([(0.2, 0.3, 'A'), (0.4, 0.7, 'B'), (0, 0.99, 'C')], vdims='z')
        img = aggregate(points, dynamic=False,  x_range=(0, 0), y_range=(0, 1),
                        aggregator=ds.count_cat('z'), height=2)
        xs, ys = [], [0.25, 0.75]
        params = dict(bounds=(0, 0, 0, 1), xdensity=1)
        expected = NdOverlay({'A': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params),
                              'B': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params),
                              'C': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params)},
                             kdims=['z'])
        self.assertEqual(img, expected)

    def test_aggregate_curve(self):
        curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(curve, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_aggregate_curve_datetimes(self):
        dates = pd.date_range(start="2016-01-01", end="2016-01-03", freq='1D')
        curve = Curve((dates, [1, 2, 3]))
        img = aggregate(curve, width=2, height=2, dynamic=False)
        bounds = (np.datetime64('2016-01-01T00:00:00.000000'), 1.0,
                  np.datetime64('2016-01-03T00:00:00.000000'), 3.0)
        dates = [np.datetime64('2016-01-01T12:00:00.000000000'),
                 np.datetime64('2016-01-02T12:00:00.000000000')]
        expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]),
                         datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0))
        self.assertEqual(img, expected)

    def test_aggregate_curve_datetimes_dask(self):
        df = pd.DataFrame(
            data=np.arange(1000), columns=['a'],
            index=pd.date_range('2019-01-01', freq='1T', periods=1000),
        )
        ddf = dd.from_pandas(df, npartitions=4)
        curve = Curve(ddf, kdims=['index'], vdims=['a'])
        img = aggregate(curve, width=2, height=3, dynamic=False)
        bounds = (np.datetime64('2019-01-01T00:00:00.000000'), 0.0,
                  np.datetime64('2019-01-01T16:39:00.000000'), 999.0)
        dates = [np.datetime64('2019-01-01T04:09:45.000000000'),
                 np.datetime64('2019-01-01T12:29:15.000000000')]
        expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]),
                         kdims=['index', 'a'], vdims=Dimension('Count', nodata=0),
                         datatype=['xarray'], bounds=bounds)
        self.assertEqual(img, expected)

    def test_aggregate_curve_datetimes_microsecond_timebase(self):
        dates = pd.date_range(start="2016-01-01", end="2016-01-03", freq='1D')
        xstart = np.datetime64('2015-12-31T23:59:59.723518000', 'us')
        xend = np.datetime64('2016-01-03T00:00:00.276482000', 'us')
        curve = Curve((dates, [1, 2, 3]))
        img = aggregate(curve, width=2, height=2, x_range=(xstart, xend), dynamic=False)
        bounds = (np.datetime64('2015-12-31T23:59:59.723518'), 1.0,
                  np.datetime64('2016-01-03T00:00:00.276482'), 3.0)
        dates = [np.datetime64('2016-01-01T11:59:59.861759000',),
                 np.datetime64('2016-01-02T12:00:00.138241000')]
        expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]),
                         datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0))
        self.assertEqual(img, expected)

    def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self):
        dates = pd.date_range(start="2016-01-01", end="2016-01-03", freq='1D')
        xstart = np.datetime64('2015-12-31T23:59:59.723518000', 'us')
        xend = np.datetime64('2016-01-03T00:00:00.276482000', 'us')
        curve = Curve((dates, [1, 2, 3]))
        curve2 = Curve((dates, [3, 2, 1]))
        ndoverlay = NdOverlay({0: curve, 1: curve2}, 'Cat')
        imgs = aggregate(ndoverlay, aggregator=ds.count_cat('Cat'), width=2, height=2,
                         x_range=(xstart, xend), dynamic=False)
        bounds = (np.datetime64('2015-12-31T23:59:59.723518'), 1.0,
                  np.datetime64('2016-01-03T00:00:00.276482'), 3.0)
        dates = [np.datetime64('2016-01-01T11:59:59.861759000',),
                 np.datetime64('2016-01-02T12:00:00.138241000')]
        expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]),
                         datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0))
        expected2 = Image((dates, [1.5, 2.5], [[0, 1], [1, 1]]),
                         datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0))
        self.assertEqual(imgs[0], expected)
        self.assertEqual(imgs[1], expected2)

    def test_aggregate_dt_xaxis_constant_yaxis(self):
        df = pd.DataFrame({'y': np.ones(100)}, index=pd.date_range('1980-01-01', periods=100, freq='1T'))
        img = rasterize(Curve(df), dynamic=False, width=3)
        xs = np.array(['1980-01-01T00:16:30.000000', '1980-01-01T00:49:30.000000',
                       '1980-01-01T01:22:30.000000'], dtype='datetime64[us]')
        ys = np.array([])
        bounds = (np.datetime64('1980-01-01T00:00:00.000000'), 1.0,
                  np.datetime64('1980-01-01T01:39:00.000000'), 1.0)
        expected = Image((xs, ys, np.empty((0, 3))), ['index', 'y'],
                         vdims=Dimension('Count', nodata=0), xdensity=1,
                         ydensity=1, bounds=bounds)
        self.assertEqual(img, expected)

    def test_aggregate_ndoverlay(self):
        ds = Dataset([(0.2, 0.3, 0), (0.4, 0.7, 1), (0, 0.99, 2)], kdims=['x', 'y', 'z'])
        ndoverlay = ds.to(Points, ['x', 'y'], [], 'z').overlay()
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(ndoverlay, dynamic=False, x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_aggregate_path(self):
        path = Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(path, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_aggregate_contours_with_vdim(self):
        contours = Contours([[(0.2, 0.3, 1), (0.4, 0.7, 1)], [(0.4, 0.7, 2), (0.8, 0.99, 2)]], vdims='z')
        img = rasterize(contours, dynamic=False)
        self.assertEqual(img.vdims, ['z'])

    def test_aggregate_contours_without_vdim(self):
        contours = Contours([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]])
        img = rasterize(contours, dynamic=False)
        self.assertEqual(img.vdims, [Dimension('Any', nodata=0)])

    def test_aggregate_dframe_nan_path(self):
        path = Path([Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]).dframe()])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = aggregate(path, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_spikes_aggregate_count(self):
        spikes = Spikes([1, 2, 3])
        agg = rasterize(spikes, width=5, dynamic=False, expand=False)
        expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0),
                         xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 0.5))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_count_dask(self):
        spikes = Spikes([1, 2, 3], datatype=['dask'])
        agg = rasterize(spikes, width=5, dynamic=False, expand=False)
        expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0),
                         xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 0.5))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_dt_count(self):
        spikes = Spikes([dt.datetime(2016, 1, 1),  dt.datetime(2016, 1, 2), dt.datetime(2016, 1, 3)])
        agg = rasterize(spikes, width=5, dynamic=False, expand=False)
        bounds = (np.datetime64('2016-01-01T00:00:00.000000'), 0,
                  np.datetime64('2016-01-03T00:00:00.000000'), 0.5)
        expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), bounds=bounds)
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_dt_count_dask(self):
        spikes = Spikes([dt.datetime(2016, 1, 1),  dt.datetime(2016, 1, 2), dt.datetime(2016, 1, 3)],
                        datatype=['dask'])
        agg = rasterize(spikes, width=5, dynamic=False, expand=False)
        bounds = (np.datetime64('2016-01-01T00:00:00.000000'), 0,
                  np.datetime64('2016-01-03T00:00:00.000000'), 0.5)
        expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), bounds=bounds)
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_spike_length(self):
        spikes = Spikes([1, 2, 3])
        agg = rasterize(spikes, width=5, dynamic=False, expand=False, spike_length=7)
        expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0),
                         xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 7.0))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_with_height_count(self):
        spikes = Spikes([(1, 0.2), (2, 0.8), (3, 0.4)], vdims='y')
        agg = rasterize(spikes, width=5, height=5, y_range=(0, 1), dynamic=False)
        xs = [1.2, 1.6, 2.0, 2.4, 2.8]
        ys = [0.1, 0.3, 0.5, 0.7, 0.9]
        arr = np.array([
            [1, 0, 1, 0, 1],
            [1, 0, 1, 0, 1],
            [0, 0, 1, 0, 1],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_with_height_count_override(self):
        spikes = Spikes([(1, 0.2), (2, 0.8), (3, 0.4)], vdims='y')
        agg = rasterize(spikes, width=5, height=5, y_range=(0, 1),
                        spike_length=0.3, dynamic=False)
        xs = [1.2, 1.6, 2.0, 2.4, 2.8]
        ys = [0.1, 0.3, 0.5, 0.7, 0.9]
        arr = np.array([[1, 0, 1, 0, 1],
                        [1, 0, 1, 0, 1],
                        [0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0]])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_rasterize_regrid_and_spikes_overlay(self):
        img = Image(([0.5, 1.5], [0.5, 1.5], [[0, 1], [2, 3]]))
        spikes = Spikes([(0.5, 0.2), (1.5, 0.8), ], vdims='y')

        expected_regrid = Image(([0.25, 0.75, 1.25, 1.75],
                                 [0.25, 0.75, 1.25, 1.75],
                                 [[0, 0, 1, 1],
                                  [0, 0, 1, 1],
                                  [2, 2, 3, 3],
                                  [2, 2, 3, 3]]))
        spikes_arr = np.array([[0, 1, 0, 1],
                               [0, 1, 0, 1],
                               [0, 0, 0, 0],
                               [0, 0, 0, 0]])
        expected_spikes = Image(([0.25, 0.75, 1.25, 1.75],
                                 [0.25, 0.75, 1.25, 1.75], spikes_arr), vdims=Dimension('Count', nodata=0))
        overlay = img * spikes
        agg = rasterize(overlay, width=4, height=4, x_range=(0, 2), y_range=(0, 2),
                        spike_length=0.5, upsample=True, dynamic=False)
        self.assertEqual(agg.Image.I, expected_regrid)
        self.assertEqual(agg.Spikes.I, expected_spikes)


    def test_spikes_aggregate_with_height_count_dask(self):
        spikes = Spikes([(1, 0.2), (2, 0.8), (3, 0.4)], vdims='y', datatype=['dask'])
        agg = rasterize(spikes, width=5, height=5, y_range=(0, 1), dynamic=False)
        xs = [1.2, 1.6, 2.0, 2.4, 2.8]
        ys = [0.1, 0.3, 0.5, 0.7, 0.9]
        arr = np.array([
            [1, 0, 1, 0, 1],
            [1, 0, 1, 0, 1],
            [0, 0, 1, 0, 1],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_with_negative_height_count(self):
        spikes = Spikes([(1, -0.2), (2, -0.8), (3, -0.4)], vdims='y', datatype=['dask'])
        agg = rasterize(spikes, width=5, height=5, y_range=(-1, 0), dynamic=False)
        xs = [1.2, 1.6, 2.0, 2.4, 2.8]
        ys = [-0.9, -0.7, -0.5, -0.3, -0.1]
        arr = np.array([
            [0, 0, 0, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 1],
            [1, 0, 1, 0, 1]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_spikes_aggregate_with_positive_and_negative_height_count(self):
        spikes = Spikes([(1, -0.2), (2, 0.8), (3, -0.4)], vdims='y', datatype=['dask'])
        agg = rasterize(spikes, width=5, height=5, y_range=(-1, 1), dynamic=False)
        xs = [1.2, 1.6, 2.0, 2.4, 2.8]
        ys = [-0.8, -0.4, 0.0, 0.4, 0.8]
        arr = np.array([
            [0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1],
            [1, 0, 1, 0, 1],
            [0, 0, 1, 0, 0],
            [0, 0, 1, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_rectangles_aggregate_count(self):
        rects = Rectangles([(0, 0, 1, 2), (1, 1, 3, 2)])
        agg = rasterize(rects, width=4, height=4, dynamic=False)
        xs = [0.375, 1.125, 1.875, 2.625]
        ys = [0.25, 0.75, 1.25, 1.75]
        arr = np.array([
            [1, 1, 0, 0],
            [1, 1, 0, 0],
            [1, 2, 1, 1],
            [0, 0, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_rectangles_aggregate_count_cat(self):
        rects = Rectangles([(0, 0, 1, 2, 'A'), (1, 1, 3, 2, 'B')], vdims=['cat'])
        agg = rasterize(rects, width=4, height=4, aggregator=ds.count_cat('cat'),
                        dynamic=False)
        xs = [0.375, 1.125, 1.875, 2.625]
        ys = [0.25, 0.75, 1.25, 1.75]
        arr1 = np.array([
            [1, 1, 0, 0],
            [1, 1, 0, 0],
            [1, 1, 0, 0],
            [0, 0, 0, 0]
        ])
        arr2 = np.array([
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 1, 1, 1],
            [0, 0, 0, 0]
        ])
        expected1 = Image((xs, ys, arr1), vdims=Dimension('cat Count', nodata=0))
        expected2 = Image((xs, ys, arr2), vdims=Dimension('cat Count', nodata=0))
        expected = NdOverlay({'A': expected1, 'B': expected2}, kdims=['cat'])
        self.assertEqual(agg, expected)

    def test_rectangles_aggregate_sum(self):
        rects = Rectangles([(0, 0, 1, 2, 0.5), (1, 1, 3, 2, 1.5)], vdims=['value'])
        agg = rasterize(rects, width=4, height=4, aggregator='sum', dynamic=False)
        xs = [0.375, 1.125, 1.875, 2.625]
        ys = [0.25, 0.75, 1.25, 1.75]
        nan = np.nan
        arr = np.array([
            [0.5, 0.5, nan, nan],
            [0.5, 0.5, nan, nan],
            [0.5, 2. , 1.5, 1.5],
            [nan, nan, nan, nan]
        ])
        expected = Image((xs, ys, arr), vdims='value')
        self.assertEqual(agg, expected)

    def test_rectangles_aggregate_dt_count(self):
        rects = Rectangles([
            (0, dt.datetime(2016, 1, 2), 4, dt.datetime(2016, 1, 3)),
            (1, dt.datetime(2016, 1, 1), 2, dt.datetime(2016, 1, 5))
        ])
        agg = rasterize(rects, width=4, height=4, dynamic=False)
        xs = [0.5, 1.5, 2.5, 3.5]
        ys = [
            np.datetime64('2016-01-01T12:00:00'), np.datetime64('2016-01-02T12:00:00'),
            np.datetime64('2016-01-03T12:00:00'), np.datetime64('2016-01-04T12:00:00')
        ]
        arr = np.array([
            [0, 1, 1, 0],
            [1, 2, 2, 1],
            [0, 1, 1, 0],
            [0, 0, 0, 0]
        ])
        bounds = (0.0, np.datetime64('2016-01-01T00:00:00'),
                  4.0, np.datetime64('2016-01-05T00:00:00'))
        expected = Image((xs, ys, arr), bounds=bounds, vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_segments_aggregate_count(self):
        segments = Segments([(0, 1, 4, 1), (1, 0, 1, 4)])
        agg = rasterize(segments, width=4, height=4, dynamic=False)
        xs = [0.5, 1.5, 2.5, 3.5]
        ys = [0.5, 1.5, 2.5, 3.5]
        arr = np.array([
            [0, 1, 0, 0],
            [1, 2, 1, 1],
            [0, 1, 0, 0],
            [0, 1, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_segments_aggregate_sum(self, instance=False):
        segments = Segments([(0, 1, 4, 1, 2), (1, 0, 1, 4, 4)], vdims=['value'])
        if instance:
            agg = rasterize.instance(
                width=10, height=10, dynamic=False, aggregator='sum'
            )(segments, width=4, height=4)
        else:
            agg = rasterize(
                segments, width=4, height=4, dynamic=False, aggregator='sum'
            )
        xs = [0.5, 1.5, 2.5, 3.5]
        ys = [0.5, 1.5, 2.5, 3.5]
        na = np.nan
        arr = np.array([
            [na, 4, na, na],
            [2 , 6, 2 , 2 ],
            [na, 4, na, na],
            [na, 4, na, na]
        ])
        expected = Image((xs, ys, arr), vdims='value')
        self.assertEqual(agg, expected)

    def test_segments_aggregate_sum_instance(self):
        self.test_segments_aggregate_sum(instance=True)

    def test_segments_aggregate_dt_count(self):
        segments = Segments([
            (0, dt.datetime(2016, 1, 2), 4, dt.datetime(2016, 1, 2)),
            (1, dt.datetime(2016, 1, 1), 1, dt.datetime(2016, 1, 5))
        ])
        agg = rasterize(segments, width=4, height=4, dynamic=False)
        xs = [0.5, 1.5, 2.5, 3.5]
        ys = [
            np.datetime64('2016-01-01T12:00:00'), np.datetime64('2016-01-02T12:00:00'),
            np.datetime64('2016-01-03T12:00:00'), np.datetime64('2016-01-04T12:00:00')
        ]
        arr = np.array([
            [0, 1, 0, 0],
            [1, 2, 1, 1],
            [0, 1, 0, 0],
            [0, 1, 0, 0]
        ])
        bounds = (0.0, np.datetime64('2016-01-01T00:00:00'),
                  4.0, np.datetime64('2016-01-05T00:00:00'))
        expected = Image((xs, ys, arr), bounds=bounds, vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_area_aggregate_simple_count(self):
        area = Area([1, 2, 1])
        agg = rasterize(area, width=4, height=4, y_range=(0, 3), dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.375, 1.125, 1.875, 2.625]
        arr = np.array([
            [1, 1, 1, 1],
            [1, 1, 1, 1],
            [0, 1, 1, 0],
            [0, 0, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_area_aggregate_negative_count(self):
        area = Area([-1, -2, -3])
        agg = rasterize(area, width=4, height=4, y_range=(-3, 0), dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [-2.625, -1.875, -1.125, -0.375]
        arr = np.array([
            [0, 0, 0, 1],
            [0, 1, 1, 1],
            [1, 1, 1, 1],
            [1, 1, 1, 1]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_area_aggregate_crossover_count(self):
        area = Area([-1, 2, 3])
        agg = rasterize(area, width=4, height=4, y_range=(-3, 3), dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [-2.25, -0.75, 0.75, 2.25]
        arr = np.array([
            [0, 0, 0, 0],
            [1, 0, 0, 0],
            [1, 1, 1, 1],
            [0, 0, 1, 1]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_spread_aggregate_symmetric_count(self):
        spread = Spread([(0, 1, 0.8), (1, 2, 0.3), (2, 3, 0.8)])
        agg = rasterize(spread, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.65, 1.55, 2.45, 3.35]
        arr = np.array([
            [0, 0, 0, 0],
            [1, 0, 0, 0],
            [0, 1, 1, 0],
            [0, 0, 0, 1]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_spread_aggregate_assymmetric_count(self):
        spread = Spread([(0, 1, 0.4, 0.8), (1, 2, 0.8, 0.4), (2, 3, 0.5, 1)],
                        vdims=['y', 'pos', 'neg'])
        agg = rasterize(spread, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.6125, 1.4375, 2.2625, 3.0875]
        arr = np.array([
            [0, 0, 0, 0],
            [1, 0, 0, 0],
            [0, 1, 1, 0],
            [0, 0, 1, 1]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    def test_rgb_regrid_packed(self):
        coords = {'x': [1, 2], 'y': [1, 2], 'band': [0, 1, 2]}
        arr = np.array([
            [[255, 10],
             [  0, 30]],
            [[  1,  0],
             [  0,  0]],
            [[127,  0],
             [  0, 68]],
        ]).T
        da = xr.DataArray(data=arr, dims=('x', 'y', 'band'), coords=coords)
        im = RGB(da, ['x', 'y'])
        agg = rasterize(im, width=3, height=3, dynamic=False, upsample=True)
        xs = [0.8333333, 1.5, 2.166666]
        ys = [0.8333333, 1.5, 2.166666]
        arr = np.array([
            [[255, 255, 10],
             [255, 255, 10],
             [  0,   0, 30]],
            [[  1,   1,  0],
             [  1,   1,  0],
             [  0,   0,  0]],
            [[127, 127,  0],
             [127, 127,  0],
             [  0,   0, 68]],
        ]).transpose((1, 2, 0))
        expected = RGB((xs, ys, arr))
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_line_rasterize(self):
        path = Path([[(0, 0), (1, 1), (2, 0)], [(0, 0), (0, 1)]], datatype=['spatialpandas'])
        agg = rasterize(path, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.125, 0.375, 0.625, 0.875]
        arr = np.array([
            [2, 0, 0, 1],
            [1, 1, 0, 1],
            [1, 1, 1, 0],
            [1, 0, 1, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_multi_line_rasterize(self):
        path = Path([{'x': [0, 1, 2, np.nan, 0, 0], 'y': [0, 1, 0, np.nan, 0, 1]}],
                    datatype=['spatialpandas'])
        agg = rasterize(path, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.125, 0.375, 0.625, 0.875]
        arr = np.array([
            [2, 0, 0, 1],
            [1, 1, 0, 1],
            [1, 1, 1, 0],
            [1, 0, 1, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_ring_rasterize(self):
        path = Path([{'x': [0, 1, 2], 'y': [0, 1, 0], 'geom_type': 'Ring'}], datatype=['spatialpandas'])
        agg = rasterize(path, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.125, 0.375, 0.625, 0.875]
        arr = np.array([
            [1, 1, 1, 1],
            [0, 1, 0, 1],
            [0, 1, 1, 0],
            [0, 0, 1, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_polygon_rasterize(self):
        poly = Polygons([
            {'x': [0, 1, 2], 'y': [0, 1, 0],
             'holes': [[[(1.6, 0.2), (1, 0.8), (0.4, 0.2)]]]}
        ])
        agg = rasterize(poly, width=6, height=6, dynamic=False)
        xs = [0.166667, 0.5, 0.833333, 1.166667, 1.5, 1.833333]
        ys = [0.083333, 0.25, 0.416667, 0.583333, 0.75, 0.916667]
        arr = np.array([
            [1, 1, 1, 1, 1, 1],
            [0, 0, 0, 0, 0, 0],
            [0, 1, 0, 0, 1, 0],
            [0, 0, 0, 0, 0, 0],
            [0, 0, 1, 1, 0, 0],
            [0, 0, 0, 0, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_polygon_rasterize_mean_agg(self):
        poly = Polygons([
            {'x': [0, 1, 2], 'y': [0, 1, 0], 'z': 2.4},
            {'x': [0, 0, 1], 'y': [0, 1, 1], 'z': 3.6}
        ], vdims='z')
        agg = rasterize(poly, width=4, height=4, dynamic=False, aggregator='mean')
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.125, 0.375, 0.625, 0.875]
        arr = np.array([
            [ 2.4,  2.4,  2.4,    2.4],
            [ 3.6,  2.4,  2.4,    np.nan],
            [ 3.6,  2.4,  2.4,    np.nan],
            [ 3.6,  3.6,  np.nan, np.nan]])
        expected = Image((xs, ys, arr), vdims='z')
        self.assertEqual(agg, expected)

    @spatialpandas_skip
    def test_multi_poly_rasterize(self):
        poly = Polygons([{'x': [0, 1, 2, np.nan, 0, 0, 1],
                          'y': [0, 1, 0, np.nan, 0, 1, 1]}],
                        datatype=['spatialpandas'])
        agg = rasterize(poly, width=4, height=4, dynamic=False)
        xs = [0.25, 0.75, 1.25, 1.75]
        ys = [0.125, 0.375, 0.625, 0.875]
        arr = np.array([
            [1, 1, 1, 1],
            [1, 1, 1, 0],
            [1, 1, 1, 0],
            [1, 1, 0, 0]
        ])
        expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0))
        self.assertEqual(agg, expected)



class DatashaderCatAggregateTests(ComparisonTestCase):

    def setUp(self):
        if ds_version < Version('0.11.0'):
            raise SkipTest('Regridding operations require datashader>=0.11.0')

    def test_aggregate_points_categorical(self):
        points = Points([(0.2, 0.3, 'A'), (0.4, 0.7, 'B'), (0, 0.99, 'C')], vdims='z')
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2, aggregator=ds.by('z', ds.count()))
        xs, ys = [0.25, 0.75], [0.25, 0.75]
        expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims=Dimension('z Count', nodata=0)),
                              'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0)),
                              'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0))},
                             kdims=['z'])
        self.assertEqual(img, expected)


    def test_aggregate_points_categorical_mean(self):
        points = Points([(0.2, 0.3, 'A', 0.1), (0.4, 0.7, 'B', 0.2), (0, 0.99, 'C', 0.3)], vdims=['cat', 'z'])
        img = aggregate(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2, aggregator=ds.by('cat', ds.mean('z')))
        xs, ys = [0.25, 0.75], [0.25, 0.75]
        expected = NdOverlay({'A': Image((xs, ys, [[0.1, nan], [nan, nan]]), vdims='z'),
                              'B': Image((xs, ys, [[nan, nan], [0.2, nan]]), vdims='z'),
                              'C': Image((xs, ys, [[nan, nan], [0.3, nan]]), vdims='z')},
                             kdims=['cat'])
        self.assertEqual(img, expected)



class DatashaderShadeTests(ComparisonTestCase):

    def test_shade_categorical_images_xarray(self):
        xs, ys = [0.25, 0.75], [0.25, 0.75]
        data = NdOverlay({'A': Image((xs, ys, np.array([[1, 0], [0, 0]], dtype='u4')),
                                     datatype=['xarray'], vdims=Dimension('z Count', nodata=0)),
                          'B': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')),
                                     datatype=['xarray'], vdims=Dimension('z Count', nodata=0)),
                          'C': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')),
                                     datatype=['xarray'], vdims=Dimension('z Count', nodata=0))},
                         kdims=['z'])
        shaded = shade(data, rescale_discrete_levels=False)
        r = [[228, 120], [66, 120]]
        g = [[26, 109], [150, 109]]
        b = [[28, 95], [129, 95]]
        a = [[40, 0], [255, 0]]
        expected = RGB((xs, ys, r, g, b, a), datatype=['grid'],
                       vdims=RGB.vdims+[Dimension('A', range=(0, 1))])
        self.assertEqual(shaded, expected)

    def test_shade_categorical_images_grid(self):
        xs, ys = [0.25, 0.75], [0.25, 0.75]
        data = NdOverlay({'A': Image((xs, ys, np.array([[1, 0], [0, 0]], dtype='u4')),
                                     datatype=['grid'], vdims=Dimension('z Count', nodata=0)),
                          'B': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')),
                                     datatype=['grid'], vdims=Dimension('z Count', nodata=0)),
                          'C': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')),
                                     datatype=['grid'], vdims=Dimension('z Count', nodata=0))},
                         kdims=['z'])
        shaded = shade(data, rescale_discrete_levels=False)
        r = [[228, 120], [66, 120]]
        g = [[26, 109], [150, 109]]
        b = [[28, 95], [129, 95]]
        a = [[40, 0], [255, 0]]
        expected = RGB((xs, ys, r, g, b, a), datatype=['grid'],
                       vdims=RGB.vdims+[Dimension('A', range=(0, 1))])
        self.assertEqual(shaded, expected)

    def test_shade_dt_xaxis_constant_yaxis(self):
        df = pd.DataFrame({'y': np.ones(100)}, index=pd.date_range('1980-01-01', periods=100, freq='1T'))
        rgb = shade(rasterize(Curve(df), dynamic=False, width=3))
        xs = np.array(['1980-01-01T00:16:30.000000', '1980-01-01T00:49:30.000000',
                       '1980-01-01T01:22:30.000000'], dtype='datetime64[us]')
        ys = np.array([])
        bounds = (np.datetime64('1980-01-01T00:00:00.000000'), 1.0,
                  np.datetime64('1980-01-01T01:39:00.000000'), 1.0)
        expected = RGB((xs, ys, np.empty((0, 3, 4))), ['index', 'y'],
                       xdensity=1, ydensity=1, bounds=bounds)
        self.assertEqual(rgb, expected)



class DatashaderRegridTests(ComparisonTestCase):
    """
    Tests for datashader aggregation
    """

    def setUp(self):
        if ds_version <= Version('0.5.0'):
            raise SkipTest('Regridding operations require datashader>=0.6.0')

    def test_regrid_mean(self):
        img = Image((range(10), range(5), np.arange(10) * np.arange(5)[np.newaxis].T))
        regridded = regrid(img, width=2, height=2, dynamic=False)
        expected = Image(([2., 7.], [0.75, 3.25], [[1, 5], [6, 22]]))
        self.assertEqual(regridded, expected)

    def test_regrid_mean_xarray_transposed(self):
        img = Image((range(10), range(5), np.arange(10) * np.arange(5)[np.newaxis].T),
                    datatype=['xarray'])
        img.data = img.data.transpose()
        regridded = regrid(img, width=2, height=2, dynamic=False)
        expected = Image(([2., 7.], [0.75, 3.25], [[1, 5], [6, 22]]))
        self.assertEqual(regridded, expected)

    def test_regrid_rgb_mean(self):
        arr = (np.arange(10) * np.arange(5)[np.newaxis].T).astype('f')
        rgb = RGB((range(10), range(5), arr, arr*2, arr*2))
        regridded = regrid(rgb, width=2, height=2, dynamic=False)
        new_arr = np.array([[1.6, 5.6], [6.4, 22.4]])
        expected = RGB(([2., 7.], [0.75, 3.25], new_arr, new_arr*2, new_arr*2), datatype=['xarray'])
        self.assertEqual(regridded, expected)

    def test_regrid_max(self):
        img = Image((range(10), range(5), np.arange(10) * np.arange(5)[np.newaxis].T))
        regridded = regrid(img, aggregator='max', width=2, height=2, dynamic=False)
        expected = Image(([2., 7.], [0.75, 3.25], [[8, 18], [16, 36]]))
        self.assertEqual(regridded, expected)

    def test_regrid_upsampling(self):
        img = Image(([0.5, 1.5], [0.5, 1.5], [[0, 1], [2, 3]]))
        regridded = regrid(img, width=4, height=4, upsample=True, dynamic=False)
        expected = Image(([0.25, 0.75, 1.25, 1.75], [0.25, 0.75, 1.25, 1.75],
                          [[0, 0, 1, 1],
                           [0, 0, 1, 1],
                           [2, 2, 3, 3],
                           [2, 2, 3, 3]]))
        self.assertEqual(regridded, expected)

    def test_regrid_upsampling_linear(self):
        img = Image(([0.5, 1.5], [0.5, 1.5], [[0, 1], [2, 3]]))
        regridded = regrid(img, width=4, height=4, upsample=True, interpolation='linear', dynamic=False)
        expected = Image(([0.25, 0.75, 1.25, 1.75], [0.25, 0.75, 1.25, 1.75],
                          [[0, 0, 0, 1],
                           [0, 1, 1, 1],
                           [1, 1, 2, 2],
                           [2, 2, 2, 3]]))
        self.assertEqual(regridded, expected)

    def test_regrid_disabled_upsampling(self):
        img = Image(([0.5, 1.5], [0.5, 1.5], [[0, 1], [2, 3]]))
        regridded = regrid(img, width=3, height=3, dynamic=False, upsample=False)
        self.assertEqual(regridded, img)

    def test_regrid_disabled_expand(self):
        img = Image(([0.5, 1.5], [0.5, 1.5], [[0., 1.], [2., 3.]]))
        regridded = regrid(img, width=2, height=2, x_range=(-2, 4), y_range=(-2, 4), expand=False,
                           dynamic=False)
        self.assertEqual(regridded, img)

    def test_regrid_zero_range(self):
        ls = np.linspace(0, 10, 200)
        xx, yy = np.meshgrid(ls, ls)
        img = Image(np.sin(xx)*np.cos(yy), bounds=(0, 0, 1, 1))
        regridded = regrid(img, x_range=(-1, -0.5), y_range=(-1, -0.5), dynamic=False)
        expected = Image(np.zeros((0, 0)), bounds=(0, 0, 0, 0), xdensity=1, ydensity=1)
        self.assertEqual(regridded, expected)



class DatashaderRasterizeTests(ComparisonTestCase):
    """
    Tests for datashader aggregation
    """

    def setUp(self):
        if ds_version <= Version('0.6.4'):
            raise SkipTest('Regridding operations require datashader>=0.7.0')

        self.simplexes = [(0, 1, 2), (3, 2, 1)]
        self.vertices = [(0., 0.), (0., 1.), (1., 0), (1, 1)]
        self.simplexes_vdim = [(0, 1, 2, 0.5), (3, 2, 1, 1.5)]
        self.vertices_vdim = [(0., 0., 1), (0., 1., 2), (1., 0, 3), (1, 1, 4)]

    def test_rasterize_trimesh_no_vdims(self):
        trimesh = TriMesh((self.simplexes, self.vertices))
        img = rasterize(trimesh, width=3, height=3, dynamic=False)
        image = Image(np.array([[True, True, True], [True, True, True], [True, True, True]]),
                      bounds=(0, 0, 1, 1), vdims=Dimension('Any', nodata=0))
        self.assertEqual(img, image)

    def test_rasterize_trimesh_no_vdims_zero_range(self):
        trimesh = TriMesh((self.simplexes, self.vertices))
        img = rasterize(trimesh, height=2, x_range=(0, 0), dynamic=False)
        image = Image(([], [0.25, 0.75], np.zeros((2, 0))),
                      bounds=(0, 0, 0, 1), xdensity=1, vdims=Dimension('Any', nodata=0))
        self.assertEqual(img, image)

    def test_rasterize_trimesh_with_vdims_as_wireframe(self):
        trimesh = TriMesh((self.simplexes_vdim, self.vertices), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, aggregator='any', interpolation=None, dynamic=False)
        array = np.array([
            [True, True, True],
            [True, True, True],
            [True, True, True]
        ])
        image = Image(array, bounds=(0, 0, 1, 1), vdims=Dimension('Any', nodata=0))
        self.assertEqual(img, image)

    def test_rasterize_trimesh(self):
        trimesh = TriMesh((self.simplexes_vdim, self.vertices), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, dynamic=False)
        array = np.array([
            [0.5, 1.5, 1.5],
            [0.5, 0.5, 1.5],
            [0.5, 0.5, 0.5]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_pandas_trimesh_implicit_nodes(self):
        simplex_df = pd.DataFrame(self.simplexes, columns=['v0', 'v1', 'v2'])
        vertex_df = pd.DataFrame(self.vertices_vdim, columns=['x', 'y', 'z'])

        trimesh = TriMesh((simplex_df, vertex_df))
        img = rasterize(trimesh, width=3, height=3, dynamic=False)

        array = np.array([
            [2.166667, 2.833333, 3.5     ],
            [1.833333, 2.5,      3.166667],
            [1.5,      2.166667, 2.833333]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_dask_trimesh_implicit_nodes(self):
        simplex_df = pd.DataFrame(self.simplexes, columns=['v0', 'v1', 'v2'])
        vertex_df = pd.DataFrame(self.vertices_vdim, columns=['x', 'y', 'z'])

        simplex_ddf = dd.from_pandas(simplex_df, npartitions=2)
        vertex_ddf = dd.from_pandas(vertex_df, npartitions=2)

        trimesh = TriMesh((simplex_ddf, vertex_ddf))

        ri = rasterize.instance()
        img = ri(trimesh, width=3, height=3, dynamic=False, precompute=True)

        cache = ri._precomputed
        self.assertEqual(len(cache), 1)
        self.assertIn(trimesh._plot_id, cache)
        self.assertIsInstance(cache[trimesh._plot_id]['mesh'], dd.DataFrame)

        array = np.array([
            [2.166667, 2.833333, 3.5     ],
            [1.833333, 2.5,      3.166667],
            [1.5,      2.166667, 2.833333]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_dask_trimesh(self):
        simplex_df = pd.DataFrame(self.simplexes_vdim, columns=['v0', 'v1', 'v2', 'z'])
        vertex_df = pd.DataFrame(self.vertices, columns=['x', 'y'])

        simplex_ddf = dd.from_pandas(simplex_df, npartitions=2)
        vertex_ddf = dd.from_pandas(vertex_df, npartitions=2)

        tri_nodes = Nodes(vertex_ddf, ['x', 'y', 'index'])
        trimesh = TriMesh((simplex_ddf, tri_nodes), vdims=['z'])

        ri = rasterize.instance()
        img = ri(trimesh, width=3, height=3, dynamic=False, precompute=True)

        cache = ri._precomputed
        self.assertEqual(len(cache), 1)
        self.assertIn(trimesh._plot_id, cache)
        self.assertIsInstance(cache[trimesh._plot_id]['mesh'], dd.DataFrame)

        array = np.array([
            [0.5, 1.5, 1.5],
            [0.5, 0.5, 1.5],
            [0.5, 0.5, 0.5]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_dask_trimesh_with_node_vdims(self):
        simplex_df = pd.DataFrame(self.simplexes, columns=['v0', 'v1', 'v2'])
        vertex_df = pd.DataFrame(self.vertices_vdim, columns=['x', 'y', 'z'])

        simplex_ddf = dd.from_pandas(simplex_df, npartitions=2)
        vertex_ddf = dd.from_pandas(vertex_df, npartitions=2)

        tri_nodes = Nodes(vertex_ddf, ['x', 'y', 'index'], ['z'])
        trimesh = TriMesh((simplex_ddf, tri_nodes))

        ri = rasterize.instance()
        img = ri(trimesh, width=3, height=3, dynamic=False, precompute=True)

        cache = ri._precomputed
        self.assertEqual(len(cache), 1)
        self.assertIn(trimesh._plot_id, cache)
        self.assertIsInstance(cache[trimesh._plot_id]['mesh'], dd.DataFrame)

        array = np.array([
            [2.166667, 2.833333, 3.5     ],
            [1.833333, 2.5,      3.166667],
            [1.5,      2.166667, 2.833333]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_trimesh_node_vdim_precedence(self):
        nodes = Points(self.vertices_vdim, vdims=['node_z'])
        trimesh = TriMesh((self.simplexes_vdim, nodes), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, dynamic=False)

        array = np.array([
            [2.166667, 2.833333, 3.5     ],
            [1.833333, 2.5,      3.166667],
            [1.5,      2.166667, 2.833333]
        ])
        image = Image(array, bounds=(0, 0, 1, 1), vdims='node_z')
        self.assertEqual(img, image)

    def test_rasterize_trimesh_node_explicit_vdim(self):
        nodes = Points(self.vertices_vdim, vdims=['node_z'])
        trimesh = TriMesh((self.simplexes_vdim, nodes), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, dynamic=False, aggregator=ds.mean('z'))

        array = np.array([
            [0.5, 1.5, 1.5],
            [0.5, 0.5, 1.5],
            [0.5, 0.5, 0.5]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_trimesh_zero_range(self):
        trimesh = TriMesh((self.simplexes_vdim, self.vertices), vdims=['z'])
        img = rasterize(trimesh, x_range=(0, 0), height=2, dynamic=False)
        image = Image(([], [0.25, 0.75], np.zeros((2, 0))),
                      bounds=(0, 0, 0, 1), xdensity=1)
        self.assertEqual(img, image)

    def test_rasterize_trimesh_vertex_vdims(self):
        simplices = [(0, 1, 2), (3, 2, 1)]
        vertices = [(0., 0., 1), (0., 1., 2), (1., 0., 3), (1., 1., 4)]
        trimesh = TriMesh((simplices, Points(vertices, vdims='z')))
        img = rasterize(trimesh, width=3, height=3, dynamic=False)

        array = np.array([
            [2.166667, 2.833333, 3.5     ],
            [1.833333, 2.5,      3.166667],
            [1.5,      2.166667, 2.833333]
        ])
        image = Image(array, bounds=(0, 0, 1, 1), vdims='z')
        self.assertEqual(img, image)

    def test_rasterize_trimesh_ds_aggregator(self):
        trimesh = TriMesh((self.simplexes_vdim, self.vertices), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, dynamic=False, aggregator=ds.mean('z'))
        array = np.array([
            [0.5, 1.5, 1.5],
            [0.5, 0.5, 1.5],
            [0.5, 0.5, 0.5]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_trimesh_string_aggregator(self):
        trimesh = TriMesh((self.simplexes_vdim, self.vertices), vdims=['z'])
        img = rasterize(trimesh, width=3, height=3, dynamic=False, aggregator='mean')
        array = np.array([
            [0.5, 1.5, 1.5],
            [0.5, 0.5, 1.5],
            [0.5, 0.5, 0.5]
        ])
        image = Image(array, bounds=(0, 0, 1, 1))
        self.assertEqual(img, image)

    def test_rasterize_quadmesh(self):
        qmesh = QuadMesh(([0, 1], [0, 1], np.array([[0, 1], [2, 3]])))
        img = rasterize(qmesh, width=3, height=3, dynamic=False, aggregator=ds.mean('z'))
        image = Image(np.array([[2, 3, 3], [2, 3, 3], [0, 1, 1]]),
                      bounds=(-.5, -.5, 1.5, 1.5))
        self.assertEqual(img, image)

    def test_rasterize_quadmesh_string_aggregator(self):
        qmesh = QuadMesh(([0, 1], [0, 1], np.array([[0, 1], [2, 3]])))
        img = rasterize(qmesh, width=3, height=3, dynamic=False, aggregator='mean')
        image = Image(np.array([[2, 3, 3], [2, 3, 3], [0, 1, 1]]),
                      bounds=(-.5, -.5, 1.5, 1.5))
        self.assertEqual(img, image)

    def test_rasterize_points(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
        img = rasterize(points, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        self.assertEqual(img, expected)

    def test_rasterize_curve(self):
        curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = rasterize(curve, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_rasterize_ndoverlay(self):
        ds = Dataset([(0.2, 0.3, 0), (0.4, 0.7, 1), (0, 0.99, 2)], kdims=['x', 'y', 'z'])
        ndoverlay = ds.to(Points, ['x', 'y'], [], 'z').overlay()
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = rasterize(ndoverlay, dynamic=False, x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_rasterize_path(self):
        path = Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]])
        expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]),
                         vdims=[Dimension('Count', nodata=0)])
        img = rasterize(path, dynamic=False,  x_range=(0, 1), y_range=(0, 1),
                        width=2, height=2)
        self.assertEqual(img, expected)

    def test_rasterize_image(self):
        img = Image((range(10), range(5), np.arange(10) * np.arange(5)[np.newaxis].T))
        regridded = regrid(img, width=2, height=2, dynamic=False)
        expected = Image(([2., 7.], [0.75, 3.25], [[1, 5], [6, 22]]))
        self.assertEqual(regridded, expected)

    def test_rasterize_image_string_aggregator(self):
        img = Image((range(10), range(5), np.arange(10) * np.arange(5)[np.newaxis].T))
        regridded = regrid(img, width=2, height=2, dynamic=False, aggregator='mean')
        expected = Image(([2., 7.], [0.75, 3.25], [[1, 5], [6, 22]]))
        self.assertEqual(regridded, expected)


class DatashaderSpreadTests(ComparisonTestCase):

    def test_spread_rgb_1px(self):
        arr = np.array([[[0, 0, 0], [0, 1, 1], [0, 1, 1]],
                        [[0, 0, 0], [0, 0, 0], [0, 0, 0]],
                        [[0, 0, 0], [0, 0, 0], [0, 0, 0]]], dtype=np.uint8).T*255
        spreaded = spread(RGB(arr))
        arr = np.array([[[0, 0, 1], [0, 0, 1], [0, 0, 1]],
                        [[0, 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]]], dtype=np.uint8).T*255
        self.assertEqual(spreaded, RGB(arr))

    def test_spread_img_1px(self):
        if ds_version < Version('0.12.0'):
            raise SkipTest('Datashader does not support DataArray yet')
        arr = np.array([[0, 0, 0], [0, 0, 0], [1, 1, 1]]).T
        spreaded = spread(Image(arr))
        arr = np.array([[0, 0, 0], [2, 3, 2], [2, 3, 2]]).T
        self.assertEqual(spreaded, Image(arr))


class DatashaderStackTests(ComparisonTestCase):

    def setUp(self):
        self.rgb1_arr = np.array([[[0, 1], [1, 0]],
                                  [[1, 0], [0, 1]],
                                  [[0, 0], [0, 0]]], dtype=np.uint8).T*255
        self.rgb2_arr = np.array([[[0, 0], [0, 0]],
                                  [[0, 0], [0, 0]],
                                  [[1, 0], [0, 1]]], dtype=np.uint8).T*255
        self.rgb1 = RGB(self.rgb1_arr)
        self.rgb2 = RGB(self.rgb2_arr)


    def test_stack_add_compositor(self):
        combined = stack(self.rgb1*self.rgb2, compositor='add')
        arr = np.array([[[0, 255, 255], [255,0, 0]], [[255, 0, 0], [0, 255, 255]]], dtype=np.uint8)
        expected = RGB(arr)
        self.assertEqual(combined, expected)

    def test_stack_over_compositor(self):
        combined = stack(self.rgb1*self.rgb2, compositor='over')
        self.assertEqual(combined, self.rgb2)

    def test_stack_over_compositor_reverse(self):
        combined = stack(self.rgb2*self.rgb1, compositor='over')
        self.assertEqual(combined, self.rgb1)

    def test_stack_saturate_compositor(self):
        combined = stack(self.rgb1*self.rgb2, compositor='saturate')
        self.assertEqual(combined, self.rgb1)

    def test_stack_saturate_compositor_reverse(self):
        combined = stack(self.rgb2*self.rgb1, compositor='saturate')
        self.assertEqual(combined, self.rgb2)


class GraphBundlingTests(ComparisonTestCase):

    def setUp(self):
        if ds_version <= Version('0.7.0'):
            raise SkipTest('Regridding operations require datashader>=0.7.0')
        self.source = np.arange(8)
        self.target = np.zeros(8)
        self.graph = Graph(((self.source, self.target),))

    def test_directly_connect_paths(self):
        direct = directly_connect_edges(self.graph)._split_edgepaths
        self.assertEqual(direct, self.graph.edgepaths)

class InspectorTests(ComparisonTestCase):
    """
    Tests for inspector operations
    """
    def setUp(self):
        points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)])
        self.pntsimg = rasterize(points, dynamic=False,
                             x_range=(0, 1), y_range=(0, 1), width=4, height=4)
        if spatialpandas is None:
            return

        xs1, xs2, ys1, ys2 = [1, 2, 3], [6, 7, 3], [2, 0, 7], [7, 5, 2]
        holes = [ [[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]],]
        polydata = [{'x': xs1, 'y': ys1, 'holes': holes, 'z': 1},
                    {'x': xs2, 'y': ys2, 'holes': [[]], 'z': 2}]
        self.polysrgb = datashade(Polygons(polydata, vdims=['z'],
                                           datatype=['spatialpandas']),
                                  x_range=(0, 7), y_range=(0, 7), dynamic=False)

    def tearDown(self):
        Tap.x, Tap.y = None, None


    def test_inspect_points_or_polygons(self):
        if spatialpandas is None:
            raise SkipTest('Polygon inspect tests require spatialpandas')
        polys = inspect(self.polysrgb,
                        max_indicators=3, dynamic=False, pixels=1, x=6, y=5)
        self.assertEqual(polys, Polygons([{'x': [6, 3, 7], 'y': [7, 2, 5], 'z': 2}], vdims='z'))
        points = inspect(self.pntsimg, max_indicators=3, dynamic=False, pixels=1, x=-0.1, y=-0.1)
        self.assertEqual(points.dimension_values('x'), np.array([]))
        self.assertEqual(points.dimension_values('y'), np.array([]))

    def test_points_inspection_1px_mask(self):
        points = inspect_points(self.pntsimg, max_indicators=3, dynamic=False, pixels=1, x=-0.1, y=-0.1)
        self.assertEqual(points.dimension_values('x'), np.array([]))
        self.assertEqual(points.dimension_values('y'), np.array([]))

    def test_points_inspection_2px_mask(self):
        points = inspect_points(self.pntsimg, max_indicators=3, dynamic=False, pixels=2, x=-0.1, y=-0.1)
        self.assertEqual(points.dimension_values('x'), np.array([0.2]))
        self.assertEqual(points.dimension_values('y'), np.array([0.3]))

    def test_points_inspection_4px_mask(self):
        points = inspect_points(self.pntsimg, max_indicators=3, dynamic=False, pixels=4, x=-0.1, y=-0.1)
        self.assertEqual(points.dimension_values('x'), np.array([0.2, 0.4]))
        self.assertEqual(points.dimension_values('y'), np.array([0.3, 0.7]))

    def test_points_inspection_5px_mask(self):
        points = inspect_points(self.pntsimg, max_indicators=3, dynamic=False, pixels=5, x=-0.1, y=-0.1)
        self.assertEqual(points.dimension_values('x'), np.array([0.2, 0.4, 0]))
        self.assertEqual(points.dimension_values('y'), np.array([0.3, 0.7, 0.99]))

    def test_inspection_5px_mask_points_df(self):
        inspector = inspect.instance(max_indicators=3, dynamic=False, pixels=5,
                                     x=-0.1, y=-0.1)
        inspector(self.pntsimg)
        self.assertEqual(list(inspector.hits['x']),[0.2,0.4,0.0])
        self.assertEqual(list(inspector.hits['y']),[0.3,0.7,0.99])

    def test_points_inspection_dict_streams(self):
        Tap.x, Tap.y = 0.4, 0.7
        points = inspect_points(self.pntsimg, max_indicators=3, dynamic=True,
                                pixels=1, streams=dict(x=Tap.param.x, y=Tap.param.y))
        self.assertEqual(len(points.streams), 1)
        self.assertEqual(isinstance(points.streams[0], Tap), True)
        self.assertEqual(points.streams[0].x, 0.4)
        self.assertEqual(points.streams[0].y, 0.7)

    def test_points_inspection_dict_streams_instance(self):
        Tap.x, Tap.y = 0.2, 0.3
        inspector = inspect_points.instance(max_indicators=3, dynamic=True, pixels=1,
                                            streams=dict(x=Tap.param.x, y=Tap.param.y))
        points = inspector(self.pntsimg)
        self.assertEqual(len(points.streams), 1)
        self.assertEqual(isinstance(points.streams[0], Tap), True)
        self.assertEqual(points.streams[0].x, 0.2)
        self.assertEqual(points.streams[0].y, 0.3)

    def test_polys_inspection_1px_mask_hit(self):
        if spatialpandas is None:
            raise SkipTest('Polygon inspect tests require spatialpandas')
        polys = inspect_polygons(self.polysrgb,
                                 max_indicators=3, dynamic=False, pixels=1, x=6, y=5)
        self.assertEqual(polys, Polygons([{'x': [6, 3, 7], 'y': [7, 2, 5], 'z': 2}],
                                         vdims='z'))


    def test_inspection_1px_mask_poly_df(self):
        if spatialpandas is None:
            raise SkipTest('Polygon inspect tests require spatialpandas')
        inspector = inspect.instance(max_indicators=3, dynamic=False, pixels=1, x=6, y=5)
        inspector(self.polysrgb)
        self.assertEqual(len(inspector.hits), 1)
        data = [[6.0, 7.0, 3.0, 2.0, 7.0, 5.0, 6.0, 7.0]]
        self.assertEqual(inspector.hits.iloc[0].geometry,
                         spatialpandas.geometry.polygon.Polygon(data))


    def test_polys_inspection_1px_mask_miss(self):
        if spatialpandas is None:
            raise SkipTest('Polygon inspect tests require spatialpandas')
        polys = inspect_polygons(self.polysrgb,
                                 max_indicators=3, dynamic=False, pixels=1, x=0, y=0)
        self.assertEqual(polys, Polygons([], vdims='z'))


@pytest.mark.parametrize("dtype", [np.uint8, np.uint16, np.uint32])
def test_uint_dtype(dtype):
    df = pd.DataFrame(np.arange(2, dtype=dtype), columns=["A"])
    curve = Curve(df)
    img = rasterize(curve, dynamic=False, height=10, width=10)
    assert (np.asarray(img.data["Count"]) == np.eye(10)).all()


def test_uint64_dtype():
    df = pd.DataFrame(np.arange(2, dtype=np.uint64), columns=["A"])
    curve = Curve(df)
    with pytest.raises(TypeError, match="Dtype of uint64 for column A is not supported."):
        rasterize(curve, dynamic=False, height=10, width=10)
