Load cryptocurrency prices directly from the web and create linked interactive candlestick and market volume charts using Altair.
Follow along with the exercise 00:23
Pre-requisites
Installed and configured miniconda. See either Up and running with miniconda and pycharm on macOS or Up And Running With Miniconda And PyCharm On Windows.
Get code, setup conda environment, configure JupyterLab
Familiar procedure for cloning git repo, configuring and activating conda env, with the one major difference that you have to install the jupyterlab_bokeh
extension.
git clone git@github.com:cpbotha/bokeh_basic_charting.git cd bokeh_basic_charting conda env update conda activate vxuni_bokeh # this only has to happen once for this environment jupyter labextension install jupyterlab_bokeh
Start the Lab
With the environment active, start JupyterLab:
# make sure you are in the correct directory cd bokeh_basic_charting # startup the lab jupyter lab
Overview of our goal 01:23
- Create candlestick and volume charts with cryptocurrency data
- Could be any stock market data with open / close / high / low values
Create candlesticks: General procedure 02:00
In Bokeh, create Figure
object, then set properties and call methods on object to configure plot and create visual marks, called glyphs.
We start by creating a function that takes the price data frame, and returns a configured bokeh Figure
:
def make_candlesticks(price: pd.DataFrame) -> Figure:
Extract data from coinmarketcap into pandas 02:49
We use pandas.read_html()
to convert a <table>
from a webpage straight into a dataframe. Neat!
Bokeh charts out of the box 03:27
- Use
output_notebook()
to tell bokeh that the plot should be embedded in the notebook. - Interaction tools such as panning, zooming, reset
- Tooltips.
Prepare data 04:42
Besides main dataframe which we can query in place, here we create dataframe masks for red and green candlesticks:
inc = price.Close > price.Open dec = price.Open > price.Close
Construct the bokeh.plotting.figure.Figure
object 05:15
Going to add extra y-range later, so have to specify primary range:
p: Figure = figure( x_axis_type="datetime", tools=TOOLS, plot_width=1000, title = "ETH prices", height=400, y_range=(150, price.High.max() * 1.1))
Manipulate properties and call methods to create glyphs 06:38
p.grid.grid_line_alpha = 0.7 # line segment glyphs p.segment(price.Date, price.High, price.Date, price.Low, color="black") # green bar glyphs p.vbar(price.Date[inc], w, price.Open[inc], price.Close[inc], fill_color="#06982d", line_color="black") # red bar glyphs p.vbar(price.Date[dec], w, price.Open[dec], price.Close[dec], fill_color="#ae1325", line_color="black")
Using the bokeh ColumnDataSource 09:16
- CDS is what bokeh uses under the covers for everything.
- Each glyph call gets
source=cds
argument, refers to columns by name.
# this is the basic bokeh data container # even when we pass dataframe, CDS is created under the covers cds = ColumnDataSource(price) # line segment glyphs p.segment("Date", "High", "Date", "Low", color="black", source=cds)
Tooltips 10:53
- Use for tooltips in this case, and linked selection in general case.
- Tooltips: Refer to CDS columns using
@
. Intrinsic plot vars start with$
.
TOOLTIPS = [ ("index", "$index"), ("(x,y)", "($x, $y)"), ("open", "@Open"), ("close", "@Close") ] # have to fix y-range here as we are adding extra_y_range later p: Figure = figure( x_axis_type="datetime", tools=TOOLS, plot_width=1000, tooltips=TOOLTIPS, title = "ETH prices", height=400, y_range=(150, price.High.max() * 1.1)) p.grid.grid_line_alpha = 0.7
Filtering with CDS 12:15
Filtering is slightly less elegant than with DataFrames:
# green bar glyphs # you can't pass a mix of data source and iterable values, i.e. df + cds not gonna fly inc_view = CDSView(source=cds, filters=[BooleanFilter(price.Close > price.Open)]) p.vbar("Date", w, "Open", "Close", fill_color="#06982d", line_color="black", source=cds, view=inc_view) # red bar glyphs dec_view = CDSView(source=cds, filters=[BooleanFilter(price.Open > price.Close)]) p.vbar("Date", w, "Open", "Close", fill_color="#ae1325", line_color="black", source=cds, view=dec_view)
Bokeh also employs JSON representation of figure 14:03
Similarly to Altair, Bokeh creates a JSON representation of the visualization which is sent to Bokeh.JS on the frontend for rendering.
The following command in the notebook will print the JSON representation:
p.document.to_json()
(Have to access containing document to capture all bokeh models referred to.)
Bokeh representation 14:27
- Every visualization in bokeh is a scene graph of objects, such as the example below.
Document
keeps everything together, and is the unit of serialization, including communication with front-end.- Read more in the bokeh reference.
Add volume bars to the same plot 15:10
Function takes existing figure object and continues with configuration and augmentation.
In this case add volume bars to existing figure, install them at the bottom, add extra axis on the right.
def add_volume_bars(price: pd.DataFrame, p: Figure) -> Figure: # note that we set the y-range here to be 3 times the data range so that # the volume bars appear in the bottom third p.extra_y_ranges = {"vol": Range1d(start=price.Volume.min(), end=price.Volume.max()*3)} # use bottom=price.Volume.min() to have bottom of bars clipped off. p.vbar(price.Date, w, top=price.Volume, y_range_name="vol") # add an additional axis for the volume bars # configure tick formatter for currency formatting p.add_layout(LinearAxis(y_range_name="vol", formatter=NumeralTickFormatter(format='$0,0')), 'right') return p
Make separate but linked volume figure 19:05
Extra example of same bokeh pattern:
- Create figure.
- Configure via properties.
- Create and configure glyphs.
def make_volume_bars(price: pd.DataFrame) -> Figure: p = figure(x_axis_type="datetime", tools=TOOLS, plot_width=1000, title="ETH volume", y_range=(150, price.Volume.max() * 1.1)) p.height = 150 # set y_range so that minimum amount is a bar of 0 height p.y_range = Range1d(start=price.Volume.min(), end=price.Volume.max()*1.1) # set the primary axis formatter to currency p.yaxis[0].formatter = NumeralTickFormatter(format='$0,0') # create the bars! p.vbar(price.Date, w, top=price.Volume) return p
Link figures by sharing x_range
property 20:00
- Share e.g.
x_range
between two figures will synchronize panning and zooming on the x-axis. - Pretty elegant!
def make_linked_candlesticks_and_volume(price: pd.DataFrame) -> Column: c = make_candlesticks(price) v = make_volume_bars(price) # it's this simple! we now have bidirectional linking v.x_range = c.x_range return gridplot([[c], [v]])
PyCharm online documentation setup for bokeh 21:16
Almost all of the bokeh.plotting
calls are methods of the Figure
class, see the reference.
You can setup a URL pattern in PyCharm under “Python External Documentation” to have Shift-F1
jump to the right place:
Module bokeh.plotting
gets URL pattern https://bokeh.pydata.org/en/latest/docs/reference/plotting.html#{module.name}.{class.name}.{element.name}
On my PyCharm, adding additional bokeh doc patterns for modules within bokeh.models
would only result in “No external documentation URL configured for bokeh”.
Defining directly for bokeh.models
does work, but then PyCharm system does not offer a macro for extracting e.g. only annotations
or formatters
from the full module spec.
Conclusion 23:16
- Planned lectures with more advanced Bokeh and Altair
- When Altair has what you need, use it. When you need more flexibility, look at Bokeh.