I need to plot a profile of an image, which is, to plot values of a matrix column.
And to implement it as a drag tool, which would automatically update the lower plot based on cursor position over the upper plot:
Based on "A New Custom Tool" example from the docs I've written a code which works fine but has several problems:
import numpy as np
import bokeh.plotting as bp
from bokeh.models import CustomJS
from bokeh.layouts import layout, column, row
from bokeh.io import reset_output
from PIL import Image
im = Image.open(r'C:\Documents\image1.jpg')
z = np.array(im)[:,:,0]
from bokeh.core.properties import Instance, Float
from bokeh.io import output_file, show, output_notebook
from bokeh.models import ColumnDataSource, Tool
from bokeh.plotting import figure
from bokeh.util.compiler import TypeScript
from bokeh.layouts import layout, column, row
#output_file("a.html")
#reset_output()
# image vertical profile tool
TS_CODE = """
import {GestureTool, GestureToolView} from "models/tools/gestures/gesture_tool"
import {ColumnDataSource} from "models/sources/column_data_source"
import {GestureEvent} from "core/ui_events"
import * as p from "core/properties"
export class DrawToolView extends GestureToolView {
model: DrawTool
//this is executed when the pan/drag event starts
_pan_start(_ev: GestureEvent): void {
this.model.source.data = {x: [], y: []}
}
//this is executed on subsequent mouse/touch moves
_pan(ev: GestureEvent): void {
const {frame} = this.plot_view
const {sx, sy} = ev
if (!frame.bbox.contains(sx, sy))
return
const x = frame.xscales.default.invert(sx)
const y = frame.yscales.default.invert(sy)
var res = Array(128);
var rx = Math.round(x);
for(var i=0; i<128; i++) res[i] = this.model.zz.data["z"][i*225+rx];
this.model.source.data = {
x: Array(128).fill(0).map(Number.call, Number),
y: res
};
this.model.source.change.emit()
}
// this is executed then the pan/drag ends
_pan_end(_ev: GestureEvent): void {}
}
export namespace DrawTool {
export type Attrs = p.AttrsOf<Props>
export type Props = GestureTool.Props & {
source: p.Property<ColumnDataSource>,
zz: p.Property<ColumnDataSource>,
width: p.Float
}
}
export interface DrawTool extends DrawTool.Attrs {}
export class DrawTool extends GestureTool {
properties: DrawTool.Props
constructor(attrs?: Partial<DrawTool.Attrs>) {
super(attrs)
}
tool_name = "Drag Span"
icon = "bk-tool-icon-lasso-select"
event_type = "pan" as "pan"
default_order = 12
static initClass(): void {
this.prototype.type = "DrawTool"
this.prototype.default_view = DrawToolView
this.define<DrawTool.Props>({
source: [ p.Instance ],
zz: [ p.Instance ],
width: [ p.Float ]
})
}
}
DrawTool.initClass()
"""
class DrawTool(Tool):
__implementation__ = TypeScript(TS_CODE)
source = Instance(ColumnDataSource)
zz = Instance(ColumnDataSource)
width = Float()
output_notebook()
source = ColumnDataSource(data=dict(x=[], y=[]))
zz = ColumnDataSource(data=dict(z=z.flatten()))
p1 = figure(plot_width=600, plot_height=200, x_range=(0, 225), y_range=(0, 128),
tools=[DrawTool(source=source, zz=zz, width=225)])
im = p1.image(image=[np.flipud(z)], x=0, y=0, dw=225,
dh=128, palette='Greys256')
p2 = figure(plot_width=600, plot_height=200)
p2.line('x', 'y', source=source)
bp.show(column(p1, p2))
1) Image dimensions are hard-coded now: how do I feed image dimensions from python to js?
2) The image is transferred to the client twice: first as an argument to image(), then as source for the button plot. How to access the image "source" from the DrawTool?
3) If (all this code being in one jupyter cell) I run it the second time it refuses to plot anything with a javascript error in console Model 'DrawTool' does not exist. Running it the third time, fourth time and further on works fine. What exactly is bokeh trying to tell me in this error message?
1) Image dimensions are hard-coded now: how do I feed image dimensions
from python to js?
2) The image is transferred to the client twice: first as an argument
to image(), then as source for the button plot. How to access the
image "source" from the DrawTool?
The answer to these is the same, add more properties (on both the Python and the JS sides) for the data you want to store on the DrawTool. E.g. another Instance for another ColumnDataSource that holds the image data, and integer properties for the width and height.
3) If (all this code being in one jupyter cell) I run it the second time it refuses to plot anything with a javascript error in console Model 'DrawTool' does not exist. Running it the third time, fourth time and further on works fine. What exactly is bokeh trying to tell me in this error message?
This message is stating that BokehJS does not know anything about any DrawTool, and the reason for this is that, due to the way things work in the notebook, custom extensions only get registered when you call output_notebook. So you will have to call output_notebook again after you define the custom extension. I don't like this state of affairs but there is nothing we can do about it.
Related
In Bokeh, I have a CustomJS triggering a callback updating a ColumnDataSource. I would like to have any update to this trigger some Python code. However, this does not work by specifying an on_change callback to the data source nor can I access the updated data. What am I doing wrong? My code...
from bokeh.models import ColumnDataSource, CustomJS, Button
from bokeh.layouts import layout
from bokeh.tile_providers import get_provider
from bokeh.plotting import figure
from bokeh.io import curdoc
from bokeh.events import Tap
def callback():
print('button checks source:')
print(source.data)
def callback_source(attr, old, new):
print('source changed!')
print(source.data)
# define the source, initially empty
source = ColumnDataSource(data=dict(lon=[], lat=[]))
# set a map
tile_provider = get_provider('CARTODBPOSITRON')
initial_bounds = ((-1000000, 4000000), (3000000, 8000000))
plot = figure(
x_range=initial_bounds[0], y_range=initial_bounds[1],
x_axis_type="mercator", y_axis_type="mercator"
)
plot.add_tile(tile_provider)
# show the current lon/lat pair in source
plot.circle(x="lon", y="lat", size=15, fill_alpha=0.8, source=source)
plot.js_on_event(Tap, CustomJS(args=dict(source=source), code="""
// get the source.data contents
const x = source.data['lon'];
const y = source.data['lat'];
// if source.data is empty, append the cursor coords
if (x.length == 0) {
x.push(cb_obj.x);
y.push(cb_obj.y);
// if source.data is populated, replace the cursor coords
} else {
x[0] = cb_obj.x;
y[0] = cb_obj.y;
}
source.change.emit();
"""))
# how to detect changes in source.data which trigger Python code?
source.on_change('data', callback_source)
button = Button(label="show source.data status", button_type="success")
# print the source status
button.on_click(callback)
curdoc().add_root(layout([button, plot]))
leads to a plot, where my Tap Events are correctly updating the circle, but apparently not updating the source variable.
2022-01-07 08:12:46,924 Starting Bokeh server version 2.4.2 (running on Tornado 6.1)
2022-01-07 08:12:46,927 User authentication hooks NOT provided (default user enabled)
2022-01-07 08:12:46,931 Bokeh app running at: http://localhost:5006/temp_test
2022-01-07 08:12:46,931 Starting Bokeh server with process id: 4797
2022-01-07 08:12:48,212 WebSocket connection opened
2022-01-07 08:12:48,214 ServerConnection created
button checks source:
{'lon': [], 'lat': []}
Based on the answer provided by #bigreddot, the following new JS code (source.change.emit() is not necessary) works:
const x=[cb_obj.x];
const y=[cb_obj.y];
const new_data = {lon:x, lat:y};
source.data = new_data;
To get a property update event sent back to Python you will have to actually assign an entire new ("dict") value to source.data, and not just update the arrays "in place". The call to source.change.emit() is purely a JS-side event and has no effect whatsoever on the Python side.
I am using Bokeh DataTable to present an editable table and I wish to color the text in the cell if the value has changed by the user.
I was trying to use HTMLTemplateFormatter but I am not sure what to do.
If a user changed the value of row #2 I wish the text to be colored like that:
an example based on How to color rows and/or cells in a Bokeh DataTable?:
from bokeh.plotting import curdoc
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, TableColumn, HTMLTemplateFormatter
orig_data = dict(
cola=[1, 2, 3, 4, 5, 6],
)
data = orig_data
source = ColumnDataSource(data)
template = """
<div style="color: <%=
(function colorfromint(){
if(orig_data.cola != data.cola){return('red')} // I don't know what to write here
}()) %>;">
<%= value %>
</font>
</div>
"""
formatter = HTMLTemplateFormatter(template=template)
columns = [TableColumn(field="cola", title="CL1", formatter=formatter, width=100)]
data_table = DataTable(source=source,
columns=columns,
editable=True,
width=100)
curdoc().add_root(data_table)
Can I compare different tables using the HTMLTemplateFormatter block?
if not, from the HTMLTemplateFormatter Bokeh documentation:
"The formatter has access other items in the row via the dataContext object passed to the formatted"
So one solution I can think of is joining the tables and make the compare with the dataContext object, presenting only the columns I select
But, I'm not sure how to it and it seems to me like a "dirty" workaround
I'm quite familiar with python but I'm new with Bokeh.
Is there a good and easy way to do it?
maybe other methods other than HTMLTemplateFormatter?
Since default Bokeh formatters work on whole columns, you will have to create your own formatter.
The example below works even without Bokeh server, that's why it uses show. But you can replace it with curdoc().add_root - it should work just the same.
from bokeh.core.property.container import List
from bokeh.core.property.primitive import Int
from bokeh.io import show
from bokeh.models import ColumnDataSource, CustomJS, StringFormatter
from bokeh.models.widgets import DataTable, TableColumn
data = dict(cola=[1, 2, 3, 4, 5, 6],
colb=[1, 2, 3, 4, 5, 6])
orig_ds = ColumnDataSource(data)
ds = ColumnDataSource(copy.deepcopy(data))
class RowIndexFormatter(StringFormatter):
rows = List(Int, default=[])
# language=TypeScript
__implementation__ = """\
import {StringFormatter} from "models/widgets/tables/cell_formatters"
import * as p from "core/properties"
import {div} from "core/dom"
export namespace RowIndexFormatter {
export type Attrs = p.AttrsOf<Props>
export type Props = StringFormatter.Props & {
rows: p.Property<number[]>
}
}
export interface RowIndexFormatter extends RowIndexFormatter.Attrs {}
export class RowIndexFormatter extends StringFormatter {
properties: RowIndexFormatter.Props
constructor(attrs?: Partial<RowIndexFormatter.Attrs>) {
super(attrs)
}
static init_RowIndexFormatter(): void {
this.define<RowIndexFormatter.Props>({
rows: [ p.Array, [] ]
})
}
doFormat(row: any, _cell: any, value: any, _columnDef: any, _dataContext: any): string {
// Mostly copied from `StringFormatter`, except for taking `rows` into account.
const {font_style, text_align, text_color} = this
const text = div({}, value == null ? "" : `${value}`)
switch (font_style) {
case "bold":
text.style.fontWeight = "bold"
break
case "italic":
text.style.fontStyle = "italic"
break
}
if (text_align != null)
text.style.textAlign = text_align
if (text_color != null && this.rows.includes(row))
text.style.color = text_color
return text.outerHTML
}
}
"""
columns = [TableColumn(field="cola", title="CL1", formatter=RowIndexFormatter(text_color='red')),
TableColumn(field="colb", title="CL2", formatter=RowIndexFormatter(text_color='blue'))]
table = DataTable(source=ds, columns=columns, editable=True)
cb = CustomJS(args=dict(orig_ds=orig_ds, table=table),
code="""\
const columns = new Map(table.columns.map(c => [c.field, c]));
for (const c of cb_obj.columns()) {
const orig_col = orig_ds.data[c];
const formatter = columns.get(c).formatter;
formatter.rows = [];
cb_obj.data[c].forEach((val, idx) => {
if (val != orig_col[idx]) {
formatter.rows.push(idx);
}
});
}
table.change.emit();
""")
ds.js_on_change('data', cb)
ds.js_on_change('patching', cb)
show(table)
In the example below, I am trying to get the x and y coordinates that appear in the Div next to the plot when the bokeh plot is Tapped to be appended to the data dictionary coordList in their respective list.
import numpy as np
from bokeh.io import show, output_notebook
from bokeh.plotting import figure
from bokeh.models import CustomJS, Div
from bokeh.layouts import column, row
from bokeh.events import Tap
coordList = dict(x=[], y=[])
output_notebook()
def display_event(div, attributes=[], style = 'float:left;clear:left;font_size=10pt'):
"Build a suitable CustomJS to display the current event in the div model."
return CustomJS(args=dict(div=div), code="""
var attrs = %s; var args = [];
for (var i = 0; i<attrs.length; i++) {
args.push(Number(cb_obj[attrs[i]]).toFixed(2));
}
var line = "<span style=%r>(" + args.join(", ") + ")</span>\\n";
var text = div.text.concat(line);
var lines = text.split("\\n")
if (lines.length > 35)
lines.shift();
div.text = lines.join("\\n");
""" % (attributes, style))
x = np.random.random(size=4000) * 100
y = np.random.random(size=4000) * 100
radii = np.random.random(size=4000) * 1.5
colors = ["#%02x%02x%02x" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)]
p = figure(tools="pan,wheel_zoom,zoom_in,zoom_out,reset")
p.scatter(x, y, radius=np.random.random(size=4000) * 1.5,
fill_color=colors, fill_alpha=0.6, line_color=None)
div = Div(width=400, height=p.plot_height)
layout = row(p, div)
point_attributes = ['x', 'y']
p.js_on_event(Tap, display_event(div, attributes=point_attributes))
show(layout)
I'm not sure how the coordinates are saved and how to access them and append them to the lists.
There is no way to append to coordinates to a python object with code like above, because that code is generating standalone output (i.e. it is using "show"). Standalone output is pure static HTML and Bokeh JSON that is sent to browser, without any sort of connection to any Python process. If you want to connect Bokeh visualizations to a real running Python process, that is what the Bokeh server is for.
If you run a Bokeh server application, then you can use on_event with a real python callback to run whatever python code you want with the Tap even values:
def callback(event):
# use event['x'], event['y'], event['sx'], event['sy']
p.on_event(Tap, callback)
I have a Bokeh plot where I add some data in the form of a LabelSet and BoxAnnotation as overlay, but I want to be able to dynamically enable/disable this overlay.
I can enable/hide some of the lines in the plot already, but the system for the Annotations seems to be different. I've got so far already
Initialising
from ipywidgets import interact
from bokeh.plotting import figure as bf
from bokeh.layouts import layout as bl
from bokeh.models import Toggle, BoxAnnotation, CustomJS
from bokeh.io import push_notebook, show, output_notebook
output_notebook()
Widget generation
p = bf(title='test', x_range=(0,1), y_range=(0,1))
x = [1/3, 2/3]
y=[1/3, 2/3]
p.circle(x=x, y=y, size=15)
box = BoxAnnotation(left=None, right=0.5, fill_color='red', fill_alpha=0.1)
p.add_layout(box)
Interactivity
code = '''\
if toggle.active
box.visible = true
console.log 'enabling box'
else
box.visible = false
console.log 'disabling box'
'''
callback = CustomJS.from_coffeescript(code=code, args={})
toggle = Toggle(label="Red Box", button_type="success", callback=callback)
callback.args = {'toggle': toggle, 'box': box}
layout = bl([p], [toggle])
show(layout)
When I check the JS console, the if/else clauses get triggered as expected, so the Toggle works but the red box stays in place, both in Firefox as in IE
I think there might be some plumbing that is not hooked up on the BokehJS side to respond to visible. If so, that's a bug. Please make an issue with all this information in the Project Issue Tracker.
In the mean time, you can accomplish the same visual effect by manipulating the alpha values instead:
code = '''\
if toggle.active
box.fill_alpha = 0.1
box.line_alpha = 1
console.log 'enabling box'
else
box.fill_alpha = 0
box.line_alpha = 0
console.log 'disabling box'
'''
callback = CustomJS.from_coffeescript(code=code, args={})
toggle = Toggle(label="Red Box", button_type="success", callback=callback)
callback.args = {'toggle': toggle, 'box': box}
layout = bl([p], [toggle])
show(layout)
I am experimenting with bokeh data table to display data embedded in web page. It works quite nicely.
Is there a way to save the table content from the displayed data table? Other bokeh plots have tool bar for various functions including saving, but the DataTable does not seem to come with it. I know very little about javascript or slickgrid, which bokeh data table uses. And wondering if it can be done.
Thanks!
EDIT - It appears the my original question was not clear enough. Hope following pictures can help to illustrate:
Bokeh plot has toolbars associated:
But data table does not have it by default, and it won't take 'tools' parameter either:
Is it possible to add 'save' button to data table so the person view the table can download as tab delimited or csv files? Not necessarily need to be look the same, but with the same function for saving.
2021 Update: adjusted code that works in python 3.8 and bokeh 2.2.3
For those who have trouble adjusting or finding the example on the bokeh website or are just very lazy, the below code does the minimal job:
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Button
from bokeh.io import show
import os
source = ColumnDataSource({'list1':[0,1,2,3],'list2':[4,5,6,7]})
button = Button(label="Download", button_type="success")
button.js_on_click(CustomJS(args=dict(source=source),code=open(os.path.join(os.path.dirname(__file__),"download.js")).read()))
show(button)
And the file download.js:
function table_to_csv(source) {
const columns = Object.keys(source.data)
const nrows = source.get_length()
const lines = [columns.join(',')]
for (let i = 0; i < nrows; i++) {
let row = [];
for (let j = 0; j < columns.length; j++) {
const column = columns[j]
row.push(source.data[column][i].toString())
}
lines.push(row.join(','))
}
return lines.join('\n').concat('\n')
}
const filename = 'data_result.csv'
const filetext = table_to_csv(source)
const blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' })
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, filename)
} else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
It would be nice if bokeh provides a tool button for saving/exporting the data table to csv / txt / excel files. If it already does, I have not found it in the document yet.
In the mean time, a possible answer is to export the js array (that is underneath the bokeh data table) to CSV using native javascript. It has been described here and here.
ADD: bokeh has callbacks for using js. A simple description is here. still reading about it ...
EDIT: It is probably there for a while now, but I have just noticed an example on Bokeh website for saving csv from data table.
Related to my response to this stackoverflow question. Response copied below:
Here is a working example with Python 3.7.5 and Bokeh 1.4.0
public github link to this jupyter notebook:
https://github.com/surfaceowl-ai/python_visualizations/blob/master/notebooks/bokeh_save_linked_plot_data.ipynb
environment report:
virtual env python version: Python 3.7.5
virtual env ipython version: 7.9.0
watermark package reports:
bokeh 1.4.0
jupyter 1.0.0
numpy 1.17.4
pandas 0.25.3
rise 5.6.0
watermark 2.0.2
# Generate linked plots + TABLE displaying data + save button to export cvs of selected data
from random import random
from bokeh.io import output_notebook # prevent opening separate tab with graph
from bokeh.io import show
from bokeh.layouts import row
from bokeh.layouts import grid
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.models import Button # for saving data
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.models import HoverTool
from bokeh.plotting import figure
# create data
x = [random() for x in range(500)]
y = [random() for y in range(500)]
# create first subplot
plot_width = 400
plot_height = 400
s1 = ColumnDataSource(data=dict(x=x, y=y))
fig01 = figure(
plot_width=plot_width,
plot_height=plot_height,
tools=["lasso_select", "reset", "save"],
title="Select Here",
)
fig01.circle("x", "y", source=s1, alpha=0.6)
# create second subplot
s2 = ColumnDataSource(data=dict(x=[], y=[]))
# demo smart error msg: `box_zoom`, vs `BoxZoomTool`
fig02 = figure(
plot_width=400,
plot_height=400,
x_range=(0, 1),
y_range=(0, 1),
tools=["box_zoom", "wheel_zoom", "reset", "save"],
title="Watch Here",
)
fig02.circle("x", "y", source=s2, alpha=0.6, color="firebrick")
# create dynamic table of selected points
columns = [
TableColumn(field="x", title="X axis"),
TableColumn(field="y", title="Y axis"),
]
table = DataTable(
source=s2,
columns=columns,
width=400,
height=600,
sortable=True,
selectable=True,
editable=True,
)
# fancy javascript to link subplots
# js pushes selected points into ColumnDataSource of 2nd plot
# inspiration for this from a few sources:
# credit: https://stackoverflow.com/users/1097752/iolsmit via: https://stackoverflow.com/questions/48982260/bokeh-lasso-select-to-table-update
# credit: https://stackoverflow.com/users/8412027/joris via: https://stackoverflow.com/questions/34164587/get-selected-data-contained-within-box-select-tool-in-bokeh
s1.selected.js_on_change(
"indices",
CustomJS(
args=dict(s1=s1, s2=s2, table=table),
code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (var i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.change.emit();
table.change.emit();
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
""",
),
)
# create save button - saves selected datapoints to text file onbutton
# inspriation for this code:
# credit: https://stackoverflow.com/questions/31824124/is-there-a-way-to-save-bokeh-data-table-content
# note: savebutton line `var out = "x, y\\n";` defines the header of the exported file, helpful to have a header for downstream processing
savebutton = Button(label="Save", button_type="success")
savebutton.callback = CustomJS(
args=dict(source_data=s1),
code="""
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(file);
elem.download = 'selected-data.txt';
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
""",
)
# add Hover tool
# define what is displayed in the tooltip
tooltips = [
("X:", "#x"),
("Y:", "#y"),
("static text", "static text"),
]
fig02.add_tools(HoverTool(tooltips=tooltips))
# display results
# demo linked plots
# demo zooms and reset
# demo hover tool
# demo table
# demo save selected results to file
layout = grid([fig01, fig02, table, savebutton], ncols=3)
output_notebook()
show(layout)
# things to try:
# select random shape of blue dots with lasso tool in 'Select Here' graph
# only selected points appear as red dots in 'Watch Here' graph -- try zooming, saving that graph separately
# selected points also appear in the table, which is sortable
# click the 'Save' button to export a csv
# TODO: export from Bokeh to pandas dataframe