Timeseries plots#

Units#

Bokeh can automatically handle many kinds of datetime types, for instance Numpy datetime arrays and Pandas datetime series, as well as Python built-in datetime types. It can sometimes be helpful to understand how Bokeh represents these values.

Internally, all datetime values are floating point values that represent milliseconds-since-epoch (specifically, “epoch” here refers to unix time, i.e. 1 January 1970 00:00:00 UTC). Bokeh will convert datetime values to this floating point format before passing on to BokehJS. On occasion (e.g. in CustomJS callbacks) it may be necessary to use these values directly.

Range tool#

It is often desirable to be able to zoom in on one region of a timeseries while still seeing a view of the full series for context. The RangeTool can be used to interactively define a region in one plot that results in a zoomed view on another plot. This is demonstrated below:

import numpy as np

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, RangeTool
from bokeh.plotting import figure, show
from bokeh.sampledata.stocks import AAPL

dates = np.array(AAPL['date'], dtype=np.datetime64)
source = ColumnDataSource(data=dict(date=dates, close=AAPL['adj_close']))

p = figure(height=300, width=800, tools="xpan", toolbar_location=None,
           x_axis_type="datetime", x_axis_location="above",
           background_fill_color="#efefef", x_range=(dates[1500], dates[2500]))

p.line('date', 'close', source=source)
p.yaxis.axis_label = 'Price'

select = figure(title="Drag the middle and edges of the selection box to change the range above",
                height=130, width=800, y_range=p.y_range,
                x_axis_type="datetime", y_axis_type=None,
                tools="", toolbar_location=None, background_fill_color="#efefef")

range_tool = RangeTool(x_range=p.x_range, start_gesture="pan")
range_tool.overlay.fill_color = "navy"
range_tool.overlay.fill_alpha = 0.2

select.line('date', 'close', source=source)
select.ygrid.grid_line_color = None
select.add_tools(range_tool)

show(column(p, select))

Candlestick chart#

import pandas as pd

from bokeh.models import BoxAnnotation
from bokeh.plotting import figure, show
from bokeh.sampledata.stocks import MSFT

df = pd.DataFrame(MSFT)[60:120]
df["date"] = pd.to_datetime(df["date"])

inc = df.close > df.open
dec = df.open > df.close

non_working_days = df[['date']].assign(diff=df['date'].diff()-pd.Timedelta('1D'))
non_working_days = non_working_days[non_working_days['diff']>=pd.Timedelta('1D')]

df['date'] += pd.Timedelta('12h') # move candles to the center of the day

TOOLS = "pan,wheel_zoom,box_zoom,reset,save"

p = figure(x_axis_type="datetime", tools=TOOLS, width=1000, height=400,
           title="MSFT Candlestick", background_fill_color="#efefef")
p.xaxis.major_label_orientation = 0.8 # radians

boxes = [
    BoxAnnotation(fill_color="#bbbbbb", fill_alpha=0.2, left=date-diff, right=date)
    for date, diff in non_working_days.values
]
p.renderers.extend(boxes)

p.segment(df.date, df.high, df.date, df.low, color="black")

p.vbar(df.date[dec], pd.Timedelta('16h'), df.open[dec], df.close[dec], color="#eb3c40")
p.vbar(df.date[inc], pd.Timedelta('16h'), df.open[inc], df.close[inc], fill_color="white",
       line_color="#49a3a3", line_width=2)

show(p)

Missing dates#

import pandas as pd

from bokeh.plotting import figure, show
from bokeh.sampledata.stocks import MSFT

df = pd.DataFrame(MSFT)[60:120]
df["date"] = pd.to_datetime(df["date"])

inc = df.close > df.open
dec = df.open > df.close

TOOLS = "pan,wheel_zoom,box_zoom,reset,save"

p = figure(tools=TOOLS, width=1000, height=400,
           title="MSFT Candlestick without missing dates",
           background_fill_color="#efefef")
p.xaxis.major_label_orientation = 0.8 # radians
p.x_range.range_padding = 0.05

# map dataframe indices to date strings and use as label overrides
p.xaxis.major_label_overrides = {
    i: date.strftime('%b %d') for i, date in zip(df.index, df["date"])
}

# one tick per week (5 weekdays)
p.xaxis.ticker = list(range(df.index[0], df.index[-1], 5))

p.segment(df.index, df.high, df.index, df.low, color="black")

p.vbar(df.index[dec], 0.6, df.open[dec], df.close[dec], color="#eb3c40")
p.vbar(df.index[inc], 0.6, df.open[inc], df.close[inc], fill_color="white",
       line_color="#49a3a3", line_width=2)

show(p)