Show only major category in bokeh legend

Viewed 944

I'd like to have bokeh display a legend for categorical bar chart data, but control which level of category is shown in the legend.

e.g. using the bokeh sample code below, I'd like the legend to show only the years. So "2015", "2016", "2017", instead of the current "Apples, 2015" etc.

Additionally I'm trying to hide the years on display on the x axis, so it only shows the fruits.

I've searched through bokeh's documentation for a while but can't see how to do this. I suppose I need to set the legend attribute to some sort of format string when creating the vbar but I have no idea what formats are allowed. What's the proper way to do this?

from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import figure
from bokeh.transform import factor_cmap

output_file("bars.html")

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ['2015', '2016', '2017']

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 3, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

# this creates [ ("Apples", "2015"), ("Apples", "2016"), ("Apples", "2017"), ("Pears", "2015), ... ]
x = [ (fruit, year) for fruit in fruits for year in years ]
counts = sum(zip(data['2015'], data['2016'], data['2017']), ()) # like an hstack

source = ColumnDataSource(data=dict(x=x, counts=counts))

p = figure(x_range=FactorRange(*x), plot_height=250, title="Fruit Counts by Year",
           toolbar_location=None, tools="")

palette = ["Red", "Green", "Blue"]
#p.vbar(x='x', top='counts', width=0.9, source=source)
p.vbar(x='x', top='counts', width=0.9, source=source, line_color="white",
       fill_color=factor_cmap('x', palette=palette, factors=years, start=1, end=2),
       # legend='x[0]'
       legend='x',
      )


p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
p.legend.location = "top_right"

show(p)
3 Answers

This can be accomplished by adding another column to the ColumnDataSource which maps to the category level of interest. In question code, add the following to the ColumnDataSource:

legend_names = [level1 for level0, level1 in x]
source = ColumnDataSource(data=dict(x=x, counts=counts, legend_name=legend_names))

Then change the vbar to use that field as the legend (as opposed to the normal legend parameter):

p.vbar(<other arguments>, legend_field='legend_name')

This makes it easy to choose any level to use as the legend.

Result: enter image description here

Full code:

from bokeh.io import show
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import Figure
from bokeh.transform import factor_cmap

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ['2015', '2016', '2017']

data = {'fruits': fruits,
        '2015': [2, 1, 4, 3, 2, 4],
        '2016': [5, 3, 3, 2, 4, 6],
        '2017': [3, 2, 4, 4, 5, 3]}

x = [(fruit, year) for fruit in fruits for year in years]
counts = sum(zip(data['2015'], data['2016'], data['2017']), ())
legend_names = [level1 for level0, level1 in x]
source = ColumnDataSource(data=dict(x=x, counts=counts, legend_name=legend_names))

figure = Figure(x_range=FactorRange(*x), plot_height=250, title="Fruit Counts by Year",
                toolbar_location=None, tools="")

palette = ["Red", "Green", "Blue"]
figure.vbar(x='x', top='counts', width=0.9, source=source, line_color="white",
            fill_color=factor_cmap('x', palette=palette, factors=years, start=1, end=2),
            legend_field='legend_name')

figure.y_range.start = 0
figure.x_range.range_padding = 0.1
figure.xaxis.major_label_orientation = 1
figure.xgrid.grid_line_color = None
figure.legend.location = "top_right"

show(figure)

If you don't want the hierarchical axis labeling, then you will need to use the method decribed in the Visual Dodge section of the Handling Categorical Data chapter of the User's Guide.

Unless I am mistaken, the example there is exactly what you are asking:

from bokeh.core.properties import value
from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.transform import dodge

output_file("dodged_bars.html")

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ['2015', '2016', '2017']

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 3, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

source = ColumnDataSource(data=data)

p = figure(x_range=fruits, y_range=(0, 10), plot_height=250, 
           title="Fruit Counts by Year", toolbar_location=None, tools="")

p.vbar(x=dodge('fruits', -0.25, range=p.x_range), top='2015', width=0.2, 
       source=source, color="#c9d9d3", legend=value("2015"))

p.vbar(x=dodge('fruits',  0.0,  range=p.x_range), top='2016', width=0.2, 
       source=source, color="#718dbf", legend=value("2016"))

p.vbar(x=dodge('fruits',  0.25, range=p.x_range), top='2017', width=0.2, 
       source=source, color="#e84d60", legend=value("2017"))

p.x_range.range_padding = 0.1
p.xgrid.grid_line_color = None
p.legend.location = "top_left"
p.legend.orientation = "horizontal"

show(p)

enter image description here

Here's some quick code to build the chart dynamically. Define increment separately, then picking up from figure above:

def incrementer(labels, base_inc=0.25):
    num_labels = len(labels)
    midpoint = num_labels / 2 if num_labels % 2 == 0 else num_labels / 2 - 0.5
    offset = base_inc * midpoint
    even_offset = base_inc / 2 if num_labels % 2 == 0 else 0

    return [i * base_inc - offset + even_offset for i in range(num_labels)]

..... 


from bokeh.palettes import Category20

palette = Category20[20]
increments = incrementer(regions, base_inc=0.125)
for i, region in enumerate(regions):
    p.vbar(
        x=dodge('x', increments[i], range=p.x_range), top=region, width=0.1, 
           source=source, color=palette[i], legend_label=region
        )
Related