Related
I currently have a gmap displaying gps points, however, I was hoping there was a way to colour code my GPS points based on which month they were recorded ? I have looked around online but am struggling to implement it into my own code. My dataset consists of GPS points collected throughout 2017, with a localDate index (in datetime format), and a longitude and latitude:
2017-11-12 |5.043978|118.715237
Bokeh and gmap code:
def plot(lat, lng, zoom=10, map_type='roadmap'):
gmap_options = GMapOptions(lat=lat, lng=lng,
map_type=map_type, zoom=zoom)
# the tools are defined below:
hover = HoverTool(
tooltips = [
# #price refers to the price column
# in the ColumnDataSource.
('Date', '#{Local Date}{%c}'),
('Lat', '#Lat'),
('Lon', '#Lon'),
],
formatters={'#{Local Date}': 'datetime'}
)
# below we replaced 'hover' (the default hover tool),
# by our custom hover tool
p = gmap(api_key, gmap_options, title='Malaysia',
width=bokeh_width, height=bokeh_height,
tools=[hover, 'reset', 'wheel_zoom', 'pan'])
source = ColumnDataSource(day2017Averageddf)
center = p.circle('Lon', 'Lat', size=4, alpha=0.5,
color='yellow', source=source)
show(p)
return p
p = plot(Lat, Lon, map_type='satellite')
The base idea is to pass the colors to the color keyword in p.circle(). You are using one color, but you could create also a list of colors with the correct length and implement your own logic or you could make use of a mapper.
The code below is a copy from the original documentation about mappers.
from bokeh.models import ColumnDataSource
from bokeh.palettes import Spectral6
from bokeh.plotting import figure, output_notebook, show
from bokeh.transform import linear_cmap
output_notebook()
x = [1,2,3,4,5,7,8,9,10]
y = [1,2,3,4,5,7,8,9,10]
#Use the field name of the column source
mapper = linear_cmap(field_name='y', palette=Spectral6 ,low=min(y) ,high=max(y))
source = ColumnDataSource(dict(x=x,y=y))
p = figure(width=300, 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)
p.add_layout(color_bar, 'right')
show(p)
To come back to you problem. If the items in your Local Date column are of type pd.Timestamp, you can create a column "month" by this line
day2017Averageddf["month"] = day2017Averageddf["Local Date"].month
and use it for the mapper.
Original Question
I have a graph defined by the function y/100 = (x/100)^n
I want to decide x with a slider on bokeh and (i) mark the point with dotted lines on the x- and y-axes, and (ii) find the related y-coordinate on a table below the slider.
Here is what I have tried, but with no success (the slider, table and graph are all there, but nothing happens when the slider is moved).
### Slider:: Interactive Graph with Python Bokeh
### Source:: https://docs.bokeh.org/en/latest/docs/gallery/slider.html
import numpy as np
import pandas as pd
from bokeh.layouts import row, column
from bokeh.models import CustomJS, Slider, Title
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models.widgets import DataTable, TableColumn
#%% Read data from graph
## Define parameters and function
n = 0.333333333333
w1, h1 = 500, 400
w2, h2 = 200, 80
def x_to_y(x):
y = 100*((x*0.01)**n)
y = np.round(y, 2)
return y
## Initialize the graph
x = np.linspace(0, 100, 1001)
y = x_to_y(x)
cust_slider = Slider(start=0, end=100, step=0.1,
value=30,
width=w2, height=h2,
title="% Customers")
## Create a table with (x, y) values of the graph...
## read x from slider
tabl = dict(cust=[cust_slider.value],
potn=[x_to_y(cust_slider.value)])
values = ColumnDataSource(tabl)
columns = [TableColumn(field="cust", title="% Customers"),
TableColumn(field="potn", title="% Potential")]
data_table = DataTable(source=values,
columns=columns,
width=w2, height=h2,
editable=True)
## Plot the graph from function (later to be read from source)
source = ColumnDataSource(data=dict(x=x, y=y))
TOOLTIPS = [("Customers: ", "$x"),
("Potential: ", "$y")
]
plot = figure(x_range=(0,100),
y_range=(0, 100),
tooltips=TOOLTIPS,
plot_width=w1,
plot_height=h1)
plot.line('x', 'y', source=source)
plot.line([0,cust_slider.value,cust_slider.value],
[x_to_y(cust_slider.value),x_to_y(cust_slider.value),0],
line_dash="dashed"
)
plot.add_layout(Title(text="Customers (%)", align="center"), "below")
plot.add_layout(Title(text="Potential (%)", align="center"), "left")
## Try to make table and plot interactive (...table not interacting)
callback = CustomJS(args=dict(source=tabl,
slider=cust_slider),
code="""const source = source.data;
const xx = source['cust'];
const yy = source['potn'];
xx = slider.value;
yy = x_to_y(xx);
source.change.emit();"""
)
cust_slider.js_on_change('value', callback)
layout = row(plot,
column(cust_slider,
data_table)
)
output_file("slider_mwe.html", title="Graph")
show(layout)
Here is a snapshot of the figure I got:
Edit: Solution inspired by bigreddot
### Slider:: Interactive Graph with Python Bokeh
### Source:: https://docs.bokeh.org/en/latest/docs/gallery/slider.html
import numpy as np
import pandas as pd
from bokeh.layouts import row, column
from bokeh.models import CustomJS, Slider, Title
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models.widgets import DataTable, TableColumn
#%% Read data from graph
## Define parameters and function
n = 0.333333333333
w1, h1 = 500, 400
w2, h2 = 200, 80
def x_to_y(x):
y = 100*((x*0.01)**n)
y = np.round(y, 2)
return y
## Initialize the graph
x = np.linspace(0, 100, 1001)
y = x_to_y(x)
## ...and the data-point in focus
x0 = np.linspace(30, 30, 1)
y0 = x_to_y(x0)
cust_slider = Slider(start=0, end=100, step=0.1,
value=30,
width=w2, height=h2,
title="% Customers")
## Create a table with (x, y) values of the graph...
## read x from slider
tabl = dict(cust=[cust_slider.value],
potn=[x_to_y(cust_slider.value)])
tlin = ColumnDataSource(data=dict(lin_cust=np.insert(x0, 0, [0, x0[0]]),
lin_potn=np.insert(y0, 1, [y0[0], 0])))
values = ColumnDataSource(tabl)
columns = [TableColumn(field="cust", title="% Customers"),
TableColumn(field="potn", title="% Potential")]
data_table = DataTable(source=values,
columns=columns,
width=w2, height=h2,
editable=True)
## Plot the graph from function (later to be read from source)
source = ColumnDataSource(data=dict(x=x, y=y))
TOOLTIPS = [("Customers: ", "$x"),
("Potential: ", "$y")
]
plot = figure(x_range=(0,100),
y_range=(0, 100),
tooltips=TOOLTIPS,
plot_width=w1,
plot_height=h1)
plot.line('x', 'y', source=source)
plot.line('lin_cust', 'lin_potn', source=tlin, line_dash="dashed")
plot.add_layout(Title(text="Customers (%)", align="center"), "below")
plot.add_layout(Title(text="Potential (%)", align="center"), "left")
## Plot made interactive by writing to values & tlin
## (rather than declared constants)
callback = CustomJS(args=dict(source=source,
values=values,
tlin=tlin,
slider=cust_slider),
code="""values.data['cust'][0] = slider.value;
for (var i = 0; i < source.data['x'].length; i++)
{
if (source.data['x'][i] == slider.value)
{
values.data['potn'][0] = source.data['y'][i];
}
}
tlin.data['lin_cust'][1] = values.data['cust'][0]
tlin.data['lin_cust'][2] = values.data['cust'][0]
tlin.data['lin_potn'][0] = values.data['potn'][0]
tlin.data['lin_potn'][1] = values.data['potn'][0]
values.change.emit();
tlin.change.emit();"""
)
cust_slider.js_on_change('value', callback)
layout = row(plot,
column(cust_slider,
data_table)
)
output_file("slider_mwe.html", title="Graph")
show(layout)
which gives everything in place:
...except that at times the corner of the dotted line falls off the curve.
Well, I thought it should be given that little freedom for the sake of what it all has done for me !
There are a few issues. First, your CustomJS code:
const source = source.data;
const xx = source['cust'];
const yy = source['potn'];
xx = slider.value;
yy = x_to_y(xx);
source.change.emit();
Does not actually do anything that has any effect. When you create local variables xx and yy and then re-assign values to them, that only affects the local variables ,and nothing else. In particular, nothing about the Bokeh data source was changed (so nothing about the plot will change).
Additionally, the values in a ColumnDataSource must be actual columns (i.e. lists or arrays) not single numbers as you are assigning above.
You probably intend something more like:
source.data['cust'] = [0, slider.value, slider.value]
which actually modifies the contents of source.data.
However, you also have one other issue, which is that for the other column, you are trying to call the Python function x_to_y in your CustomJS callback code. This can never work. CustomJS callback code is executed in the browser, and browsers only know how to execute JavaScript code. You will have to perform that conversion with JS code in 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
I've followed the example here: http://docs.bokeh.org/en/latest/docs/user_guide/geo.html#tile-providers
I got a basic map loading a GeoJSON file with a list of polygons (already projected to Web Mercator EPSG:3857) so then I could use STAMEN_TONER as a tile provider.
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.tile_providers import STAMEN_TONER, STAMEN_TERRAIN
from bokeh.models import Range1d, GeoJSONDataSource
# bokeh configuration for jupyter
from bokeh.io import output_notebook
output_notebook()
# bounding box (x,y web mercator projection, not lon/lat)
mercator_extent_x = dict(start=x_low, end=x_high, bounds=None)
mercator_extent_y = dict(start=y_low, end=y_high, bounds=None)
x_range1d = Range1d(**mercator_extent_x)
y_range1d = Range1d(**mercator_extent_y)
fig = figure(
tools='pan, zoom_in, zoom_out, box_zoom, reset, save',
x_range=x_range1d,
y_range=y_range1d,
plot_width=800,
plot_height=600
)
fig.axis.visible = False
fig.add_tile(STAMEN_TERRAIN)
# the GeoJSON is already in x,y web mercator projection, not lon/lat
with open('/path/to/my_polygons.geojson', 'r') as f:
my_polygons_geo_json = GeoJSONDataSource(geojson=f.read())
fig.multi_line(
xs='xs',
ys='ys',
line_color='black',
line_width=1,
source=my_polygons_geo_json
)
show(fig)
However I am not able to set a default zoom level for the tiles. I thought it could have been a tool setting (http://docs.bokeh.org/en/latest/docs/user_guide/tools.html) but in there I can not find a default value for the zoom capabilities.
How can I set a default value for the zoom level of the tiles?
Old question but answering if someone would have the same problem. Set range for your map and this way you can zoom into the desired area on load. Below example with Papua New Guinea
p = figure(title="PNG Highlands Earthquake 7.5 Affected Villages",y_range=(-4.31509, -7.0341),x_range=( 141.26667, 145.56598))
p.xaxis.axis_label = 'longitude'
p.yaxis.axis_label = 'latitude'
I've just run into this issue myself, and found a good solution that should work under most circumstances. This requires making sure the data and the x_range/y_range to be projected properly (I used Proj and transform from pyproj but I'm sure there are other packages that will work the same).
Import modules:
import pandas as pd
import numpy as np
from pyproj import Proj, transform
import datashader as ds
from datashader import transfer_functions as tf
from datashader.bokeh_ext import InteractiveImage
from datashader.utils import export_image
from datashader.colors import colormap_select, Greys9, Hot, viridis, inferno
from IPython.core.display import HTML, display
from bokeh.plotting import figure, output_notebook, output_file, show
from bokeh.tile_providers import CARTODBPOSITRON
from bokeh.tile_providers import STAMEN_TONER
from bokeh.tile_providers import STAMEN_TERRAIN
from bokeh.embed import file_html
from functools import partial
output_notebook()
Read in data (I took a few extra steps to try and clean the coordinates since I'm working with an extremely messy dataset that contains NaN and broken text in the coordinates columns):
df = pd.read_csv('data.csv', usecols=['latitude', 'longitude'])
df.apply(lambda x: pd.to_numeric(x,errors='coerced')).dropna()
df = df.loc[(df['latitude'] > - 90) & (df['latitude'] < 90) & (df['longitude'] > -180) & (df['longitude'] < 180)]
Reproject data:
# WGS 84
inProj = Proj(init='epsg:4326')
# WGS84 Pseudo Web Mercator, projection for most WMS services
outProj = Proj(init='epsg:3857')
df['xWeb'],df['yWeb'] = transform(inProj,outProj,df['longitude'].values,df['latitude'].values)
Reproject the x_range, y_range. This is critical as these values set the extent of the bokeh map - the coordinates of these values need to match the projection. To make sure you have the correct coordinates, I suggest using http://bboxfinder.com to create a bounding box AOI and get the correct min/max and min/max coordinates (making sure EPSG:3857 - WGS 84/Pseudo-Mercator is selected). Using this method, just copy the coodinates next to "box" - these are in the order of minx,miny,maxx,maxy and should then be reordered as minx,maxx,miny,maxy (x_range = (minx,maxx))(y_range=(miny,maxy)):
world = x_range, y_range = ((-18706892.5544, 21289852.6142), (-7631472.9040, 12797393.0236))
plot_width = int(950)
plot_height = int(plot_width//1.2)
def base_plot(tools='pan,wheel_zoom,save,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
options = dict(line_color=None, fill_color='blue', size=1.5, alpha=0.25)
background = "black"
export = partial(export_image, export_path="export", background=background)
cm = partial(colormap_select, reverse=(background=="white"))
def create_image(x_range, y_range, w=plot_width, h=plot_height):
cvs = ds.Canvas(plot_width=w, plot_height=h, x_range=x_range, y_range=y_range)
agg = cvs.points(df, 'xWeb', 'yWeb')
magma = ['#3B0F6F', '#8C2980', '#DD4968', '#FD9F6C', '#FBFCBF']
img = tf.shade(agg, cmap=magma, how='eq_hist') # how='linear', 'log', 'eq_hist'
return tf.dynspread(img, threshold=.05, max_px=15)
p = base_plot()
p.add_tile("WMS service")
#used to export image (without the WMS)
export(create_image(*world),"TweetGeos")
#call interactive image
InteractiveImage(p, create_image)
The notion of a zoom "level" only applies to GMapPlot and there only because google controls the presentation of the maps very carefully, and that is the API they provide. All other Bokeh plots have explicitly user-settable x_range and y_range properties. You can set the start and end of these ranges to be whatever you want, and the plot will display the corresponding area defined by those bounds.
I'm a very nooby programmer and this is my first Stack Overflow question. :)
So I'm trying to animate a car's trip on google maps using Python. I used matplotlib at first and could get a dot animated over the path line... then I tried using bokeh and successfully got the path to overlay on google maps...
My problem is that I haven't found a good way to do both (animate plot over google maps).
My data is in the form of Lat/Long coordinates.
Any advice? Thanks in advance!
EDIT: Here's my code that does the gmapplot... I'd rather have this and no animation than animation with no GMAP. My goal is to animate that "car" dot.
import numpy as np
from bokeh.io import output_file, show, vform
from bokeh.models.widgets import Dropdown
from bokeh.models import (GMapPlot, GMapOptions, ColumnDataSource, Line, Circle,
DataRange1d, PanTool, WheelZoomTool, BoxSelectTool, HoverTool)
data = np.genfromtxt('Desktop\Temp Data for Python\test data 3.csv', delimiter=',',
names=True)
map_options = GMapOptions(lat=np.average(data['Latitude']),
lng=np.average(data['Longitude']), map_type="roadmap", zoom=13)
plot = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=map_options,
title="My Drive")
source = ColumnDataSource(data=dict(lat=data['Latitude'], lon=data['Longitude'],
speed=data['GpsSpeed'],))
path = Line(x="lon", y="lat", line_width = 2, line_color='blue')
car = Circle(x=data['Longitude'][0], y=data['Latitude'][0], size=5, fill_color='red')
plot.add_glyph(source, path)
plot.add_glyph(source, car)
plot.add_tools(PanTool(), WheelZoomTool(), BoxSelectTool(),
HoverTool(tooltips=[("Speed", "#speed"),]))
output_file("gmap_plot.html")
show(plot)
This may not be exactly what you are looking for, but you could have a slider widget that controls the position of your car dot. The slider example found in the bokeh docs (or github repository, I can't remember) helped me when I started using sliders.
Just so you are aware, I was having problems with latlng points showing up in the correct locations. There is about a 10px offset. This is an open issue (github issue 2964).
The following code currently is just producing a generic bokeh Figure, but in theory, if you change it from a Figure to a GMapPlot it should work. I wasn't able to get this working with GMapPlots directly. I think this may be because of github issue 3737. I can't even run the Austin example from the bokeh docs.
Hopefully this is what you had in mind
from bokeh.plotting import Figure, ColumnDataSource, show, vplot
from bokeh.io import output_file
from bokeh.models import (Slider, CustomJS, GMapPlot,
GMapOptions, DataRange1d, Circle, Line)
import numpy as np
output_file("path.html")
# Create path around roundabout
r = 0.000192
x1 = np.linspace(-1,1,100)*r
x2 = np.linspace(1,-1,100)*r
x = np.hstack((x1,x2))
f = lambda x : np.sqrt(r**2 - x**2)
y1 = f(x1)
y2 = -f(x2)
y = np.hstack((y1,y2))
init_x = 40.233688
init_y = -111.646784
lon = init_x + x
lat = init_y + y
# Initialize data sources.
location = ColumnDataSource(data=dict(x=[lon[0]], y=[lat[0]]))
path = ColumnDataSource(data=dict(x=lon, y=lat))
# Initialize figure, path, and point
"""I haven't been able to verify that the GMapPlot code below works, but
this should be the right thing to do. The zoom may be totally wrong,
but my latlng points should be a path around a roundabout.
"""
##options = GMapOptions(lat=40.233681, lon=-111.646595, map_type="roadmap", zoom=15)
##fig = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=options)
fig = Figure(plot_height=600, plot_width=600)
c = Circle(x='x', y='y', size=10)
p = Line(x='x', y='y')
fig.add_glyph(location, c)
fig.add_glyph(path, p)
# Slider callback
callback = CustomJS(args=dict(location=location, path=path), code="""
var loc = location.get('data');
var p = path.get('data');
t = cb_obj.get('value');
/* set the point location to the path location that
corresponds to the slider position */
loc['x'][0] = p['x'][t];
loc['y'][0] = p['y'][t];
location.trigger('change');
""")
# The way I have written this, 'start' has to be 0 and
# 'end' has to be the length of the array of path points.
slider = Slider(start=0, end=200, step=1, callback=callback)
show(vplot(fig, slider))