I'm trying to plot some datapoint on a map in Bokeh but somehow nothing shows up, only the map background.
import pandas as pd
from IPython.core.display import HTML, display
%matplotlib inline
sample = pd.DataFrame({'Lat': [40.7260,40.7209], 'Lon': [-73.991,-74.0507], 'Count': 1})
from bokeh.plotting import figure, output_notebook, show
output_notebook()
from bokeh.tile_providers import STAMEN_TERRAIN
x_range, y_range = ((-8242000,-8210000), (4965000,4990000))
plot_width = int(750)
plot_height = int(plot_width//1.2)
def base_plot(tools='pan,wheel_zoom,reset',plot_width=plot_width, plot_height=plot_height, **plot_args):
p = figure(tools=tools, plot_width=plot_width, plot_height=plot_height,
x_range=x_range, y_range=y_range, outline_line_color=None,
min_border=0, min_border_left=0, min_border_right=0,
min_border_top=0, min_border_bottom=0, **plot_args)
p.axis.visible = False
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
return p
p = base_plot()
p.add_tile(STAMEN_TERRAIN)
p.circle(x=samples['Lat'], y=samples['Lon'], **options)
show(p)
Thanks for advice.
The plot ranges are in Web Mercator units:
((-8242000,-8210000), (4965000,4990000))
But the data points in your sample DataFrame are in lat/lon units. You can either:
add an "extra range" in lat/lon units (that match up!) and have p.circle reference the extra range instead of the default range.
Convert your circle coordinates to Web Mercator
The latter is probably easier. This page has a function that can do the conversion. Using it, you'd get
sample = pd.DataFrame({
'easting': [-8236640.443285105, -8243286.216885463],
'northing': [4972010.345629457, 4971261.231184175]
})
Updating your code to use this:
import pandas as pd
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.tile_providers import STAMEN_TERRAIN
samples = pd.DataFrame({
'easting': [-8236640.443285105, -8243286.216885463],
'northing': [4972010.345629457, 4971261.231184175]
})
x_range, y_range = ((-8242000,-8210000), (4965000,4990000))
plot_width = int(750)
plot_height = int(plot_width//1.2)
def base_plot(tools='pan,wheel_zoom,reset',plot_width=plot_width, plot_height=plot_height, **plot_args):
p = figure(tools=tools, plot_width=plot_width, plot_height=plot_height,
x_range=x_range, y_range=y_range, outline_line_color=None,
min_border=0, min_border_left=0, min_border_right=0,
min_border_top=0, min_border_bottom=0, **plot_args)
p.axis.visible = False
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
return p
p = base_plot()
p.add_tile(STAMEN_TERRAIN)
p.circle(x=samples['easting'], y=samples['northing'], size=20, color="red")
output_file("map.html")
show(p)
yields this plot:
Related
I am trying to get to something like this but with more cats for each scenario (I have 4 scenarios but many cats):
I can only achieve this when the number of 'Cat's is equal to the number of 'Scenario's. I don't fully understand how the factors line in the code is working and I think the answer lies within that.
whenever I add more Cats I get this error:
IndexError: list index out of range
The code I have is follows:
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.models import Range1d
from calendar import month_abbr
import numpy as np
from bokeh.palettes import Spectral3
from bokeh.transform import factor_cmap
systems = ["Scenario1", "Scenario2", "Scenario3", "Scenario4"]
subsystems =["Cat1","Cat2", "Cat3", "Cat4"]#, "Cat5", "Cat6"]
factors =[(systems[ind],subsystem) for ind, subsystem in enumerate(subsystems) for subsystem in subsystems]
count_closed = [52,52,49,26,9,8, 32,20]#,33,66,9,8]
count_open = [0,0,1, 0]
count_waived = [3,1,0,0]
statuses = ["count_closed", "count_open", "count_waived"]
data = dict(factors = factors, count_closed=count_closed, count_open=count_open, count_waived=count_waived )
source = ColumnDataSource(data=data)
p = figure(x_range = FactorRange(*factors), plot_height=250, title="Repeat 10 cats for each scenario",
toolbar_location = 'right',
tools = "hover", tooltips="$name #subsystems: #$name")
p.vbar_stack(statuses, x="factors", width=0.9, alpha = 0.5, color=["navy","red","pink"], source=source, legend_label=statuses)
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
p.legend.location = "top_center"
p.legend.orientation = "horizontal"
show(p)
To generate a list with tuples of all combinations, your variable factors, you can use
from itertools import product
factors = list(product(systems, subsystems))
This will create a list which is understood by bokehs FactorRange.
Complete Example
from itertools import product
import numpy as np
from bokeh.models import ColumnDataSource, FactorRange, Range1d
from bokeh.plotting import figure, show, output_notebook
output_notebook()
systems = ["Scenario1", "Scenario2", "Scenario3", "Scenario4"]
subsystems =["Cat1","Cat2", "Cat3", "Cat4", "Cat5", "Cat6"]
factors = list(product(systems, subsystems))
count_closed = np.random.randint(0,20, len(factors))
count_open = np.random.randint(0,20, len(factors))
count_waived = np.random.randint(0,20, len(factors))
statuses = ["count_closed", "count_open", "count_waived"]
data = dict(factors = factors, count_closed=count_closed, count_open=count_open, count_waived=count_waived )
source = ColumnDataSource(data=data)
p = figure(x_range = FactorRange(*factors), plot_height=250, title="Repeat 10 cats for each scenario",
toolbar_location = 'right',
tools = "hover", tooltips="$name #subsystems: #$name")
p.vbar_stack(statuses, x="factors", width=0.9, alpha = 0.5, color=["navy","red","pink"], source=source, legend_label=statuses)
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
p.legend.location = "top_center"
p.legend.orientation = "horizontal"
show(p)
Output
I am able to use Bokeh to plot glyphs from a geopandas dataframe over a Google Map using the gmap() function.
from bokeh.io import output_notebook, show, output_file
from bokeh.plotting import figure
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar
from bokeh.palettes import brewer#Input GeoJSON source that contains features for plotting.
import json
from bokeh.models import ColumnDataSource, GMapOptions
from bokeh.plotting import gmap
def make_dataset(df, candidate):
#df_copy = df.copy()
df_copy = get_df(candidate)
merged_json = json.loads(df_copy.to_json())#Convert to String like object.
json_data = json.dumps(merged_json)
geosource = GeoJSONDataSource(geojson = json_data)
return geosource
def make_plot(candidate):
src = make_dataset(df,candidate)
#Input GeoJSON source that contains features for plotting.
p = figure(title = 'Results of candidate X', plot_height = 600 , plot_width = 950, toolbar_location = None)
map_options = GMapOptions(lat=42, lng=44, map_type="roadmap", zoom=7)
p = gmap("my-key", map_options, title="Austin")
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None#Add patch renderer to figure.
p.patches('xs','ys', source = src,fill_color = {'field' :'results', 'transform' : color_mapper},
line_color = 'black', line_width = 0.25, fill_alpha = 1)#Specify figure layout.
p.add_layout(color_bar, 'below')#Display figure inline in Jupyter Notebook.
output_notebook()#Display figure.
return p
It gives me:
However when I plot using Carto as a provider as explained here there is an error in the axes:
tile_provider = get_provider(Vendors.CARTODBPOSITRON)
# range bounds supplied in web mercator coordinates
p = figure(x_range=(-2000000, 6000000), y_range=(-1000000, 7000000))#, x_axis_type="mercator", y_axis_type="mercator")
p.add_tile(tile_provider)
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None#Add patch renderer to figure.
p.patches('xs','ys', source = src,fill_color = {'field' :'results', 'transform' : color_mapper},
line_color = 'black', line_width = 0.25, fill_alpha = 1)#Specify figure layout.
p.add_layout(color_bar, 'below')#Display figure inline in Jupyter Notebook.
output_notebook()#Display figure.
return p
So it is located wrong in the map, where one can see the red circle:
Looks like the map is in EPSG:3857 ("web mercator") while my source is probably in EPSG:4326. How can I do to plot it correctly?
Here is the first few lines of my data:
id parent_id common_id common_name has_children shape_type_id \
64 70140 69935 3 63-3 False 4
65 70141 69935 2 63-2 False 4
66 70142 69935 5 63-5 False 4
67 70143 69935 6 63-6 False 4
68 70144 69935 8 63-8 False 4
shape_type_name value color title_location results \
64 Precinct No Data None Precinct: 63-3 65.16
65 Precinct No Data None Precinct: 63-2 57.11
66 Precinct No Data None Precinct: 63-5 54.33
67 Precinct No Data None Precinct: 63-6 59.15
68 Precinct No Data None Precinct: 63-8 61.86
turnout \
64 {'pct': 46.38, 'count': 686.0, 'eligible': 1479}
65 {'pct': 49.62, 'count': 394.0, 'eligible': 794}
66 {'pct': 58.26, 'count': 624.0, 'eligible': 1071}
67 {'pct': 57.54, 'count': 492.0, 'eligible': 855}
68 {'pct': 50.75, 'count': 506.0, 'eligible': 997}
geometry
64 POLYGON ((42.18180 42.18530, 42.18135 42.18593...
65 POLYGON ((42.20938 42.20621, 42.21156 42.20706...
66 POLYGON ((42.08429 42.20468, 42.08489 42.20464...
67 POLYGON ((42.16270 42.16510, 42.16661 42.16577...
68 POLYGON ((42.16270 42.16510, 42.16315 42.16640...
You have to reproject your data from EPSG:4326 to EPSG:3857
Here's a solution with some GeoJSON data:
# requirements
# !pip install pandas numpy bokeh geopandas
import pandas as pd
import numpy as np
def lon_to_web_mercator(lon):
k = 6378137
return lon * (k * np.pi / 180.0)
def lat_to_web_mercator(lat):
k = 6378137
return np.log(np.tan((90 + lat) * np.pi / 360.0)) * k
def wgs84_to_web_mercator(df, lon="lon", lat="lat"):
"""Converts decimal longitude/latitude to Web Mercator format"""
k = 6378137
df["x"] = df[lon] * (k * np.pi / 180.0)
df["y"] = np.log(np.tan((90 + df[lat]) * np.pi / 360.0)) * k
return df
BerlinWGS84 = [13.08835, 13.76116, 52.33826, 52.67551]
Berlin = x_range, y_range = ((lon_to_web_mercator(BerlinWGS84[0]), lon_to_web_mercator(BerlinWGS84[1])),
(lat_to_web_mercator(BerlinWGS84[2]), lat_to_web_mercator(BerlinWGS84[3])))
# plot it
from bokeh.plotting import figure, show, output_notebook
from bokeh.tile_providers import get_provider, Vendors
output_notebook()
tile_provider = get_provider(Vendors.CARTODBPOSITRON)
# range bounds sgupplied in web mercator coordinates
p = figure(x_range=x_range, y_range=y_range,
x_axis_type="mercator", y_axis_type="mercator")
p.add_tile(tile_provider)
show(p)
# geopandas
import geopandas as gpd
import requests
def remoteGeoJSONToGDF(url, display=False):
# source: https://medium.com/#maptastik/remote-geojson-to-geodataframe-19c3c1282a64
"""Import remote GeoJSON to a GeoDataFrame
Keyword arguments:
url -- URL to GeoJSON resource on web
display -- Displays geometries upon loading (default: False)
"""
r = requests.get(url)
data = r.json()
gdf = gpd.GeoDataFrame.from_features(data['features'])
if display:
gdf.plot()
return gdf
url = 'https://gist.githubusercontent.com/sabman/96730f5949576e7793a3f79eb390f90c/raw/7ffcf34239175cafcc9a63382e6beacd0cab9fa9/BerlinFeatures.geojson'
gdf = remoteGeoJSONToGDF(url)
gdf.plot()
# make sure initial projection is defined
gdf.crs = {'init': 'epsg:4326'}
gdf_webmerc = gdf.copy()
# reproject
gdf_webmerc = gdf['geometry'].to_crs(epsg=3857)
gdf_webmerc.plot()
from bokeh.models import GeoJSONDataSource
geo_source = GeoJSONDataSource(geojson=gdf_webmerc.to_json())
# let's plot and look
p.circle(x='x', y='y', size=15, alpha=0.7, source=geo_source)
show(p)
I'm creating a nested categorical bar chart with bokeh and pandas. I tested the exampled included in Bokeh docs (shown below)
from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.palettes import Spectral5
from bokeh.sampledata.autompg import autompg_clean as df
from bokeh.transform import factor_cmap
output_file("bar_pandas_groupby_nested.html")
df.cyl = df.cyl.astype(str)
df.yr = df.yr.astype(str)
group = df.groupby(by=['cyl', 'mfr'])
index_cmap = factor_cmap('cyl_mfr', palette=Spectral5, factors=sorted(df.cyl.unique()), end=1)
p = figure(plot_width=800, plot_height=300, title="Mean MPG by # Cylinders and Manufacturer",
x_range=group, toolbar_location=None, tooltips=[("MPG", "#mpg_mean"), ("Cyl, Mfr", "#cyl_mfr")])
p.vbar(x='cyl_mfr', top='mpg_mean', width=1, source=group,
line_color="white", fill_color=index_cmap, )
p.y_range.start = 0
p.x_range.range_padding = 0.05
p.xgrid.grid_line_color = None
p.xaxis.axis_label = "Manufacturer grouped by # Cylinders"
p.xaxis.major_label_orientation = 1.2
p.outline_line_color = None
show(p)
I'm trying to apply this with my out set of data. However, we i run the script i get the error
Js error
This is my code:
def test(data):
output_file("bar_pandas_groupby_nested.html")
print(df.head())
data.prueba = data.prueba.astype(str)
data.inst_nombre_institucion = data.inst_nombre_institucion.astype(str)
group = data.groupby(by=['prueba', 'inst_nombre_institucion'])
index_cmap = factor_cmap('prueba_inst_nombre_institucion', palette=Spectral5, factors=sorted(data.prueba.unique()), end=1)
p = figure(plot_width=800, plot_height=300, title="Mean",
x_range=group, toolbar_location=None, tooltips=[("MPG", "#media_mod_ingles_mean"), ("prueba, institucion", "#prueba_inst_nombre_institucion")])
p.vbar(x='prueba_inst_nombre_institucion', top='media_mod_ingles_mean', width=1, source=group,
line_color="white", fill_color=index_cmap, )
p.y_range.start = 0
p.x_range.range_padding = 0.05
p.xgrid.grid_line_color = None
p.xaxis.axis_label = "Mean"
p.xaxis.major_label_orientation = 1.2
p.outline_line_color = None
show(p)
return True
And my data looks like this:
data.head()
Why do i get this error?
Thanks for your time!
UPDATE:
data.csv and script can be downloaded here
The issue is that your labels are way, way too long to fit inside a 300px high plot when they are oriented vertical-ish. If I change the orientation to
p.xaxis.major_label_orientation = 0.2
Then the plot can render, but you can also see the problem:
Alternatively, if I make the labels actually nearly vertical (~pi/2), and make the plot height be 800px, everything is visible:
But I would say that still is fairly hard to interpret/read. I would suggest trying to find shorter strings to use for your categories.
I am creating a bokeh plot with a slider to refresh plot accordingly. There are 2 issues with the code posted.
1. The plot is not refreshed as per the slider. Please help in providing a fix for this issue.
2. Plot is not displayed with curdoc() when bokeh serve --show fn.ipynb is used
I'm trying to visualise this CSV file.
import pandas as pd
import numpy as np
from bokeh.models import ColumnDataSource, CategoricalColorMapper, HoverTool, Slider
from bokeh.plotting import figure, curdoc
from bokeh.palettes import viridis
from bokeh.layouts import row, widgetbox
#Importing and processing data file
crop = pd.read_csv('crop_production.csv')
#Cleaning Data
crop.fillna(np.NaN)
crop['Season'] = crop.Season.str.strip()
#Removing Whitespace #Filtering the dataset by Season
crop_season = crop[crop.Season == 'Whole Year']
crop_dt = crop_season.groupby(['State_Name', 'District_Name', 'Crop_Year']).mean().round(1)
#Creating Column Data Source
source = ColumnDataSource({
'x' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].Area,
'y' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].Production,
'state' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].index.get_level_values('State_Name'),
'district' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].index.get_level_values('District_Name')
})
#Creating color palette for plot
district_list = crop_dt.loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
# Creating the figure
#xmin, xmax = min(data.Crop_Year), max(data.Crop_Year)
#ymin, ymax = min(data.Production), max(data.Production)
p = figure(
title = 'Crop Area vs Production',
x_axis_label = 'Area',
y_axis_label = 'Production',
plot_height=900,
plot_width=1200,
tools = [HoverTool(tooltips='#district')]
)
p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district')
p.legend.location = 'top_right'
def update_plot(attr, old, new):
yr = slider.value
new_data = {
'x' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].Area,
'y' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].Production,
'state' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].index.get_level_values('State_Name'),
'district' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].index.get_level_values('District_Name')
}
source.data = new_data
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year')
slider.on_change('value',update_plot)
layout = row(widgetbox(slider), p)
curdoc().add_root(layout)
show(layout)
Also tried a different option using CustomJS as below, but still no luck.
callback = CustomJS(args=dict(source=source), code="""
var data = source.data;
var yr = slider.value;
var x = data['x']
var y = data['y']
'x' = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['ABC']), :].Area;
'y' = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['ABC']), :].Production;
p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district');
}
source.change.emit();
""")
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
yr_slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year', callback=callback)
callback.args["slider"] = yr_slider
Had a lot of issues trying to execute your code and I have changed some things, so feel free to correct me if did something wrong.
The error was caused by the creation of the ColumnDataSource, I had to change the level value to Crop_Year instead of Year. The loc 'ABC' also caused an error so I removed that too (And I had to add source = ColumnDataSource({, you probably forgot to copy that)
I also added a dropdown menu so it's possible to only show the data from one district.
Also, I'm not quite sure if it's possible to start a bokeh server by supplying a .ipynb file to --serve. But don't pin me down on that, I never use notebooks. I've tested this with a .py file.
#!/usr/bin/python3
import pandas as pd
import numpy as np
from bokeh.models import ColumnDataSource, CategoricalColorMapper, HoverTool
from bokeh.plotting import figure, curdoc
from bokeh.palettes import viridis
from bokeh.layouts import row, widgetbox
from bokeh.models.widgets import Select, Slider
#Importing and processing data file
crop = pd.read_csv('crop_production.csv')
#Cleaning Data
crop.fillna(np.NaN)
crop['Season'] = crop.Season.str.strip()
#Removing Whitespace #Filtering the dataset by Season
crop_season = crop[crop.Season == 'Whole Year']
crop_dt = crop_season.groupby(['State_Name', 'District_Name', 'Crop_Year']).mean().round(1)
crop_dt_year = crop_dt[crop_dt.index.get_level_values('Crop_Year')==2001]
crop_dt_year_state = crop_dt_year[crop_dt_year.index.get_level_values('State_Name')=='Tamil Nadu']
#Creating Column Data Source
source = ColumnDataSource({
'x': crop_dt_year_state.Area.tolist(),
'y': crop_dt_year_state.Production.tolist(),
'state': crop_dt_year_state.index.get_level_values('State_Name').tolist(),
'district': crop_dt_year_state.index.get_level_values('District_Name').tolist()
})
#Creating color palette for plot
district_list = crop_dt.loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
# Creating the figure
p = figure(
title = 'Crop Area vs Production',
x_axis_label = 'Area',
y_axis_label = 'Production',
plot_height=900,
plot_width=1200,
tools = [HoverTool(tooltips='#district')]
)
glyphs = p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district')
p.legend.location = 'top_right'
def update_plot(attr, old, new):
#Update glyph locations
yr = slider.value
state = select.value
crop_dt_year = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr]
crop_dt_year_state = crop_dt_year[crop_dt_year.index.get_level_values('State_Name')==state]
new_data = {
'x': crop_dt_year_state.Area.tolist(),
'y': crop_dt_year_state.Production.tolist(),
'state': crop_dt_year_state.index.get_level_values('State_Name').tolist(),
'district': crop_dt_year_state.index.get_level_values('District_Name').tolist()
}
source.data = new_data
#Update colors
district_list = crop_dt.loc[([state]), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
glyphs.glyph.fill_color = dict(field='district', transform=color_mapper)
glyphs.glyph.line_color = dict(field='district', transform=color_mapper)
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year')
slider.on_change('value',update_plot)
#Creating drop down for state
options = list(set(crop_dt.index.get_level_values('State_Name').tolist()))
options.sort()
select = Select(title="State:", value="Tamil Nadu", options=options)
select.on_change('value', update_plot)
layout = row(widgetbox(slider, select), p)
curdoc().add_root(layout)
#Jasper Thanks a lot. This works, however it doesnt work with .loc[(['Tamil Nadu']), :]. Reason for having this is to filter the data by adding a bokeh dropdown or radio button object and refresh the plot based on the filters. The below code works only if .loc[(['Tamil Nadu']), :] is removed. Is there any other way to fix this please?
def update_plot(attr, old, new):
yr = slider.value
new_data = {
'x' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].Area.tolist(),
'y' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].Production.tolist(),
'state' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].index.get_level_values('State_Name').tolist(),
'district' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').tolist()
}
source.data = new_data
Given the following bokeh chart (this code must be run in a jupyter notebook):
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[0, 1])
show(column([p, checkbox_button_group])) # show the results
Which produces a chart like this:
How can I connect up the checkbox buttons so that they show/hide the relevant series on the plot?
Note:
I know that I can click the legend to achieve this effect. However, I want to plot more series than the legend can show (e.g. it only shows 13 series in the screenshot). Obviously, people will only have maybe 10 series shown at any one time otherwise it becomes hard to see information.
Here is my attempt. It feels clunky though, is there a better solution? Also, how can I call my callback automatically when the plot has loaded, so that series [0,1,2,3] only are made active?
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
series = []
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
series.append(p.line(x, y, line_width=1, legend=str(n), color=next(colors)))
p.legend.location = "top_left"
p.legend.click_policy="hide"
js = ""
for n in range(nseries):
js_ = """
if (checkbox.active.indexOf({n}) >-1) {{
l{n}.visible = true
}} else {{
l{n}.visible = false
}} """
js += js_.format(n=n)
callback = CustomJS(code=js, args={})
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[0,1,2,3], callback=callback)
callback.args = dict([('l{}'.format(n), series[n]) for n in range(nseries)])
callback.args['checkbox'] = checkbox_button_group
show(column([p, checkbox_button_group])) # show the results
Your solution is fine.
Here is a more compact js callback that relies on the line being numbered with their "name" attribute
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors), name=str(n))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[])
code = """
active = cb_obj.active;
rend_list = fig.renderers;
for (rend of rend_list) {
if (rend.name!==null) {
rend.visible = !active.includes(Number(rend.name));
}
}
"""
checkbox_button_group.callback = CustomJS(args={'fig':p},code=code)
show(column([p, checkbox_button_group])) # show the results
It's also useful if you want to hide groups of lines via keywords by having them share those in their "name" attribute
And here is how you can do it with the bokeh server:
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
line_list = []
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
line_list += [p.line(x, y, line_width=1, legend=str(n), color=next(colors))]
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[])
def update(attr,old,new):
for lineID,line in enumerate(line_list):
line.visible = lineID in new
checkbox_button_group.on_change('active',update)
curdoc().add_root(column([p, checkbox_button_group]))
def init_active():
checkbox_button_group.active = range(3)
curdoc().add_timeout_callback(init_active,1000)