def bokeh_spacetimepop( frm, geometry, title = '', preamble = '', varNames = None, varNotes = dict(), pw = 700, ph = 700, xZones = dict(), ): import numpy as np import pandas as pd df = pd.DataFrame idx = pd.IndexSlice import geopandas as gpd gdf = gpd.GeoDataFrame from bokeh.models import ColumnDataSource, HoverTool, Legend, LegendItem, CDSView, IndexFilter from bokeh.plotting import figure, show from bokeh.io import output_notebook # frm = frm.reset_index().pivot(index = frm.index.names[0], columns = frm.index.names[1]) frm = frm.copy() frm = frm.sort_index() # geometry = geometry.copy() from bokeh.models import Div title = f'<h1>{title}</h1>' title = Div( text = title, width = pw, ) preamble = Div( text = preamble, width = pw, ) if varNames is None: varNames = frm.columns.sort_values() varMetaName = varNames.name else: varMetaName = 'variable' varNames = list(varNames) seriesNames = frm.index.levels[1].sort_values() seriesMetaName = seriesNames.name seriesNames = list(seriesNames) dates = [str(int(round(i.to_numpy().astype(int) / 1e6))) for i in frm.index.levels[0]] frm.index = frm.index.set_levels(dates, level = 0) defaultVar = varNames[0] defaultDate = dates[-1] pivotFrm = frm.reset_index() \ .pivot(index = frm.index.names[0], columns = frm.index.names[1]) \ .sort_index() defaultVar = varNames[0] defaultDate = dates[-1] for key in varNames: if not key in varNotes: varNotes[key] = '' else: varNotes[key] = f'<i>{varNotes[key]}</i>' varNote = Div( text = varNotes[defaultVar], width = pw - 120, ) lineSources = { key: ColumnDataSource(pivotFrm[key]) for key in pivotFrm.columns.levels[0] } lineSource = ColumnDataSource(pivotFrm[defaultVar]) lineSource.name = defaultVar barSources = dict() for varName in varNames: for index, date in zip(sorted(pivotFrm.index), dates): series = pivotFrm.loc[index, varName] subFrm = df(dict( name = series.index, value = series.values, height = abs(series.values), offset = series.values / 2. )) barSources[varName + '_' + date] = ColumnDataSource(subFrm) barSource = ColumnDataSource(barSources[defaultVar + '_' + defaultDate].data) barSource.name = ', '.join([str(defaultVar), str(defaultDate)]) bounds = geometry.bounds minx = np.min(bounds['minx']) maxx = np.max(bounds['maxx']) miny = np.min(bounds['miny']) maxy = np.max(bounds['maxy']) aspect = (maxx - minx) / (maxy - miny) from shapely.geometry import Polygon import itertools corners = list(itertools.product(geometry.total_bounds[::2], geometry.total_bounds[1::2])) allPoly = Polygon([corners[0], corners[1], corners[3], corners[2]]) allPoly = allPoly.centroid.buffer(np.sqrt(allPoly.area) / 1e6) for name in frm.index.levels[1]: if not name in geometry.index: geometry[name] = allPoly geometry = geometry.simplify(np.sqrt(geometry.area).min() * 10. ** 3.5) geoFrm = frm.reset_index().pivot(index = frm.index.names[1], columns = frm.index.names[0]) geoFrm.columns = geoFrm.columns.map('_'.join).str.strip('_') geoFrm['geometry'] = geometry geoFrm = gdf(geoFrm) from bokeh.models import GeoJSONDataSource geoJSON = geoFrm.reset_index().to_json() geoSource = GeoJSONDataSource(geojson = geoJSON) mins = {n: frm[n].min() for n in varNames} maxs = {n: frm[n].max() for n in varNames} xName = frm.index.names[0] lineFig = figure( x_axis_type = 'datetime', y_range = (mins[defaultVar], maxs[defaultVar]), plot_height = int((ph - 100) * 1. / 3.), plot_width = pw, toolbar_location = 'left', tools = 'save, xpan, box_zoom, reset, xwheel_zoom', active_scroll = 'auto', # title = title, ) barFig = figure( x_range = seriesNames, plot_height = int((ph - 100) * 1. / 2.), plot_width = pw, # title = "Scores on my birthday", toolbar_location = None, tools = "" ) barFig.xgrid.grid_line_color = None barFig.xaxis.major_label_orientation = 'vertical' mapFig = figure( plot_width = pw - 20, plot_height = int(round((pw - 20) / aspect)), toolbar_location = 'right', tools = 'pan, wheel_zoom, reset', background_fill_color = "lightgrey" ) mapFig.xgrid.grid_line_color = None mapFig.ygrid.grid_line_color = None from matplotlib.pyplot import get_cmap from matplotlib.colors import rgb2hex cmap = get_cmap('nipy_spectral') cs = [rgb2hex(cmap(i / len(seriesNames), alpha = 0.5)) for i in range(len(seriesNames))] lines = [] for seriesName, colour in zip(seriesNames, cs): line = lineFig.line( xName, seriesName, source = lineSource, color = colour, alpha = 0.8, muted_color = 'gray', muted_alpha = 0.3, muted = True, line_width = 2, # legend_label = seriesName, ) from bokeh.models import HoverTool lineFig.add_tools(HoverTool( renderers = [ line, ], tooltips = [ (seriesMetaName.capitalize(), seriesName), (xName.capitalize(), f'@{xName}' + '{%Y-%m-%d}'), ('Value', f'@{{{seriesName}}}'), ], formatters = { f'@{xName}': 'datetime', seriesName: 'numeral', }, toggleable = False )) lines.append(line) bars = [] for i, (seriesName, colour) in enumerate(zip(seriesNames, cs)): view = CDSView(source = barSource, filters = [IndexFilter([i,]),]) bar = barFig.rect( source = barSource, view = view, x = 'name', y = 'offset', height = 'height', width = 0.9, color = colour, muted_color = 'gray', muted_alpha = 0.3, muted = True, ) bars.append(bar) from bokeh.palettes import Viridis256 from bokeh.models import LinearColorMapper, ColorBar palette = Viridis256 mapColourMapper = LinearColorMapper( palette = palette, low = frm.loc[idx[defaultDate, :], defaultVar].min(), high = frm.loc[idx[defaultDate, :], defaultVar].max(), ) mapColourBar = ColorBar( color_mapper = mapColourMapper, label_standoff = 8, width = 30, height = int(round(mapFig.plot_height * 0.9)), border_line_color = None, location = (0, 0), orientation = 'vertical', ) mapFig.add_layout(mapColourBar, 'left') patches = [] for i, seriesName in enumerate(seriesNames): view = CDSView(source = geoSource, filters = [IndexFilter([i,]),]) patch = mapFig.patches( 'xs', 'ys', source = geoSource, view = view, fill_color = dict( field = '_'.join([defaultVar, defaultDate]), transform = mapColourMapper, ), line_color = 'grey', line_width = 0.25, fill_alpha = 0., name = '_'.join([defaultVar, defaultDate]) ) patches.append(patch) from bokeh.models import HoverTool mapHover = HoverTool( renderers = patches, tooltips = [ (seriesMetaName.capitalize(), f'@{seriesMetaName}'), ('Value', '@$name'), ] ) mapFig.add_tools(mapHover) from bokeh.models import BoxAnnotation from bokeh.models import Label for name, zone in xZones.items(): convD = lambda x: int(round(pd.Timestamp(x).to_numpy().astype(int) / 1e6)) left, right = [None if val is None else convD(val) for val in zone] zone = BoxAnnotation( left = left, right = right, fill_alpha = 0.1, fill_color = 'gray', ) zoneLabel = Label( text = name + ' (end)' if left is None else name, text_font_size = '8pt', x = right if left is None else left, y = 10, x_units = 'data', y_units = 'screen', angle = -90 if left is None else 90, angle_units = 'deg', x_offset = -10 if left is None else 10, y_offset = 5 * (len(name) + 6) if left is None else 0 ) lineFig.add_layout(zone) lineFig.add_layout(zoneLabel) from bokeh.models import Span span = Span( location = int(defaultDate), dimension = 'height', line_color = 'red', # line_dash = 'dashed', line_width = 1 ) lineFig.add_layout(span) from bokeh.models.widgets import DateSlider slider = DateSlider( title = 'Date', start = int(dates[0]), end = int(dates[-1]), step = int(8.64 * 1e7), # days value = int(defaultDate), width = pw - 60, align = 'end' ) from bokeh.models.widgets import Select select = Select( title = "Choose data:", options = varNames, value = defaultVar, width = 100, ) from bokeh.models import CheckboxGroup checkboxes = CheckboxGroup( labels = seriesNames, active = [], ) checkboxAll = CheckboxGroup( labels = ['All',], active = [], ) from bokeh.models import CustomJS callback = CustomJS( args = dict( y_range = lineFig.y_range, lineSources = lineSources, lineSource = lineSource, barSources = barSources, barSource = barSource, bars = bars, lines = lines, patches = patches, select = select, slider = slider, span = span, checkboxes = checkboxes, varNote = varNote, varNotes = varNotes, geoSource = geoSource, mapColourMapper = mapColourMapper, mins = mins, maxs = maxs, ), code = """ lineSource.data = lineSources[select.value].data lineSource.name = select.value lineSource.change.emit() span.location = slider.value span.change.emit() y_range.setv({'start': mins[select.value], 'end': maxs[select.value]}) varNote.text = varNotes[select.value] varNote.change.emit() const barChoice = select.value + '_' + slider.value barSource.data = barSources[barChoice].data barSource.name = select.value.toString() + ', ' + slider.value.toString() barSource.change.emit() for (let i = 0; i < lines.length; i++){ let checked = checkboxes.active.includes(i) lines[i].muted = !(checked) bars[i].muted = !(checked) var alpha = checked ? 1 : 0; patches[i].glyph.fill_alpha = alpha } const newCol = select.value + '_' + slider.value for (let i = 0; i < lines.length; i++){ patches[i].glyph.fill_color['field'] = newCol patches[i].name = newCol } mapColourMapper.low = mins[select.value] mapColourMapper.high = maxs[select.value] geoSource.change.emit() """, ) allCheckCallback = CustomJS( args = dict( lines = lines, checkboxes = checkboxes, checkboxAll = checkboxAll, callback = callback ), code = """ checkboxes.active.length = 0 if (checkboxAll.active.length > 0) { let arr = [] for (let i = 0; i < lines.length; i++){ arr.push(i) } checkboxes.active.push(...arr) } checkboxes.change.emit() callback.execute() """ ) slider.js_on_change('value', callback) select.js_on_change('value', callback) checkboxes.js_on_change('active', callback) checkboxAll.js_on_change('active', allCheckCallback) from bokeh.layouts import column, row layout = column( title, preamble, row(select, varNote), row(column(lineFig, slider, barFig), column(checkboxes, checkboxAll)), mapFig ) return layout
plots[0].x_range.end=len; plots[0].y_range.end=closest; plots[0].y_range.start=0; plots[1].x_range.end=len; plots[1].y_range.end=closest; plots[1].y_range.start=1; ''') #changes x range to be (0,slider_value) #changes y range to be (0 or 1, the closest stastics value to slider) slider = DateSlider(start=min(dates), end=max(dates), value=max(dates), step=1, title='Time') slider.js_on_change('value', callback) loopchange = CustomJS(args=dict(slider=slider, endlen=max(dates), minlen=min(dates)), code=''' function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function run() { var len = slider.value; var loopcount=0; while (cb_obj.active){ if (len>=endlen){ loopcount++; if(loopcount==2){
def make_dateMap(frm, name, title, size = 600, nonVisKeys = {}): minx = np.min(frm.bounds['minx']) maxx = np.max(frm.bounds['maxx']) miny = np.min(frm.bounds['miny']) maxy = np.max(frm.bounds['maxy']) aspect = (maxx - minx) / (maxy - miny) ts = sorted(set([n.split('_')[-1] for n in frm.columns])) ts = [n for n in ts if n.isnumeric()] assert len(ts) ns = sorted(set([n.split('_')[0] for n in frm.columns])) ns = [n for n in ns if not n in [*nonVisKeys, 'geometry']] assert len(ns) defaultCol = '_'.join([ns[0], ts[-1]]) indexName = frm.index.name mins = {n: frm[['_'.join([n, t]) for t in ts]].min().min() for n in ns} maxs = {n: frm[['_'.join([n, t]) for t in ts]].max().max() for n in ns} from bokeh.models import GeoJSONDataSource geoJSON = frm.reset_index().to_json() source = GeoJSONDataSource(geojson = geoJSON) from bokeh.io import output_file outFilename = name + '.html' outPath = os.path.join(dataDir, outFilename) if os.path.isfile(outPath): os.remove(outPath) output_file(outPath) from bokeh.plotting import figure fig = figure( title = title, plot_height = size, plot_width = int(round(size * aspect)) + 50, toolbar_location = 'right', tools = 'pan, zoom_in, zoom_out, wheel_zoom, reset', background_fill_color = "lightgrey" ) fig.xgrid.grid_line_color = None fig.ygrid.grid_line_color = None from bokeh.palettes import Viridis256 from bokeh.models import LinearColorMapper, ColorBar palette = Viridis256 colourMapper = LinearColorMapper( palette = palette, low = mins[ns[0]], high = maxs[ns[0]], ) colourBar = ColorBar( color_mapper = colourMapper, label_standoff = 8, width = 30, height = int(round(fig.plot_height * 0.9)), border_line_color = None, location = (0, 0), orientation = 'vertical', ) fig.add_layout(colourBar, 'left') patches = fig.patches( 'xs', 'ys', source = source, fill_color = dict( field = defaultCol, transform = colourMapper, ), line_color = 'grey', line_width = 0.25, fill_alpha = 1, name = defaultCol ) from bokeh.models.widgets import DateSlider as Slider slider = Slider( title = 'Date', start = int(ts[0]), end = int(ts[-1]), step = int(8.64 * 1e7), # days value = int(ts[-1]), width = fig.plot_width - 70 ) from bokeh.models.widgets import Select select = Select( title = "Dataset", options = ns, value = defaultCol.split('_')[0], width = 60 ) from bokeh.models import CustomJS callback = CustomJS( args = dict( patches = patches, source = source, slider = slider, # key = 'stay', # <--- TESTING select = select, colourMapper = colourMapper, mins = mins, maxs = maxs, ), code = """ const newCol = select.value + '_' + slider.value patches.glyph.fill_color['field'] = newCol patches.name = newCol colourMapper.low = mins[select.value] colourMapper.high = maxs[select.value] source.change.emit() """, ) from bokeh.models import HoverTool tooltips = [ ('Index', '@' + indexName), ('Value', '@$name') ] tooltips.extend([(k.capitalize(), '@' + k) for k in nonVisKeys]) hover = HoverTool( renderers = [patches], tooltips = tooltips ) fig.add_tools(hover) slider.js_on_change('value', callback) select.js_on_change('value', callback) from bokeh.layouts import column, row layout = column(fig, row(select, slider)) from bokeh.io import show show(layout)