Skip to content

fix(minian): keep plot titles rendering when the dask dashboard is open - #15

Merged
daharoni merged 1 commit into
mainfrom
fix/bokeh-str-title-with-dask-dashboard
Aug 2, 2026
Merged

fix(minian): keep plot titles rendering when the dask dashboard is open#15
daharoni merged 1 commit into
mainfrom
fix/bokeh-str-title-with-dask-dashboard

Conversation

@daharoni

@daharoni daharoni commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The bug

Partway through tutorials/minian/pipeline_no_deconv.ipynb, every cell that draws a titled HoloViews plot starts failing, and the kernel never recovers on its own. Reported from a live run at visualize_seeds(max_proj, seeds, "mask_pnr"):

File .../holoviews/plotting/bokeh/element.py:1502, in ElementPlot._update_title
   1502     plot.title.update(**props)

AttributeError: 'str' object has no attribute 'update'

Everything after that point stays broken until the kernel is restarted, which mid-workshop means re-running the whole pipeline.

Root cause

HoloViews builds each figure with a plain-string title (bokeh.plotting.figure(title=...), element.py:1218). Bokeh converts that into a Title model only through the coercion registered on Plot.title:

# bokeh/models/plots.py:586
""").accepts(String, lambda text: Title(text=text))

That coercion lives inside the validation branch of Property.prepare_value, so when validation is off the figure keeps a raw str, and the later plot.title.update(**props) raises.

Validation is gated by Property._should_validate — a process-global that Bokeh's validate() saves and restores with no lock and no thread-locality. This notebook runs two independent togglers in one process:

  • HoloViews itself wraps every datasource update in validate(False) (holoviews/plotting/bokeh/plot.py:223), which the VArrayViewer widgets drive continuously.
  • The in-process dask scheduler's dashboard decorates its callbacks with @without_property_validation and runs them on its own IO loop thread.

When the two scopes overlap, the second captures the first's temporary False as its old value and restores that on exit:

thread A: old=True,  flag=False
thread B: old=False, flag=False     <- captured A's temporary value
thread A: flag=old -> True
thread B: flag=old -> False         <- stuck off for the rest of the kernel

That latch is why the failure is permanent rather than intermittent, and why it appears "in the middle" of a run rather than at the first plot.

The fix

One guard cell after the HoloViews setup cell, re-doing the coercion so rendering no longer depends on the flag:

if not getattr(ElementPlot._update_title, "_coerces_str_title", False):
    _hv_update_title = ElementPlot._update_title

    def _update_title(self, key, plot, element):
        if isinstance(plot.title, str):
            plot.title = Title(text=plot.title)
        return _hv_update_title(self, key, plot, element)

    _update_title._coerces_str_title = True
    ElementPlot._update_title = _update_title

Verification

Executed the cell's source verbatim out of the committed notebook, against a background thread toggling the flag the way the dashboard does:

renders raising AttributeError
without the cell 135 / 150
with the cell 0 / 150

Also confirmed: titles render as real Title objects with the correct text (not just non-crashing); re-running the cell is a no-op rather than stacking another wrapper; and a kernel that has already latched the flag off renders correctly once the cell runs. scripts/validate_notebooks.py passes on all 18 notebooks.

Scope and follow-ups

Deliberately scoped to the workshop notebook. notebooks/pipeline/pipeline.ipynb and notebooks/pipeline_groundtruth/pipeline_groundtruth.ipynb share the exposure (in-process LocalCluster plus titled plots) and are untouched here.

For anyone hitting this in a running kernel, it can be unstuck in place without a restart:

from bokeh.core.property.bases import Property
Property._should_validate = True

The real defect is upstream — Bokeh's validate() mutating a process-global from multiple threads — and is worth reporting there.

🤖 Generated with Claude Code

Partway through the pipeline, every cell that draws a titled HoloViews plot
starts failing with `'str' object has no attribute 'update'`, and the kernel
never recovers on its own — `visualize_seeds(max_proj, seeds, "mask_pnr")` and
everything after it stays broken until a restart.

HoloViews builds each figure with a plain-string title
(`bokeh.plotting.figure(title=...)`), and Bokeh turns that into a `Title` model
only through the coercion registered on `Plot.title`
(`.accepts(String, lambda text: Title(text=text))`). That coercion lives inside
the validation branch of `Property.prepare_value`, so when validation is off the
figure keeps a raw `str` and the later `plot.title.update(**props)` in
`ElementPlot._update_title` raises.

Validation is gated by `Property._should_validate`, a process-global that
Bokeh's `validate()` saves and restores with no lock and no thread-locality.
This notebook runs two independent togglers in one process: HoloViews itself,
which wraps every datasource update in `validate(False)`, and the in-process
dask scheduler's dashboard, whose callbacks are decorated with
`@without_property_validation` and run on its own IO loop thread. When the two
scopes overlap, the second captures the first's temporary `False` as its "old"
value and restores that on exit, latching validation off for the rest of the
kernel. That latch is why the failure is permanent rather than intermittent.

Re-do the coercion in `_update_title` so rendering no longer depends on the
flag. Verified against a thread toggling the flag the way the dashboard does:
135/150 renders raised before, 0/150 after, titles intact. Re-running the cell
is a no-op rather than stacking another wrapper, and a kernel that has already
latched the flag off renders correctly once the cell runs.

`notebooks/pipeline/pipeline.ipynb` and
`notebooks/pipeline_groundtruth/pipeline_groundtruth.ipynb` share the exposure
(in-process `LocalCluster` plus titled plots); this change is deliberately
scoped to the workshop notebook. The root cause belongs upstream in Bokeh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daharoni
daharoni merged commit 51e0e26 into main Aug 2, 2026
4 checks passed
@daharoni
daharoni deleted the fix/bokeh-str-title-with-dask-dashboard branch August 2, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant