def daily_plot(self, days):
        """Bar plot displaying the number of block visits per day.

        The number of block visits is shown for the `days` days leading to but excluding `self.date`. A day here refers
        to the time from noon to noon. For example, 22 May 2016 refers to the period from 22 May 2016, 12:00 to 23 May
        2016, 12:00.

        Params:
        -------
        days : int
            Number of days.

        Returns:
        --------
        app.plot.plot.TimeBarPlot
            Plot of number of block visits as a function of the day.
        """

        start_date, end_date = day_range(self.date, days)
        trend_func = functools.partial(day_running_average, ignore_missing_values=False)

        df = self.df.copy()
        df['EngineeringTimePercentage'] = 100 * np.divide(df.EngineeringTime, df.NightLength)
        df['EngineeringTime'] /= 60

        return daily_bar_plot(df=df,
                              start_date=start_date,
                              end_date=end_date,
                              date_column='Date',
                              y_column=['EngineeringTime', 'EngineeringTimePercentage'],
                              y_range=[Range1d(start=0, end=200), Range1d(start=0, end=30)],
                              y_formatters=[PrintfTickFormatter(format='%.0fm'), PrintfTickFormatter(format='%.0f%%')],
                              trend_func=trend_func,
                              **self.kwargs)
    def daily_plot(self, days):
        """Bar plot displaying the operation efficiency per day.

        The operation efficiency is shown for the `days` days leading to but excluding `self.date`. A day here refers
        to the time from noon to noon. For example, 22 May 2016 refers to the period from 22 May 2016, 12:00 to 23 May
        2016, 12:00.

        Params:
        -------
        days : int
            Number of days.

        Returns:
        --------
        app.plot.plot.TimeBarPlot
            Plot of operation efficiency as a function of the day.
        """

        df = self.df.copy()
        df['OperationEfficiency'] = 100 * np.divide(df.ObsTime, df.ScienceTime)

        start_date, end_date = day_range(self.date, days)
        trend_func = functools.partial(day_running_average,
                                       ignore_missing_values=True)

        return daily_bar_plot(
            df=df,
            start_date=start_date,
            end_date=end_date,
            date_column='Date',
            y_column='OperationEfficiency',
            y_range=Range1d(start=0, end=140),
            trend_func=trend_func,
            y_formatters=[PrintfTickFormatter(format='%d%%')],
            **self.kwargs)
def plot_rsi(stock):
    p = figure(x_axis_type="datetime",
               plot_width=WIDTH_PLOT,
               plot_height=200,
               title="RSI 15 days",
               tools=TOOLS,
               toolbar_location='above')

    p.line(x='date', y='rsi_15', line_width=2, color=BLUE, source=stock)

    low_box = BoxAnnotation(top=30, fill_alpha=0.1, fill_color=RED)
    p.add_layout(low_box)
    high_box = BoxAnnotation(bottom=70, fill_alpha=0.1, fill_color=GREEN)
    p.add_layout(high_box)

    # Horizontal line
    hline = Span(location=50,
                 dimension='width',
                 line_color='black',
                 line_width=0.5)
    p.renderers.extend([hline])

    p.y_range = Range1d(0, 100)
    p.yaxis.ticker = [30, 50, 70]
    p.yaxis.formatter = PrintfTickFormatter(format="%f%%")
    p.grid.grid_line_alpha = 0.3

    return p
Example #4
0
def ridgeplot(courses_obj):
    # first format 'courses_obj' into 'probly' DataFrame format
    # courses_obj: [{'course_name': 'Calculus...', ...}, ...]
    grades = [[100*y[2]/y[3] for y in x['assignments']] for x in courses_obj]
    # turn this list of lists into a complete NumPy array
    length = len(sorted(grades, key=len, reverse=True)[0])
    grades = np.array([xi+[None]*(length-len(xi)) for xi in grades], dtype='float')
    columns = [x['course_name'] for x in courses_obj]
    grades = grades.transpose()
    probly = pd.DataFrame(grades, columns=columns)


    cats = list(reversed(probly.keys()))


    palette = [cc.rainbow[i*15] for i in range(17)]

    x = np.linspace(-20,110, 500)

    source = ColumnDataSource(data=dict(x=x))

    p = figure(y_range=cats, plot_width=900, plot_height = 300, x_range=(-5, 120))#, toolbar_location=None)

    for i, cat in enumerate(reversed(cats)):
        adjusted = probly[cat].replace([np.inf, -np.inf], np.nan).dropna(how="all")
        if adjusted.size == 1 or pd.unique(adjusted).size == 1: # this means we can't compute
            continue
        pdf = gaussian_kde(adjusted)
        #p = figure(plot_width=400, plot_height=400)
        #p.line(x, pdf(x))
        y = ridge(cat, pdf(x), scale=2)
        #p.line(x, y, color='black')
        #show(p)
        source.add(y, cat)
        p.patch('x', cat, color=palette[i], alpha=0.6, line_color="black", source=source)

    p.outline_line_color = None
    p.background_fill_color = "#efefef"

    p.tools = [PanTool(), CrosshairTool(), HoverTool(), SaveTool(), BoxZoomTool(), WheelZoomTool(), ResetTool()]

    ticks = list(np.array([np.array([0,3,7])+i*10 for i in range(10)]).flatten()) + [100]
    p.xaxis.ticker = FixedTicker(ticks=ticks)
    p.xaxis.formatter = PrintfTickFormatter(format="%d")
    p.xaxis.axis_label = "Your Grade Distribution"

    p.yaxis.axis_label = "Your Courses"

    p.ygrid.grid_line_color = None
    p.xgrid.grid_line_color = "Grey"
    p.xgrid.ticker = p.xaxis[0].ticker

    p.axis.minor_tick_line_color = None
    p.axis.major_tick_line_color = None
    p.axis.axis_line_color = None

    p.y_range.range_padding = 0.12

    return p
Example #5
0
    def monthly_plot(self, months):
        """Bar plot displaying the shutter open efficiency per momth.

        The operation efficiency is shown for the `months` months leading to but excluding the month containing
        `self.date`. A month here refers start at noon of the first of the month. For example, May 2016 refers to the
        period from 1 May 2016, 12:00 to 1 June 2016, 12:00.

        Params:
        -------
        months : int
            Number of months.

        Returns:
        --------
        app.plot.plot.TimeBarPlot
            Plot of shutter open efficiency as a function of the month.
        """

        start_date, end_date = month_range(self.date, months)
        trend_func = functools.partial(month_running_average,
                                       ignore_missing_values=False)

        def post_binning_func(df):
            df['TimeLostToProblemsPercentage'] = 100 * np.divide(
                df.TimeLostToProblems, df.NightLength)
            df.TimeLostToProblems /= 3600

        return monthly_bar_plot(
            df=self.df,
            start_date=start_date,
            end_date=end_date,
            date_column='Date',
            month_column='Month',
            y_column=['TimeLostToProblems', 'TimeLostToProblemsPercentage'],
            y_range=[Range1d(start=0, end=60),
                     Range1d(start=0, end=20)],
            y_formatters=[
                PrintfTickFormatter(format='%.0fh'),
                PrintfTickFormatter(format='%.0f%%')
            ],
            trend_func=trend_func,
            post_binning_func=post_binning_func,
            **self.kwargs)
Example #6
0
def wrap_formatter(formatter, axis):
    """
    Wraps formatting function or string in
    appropriate bokeh formatter type.
    """
    if isinstance(formatter, TickFormatter):
        pass
    elif isinstance(formatter, FunctionType):
        msg = ('%sformatter could not be '
               'converted to tick formatter. ' % axis)
        jsfunc = py2js_tickformatter(formatter, msg)
        if jsfunc:
            formatter = FuncTickFormatter(code=jsfunc)
        else:
            formatter = None
    else:
        formatter = PrintfTickFormatter(format=formatter)
    return formatter
Example #7
0
def make_tpf_figure_elements(tpf,
                             tpf_source,
                             pedestal=None,
                             fiducial_frame=None,
                             plot_width=370,
                             plot_height=340,
                             scale='log',
                             vmin=None,
                             vmax=None,
                             cmap='Viridis256',
                             tools='tap,box_select,wheel_zoom,reset'):
    """Returns the lightcurve figure elements.

    Parameters
    ----------
    tpf : TargetPixelFile
        TPF to show.
    tpf_source : bokeh.plotting.ColumnDataSource
        TPF data source.
    pedestal: float
        A scalar value to be added to the TPF flux values, often to avoid
        taking the log of a negative number in colorbars.
        Defaults to `-min(tpf.flux) + 1`
    fiducial_frame: int
        The tpf slice to start with by default, it is assumed the WCS
        is exact for this frame.
    scale: str
        Color scale for tpf figure. Default is 'log'
    vmin: int [optional]
        Minimum color scale for tpf figure
    vmax: int [optional]
        Maximum color scale for tpf figure
    cmap: str
        Colormap to use for tpf plot. Default is 'Viridis256'
    tools: str
        Bokeh tool list
    Returns
    -------
    fig, stretch_slider : bokeh.plotting.figure.Figure, RangeSlider
    """
    if pedestal is None:
        pedestal = -np.nanmin(tpf.flux.value) + 1
    if scale == 'linear':
        pedestal = 0

    if tpf.mission in ['Kepler', 'K2']:
        title = 'Pixel data (CCD {}.{})'.format(tpf.module, tpf.output)
    elif tpf.mission == 'TESS':
        title = 'Pixel data (Camera {}.{})'.format(tpf.camera, tpf.ccd)
    else:
        title = "Pixel data"

    # We subtract 0.5 from the range below because pixel coordinates refer to
    # the middle of a pixel, e.g. (col, row) = (10.0, 20.0) is a pixel center.
    fig = figure(plot_width=plot_width,
                 plot_height=plot_height,
                 x_range=(tpf.column - 0.5, tpf.column + tpf.shape[2] - 0.5),
                 y_range=(tpf.row - 0.5, tpf.row + tpf.shape[1] - 0.5),
                 title=title,
                 tools=tools,
                 toolbar_location="below",
                 border_fill_color="whitesmoke")

    fig.yaxis.axis_label = 'Pixel Row Number'
    fig.xaxis.axis_label = 'Pixel Column Number'

    vlo, lo, hi, vhi = np.nanpercentile(tpf.flux.value + pedestal,
                                        [0.2, 1, 95, 99.8])
    if vmin is not None:
        vlo, lo = vmin, vmin
    if vmax is not None:
        vhi, hi = vmax, vmax

    if scale == 'log':
        vstep = (np.log10(vhi) -
                 np.log10(vlo)) / 300.0  # assumes counts >> 1.0!
    if scale == 'linear':
        vstep = (vhi - vlo) / 300.0  # assumes counts >> 1.0!

    if scale == 'log':
        color_mapper = LogColorMapper(palette=cmap, low=lo, high=hi)
    elif scale == 'linear':
        color_mapper = LinearColorMapper(palette=cmap, low=lo, high=hi)
    else:
        raise ValueError(
            'Please specify either `linear` or `log` scale for color.')

    fig.image([tpf.flux.value[fiducial_frame, :, :] + pedestal],
              x=tpf.column - 0.5,
              y=tpf.row - 0.5,
              dw=tpf.shape[2],
              dh=tpf.shape[1],
              dilate=True,
              color_mapper=color_mapper,
              name="tpfimg")

    # The colorbar will update with the screen stretch slider
    # The colorbar margin increases as the length of the tick labels grows.
    # This colorbar share of the plot window grows, shrinking plot area.
    # This effect is known, some workarounds might work to fix the plot area:
    # https://github.com/bokeh/bokeh/issues/5186

    if scale == 'log':
        ticker = LogTicker(desired_num_ticks=8)
    elif scale == 'linear':
        ticker = BasicTicker(desired_num_ticks=8)

    color_bar = ColorBar(color_mapper=color_mapper,
                         ticker=ticker,
                         label_standoff=-10,
                         border_line_color=None,
                         location=(0, 0),
                         background_fill_color='whitesmoke',
                         major_label_text_align='left',
                         major_label_text_baseline='middle',
                         title='e/s',
                         margin=0)
    fig.add_layout(color_bar, 'right')

    color_bar.formatter = PrintfTickFormatter(format="%14i")

    if tpf_source is not None:
        fig.rect('xx',
                 'yy',
                 1,
                 1,
                 source=tpf_source,
                 fill_color='gray',
                 fill_alpha=0.4,
                 line_color='white')

    # Configure the stretch slider and its callback function
    if scale == 'log':
        start, end = np.log10(vlo), np.log10(vhi)
        values = (np.log10(lo), np.log10(hi))
    elif scale == 'linear':
        start, end = vlo, vhi
        values = (lo, hi)

    stretch_slider = RangeSlider(start=start,
                                 end=end,
                                 step=vstep,
                                 title='Screen Stretch ({})'.format(scale),
                                 value=values,
                                 orientation='horizontal',
                                 width=200,
                                 direction='ltr',
                                 show_value=True,
                                 sizing_mode='fixed',
                                 height=15,
                                 name='tpfstretch')

    def stretch_change_callback_log(attr, old, new):
        """TPF stretch slider callback."""
        fig.select('tpfimg')[0].glyph.color_mapper.high = 10**new[1]
        fig.select('tpfimg')[0].glyph.color_mapper.low = 10**new[0]

    def stretch_change_callback_linear(attr, old, new):
        """TPF stretch slider callback."""
        fig.select('tpfimg')[0].glyph.color_mapper.high = new[1]
        fig.select('tpfimg')[0].glyph.color_mapper.low = new[0]

    if scale == 'log':
        stretch_slider.on_change('value', stretch_change_callback_log)
    if scale == 'linear':
        stretch_slider.on_change('value', stretch_change_callback_linear)

    return fig, stretch_slider
Example #8
0
def make_tpf_figure_elements(tpf,
                             tpf_source,
                             pedestal=None,
                             fiducial_frame=None,
                             plot_width=370,
                             plot_height=340):
    """Returns the lightcurve figure elements.

    Parameters
    ----------
    tpf : TargetPixelFile
        TPF to show.
    tpf_source : bokeh.plotting.ColumnDataSource
        TPF data source.
    pedestal: float
        A scalar value to be added to the TPF flux values, often to avoid
        taking the log of a negative number in colorbars.
        Defaults to `-min(tpf.flux) + 1`
    fiducial_frame: int
        The tpf slice to start with by default, it is assumed the WCS
        is exact for this frame.

    Returns
    -------
    fig, stretch_slider : bokeh.plotting.figure.Figure, RangeSlider
    """
    if pedestal is None:
        pedestal = -np.nanmin(tpf.flux) + 1

    if tpf.mission in ['Kepler', 'K2']:
        title = 'Pixel data (CCD {}.{})'.format(tpf.module, tpf.output)
    elif tpf.mission == 'TESS':
        title = 'Pixel data (Camera {}.{})'.format(tpf.camera, tpf.ccd)
    else:
        title = "Pixel data"

    fig = figure(plot_width=plot_width,
                 plot_height=plot_height,
                 x_range=(tpf.column, tpf.column + tpf.shape[2]),
                 y_range=(tpf.row, tpf.row + tpf.shape[1]),
                 title=title,
                 tools='tap,box_select,wheel_zoom,reset',
                 toolbar_location="below",
                 border_fill_color="whitesmoke")

    fig.yaxis.axis_label = 'Pixel Row Number'
    fig.xaxis.axis_label = 'Pixel Column Number'

    vlo, lo, hi, vhi = np.nanpercentile(tpf.flux + pedestal,
                                        [0.2, 1, 95, 99.8])
    vstep = (np.log10(vhi) - np.log10(vlo)) / 300.0  # assumes counts >> 1.0!
    color_mapper = LogColorMapper(palette="Viridis256", low=lo, high=hi)

    fig.image([tpf.flux[fiducial_frame, :, :] + pedestal],
              x=tpf.column,
              y=tpf.row,
              dw=tpf.shape[2],
              dh=tpf.shape[1],
              dilate=True,
              color_mapper=color_mapper,
              name="tpfimg")

    # The colorbar will update with the screen stretch slider
    # The colorbar margin increases as the length of the tick labels grows.
    # This colorbar share of the plot window grows, shrinking plot area.
    # This effect is known, some workarounds might work to fix the plot area:
    # https://github.com/bokeh/bokeh/issues/5186
    color_bar = ColorBar(color_mapper=color_mapper,
                         ticker=LogTicker(desired_num_ticks=8),
                         label_standoff=-10,
                         border_line_color=None,
                         location=(0, 0),
                         background_fill_color='whitesmoke',
                         major_label_text_align='left',
                         major_label_text_baseline='middle',
                         title='e/s',
                         margin=0)
    fig.add_layout(color_bar, 'right')

    color_bar.formatter = PrintfTickFormatter(format="%14u")

    if tpf_source is not None:
        fig.rect('xx',
                 'yy',
                 1,
                 1,
                 source=tpf_source,
                 fill_color='gray',
                 fill_alpha=0.4,
                 line_color='white')

    # Configure the stretch slider and its callback function
    stretch_slider = RangeSlider(start=np.log10(vlo),
                                 end=np.log10(vhi),
                                 step=vstep,
                                 title='Screen Stretch (log)',
                                 value=(np.log10(lo), np.log10(hi)),
                                 orientation='horizontal',
                                 width=200,
                                 height=10,
                                 direction='ltr',
                                 show_value=True,
                                 sizing_mode='fixed',
                                 name='tpfstretch')

    def stretch_change_callback(attr, old, new):
        """TPF stretch slider callback."""
        fig.select('tpfimg')[0].glyph.color_mapper.high = 10**new[1]
        fig.select('tpfimg')[0].glyph.color_mapper.low = 10**new[0]

    stretch_slider.on_change('value', stretch_change_callback)

    return fig, stretch_slider
Example #9
0
def get_data():
    cursor = collection.find()
    data = pd.DataFrame(list(cursor))
    data = data.set_index('_id')
    data = data.sort_index(ascending=True)
    timestamps = pd.to_datetime(data.index, unit='s').to_series()
    returns = data.returns.cumsum()*100
    return timestamps, returns

timestamps, returns = get_data()
output_server('bitpredict_performance')

background = '#f2f2f2'
ylabel_standoff = 0
xformatter = DatetimeTickFormatter(formats=dict(hours=["%H:%M"]))
yformatter = PrintfTickFormatter(format="%8.1f")

p = figure(title=None,
           plot_width=750,
           plot_height=500,
           x_axis_type='datetime',
           min_border_top=5,
           min_border_bottom=10,
           background_fill=background,
           x_axis_label='Date',
           tools='',
           toolbar_location=None)
p.line(x=timestamps,
       y=returns,
       name='returns',
       color='#8959a8',
Example #10
0
#high_box = BoxAnnotation(bottom=70, fill_alpha=0.1, fill_color=GREEN)
#p3.add_layout(high_box)

box = BoxAnnotation(bottom=30, top=70, fill_alpha=0.25, fill_color=PURPLE)
p3.add_layout(box)
hline = Span(location=30,
             dimension='width',
             line_color='black',
             line_dash=[5, 10],
             line_width=0.5)
p3.renderers.extend([hline])
hline = Span(location=50,
             dimension='width',
             line_color='black',
             line_width=0.5)
p3.renderers.extend([hline])
hline = Span(location=70,
             dimension='width',
             line_color='black',
             line_dash=[5, 10],
             line_width=0.5)
p3.renderers.extend([hline])

#p3.y_range = Range1d(0, 100)
p3.yaxis.ticker = [30, 50, 70]
p3.yaxis.formatter = PrintfTickFormatter(format="%f%%")
p3.grid.grid_line_alpha = 0.3

output_file("Crypto_ta.html", title="Crypto TA")
show(column(p, p2, p3))
Example #11
0
def run_chart(exchange, asset, limit):
    client = pymongo.MongoClient()
    db = client['bitpredict_' + exchange]
    collection = db[asset_ + '_predictions']

    def get_data():
        cursor = collection.find().limit(limit).sort('_id', pymongo.DESCENDING)
        data = pd.DataFrame(list(cursor))
        data = data.set_index('_id')
        data = data.sort_index(ascending=True)
        timestamps = pd.to_datetime(data.index, unit='s').to_series()
        prices = data.price
        predictions = data.prediction * 10000
        returns = (data.position * data.change).cumsum() * 10000
        return timestamps, prices, predictions, returns

    timestamps, prices, predictions, returns = get_data()
    output_server('bitpredict_' + exchange + '_extended')

    background = '#f2f2f2'
    ylabel_standoff = 0
    xformatter = DatetimeTickFormatter(formats=dict(hours=["%H:%M"]))
    yformatter = PrintfTickFormatter(format="%8.1f")
    p1 = figure(title=None,
                plot_width=750,
                plot_height=300,
                x_axis_type='datetime',
                min_border_top=10,
                min_border_bottom=33,
                background_fill=background,
                tools='',
                toolbar_location=None)
    p1.line(x=timestamps,
            y=prices,
            name='prices',
            color='#4271ae',
            line_width=1,
            legend='Bitcoin Bid/Ask Midpoint',
            line_cap='round',
            line_join='round')
    p1.legend.orientation = 'top_left'
    p1.legend.border_line_color = background
    p1.outline_line_color = None
    p1.xgrid.grid_line_color = 'white'
    p1.ygrid.grid_line_color = 'white'
    p1.axis.axis_line_color = None
    p1.axis.major_tick_line_color = None
    p1.axis.minor_tick_line_color = None
    p1.yaxis.axis_label = 'Price'
    p1.yaxis.axis_label_standoff = ylabel_standoff
    p1.xaxis.formatter = xformatter
    p1.yaxis.formatter = PrintfTickFormatter(format='%8.2f')
    p1.yaxis.major_label_text_font = 'courier'
    p1.xaxis.major_label_text_font = 'courier'

    p2 = figure(title=None,
                plot_width=750,
                plot_height=295,
                x_axis_type='datetime',
                min_border_top=5,
                min_border_bottom=33,
                background_fill=background,
                tools='',
                toolbar_location=None)
    p2.line(x=timestamps,
            y=predictions,
            name='predictions',
            color='#c82829',
            line_width=1,
            legend='30 Second Prediction',
            line_cap='round',
            line_join='round')
    p2.legend.orientation = 'top_left'
    p2.legend.border_line_color = background
    p2.outline_line_color = None
    p2.xgrid.grid_line_color = 'white'
    p2.ygrid.grid_line_color = 'white'
    p2.axis.axis_line_color = None
    p2.axis.major_tick_line_color = None
    p2.axis.minor_tick_line_color = None
    p2.yaxis.axis_label = 'Basis Points'
    p2.yaxis.axis_label_standoff = ylabel_standoff
    p2.xaxis.formatter = xformatter
    p2.yaxis.formatter = yformatter
    p2.yaxis.major_label_text_font = 'courier'
    p2.xaxis.major_label_text_font = 'courier'
    p2.x_range = p1.x_range

    p3 = figure(title=None,
                plot_width=750,
                plot_height=320,
                x_axis_type='datetime',
                min_border_top=5,
                min_border_bottom=10,
                background_fill=background,
                x_axis_label='Greenwich Mean Time',
                tools='',
                toolbar_location=None)
    p3.line(x=timestamps,
            y=returns,
            name='returns',
            color='#8959a8',
            line_width=1,
            legend='Cumulative Return',
            line_cap='round',
            line_join='round')
    p3.legend.orientation = 'top_left'
    p3.legend.border_line_color = background
    p3.outline_line_color = None
    p3.xgrid.grid_line_color = 'white'
    p3.ygrid.grid_line_color = 'white'
    p3.axis.axis_line_color = None
    p3.axis.major_tick_line_color = None
    p3.axis.minor_tick_line_color = None
    p3.yaxis.axis_label = 'Basis Points'
    p3.yaxis.axis_label_standoff = ylabel_standoff
    p3.xaxis.formatter = xformatter
    p3.yaxis.formatter = yformatter
    p3.xaxis.axis_label_standoff = 12
    p3.yaxis.major_label_text_font = 'courier'
    p3.xaxis.major_label_text_font = 'courier'
    p3.x_range = p1.x_range

    vp = vplot(p1, p2, p3)
    push()
    ip = load(urlopen('http://jsonip.com'))['ip']
    ssn = cursession()
    ssn.publish()
    embed.autoload_server(vp, ssn, public=True)

    renderer = p1.select(dict(name='prices'))
    ds_prices = renderer[0].data_source
    renderer = p2.select(dict(name='predictions'))
    ds_predictions = renderer[0].data_source
    renderer = p3.select(dict(name='returns'))
    ds_returns = renderer[0].data_source

    while True:
        timestamps, prices, predictions, returns = get_data(exchange, limit)
        ds_prices.data['x'] = timestamps
        ds_predictions.data['x'] = timestamps
        ds_returns.data['x'] = timestamps
        ds_prices.data['y'] = prices
        ds_predictions.data['y'] = predictions
        ds_returns.data['y'] = returns
        ssn.store_objects(ds_prices)
        ssn.store_objects(ds_predictions)
        ssn.store_objects(ds_returns)
        time.sleep(60)
Example #12
0
    data = data.set_index('_id')
    data = data.sort_index(ascending=True)
    timestamps = pd.to_datetime(data.index, unit='s').to_series()
    prices = data.price
    predictions = data.prediction * 10000
    returns = (data.position * data.change).cumsum() * 10000
    return timestamps, prices, predictions, returns


timestamps, prices, predictions, returns = get_data()
output_server('bitpredict')

background = '#f2f2f2'
ylabel_standoff = 0
xformatter = DatetimeTickFormatter(formats=dict(minutes=["%H:%M"]))
yformatter = PrintfTickFormatter(format="%8.1f")
p1 = figure(title=None,
            plot_width=750,
            plot_height=300,
            x_axis_type='datetime',
            min_border_top=10,
            min_border_bottom=33,
            background_fill=background,
            tools='',
            toolbar_location=None)
p1.line(x=timestamps,
        y=prices,
        name='prices',
        color='#4271ae',
        line_width=1,
        legend='Bitcoin Bid/Ask Midpoint',
def __make_daybyday_interactive_timeline(
    df: pd.DataFrame,
    *,
    geo_df: geopandas.GeoDataFrame,
    value_col: str,
    transform_df_func: Callable[[pd.DataFrame], pd.DataFrame] = None,
    stage: Union[DiseaseStage, Literal[Select.ALL]] = Select.ALL,
    count: Union[Counting, Literal[Select.ALL]] = Select.ALL,
    out_file_basename: str,
    subplot_title_prefix: str,
    plot_aspect_ratio: float = None,
    cmap=None,
    n_cbar_buckets: int = None,
    n_buckets_btwn_major_ticks: int = None,
    n_minor_ticks_btwn_major_ticks: int = None,
    per_capita_denominator: int = None,
    x_range: Tuple[float, float],
    y_range: Tuple[float, float],
    min_visible_y_range: float,
    should_make_video: bool,
) -> InfoForAutoload:
    """Create the bokeh interactive timeline plot(s)

    This function takes the given DataFrame, which must contain COVID data for locations
    on different dates, and a GeoDataFrame, which contains the long/lat coords for those
    locations, and creates an interactive choropleth of the COVID data over time.

    :param df: The COVID data DataFrame
    :type df: pd.DataFrame
    :param geo_df: The geometry GeoDataFrame for the locations in `df`
    :type geo_df: geopandas.GeoDataFrame
    :param value_col: The column of `df` containing the values to plot in the
    choropleth; should be something like "Case_Counts" or "Case_Diff_From_Prev_Day"
    :type value_col: str
    :param stage: The DiseaseStage to plot, defaults to Select.ALL. If ALL, then all
    stages are plotted and are stacked vertically.
    :type stage: Union[DiseaseStage, Literal[Select.ALL]], optional
    :param count: The Counting to plot, defaults to Select.ALL. If ALL, then all
    count types are plotted and are stacked horizontally.
    :type count: Union[Counting, Literal[Select.ALL]], optional
    :param out_file_basename: The basename of the file to save the interactive plots to
    (there are two components, the JS script and the HTML <div>)
    :type out_file_basename: str
    :param subplot_title_prefix: What the first part of the subplot title should be;
    probably a function of `value_col` (if value_col is "Case_Counts" then this param
    might be "Cases" or "# of Cases")
    :type subplot_title_prefix: str
    :param x_range: The range of the x-axis as (min, max)
    :type x_range: Tuple[float, float]
    :param y_range: The range of the y-axis as (min, max)
    :type y_range: Tuple[float, float]
    :param min_visible_y_range: The minimum height (in axis units) of the y-axis; it
    will not be possible to zoom in farther than this on the choropleth.
    :type min_visible_y_range: float
    :param should_make_video: Optionally run through the timeline day by day, capture
    a screenshot for each day, and then stitch the screenshots into a video. The video
    shows the same info as the interactive plots, but not interactively. This easily
    takes 20x as long as just making the graphs themselves, so use with caution.
    :type should_make_video: bool
    :param transform_df_func: This function expects data in a certain format, and does
    a bunch of preprocessing (expected to be common) before plotting. This gives you a
    chance to do any customization on the postprocessed df before it's plotted. Defaults
    to None, in which case no additional transformation is performed.
    :type transform_df_func: Callable[[pd.DataFrame], pd.DataFrame], optional
    :param plot_aspect_ratio: The aspect ratio of the plot as width/height; if set, the
    aspect ratio will be fixed to this. Defaults to None, in which case the aspect ratio
    is determined from the x_range and y_range arguments
    :type plot_aspect_ratio: float, optional
    :param cmap: The colormap to use as either a matplotlib-compatible colormap or a
    list of hex strings (e.g., ["#ae8f1c", ...]). Defaults to None in which case a
    reasonable default is used.
    :type cmap: Matplotlib-compatible colormap or List[str], optional
    :param n_cbar_buckets: How many colorbar buckets to use. Has little effect if the
    colormap is continuous, but always works in conjunction with
    n_buckets_btwn_major_ticks to determine the number of major ticks. Defaults to 6.
    :type n_cbar_buckets: int, optional
    :param n_buckets_btwn_major_ticks: How many buckets are to lie between colorbar
    major ticks, determining how many major ticks are drawn. Defaults to 1.
    :type n_buckets_btwn_major_ticks: int, optional
    :param n_minor_ticks_btwn_major_ticks: How many minor ticks to draw between colorbar
    major ticks. Defaults to 8 (which means each pair of major ticks has 10 ticks
    total).
    :type n_minor_ticks_btwn_major_ticks: int, optional
    :param per_capita_denominator: When describing per-capita numbers, what to use as
    the denominator (e.g., cases per 100,000 people). If None, it is automatically
    computed per plot to be appropriately scaled for the data.
    :type per_capita_denominator: int, optional
    :raises ValueError: [description]
    :return: The two pieces of info required to make a Bokeh autoloading HTML+JS plot:
    the HTML div to be inserted somewhere in the HTML body, and the JS file that will
    load the plot into that div.
    :rtype: InfoForAutoload
    """

    Counting.verify(count, allow_select=True)
    DiseaseStage.verify(stage, allow_select=True)

    # The date as a string, so that bokeh can use it as a column name
    STRING_DATE_COL = "String_Date_"
    # A column whose sole purpose is to be a (the same) date associated with each
    # location
    FAKE_DATE_COL = "Fake_Date_"
    # The column we'll actually use for the colors; it's computed from value_col
    COLOR_COL = "Color_"

    # Under no circumstances may you change this date format
    # It's not just a pretty date representation; it actually has to match up with the
    # date strings computed in JS
    DATE_FMT = r"%Y-%m-%d"

    ID_COLS = [
        REGION_NAME_COL,
        Columns.DATE,
        Columns.STAGE,
        Columns.COUNT_TYPE,
    ]

    if cmap is None:
        cmap = cmocean.cm.matter

    if n_cbar_buckets is None:
        n_cbar_buckets = 6

    if n_buckets_btwn_major_ticks is None:
        n_buckets_btwn_major_ticks = 1

    if n_minor_ticks_btwn_major_ticks is None:
        n_minor_ticks_btwn_major_ticks = 8

    n_cbar_major_ticks = n_cbar_buckets // n_buckets_btwn_major_ticks + 1

    try:
        color_list = [
            # Convert matplotlib colormap to bokeh (list of hex strings)
            # https://stackoverflow.com/a/49934218
            RGB(*rgb).to_hex()
            for i, rgb in enumerate((255 * cmap(range(256))).astype("int"))
        ]
    except TypeError:
        color_list = cmap

    color_list: List[BokehColor]

    if stage is Select.ALL:
        stage_list = list(DiseaseStage)
    else:
        stage_list = [stage]

    if count is Select.ALL:
        count_list = list(Counting)
    else:
        count_list = [count]

    stage_list: List[DiseaseStage]
    count_list: List[Counting]

    stage_count_list: List[Tuple[DiseaseStage, Counting]] = list(
        itertools.product(stage_list, count_list))

    df = df.copy()

    # Unadjust dates (see SaveFormats._adjust_dates)
    normalized_dates = df[Columns.DATE].dt.normalize()
    is_at_midnight = df[Columns.DATE] == normalized_dates
    df.loc[is_at_midnight, Columns.DATE] -= pd.Timedelta(days=1)
    df.loc[~is_at_midnight, Columns.DATE] = normalized_dates[~is_at_midnight]

    min_date, max_date = df[Columns.DATE].agg(["min", "max"])
    dates: List[pd.Timestamp] = pd.date_range(start=min_date,
                                              end=max_date,
                                              freq="D")
    max_date_str = max_date.strftime(DATE_FMT)

    # Get day-by-day case diffs per location, date, stage, count-type

    # Make sure data exists for every date for every state so that the entire country is
    # plotted each day; fill missing data with 0 (missing really *is* as good as 0)
    # enums will be replaced by their name (kind of important)
    id_cols_product: pd.MultiIndex = pd.MultiIndex.from_product(
        [
            df[REGION_NAME_COL].unique(),
            dates,
            [s.name for s in DiseaseStage],
            [c.name for c in Counting],
        ],
        names=ID_COLS,
    )

    df = (id_cols_product.to_frame(index=False).merge(
        df,
        how="left",
        on=ID_COLS,
    ).sort_values(ID_COLS))

    df[STRING_DATE_COL] = df[Columns.DATE].dt.strftime(DATE_FMT)
    df[Columns.CASE_COUNT] = df[Columns.CASE_COUNT].fillna(0)

    if transform_df_func is not None:
        df = transform_df_func(df)

    df = geo_df.merge(df, how="inner", on=REGION_NAME_COL)[[
        REGION_NAME_COL,
        Columns.DATE,
        STRING_DATE_COL,
        Columns.STAGE,
        Columns.COUNT_TYPE,
        value_col,
    ]]

    dates: List[pd.Timestamp] = [
        pd.Timestamp(d) for d in df[Columns.DATE].unique()
    ]

    values_mins_maxs = (df[df[value_col] > 0].groupby(
        [Columns.STAGE, Columns.COUNT_TYPE])[value_col].agg(["min", "max"]))

    vmins: pd.Series = values_mins_maxs["min"]
    vmaxs: pd.Series = values_mins_maxs["max"]

    pow10s_series: pd.Series = vmaxs.map(
        lambda x: int(10**(-np.floor(np.log10(x)))))

    # _pow_10s_series_dict = {}
    # for stage in DiseaseStage:
    #     _pow_10s_series_dict.update(
    #         {
    #             (stage.name, Counting.TOTAL_CASES.name): 100000,
    #             (stage.name, Counting.PER_CAPITA.name): 10000,
    #         }
    #     )

    # pow10s_series = pd.Series(_pow_10s_series_dict)

    vmins: dict = vmins.to_dict()
    vmaxs: dict = vmaxs.to_dict()

    for stage in DiseaseStage:
        _value_key = (stage.name, Counting.PER_CAPITA.name)
        if per_capita_denominator is None:
            _max_pow10 = pow10s_series.loc[(slice(None),
                                            Counting.PER_CAPITA.name)].max()
        else:
            _max_pow10 = per_capita_denominator

        vmins[_value_key] *= _max_pow10
        vmaxs[_value_key] *= _max_pow10
        pow10s_series[_value_key] = _max_pow10

    percap_pow10s: pd.Series = df.apply(
        lambda row: pow10s_series[
            (row[Columns.STAGE], row[Columns.COUNT_TYPE])],
        axis=1,
    )

    _per_cap_rows = df[Columns.COUNT_TYPE] == Counting.PER_CAPITA.name
    df.loc[_per_cap_rows, value_col] *= percap_pow10s.loc[_per_cap_rows]

    # Ideally we wouldn't have to pivot, and we could do a JIT join of state longs/lats
    # after filtering the data. Unfortunately this is not possible, and a long data
    # format leads to duplication of the very large long/lat lists; pivoting is how we
    # avoid that. (This seems to be one downside of bokeh when compared to plotly)
    df = (df.pivot_table(
        index=[REGION_NAME_COL, Columns.STAGE, Columns.COUNT_TYPE],
        columns=STRING_DATE_COL,
        values=value_col,
        aggfunc="first",
    ).reset_index().merge(
        geo_df[[REGION_NAME_COL, LONG_COL, LAT_COL]],
        how="inner",
        on=REGION_NAME_COL,
    ))

    # All three oclumns are just initial values; they'll change with the date slider
    df[value_col] = df[max_date_str]
    df[FAKE_DATE_COL] = max_date_str
    df[COLOR_COL] = np.where(df[value_col] > 0, df[value_col], "NaN")

    # Technically takes a df but we don't need the index
    bokeh_data_source = ColumnDataSource(
        {k: v.tolist()
         for k, v in df.to_dict(orient="series").items()})

    filters = [[
        GroupFilter(column_name=Columns.STAGE, group=stage.name),
        GroupFilter(column_name=Columns.COUNT_TYPE, group=count.name),
    ] for stage, count in stage_count_list]

    figures = []

    for subplot_index, (stage, count) in enumerate(stage_count_list):
        # fig = bplotting.figure()
        # ax: plt.Axes = fig.add_subplot(
        #     len(stage_list), len(count_list), subplot_index
        # )

        # # Add timestamp to top right axis
        # if subplot_index == 2:
        #     ax.text(
        #         1.25,  # Coords are arbitrary magic numbers
        #         1.23,
        #         f"Last updated {NOW_STR}",
        #         horizontalalignment="right",
        #         fontsize="small",
        #         transform=ax.transAxes,
        #     )

        view = CDSView(source=bokeh_data_source,
                       filters=filters[subplot_index])

        vmin = vmins[(stage.name, count.name)]
        vmax = vmaxs[(stage.name, count.name)]

        # Compute and set axes titles
        if stage is DiseaseStage.CONFIRMED:
            fig_stage_name = "Cases"
        elif stage is DiseaseStage.DEATH:
            fig_stage_name = "Deaths"
        else:
            raise ValueError

        fig_title_components: List[str] = []
        if subplot_title_prefix is not None:
            fig_title_components.append(subplot_title_prefix)

        fig_title_components.append(fig_stage_name)

        if count is Counting.PER_CAPITA:
            _per_cap_denom = pow10s_series[(stage.name, count.name)]
            fig_title_components.append(f"Per {_per_cap_denom:,d} people")
            formatter = PrintfTickFormatter(format=r"%2.3f")
            label_standoff = 12
            tooltip_fmt = "{0.000}"
        else:
            formatter = NumeralTickFormatter(format="0.0a")
            label_standoff = 10
            tooltip_fmt = "{0}"

        color_mapper = LogColorMapper(
            color_list,
            low=vmin,
            high=vmax,
            nan_color="#f2f2f2",
        )

        fig_title = " ".join(fig_title_components)

        if plot_aspect_ratio is None:
            if x_range is None or y_range is None:
                raise ValueError("Must provide both `x_range` and `y_range`" +
                                 " when `plot_aspect_ratio` is None")
            plot_aspect_ratio = (x_range[1] - x_range[0]) / (y_range[1] -
                                                             y_range[0])

        # Create figure object
        p = bplotting.figure(
            title=fig_title,
            title_location="above",
            tools=[
                HoverTool(
                    tooltips=[
                        ("Date", f"@{{{FAKE_DATE_COL}}}"),
                        ("State", f"@{{{REGION_NAME_COL}}}"),
                        ("Count", f"@{{{value_col}}}{tooltip_fmt}"),
                    ],
                    toggleable=False,
                ),
                PanTool(),
                BoxZoomTool(match_aspect=True),
                ZoomInTool(),
                ZoomOutTool(),
                ResetTool(),
            ],
            active_drag=None,
            aspect_ratio=plot_aspect_ratio,
            output_backend="webgl",
            lod_factor=4,
            lod_interval=400,
            lod_threshold=1000,
            lod_timeout=300,
        )

        p.xgrid.grid_line_color = None
        p.ygrid.grid_line_color = None
        # Finally, add the actual choropleth data we care about
        p.patches(
            LONG_COL,
            LAT_COL,
            source=bokeh_data_source,
            view=view,
            fill_color={
                "field": COLOR_COL,
                "transform": color_mapper
            },
            line_color="black",
            line_width=0.25,
            fill_alpha=1,
        )

        # Add evenly spaced ticks and their labels to the colorbar
        # First major, then minor
        # Adapted from https://stackoverflow.com/a/50314773
        bucket_size = (vmax / vmin)**(1 / n_cbar_buckets)
        tick_dist = bucket_size**n_buckets_btwn_major_ticks

        # Simple log scale math
        major_tick_locs = (
            vmin * (tick_dist**np.arange(0, n_cbar_major_ticks))
            # * (bucket_size ** 0.5) # Use this if centering ticks on buckets
        )
        # Get minor locs by linearly interpolating between major ticks
        minor_tick_locs = []
        for major_tick_index, this_major_tick in enumerate(
                major_tick_locs[:-1]):
            next_major_tick = major_tick_locs[major_tick_index + 1]

            # Get minor ticks as numbers in range [this_major_tick, next_major_tick]
            # and exclude the major ticks themselves (once we've used them to
            # compute the minor tick locs)
            minor_tick_locs.extend(
                np.linspace(
                    this_major_tick,
                    next_major_tick,
                    n_minor_ticks_btwn_major_ticks + 2,
                )[1:-1])

        color_bar = ColorBar(
            color_mapper=color_mapper,
            ticker=FixedTicker(ticks=major_tick_locs,
                               minor_ticks=minor_tick_locs),
            formatter=formatter,
            label_standoff=label_standoff,
            major_tick_out=0,
            major_tick_in=13,
            major_tick_line_color="white",
            major_tick_line_width=1,
            minor_tick_out=0,
            minor_tick_in=5,
            minor_tick_line_color="white",
            minor_tick_line_width=1,
            location=(0, 0),
            border_line_color=None,
            bar_line_color=None,
            orientation="vertical",
        )

        p.add_layout(color_bar, "right")
        p.hover.point_policy = "follow_mouse"

        # Bokeh axes (and most other things) are splattable
        p.axis.visible = False

        figures.append(p)

    # Make all figs pan and zoom together by setting their axes equal to each other
    # Also fix the plots' aspect ratios
    figs_iter = iter(np.ravel(figures))
    anchor_fig = next(figs_iter)

    if x_range is not None and y_range is not None:
        data_aspect_ratio = (x_range[1] - x_range[0]) / (y_range[1] -
                                                         y_range[0])
    else:
        data_aspect_ratio = plot_aspect_ratio

    if x_range is not None:
        anchor_fig.x_range = Range1d(
            *x_range,
            bounds="auto",
            min_interval=min_visible_y_range * data_aspect_ratio,
        )

    if y_range is not None:
        anchor_fig.y_range = Range1d(*y_range,
                                     bounds="auto",
                                     min_interval=min_visible_y_range)

    for fig in figs_iter:
        fig.x_range = anchor_fig.x_range
        fig.y_range = anchor_fig.y_range

    # 2x2 grid (for now)
    gp = gridplot(
        figures,
        ncols=len(count_list),
        sizing_mode="scale_both",
        toolbar_location="above",
    )
    plot_layout = [gp]

    # Ok, pause
    # Now we're going into a whole other thing: we're doing all the JS logic behind a
    # date slider that changes which date is shown on the graphs. The structure of the
    # data is one column per date, one row per location, and a few extra columns to
    # store the data the graph will use. When we adjust the date of the slider, we copy
    # the relevant column of the df into the columns the graphs are looking at.
    # That's the easy part; the hard part is handling the "play button" functionality,
    # whereby the user can click one button and the date slider will periodically
    # advance itself. That requires a fair bit of logic to schedule and cancel the
    # timers and make it all feel right.

    # Create unique ID for the JS playback info object for this plot (since it'll be on
    # the webpage with other plots, and their playback info isn't shared)
    _THIS_PLOT_ID = uuid.uuid4().hex

    __TIMER = "'timer'"
    __IS_ACTIVE = "'isActive'"
    __SELECTED_INDEX = "'selectedIndex'"
    __BASE_INTERVAL_MS = "'BASE_INTERVAL'"  # Time (in MS) btwn frames when speed==1
    __TIMER_START_DATE = "'startDate'"
    __TIMER_ELAPSED_TIME_MS = "'elapsedTimeMS'"
    __TIMER_ELAPSED_TIME_PROPORTION = "'elapsedTimeProportion'"
    __SPEEDS_KEY = "'SPEEDS'"
    __PLAYBACK_INFO = f"window._playbackInfo_{_THIS_PLOT_ID}"

    _PBI_TIMER = f"{__PLAYBACK_INFO}[{__TIMER}]"
    _PBI_IS_ACTIVE = f"{__PLAYBACK_INFO}[{__IS_ACTIVE}]"
    _PBI_SELECTED_INDEX = f"{__PLAYBACK_INFO}[{__SELECTED_INDEX}]"
    _PBI_TIMER_START_DATE = f"{__PLAYBACK_INFO}[{__TIMER_START_DATE}]"
    _PBI_TIMER_ELAPSED_TIME_MS = f"{__PLAYBACK_INFO}[{__TIMER_ELAPSED_TIME_MS}]"
    _PBI_TIMER_ELAPSED_TIME_PROPORTION = (
        f"{__PLAYBACK_INFO}[{__TIMER_ELAPSED_TIME_PROPORTION}]")
    _PBI_BASE_INTERVAL = f"{__PLAYBACK_INFO}[{__BASE_INTERVAL_MS}]"
    _PBI_SPEEDS = f"{__PLAYBACK_INFO}[{__SPEEDS_KEY}]"
    _PBI_CURR_INTERVAL_MS = (
        f"{_PBI_BASE_INTERVAL} / {_PBI_SPEEDS}[{_PBI_SELECTED_INDEX}]")

    _SPEED_OPTIONS = [0.25, 0.5, 1.0, 2.0]
    _DEFAULT_SPEED = 1.0
    _DEFAULT_SELECTED_INDEX = _SPEED_OPTIONS.index(_DEFAULT_SPEED)

    _SETUP_WINDOW_PLAYBACK_INFO = f"""
        if (typeof({__PLAYBACK_INFO}) === 'undefined') {{
            {__PLAYBACK_INFO} = {{
                {__TIMER}: null,
                {__IS_ACTIVE}: false,
                {__SELECTED_INDEX}: {_DEFAULT_SELECTED_INDEX},
                {__TIMER_START_DATE}: null,
                {__TIMER_ELAPSED_TIME_MS}: 0,
                {__TIMER_ELAPSED_TIME_PROPORTION}: 0,
                {__BASE_INTERVAL_MS}: 1000,
                {__SPEEDS_KEY}: {_SPEED_OPTIONS}
            }};
        }}

    """

    _DEFFUN_INCR_DATE = f"""
        // See this link for why this works (it's an undocumented feature?)
        // https://discourse.bokeh.org/t/5254
        // Tl;dr we need this to automatically update the hover as the play button plays
        // Without this, the hover tooltip only updates when we jiggle the mouse
        // slightly

        let prev_val = null;
        source.inspect.connect(v => prev_val = v);

        function updateDate() {{
            {_PBI_TIMER_START_DATE} = new Date();
            {_PBI_TIMER_ELAPSED_TIME_MS} = 0
            if (dateSlider.value < maxDate) {{
                dateSlider.value += 86400000;
            }}

            if (dateSlider.value >= maxDate) {{
                console.log(dateSlider.value, maxDate)
                console.log('reached end')
                clearInterval({_PBI_TIMER});
                {_PBI_IS_ACTIVE} = false;
                playPauseButton.active = false;
                playPauseButton.change.emit();
                playPauseButton.label = 'Restart';
            }}

            dateSlider.change.emit();

            // This is pt. 2 of the prev_val/inspect stuff above
            if (prev_val !== null) {{
                source.inspect.emit(prev_val);
            }}
        }}
    """

    _DO_START_TIMER = f"""
        function startLoopTimer() {{
            updateDate();
            if ({_PBI_IS_ACTIVE}) {{
                {_PBI_TIMER} = setInterval(updateDate, {_PBI_CURR_INTERVAL_MS})
            }}

        }}

        {_PBI_TIMER_START_DATE} = new Date();

        // Should never be <0 or >1 but I am being very defensive here
        const proportionRemaining = 1 - (
            {_PBI_TIMER_ELAPSED_TIME_PROPORTION} <= 0
            ? 0
            : {_PBI_TIMER_ELAPSED_TIME_PROPORTION} >= 1
            ? 1
            : {_PBI_TIMER_ELAPSED_TIME_PROPORTION}
        );
        const remainingTimeMS = (
            {_PBI_CURR_INTERVAL_MS} * proportionRemaining
        );
        const initialInterval = (
            {_PBI_TIMER_ELAPSED_TIME_MS} === 0
            ? 0
            : remainingTimeMS
        );

        {_PBI_TIMER} = setTimeout(
            startLoopTimer,
            initialInterval
        );
    """

    _DO_STOP_TIMER = f"""
        const now = new Date();
        {_PBI_TIMER_ELAPSED_TIME_MS} += (
            now.getTime() - {_PBI_TIMER_START_DATE}.getTime()
        );
        {_PBI_TIMER_ELAPSED_TIME_PROPORTION} = (
            {_PBI_TIMER_ELAPSED_TIME_MS} / {_PBI_CURR_INTERVAL_MS}
        );
        clearInterval({_PBI_TIMER});
    """

    update_on_date_change_callback = CustomJS(
        args={"source": bokeh_data_source},
        code=f"""

        {_SETUP_WINDOW_PLAYBACK_INFO}

        const sliderValue = cb_obj.value;
        const sliderDate = new Date(sliderValue)
        // Ugh, actually requiring the date to be YYYY-MM-DD (matching DATE_FMT)
        const dateStr = sliderDate.toISOString().split('T')[0]

        const data = source.data;

        {_PBI_TIMER_ELAPSED_TIME_MS} = 0

        if (typeof(data[dateStr]) !== 'undefined') {{
            data['{value_col}'] = data[dateStr]

            const valueCol = data['{value_col}'];
            const colorCol = data['{COLOR_COL}'];
            const fakeDateCol = data['{FAKE_DATE_COL}']

            for (var i = 0; i < data['{value_col}'].length; i++) {{
                const value = valueCol[i]
                if (value == 0) {{
                    colorCol[i] = 'NaN';
                }} else {{
                    colorCol[i] = value;
                }}

                fakeDateCol[i] = dateStr;
            }}

            source.change.emit();

        }}

        """,
    )

    # Taking day-over-day diffs means the min slider day is one more than the min data
    # date (might be off by 1 if not using day over diffs but in practice not an issue)
    min_slider_date = min_date + pd.Timedelta(days=1)
    date_slider = DateSlider(
        start=min_slider_date,
        end=max_date,
        value=max_date,
        step=1,
        sizing_mode="stretch_width",
        width_policy="fit",
    )
    date_slider.js_on_change("value", update_on_date_change_callback)

    play_pause_button = Toggle(
        label="Start playing",
        button_type="success",
        active=False,
        sizing_mode="stretch_width",
    )

    animate_playback_callback = CustomJS(
        args={
            "source": bokeh_data_source,
            "dateSlider": date_slider,
            "playPauseButton": play_pause_button,
            "maxDate": max_date,
            "minDate": min_slider_date,
        },
        code=f"""

        {_SETUP_WINDOW_PLAYBACK_INFO}
        {_DEFFUN_INCR_DATE}

        if (dateSlider.value >= maxDate) {{
            if (playPauseButton.active) {{
                dateSlider.value = minDate;
                dateSlider.change.emit();

                // Hack to get timer to wait after date slider wraps; any positive
                // number works but the smaller the better
                {_PBI_TIMER_ELAPSED_TIME_MS} = 1;
            }}
        }}

        const active = cb_obj.active;
        {_PBI_IS_ACTIVE} = active;

        if (active) {{
            playPauseButton.label = 'Playing – Click/tap to pause'
            {_DO_START_TIMER}
        }} else {{
            playPauseButton.label = 'Paused – Click/tap to play'
            {_DO_STOP_TIMER}
        }}

        """,
    )

    play_pause_button.js_on_click(animate_playback_callback)

    change_playback_speed_callback = CustomJS(
        args={
            "source": bokeh_data_source,
            "dateSlider": date_slider,
            "playPauseButton": play_pause_button,
            "maxDate": max_date,
        },
        code=f"""

        {_SETUP_WINDOW_PLAYBACK_INFO}
        {_DEFFUN_INCR_DATE}

        // Must stop timer before handling changing the speed, as stopping the timer
        // saves values based on the current (unchaged) speed selection
        if ({_PBI_TIMER} !== null) {{
            {_DO_STOP_TIMER}
        }}

        const selectedIndex = cb_obj.active;
        {_PBI_SELECTED_INDEX} = selectedIndex;

        if ({_PBI_IS_ACTIVE}) {{
            {_DO_START_TIMER}
        }} else {{
            {_PBI_TIMER_ELAPSED_TIME_MS} = 0
        }}

        console.log({__PLAYBACK_INFO})

    """,
    )

    playback_speed_radio = RadioButtonGroup(
        labels=[f"{speed:.2g}x speed" for speed in _SPEED_OPTIONS],
        active=_DEFAULT_SELECTED_INDEX,
        sizing_mode="stretch_width",
    )
    playback_speed_radio.js_on_click(change_playback_speed_callback)

    plot_layout.append(
        layout_column(
            [
                date_slider,
                layout_row(
                    [play_pause_button, playback_speed_radio],
                    height_policy="min",
                ),
            ],
            width_policy="fit",
            height_policy="min",
        ))
    plot_layout = layout_column(plot_layout, sizing_mode="scale_both")

    # grid = gridplot(figures, ncols=len(count_list), sizing_mode="stretch_both")

    # Create the autoloading bokeh plot info (HTML + JS)
    js_path = str(Path(out_file_basename + "_autoload").with_suffix(".js"))
    tag_html_path = str(
        Path(out_file_basename + "_div_tag").with_suffix(".html"))

    js_code, tag_code = autoload_static(plot_layout, CDN, js_path)
    tag_uuid = re.search(r'id="([^"]+)"', tag_code).group(1)
    tag_code = re.sub(r'src="([^"]+)"', f'src="\\1?uuid={tag_uuid}"', tag_code)

    with open(Paths.DOCS / js_path,
              "w") as f_js, open(Paths.DOCS / tag_html_path, "w") as f_html:
        f_js.write(js_code)
        f_html.write(tag_code)

    # Create the video by creating stills of the graphs for each date and then stitching
    # the images into a video
    if should_make_video:
        save_dir: Path = PNG_SAVE_ROOT_DIR / out_file_basename
        save_dir.mkdir(parents=True, exist_ok=True)

        STILL_WIDTH = 1500
        STILL_HEIGHT = int(np.ceil(STILL_WIDTH / plot_aspect_ratio) *
                           1.05)  # Unclear why *1.05 is necessary
        gp.height = STILL_HEIGHT
        gp.width = STILL_WIDTH
        gp.sizing_mode = "fixed"
        orig_title = anchor_fig.title.text

        for date in dates:
            date_str = date.strftime(DATE_FMT)
            anchor_fig.title = Title(text=f"{orig_title} {date_str}")

            for p in figures:
                p.title = Title(text=p.title.text, text_font_size="20px")

            # Just a reimplementation of the JS code in the date slider's callback
            data = bokeh_data_source.data
            data[value_col] = data[date_str]

            for i, value in enumerate(data[value_col]):
                if value == 0:
                    data[COLOR_COL][i] = "NaN"
                else:
                    data[COLOR_COL][i] = value

                data[FAKE_DATE_COL][i] = date_str

            save_path: Path = (save_dir / date_str).with_suffix(".png")
            export_png(gp, filename=save_path)
            resize_to_even_dims(save_path, pad_bottom=0.08)

            if date == max(dates):
                poster_path: Path = (
                    PNG_SAVE_ROOT_DIR /
                    (out_file_basename + "_poster")).with_suffix(".png")
                poster_path.write_bytes(save_path.read_bytes())

        make_video(save_dir, out_file_basename, 0.9)

    print(f"Did interactive {out_file_basename}")

    return (js_code, tag_code)
def run_chart(exchange, asset, limit):
    client = pymongo.MongoClient()
    db = client['bitpredict_'+exchange]
    collection = db[asset_+'_predictions']

    def get_data():
        if (limit)
            cursor = collection.find()
        else
            cursor = collection.find().limit(limit)
        data = pd.DataFrame(list(cursor))
        data = data.set_index('_id')
        data = data.sort_index(ascending=True)
        timestamps = pd.to_datetime(data.index, unit='s').to_series()
        returns = data.returns.cumsum()*100
        return timestamps, returns

    timestamps, returns = get_data()
    output_server('bitpredict_'+exchange+'_performance')

    background = '#f2f2f2'
    ylabel_standoff = 0
    xformatter = DatetimeTickFormatter(formats=dict(hours=["%H:%M"]))
    yformatter = PrintfTickFormatter(format="%8.1f")
    p = figure(title=None,
                plot_width=750,
                plot_height=500,
                x_axis_type='datetime',
                min_border_top=5,
                min_border_bottom=10,
                background_fill=background,
                x_axis_label='Date',
                tools='',
                toolbar_location=None)
    p.line(x=timestamps,
            y=returns,
            name='returns',
            color='#8959a8',
            line_width=1,
            legend='Cumulative Return',
            line_cap='round',
            line_join='round')
    p.legend.orientation = 'top_left'
    p.legend.border_line_color = background
    p.outline_line_color = None
    p.xgrid.grid_line_color = 'white'
    p.ygrid.grid_line_color = 'white'
    p.axis.axis_line_color = None
    p.axis.major_tick_line_color = None
    p.axis.minor_tick_line_color = None
    p.yaxis.axis_label = 'Percent'
    p.yaxis.axis_label_standoff = ylabel_standoff
    p.xaxis.formatter = xformatter
    p.yaxis.formatter = yformatter
    p.xaxis.axis_label_standoff = 12
    p.yaxis.major_label_text_font = 'courier'
    p.xaxis.major_label_text_font = 'courier'

    push()
    ip = load(urlopen('http://jsonip.com'))['ip']
    ssn = cursession()
    ssn.publish()
    tag = embed.autoload_server(p, ssn, public=True)
    renderer = p.select(dict(name='returns'))
    ds_returns = renderer[0].data_source

    while True:
        timestamps, returns = get_data()
        ds_returns.data['x'] = timestamps
        ds_returns.data['y'] = returns
        ssn.store_objects(ds_returns)
        time.sleep(300)