How can i synchronize series visibility across charts, such that when user show/hide series in one chart, all the others chart's series visibility change as well
charts = []
renderers = []
#iterate over and gather all legend renderers
for spec in charts_specs:
chart = createChart(spec)
charts.append(chart)
for legend in chart.legend:
for legend_item in legend.items:
renderers.append(legend_item.renderers[0])
#register for visibility changed and update matching legends in other charts
for renderer in renderers:
renderer.js_on_change("visible", CustomJS(args=(renderer=renderer, renderers = renderers), code = """
for(var index =0 ; index < renderers.length; index++)
{
var global_renderer = renderers[index];
if(global_renderer.name === renderer.name)
{
global.renderer.visible = renderer.visible
}
}
""")
#plot charts
for chart in charts:
show(chart)
Question - with above (1) I do not see anything update (2) Why does this not cause a stack overflow with visiblity callback called recursively. I suppose i am not pushing the changes correctly?
I'm more of a basic Python user so I could be saying something stupid, but shouldn't you be using global_renderer.visible instead of global.renderer.visible? Maybe that's the answer to everything? I don't know actually.
Anyway, I'll tell you my approach so that you can evaluate if it's doable.
The thing is that you want all charts to switch altogether from one state to the other whenever the user asks for it, therefore, I'd use 2 global variables:
A boolean to store the present state (STATE).
A boolean to know if charts are in the middle of switching (CHANGING).
The behaviour of the charts would also be a bit different from yours, let me write it with pseudo-code so that it's more understandable.
1st chart would:
if not(CHANGING):
STATE = !STATE
CHANGING = True
for(chart in charts):
"visible" = STATE
Charts in the middle would:
if not(CHANGING):
1st_chart."visible" = !STATE
Last chart would:
if not(CHANGING):
1st_chart."visible" = !STATE
else:
CHANGING = False
Why do it like this? This way, whenever a chart is changed by the user, it will trigger the 1st chart change, thus switching the STATE variable and activating the CHANGING mode. In this mode the 1st chart changes all of the other charts' visiblility into the new STATE. As globals are "protected" in CHANGING mode, the STATE will be the same for everybody and no recursive process should occur. Finally, when the 1st chart pulls a change into the last chart the CHANGING mode is set to False and the iteration ends.
Hope this was useful somehow ;)
Related
I have a Plotly figure built in Python that updates automatically. I want to preserve dashboard zooms even with automatic updates. The documentation in Plotly says this can be done using the layout uirevision field, per the this community writeup. The docs give this as an example of the return dictionary:
return {
'data': data,
'layout': {
# `uirevsion` is where the magic happens
# this key is tracked internally by `dcc.Graph`,
# when it changes from one update to the next,
# it resets all of the user-driven interactions
# (like zooming, panning, clicking on legend items).
# if it remains the same, then that user-driven UI state
# doesn't change.
# it can be equal to anything, the important thing is
# to make sure that it changes when you want to reset the user
# state.
#
# in this example, we *only* want to reset the user UI state
# when the user has changed their dataset. That is:
# - if they toggle on or off reference, don't reset the UI state
# - if they change the color, then don't reset the UI state
# so, `uirevsion` needs to change when the `dataset` changes:
# this is easy to program, we'll just set `uirevision` to be the
# `dataset` value itself.
#
# if we wanted the `uirevision` to change when we add the "reference"
# line, then we could set this to be `'{}{}'.format(dataset, reference)`
'uirevision': dataset,
'legend': {'x': 0, 'y': 1}
}
}
However, my figure is built more like this:
import plotly.express as px
#app.callback(
Output("graph", "figure"),
[Input("interval-component", "n_intervals")])
def display_graph(n_intervals):
# Logic for obtaining data/processing is not shown
my_figure = px.line(my_data_frame, x=my_data_frame.index, y=['line_1', 'line_2'],
title='Some Title', template='plotly_dark')
return my_figure
In other words, since I am not returning a dictionary, but a plotly express figure directly, how can I directly access the uirevision value so that UI changes from the user are preserved?
You can use the update_layout member function of the figure.
my_figure.update_layout(uirevision=<your data>)
More information here: https://plotly.com/python/creating-and-updating-figures/#updating-figure-layouts
Use the figure dictionary, which can be accessed like so:
my_figure['layout']['uirevision'] = 'some_value'
This can also be used to access other useful aspects of the figure, such as changing the line color of a specific line entry:
my_figure['data'][2]['line']['color'] = '#FFFF00'
To see the other entry options, print out my_figure in a Python session.
Note: since the uirevision option isn't documented very well (at least, not in my searching online), I thought it worth posting this as an option.
I'd like to be able to change things about the slider (the value, the start/end values) programmatically.
So I take the standard slider.py demo, and just add this at the end:
for i in range(5):
amp_slider.value = amp_slider.value + 1
time.sleep(1)
That should move the value upwards every second for a few seconds. But the slider doesn't move. What am I doing wrong? Or similarly if I try to change the .end or .start value.
[I know sliders are supposed to be INPUT not OUTPUT devices. But nonetheless I'm trying to control its behavior.]
bokeh show() outputs the chart as html & javascript. Once it has done this it can no longer be modified (unless you wrote some javascript which was included to modify the page).
You need a library that renders in a 'dynamic' window (such as matplotlib to be able to replot a chart like this.
The only code inside your program that will be used again once the page is created is in the callback functions. If you adjust sliders.py so it reads:
def update_title(attrname, old, new):
amplitude.value += 1
Every time you update the text, the amplitude will increase.
I have a little Bokeh plot with data points and associated text labels. What I want is for the text labels to only appear when the user selects points with the box select tool. This gets me close:
from bokeh.plotting import ColumnDataSource,figure,show
source = ColumnDataSource(
data=dict(
x=test[:,0],
y=test[:,1],
label=[unquote_plus(vocab_idx[i]) for i in range(len(test))]))
TOOLS="box_zoom,pan,reset,box_select"
p = figure(plot_width=400, plot_height=400,tools=TOOLS)
p.circle(x='x',y='y', size=10, color="red", alpha=0.25,source=source)
renderer = p.text(x='x',y='y',text='label',source=source)
renderer.nonselection_glyph.text_alpha=0.
show(p)
This is close, in that if I draw a box around some points, those text labels are shown and the rest are hidden, but the problem is that it renders all the text labels to start (which is not what I want). The initial plot should have all labels hidden, and they should only appear upon a box_select.
I thought I could start by rendering everything with alpha=0.0, and then setting a selection_glyph parameter, like this:
...
renderer = p.text(x='x',y='y',text='label',source=source,alpha=0.)
renderer.nonselection_glyph.text_alpha=0.
renderer.selection_glyph.text_alpha=1.
...
But this throws an error:
AttributeError: 'NoneType' object has no attribute 'text_alpha'
When trying to access the text_alpha attribute of selection_glyph.
I know I could use a hover effect here or similar, but need the labels to default to not being visible. An alternative, but not ideal, solution would be to have a toggle button that switches the labels on and off, but I'm not sure how to do that either.
What am I doing wrong here?
As of version 0.11.1, the value of selection_glyph is None by default. This is interpreted by BokehJS as "don't do anything different, just draw the glyph as normal". So you need to actually create a selection_glyph. There are two ways to do this, both demonstrated here:
http://docs.bokeh.org/en/latest/docs/user_guide/styling.html#selected-and-unselected-glyphs
Basically, they are
by hand
Create an actual Circle Bokeh model, something like:
selected_circle = Circle(fill_alpha=1, fill_color="firebrick", line_color=None)
renderer.selection_glyph = selected_circle
OR
using glyph method parameters
Alternatively, as a convenience Figure.circle accepts paramters like selection_fill_alpha or selection_color (basically any line or fill or text property, prefixed with selection_) :
p.circle(..., selection_color="firebrick")
Then a Circle will be created automatically and used for renderer.selection_glyph
I hope this is useful information. If so, it highlights that there are two possible ways that the project could be improved:
updating the docs to be explicit and highlight that renderer.selection_glyph is None by default
changing code so that renderer.selection_glyph is just a copy of renderer.glyph by default (then your original code would work)
Either would be small in scope and ideal for a new contributor. If you would be interested in working up a Pull Request to do either of these tasks, we (and other users) would certainly be grateful for the contribution. In which case, please just make an issue first at
https://github.com/bokeh/bokeh/issues
that references this discussion, and we can provide more details or answer any questions.
I am trying to write a python script that uses matplotlib. The idea is that when the user runs the script, an interactive window pops up in which they can toggle certain plots on and off with the CheckButtons that matplotlib provides. I managed to figure out how to change the visibility of the plots themselves, however, I am struggling to do the same for the annotation. For the lines I have the following code:
def plotsetlines(lines,toggle):
""" plot vertical, labeled lines """
x = []
lmin = 4
lmax = 6
for name in lines:
x.append(lines[name])
plt.annotate(s=name, xy=(lines[name], lmax), xytext=(lines[name], lmax+1.1), rotation=90,size='large', visible=toggle)
print x
return plt.vlines(x, lmin, lmax, lw=2,visible=toggle)
Here lines is a dictionary of the form:
lines1 = {"a":115.27, "b":115.0, "c":112.0}
and toggle is a boolean. Once this function has been called, I can change the visibility of the lines as follows:
lns1 = plotsetlines(lines1,True)
lns1.set_visible(not lns1.get_visible())
The problem is, I have no idea how I can do the same thing for my annotations easily. I know that the Annotate object has the get/set_visible methods as well, but the function I wrote doesn't return the annotations in the same way that it returns my lines, so I'm not sure what to call the methods on. Any suggestions and ideas are welcome.
Also, since this is my first question posted here, please let me know if you have suggestions about the layout/wording etc. of the question itself. Thanks!
I have a Python Bokeh plot containing multiple lines, Is there a way I can interactively switch some of these lines on and off?
p1.line(Time,Temp0,size=12,color=getcolor())
p1.line(Time,Temp1,size=12,color=getcolor())
p1.line(Time,Temp2,size=12,color=getcolor())
p1.line(Time,Temp3,size=12,color=getcolor())
....
show(p1)
I just came across this problem myself in a similar scenario. In my case, I also wanted to do other operations on it.
There are 2 possible approaches:
1.) Client-server approach
2.) Client only approach
1.) Client Server Approach ak Bokeh Server
One way how you can achieve this interactivity is by using the bokeh server which you can read more about here. I will describe this way in more detail since at this point, I am a bit more familiar with it.
Going by your example above, if I were to use the bokeh serve, I would first setup a ColumnDataSource like so:
source = ColumnDataSource(data = dict(
time = Time,
temp0 = [],
temp1 = [],
temp2 = [],
temp3 = [],
)
Next I would setup a widget that allows you to toggle what temperatures to show:
multi_select = MultiSelect(title="Option:", value=["Temp1"],
options=["Temp1", "Temp2", "Temp3"])
# Add an event listener on the python side.
multi_select.on_change('value', lambda attr, old, new: update())
Then I would define the update function like below. The purpose of the update function is to update the ColumnDataSource (which was previously empty) with values you want to populate in the graph now.
def update():
"""This function will syncronize the server data object with
your browser data object. """
# Here I retrieve the value of selected elements from multi-select
selection_options = multi_select.options
selections = multi_select.value
for option in selection_options:
if option not in selections:
source.data[option] = []
else:
# I am assuming your temperatures are in a dataframe.
source.data[option] = df[option]
The last thing to do is to redefine how you plot your glyphs. Instead of drawing from lists, or dataframes, we will draw our data from a ColumnDataSource like so:
p1.line("time","temp0", source=source, size=12,color=getcolor())
p1.line("time","temp1", source=source, size=12,color=getcolor())
p1.line("time","temp2", source=source, size=12,color=getcolor())
p1.line(Time,Temp3, source=source, size=12,color=getcolor())
So basically by controlling the content of the ColumnDataSource which is synchronized with the browser object, I can toggle whether data points are shown or not. You may or may not need to define multiple ColumnDataSources. Try it out this way first.
2.) Client only approach ak Callbacks
The approach above uses a client-server architecture. Another possible approach would be to do this all on the front-end. This link shows how some simple interactions can be done completely on the browser side via various forms of callbacks.
Anyway, I hope this is helpful. Cheers!
The question is from some time back but Bokeh now has the interactive legend functionality - you can just specify
your_figure.legend.click_policy = 'hide'
And this makes legend while listing your lines interactive and you can switch each line on/off