☰Read-only archive⌕
Looking for help with Bokeh?

This is a read-only archive. Ask questions and join the conversation on GitHub.

Visit Community Support on GitHub →
Archived discussions · Captured October 28, 2026

Wheel Zoom Tool and Categorical Axis Range

Community Support
gzang
gzang

Hello! I am trying to add a wheel zoom tool to my bokeh boxplot, but for some reason I can’t get the range to be constrained. I’m using the bokeh documentation with the cars as an example:

p.x_range = FactorRange(factors = kinds, bounds = 'auto') 
wheel_zoom = WheelZoomTool(maintain_focus=False)
pan = PanTool()
p.add_tools(wheel_zoom, pan)
p.toolbar.active_scroll = wheel_zoom
p.toolbar.active_drag = pan
p.add_tools(ResetTool())

Even though I’ve set the bounds to be automatically assigned according to the labels, I am still able to zoom out beyond the range of the categorical axis. Is there something I can do to constrain the range of the x-axis between the first label and the last label when using Wheel Zoom? Thanks!

Here is the complete code for reproducibility

import pandas as pd

from bokeh.models import ColumnDataSource, Whisker, WheelZoomTool, ResetTool, PanTool, FactorRange
from bokeh.plotting import figure, show
from bokeh.sampledata.autompg2 import autompg2
from bokeh.transform import factor_cmap

df = autompg2[["class", "hwy"]].rename(columns={"class": "kind"})

kinds = df.kind.unique()

# compute quantiles
grouper = df.groupby("kind")
qs = grouper.hwy.quantile([0.25, 0.5, 0.75]).unstack().reset_index()
qs.columns = ["kind", "q1", "q2", "q3"]

# compute IQR outlier bounds
iqr = qs.q3 - qs.q1
qs["upper"] = qs.q3 + 1.5*iqr
qs["lower"] = qs.q1 - 1.5*iqr

# update the whiskers to actual data points
for kind, group in grouper:
    qs_idx = qs.query(f"kind=={kind!r}").index[0]
    data = group["hwy"]

    # the upper whisker is the maximum between p3 and upper
    q3 = qs.loc[qs_idx, "q3"]
    upper = qs.loc[qs_idx, "upper"]
    wiskhi = group[(q3 <= data) & (data <= upper)]["hwy"]
    qs.loc[qs_idx, "upper"] = q3 if len(wiskhi) == 0 else wiskhi.max()

    # the lower whisker is the minimum between q1 and lower
    q1 = qs.loc[qs_idx, "q1"]
    lower = qs.loc[qs_idx, "lower"]
    wisklo = group[(lower <= data) & (data<= q1)]["hwy"]
    qs.loc[qs_idx, "lower"] = q1 if len(wisklo) == 0 else wisklo.min()

df = pd.merge(df, qs, on="kind", how="left")

source = ColumnDataSource(qs)

p = figure(x_range=kinds, tools="", toolbar_location=None,
           title="Highway MPG distribution by vehicle class",
           background_fill_color="#eaefef", y_axis_label="MPG")

# outlier range
whisker = Whisker(base="kind", upper="upper", lower="lower", source=source)
whisker.upper_head.size = whisker.lower_head.size = 20
p.add_layout(whisker)

# quantile boxes
cmap = factor_cmap("kind", "TolRainbow7", kinds)
p.vbar("kind", 0.7, "q2", "q3", source=source, color=cmap, line_color="black")
p.vbar("kind", 0.7, "q1", "q2", source=source, color=cmap, line_color="black")

# outliers
outliers = df[~df.hwy.between(df.lower, df.upper)]
p.scatter("kind", "hwy", source=outliers, size=6, color="black", alpha=0.3)

p.xgrid.grid_line_color = None
p.axis.major_label_text_font_size="14px"
p.axis.axis_label_text_font_size="12px"

p.toolbar_location = None

p.x_range = FactorRange(factors = kinds, bounds = 'auto') 
wheel_zoom = WheelZoomTool(maintain_focus=False)
pan = PanTool()
p.add_tools(wheel_zoom, pan)
p.toolbar.active_scroll = wheel_zoom
p.toolbar.active_drag = pan
p.add_tools(ResetTool())

show(p)
⌁   Link to post
Bryan
Bryan

I’ve gone ahead and triaged the issue. But I should be clear that Bokeh is a volunteer OSS project, and outside occasional officially funded and scoped grant work, any “escalation” is purely up to individual contributors’ personal available bandwidth and interest.

⌁   Link to post

End of archived discussion · Replies are disabled