Repository: holoviz/holoviews

Issue:
hv.QuadMesh - strange behavior with the `vdim` parameter
<!--
Thanks for contacting us! Please read and follow these instructions carefully, then you can delete this introductory text. Note that the issue tracker is NOT the place for usage questions and technical assistance; post those at [Discourse](https://discourse.holoviz.org) instead. Issues without the required information below may be closed immediately.
-->


#### ALL software version info
(this library, plus any other relevant software, e.g. bokeh, python, notebook, OS, browser, etc should be added within the dropdown below.)

<details>
    <summary>Software Version Info</summary>

```plaintext
holoviews:  1.19.1 
bokeh: 3.5.2
python: 3.11.10
jupyterlab: 4.2.5

OS: macOS 12.7.5 
Browser: Chrome
```
</details>

#### Description of expected behavior and the observed behavior

Example code for `QuadMesh` from the docs - slightly modified by using explicit `kdims` and `vdims`, working as expected:

```python
import numpy as np
import holoviews as hv
hv.extension('bokeh')

n = 8  # Number of bins in each direction
xs = np.logspace(1, 3, n)
ys = np.linspace(1, 10, n)
zs = np.arange((n-1)**2).reshape(n-1, n-1)

hv.QuadMesh((xs, ys, zs), ['x', 'y'], ['foo'])
```

Result:

<img src="https://github.com/user-attachments/assets/cb37f0eb-33e1-44ce-9db0-b427d7bc8f21" width=300 />


#### Complete, minimal, self-contained example code that reproduces the issue

Same code but having a name for the `vdim` that isn't a valid variable name in python (such as `' foo (unit)' ` produces a blank plot (no error)

```python
n = 8  # Number of bins in each direction
xs = np.logspace(1, 3, n)
ys = np.linspace(1, 10, n)
zs = np.arange((n-1)**2).reshape(n-1, n-1)

hv.QuadMesh((xs, ys, zs), ['x', 'y'], ['foo (unit)'])
```

Result:

<img src="https://github.com/user-attachments/assets/455bc0f5-bec8-483f-8125-7e5d1f081a70" width=300 />

I tried this with other element types (...actually only with `hv.Curve`) and a `vdim` name that is not a valid python name still works as expected:

```python
xs = range(-10,11)
ys = [100-x**2 for x in xs]

hv.Curve((xs, ys), 'foo', 'bar (unit)')
```

Result:

<img src="https://github.com/user-attachments/assets/f759181e-9b51-4cd1-9181-855e9bf38fcf" width=300 />

So this likely an issue with `QuadMesh` specifically. Any ideas?

#### One more strangeness

Strangely enough, the above example that resulted in a blank `QuadMesh` plot, suddenly works when the X and Y dimensions are 2D instead of 1D:

```python
n = 8  # Number of bins in each direction
xs = np.logspace(1, 3, n)
ys = np.linspace(1, 10, n)
zs = np.arange((n-1)**2).reshape(n-1, n-1)

XX, YY = np.meshgrid(xs, ys)

hv.QuadMesh((XX, YY, zs), ['x', 'y'], 'foo (unit)')
``` 

Result:

<img src="https://github.com/user-attachments/assets/bc8575cf-f809-4224-93f8-1b7c7890c5dc" width=300 />

Now, I am genuinely puzzled by what's going on...



Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6462?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.50%. Comparing base [(`93040aa`)](https://app.codecov.io/gh/holoviz/holoviews/commit/93040aa71a19f01dfaf1458772239a49ea5b20cf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`6c0761a`)](https://app.codecov.io/gh/holoviz/holoviews/commit/6c0761aaf653650037f9617b4e00ddcde45bb9e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #6462   +/-   ##
=======================================
  Coverage   88.50%   88.50%           
=======================================
  Files         323      323           
  Lines       68631    68652   +21     
=======================================
+ Hits        60741    60760   +19     
- Misses       7890     7892    +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6462?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


Patch:
diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py
index 056e49e5c..99f524752 100644
--- a/holoviews/plotting/bokeh/raster.py
+++ b/holoviews/plotting/bokeh/raster.py
@@ -363,7 +363,7 @@ class QuadMeshPlot(ColorbarPlot):
 
         if self.invert_axes: x, y = y, x
         cmapper = self._get_colormapper(z, element, ranges, style)
-        cmapper = {'field': z.name, 'transform': cmapper}
+        cmapper = {'field': dimension_sanitizer(z.name), 'transform': cmapper}
 
         irregular = (element.interface.irregular(element, x) or
                      element.interface.irregular(element, y))
@@ -405,11 +405,10 @@ class QuadMeshPlot(ColorbarPlot):
                     mask.append(False)
             mask = np.array(mask)
 
-            data = {'xs': XS, 'ys': YS, z.name: zvals[mask]}
+            data = {'xs': XS, 'ys': YS, dimension_sanitizer(z.name): zvals[mask]}
             if 'hover' in self.handles:
                 if not self.static_source:
-                    hover_data = self._collect_hover_data(
-                            element, mask, irregular=True)
+                    hover_data = self._collect_hover_data(element, mask, irregular=True)
                 hover_data[x] = np.array(xc)
                 hover_data[y] = np.array(yc)
         else:


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_quadmeshplot.py b/holoviews/tests/plotting/bokeh/test_quadmeshplot.py
index 81b9a467d..7e59e2b2c 100644
--- a/holoviews/tests/plotting/bokeh/test_quadmeshplot.py
+++ b/holoviews/tests/plotting/bokeh/test_quadmeshplot.py
@@ -55,6 +55,33 @@ class TestQuadMeshPlot(TestBokehPlot):
         source = plot.handles['source']
         self.assertEqual(source.data['z'], flattened)
 
+    def test_quadmesh_non_sanitized_name(self):
+        # https://github.com/holoviz/holoviews/issues/6460
+        xs = [0, 1, 2]
+        ys = [2, 1, 0]
+        data = np.array([[0,1,2], [3,4,5], [6,7,8]])
+        qmesh = QuadMesh((xs, ys, data), vdims="z (x,y)")
+        plot = bokeh_renderer.get_plot(qmesh)
+        source = plot.handles['source']
+
+        flattened = np.array([6, 3, 0, 7, 4, 1, 8, 5, 2])
+        sanitized_name = 'z_left_parenthesis_x_comma_y_right_parenthesis'
+        np.testing.assert_equal(source.data[sanitized_name], flattened)
+
+    def test_quadmesh_non_sanitized_name_grid(self):
+        # https://github.com/holoviz/holoviews/issues/6460
+        xs = [0, 1, 2]
+        ys = [2, 1, 0]
+        data = np.array([[0,1,2], [3,4,5], [6,7,8]])
+        XX, YY = np.meshgrid(xs, ys)
+        qmesh = QuadMesh((XX, YY, data), vdims="z (x,y)")
+        plot = bokeh_renderer.get_plot(qmesh)
+        source = plot.handles['source']
+
+        flattened = data.flatten()
+        sanitized_name = 'z_left_parenthesis_x_comma_y_right_parenthesis'
+        np.testing.assert_equal(source.data[sanitized_name], flattened)
+
     def test_quadmesh_nodata_uint(self):
         xs = [0, 1, 2]
         ys = [2, 1, 0]