content: from __future__ import annotations
  
  from numbers import Number
  from math import log10
  import warnings
  import contextlib
  
  import numpy as np
  import pandas as pd
  from packaging.version import Version
  from xarray import DataArray, Dataset
  
  from .utils import Dispatcher, ngjit, calc_res, calc_bbox, orient_array, \
      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
  from . import reductions as rd
  
  try:
      import dask.dataframe as dd
  except ImportError:
      dd = None
  
  try:
      import dask.array as da
  except ImportError:
      da = None
  
  try:
      import cudf
  except Exception:
      cudf = None
  
  try:
      import dask_cudf
  except Exception:
      dask_cudf = None
  
  try:
      import spatialpandas
  except Exception:
      spatialpandas = None
  
  class Axis:
      """Interface for implementing axis transformations.
  
      Instances hold implementations of transformations to and from axis space.
      The default implementation is equivalent to:
  
      >>> def forward_transform(data_x):
      ...     scale * mapper(data_x) + t
      >>> def inverse_transform(axis_x):
      ...     inverse_mapper((axis_x - t)/s)
  
      Where ``mapper`` and ``inverse_mapper`` are elementwise functions mapping
      to and from axis-space respectively, and ``scale`` and ``transform`` are
      parameters describing a linear scale and translate transformation, computed
      by the ``compute_scale_and_translate`` method.
      """
  
      def compute_scale_and_translate(self, range, n):
          """Compute the scale and translate parameters for a linear transformation
          ``output = s * input + t``, mapping from data space to axis space.
  
          Parameters
          ----------
          range : tuple
              A tuple representing the range ``[min, max]`` along the axis, in
              data space. Both min and max are inclusive.
          n : int
              The number of bins along the axis.
  
          Returns
          -------
          s, t : floats
          """
          start, end = map(self.mapper, range)
          s = n/(end - start)
          t = -start * s
          return s, t
  
      def compute_index(self, st, n):
          """Compute a 1D array representing the axis index.
  
          Parameters
          ----------
          st : tuple
              A tuple of ``(scale, translate)`` parameters.
          n : int
              The number of bins along the dimension.
  
          Returns
          -------
          index : ndarray
          """
          px = np.arange(n)+0.5
          s, t = st
          return self.inverse_mapper((px - t)/s)
  
      def mapper(val):
          """A mapping from data space to axis space"""
          raise NotImplementedError
  
      def inverse_mapper(val):
          """A mapping from axis space to data space"""
          raise NotImplementedError
  
      def validate(self, range):
          """Given a range (low,high), raise an error if the range is invalid for this axis"""
          pass
  
  
  class LinearAxis(Axis):
      """A linear Axis"""
      @staticmethod
      @ngjit
      def mapper(val):
          return val
  
      @staticmethod
      @ngjit
      def inverse_mapper(val):
          return val
  
  
  class LogAxis(Axis):
      """A base-10 logarithmic Axis"""
      @staticmethod
      @ngjit
      def mapper(val):
          return log10(float(val))
  
      @staticmethod
      @ngjit
      def inverse_mapper(val):
          y = 10 # temporary workaround for https://github.com/numba/numba/issues/3135 (numba 0.39.0)
          return y**val
  
      def validate(self, range):
          if range is None:
              # Nothing to check if no range
              return
          if range[0] <= 0 or range[1] <= 0:
              raise ValueError('Range values must be >0 for logarithmic axes')
  
  
  _axis_lookup = {'linear': LinearAxis(), 'log': LogAxis()}
  
  
  def validate_xy_or_geometry(glyph, x, y, geometry):
      if (geometry is None and (x is None or y is None) or
              geometry is not None and (x is not None or y is not None)):
          raise ValueError(f"""
  {glyph} coordinates may be specified by providing both the x and y arguments, or by
  providing the geometry argument. Received:
      x: {x!r}
      y: {y!r}
      geometry: {geometry!r}
  """)
  
  
  class Canvas:
      """An abstract canvas representing the space in which to bin.
  
      Parameters
      ----------
      plot_width, plot_height : int, optional
          Width and height of the output aggregate in pixels.
      x_range, y_range : tuple, optional
          A tuple representing the bounds inclusive space ``[min, max]`` along
          the axis.
      x_axis_type, y_axis_type : str, optional
          The type of the axis. Valid options are ``'linear'`` [default], and
          ``'log'``.
      """
      def __init__(self, plot_width=600, plot_height=600,
                   x_range=None, y_range=None,
                   x_axis_type='linear', y_axis_type='linear'):
          self.plot_width = plot_width
          self.plot_height = plot_height
          self.x_range = None if x_range is None else tuple(x_range)
          self.y_range = None if y_range is None else tuple(y_range)
          self.x_axis = _axis_lookup[x_axis_type]
          self.y_axis = _axis_lookup[y_axis_type]
  
      def points(self, source, x=None, y=None, agg=None, geometry=None):
          """Compute a reduction by pixel, mapping data to pixels as points.
  
          Parameters
          ----------
          source : pandas.DataFrame, dask.DataFrame, or xarray.DataArray/Dataset
              The input datasource.
          x, y : str
              Column names for the x and y coordinates of each point. If provided,
              the geometry argument may not also be provided.
          agg : Reduction, optional
              Reduction to compute. Default is ``count()``.
          geometry: str
              Column name of a PointsArray of the coordinates of each point. If provided,
              the x and y arguments may not also be provided.
          """
          from .glyphs import Point, MultiPointGeometry
          from .reductions import count as count_rdn
  
          validate_xy_or_geometry('Point', x, y, geometry)
  
          if agg is None:
              agg = count_rdn()
  
          if geometry is None:
              glyph = Point(x, y)
          else:
              if spatialpandas and isinstance(source, spatialpandas.dask.DaskGeoDataFrame):
                  # Downselect partitions to those that may contain points in viewport
                  x_range = self.x_range if self.x_range is not None else (None, None)
                  y_range = self.y_range if self.y_range is not None else (None, None)
                  source = source.cx_partitions[slice(*x_range), slice(*y_range)]
                  glyph = MultiPointGeometry(geometry)
              elif spatialpandas and isinstance(source, spatialpandas.GeoDataFrame):
                  glyph = MultiPointGeometry(geometry)
              elif (geopandas_source := self._source_from_geopandas(source)) is not None:
                  source = geopandas_source
                  from datashader.glyphs.points import MultiPointGeoPandas
                  glyph = MultiPointGeoPandas(geometry)
              else:
                  raise ValueError(
                      "source must be an instance of spatialpandas.GeoDataFrame, "
                      "spatialpandas.dask.DaskGeoDataFrame, geopandas.GeoDataFrame, or "
                      f"dask_geopandas.GeoDataFrame. Received objects of type {type(source)}")
  
          return bypixel(source, self, glyph, agg)
  
      def line(self, source, x=None, y=None, agg=None, axis=0, geometry=None,
               line_width=0, antialias=False):
          """Compute a reduction by pixel, mapping data to pixels as one or
          more lines.
  
          For aggregates that take in extra fields, the interpolated bins will
          receive the fields from the previous point. In pseudocode:
  
          >>> for i in range(len(rows) - 1):    # doctest: +SKIP
          ...     row0 = rows[i]
          ...     row1 = rows[i + 1]
          ...     for xi, yi in interpolate(row0.x, row0.y, row1.x, row1.y):
          ...         add_to_aggregate(xi, yi, row0)
  
          Parameters
          ----------
          source : pandas.DataFrame, dask.DataFrame, or xarray.DataArray/Dataset
              The input datasource.
          x, y : str or number or list or tuple or np.ndarray
              Specification of the x and y coordinates of each vertex
              * str or number: Column labels in source
              * list or tuple: List or tuple of column labels in source
              * np.ndarray: When axis=1, a literal array of the
                coordinates to be used for every row
          agg : Reduction, optional
              Reduction to compute. Default is ``any()``.
          axis : 0 or 1, default 0
              Axis in source to draw lines along
              * 0: Draw lines using data from the specified columns across
                   all rows in source
              * 1: Draw one line per row in source using data from the
                   specified columns
          geometry : str
              Column name of a LinesArray of the coordinates of each line. If provided,
              the x and y arguments may not also be provided.
          line_width : number, optional
              Width of the line to draw, in pixels. If zero, the
              default, lines are drawn using a simple algorithm with a
              blocky single-pixel width based on whether the line passes
              through each pixel or does not. If greater than one, lines
              are drawn with the specified width using a slower and
              more complex antialiasing algorithm with fractional values
              along each edge, so that lines have a more uniform visual
              appearance across all angles. Line widths between 0 and 1
              effectively use a line_width of 1 pixel but with a
              proportionate reduction in the strength of each pixel,
              approximating the visual appearance of a subpixel line
              width.
          antialias : bool, optional
              This option is kept for backward compatibility only.
              ``True`` is equivalent to ``line_width=1`` and
              ``False`` (the default) to ``line_width=0``. Do not specify
              both ``antialias`` and ``line_width`` in the same call as a
              ``ValueError`` will be raised if they disagree.
  
          Examples
          --------
          Define a canvas and a pandas DataFrame with 6 rows
          >>> import pandas as pd  # doctest: +SKIP
          ... import numpy as np
          ... import datashader as ds
          ... from datashader import Canvas
          ... import datashader.transfer_functions as tf
          ... cvs = Canvas()
          ... df = pd.DataFrame({
          ...    'A1': [1, 1.5, 2, 2.5, 3, 4],
          ...    'A2': [1.5, 2, 3, 3.2, 4, 5],
          ...    'B1': [10, 12, 11, 14, 13, 15],
          ...    'B2': [11, 9, 10, 7, 8, 12],
          ... }, dtype='float64')
  
          Aggregate one line across all rows, with coordinates df.A1 by df.B1
          >>> agg = cvs.line(df, x='A1', y='B1', axis=0)  # doctest: +SKIP
          ... tf.spread(tf.shade(agg))
  
          Aggregate two lines across all rows. The first with coordinates
          df.A1 by df.B1 and the second with coordinates df.A2 by df.B2
          >>> agg = cvs.line(df, x=['A1', 'A2'], y=['B1', 'B2'], axis=0)  # doctest: +SKIP
          ... tf.spread(tf.shade(agg))
  
          Aggregate two lines across all rows where the lines share the same
          x coordinates. The first line will have coordinates df.A1 by df.B1
          and the second will have coordinates df.A1 by df.B2
          >>> agg = cvs.line(df, x='A1', y=['B1', 'B2'], axis=0)  # doctest: +SKIP
          ... tf.spread(tf.shade(agg))
  
          Aggregate 6 length-2 lines, one per row, where the ith line has
          coordinates [df.A1[i], df.A2[i]] by [df.B1[i], df.B2[i]]
          >>> agg = cvs.line(df, x=['A1', 'A2'], y=['B1', 'B2'], axis=1)  # doctest: +SKIP
          ... tf.spread(tf.shade(agg))
  
          Aggregate 6 length-4 lines, one per row, where the x coordinates
          of every line are [0, 1, 2, 3] and the y coordinates of the ith line
          are [df.A1[i], df.A2[i], df.B1[i], df.B2[i]].
          >>> agg = cvs.line(df,  # doctest: +SKIP
          ...                x=np.arange(4),
          ...                y=['A1', 'A2', 'B1', 'B2'],
          ...                axis=1)
          ... tf.spread(tf.shade(agg))
  
          Aggregate RaggedArrays of variable length lines, one per row
          (requires pandas >= 0.24.0)
          >>> df_ragged = pd.DataFrame({  # doctest: +SKIP
          ...    'A1': pd.array([
          ...        [1, 1.5], [2, 2.5, 3], [1.5, 2, 3, 4], [3.2, 4, 5]],
          ...        dtype='Ragged[float32]'),
          ...    'B1': pd.array([
          ...        [10, 12], [11, 14, 13], [10, 7, 9, 10], [7, 8, 12]],
          ...        dtype='Ragged[float32]'),
          ...    'group': pd.Categorical([0, 1, 2, 1])
          ... })
          ...
          ... agg = cvs.line(df_ragged, x='A1', y='B1', axis=1)
          ... tf.spread(tf.shade(agg))
  
          Aggregate RaggedArrays of variable length lines by group column,
          one per row (requires pandas >= 0.24.0)
          >>> agg = cvs.line(df_ragged, x='A1', y='B1',  # doctest: +SKIP
          ...                agg=ds.count_cat('group'), axis=1)
          ... tf.spread(tf.shade(agg))
          """
          from .glyphs import (LineAxis0, LinesAxis1, LinesAxis1XConstant,
                               LinesAxis1YConstant, LineAxis0Multi,
                               LinesAxis1Ragged, LineAxis1Geometry, LinesXarrayCommonX)
  
          validate_xy_or_geometry('Line', x, y, geometry)
  
          if agg is None:
              agg = rd.any()
  
          if line_width is None:
              line_width = 0
  
          # Check and convert antialias kwarg to line_width.
          if antialias and line_width != 0:
              raise ValueError(
                  "Do not specify values for both the line_width and \n"
                  "antialias keyword arguments; use line_width instead.")
          if antialias:
              line_width = 1.0
  
          if geometry is not None:
              if spatialpandas and isinstance(source, spatialpandas.dask.DaskGeoDataFrame):
                  # Downselect partitions to those that may contain lines in viewport
                  x_range = self.x_range if self.x_range is not None else (None, None)
                  y_range = self.y_range if self.y_range is not None else (None, None)
                  source = source.cx_partitions[slice(*x_range), slice(*y_range)]
                  glyph = LineAxis1Geometry(geometry)
              elif spatialpandas and isinstance(source, spatialpandas.GeoDataFrame):
                  glyph = LineAxis1Geometry(geometry)
              elif (geopandas_source := self._source_from_geopandas(source)) is not None:
                  source = geopandas_source
                  from datashader.glyphs.line import LineAxis1GeoPandas
                  glyph = LineAxis1GeoPandas(geometry)
              else:
                  raise ValueError(
                      "source must be an instance of spatialpandas.GeoDataFrame, "
                      "spatialpandas.dask.DaskGeoDataFrame, geopandas.GeoDataFrame, or "
                      f"dask_geopandas.GeoDataFrame. Received objects of type {type(source)}")
  
          elif isinstance(source, Dataset) and isinstance(x, str) and isinstance(y, str):
              x_arr = source[x]
              y_arr = source[y]
              if x_arr.ndim != 1:
                  raise ValueError(f"x array must have 1 dimension not {x_arr.ndim}")
  
              if y_arr.ndim != 2:
                  raise ValueError(f"y array must have 2 dimensions not {y_arr.ndim}")
              if x not in y_arr.dims:
                  raise ValueError("x must be one of the coordinate dimensions of y")
  
              y_coord_dims = list(y_arr.coords.dims)
              x_dim_index = y_coord_dims.index(x)
              glyph = LinesXarrayCommonX(x, y, x_dim_index)
          else:
              # Broadcast column specifications to handle cases where
              # x is a list and y is a string or vice versa
              orig_x, orig_y = x, y
              x, y = _broadcast_column_specifications(x, y)
  
              if axis == 0:
                  if (isinstance(x, (Number, str)) and
                          isinstance(y, (Number, str))):
                      glyph = LineAxis0(x, y)
                  elif (isinstance(x, (list, tuple)) and
                          isinstance(y, (list, tuple))):
                      glyph = LineAxis0Multi(tuple(x), tuple(y))
                  else:
                      raise ValueError(f"""
  Invalid combination of x and y arguments to Canvas.line when axis=0.
      Received:
          x: {repr(orig_x)}
          y: {repr(orig_y)}
  See docstring for more information on valid usage""")
  
              elif axis == 1:
                  if isinstance(x, (list, tuple)) and isinstance(y, (list, tuple)):
                      glyph = LinesAxis1(tuple(x), tuple(y))
                  elif (isinstance(x, np.ndarray) and
                        isinstance(y,  (list, tuple))):
                      glyph = LinesAxis1XConstant(x, tuple(y))
                  elif (isinstance(x, (list, tuple)) and
                        isinstance(y, np.ndarray)):
                      glyph = LinesAxis1YConstant(tuple(x), y)
                  elif (isinstance(x, (Number, str)) and
                          isinstance(y, (Number, str))):
                      glyph = LinesAxis1Ragged(x, y)
                  else:
                      raise ValueError(f"""
  Invalid combination of x and y arguments to Canvas.line when axis=1.
      Received:
          x: {repr(orig_x)}
          y: {repr(orig_y)}
  See docstring for more information on valid usage""")
  
              else:
                  raise ValueError(f"""
  The axis argument to Canvas.line must be 0 or 1
      Received: {axis}""")
  
          if (line_width > 0 and ((cudf and isinstance(source, cudf.DataFrame)) or
                                 (dask_cudf and isinstance(source, dask_cudf.DataFrame)))):
              warnings.warn(
                  "Antialiased lines are not supported for CUDA-backed sources, "
                  "so reverting to line_width=0")
              line_width = 0
  
          glyph.set_line_width(line_width)
  
          if glyph.antialiased:
              # This is required to identify and report use of reductions that do
              # not yet support antialiasing.
              non_cat_agg = agg
              if isinstance(non_cat_agg, rd.by):
                  non_cat_agg = non_cat_agg.reduction
  
              if not isinstance(non_cat_agg, (
                  rd.any, rd.count, rd.max, rd.min, rd.sum, rd.summary, rd._sum_zero,
                  rd._first_or_last, rd.mean, rd.max_n, rd.min_n, rd._first_n_or_last_n,
                  rd._max_or_min_row_index, rd._max_n_or_min_n_row_index, rd.where,
              )):
                  raise NotImplementedError(
                      f"{type(non_cat_agg)} reduction not implemented for antialiased lines")
  
          return bypixel(source, self, glyph, agg, antialias=glyph.antialiased)
  
      def area(self, source, x, y, agg=None, axis=0, y_stack=None):
          """Compute a reduction by pixel, mapping data to pixels as a filled
          area region
  
          Parameters
          ----------
          source : pandas.DataFrame, dask.DataFrame, or xarray.DataArray/Dataset
              The input datasource.
          x, y : str or number or list or tuple or np.ndarray
              Specification of the x and y coordinates of each vertex of the
              line defining the starting edge of the area region.
              * str or number: Column labels in source
              * list or tuple: List or tuple of column labels in source
              * np.ndarray: When axis=1, a literal array of the
                coordinates to be used for every row
          agg : Reduction, optional
              Reduction to compute. Default is ``count()``.
          axis : 0 or 1, default 0
              Axis in source to draw lines along
              * 0: Draw area regions using data from the specified columns
                   across all rows in source
              * 1: Draw one area region per row in source using data from the
                   specified columns
          y_stack: str or number or list or tuple or np.ndarray or None
              Specification of the y coordinates of each vertex of the line
              defining the ending edge of the area region, where the x
              coordinate is given by the x argument described above.
  
              If y_stack is None, then the area region is filled to the y=0 line
  
              If y_stack is not None, then the form of y_stack must match the
              form of y.
  
          Examples
          --------
          Define a canvas and a pandas DataFrame with 6 rows
          >>> import pandas as pd  # doctest: +SKIP
          ... import numpy as np
          ... import datashader as ds
          ... from datashader import Canvas
          ... import datashader.transfer_functions as tf
          ... cvs = Canvas()
          ... df = pd.DataFrame({
          ...    'A1': [1, 1.5, 2, 2.5, 3, 4],
          ...    'A2': [1.6, 2.1, 2.9, 3.2, 4.2, 5],
          ...    'B1': [10, 12, 11, 14, 13, 15],
          ...    'B2': [11, 9, 10, 7, 8, 12],
          ... }, dtype='float64')
  
          Aggregate one area region across all rows, that starts with
          coordinates df.A1 by df.B1 and is filled to the y=0 line
          >>> agg = cvs.area(df, x='A1', y='B1',  # doctest: +SKIP
          ...                agg=ds.count(), axis=0)
          ... tf.shade(agg)
  
          Aggregate one area region across all rows, that starts with
          coordinates df.A1 by df.B1 and is filled to the line with coordinates
          df.A1 by df.B2
          >>> agg = cvs.area(df, x='A1', y='B1', y_stack='B2', # doctest: +SKIP
          ...                agg=ds.count(), axis=0)
          ... tf.shade(agg)
  
          Aggregate two area regions across all rows. The first starting
          with coordinates df.A1 by df.B1 and the second with coordinates
          df.A2 by df.B2. Both regions are filled to the y=0 line
          >>> agg = cvs.area(df, x=['A1', 'A2'], y=['B1', 'B2'],  # doctest: +SKIP
                             agg=ds.count(), axis=0)
          ... tf.shade(agg)
  
          Aggregate two area regions across all rows where the regions share the
          same x coordinates. The first region will start with coordinates
          df.A1 by df.B1 and the second will start with coordinates
          df.A1 by df.B2. Both regions are filled to the y=0 line
          >>> agg = cvs.area(df, x='A1', y=['B1', 'B2'], agg=ds.count(), axis=0)  # doctest: +SKIP
          ... tf.shade(agg)
  
          Aggregate 6 length-2 area regions, one per row, where the ith region
          starts with coordinates [df.A1[i], df.A2[i]] by [df.B1[i], df.B2[i]]
          and is filled to the y=0 line
          >>> agg = cvs.area(df, x=['A1', 'A2'], y=['B1', 'B2'],  # doctest: +SKIP
                             agg=ds.count(), axis=1)
          ... tf.shade(agg)
  
          Aggregate 6 length-4 area regions, one per row, where the
          starting x coordinates of every region are [0, 1, 2, 3] and
          the starting y coordinates of the ith region are
          [df.A1[i], df.A2[i], df.B1[i], df.B2[i]].  All regions are filled to
          the y=0 line
          >>> agg = cvs.area(df,  # doctest: +SKIP
          ...                x=np.arange(4),
          ...                y=['A1', 'A2', 'B1', 'B2'],
          ...                agg=ds.count(),
          ...                axis=1)
          ... tf.shade(agg)
  
          Aggregate RaggedArrays of variable length area regions, one per row.
          The starting coordinates of the ith region are df_ragged.A1 by
          df_ragged.B1 and the regions are filled to the y=0 line.
          (requires pandas >= 0.24.0)
          >>> df_ragged = pd.DataFrame({  # doctest: +SKIP
          ...    'A1': pd.array([
          ...        [1, 1.5], [2, 2.5, 3], [1.5, 2, 3, 4], [3.2, 4, 5]],
          ...        dtype='Ragged[float32]'),
          ...    'B1': pd.array([
          ...        [10, 12], [11, 14, 13], [10, 7, 9, 10], [7, 8, 12]],
          ...        dtype='Ragged[float32]'),
          ...    'B2': pd.array([
          ...        [6, 10], [9, 10, 18], [9, 5, 6, 8], [4, 5, 11]],
          ...        dtype='Ragged[float32]'),
          ...    'group': pd.Categorical([0, 1, 2, 1])
          ... })
          ...
          ... agg = cvs.area(df_ragged, x='A1', y='B1', agg=ds.count(), axis=1)
          ... tf.shade(agg)
  
          Instead of filling regions to the y=0 line, fill to the line with
          coordinates df_ragged.A1 by df_ragged.B2
          >>> agg = cvs.area(df_ragged, x='A1', y='B1', y_stack='B2', # doctest: +SKIP
          ...                agg=ds.count(), axis=1)
          ... tf.shade(agg)
  
          (requires pandas >= 0.24.0)
          """
          from .glyphs import (
              AreaToZeroAxis0, AreaToLineAxis0,
              AreaToZeroAxis0Multi, AreaToLineAxis0Multi,
              AreaToZeroAxis1, AreaToLineAxis1,
              AreaToZeroAxis1XConstant, AreaToLineAxis1XConstant,
              AreaToZeroAxis1YConstant, AreaToLineAxis1YConstant,
              AreaToZeroAxis1Ragged, AreaToLineAxis1Ragged,
          )
          from .reductions import any as any_rdn
          if agg is None:
              agg = any_rdn()
  
          # Broadcast column specifications to handle cases where
          # x is a list and y is a string or vice versa
          orig_x, orig_y, orig_y_stack = x, y, y_stack
          x, y, y_stack = _broadcast_column_specifications(x, y, y_stack)
  
          if axis == 0:
              if y_stack is None:
                  if (isinstance(x, (Number, str)) and
                          isinstance(y, (Number, str))):
                      glyph = AreaToZeroAxis0(x, y)
                  elif (isinstance(x, (list, tuple)) and
                        isinstance(y, (list, tuple))):
                      glyph = AreaToZeroAxis0Multi(tuple(x), tuple(y))
                  else:
                      raise ValueError(f"""
  Invalid combination of x and y arguments to Canvas.area when axis=0.
      Received:
          x: {repr(x)}
          y: {repr(y)}
  See docstring for more information on valid usage""")
              else:
                  # y_stack is not None
                  if (isinstance(x, (Number, str)) and
                          isinstance(y, (Number, str)) and
                          isinstance(y_stack, (Number, str))):
  
                      glyph = AreaToLineAxis0(x, y, y_stack)
                  elif (isinstance(x, (list, tuple)) and
                        isinstance(y, (list, tuple)) and
                        isinstance(y_stack, (list, tuple))):
                      glyph = AreaToLineAxis0Multi(
                          tuple(x), tuple(y), tuple(y_stack))
                  else:
                      raise ValueError(f"""
  Invalid combination of x, y, and y_stack arguments to Canvas.area when axis=0.
      Received:
          x: {repr(orig_x)}
          y: {repr(orig_y)}
          y_stack: {repr(orig_y_stack)}
  See docstring for more information on valid usage""")
  
          elif axis == 1:
              if y_stack is None:
                  if (isinstance(x, (list, tuple)) and
                          isinstance(y, (list, tuple))):
                      glyph = AreaToZeroAxis1(tuple(x), tuple(y))
                  elif (isinstance(x, np.ndarray) and
                        isinstance(y, (list, tuple))):
                      glyph = AreaToZeroAxis1XConstant(x, tuple(y))
                  elif (isinstance(x, (list, tuple)) and
                        isinstance(y, np.ndarray)):
                      glyph = AreaToZeroAxis1YConstant(tuple(x), y)
                  elif (isinstance(x, (Number, str)) and
                        isinstance(y, (Number, str))):
                      glyph = AreaToZeroAxis1Ragged(x, y)
                  else:
                      raise ValueError(f"""
  Invalid combination of x and y arguments to Canvas.area when axis=1.
      Received:
          x: {repr(x)}
          y: {repr(y)}
  See docstring for more information on valid usage""")
              else:
                  if (isinstance(x, (list, tuple)) and
                          isinstance(y, (list, tuple)) and
                          isinstance(y_stack, (list, tuple))):
                      glyph = AreaToLineAxis1(
                          tuple(x), tuple(y), tuple(y_stack))
                  elif (isinstance(x, np.ndarray) and
                        isinstance(y, (list, tuple)) and
                        isinstance(y_stack, (list, tuple))):
                      glyph = AreaToLineAxis1XConstant(
                          x, tuple(y), tuple(y_stack))
                  elif (isinstance(x, (list, tuple)) and
                        isinstance(y, np.ndarray) and
                        isinstance(y_stack, np.ndarray)):
                      glyph = AreaToLineAxis1YConstant(tuple(x), y, y_stack)
                  elif (isinstance(x, (Number, str)) and
                        isinstance(y, (Number, str)) and
                        isinstance(y_stack, (Number, str))):
                      glyph = AreaToLineAxis1Ragged(x, y, y_stack)
                  else:
                      raise ValueError(f"""
  Invalid combination of x, y, and y_stack arguments to Canvas.area when axis=1.
      Received:
          x: {repr(orig_x)}
          y: {repr(orig_y)}
          y_stack: {repr(orig_y_stack)}
  See docstring for more information on valid usage""")
          else:
              raise ValueError(f"""
  The axis argument to Canvas.area must be 0 or 1
      Received: {axis}""")
  
          return bypixel(source, self, glyph, agg)
  
      def polygons(self, source, geometry, agg=None):
          """Compute a reduction by pixel, mapping data to pixels as one or
          more filled polygons.
  
          Parameters
          ----------
          source : xarray.DataArray or Dataset
              The input datasource.
          geometry : str
              Column name of a PolygonsArray of the coordinates of each line.
          agg : Reduction, optional
              Reduction to compute. Default is ``any()``.
  
          Returns
          -------
          data : xarray.DataArray
  
          Examples
          --------
          >>> import datashader as ds  # doctest: +SKIP
          ... import datashader.transfer_functions as tf
          ... from spatialpandas.geometry import PolygonArray
          ... from spatialpandas import GeoDataFrame
          ... import pandas as pd
          ...
          ... polygons = PolygonArray([
          ...     # First Element
          ...     [[0, 0, 1, 0, 2, 2, -1, 4, 0, 0],  # Filled quadrilateral (CCW order)
          ...      [0.5, 1,  1, 2,  1.5, 1.5,  0.5, 1],     # Triangular hole (CW order)
          ...      [0, 2, 0, 2.5, 0.5, 2.5, 0.5, 2, 0, 2],  # Rectangular hole (CW order)
          ...      [2.5, 3, 3.5, 3, 3.5, 4, 2.5, 3],  # Filled triangle
          ...     ],
          ...
          ...     # Second Element
          ...     [[3, 0, 3, 2, 4, 2, 4, 0, 3, 0],  # Filled rectangle (CCW order)
          ...      # Rectangular hole (CW order)
          ...      [3.25, 0.25, 3.75, 0.25, 3.75, 1.75, 3.25, 1.75, 3.25, 0.25],
          ...     ]
          ... ])
          ...
          ... df = GeoDataFrame({'polygons': polygons, 'v': range(len(polygons))})
          ...
          ... cvs = ds.Canvas()
          ... agg = cvs.polygons(df, geometry='polygons', agg=ds.sum('v'))
          ... tf.shade(agg)
          """
          from .glyphs import PolygonGeom
          from .reductions import any as any_rdn
  
          if spatialpandas and isinstance(source, spatialpandas.dask.DaskGeoDataFrame):
              # Downselect partitions to those that may contain polygons in viewport
              x_range = self.x_range if self.x_range is not None else (None, None)
              y_range = self.y_range if self.y_range is not None else (None, None)
              source = source.cx_partitions[slice(*x_range), slice(*y_range)]
              glyph = PolygonGeom(geometry)
          elif spatialpandas and isinstance(source, spatialpandas.GeoDataFrame):
              glyph = PolygonGeom(geometry)
          elif (geopandas_source := self._source_from_geopandas(source)) is not None:
              source = geopandas_source
              from .glyphs.polygon import GeopandasPolygonGeom
              glyph = GeopandasPolygonGeom(geometry)
          else:
              raise ValueError(
                  "source must be an instance of spatialpandas.GeoDataFrame, "
                  "spatialpandas.dask.DaskGeoDataFrame, geopandas.GeoDataFrame or "
                  f"dask_geopandas.GeoDataFrame, not {type(source)}")
  
          if agg is None:
              agg = any_rdn()
          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
  