1

I'm trying to create a dashboard in the Jupyter Notebook with an interactive bokeh plot and ipywidgets as widgets for controlling the plot. My use-case is a little bit complicated because I want to create a plot with different markers that designate different groups in the scatter plot, and I want to enable the user to change the division of the groups (thus changing the markers displayed in the plot).

I've considered two different approaches:

  1. Use the chart API of bokeh to generate the scatter plot with the different markers. Unless I'm mistaken, this will not allow me to update the plot, rather I will have to recreate it with every change coming from the user.

  2. Create the plot myself using the bokeh.plotting API. For this I will have to segment the data into groups myself and update the coordinates of the data myself for each group of markers separately.

I've started experimenting with the second option since I want to try to update the plot myself instead of re-creating it. I created a simple example, which already presents some problems for me. Here is the code:

p = figure()
s = widgets.Dropdown(options=['circle', 'square'])

x = np.random.normal(size=10)
y = np.random.normal(size=10)

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

r = p.scatter(source=data, x='x', y='y', size=14)

def select_value(change):
    renderers = [x for x in p.renderers if x.__view_model__ == "GlyphRenderer"]
    for r in renderers:
        p.renderers.remove(r)
    if change['new'] == 'circle':
        r = p.circle(x, y, size=14)
    else:
        r = p.square(x, y, size=14)
    push_notebook()


s.observe(select_value, 'value')
display(s)
show(p, notebook_handle=True)

This code block already doesn't really work. When I select a 'square' from the widget, the circles are removed, but only one square is added in the middle of the plot, rather than in the previous positions of the circles. I tried using the p.add_glyph function which yielded the same results, as well as making sure the data source is the same for the square as it is for the circles (it is).

Any help would be greatly appreciated! Cheers, Omri

1 Answers1

0

First, I couldn't make widgets.Dropdown to work with options, I used bokeh.models.widgets.Select. It might be a bokeh version, I'm using 0.12.4 version. Second, in the select_value update function I add glyphs, use bokeh.io.push_notebook(), then remove the original renderers, and again bokeh.io.push_notebook()

In one of the cells I have:

import bokeh
import bokeh.plotting
import numpy as np
from ipywidgets import interact
bokeh.io.output_notebook()

p = bokeh.plotting.figure()
s = bokeh.models.widgets.Select(options=['circle', 'square'])

x = np.random.normal(size=10)
y = np.random.normal(size=10)

source = bokeh.models.ColumnDataSource(dict(x=x, y=y))

scatter_plot = p.scatter(source=source, x='x', y='y',color="blue",marker="circle", size=14)

def select_value(change):
    renderers = [x for x in p.renderers if x.__view_model__ == "GlyphRenderer"]
    for r in renderers:
        if change == 'circle':
            myglyph = bokeh.models.Circle(x="x",y="y",fill_color="blue", line_color="blue",size=14)
        elif change == 'square':
            myglyph = bokeh.models.Square(x="x",y="y",fill_color="blue", line_color="blue",size=14)
        r.glyph = myglyph
        p.add_glyph(source,myglyph)
        bokeh.io.push_notebook()
        p.renderers.remove(r)
        bokeh.io.push_notebook()

bokeh.io.show(p, notebook_handle=True)

which creates the figure:

mainfigure

And in another cell I have:

interact(select_value,change=['circle', 'square']);

which creates the interactive tool:

interactive_tool

UPDATE

An alternative is to play with the visible parameter. You create all the marker types you want, and then you make your choice visible while the others not:

import bokeh
import bokeh.plotting
import numpy as np
from ipywidgets import interact
bokeh.io.output_notebook()

p = bokeh.plotting.figure()
s = bokeh.models.widgets.Select(options=['circle', 'square'])

x = np.random.normal(size=10)
y = np.random.normal(size=10)

source = bokeh.models.ColumnDataSource(dict(x=x, y=y))

scatter_circle = p.scatter(source=source, x='x', y='y',color="blue", marker='circle', size=14)
scatter_square = p.scatter(source=source, x='x', y='y',color="blue", marker='square', size=14)
scatter_square.visible=False
handle = bokeh.io.show(p, notebook_handle=True)

def select_value(change):
    if change == 'circle':
        scatter_circle.visible=True
        scatter_square.visible=False
    elif change == 'square':
        scatter_square.visible=True
        scatter_circle.visible=False

    bokeh.io.push_notebook(handle=handle)
Pablo Reyes
  • 3,073
  • 1
  • 20
  • 30
  • Perfect, thanks! I should have included my imports (I used the ipywidgets.Dropdown widget rather than that from bokeh). – Omri Har-Shemesh Apr 11 '17 at 08:30
  • Can you explain why you assign the glyph to the first renderer and then, after using add_glyph, you remove all previous renderers? This seems a bit random... – Omri Har-Shemesh Apr 11 '17 at 08:45
  • Well, it turned out that in this example `renderers` was a list of only one element, that is why I was indexing it renderers[0]. I've modified my code to have the change inside the for loop, though the loop only executes once. Is the code answering your question? does it work? – Pablo Reyes Apr 11 '17 at 16:52
  • Hi Pablo, yes, it works, thanks. I should have phrased my question more clearly - I understand that there is only one GlyphRenderer in the plot and that's why you used `renderers[0]`. What I don't understand is the procedure of modifying the glyph of an existing renderer, then using `add_glyph` and then disposing of the renderer to which we added the glyph. Do you understand that? The reason I'm asking is because in my use-case I plan to have many renderers and will have to keep track of all of them. – Omri Har-Shemesh Apr 11 '17 at 18:28
  • Apparently when one creates new glyphs, e.g. with `bokeh.models.Circle` one needs to specify many parameters that are not inherit from the previous glyphs. For instance, the color needs to be specified. And most likely the coordinate reference should be specified again. That is why, when you erase the renderers first, the glyphs appear at the center of the graph. By first adding the glyphs and then remove the previous ones, the coordinates reference is somehow preserved. – Pablo Reyes Apr 11 '17 at 18:39
  • @Omri . An alternative is using the `visible` parameter. See my updated code in my solution above. – Pablo Reyes Apr 11 '17 at 19:47
  • Thanks again @pablo-reyes! Eventually I'm going for a similar solution - create a ColumnDataSource for each type of marker I want, then reuse the markers by changing their data sources, hiding the unwanted ones or creating new ones whenever necessary. I wish there was just a simple interface in bokeh.plotting for this (i.e. like specifying a list of colors, one could specify a list of markers) - would have made my life a lot easier! – Omri Har-Shemesh Apr 12 '17 at 11:10