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
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
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)
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
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
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
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',
#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))
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)
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)