0

Suppose I have the following bar plot from the documentation:

from bokeh.io import show, output_notebook
from bokeh.plotting import figure
output_notebook()
  • Here is a list of categorical values (or factors)
fruits = [`Apples, Pears`, `Nectarines`, `Plums`, `Grapes`, `Strawberries`]
  • Set the x_range to the list of categories above
p = figure(x_range=fruits, plot_height=250, title=“Fruit Counts”)
  • Categorical values can also be used as coordinates
p.vbar(x=fruits, top=[5, 3, 4, 2, 4, 6], width=0.9)
  • Set some properties to make the plot look better
p.xgrid.grid_line_color = None
p.y_range.start = 0
show(p)

How do I set it so that the initial zoom shows only the first four categories (but the user can pan around to see the other two, zoom out, etc.)?

I tried modifying the x_range values, but it looks like x_range needs to equal fruits.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
mang
  • 1

2 Answers2

0

One option I'm thinking of is to add a slider that enables to filter rows in your dataframe.

You can try this code in a jupyter notebook:

from bokeh.io import show, output_notebook
from bokeh.models import ColumnDataSource, CustomJS, Slider, DataRange1d
from bokeh.plotting import figure
import pandas as pd
from bokeh.layouts import column
output_notebook()

def create_plots(doc):
    
    # Create dataframe
    df = pd.DataFrame({
        'fruits': ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries'],
        'top': [5, 3, 4, 2, 4, 6]
    })
    fruits=list(df['fruits'])
    max_top = max(df['top'])
    
    # Create Column Data Source
    source = ColumnDataSource(data=df)
    
    # Create figure
    p = figure(x_range=fruits, plot_height=250, title='Fruit Counts',
               y_range=DataRange1d(0, max_top + 1, only_visible=True),  # with DataRange1d + only_visible=True, the 
               # y scale will be updated automatically depending on the data displayed on the chart
              )
    p.vbar(x='fruits', top='top', width=0.9, source=source)

    p.xgrid.grid_line_color = None
    p.y_range.start = 0

    # Create slider widget
    slider = Slider(start=1, end=max_top, value=max_top, step=1, title="Slider example - current value")
    
    # Update chart with widget value
    slider.js_on_change("value", CustomJS(code="""
        console.log('slider: value=' + this.value, this.toString())
    """))  # I'm not comfortable with javascript but this code can be found in Bokeh documentation: 
    # https://docs.bokeh.org/en/latest/docs/user_guide/interaction/widgets.html#slider

    def update_data(df, max_index):
        # update the Column Data Source with the filtered data
        df = df[df.index <= max_index]
        source.data = df
        
        # update the x axis - hidden fruits should not be displayed on the x axis
        p.x_range.factors = list(df['fruits'])

    def update(attr, old, new):
        max_index_to_display = slider.value - 1
        update_data(df, max_index_to_display)

    slider.on_change('value', update) # call the update funtion everytime the slider is clicked on

    # Layout
    doc.add_root(column(slider, p))

show(create_plots)
Juliette B
  • 186
  • 1
  • 5
0

It is not possible to use a CategoricalAxis with a FactorRange and set the x_range because start and end are readonly properties.

But there are workarounds, two are listed below.

using CustomJSTickFormatter

In the bokeh version 3.0.0 a CustomJSTickFormatter was added. With this it is possible to use a normal LinearRange and format the ticks. With this approach you can set x_range as you like it.

from bokeh.models import CustomJSTickFormatter, Range1d
from bokeh.plotting import figure, show, output_notebook
output_notebook()

fruits = ["Apples", "Pears", "Nectarines", "Plums", "Grapes", "Strawberries"]

x = list(range(len(fruits)))
y = [5, 3, 4, 2, 4, 6]

p = figure(height=250, title="Fruit Counts")
p.x_range = Range1d(-0.5, 3.5, bounds=(-0.5, 4.5))
p.y_range = Range1d(0, 7)
p.vbar(x=x, top=y, width=0.9)
p.xgrid.grid_line_color = None
p.xaxis.formatter = CustomJSTickFormatter(
    code="""
    var new_tick=  tick.toFixed(2)
    const fruits = ["Apples", "Pears", "Nectarines", "Plums", "Grapes", "Strawberries"]
    const range = [...Array(fruits.length).keys()]
    if( tick in range )
        new_tick= fruits[tick]
    else
        new_tick= ''
    return new_tick
    """
)
p.xaxis.minor_tick_line_color = None
p.xaxis.axis_label_text_color  = None

show(p)

using FixedTicker and major_label_overrides

With a FixedTicker you can set the ticks to you wanted positions and major_label_overrides offers you the opportunity to set the label text very precise. This is also available before the 3.0.0 release.

from bokeh.models import FixedTicker, Range1d
from bokeh.plotting import figure, show, output_notebook
output_notebook()

fruits = ["Apples", "Pears", "Nectarines", "Plums", "Grapes", "Strawberries"]

x = list(range(len(fruits)))
y = [5, 3, 4, 2, 4, 6]

p = figure(height=250, title="Fruit Counts")
p.x_range = Range1d(-0.5, 3.5, bounds=(-0.5, 4.5))
p.y_range = Range1d(0, 7)
p.vbar(x=x, top=y, width=0.9)
p.xgrid.grid_line_color = None
p.xaxis.ticker = FixedTicker(
    desired_num_ticks=len(fruits),
    num_minor_ticks = 0,
    ticks=x
)
p.xaxis.major_label_overrides = {i: label for i, label in zip(x, fruits)}

show(p)

The visual results should be the same and the inital view looks like below:

initial view

mosc9575
  • 5,618
  • 2
  • 9
  • 32
  • Unfortunately I have to use an earlier version of Bokeh, which doesn't support CustomJStickFormatter. – mang Jul 21 '23 at 17:16