Source code for interactive_plotting

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

'''
.. module:: interactive_plotting

   :synopsis: The bokeh module contains interactive plotting functions.

.. moduleauthor:: Bob Davison <rubydatasystems@fastmail.net>

'''

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, 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: 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)