Repository: holoviz/holoviews

Issue:
`y_range.tags` expected to be a dictionary, but it's a list
Thanks for contacting us! Please read and follow these instructions carefully, then delete this introductory text to keep your issue easy to read. 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
<details>
<summary>versions:</summary>

```
Python              :  3.9.16 | packaged by conda-forge | (main, Feb  1 2023, 21:38:11) 
[Clang 14.0.6 ]
Operating system    :  macOS-13.4.1-arm64-arm-64bit
holoviews           :  1.17.0

bokeh               :  3.2.1
colorcet            :  3.0.1
cudf                :  -
dask                :  2023.7.1
datashader          :  0.15.1
geoviews            :  -
hvplot              :  0.8.4
ibis                :  -
IPython             :  8.14.0
jupyter_bokeh       :  3.0.7
jupyterlab          :  3.5.3
matplotlib          :  3.7.2
networkx            :  3.1
notebook            :  6.5.4
numba               :  0.57.1
numpy               :  1.24.4
pandas              :  2.0.3
panel               :  1.2.1
param               :  1.13.0
PIL                 :  10.0.0
plotly              :  -
pyarrow             :  12.0.1
scipy               :  1.11.1
skimage             :  0.21.0
spatialpandas       :  -
streamz             :  -
xarray              :  2023.7.0
```
</details>

#### Description of expected behavior and the observed behavior

Player widget should be controlling the playback of several plots arranged in a layout, but as soon as the "right" plot is added, the traceback occurs. 

#### Complete, minimal, self-contained example code that reproduces the issue

```
import panel as pn
import numpy as np
import xarray as xr
import hvplot.xarray
import holoviews as hv

data_values = np.random.rand(10, 20, 20)
frame_coords = np.arange(10)
height_coords = np.linspace(0, 1, 20)
width_coords = np.linspace(0, 1, 20)

data = xr.DataArray(
    data_values,
    dims=["frame", "height", "width"],
    coords={"frame": frame_coords, "height": height_coords, "width": width_coords}
)

fs = 30  # frames per second
video_player = pn.widgets.Player(
    length=len(data.coords["frame"]),
    interval=1000 // fs,  # ms per frame
    value=0,
    max_width=400,
    max_height=90,
    loop_policy="loop",
    sizing_mode="stretch_width",
)

main = data.hvplot.image(
    groupby="frame",
    cmap="Viridis",
    frame_height=400,
    frame_width=400,
    colorbar=False,
    widgets={"frame": video_player},
)

def update_vline(value):
    return hv.VLine(value)

def update_hline(value):
    return hv.HLine(value)

dmap_vline = hv.DynamicMap(update_vline, streams=[video_player.param.value]).opts(color='red', alpha=.6, line_width=3)
dmap_hline = hv.DynamicMap(update_hline, streams=[video_player.param.value]).opts(color='red', alpha=.6, line_width=3)

right = data.mean(['width']).hvplot.image(x='frame',
    cmap="Viridis",
    frame_height=400,
    frame_width=100,
    colorbar=False,
    title='_',
) * dmap_hline

bottom = data.mean(['height']).hvplot.image(y='frame',
    cmap="Viridis",
    frame_height=100,
    frame_width=400,
    colorbar=False,
) * dmap_vline
 
pn.Column(video_player, pn.Row(main[0], right), bottom)

```

#### Stack traceback and/or browser JavaScript console output
```
Traceback (most recent call last):
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/pyviz_comms/__init__.py", line 340, in _handle_msg
    self._on_msg(msg)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/viewable.py", line 471, in _on_msg
    doc.unhold()
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/bokeh/document/document.py", line 776, in unhold
    self.callbacks.unhold()
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/bokeh/document/callbacks.py", line 431, in unhold
    self.trigger_on_change(event)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/bokeh/document/callbacks.py", line 408, in trigger_on_change
    invoke_with_curdoc(doc, event.callback_invoker)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/bokeh/document/callbacks.py", line 443, in invoke_with_curdoc
    return f()
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/bokeh/util/callback_manager.py", line 185, in invoke
    callback(attr, old, new)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/reactive.py", line 469, in _comm_change
    state._handle_exception(e)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/io/state.py", line 436, in _handle_exception
    raise exception
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/reactive.py", line 467, in _comm_change
    self._schedule_change(doc, comm)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/reactive.py", line 449, in _schedule_change
    self._change_event(doc)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/reactive.py", line 445, in _change_event
    self._process_events(events)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/panel/reactive.py", line 384, in _process_events
    self.param.update(**self_events)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/param/parameterized.py", line 1902, in update
    self_._batch_call_watchers()
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/param/parameterized.py", line 2063, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/param/parameterized.py", line 2025, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/streams.py", line 760, in _watcher
    self.trigger([self])
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/streams.py", line 186, in trigger
    subscriber(**dict(union))
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/plot.py", line 232, in refresh
    raise e
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/plot.py", line 228, in refresh
    self._trigger_refresh(stream_key)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/plot.py", line 245, in _trigger_refresh
    self.update(key)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/plot.py", line 944, in update
    item = self.__getitem__(key)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/plot.py", line 431, in __getitem__
    self.update_frame(frame)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/bokeh/element.py", line 2836, in update_frame
    self._update_ranges(element, ranges)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/bokeh/element.py", line 952, in _update_ranges
    self._update_main_ranges(element, x_range, y_range, ranges)
  File "/Users/droumis/opt/miniconda3/envs/neuro-video-viewer/lib/python3.9/site-packages/holoviews/plotting/bokeh/element.py", line 1115, in _update_main_ranges
    y_range.tags[1]['invert_yaxis'] if y_range.tags else False,
TypeError: list indices must be integers or slices, not str
```

#### Screenshots or screencasts of the bug in action
![image](https://github.com/holoviz/holoviews/assets/6613202/622ab6a7-e17a-4ac4-808e-8799be00b0bd)



Discussion:

Resolving patch:
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -965,7 +965,7 @@ def _update_ranges(self, element, ranges):
                 continue
             self._update_range(
                 extra_y_range, b, t, factors,
-                extra_y_range.tags[1]['invert_yaxis'] if extra_y_range.tags else False,
+                self._get_tag(extra_y_range, 'invert_yaxis'),
                 self._shared.get(extra_y_range.name, False), log, streaming
             )
 
@@ -1111,10 +1111,24 @@ def _update_main_ranges(self, element, x_range, y_range, ranges):
             self._update_range(x_range, l, r, xfactors, self.invert_xaxis,
                                self._shared['x-main-range'], self.logx, streaming)
         if not self.drawn or yupdate:
-            self._update_range(y_range, b, t, yfactors,
-                               y_range.tags[1]['invert_yaxis'] if y_range.tags else False,
-                               self._shared['y-main-range'], self.logy, streaming)
+            self._update_range(
+                y_range, b, t, yfactors, self._get_tag(y_range, 'invert_yaxis'),
+                self._shared['y-main-range'], self.logy, streaming
+            )
 
+    def _get_tag(self, model, tag_name):
+        """Get a tag from a Bokeh model
+
+        Args:
+            model (Model): Bokeh model
+            tag_name (str): Name of tag to get
+        Returns:
+            tag_value: Value of tag or False if not found
+        """
+        for tag in model.tags:
+            if isinstance(tag, dict) and tag_name in tag:
+                return tag[tag_name]
+        return False
 
     def _update_range(self, axis_range, low, high, factors, invert, shared, log, streaming=False):
         if isinstance(axis_range, FactorRange):


Test patch: