Load cryptocurrency prices directly from the web and create linked interactive candlestick and market volume charts using Altair.

Video

Follow along with the exercise 00:23

Pre-requisites

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.

Leave a Reply

Contact Us

We're not around right now. But you can send us an email and we'll get back to you, asap.

Not readable? Change text. captcha txt

Start typing and press Enter to search