I'm trying to create an interactive filter in Bokeh for a graph representation
The source data come from a graph. The function SpotlightData(G) create the pandas dataframes with node and edge informations.
I need to pass the value from a range slider to the CDSView.
I try using js_on_change with a callback function (but I'm not a JS programmer).
Basically I want to create an array of bool values (evaluating a logical expression) and pass it to the view filter.
This is the code:
mytool = "pan , wheel_zoom , undo , reset"
plot = figure(title="Graph layout demonstration"
, tools=mytool
, width=800
, height=600
, background_fill_color='#efefef'
, toolbar_location = "below")
plot.axis.visible = False
plot.grid.grid_line_color = None
NodeDF , EdgeDF = SpotlightData(Spotlight)
Saldo_maxscale = math.ceil((NodeDF["Saldo"].abs().quantile(q=0.9))/1000)*1000
NodeSource = ColumnDataSource(NodeDF)
EdgeSource = ColumnDataSource(EdgeDF)
BF= BooleanFilter([True]*len(EdgeDF))
NodeView = CDSView(source=NodeSource ,filters=[])
EdgeView = CDSView(source=EdgeSource ,filters=[BF])
days_slider = RangeSlider(title="Dates",start=1,end=31,step=1,value=(1, 31))
callback = CustomJS(args=dict(source=EdgeDF["dt_ref_day"],boolout=BF.booleans), code="""
const data = source;
const s = cb_obj.value[0];
const e = cb_obj.value[1];
for (let i = 0; i < data.length; i++) {
boolout[i] = data[i]>=s && data[i]<=e
}
boolout.change.emit();
""")
days_slider.js_on_change('value', callback)
graph = GraphRenderer()
graph.layout_provider = StaticLayoutProvider(graph_layout=nx.spring_layout(Spotlight))
graph.node_renderer.data_source = NodeSource
graph.edge_renderer.data_source = EdgeSource
graph.node_renderer.view = NodeView
graph.edge_renderer.view = EdgeView
NodeColor = LinearColorMapper(palette = cc.CET_D3, low=(Saldo_maxscale*-1), high=Saldo_maxscale)
graph.node_renderer.glyph = Circle(size="LogSize",fill_color=transform('Saldo', NodeColor))
graph.edge_renderer.glyph = MultiLine(line_alpha=0.5, line_width="LogSize")
plot.renderers.append(graph)
show(column(days_slider, plot))
But nothing change in the visualization despite moving the slider.
Thank you all I'm quite a noob with Bokeh
PS: There is a constraint: In this case I can't run a bokeh server
Related
I try to make a dashboard with bokeh but my hover tool got broken, since I tried to plot the data in a single diagram. I found some answers to this problem, but unfortunately I can't adapt it to my code.
I want to be able to turn off and on the visibility of data.
(Another problem is, that the whole plot vanishes, if you change the daterange to an area, where no values exist, maybe someone has a workaround for this. In an older version I tried to only change source emit, when range is on data, but this leads to other problems. Maybe someone has an idea for that too.)
Here is a reduced minimal working version of my code:
import pandas as pd
import numpy as np
from datetime import datetime
from bokeh.models import Button, CheckboxGroup, ColumnDataSource, CustomJS, DatetimeTickFormatter, HoverTool
from bokeh.models.widgets import DateRangeSlider
from bokeh.layouts import layout, column, row
from bokeh.palettes import Category20
from bokeh.plotting import figure, output_file, show
datesX = pd.date_range(start='1/1/2018', periods=100) #incl. '2018-01-01' to '2018-04-10'
numbof = 3
datesX = datesX[0:10].union(datesX[20:100])
valuesY = pd.DataFrame(np.random.randint(0,25,size=(90, numbof)), columns=list((f'V{i}' for i in range(numbof))))
movingY = pd.DataFrame(np.random.randint(0,10,size=(90, numbof)), columns=list((f'M{i}' for i in range(numbof))))
preCDS ={}
for i in range(numbof):
preCDS.update({f'x{i}': datesX})
preCDS.update({f'y{i}': valuesY[f'V{i}']})
preCDS.update({f'm{i}': movingY[f'M{i}']})
source = ColumnDataSource(preCDS)
source2 = ColumnDataSource(preCDS)
# output to static HTML file
output_file('file.html')
# create a new plot with a title and axis labels
p = figure(
title='file1', x_axis_label='Date', y_axis_label='yValue',
y_range=(0, 30), x_axis_type='datetime',
tools="pan, wheel_zoom, box_zoom, reset, save",
plot_width=1800, plot_height=480)
ypsilon = [f'y{i}' for i in range(numbof)]
em = [f'm{i}' for i in range(numbof)]
hover = HoverTool(line_policy='nearest', names=ypsilon + em,
tooltips=[('Timestamp', '#x{%Y-%m-%d %H:%M:%S}'), ('Wert', '#y')], ###what to do with #x and #y to make it iterable: x0, x1, x2, y0, ...
formatters={'#x': 'datetime'})
p.add_tools(hover)
date_range_slider = DateRangeSlider(
title="DateRange", start=datesX[0], end=datesX[89],
value=(datesX[0], datesX[89]), width=800, bar_color = (32,120,180))
checkbox_vals = CheckboxGroup(labels=[f'Plot {i+1}' for i in range(numbof)], active=[i for i in range(numbof)])
checkbox_mova = CheckboxGroup(labels=[f'Plot {i+1}' for i in range(numbof)], active=[i for i in range(numbof)])
vals=[]
mova=[]
for i in range(numbof):
vals.append(p.circle(x=f'x{i}', y=f'y{i}', source=source, color = Category20[20][2*i], size=3, name=f'y{i}'))
mova.append(p.line(x=f'x{i}', y=f'm{i}', source=source, line_color = Category20[20][2*i+1], line_width=2, name=f'm{i}'))
rangeCallback = CustomJS(args=dict(source=source, ref_source=source2, dRs=date_range_slider), code="""
// print out array of date from, date to
//console.log(dRs.value);
// dates returned from slider are not at round intervals and include time
const date_from = Date.parse(new Date(dRs.value[0]).toDateString());
const date_to = Date.parse(new Date(dRs.value[1]).toDateString());
//console.log(date_from, date_to)
// Creating the Data Sources
const data = source.data;
const ref = ref_source.data;
for (const [key, value] of Object.entries(data)) {
console.log(key)
// Creating new Array and appending correctly parsed dates
if(key.indexOf("x") == 0){
let new_ref = [];
let from_pos = [];
let to_pos = [];
ref[key].forEach(elem => {
elem = Date.parse(new Date(elem).toDateString());
new_ref.push(elem);
})
// Creating Indices with new Array
from_pos[key] = new_ref.indexOf(date_from);
to_pos[key] = new_ref.indexOf(date_to) + 1;
// re-create the source data from "reference"
const dataKeyM = key.replace('x', 'm');
const dataKeyY = key.replace('x', 'y');
data[dataKeyM] = ref[dataKeyM].slice(from_pos[key], to_pos[key]);
data[key] = ref[key].slice(from_pos[key], to_pos[key]);
data[dataKeyY] = ref[dataKeyY].slice(from_pos[key], to_pos[key]);
}
}
/*if (new_ref.includes(date_from) && new_ref.includes(date_to)) {
source.change.emit();
}*/
source.change.emit();
""")
checkboxValsCallback = CustomJS(args=dict(vals=vals, checkboxVals=checkbox_vals), code="""
var indexOf = [].indexOf || function(item) {
for (var i = 0, l = this.length; i < l; i++) {
if (i in this && this[i] === item)
return i;
}
return -1;
};
//vals.visible = indexOf.call(checkboxVals.active,0)>=0;
for (let i=0;i<vals.length;i++) {
vals[i].visible = indexOf.call(checkboxVals.active,i)>=0;
}
""")
checkboxMovaCallback = CustomJS(args=dict(mova=mova, checkboxMova=checkbox_mova), code="""
var indexOf = [].indexOf || function(item) {
for (var i = 0, l = this.length; i < l; i++) {
if (i in this && this[i] === item)
return i;
}
return -1;
};
//mova.visible = indexOf.call(checkboxMova.active,0)>=0;
for (let i=0;i<mova.length;i++) {
mova[i].visible = indexOf.call(checkboxMova.active,i)>=0;
}
""")
checkbox_vals.js_on_change('active', checkboxValsCallback)
checkbox_mova.js_on_change('active', checkboxMovaCallback)
date_range_slider.js_on_change('value', rangeCallback)
b1 = Button(label="select all", max_width = 300, button_type="primary")
b1.js_on_click(CustomJS(args=dict(s=checkbox_vals, t=checkbox_mova), code="""
s.active = [0,1,2]
t.active = [0,1,2]
"""))
b2 = Button(label="unselect all", max_width = 300)
b2.js_on_click(CustomJS(args=dict(s=checkbox_vals, t=checkbox_mova), code="""
s.active = []
t.active = []
"""))
layout = column(p, row(checkbox_vals, checkbox_mova, date_range_slider), row(b1, b2))
show(layout)
Thank You in advance.
For the tooltips: you specified x and y for the hover but when you look at the datasource of e.g. your line glyphs stored in mova you will see there is no x or y but many different fields. I changed the tooltips to x1 and y0 but I am not sure if this is wat you want. Just put a breakpoint somewhere and look e.g. in the mova[0].data_source.data and you will see all the available fields and choose the ones you want for your tooltips.
hover = HoverTool(line_policy='nearest', names=ypsilon + em,
tooltips=[('Timestamp', '#x1{%Y-%m-%d %H:%M:%S}'), ('Wert', '#y0')],
formatters={'#x1': 'datetime'})
My goal is to define scale bars programmatically, that have a fixed length, but may be changed afterwards by the graphic designer.
I have come sofar as to define a closed path within the document:
def addLine(doc):
def point(x, y):
result = Dispatch("Photoshop.PathPointInfo")
result.Kind = 2 # for PsPointKind --> 2 (psCornerPoint)
result.LeftDirection = result.rightDirection = result.Anchor = (x, y)
return result
points = [
point(100, 100),
point(200, 100),
point(200, 110),
point(100, 110)
]
lineSubPathArray = Dispatch("Photoshop.SubPathInfo")
lineSubPathArray.Operation = 1 #for PsShapeOperation --> 1 (psShapeAdd
lineSubPathArray.Closed = True
lineSubPathArray.EntireSubPath = points
myPathItem = doc.PathItems.Add("bar-path", [lineSubPathArray])
From here, I can load the saved document back into photoshop (CS6) and then create a shape layer manually: Layer | New fill layer | solid color ...
This results in a shape layer, similar to what I get by using the line tool, in which the line effectively is a rectangle whose height may be changed later.
First question: how to create the fill layer by using the API?
Second: I defined a rectangle 100pixels wide, but I get one 418 pixels wide. The doc has its doc.application.preferences.rulerUnits set to psPixels (1). Why is this?
Last: Isn't it possible to define a line as true line defined by two end points and set its stroke width instead of it's height?
This may be of use:
You can define a new colour with new SolidColor()
// define fillColor
var fillColor = new SolidColor();
var myColour = [57, 181,74];
fillColor.rgb.red = myColour[0];
fillColor.rgb.green = myColour[1];
fillColor.rgb.blue = myColour[2];
and then fill your path by adding myPathItem.fillPath(fillColor,ColorBlendMode.NORMAL,100,false,0,true,true);
// Switch off any dialog boxes
displayDialogs = DialogModes.NO; // OFF
var originalUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// call the source document
var srcDoc = app.activeDocument;
create_path("bar-path");
function create_path(linename)
{
var points = [
[100, 100],
[200, 100],
[200, 110],
[100, 110]
];
// create the array of PathPointInfo objects
var lineArray = new Array();
for (var i = 0; i < points.length; i++)
{
lineArray[i] = new PathPointInfo;
lineArray[i].kind = PointKind.CORNERPOINT;
lineArray[i].anchor = points[i];
lineArray[i].leftDirection = lineArray[i].anchor;
lineArray[i].rightDirection = lineArray[i].anchor;
}
// create a SubPathInfo object, which holds the line array in its entireSubPath property.
var lineSubPathArray = new Array();
lineSubPathArray.push(new SubPathInfo());
lineSubPathArray[0].operation = ShapeOperation.SHAPEXOR;
lineSubPathArray[0].closed = true;
lineSubPathArray[0].entireSubPath = lineArray;
//create the path item, passing subpath to add method
var myPathItem = srcDoc.pathItems.add(linename, lineSubPathArray);
// define fillColor
var fillColor = new SolidColor();
var myColour = [57, 181,74];
fillColor.rgb.red = myColour[0];
fillColor.rgb.green = myColour[1];
fillColor.rgb.blue = myColour[2];
//fill the path so we can see something also
myPathItem.fillPath(fillColor,ColorBlendMode.NORMAL,100,false,0,true,true);
// deselect path
deselect_path();
}
// switch back to normal
app.preferences.rulerUnits = originalUnits;
// Set Display Dialogs back to normal
displayDialogs = DialogModes.ALL; // NORMAL
function deselect_path()
{
// =======================================================
var idslct = charIDToTypeID( "slct" );
var desc76 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref63 = new ActionReference();
var idPath = charIDToTypeID( "Path" );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref63.putEnumerated( idPath, idOrdn, idTrgt );
desc76.putReference( idnull, ref63 );
var idselectionModifier = stringIDToTypeID( "selectionModifier" );
var idselectionModifierType = stringIDToTypeID( "selectionModifierType" );
var idremoveFromSelection = stringIDToTypeID( "removeFromSelection" );
desc76.putEnumerated( idselectionModifier, idselectionModifierType, idremoveFromSelection );
executeAction( idslct, desc76, DialogModes.NO );
}
As for rectangle being too large: What resolution and units do you have the psd file set to? The script below will switch to pixels, and set the resolution of your document to 72dpi
As for doing a stroke to replace the line. Well... you've got options.
You can do a brush stroke with a two point line:
var idStrk = charIDToTypeID( "Strk" );
var desc105 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref35 = new ActionReference();
var idPath = charIDToTypeID( "Path" );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref35.putEnumerated( idPath, idOrdn, idTrgt );
desc105.putReference( idnull, ref35 );
var idUsng = charIDToTypeID( "Usng" );
var idPbTl = charIDToTypeID( "PbTl" );
desc105.putClass( idUsng, idPbTl );
executeAction( idStrk, desc105, DialogModes.NO );
However, I don't think that's what your after. You can turn your path into a shape and then have a stroke and fill colour assigned. However... you need a minimum of 3 points for that to work.
As a suggestion, instead of a path create a shape from start - which oddly will work with a minimum of two points. But I have no idea how to do that in code!
There are two lessons I have learned form translating the solution by Ghoul Fool into Python:
using COM in python, one can make a lot of mistakes that go unnoticed, otherwise than cryptic error messages far beside the point. Thies include syntactic errors such as missing/redundant parentheses and wrong capitalisation.
solutions that work with COM in python do not work the same with the Python-Photoshop-API and vice versa, even though, following the source code of the library, apparently exactly the same thing is happening. I have not always been able to divine solutions that work both ways.
Here is, for those who have come to this question later, what I have come to to get a shape layer with a bar that can be changed in height afterwards. The action that is called is nothing more than a recording of New Fill Layer from the layer-menu (CS6):
def makeBar(doc):
app = doc.parent
app.preferences.rulerUnits = Units.Pixels
def point(x, y):
result = Dispatch("Photoshop.PathPointInfo")
result.Kind = PointKind.CornerPoint
result.LeftDirection = result.rightDirection = result.Anchor = (x, y)
return result
points = [
point(100, 100),
point(200, 100),
point(200, 110),
point(100, 110)
]
lineSubPathArray = Dispatch("Photoshop.SubPathInfo")
lineSubPathArray.Operation = ShapeOperation.ShapeAdd
lineSubPathArray.Closed = True
lineSubPathArray.EntireSubPath = points
doc.PathItems.Add("bar-path", [lineSubPathArray])
app = Dispatch("Photoshop.Application")
doc = app.Open(self.fileName)
As for my second question: The unit of the path's cornerpoints are invariantly in Points, no matter what he rulers are set to.
I am currently working on creating a Python-Bokeh based webapp Application running on server.In this application,user can preview the data from a pandas dataframe(shown using BOKEH DATATABLE) and can modify the data as per business needs. After refreshing the dataframe,the user will need to export the dataframe to his local system(in csv format) by clicking a BOKEH BUTTON widget.
Now i was able to create the webapp application but it is causing issues while exporting the data to the local system. When i click on the button to download, a csv file gets downloaded having the intial default data present in the dataframe.After that even though I update the dataframe and again click on the Download BUTTON,same old default data is getting downloaded instead of the updated dataframe.
Below is the code which I tried at my end. Please suggest, what changes need to be done to the below snippet so that everytime the data gets refreshed and download button is clicked it will export the latest data showing in the datatable.
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import DataTable, NumberFormatter, TableColumn, Button
from bokeh.layouts import column,row
import pandas as pd
import numpy as np
# Create Default data
data = {'COL_1_DEFAULT': np.random.randint(200, size=100),
'COL_2_DEFAULT': np.random.randint(200, size=100),
'COL_3_DEFAULT': np.random.randint(200, size=100),
'COL_4_DEFAULT': np.random.randint(200, size=100)}
TABLE1_DATA = pd.DataFrame(data)
source_new = ColumnDataSource(TABLE1_DATA)
Columns_tab1 = [TableColumn(field=Ci, title=Ci) for Ci in TABLE1_DATA.columns] # bokeh columns
data_table1 = DataTable(columns=Columns_tab1, source=source_new,height = 200)
# Javascript for exporting data
js_code="""
var data = source.data;
var columns = Object.keys(source.data);
var filetext = [columns.join(',')].shift().concat('\\n');
var nrows = source.get_length();
for (let i=0; i < nrows; i++) {
let currRow = [];
for (let j = 0; j < columns.length; j++) {
var column = columns[j]
currRow.push(source.data[column][i].toString())
}
currRow = currRow.concat('\\n')
var joined = currRow.join();
filetext = filetext.concat(joined);
}
var filename = 'data_output.csv';
var blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' });
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, filename);
}
else {
var link = document.createElement("a");
link = document.createElement('a')
link.href = URL.createObjectURL(blob);
link.download = filename
link.target = "_blank";
link.style.visibility = 'hidden';
link.click();
URL.revokeObjectURL(link.href);
}
"""
# Created Button for Export
Exportbutton = Button(label="Download", button_type="success")
Exportbutton.js_on_click(CustomJS(args=dict(source=source_new),code = js_code ))
def refresh_table():
global source_new,Columns_tab1,data_table1,button
global TABLE1_DATA
df = []
for ii in range(1, 11):
df.append({'x': ii,
'y': 1000 * np.random.rand(),
'z' : 11 * np.random.rand()
})
df = pd.DataFrame(df)
source_new = ColumnDataSource(df)
Columns_tab1 = [TableColumn(field='x', title='Col 1'),TableColumn(field='y', title='Col 2',formatter=NumberFormatter(format='$0,0.00',text_align='right')),TableColumn(field='z', title='Col 3')]
data_table1.update(source=source_new, columns=Columns_tab1, width=500, height=200)
Exportbutton.js_on_click(CustomJS(args=dict(source=ColumnDataSource(df)),code = js_code ))
# Created Button for Refreshing dataframe data
table_Refresh_Button = Button(label='Refresh Table',height = 30,width = 200,button_type="success")
table_Refresh_Button.on_click(refresh_table)
layout = column(data_table1,table_Refresh_Button, Exportbutton)
curdoc().add_root(layout)
It's a bug. I just created https://github.com/bokeh/bokeh/issues/10146
As a workaround, replace
Exportbutton.js_on_click(CustomJS(args=dict(source=ColumnDataSource(df)),code = js_code ))
with
Exportbutton.js_event_callbacks['button_click'] = [CustomJS(args=dict(source=ColumnDataSource(df)), code=js_code)]
I want to create a candlestick plot in bokeh and dynamically update the x-axis according to user input through a MultiSelect widget. Basically the user will choose a few items in the MultiSelect and then I would like those to become the values of the x axis. I already have set up the MultiSelect widget and confirmed that it is working by attaching a DataTable to the MultiSelect and having it update accordingly (which it does). I just need help retrieving the values from the MultiSelect widget and setting them as my plot.x_range. Based on a few github/issues posts (like this one: https://github.com/bokeh/bokeh/issues/4022) I tried using a FactorRange, but it isn't working. Currently the behavior is the x axis labels stay set to the values that are set during the initial configuration of the MultiSelect ('aaa' and 'bbb'), and don't change when I choose different values in the MultiSelect widget.
Here's a code sample:
### SET UP SOURCE DF INFO ###
tab2_source = ColumnDataSource(df)
tab2_original_source = ColumnDataSource(df)
columns_t2 = [TableColumn(field='Gene', title='Gene'), TableColumn(field='row_min', title='min'), TableColumn(field='row_max', title='max'),
TableColumn(field='quantile_25', title='25th quantile'), TableColumn(field='quantile_50', title='50th quantile'), TableColumn(field='quantile_75', title='75th quantile')]
data_table_t2 = DataTable(source=tab2_source, columns=columns_t2, reorderable=False)
### STUFF FOR THE WIDGETS ###
# customJS stuff
tab2_callback_code = """
var t2_data = tab2_source.data;
var t2_original_data = tab2_original_source.data;
var gene_t2 = gene_select_obj_t2.value;
console.log("gene: " + gene_t2);
for (var key in t2_original_data) {
t2_data[key] = [];
for (var i = 0; i < t2_original_data['Gene'].length; ++i) {
if (t2_original_data['Gene'][i] === gene_t2[0] || t2_original_data['Gene'][i] === gene_t2[1]) {
t2_data[key].push(t2_original_data[key][i]);
}
}
}
tab2_source.change.emit();
target_obj_t2.change.emit();
"""
# make drop-down selectors
select_gene_t2 = MultiSelect(title = "Select up to 2 Genes to View:", value=['aaa', 'bbb'], options=list(df.Gene.unique()))
# define the callback objects now that the select widgets exist
tab2_callback = CustomJS(
args=dict(tab2_source=tab2_source,
tab2_original_source=tab2_original_source,
gene_select_obj_t2=select_gene_t2,
target_obj_t2=data_table_t2),
code=tab2_callback_code)
# connect the callbacks to the filter widgets
select_gene_t2.js_on_change('value', tab2_callback)
## PLOTTING ##
p2 = figure(plot_width=500, plot_height=400, title='Avg Gene Exp', y_range=(-2,12), x_range=FactorRange())
p2.x_range.factors = list(select_gene_t2.value)
p2.vbar(x='Gene', top='quantile_75', bottom='quantile_25', source=tab2_source, width=0.5, fill_color="#D5E1DD", line_color="black")
tab2 = Panel(child=column(select_gene_t2, p2, data_table_t2), title="Exp by Gene")
tabs = Tabs(tabs=[tab2])
save(tabs)
For anyone who is interested, this is the best solution I found:
Add this line to the tab2_callback_code (plot_xr references my figure p2 - you will also need to add these references in the CustomJS args dict):
plot_xr.x_range.factors = gene_t2;
Also it is important to instantiate the figure before the tab2_callback_code is called. This worked for me and now my x_range values change as I select different options in the MultiSelect.
I'm trying to change plot width when taptool selects something.
I invoke this using bokeh serve, and navigating to the localhost website
when I click on one of the rectangles, my console prints out "callback" and "callback2" but my plot doesn't change width.
What am I doing wrong?
counts = [1*10**7,2*10**7,3*10**7] #dummy data
l_edge = [x for x in range(len(counts))]
r_edge = [x + .85 for x in range(len(counts))]
data = {
'height': counts,
'leftEdges': l_edge,
'rightEdges': r_edge,
}
p = figure()
s = ColumnDataSource(data)
p.add_tools(TapTool())
def callbackfcn(attr,old,new):
global p
print('callback')
p.width = np.random.choice([100,200,300,1000,10000])
p.height = np.random.choice([100,200,300,1000,10000])
print('callback2')
r = p.quad(top='height',bottom=0,
left = 'leftEdges',right = 'rightEdges',
source = s)
r.data_source.on_change('selected',callbackfcn)
layout = column(p)
curdoc().add_root(layout)
Have you tried instead including sizing_mode in your layout? Take a look at the Bokeh docs for an example.