Bokeh plot auto zooming problem

[The following problem is accidentally put into the Development thread. Sorry for confusing. This is a problem on how to use bokeh correctly]
So please ignoring the logic of the code, but every time the update callback update_plot is called, the plot automatically zoom, and I have no idea why it does this. If I navigate to a different url and click back to the bokeh rendering page, the auto zooming effect is gone. This problem will not arise if I use glyph and Line class. It only arise when there is plot.line() in the callback function. Please help me how to get rid of the auto zooming which is pretty annoying

import numpy as np
import pandas as pd

from bokeh.models import ColumnDataSource, Select, CustomJS, TextInput, Legend, LegendItem
from bokeh.plotting import figure
from bokeh.layouts import row
from bokeh.io import curdoc,show
from bokeh.models.glyphs import Line

doc = curdoc()


table = [[1,2,3,4,1],[2,3,4,5,2],[3,4,5,6,3],[4,5,6,7,4]]
df = pd.DataFrame(table, columns=['a','b','c','d','e'])
colors = ["navy", "firebrick", "red", "darkorchid", "hotpink", "black", "pink"]
next_group = 1


source = ColumnDataSource(data = {'x':[], 'y':[]})
plot = figure(width=400, plot_height=300, title=None, tools='pan')

# plot.legend.location = "top_left"
# plot.legend.click_policy="mute"

# menu = Select(options=['ab', 'ac', 'bc','abc','abd','abcd'],
#  value='ab', title='Type')
text_input = TextInput(value="ab", title="Label:")
legends = []


def update_plot(attrname, old, new):
	# choice = menu.value
	global next_group, legends
	source1 = ColumnDataSource(data = {'x':[], 'y':[]})
	choice =  text_input.value
	n = len(df['e'])
	cl = [char for char in choice]
	source1.data['x'] = df['e'].tolist() * len(cl)
	source1.data['y'] = [x for e in cl for x in df[e].tolist()]
	plot.line(x='x', y= 'y', source = source1, legend_label = choice, line_width=2, color=colors[next_group], muted_alpha = 0.1)
	plot.legend.location = "top_left"
	plot.legend.click_policy="mute"
	next_group += 1

text_input.on_change('value', update_plot)

doc.add_root(row(plot, text_input))

HI @www please edit your post to have correctly formatted code. I would love to run the code to see exactly what you are seeing, because I don’t actually know what you mean by “auto-zooming” But the code is currently un-runnable due to missing formatting and also the presence of “fancy” quotes. You can either use triple-backtick fences around the code or else use the GUI formatting button, the </> in the toolbar.

Now I edited it and you can just coy and run the code and see the issue. The command is bokeh serve --show [python file]. Once you get the website to show, you can enter “a” in the textbox and hit enter and then “b” and enter and then “c” and you can find each time you hit enter the plot weirdly zoom. I really appreciate your help.

That is expected behavior, given your code. By default, Bokeh’s ranges auto-scale to encompass all glyphs that are present in a plot. This is typically the behavior most users want by default. You are adding new line glyphs every time your update callback runs, and the ranges are scaling to accommodate them, as they should. But if you don’t want auto-scaling, you can set fixed range extents in a variety of ways, e.g. you can just pass the fixed ranges you want to the call to figure:

p = figure(..., x_range=(0, 10), y_range=(-1, 1))

the problem is more subtle and complicated than you think. It is not just shifting the range, which is a good thing. It is enlarging the picture so that the graph cannot be shown in a whole! fixing the range will stop the graph from shifting but it is still zooming! As shown in the code revised under your suggestion here: (when i navigate to a different website and click back button the zooming effect is gone so I doubt this may due to some browser and bokeh communication issue. does the zooming happen on your browser too? thanks)

import numpy as np
import pandas as pd

from bokeh.models import ColumnDataSource, Select, CustomJS, TextInput, Legend, LegendItem
from bokeh.plotting import figure
from bokeh.layouts import row
from bokeh.io import curdoc,show
from bokeh.models.glyphs import Line

doc = curdoc()


table = [[1,2,3,4,1],[2,3,4,5,2],[3,4,5,6,3],[4,5,6,7,4]]
df = pd.DataFrame(table, columns=['a','b','c','d','e'])
colors = ["navy", "firebrick", "red", "darkorchid", "hotpink", "black", "pink"]
next_group = 1


source = ColumnDataSource(data = {'x':[], 'y':[]})
plot = figure(width=400, plot_height=300, title=None, tools='pan', x_range=(0, 10), y_range=(0, 10))


# plot.legend.location = "top_left"
# plot.legend.click_policy="mute"

# menu = Select(options=['ab', 'ac', 'bc','abc','abd','abcd'],
#  value='ab', title='Type')
text_input = TextInput(value="ab", title="Label:")
legends = []


def update_plot(attrname, old, new):
	# choice = menu.value
	global next_group, legends
	source1 = ColumnDataSource(data = {'x':[], 'y':[]})
	choice =  text_input.value
	n = len(df['e'])
	cl = [char for char in choice]
	source1.data['x'] = df['e'].tolist() * len(cl)
	source1.data['y'] = [x for e in cl for x in df[e].tolist()]
	plot.line(x='x', y= 'y', source = source1, legend_label = choice, line_width=2, color=colors[next_group], muted_alpha = 0.1)
	plot.legend.location = "top_left"
	plot.legend.click_policy="mute"
	next_group += 1

text_input.on_change('value', update_plot)

doc.add_root(row(plot, text_input))

I think you are going to need to make and share a screen grab gif, because I am not seeing anything like what you describe:

All that said, your code is not organized according to best practices. Bokeh works best when you can set up a plot once, up front (i.e. add all the glyphs you will need up front, once), and then subsequently, only update the data sources in order to change the plots. Bokeh data sources are highly optimized for efficiently updating. Almost all the examples in the docs and example folder follow this principle.

Alternatively, things like visibility can be easily toggled. In general, it is always preferable and more efficient to set properties on existing objects in Bokeh document, rather than add and subtract lots of new objects.

Hi. I know that bokeh works best if callbacks only influence the datasource and I am trying best to follow that philosophy. However, how to make the functionality that I can add new lines in the callback and also add the legend corresponding to it. This is the feature that I want to achieve and it seems not possible to achieve only through datasource. If I am not adding lines but dots instead then I already achieved this quite elegantly but with lines it is so difficult. I really appreciate any hint. Thank you so much

It’s not actually clear what exactly you want to accomplish. The UI with a text box is strange in this situation, for instance. If you a fixed set of lines up front that you want to let users to choose from, then an open ended text box seems like a poor choice, and instead you’d want something like the checkboxes in this example:

https://github.com/bokeh/bokeh/blob/master/examples/app/line_on_off.py

So that users are automatically precluded from entering non-sense inputs like is possible in the text box. OTOT maybe you have a text box for a reason, due to some other unstated requirements of the problem. I don’t know enough to advise you at this point.

My main functionality is not to choose from a fixed set of lines, but to let the user plot any number of lines each line with its own legend. So this will let me not to know the total number of lines in advance and need to dynamically create them. And I will also not know what lines the user want to create, since the user can enter “a” or “b” or even “ab” or “abc” in the box to create whatever lines. So this is where I got stuck: to achieve a functionality that user can plot lines dynamically. Since this is later used in the part where user can enter dynamic query to a database and get lines plot about the statistics of the database information, instead of just providing a fixed number of lines that user can choose from. Thanks for any hints.

OK well in this case maybe adding/subtracting renders is the simplest option. IN that case my only comments are:

  • I still don’t know what zooming behavior you are referring to (see my screen grab)

  • you should always update a CDS .data property “at once”, i.e.

    source.data = new_data        # Good
    source.data.update(new_data)  # Also Good
    
    # Not good:
    source.data['foo'] = [...]
    source.data['bar'] = [...]
    

    And the reason for this is that the latter form risks breaking Bokeh’s assumption that all the CDS columns are always all the same length as each other, in between those two assignments (if the new columns lengths are different than the old ones).

Thanks for the second advice. I already encountered the problem of breaking the underlying assumption that CDS column should be the same length. I fixed by always calling source.data = source.data.copy() at the very end.

Here is the gif of my screen: (now you can see how the zooming is annoying and I don’t know how to solve it. Really thanks for any advice or how to achieve my functionality of add lines and corresponding legends dynamically. Thank you so much)

Also, it will be really helpful if you can share some codes or examples or websites on how to add and delete renders properly or do you think my above solution is ok. Though still don’t know how to solve this zooming issue.

@www Well, that’s surely a bug. I have seen something like that before, but only with “svg” or “webgl” backends (which are not as highly developed yet). Are you saying that you are seeing that with the default Canvas backend? If so, can you provide more detailed version info? (OS, browser, bokeh, etc) Also are there any JS console errors in your browser when this happens? Finally, can you fix the CDS column length issue and see if it persists after that?

It also does not zoom like shown in the gif for me.

For updating multiple lines I would suggest the cds.stream and multi_line instead of line.
With stream you can keep data that was used before. You can add a rollover if you have a maximum of lines to be plotted in the picture.

import numpy as np
import pandas as pd

from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import figure
from bokeh.layouts import row
from bokeh.io import curdoc,show

doc = curdoc()


table = [[1,2,3,4,1],[2,3,4,5,2],[3,4,5,6,3],[4,5,6,7,4]]
df = pd.DataFrame(table, columns=['a','b','c','d','e'])
colors = ["navy", "firebrick", "red", "darkorchid", "hotpink", "black", "pink"]
next_group = 1

source1 = ColumnDataSource(data = {'x':[], 'y':[], 'label':[], 'colors':[]})
plot = figure(width=400, plot_height=300, title=None, tools='pan')

text_input = TextInput(value="ab", title="Label:")


def update_plot(attrname, old, new):
    global next_group

    choice =  text_input.value
    n = len(df['e'])
    cl = [char for char in choice]

    # create a new dict to set up the new source wihtout getting "columns must be of same length" warnings
    new_data = {'x': [df['e'].tolist() * len(cl)], 'y': [[x for e in cl for x in df[e].tolist()]], 'label': [choice], 'colors': [colors[next_group]]}

    # total update 
    #source1.data = new_data

    # use stream for continious updates
    source1.stream(new_data)

    next_group += 1

#plot.line(x='x', y= 'y', source = source1, legend_label = 'label', line_width=2, color=colors[next_group], muted_alpha = 0.1)
plot.multi_line(xs='x', ys= 'y', source = source1, legend = 'label', line_width=2, color='colors', muted_alpha = 0.1)
plot.legend.location = "top_left"
plot.legend.click_policy="mute"


text_input.on_change('value', update_plot)

doc.add_root(row(plot, text_input))

The only thing left to do is the label update, unfortunately this does not seem to work via source :confused:

chrome version: Version 78.0.3904.108

OS: macOS version 10.14.6

bokeh version: 1.4.0

still persist if add columns all at once.
I didn’t try to use “webgl” backends so I think it is default Canvas backend.
And finally, wow, there is indeed js error:

and if I use Safari as browser, the zooming issue is gone …

The thing with multi lines is that, firstly multi lines has a fixed number of lines so that I can not add lines dynamically in the callback, though the workaround is setting some of the lines to have empty axis, and secondly and most importantly, one multi line can have only one legend and I cannot create each legend for each line. The important concept is that mutli line is essentially a same set of things that have different parts which is clearly not my cases – each of my query add very different and independent line. So mutli line is not suited for my cases.

The thing with multi lines is that, firstly multi lines has a fixed number of lines so that I can not add lines dynamically in the callback, though the workaround is setting some of the lines to have empty axis, and secondly and most importantly, one multi line can have only one legend and I cannot create each legend for each line.

@www neither of these is true. MultiLine accepts a "list of lists` where each sublist is a new line, and you can add/subtract lines by updating the data to have more or less or different sublists. Also it is recently possible to have multiple legends for different sub-lines ot a multi-line (I don’t have time now to find the reference) but you’d have to manage the legend items manually.

1 Like

Thanks for the suggestion. If multiLine can have multiple legends for each sub-line that would be great. I am now just curious on the nature of the zooming bug in chrome browser. Is there anything recommended for me to fix it on my side? Thanks. (I will try to google how to add multi legends for MultiLine though most of information I found is years ago that don’t allow this feature)

In the code I posted you can see that the lines are added dynamically. All previous plotted lines will stay. Try to run it.

For the legends problem I figured out again how it works:
Instead of the attribute legend_label use only legend in plot.mulit_line

1 Like

thanks. I now see the beauty of stream. Will try later to see how MultiLiine performs. Thanks so much for help :smile:

1 Like