# Custom button to hide all plot lines

**URL:** <https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677>\
**Category:** Community Support\
**Created:** [December 2, 2025, 8:40am UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677 "2025-12-02T08:40:38Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![jzimmer](https://discourse.bokeh.org/letter_avatar_proxy/v4/letter/j/ecb155/32.png) [@jzimmer](https://discourse.bokeh.org/u/jzimmer)\
**Post date:** [December 2, 2025, 8:40am UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/1 "2025-12-02T08:40:39Z")

</div>

My problem is identical to the one discussed here:

> [@Posssible to hide all lines in my plot and then show them again one by one?](https://discourse.bokeh.org/t/posssible-to-hide-all-lines-in-my-plot-and-then-show-them-again-one-by-one/12213):
>
> Hi! I am very new to Bokeh and I am exploring it as an alternative to Plotly interactive graphs. I am running into a small issue / question when trying to use the interactive legends: in general, i work with plots that show 35 lines at the same time, so it is very common that i need to hide all and only show one or two at a time. This is quite easy to do with Plotly just by double clicking, but I cannot figure out how to do it on Bokeh - and haven’t found any answers online either. This is som…

I think I used the solution from above for my own plots. Now I am trying to upgrade from bokeh 2.x to bokeh 3.x and this does not work anymore. The original solution from the linked post also does not work in bokeh 3.8.1.

Here is my own example code to test both versions:

```py
"""I often produce plots that show many lines. I need a way to first hide
all of them, so I can re-enable only those I actually want to look at.
The custom "hide" button allowed that in bokeh 2.x, but does not work
anymore in bokeh 3.8.
It still hides the lines, but the muting of the legend entries is not
synced anymore, which makes it kind of pointless.
Explicitly hiding the legend items makes the whole legend disappear,
which is not the desired effect.

Going click_policy="mute" + muted_alpha=0 gives the same result
as click_policy="hide"
"""
import numpy as np
from numpy import pi, sin, cos
import bokeh
from bokeh.layouts import row, column
from bokeh.models.widgets import Div
from bokeh.models import ColumnDataSource, Button, CustomJS
from bokeh.plotting import figure, show
from packaging.version import parse

x = np.linspace(-2*pi, 2*pi, 400)
y = sin(x)
y2 = cos(x)

source = ColumnDataSource(data=dict(x=x, y=y, y2=y2))

p = figure(title="Test", height=300,
           sizing_mode='stretch_width',
           )
r1 = p.line('x', 'y', source=source, legend_label="Sin", color='blue', muted_alpha=0)
r2 = p.line('x', 'y2', source=source, legend_label="Cos", color='red', muted_alpha=0)
p.yaxis.axis_label = 'y-axis Label [unit]'
p.legend.location = "top_left"
# p.legend.click_policy = "hide" # clickable legend items
p.legend.click_policy = "mute" # clickable legend items

renderers = [r1, r2]

# Create a button to hide/show all lines
button = Button(label="Hide", button_type="default", height=50, width=50,
                sizing_mode='fixed')

# Working callback for Bokeh 2.x
callback_2 = CustomJS(args=dict(lines=renderers, button=button),
                      code="""
    let isVisible = button.label == 'Hide'
    for (let i = 0; i < lines.length; i++) {
        lines[i].visible = !isVisible;
    }

    button.label = isVisible ? 'Show' : 'Hide';
""")

# Broken callback for Bokeh 3.x
callback_3 = CustomJS(args=dict(lines=renderers, button=button,
                                items=p.legend[0].items),
                      code="""
    let isVisible = button.label == 'Hide'
    for (let i = 0; i < lines.length; i++) {
        lines[i].visible = !isVisible;
    }
    // Hiding the legend entries manually makes it disappear completely
    for (let i = 0; i < items.length; i++) {
        items[i].visible = !isVisible;
    }

    button.label = isVisible ? 'Show' : 'Hide';
""")

if parse(bokeh. __version__ ) < parse("3.0"):
    button.js_on_click(callback_2)
else:
    button.js_on_click(callback_3)

text = Div(
    text="The button should hide the lines, while only muting their "
    "legend items (as if all legend items were clicked). "
    f"Bokeh: {bokeh. __version__ }",
    height=15, sizing_mode='stretch_width')

page_row = column([text,
                   row([button, p], sizing_mode='stretch_width')],
                  sizing_mode='stretch_width'
                  )

show(page_row)

```

 ![image](https://discourse-uploads.bokeh.org/original/2X/9/9271f299c679b72ba4546ac326ee117fae271fd5.png)

---

<div class="post-metadata">

**Author:** ![gmerritt123](https://discourse.bokeh.org/user_avatar/discourse.bokeh.org/gmerritt123/32/4101_2.png) [@gmerritt123](https://discourse.bokeh.org/u/gmerritt123)\
**Post date:** [December 2, 2025, 2:15pm UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/2 "2025-12-02T14:15:12Z")

</div>

Here’s what I use in my codebase:

```auto
def genVisButton(models,visible_label,invisible_label,initial='visible'):
    '''function to create a button that can toggle visibility of a single bokeh model, or a list of bokeh models that have a visible property
    initial can be visible or invisible for initial state of model'''
    if type(models) != list:
        models = [models]
    if initial=='visible':
        for m in models:
            m.visible=True
        btn = Button(label=visible_label)
    else:
        for m in models:
            m.visible=False
        btn = Button(label=invisible_label)
    cb = CustomJS(args=dict(btn=btn,mdls=models,vlbl=visible_label,ivlbl = invisible_label)
                    ,code='''
                    if (btn.label == vlbl){
                        for (var i=0;i<mdls.length;i++){
                            mdls[i].visible = false
                            }
                        btn.label = ivlbl
                        }
                    else {
                        for (var i=0;i<mdls.length;i++){
                            mdls[i].visible = true
                            }
                        btn.label = vlbl
                    }
                    ''')
    btn.js_on_click(cb)
    return btn

```

so spliced into your example –\>

```auto
import numpy as np
from numpy import pi, sin, cos
import bokeh
from bokeh.layouts import row, column
from bokeh.models.widgets import Div
from bokeh.models import ColumnDataSource, Button, CustomJS
from bokeh.plotting import figure, show
from packaging.version import parse

def genVisButton(models,visible_label,invisible_label,initial='visible'):
    '''function to create a button that can toggle visibility of a single bokeh model, or a list of bokeh models that have a visible property
    initial can be visible or invisible for initial state of model'''
    if type(models) != list:
        models = [models]
    if initial=='visible':
        for m in models:
            m.visible=True
        btn = Button(label=visible_label)
    else:
        for m in models:
            m.visible=False
        btn = Button(label=invisible_label)
    cb = CustomJS(args=dict(btn=btn,mdls=models,vlbl=visible_label,ivlbl = invisible_label)
                    ,code='''
                    if (btn.label == vlbl){
                        for (var i=0;i<mdls.length;i++){
                            mdls[i].visible = false
                            }
                        btn.label = ivlbl
                        }
                    else {
                        for (var i=0;i<mdls.length;i++){
                            mdls[i].visible = true
                            }
                        btn.label = vlbl
                    }
                    ''')
    btn.js_on_click(cb)
    return btn
 
x = np.linspace(-2*pi, 2*pi, 400)
y = sin(x)
y2 = cos(x)

source = ColumnDataSource(data=dict(x=x, y=y, y2=y2))

p = figure(title="Test", height=300,
           sizing_mode='stretch_width',
           )
r1 = p.line('x', 'y', source=source, legend_label="Sin", color='blue', muted_alpha=0)
r2 = p.line('x', 'y2', source=source, legend_label="Cos", color='red', muted_alpha=0)
p.yaxis.axis_label = 'y-axis Label [unit]'
p.legend.location = "top_left"
# p.legend.click_policy = "hide" # clickable legend items
p.legend.click_policy = "mute" # clickable legend items

renderers = [r1, r2]

# Create a button to hide/show all lines using my function
vrends = [r1,r2]
button = genVisButton(models=vrends,visible_label='hide',invisible_label='show',initial='visible')

page_row = column([
                   row([button, p], sizing_mode='stretch_width')],
                  sizing_mode='stretch_width'
                  )

show(page_row

```

If you want the legend to follow it, you could probably expand the function a bit. Happy to help with that if you give it a go.

---

<div class="post-metadata">

**Author:** ![jzimmer](https://discourse.bokeh.org/letter_avatar_proxy/v4/letter/j/ecb155/32.png) [@jzimmer](https://discourse.bokeh.org/u/jzimmer)\
**Post date:** [December 2, 2025, 4:51pm UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/3 "2025-12-02T16:51:39Z")

</div>

I really appreciate you taking the time to apply your code to my problem. However, while it seems more flexible, in the end it seems to behave the same way my code did before.

What your post did do is prompt me to go through different bokeh versions and compare them. And I could determine that bokeh v3.6.3 is the last version that behaves as I was expecting, i.e. muting the text in the legend when the plot lines are hidden. This is broken in bokeh 3.7.0.

I think I can how write a GitHub issue, because to me this seems like a regression. 🙂

---

<div class="post-metadata">

**Author:** ![gmerritt123](https://discourse.bokeh.org/user_avatar/discourse.bokeh.org/gmerritt123/32/4101_2.png) [@gmerritt123](https://discourse.bokeh.org/u/gmerritt123)\
**Post date:** [December 2, 2025, 7:28pm UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/4 "2025-12-02T19:28:45Z")

</div>

Ah, yes, I’m currently still on bokeh 3.4.2 in my codebase. Post the issue here when you make it so I can monitor/follow 😃

---

<div class="post-metadata">

**Author:** ![jzimmer](https://discourse.bokeh.org/letter_avatar_proxy/v4/letter/j/ecb155/32.png) [@jzimmer](https://discourse.bokeh.org/u/jzimmer)\
**Post date:** [December 3, 2025, 12:43pm UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/5 "2025-12-03T12:43:29Z")

</div>

I opened the following issue:

> <https://github.com/bokeh/bokeh/issues/14741>
>
> \### Software versions
> 
> Python version : 3.12.6 | packaged by conda-forge… | (main, Sep 22 2024, 14:01:26) \[MSC v.1941 64 bit (AMD64)\]
> IPython version : 8.37.0
> Tornado version : 6.4.1
> NumPy version : 1.26.4
> Bokeh version : 3.8.1
> BokehJS static path : C:\\Users\\Nettelstroth\\anaconda3\\envs\\bokeh3\\Lib\\site-packages\\bokeh\\server\\static
> node.js version : (not installed)
> npm version : (not installed)
> jupyter\_bokeh version : (not installed)
> Operating system : Windows-11-10.0.26200-SP0
> 
> \### Browser name and version
> 
> firefox, chrome
> 
> \### Jupyter notebook / Jupyter Lab version
> 
> \_No response\_
> 
> \### Expected behavior
> 
> The following callback should allow producing a button that hides all lines in a plot, while muting their legend entries. 
> 
> \`\`\`python
> callback\_a = CustomJS(args=dict(lines=renderers, button=button),
> code="""
> let isVisible = button.label == 'Hide'
> for (let i = 0; i \< lines.length; i++) {
> lines\[i\].visible = !isVisible;
> }
> button.label = isVisible ? 'Show' : 'Hide';
> """)
> \`\`\`
> Just like \`\`p.legend.click\_policy = "hide"\`\` behaves, but toggeling all items at once.
> This worked fine until bokeh v3.6.3.
> 
> \### Observed behavior
> 
> In bokeh v3.7.0 and later the callback does not affect the legend anymore. Only the plot lines are hidden.
> 
> See these discussions for the original solution
> https://discourse.bokeh.org/t/posssible-to-hide-all-lines-in-my-plot-and-then-show-them-again-one-by-one/12213
> ...and the current issue:
> https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677
> 
> \### Example code
> 
> \`\`\`Python
> """I often produce plots that show many lines. I need a way to first hide
> all of them, so I can re-enable only those I actually want to look at.
> The custom "hide" button allowed that in bokeh v3.6.3, but does not work
> anymore in bokeh 3.7.0.
> It still hides the lines, but the muting of the legend entries is not
> synced anymore, which makes it kind of pointless.
> Explicitly hiding the legend items makes the whole legend disappear,
> which is not the desired effect.
> 
> Going click\_policy="mute" + muted\_alpha=0 gives the same result
> as click\_policy="hide"
> 
> https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677
> """
> import numpy as np
> from numpy import pi, sin, cos
> import bokeh
> from bokeh.layouts import row, column
> from bokeh.models.widgets import Div
> from bokeh.models import ColumnDataSource, Button, CustomJS
> from bokeh.plotting import figure, show
> from packaging.version import parse
> 
> x = np.linspace(-2\*pi, 2\*pi, 400)
> y = sin(x)
> y2 = cos(x)
> 
> source = ColumnDataSource(data=dict(x=x, y=y, y2=y2))
> 
> p = figure(title="Test", height=300,
> sizing\_mode='stretch\_width',
> )
> r1 = p.line('x', 'y', source=source, legend\_label="Sin", color='blue', muted\_alpha=0)
> r2 = p.line('x', 'y2', source=source, legend\_label="Cos", color='red', muted\_alpha=0)
> p.yaxis.axis\_label = 'y-axis Label \[unit\]'
> p.legend.location = "top\_left"
> p.legend.click\_policy = "hide" # clickable legend items
> \# p.legend.click\_policy = "mute" # clickable legend items
> 
> renderers = \[r1, r2\]
> 
> \# Create a button to hide/show all lines
> button = Button(label="Hide", button\_type="default", height=50, width=50,
> sizing\_mode='fixed')
> 
> \# Working callback for Bokeh 2.x
> callback\_a = CustomJS(args=dict(lines=renderers, button=button),
> code="""
> let isVisible = button.label == 'Hide'
> for (let i = 0; i \< lines.length; i++) {
> lines\[i\].visible = !isVisible;
> }
> 
> button.label = isVisible ? 'Show' : 'Hide';
> """)
> 
> \# Tests for for Bokeh 3.7.0
> callback\_b = CustomJS(args=dict(lines=renderers, button=button,
> items=p.legend\[0\].items),
> code="""
> let isVisible = button.label == 'Hide'
> for (let i = 0; i \< lines.length; i++) {
> lines\[i\].visible = !isVisible;
> }
> // Hiding the legend entries manually makes it disappear completely
> for (let i = 0; i \< items.length; i++) {
> items\[i\].visible = !isVisible;
> }
> 
> button.label = isVisible ? 'Show' : 'Hide';
> """)
> 
> if parse(bokeh.\_\_version\_\_) \< parse("3.7"):
> button.js\_on\_click(callback\_a)
> else:
> button.js\_on\_click(callback\_a)
> 
> text = Div(
> text="The button should hide the lines, while only muting their "
> "legend items (as if all legend items were clicked). "
> f"Bokeh: {bokeh.\_\_version\_\_}",
> height=15, sizing\_mode='stretch\_width')
> 
> page\_row = column(\[text,
> row(\[button, p\], sizing\_mode='stretch\_width')\],
> sizing\_mode='stretch\_width'
> )
> 
> show(page\_row)
> \`\`\`
> 
> \### Stack traceback or browser console output
> 
> \_No response\_
> 
> \### Screenshots
> 
> The discourse topic has a nice video of the desired effect:
> https://discourse.bokeh.org/t/posssible-to-hide-all-lines-in-my-plot-and-then-show-them-again-one-by-one/12213/2
> Video:
> https://discourse-uploads.bokeh.org/original/2X/7/7336fcbdc9f96cd6b23f6791efbbd089ebc6d43c.mp4

---

<div class="post-metadata">

**Author:** ![system](https://discourse-uploads.bokeh.org/original/2X/1/14eb24f9e10e4e9b78e3f10f66c04ffa96446e5a.svg) [@system](https://discourse.bokeh.org/u/system)\
**Post date:** [March 3, 2026, 12:43pm UTC](https://discourse.bokeh.org/t/custom-button-to-hide-all-plot-lines/12677/6 "2026-03-03T12:43:56Z")

</div>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.
