Source code for interactive_plotting

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
Interactive chart example running a bokeh server
'''

from bokeh.plotting import figure, ColumnDataSource
# from bokeh.models import (HoverTool, BoxZoomTool, WheelZoomTool, ResetTool,
#                           PanTool, SaveTool, UndoTool, RedoTool)
from bokeh.models import NumeralTickFormatter, Range1d, Label
from bokeh.models.widgets import Slider, Button, Select
from bokeh.layouts import column, row, widgetbox
from bokeh.models.layouts import Spacer

import numpy as np
import pandas as pd


# TODO:
# add stacked area for cat_order
# test source.date update using groupby groups/precalculated ColumnDataSources
# add size, alpha sliders
# make tabs for right side controls
# background color selection, alpha control
# add datatable
# add save underlying data (reports?)
# add mark selected employees
# add dataset selection
# add diff comparison
# add hover (with user selection)
# add tools (crosshair, etc)
# add dataset selection
# add dataset group compare
# add dataset employee compare
# add ret_only
# add other chart types
# make this the only display??
# add persist df



[docs]def bk_basic_interactive(doc, df=None, plot_height=700, plot_width=900, dot_size=5): '''run a basic interactive chart as a server app - powered by the bokeh plotting library. Run the app in the jupyter notebook as follows: .. code:: python from functools import partial import pandas as pd import interactive_plotting as ip from bokeh.io import show, output_notebook from bokeh.application.handlers import FunctionHandler from bokeh.application import Application output_notebook() proposal = 'p1' df = pd.read_pickle('dill/ds_' + proposal + '.pkl') handler = FunctionHandler(partial(ip.bk_basic_interactive, df=df)) app = Application(handler) show(app) inputs doc (required input) do not change this input df (dataframe) calculated dataset input, this is a required input plot_height (integer) height of plot in pixels plot_width (integer) width of plot in pixels Add plot_height and/or plot_width parameters as kwargs within the partial method: .. code:: python handler = FunctionHandler(partial(ip.bk_basic_interactive, df=df, plot_height=450, plot_width=625)) Note: the "df" argument is not optional, a valid dataset variable must be assigned. ''' class CallbackID(): def __init__(self, identifier): self.identifier = identifier max_month = df['mnum'].max() # set up color column egs = df['eg'].values sdict = pd.read_pickle('dill/dict_settings.pkl') cdict = pd.read_pickle('dill/dict_color.pkl') eg_cdict = cdict['eg_color_dict'] clr = np.empty(len(df), dtype='object') for eg in eg_cdict.keys(): np.put(clr, np.where(egs == eg)[0], eg_cdict[eg]) df['c'] = clr df['a'] = .7 df['s'] = dot_size # date list for animation label background date_list = list(pd.date_range(start=sdict['starting_date'], periods=max_month, freq='M')) date_list = [x.strftime('%Y %b') for x in date_list] slider_height = plot_height - 200 # create empty data source template source = ColumnDataSource(data=dict(x=[], y=[], c=[], s=[], a=[])) slider_month = Slider(start=0, end=max_month-1, value=0, step=1, title='month', height=slider_height, width=15, tooltips=False, bar_color='#ffe6cc', direction='rtl', orientation='vertical',) display_attrs = ['age', 'jobp', 'cat_order', 'spcnt', 'lspcnt', 'jnum', 'mpay', 'cpay', 'snum', 'lnum', 'ylong', 'mlong', 'idx', 'retdate', 'ldate', 'doh', 's_lmonths', 'new_order'] sel_x = Select(options=display_attrs, value='age', title='x axis attribute:', width=115, height=45) sel_y = Select(options=display_attrs, value='spcnt', title='y axis attribute:', width=115, height=45) label = Label(x=20, y=plot_height - 150, x_units='screen', y_units='screen', text='', text_alpha=.25, text_color='#b3b3b3', text_font_size='70pt') spacer1 = Spacer(height=plot_height, width=30) but_fwd = Button(label='FWD', width=60) but_back = Button(label='BACK', width=60) add_sub = widgetbox(but_fwd, but_back, height=50, width=30) def make_plot(): this_df = get_df() xcol = sel_x.value ycol = sel_y.value source.data = dict(x=this_df[sel_x.value], y=this_df[sel_y.value], c=this_df['c'], a=this_df['a'], s=this_df['s']) non_invert = ['age', 'idx', 's_lmonths', 'mlong', 'ylong', 'cpay', 'mpay'] if xcol in non_invert: xrng = Range1d(df[xcol].min(), df[xcol].max()) else: xrng = Range1d(df[xcol].max(), df[xcol].min()) if ycol in non_invert: yrng = Range1d(df[ycol].min(), df[ycol].max()) else: yrng = Range1d(df[ycol].max(), df[ycol].min()) p = figure(plot_width=plot_width, plot_height=plot_height, x_range=xrng, y_range=yrng, title='') p.circle(x='x', y='y', color='c', size='s', alpha='a', line_color=None, source=source) pcnt_cols = ['spcnt', 'lspcnt'] if xcol in pcnt_cols: p.x_range.end = -.001 p.xaxis[0].formatter = NumeralTickFormatter(format="0.0%") if ycol in pcnt_cols: p.y_range.end = -.001 p.yaxis[0].formatter = NumeralTickFormatter(format="0.0%") if xcol in ['cat_order']: p.x_range.end = -50 if ycol in ['cat_order']: p.y_range.end = -50 if xcol in ['jobp', 'jnum']: p.x_range.end = .95 if ycol in ['jobp', 'jnum']: p.y_range.end = .95 p.xaxis.axis_label = sel_x.value p.yaxis.axis_label = sel_y.value p.add_layout(label) label.text = date_list[slider_month.value] return p def get_df(): filter_df = df[df.mnum == slider_month.value][[sel_x.value, sel_y.value, 'c', 's', 'a']] return filter_df def update_data(attr, old, new): this_df = get_df() source.data = dict(x=this_df[sel_x.value], y=this_df[sel_y.value], c=this_df['c'], a=this_df['a'], s=this_df['s']) label.text = date_list[new] controls = [sel_x, sel_y] wb_controls = [sel_x, sel_y, slider_month] for control in controls: control.on_change('value', lambda attr, old, new: insert_plot()) slider_month.on_change('value', update_data) sizing_mode = 'fixed' inputs = widgetbox(*wb_controls, width=190, height=60, sizing_mode=sizing_mode) def insert_plot(): lo.children[0] = make_plot() def animate_update(): mth = slider_month.value + 1 if mth >= max_month: mth = 0 slider_month.value = mth def fwd(): slider_val = slider_month.value if slider_val < max_month - 1: slider_month.value = slider_val + 1 def back(): slider_val = slider_month.value if slider_val > 0: slider_month.value = slider_val - 1 but_back.on_click(back) but_fwd.on_click(fwd) cb = CallbackID(None) def animate(): if play_button.label == '► Play': play_button.label = '❚❚ Pause' cb.identifier = doc.add_periodic_callback(animate_update, 350) else: play_button.label = '► Play' doc.remove_periodic_callback(cb.identifier) def reset(): slider_month.value = 0 play_button = Button(label='► Play', width=60) play_button.on_click(animate) reset_button = Button(label='Reset', width=60) reset_button.on_click(reset) lo = row(make_plot(), spacer1, inputs, column(play_button, reset_button, add_sub)) doc.add_root(lo)