Repository: holoviz/datashader

Issue:
Insufficient validation of Canvas.line input lengths
The sizes of the possible inputs to `Canvas.line()` are not sufficiently validated. Consider this example of `LinesAxis1XConstant` which accepts a `numpy` array for the `x` values and a number of columns of a `pandas.DataFrame` for the `y` values:

```python
import datashader as ds
import numpy as np
import pandas as pd

# LinesAxis1XConstant
x = np.arange(1)  # Incorrect size, should have length of 2.
df = pd.DataFrame(dict(y_from = [0, 1, 0, 1, 0.0], y_to = [0, 1, 1, 0, 0.5]))

canvas = ds.Canvas()
agg = canvas.line(source=df, x=x, y=["y_from", "y_to"], axis=1, agg=ds.count())
```
Here there is only a single `x` coordinate when there should be two to match the `y` coordinates. If this example is run normally then no error is reported. If it is run with `numba` jitting disabled, i.e. `NUMBA_DISABLE_JIT=1 python test.py` then an out of bounds error is reported as follows:
```
Traceback (most recent call last):
  File "/Users/iant/github_temp/datashader_temp/issue1159.py", line 13, in <module>
    agg = canvas.line(source=df, x=x, y=["y_from", "y_to"], axis=1, agg=ds.count())
  File "/Users/iant/github/datashader/datashader/core.py", line 450, in line
    return bypixel(source, self, glyph, agg, antialias=glyph.antialiased)
  File "/Users/iant/github/datashader/datashader/core.py", line 1258, in bypixel
    return bypixel.pipeline(source, schema, canvas, glyph, agg, antialias=antialias)
  File "/Users/iant/github/datashader/datashader/utils.py", line 109, in __call__
    return lk[typ](head, *rest, **kwargs)
  File "/Users/iant/github/datashader/datashader/data_libraries/pandas.py", line 17, in pandas_pipeline
    return glyph_dispatch(glyph, df, schema, canvas, summary, antialias=antialias)
  File "/Users/iant/github/datashader/datashader/utils.py", line 112, in __call__
    return lk[cls](head, *rest, **kwargs)
  File "/Users/iant/github/datashader/datashader/data_libraries/pandas.py", line 48, in default
    extend(bases, source, x_st + y_st, x_range + y_range)
  File "/Users/iant/github/datashader/datashader/glyphs/line.py", line 382, in extend
    do_extend(
  File "/Users/iant/github/datashader/datashader/glyphs/line.py", line 1306, in extend_cpu
    perform_extend_line(
  File "/Users/iant/github/datashader/datashader/glyphs/line.py", line 1278, in perform_extend_line
    x1 = xs[j + 1]
IndexError: index 1 is out of bounds for axis 0 with size 1
```
There should be an explicit check that the coordinates have compatible lengths and an appropriate error reported, regardless of whether `numba` jitting is enabled or not.

This is using the latest commit (`a1d9513915a`) of the `main` branch.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/holoviz/datashader/pull/1160?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
> Merging [#1160](https://codecov.io/gh/holoviz/datashader/pull/1160?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) (5c735c8) into [main](https://codecov.io/gh/holoviz/datashader/commit/a1d9513915a870dda6348dec4e0df52f4f702183?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) (a1d9513) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #1160      +/-   ##
==========================================
- Coverage   85.38%   85.36%   -0.02%     
==========================================
  Files          35       35              
  Lines        7812     7818       +6     
==========================================
+ Hits         6670     6674       +4     
- Misses       1142     1144       +2     
```


| [Impacted Files](https://codecov.io/gh/holoviz/datashader/pull/1160?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) | Coverage Δ | |
|---|---|---|
| [datashader/glyphs/line.py](https://codecov.io/gh/holoviz/datashader/pull/1160/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-ZGF0YXNoYWRlci9nbHlwaHMvbGluZS5weQ==) | `92.66% <100.00%> (-0.14%)` | :arrow_down: |
| [datashader/glyphs/polygon.py](https://codecov.io/gh/holoviz/datashader/pull/1160/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-ZGF0YXNoYWRlci9nbHlwaHMvcG9seWdvbi5weQ==) | `94.15% <0.00%> (-0.65%)` | :arrow_down: |
| [datashader/macros.py](https://codecov.io/gh/holoviz/datashader/pull/1160/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-ZGF0YXNoYWRlci9tYWNyb3MucHk=) | `92.92% <0.00%> (-0.08%)` | :arrow_down: |
| [datashader/transfer\_functions/\_cuda\_utils.py](https://codecov.io/gh/holoviz/datashader/pull/1160/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-ZGF0YXNoYWRlci90cmFuc2Zlcl9mdW5jdGlvbnMvX2N1ZGFfdXRpbHMucHk=) | `26.66% <0.00%> (+0.29%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz)

ianthomas23: I am merging this following review approval. There is a CI problem with Python 3.7 on macos but that is affecting all PRs and I will address it as soon as possible.

Patch:
diff --git a/datashader/glyphs/line.py b/datashader/glyphs/line.py
index 47c7f7c..7ecd9d2 100644
--- a/datashader/glyphs/line.py
+++ b/datashader/glyphs/line.py
@@ -125,6 +125,10 @@ class LineAxis0Multi(_PointLike, _AntiAliasedLine):
         elif not all([isreal(in_dshape.measure[str(ycol)]) for ycol in self.y]):
             raise ValueError('y columns must be real')
 
+        if len(self.x) != len(self.y):
+            raise ValueError(
+                f'x and y coordinate lengths do not match: {len(self.x)} != {len(self.y)}')
+
     @property
     def x_label(self):
         return 'x'
@@ -233,6 +237,10 @@ class LinesAxis1(_PointLike, _AntiAliasedLine):
         if len(unique_y_measures) > 1:
             raise ValueError('y columns must have the same data type')
 
+        if len(self.x) != len(self.y):
+            raise ValueError(
+                f'x and y coordinate lengths do not match: {len(self.x)} != {len(self.y)}')
+
     def required_columns(self):
         return self.x + self.y
 
@@ -326,6 +334,10 @@ class LinesAxis1XConstant(LinesAxis1):
         if len(unique_y_measures) > 1:
             raise ValueError('y columns must have the same data type')
 
+        if len(self.x) != len(self.y):
+            raise ValueError(
+                f'x and y coordinate lengths do not match: {len(self.x)} != {len(self.y)}')
+
     def required_columns(self):
         return self.y
 
@@ -398,6 +410,10 @@ class LinesAxis1YConstant(LinesAxis1):
         if len(unique_x_measures) > 1:
             raise ValueError('x columns must have the same data type')
 
+        if len(self.x) != len(self.y):
+            raise ValueError(
+                f'x and y coordinate lengths do not match: {len(self.x)} != {len(self.y)}')
+
     def required_columns(self):
         return self.x
 


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 4f408e0..9d24506 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -19,7 +19,7 @@ jobs:
       matrix:
         os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
         python-version: ["3.7", "3.8", "3.9", "3.10"]
-    timeout-minutes: 90
+    timeout-minutes: 120
     defaults:
       run:
         shell: bash -l {0}
diff --git a/datashader/tests/test_pandas.py b/datashader/tests/test_pandas.py
index e5e6f9b..b545412 100644
--- a/datashader/tests/test_pandas.py
+++ b/datashader/tests/test_pandas.py
@@ -2230,3 +2230,30 @@ def test_reduction_dtype(reduction, dtype, aa_dtype):
 def test_log_axis_not_positive(df, canvas):
     with pytest.raises(ValueError, match='Range values must be >0 for logarithmic axes'):
         canvas.line(df, 'x', 'y')
+
+
+def test_line_coordinate_lengths():
+    # Issue #1159.
+    cvs = ds.Canvas(plot_width=10, plot_height=6)
+    msg = r'^x and y coordinate lengths do not match'
+
+    # LineAxis0Multi (axis=0) and LinesAxis1 (axis=1)
+    df = pd.DataFrame(
+        dict(x0=[0, 0.2, 1], y0=[0, 0.4, 1], x1=[0, 0.6, 1], y1=[1, 0.8, 1]))
+    for axis in (0, 1):
+        with pytest.raises(ValueError, match=msg):
+            cvs.line(source=df, x=["x0"], y=["y0", "y1"], axis=axis)
+        with pytest.raises(ValueError, match=msg):
+            cvs.line(source=df, x=["x0", "x1"], y=["y0"], axis=axis)
+
+    # LinesAxis1XConstant
+    df = pd.DataFrame(dict(y0=[0, 1, 0, 1], y1=[0, 1, 1, 0]))
+    for nx in (1, 3):
+        with pytest.raises(ValueError, match=msg):
+            cvs.line(source=df, x=np.arange(nx), y=["y0", "y1"], axis=1)
+
+    # LinesAxis1YConstant
+    df = pd.DataFrame(dict(x0=[0, 1, 0, 1], x1=[0, 1, 1, 0]))
+    for ny in (1, 3):
+        with pytest.raises(ValueError, match=msg):
+            cvs.line(source=df, x=["x0", "x1"], y=np.arange(ny), axis=1)