Search code examples
pythonvisualizationbokeh

Bokeh: Hovertool stacks on old instances after callback triggered


I've got a program that displays stock market info for a google. I can adjust it with a DateRangeSlider to increase the dates that we're looking at. I also have a hovertool set on the chart to display the various aspects of the data for each respective day.

The issue I'm having is on callback. Instead of the hover data being refreshed with each call, it retains memory of previous calls and just stacks the previous hover info on top of the new.

Here's my code for running in Jupyter:

from pandas_datareader import data
from pandas import Timedelta
from datetime import datetime, date
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, HoverTool, NumeralTickFormatter, DatetimeTickFormatter, TextInput, DateRangeSlider
from bokeh.io import output_notebook, curdoc

output_notebook()

def inc_dec(c, o):
"""Determines if the financial day with and Increase or Decrease"""
    if c > o:
        return "Increase"
    elif c == 0:
        return "Equal"
    else:
        return "Decrease"

def get_sources(start = date(2016, 3, 1), end = date(2016, 3, 10), company = "GOOG"):
"""Gets all my dataframes for each glyph below"""

    #Gather data for df's
    df = data.DataReader(company, data_source="yahoo", start=start, end=end)
    df["Status"] = [inc_dec(c, o) for c, o in zip(df.Close, df.Open)]
    df["Mid"] = (df.Open + df.Close) / 2
    df["Height"] = abs(df.Open - df.Close)
    df["Width"] = 12*60*60*1000
    df["Segment, Left"] = df.index - Timedelta(hours=2)
    df["Segment, Right"] = df.index + Timedelta(hours=2)

    #my df is complete. Split them
    inc_df = df[df.Status == "Increase"]
    dec_df = df[df.Status == "Decrease"]

    #inc_s = ColumnDataSource(inc_df)
    #dec_s = ColumnDataSource(dec_df)
    #tot_s = ColumnDataSource(df)
    #Dictionary of dataframes, Increasing, Decreasing, and Total
    dfs = {"I": inc_df, "D": dec_df, "T": df}

    return dfs

def fin(doc):
"""Initialize the graphs and make an application"""

    #Get df's and convert to CDS
    dfs = get_sources()
    inc_s = ColumnDataSource(dfs["I"])
    dec_s = ColumnDataSource(dfs["D"])
    tot_s = ColumnDataSource(dfs["T"])

    hover = HoverTool(names=["inc","dec"],
                    tooltips = [("Date", "@Date{%m/%d/%Y}"),
                        ("High", "@High{$0.00}"),
                        ("Low","@Low{$0.00}"),
                        ("Open","@Open{$0.00}"),
                        ("Close","@Close{$0.00}"),
                        ("Volume", "@Volume{0,0}")],
                    formatters = {"@Date" : "datetime"})

    #Formatting the plot, p
    p = figure(x_axis_type="datetime", width=1000, height=300, sizing_mode="scale_width", tools="crosshair,pan,wheel_zoom,box_zoom,reset")
    #p.title.text = "{}: Finacial Data".format(company)       #Change this with company later
    p.title.text = "GOOG: Financial Data"
    p.title.align = "center"
    p.xaxis.axis_label="Date"
    p.yaxis.axis_label="Price"   
    p.yaxis.formatter = NumeralTickFormatter(format="$0,0.00")
    p.grid.grid_line_alpha = 0.3

    #Add glyphs
    p.segment(x0='Date', y0='High', x1='Date', y1='Low', source=tot_s) #vertical
    p.segment(x0='Segment, Left', y0='High', x1='Segment, Right', y1='High', source=tot_s) #high
    p.segment(x0='Segment, Left', y0='Low', x1='Segment, Right', y1='Low', source=tot_s) #low
    p.rect(x="Date", y="Mid", width="Width", height="Height", name="inc",
        fill_color="#00FFFF", line_color="black", source=inc_s)
    p.rect(x="Date", y="Mid", width="Width", height="Height", name="dec",
        fill_color="#B22222", line_color="black", source=dec_s)

    p.add_tools(hover)

    #the callbacks
    def cb_date(attr, new, old):
    """Callback for the DateRangeSlider"""

        if not isinstance(new[0], int):
            return None
        else:
            #Get new dates, pass them to get_sources to get df's, add new CDS's
            start = date.fromtimestamp(new[0]/1000)
            end = date.fromtimestamp(new[1]/1000)
            dfs = get_sources(start=start, end=end)
            inc_s.stream(dfs["I"])
            dec_s.stream(dfs["D"])
            tot_s.stream(dfs["T"])

    #def cb_com(attr, new, old):
        #Changes the company name, do later

    #add widgets
    slider = DateRangeSlider(start=date(2016, 1, 1), 
                                     end=date(2016, 5, 1), 
                                     value=(date(2016, 3, 1), date(2016, 3, 10)), 
                                     step=1, 
                                     title="Change Dates:")

    slider.on_change('value', cb_date)

    doc.add_root(column(slider,p))

show(fin)

Let me know what how I can go forward with this. I'm pretty lost.


Solution

  • I cannot run your code without some test data, but it seems to me that the issue is with the calls to the stream function. Streaming data does not remove the old data, even if the X coordinate is the same (data sources don't know and don't care about coordinates). If you want to replace the data for some particular date, you should use patch. If you want to replace the whole data, you should just assign the new value to the data attribute of a data source.