I'm working on visualizations that require very precise zooming in on X axis - down to 0.001 units, when full range can span hundreds of units. To enable this, I'd like to use Bokeh with Spinner widgets. Here's the minimum example where X range responds to the spinners. Conveniently, manually entering the value into the spinner works as you would expect.
import numpy as np
from bokeh.io import show
from bokeh.layouts import column, row
from bokeh.models import Spinner
from bokeh.plotting import figure, output_notebook
output_notebook()
x = np.random.rand(10)
y = np.random.rand(10)
p = figure(width=400, height=200, x_range=(0, 1), y_range=(0, 1))
points = p.scatter(x=x, y=y, size=4)
spinner_xmin = Spinner(title="min X", low=0, high=1, step=0.05, value=0, width=80)
spinner_xmax = Spinner(title="max X", low=0, high=1, step=0.05, value=1, width=80)
spinner_xmin.js_link('value', p.x_range, 'start')
spinner_xmax.js_link('value', p.x_range, 'end')
show(column(p, row([spinner_xmin, spinner_xmax], width=400, sizing_mode='stretch_both')))
Is there a way for the spinners to update their values when I use Bokeh's default Zoom tool, so that both tools can be used together?
Is it easier to do in other libraries like Plotly or anything else?
If you want to display the changes of the x-axis in the spinner you can simply add another js-link in the other direction as you did before.
Add these two lines to your code to connect the x_range with the spinner.
p.x_range.js_link('start', spinner_xmin, 'value')
p.x_range.js_link('end', spinner_xmax, 'value')
Now you have a two-way connection.
Comment
The tools don't know anything about your step-size. Therefor the values can look a litte odd. But this is also the case if you enter a value by hand.
Related
I have multiple line plots to draw on a single figure and I am doing this using bokeh.plotting. Using
p0.line(),
p0.extra_y_ranges(),
and
p0.add_layout(LinearAxes())
p0 being 1 bokeh figure.
I would like to have range_sliders for each y axis separately on the right. Would this be possible using bokeh?
For the primary y axis, the range slider works fine using
# set up RangeSlider y_counts
range_slider_c = RangeSlider(
title="c",
start=-10,
end=400,
step=1,
value=(-1, 300),
height = 250,
orientation='vertical',
show_value= False,
direction = 'rtl'
)
range_slider_c.js_link("value", p0.y_range, "start", attr_selector=0)
range_slider_c.js_link("value", p0.y_range, "end", attr_selector=1)
But I am unclear about how to call the additional y axes's ranges like I did for p0.y_range in js.link.
I hope I have been able to explain my requirements properly.
This answers is based on the answer on your previous question.
As it is mentioned there, the extra y-ranges are saved in a dictionary with the keyword extra_y_ranges. Therefor you only have to change the js_link using extra_y_ranges with a valid name. For example range_slider_c.js_link("value", p0.extra_y_ranges["c", "start", attr_selector=0).
Complete minimal example
from bokeh.layouts import column
from bokeh.models import LinearAxis, Range1d, CustomJS, RangeSlider
from bokeh.plotting import figure, show, output_notebook
output_notebook()
data_x = [1,2,3,4,5]
data_y = [1,2,3,4,5]
color = ['red', 'green', 'magenta', 'black']
p = figure(plot_width=500, plot_height=300)
p.line(data_x, data_y, color='blue')
range_sliders = []
for i, c in enumerate(color, start=1):
name = f'extra_range_{i}'
lable = f'extra range {i}'
p.extra_y_ranges[name] = Range1d(start=0, end=10*i)
p.add_layout(LinearAxis(axis_label=lable, y_range_name=name), 'right')
p.line(data_x, data_y, color=c, y_range_name=name)
range_slider = RangeSlider(start=0, end=10*i, value=(1,9*i), step=1, title=f"Slider {lable}")
range_slider.js_link("value", p.extra_y_ranges[name] , "start", attr_selector=0)
range_slider.js_link("value", p.extra_y_ranges[name] , "end", attr_selector=1)
range_sliders.append(range_slider)
show(column(range_sliders+[p]))
Output
Comment
To stack the sliders and the figure i use the column layout, which takes a list of bokeh objects. Other layouts are available.
I have plot
with secondary axis added like this:
plot.extra_x_ranges['sec_x_axis'] = Range1d(0, 100)
ax2 = LinearAxis(x_range_name="sec_x_axis", axis_label="secondary x-axis")
plot.add_layout(ax2, 'above')
x_axis is x_axis_type='datetime', so bokeh show ms on second x-axis too. This is not good.
Is there a way I can put my labels on this axis? I have a list of str labels like:
my_labels = ['21.5; 315.1', '21.6; 315.0', '21.7; 315.0', '21.7; 314.9',.....]
I found FuncTickFormatter but it takes JS code inside, so I can't handle it.
Maybe there is another way to do this?
To override the values of the labels use major_label_overrides on the appropriate axis. You can pass a dictionary like {1:'A', ...}, where 1 is the place to overwrite and A is the new label.
To avoid "wrong" labels while zooming, you can set the ticker direcetlly as list unsing ticker.
In your case the axis is p.above[0].
Comment
If you add a LinearAxis to a figure with an already existing DatetimeAxis, the new axis shoudn't be effected and therefor shouldn't be formatted as datetime. I used the latest version 2.4.3 and it works as expected. Use the minimal example to try it on your own.
Minimal Example
This code is based on the twin_axis.py example published by the authors of bokeh.
from numpy import arange, linspace, pi, sin
from bokeh.models import LinearAxis, Range1d
from bokeh.plotting import figure, show, output_notebook
output_notebook()
x = arange(-2*pi, 2*pi, 0.2)
x2 = arange(-pi, pi, 0.1)
y = sin(x)
y2 = sin(x2)
p = figure(
width=400,
height=400,
x_range=(-6.5, 6.5),
y_range=(-1.1, 1.1),
min_border=80,
x_axis_type="datetime"
)
p.circle(x, y, color="crimson", size=8)
p.yaxis.axis_label = "red circles"
p.yaxis.axis_label_text_color ="crimson"
p.extra_x_ranges['foo'] = Range1d(-pi, pi)
p.circle(x2, y2, color="navy", size=8, x_range_name="foo")
ax2 = LinearAxis(x_range_name="foo", axis_label="blue circles")
ax2.axis_label_text_color ="navy"
p.add_layout(ax2, 'above')
# set ticker to avoid wrong formatted labels while zooming
p.above[0].ticker = list(range(-3,4))
# overwrite labels
p.above[0].major_label_overrides = {key: item for key, item in zip(range(-3,4), list('ABCDEFG'))}
show(p)
default
overwritten labels
Maybe the title is not clear, so let me explain my question. I tried LabelSet with all numbers at the x and y axes, it works perfectly. However, when I changed to use all Alphabet letters such as A, A+, B, etc, the LabelSet does not understand and put all annotations on the top left (I attached the image about the error below.)
I tried to debug and find out the problem is at labels = LabelSet(x = 'average_grades', y = 'exam_grades'). If I change x = integer, and y = integer, it works. Please help me to fix it since I want to apply LabelSet in many cases, not only with numbers.
#importing libraries
from bokeh.plotting import figure
from bokeh.io import curdoc
from bokeh.models.annotations import LabelSet, Label
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Select
from bokeh.layouts import layout
#create columndatasource
source=ColumnDataSource(dict(average_grades=["B+","A","D-"],
exam_grades=["A+","C","D"],
student_names=["Stephan","Helder","Riazudidn"]))
#create the figure
grade1 = ["F","D-","D","D+","C-","C","C+","B-","B","B+","A-","A","A+"]
grade2 = ["F","D-","D","D+","C-","C","C+","B-","B","B+","A-","A","A+"]
f = figure(x_range= grade1,
y_range= grade2)
f.plot_width = 1100
f.plot_height = 650
#add labels for glyphs
labels=LabelSet(x='average_grades',y='exam_grades',
text="student_names",
x_offset=20, y_offset=20,
text_color = 'red', source=source, level = 'glyph',
render_mode = "css", text_font_size = "20pt")
f.add_layout(labels)
description = Label(x = 4, y = 2, text="Hello World", render_mode = "css")
f.add_layout(description)
#create glyphs
f.circle(x="average_grades", y="exam_grades", source=source, size=8)
#create function
def update_labels(attr,old,new):
labels.text=select.value
# #labels.text = getattr(select, attr)
#create select widget
options=[("average_grades","Average Grades"),("exam_grades","Exam Grades"),("student_names","Student Names")]
select=Select(title="Attribute",options=options)
select.on_change("value",update_labels)
# #create layout and add to curdoc
lay_out=layout([[select]])
curdoc().add_root(f)
curdoc().add_root(lay_out)
Please, always provide relevant version information with every question.
This just seems like a bug. I can see the problem on latest Bokeh version 2.2.1 however if I try a 2.3 development version, the problem has gone away. So evidently this was fixed at some point recently, though perhaps inadvertently. Please file a GitHub issue so that we can make sure any fix is intentional and preserved under test. The solution is to wait for the next release 2.3 (or a 2.2.2 if one is made).
In the mean time, the only suggestion I can offer is to use integer coordinates in stead of categorical (string) coordinates, then use fixed ticker values with tick label overrides:
p.xaxis.major_label_overrides = {1: "A", 2, "B", ...}
to make it "look the same".
I'm trying to label a pandas-df (containing timeseries data) with the help of
a bokeh-lineplot, box_select tool and a TextInput widget in a jupyter-notebook. How can I access the by the box_select selected data points?
I tried to edit a similar problems code (Get selected data contained within box select tool in Bokeh) by changing the CustomJS to something like:
source.callback = CustomJS(args=dict(p=p), code="""
var inds = cb_obj.get('selected')['1d'].indices;
[source.data['xvals'][i] for i in inds] = 'b'
"""
)
but couldn't apply a change on the source of the selected points.
So the shortterm goal is to manipulate a specific column of source of the selected points.
Longterm I want to use a TextInput widget to label the selected points by the supplied Textinput. That would look like:
EDIT:
That's the current code I'm trying in the notebook, to reconstruct the issue:
from random import random
import bokeh as bk
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, HoverTool
from bokeh.plotting import figure, output_file, show, output_notebook
output_notebook()
x = [random() for x in range(20)]
y = [random() for y in range(20)]
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(data=dict(x=x, y=y, label=[i for i in "a"*20]))
p1 = figure(plot_width=400, plot_height=400, tools="box_select", title="Select Here")
p1.circle('x', 'y', source=source, alpha=0.6)
p1.add_tools(hovertool)
source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
var inds = cb_obj.indices;
for (var i = 0; i < inds.length; i++) {
source.data['label'][inds[i]] = 'b'
}
source.change.emit();
""")
)
layout = row(p1)
show(layout)
The main thing to note is that BokehJS can only automatically notice updates when actual assignments are made, e.g.
source.data = some_new_data
That would trigger an update. If you update the data "in place" then BokehJS is not able to notice that. You will have to be explicit and call source.change.emit() to let BokehJS know something has been updated.
However, you should also know that you are using three different things that are long-deprecated and will be removed in the release after next.
cb_obj.get('selected')
There is no need to ever use .get You can just access properties directly:
cb_obj.selected
The ['1d'] syntax. This dict approach was very clumsy and will be removed very soon. For most selections you want the indices property of the selection:
source.selected.indices
source.callback
This is an ancient ad-hoc callback. There is a newer general mechanism for callbacks on properties that should always be used instead
source.selected.js_on_change('indices', CustomJS(...))
Note that in this case, the cb_obj is the selection, not the data source.
With the help of this guide on how to embed a bokeh server in the notebook I figured out the following minimal example for my purpose:
from random import random
import pandas as pd
import numpy as np
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, BoxSelectTool
from bokeh.models.widgets import TextInput
output_notebook()
def modify_doc(doc):
# create a plot and style its properties
TOOLS="pan,wheel_zoom,reset"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
label = [i for i in "a"*10]
))
p.scatter("xvals", "yvals",source=source, color="white")
p.line("xvals", "yvals",source=source)
p.add_tools(BoxSelectTool(dimensions="width"))
p.add_tools(hovertool)
# create a callback that will add a number in a random location
def callback():
inds = source.selected.indices
for i in inds:
source.data['label'][i] = label_input.value.strip()
print(source.data)
new_data = pd.DataFrame(source.data)
new_data.to_csv("new_data.csv", index=False)
# TextInput to specify the label
label_input = TextInput(title="Label")
# add a button widget and configure with the call back
button = Button(label="Label Data")
button.on_click(callback)
# put the button and plot in a layout and add to the document
doc.add_root(column(button,label_input, p))
show(modify_doc, notebook_url="http://localhost:8888")
That generates the following UI:
BTW: Due to the non-existing box_select tool for the line glyph I use a workaround by combining it with invisible scatter points.
So far so good, is there a more elegant way to access the data.source/new_data df in the notebook outside modify_doc() than exporting it within the callback?
I only started to use Bokeh recently. I have a scatter plot in which I would like to color each marker according to a certain third property (say a quantity, while the x-axis is a date and the y-axis is a given value at that point in time).
Assuming my data is in a data frame, I managed to do this using a linear color map as follows:
min_q = df.quantity.min()
max_q = df.quantity.max()
mapper = linear_cmap(field_name='quantity', palette=palettes.Spectral6, low=min_q, high=max_q)
source = ColumnDataSource(data=get_data(df))
p = figure(x_axis_type="datetime")
p.scatter(x="date_column", y="value", marker="triangle", fill_color=mapper, line_color=None, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_layout(color_bar, 'right')
This seems to work as expected. Below is the plot I get upon starting the bokeh server.
Then I have a callback function update() triggered upon changing value in some widget (a select or a time picker).
def update():
# get new df (according to new date/select)
df = get_df()
# update min/max for colormap
min_q = df.quantity.min()
max_q = df.quantity.max()
# I think I should not create a new mapper but doing so I get closer
mapper = linear_cmap(field_name='quantity', palette=palettes.Spectral6 ,low=min_q, high=max_q)
color_bar.color_mapper=mapper['transform']
source.data = get_data(df)
# etc
This is the closest I could get. The color map is updated with new values, but it seems that the colors of the marker still follow the original pattern. See picture below (given that quantity I would expect green, but it is blue as it still seen as < 4000 as in the map of the first plot before the callback).
Should I just add a "color" column to the data frame? I feel there is an easier/more convenient way to do that.
EDIT: Here is a minimal working example using the answer by bigreddot:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import Button, ColumnDataSource, ColorBar, HoverTool
from bokeh.palettes import Spectral6
from bokeh.transform import linear_cmap
import numpy as np
x = [1,2,3,4,5,7,8,9,10]
y = [1,2,3,4,5,7,8,9,10]
z = [1,2,3,4,5,7,8,9,10]
source = ColumnDataSource(dict(x=x, y=y, z=z))
#Use the field name of the column source
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(y) ,high=max(y))
p = figure(plot_width=300, plot_height=300, title="Linear Color Map Based on Y")
p.circle(x='x', y='y', line_color=mapper,color=mapper, fill_alpha=1, size=12, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_tools(HoverTool(tooltips="#z", show_arrow=False, point_policy='follow_mouse'))
p.add_layout(color_bar, 'right')
b = Button()
def update():
new_z = np.exp2(z)
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(new_z), high=max(new_z))
color_bar.color_mapper=mapper['transform']
source.data = dict(x=x, y=y, z=new_z)
b.on_click(update)
curdoc().add_root(column(b, p))
Upon update, the circles will be colored according to the original scale: everything bigger than 10 will be red. Instead, I would expect everything blue until the last 3 circle on tops that should be colored green yellow and red respectively.
It's possible that is a bug, feel free to open a GitHub issue.
That said, the above code does not represent best practices for Bokeh usage, which is: always make the smallest update possible. In this case, this means setting new property values on the existing color transform, rather than replacing the existing color transform.
Here is a complete working example (made with Bokeh 1.0.2) that demonstrates the glyph's colormapped colors updating in response to the data column changing:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import Button, ColumnDataSource, ColorBar
from bokeh.palettes import Spectral6
from bokeh.transform import linear_cmap
x = [1,2,3,4,5,7,8,9,10]
y = [1,2,3,4,5,7,8,9,10]
z = [1,2,3,4,5,7,8,9,10]
#Use the field name of the column source
mapper = linear_cmap(field_name='z', palette=Spectral6 ,low=min(y) ,high=max(y))
source = ColumnDataSource(dict(x=x, y=y, z=z))
p = figure(plot_width=300, plot_height=300, title="Linear Color Map Based on Y")
p.circle(x='x', y='y', line_color=mapper,color=mapper, fill_alpha=1, size=12, source=source)
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_layout(color_bar, 'right')
b = Button()
def update():
new_z = np.exp2(z)
# update the existing transform
mapper['transform'].low=min(new_z)
mapper['transform'].high=max(new_z)
source.data = dict(x=x, y=y, z=new_z)
b.on_click(update)
curdoc().add_root(column(b, p))
Here is the original plot:
And here is the update plot after clicking the button