def test_js_on_change_executes(self, bokeh_model_page) -> None: slider = DateSlider(start=start, end=end, value=value, css_classes=["foo"], width=300) slider.js_on_change('value', CustomJS(code=RECORD("value", "cb_obj.value"))) page = bokeh_model_page(slider) drag_slider(page.driver, ".foo", 150) results = page.results assert datetime.fromtimestamp(results['value']/1000) > datetime(*date.fromisoformat("2017-08-04").timetuple()[:3]) assert page.has_no_console_errors()
def create_html_page(html_page_name, df): del states["HI"] del states["AK"] EXCLUDED = ("ak", "hi", "pr", "gu", "vi", "mp", "as") state_xs = [states[code]["lons"] for code in states] state_ys = [states[code]["lats"] for code in states] county_xs = [ counties[code]["lons"] for code in counties if counties[code]["state"] not in EXCLUDED ] county_ys = [ counties[code]["lats"] for code in counties if counties[code]["state"] not in EXCLUDED ] county_names = [ counties[code]["name"] for code in counties if counties[code]["state"] not in EXCLUDED ] col_names = list(df.columns.values) len_col = len(col_names) last_day_str = col_names[len_col - 1] last_day = datetime.strptime(last_day_str, '%Y-%m-%d %H:%M:%S').date() first_day_str = col_names[4] # first 4 colums contain names etc first_day = datetime.strptime(first_day_str, '%Y-%m-%d %H:%M:%S').date() a_dict = create_dict_forJS(df) source_new = ColumnDataSource(data=a_dict) source_visible = ColumnDataSource( data={ 'x': county_xs, 'y': county_ys, 'name': county_names, 'rate': a_dict[str(last_day)] }) ''' c_dict = create_colors_forJS(df) hex_color = rgb_to_hex((255, 255, 255)) source_visible = ColumnDataSource(data={'x': county_xs, 'y': county_ys, 'name': county_names, 'rate': a_dict[str(last_day)], 'color': [hex_color]}) ''' # Define a sequential multi-hue color palette. palette = brewer['YlGnBu'][6] palette = palette[::-1] # TODO: get the high value from median? color_mapper = LinearColorMapper(palette=palette, low=0, high=100) # TODO: add agenda? TOOLS = "pan,zoom_in,zoom_out,wheel_zoom,box_zoom,reset,hover,save" p = figure(title="US density", toolbar_location="right", output_backend="webgl", plot_width=1100, plot_height=700, tools=TOOLS, tooltips=[("County", "@name"), ("Confirmed cases", "@rate"), ("(Long, Lat)", "($x, $y)")]) # hide grid and axes p.axis.visible = None p.xgrid.grid_line_color = None p.ygrid.grid_line_color = None p.hover.point_policy = "follow_mouse" # p.patches(county_xs, county_ys, # fill_alpha=0.7, # line_color="white", line_width=0.5) p.patches(state_xs, state_ys, fill_alpha=0.7, line_color="white", line_width=2, line_alpha=0.3) p.patches( 'x', 'y', source=source_visible, fill_color={ 'field': 'rate', 'transform': color_mapper }, #fill_color='color', fill_alpha=0.7, line_color="white", line_width=0.2) date_slider = DateSlider(title="Date:", start=first_day, end=last_day, value=last_day, step=1) callback = CustomJS(args=dict(source=source_new, ts=source_visible), code=""" var data=ts.data; var data1=source.data; var f=cb_obj.value; //this is the selection value of slider var event = new Date(f); var date_selected = event.toISOString().substring(0,10); // converting date from python to JS //ts.data['color'] = '#ffffff' //color_dict.data[date_selected]; data['rate']=data1[date_selected]; ts.change.emit(); """) date_slider.js_on_change('value', callback) layout = row(column(date_slider), p) output_file(html_page_name, title="Interactive USA density map") show(layout)
def create_html_page(html_page_name, df): del states["HI"] del states["AK"] EXCLUDED = ("ak", "hi", "pr", "gu", "vi", "mp", "as") state_xs = [states[code]["lons"] for code in states] state_ys = [states[code]["lats"] for code in states] county_xs = [ counties[code]["lons"] for code in counties if counties[code]["state"] not in EXCLUDED ] county_ys = [ counties[code]["lats"] for code in counties if counties[code]["state"] not in EXCLUDED ] county_names = [ counties[code]["name"] for code in counties if counties[code]["state"] not in EXCLUDED ] county_rates = [] col_names = list(df.columns.values) len_col = len(col_names) for county_id in counties: if counties[county_id]["state"] in EXCLUDED: continue fips = str(county_id[0]).zfill(2) + str(county_id[1]).zfill(3) # len_col - 1 = its the last day that we have rate = df.loc[df['FIPS'] == fips][col_names[len_col - 1]].values if len(rate) == 0: county_rates.append(0.0) else: county_rates.append(rate[0]) source_visible = ColumnDataSource(data={ 'x': county_xs, 'y': county_ys, 'name': county_names, 'rate': county_rates }) source_new = create_dict_forJS(df) print(np.shape(source_new), type(source_new)) # Define a sequential multi-hue color palette. #palette = brewer['YlGnBu'][9] palette = Magma256 # palette = big_palette(200, bokeh.palettes.plasma) palette = palette[::-1] color_mapper = LinearColorMapper(palette=palette, low=0, high=100) #color_mapper = LogColorMapper(palette=palette) TOOLS = "pan,wheel_zoom,reset,hover,save" p = figure(title="US density", toolbar_location="left", plot_width=1100, plot_height=700, tools=TOOLS, tooltips=[("County", "@name"), ("Infection rate", "@rate"), ("(Long, Lat)", "($x, $y)")]) # hide grid and axes p.axis.visible = None p.xgrid.grid_line_color = None p.ygrid.grid_line_color = None p.hover.point_policy = "follow_mouse" p.patches( county_xs, county_ys, # fill_alpha=0.7, line_color="white", line_width=0.5) p.patches(state_xs, state_ys, fill_alpha=0.0, line_color="#884444", line_width=2, line_alpha=0.3) p.patches('x', 'y', source=source_visible, fill_color={ 'field': 'rate', 'transform': color_mapper }, fill_alpha=0.7, line_color="white", line_width=0.5) # Make a slider object: slider last_day_str = col_names[len_col - 1] last_day = datetime.strptime( last_day_str, '%Y-%m-%d %H:%M:%S').date() # + timedelta(days=1) first_day_str = col_names[4] # first 4 colums contain names etc first_day = datetime.strptime(first_day_str, '%Y-%m-%d %H:%M:%S').date() print(last_day) date_slider = DateSlider(title="Date:", start=first_day, end=last_day, value=last_day, step=1) callback = CustomJS(args=dict(source=source_new, ts=source_visible), code=""" var data=ts.data; var rate=data['rate']; var name=data['name']; var data1=source.data; var f=cb_obj.value; //this is the selection value of slider const event = new Date(f); var date_selected = event.toISOString().substring(0,10); // converting date from python to JS //console.log(typeof data1[0]); data['rate']=data1[date_selected]; ts.change.emit(); """) date_slider.js_on_change('value', callback) #layout = column(p, widgetbox(slider),widgetbox(date_slider)) layout = column(p, date_slider) output_file(html_page_name, title="Interactive USA density map") show(layout)
def bokehTile(tileFile, jsonFile, TT=[0, 0, 0], DD=[2019, 10, 1], dynamic=False, plotTitle=''): citls, h = fitsio.read(tileFile, header=True) w = (np.where(citls['IN_DESI'] == 1)[0]) inci = citls[w] if jsonFile is not None: with open(jsonFile, "r") as read_file: data = json.load(read_file) ## Coloring scheme palette = ['green', 'red', 'white'] dye = [] for tile in citls['TILEID']: rang = 2 # 'orange' if jsonFile is not None: if str(tile) in data: rang = 0 # 'green' #green default if len(data[str(tile)] ['unassigned']) > 0: # not assigned (red) rang = 1 # 'red' #'red' if (0 in data[str(tile)]['gfa_stars_percam']): print(data[str(tile)]['gfa_stars_percam']) rang = 1 # 'cyan' else: rang = 0 # green if qa.json is not provided dye.append(rang) dye = np.asarray(dye) w = (np.where(dye < 2)[0]) citls = citls[w] dye = dye[w] mapper = linear_cmap(field_name='DYE', palette=palette, low=0, high=2) ######################################################### TOOLS = [ 'pan', 'tap', 'wheel_zoom', 'box_zoom', 'reset', 'save', 'box_select' ] obsTime = dt(DD[0], DD[1], DD[2], TT[0], TT[1], TT[2]) # print(get_kp_twilights(TT,DD)) if plotTitle == '' or plotTitle is None: PTITLE = '' else: PTITLE = 'Program: ' + plotTitle p = figure(tools=TOOLS, toolbar_location="right", plot_width=800, plot_height=450, title=PTITLE, active_drag='box_select') # str(DD[1])+" - 2019") p.title.text_font_size = '16pt' p.title.text_color = 'black' p.grid.grid_line_color = "gainsboro" ############################### adding ecliptic plane+ hour grid ####################3 add_plane(p, color='red', plane='ecliptic', projection='equatorial') tiledata = dict( RA=citls['RA'], DEC=citls['DEC'], TILEID=citls['TILEID'], BRIGHTRA=citls['BRIGHTRA'][:, 0], BRIGHTDEC=citls['BRIGHTDEC'][:, 0], BRIGHTVTMAG=citls['BRIGHTVTMAG'][:, 0], EBV_MED=np.round(citls['EBV_MED'], 3), STAR_DENSITY=citls['STAR_DENSITY'], DYE=dye, program=citls['PROGRAM'], selected=np.ones(len(citls), dtype=bool), ) for colname in ['STAR_DENSITY', 'EBV_MED']: if colname in citls.dtype.names: tiledata[colname] = citls[colname] tiles = ColumnDataSource(data=tiledata) colformat = bktables.NumberFormatter(format='0,0.00') columns = [ bktables.TableColumn(field='TILEID', title='TILEID', width=80), bktables.TableColumn(field='RA', title='RA', formatter=colformat), bktables.TableColumn(field='DEC', title='DEC', formatter=colformat), ] for colname in ['STAR_DENSITY', 'EBV_MED']: if colname in tiledata: columns.append( bktables.TableColumn(field=colname, title=colname, formatter=colformat)) columns.append(bktables.TableColumn(field='selected', title='Selected')) tiletable = bktables.DataTable(columns=columns, source=tiles, width=800) tiles.selected.js_on_change( 'indices', CustomJS(args=dict(s1=tiles), code=""" var inds = cb_obj.indices; var d1 = s1.data; for (var i=0; i<d1['selected'].length; i++) { d1['selected'][i] = false; } for (var i = 0; i < inds.length; i++) { d1['selected'][inds[i]] = true; } s1.change.emit(); """)) render = p.circle( 'RA', 'DEC', source=tiles, size=9, line_color='chocolate', color=mapper, alpha=0.4, hover_color='orange', hover_alpha=1, hover_line_color='red', # set visual properties for selected glyphs selection_fill_color='orange', selection_line_color='white', # set visual properties for non-selected glyphs nonselection_fill_alpha=0.4, nonselection_fill_color=mapper) p.xaxis.axis_label = 'RA [deg]' p.yaxis.axis_label = 'Dec. [deg]' p.xaxis.axis_label_text_font_size = "14pt" p.yaxis.axis_label_text_font_size = "14pt" p.grid.grid_line_color = "gainsboro" p.yaxis.major_label_text_font_size = "12pt" p.xaxis.major_label_text_font_size = "12pt" p.x_range = Range1d(360, 0) p.y_range = Range1d(-40, 95) p.toolbar.logo = None p.toolbar_location = None # mytext = Label(x=180, y=-35, text="S", text_color='gray', text_font_size='12pt') ; p.add_layout(mytext) # mytext = Label(x=180, y=88, text="N", text_color='gray', text_font_size='12pt') ; p.add_layout(mytext) # mytext = Label(x=350, y=45, text="E", text_color='gray', text_font_size='12pt', angle=np.pi/2) ; p.add_layout(mytext) # mytext = Label(x=4, y=45, text="W", text_color='gray', text_font_size='12pt', angle=np.pi/2) ; p.add_layout(mytext) ## Javascript code to open up custom html pages, once user click on a tile code = """ var index_selected = source.selected['1d']['indices'][0]; var tileID = source.data['TILEID'][index_selected]; if (tileID!==undefined) { var win = window.open("http://www.astro.utah.edu/~u6022465/cmx/ALL_SKY/dr8/allSKY_ci_tiles/sub_pages/tile-"+tileID+".html", " "); try {win.focus();} catch (e){} } """ taptool = p.select(type=TapTool) taptool.callback = CustomJS(args=dict(source=tiles), code=code) ## The html code for the hover window that contain tile infrormation ttp = """ <div> <div> <span style="font-size: 14px; color: blue;">Tile ID:</span> <span style="font-size: 14px; font-weight: bold;">@TILEID{int}</span> </div> <div> <span style="font-size: 14px; color: blue;">RA:</span> <span style="font-size: 14px; font-weight: bold;">@RA</span> </div> <div> <span style="font-size: 14px; color: blue;">Dec:</span> <span style="font-size: 14px; font-weight: bold;">@DEC</span> </div> <div> <span style="font-size: 14px; color: blue;">EBV_MED:</span> <span style="font-size: 14px; font-weight: bold;">@EBV_MED{0.000}</span> </div> <div> <span style="font-size: 14px; color: blue;">STAR_DENSITY:</span> <span style="font-size: 14px; font-weight: bold;">@STAR_DENSITY{0}</span> </div> <div> <span style="font-size: 14px; color: blue;">BRIGHTEST_STAR_VTMAG:</span> <span style="font-size: 14px; font-weight: bold;">@BRIGHTVTMAG</span> </div> <div> <span style="font-size: 14px; color: blue;">BRIGHTEST_STAR_LOC:</span> <span style="font-size: 14px; font-weight: bold;">(@BRIGHTRA, @BRIGHTDEC)</span> </div> </div> """ hover = HoverTool(tooltips=ttp, renderers=[render]) hover.point_policy = 'snap_to_data' hover.line_policy = 'nearest' # hover.mode='vline' p.add_tools(hover) cross = CrosshairTool() # cross.dimensions='height' cross.line_alpha = 0.3 cross.line_color = 'gray' p.add_tools(cross) # Setting the second y axis range name and range p.extra_y_ranges = {"foo": p.y_range} p.extra_x_ranges = {"joo": p.x_range} # Adding the second axis to the plot. p.add_layout(LinearAxis(y_range_name="foo"), 'right') p.add_layout(LinearAxis(x_range_name="joo"), 'above') p.xaxis.major_label_text_font_size = "12pt" p.yaxis.major_label_text_font_size = "12pt" if dynamic: # twilight_source = get_kp_twilights(TT,DD) # evening and morning twilights at every TT and DD circleSource_1 = skyCircle(TT, DD, 1.5) p.circle('RA', 'DEC', source=circleSource_1, size=1.5, color='black') circleSource_2 = skyCircle(TT, DD, 2.0) p.circle('RA', 'DEC', source=circleSource_2, size=0.5, color='gray') else: circleSource = skyCircle(TT, DD, 1.5) p.circle('RA', 'DEC', source=circleSource, size=1.5, color=None) ### Dealing with the Moon and Jupiter inFile = 'moonLoc_jupLoc_fracPhase.csv' # 'moon_loc_jup_loc_fracPhase_namePhase.csv' tbl_moon_jup = np.genfromtxt(inFile, delimiter=',', filling_values=-1, names=True, dtype=None) # , dtype=np.float) loc = EarthLocation.of_site('Kitt Peak') kp_lat = 31, 57, 48 kp_lon = -111, 36, 00 mooninfo_obj = pylunar.MoonInfo((kp_lat), (kp_lon)) m_ra, m_dec, frac_phase, name_phase = moonLoc(TT, DD, loc, mooninfo_obj) j_ra, j_dec = jupLoc(TT, DD, loc) #moonSource = ColumnDataSource({"moon_RAS": tbl_moon_jup['moon_ra'], "moon_DECS": tbl_moon_jup['moon_dec'], # "Phase_frac": tbl_moon_jup['moon_phase_frac']}) moonSource = ColumnDataSource({ "moon_RAS": tbl_moon_jup['moon_ra'], "moon_DECS": tbl_moon_jup['moon_dec'], "Phase_frac": np.round(100 * tbl_moon_jup['moon_phase_frac']) }) ####moon_RADEC = ColumnDataSource({"moon_ra": [m_ra.deg], "moon_dec": [m_dec.deg], "phase_frac": [frac_phase]}) moon_RADEC_ = ColumnDataSource({ "moon_ra": [m_ra.deg - 360], "moon_dec": [m_dec.deg], "phase_frac": [frac_phase] }) moon_RADEC = ColumnDataSource({ "moon_ra": [m_ra.deg], "moon_dec": [m_dec.deg], "phase_frac": [frac_phase] }) render_moon = p.circle('moon_ra', 'moon_dec', source=moon_RADEC, size=170, color='cyan', alpha=0.2) render_moon = p.circle('moon_ra', 'moon_dec', source=moon_RADEC, size=4, color='blue') render_moon = p.circle('moon_ra', 'moon_dec', source=moon_RADEC_, size=170, color='cyan', alpha=0.2) render_moon = p.circle('moon_ra', 'moon_dec', source=moon_RADEC_, size=4, color='blue') jupSource = ColumnDataSource({ "jup_RAS": tbl_moon_jup['jup_ra'], "jup_DECS": tbl_moon_jup['jup_dec'] }) jup_RADEC = ColumnDataSource({ "jup_ra": [j_ra.deg], "jup_dec": [j_dec.deg] }) twilight = get_kp_twilights( TT, DD) # evening and morning twilights at every TT and DD twilight_source = ColumnDataSource({ "eve_twilight": [twilight[0]], "mor_twilight": [twilight[1]] }) render_jup = p.circle('jup_ra', 'jup_dec', source=jup_RADEC, size=5, color='blue') render_jup = p.circle('jup_ra', 'jup_dec', source=jup_RADEC, size=4, color='gold') from bokeh.models.glyphs import Text TXTsrc = ColumnDataSource( dict(x=[350], y=[85], text=['Moon Phase: ' + "%.0f" % (frac_phase * 100) + "%"])) glyph = Text(x="x", y="y", text="text", angle=0, text_color="black") p.add_glyph(TXTsrc, glyph) TXTsrc_moon = ColumnDataSource( dict(x=[m_ra.deg + 10], y=[m_dec.deg - 10], text=['Moon'])) glyph = Text(x="x", y="y", text="text", angle=0, text_color="blue", text_alpha=0.3, text_font_size='10pt') p.add_glyph(TXTsrc_moon, glyph) TXTsrc_jup = ColumnDataSource( dict(x=[j_ra.deg + 5], y=[j_dec.deg - 8], text=['Jup.'])) glyph = Text(x="x", y="y", text="text", angle=0, text_color="black", text_alpha=0.3, text_font_size='10pt') p.add_glyph(TXTsrc_jup, glyph) callback = CustomJS(args=dict(source_sky1=circleSource_1, source_sky2=circleSource_2, source_moon=moonSource, source_moon_RADEC=moon_RADEC, source_moon_RADEC_=moon_RADEC_, source_jup=jupSource, source_jup_RADEC=jup_RADEC, sourceTXT=TXTsrc, sourceTXTmoon=TXTsrc_moon, sourceTXTjup=TXTsrc_jup), code=""" // First set times as if they were UTC var t = new Date(time_slider.value); var d = new Date(date_slider.value); var data1 = source_sky1.data; var ra_1 = data1['RA']; var ra0_1 = data1['RA0']; var data2 = source_sky2.data; var ra_2 = data2['RA']; var ra0_2 = data2['RA0']; var data_moon = source_moon.data; var ras_moon = data_moon['moon_RAS']; var decs_moon = data_moon['moon_DECS']; var phase_frac = data_moon['Phase_frac']; var moonRADEC = source_moon_RADEC.data; var moon_ra = moonRADEC['moon_ra']; var moon_dec = moonRADEC['moon_dec']; var moonRADEC_ = source_moon_RADEC_.data; var moon_ra_ = moonRADEC_['moon_ra']; var moon_dec_ = moonRADEC_['moon_dec']; var data_jup = source_jup.data; var ras_jup = data_jup['jup_RAS']; var decs_jup = data_jup['jup_DECS']; var jupRADEC = source_jup_RADEC.data; var jup_ra = jupRADEC['jup_ra']; var jup_dec = jupRADEC['jup_dec']; var Hour = t.getUTCHours(); var Day = d.getDate(); var Month = d.getMonth(); var Year = new Array(31,28,31,30,31,30,31,31,30,31,30,31); var all_FULdays = 0; for (var i = 0; i < Month; i++) all_FULdays=all_FULdays+Year[i]; all_FULdays = all_FULdays + (Day-1); if (Hour<12) all_FULdays=all_FULdays+1; var all_minutes = all_FULdays*24+Hour; if (all_minutes<8800) { moon_ra[0] = ras_moon[all_minutes]; moon_dec[0] = decs_moon[all_minutes]; moon_ra_[0] = ras_moon[all_minutes]-360.; moon_dec_[0] = decs_moon[all_minutes]; } var jupTXTdata = sourceTXTjup.data; var x_jup = jupTXTdata['x']; var y_jup = jupTXTdata['y']; var text_jup = jupTXTdata['text']; if (all_minutes<8800) { jup_ra[0] = ras_jup[all_minutes]; jup_dec[0] = decs_jup[all_minutes]; x_jup[0] = jup_ra[0]+5; y_jup[0] = jup_dec[0]-8; } if (t.getUTCHours() < 12) { d.setTime(date_slider.value + 24*3600*1000); } else { d.setTime(date_slider.value); } d.setUTCHours(t.getUTCHours()); d.setUTCMinutes(t.getUTCMinutes()); d.setUTCSeconds(0); // Correct to KPNO local time // d object still thinks in UTC, which is 7 hours ahead of KPNO d.setTime(d.getTime() + 7*3600*1000); // noon UT on 2000-01-01 var reftime = new Date(); reftime.setUTCFullYear(2000); reftime.setUTCMonth(0); // Months are 0-11 (!) reftime.setUTCDate(1); // Days are 1-31 (!) reftime.setUTCHours(12); reftime.setUTCMinutes(0); reftime.setUTCSeconds(0); // time difference in days (starting from milliseconds) var dt = (d.getTime() - reftime.getTime()) / (24*3600*1000); // Convert to LST var mayall_longitude_degrees = -(111 + 35/60. + 59.6/3600); var LST_hours = ((18.697374558 + 24.06570982441908 * dt) + mayall_longitude_degrees/15) % 24; var LST_degrees = LST_hours * 15; for (var i = 0; i < ra_1.length; i++) { ra_1[i] = (ra0_1[i] + LST_degrees) % 360; } for (var i = 0; i < ra_2.length; i++) { ra_2[i] = (ra0_2[i] + LST_degrees) % 360; } //// Here we gtake care of the moon phasde text var TXTdata = sourceTXT.data; var x = TXTdata['x']; var y = TXTdata['y']; var text = TXTdata['text']; var moonTXTdata = sourceTXTmoon.data; var x_moon = moonTXTdata['x']; var y_moon = moonTXTdata['y']; var text_moon = moonTXTdata['text']; // x[0] = 1; // y[0] = 40; if (all_minutes<8800) { text[0] = 'Moon Phase: ' + phase_frac[all_minutes]+'%'; x_moon[0] = moon_ra[0]+10; y_moon[0] = moon_dec[0]-10; } sourceTXT.change.emit(); /////////////////////////////// Moon phase code ends. source_sky1.change.emit(); source_sky2.change.emit(); //source_moon_RADEC.change.emit(); //source_moon_RADEC_.change.emit(); //source_jup_RADEC.change.emit(); sourceTXTmoon.change.emit(); sourceTXTjup.change.emit(); //alert(d); """) if dynamic: ### TIME Timeslider = DateSlider(start=dt(2019, 9, 1, 16, 0, 0), end=dt(2019, 9, 2, 8, 0, 0), value=dt(2019, 9, 1, 16, 0, 0), step=1, title="KPNO local time(hh:mm)", format="%H:%M", width=800) ## DATE Dateslider = DateSlider(start=dt(2019, 9, 1, 16, 0, 0), end=dt(2020, 8, 31, 8, 0, 0), value=dt(2019, 10, 1, 16, 0, 0), step=1, title="Date of sunset(4pm-8am)", format="%B:%d", width=800) callback.args['time_slider'] = Timeslider callback.args['date_slider'] = Dateslider Dateslider.js_on_change('value', callback) Timeslider.js_on_change('value', callback) layout = column(p, Dateslider, Timeslider, tiletable) # show(p) return layout return p
def direction_plot(dataframe: object, ti: str) -> object: """ :param dataframe: pandas dataframe :param ti: string defining 'week', 'month'... :return: """ # use data in percent => transform db_data to percent df = (dataframe.transpose().iloc[2:, 0:] / dataframe['sum']) * 100 df.columns = dataframe['tstamp'] db_datadictstr = { str(int(time.mktime(item.timetuple()) * 1000)): list(df[item]) for item in df.columns } maxlist = df.max(1) hist = df.mean(1) # maxhist = sorted(maxlist)[-3] maxhist = mean( maxlist ) * 0.8 # don't use real max of dataset, too many discordant values sumhist = sum(hist) start = [-radians((i * 10) - 85) for i in list(range(0, 36))] end = [-radians((i * 10) - 75) for i in list(range(0, 36))] pdstart = [-radians((i * 10) - 95) for i in list(range(0, 36))] pdend = start # pdend = [-radians((i * 10) - 85) for i in list(range(0, 36))] labeltext = ("Selection of " + ti + "ly histograms") titletext = (ti + 'ly median and sum of all histograms').capitalize() # need two different sources for callback in Bokeh pdsource = ColumnDataSource( data=dict(radius=hist, start=pdstart, end=pdend)) jssource = ColumnDataSource(data=db_datadictstr) mainplot = figure(title=titletext, plot_width=400, plot_height=400, x_axis_type=None, y_axis_type=None, tools="save", min_border=0, outline_line_color=None) mainplot.title.text_font_size = "14pt" # simple rose plot mainplot.wedge(radius=hist, start_angle=start, end_angle=end, x=0, y=0, direction='clock', line_color='blue', fill_color='lightblue', alpha=0.5, legend_label='Whole dataset') # plot connected to slider mainplot.wedge(radius='radius', start_angle='start', end_angle='end', source=pdsource, x=0, y=0, alpha=0.5, direction='clock', line_color='darkred', fill_color='lightsalmon', legend_label=labeltext) # create slider day = 1000 * 3600 * 24 stepsize = day if ti == 'week': stepsize = day elif ti == 'month': stepsize = 7 * day elif ti == 'year': stepsize = 30 * day slider = DateSlider(start=min(df.columns), end=max(df.columns), value=min(df.columns), step=stepsize, title="date within histogram") callback = CustomJS(args=dict(source=pdsource, data=jssource, slid=slider), code=""" const S = slid.value; let radius = source.data.radius; const radii = Object.keys(data.data) let slidestop = radii.reduce(function(prev, curr) { (Math.abs(curr - S) < Math.abs(prev - S) ? curr : prev) return (Math.abs(curr - S) < Math.abs(prev - S) ? curr : prev); }); source.data.radius = data.data[slidestop] source.change.emit(); """) slider.js_on_change('value', callback) # create range slider rslider = DateRangeSlider(start=min(df.columns), end=max(df.columns), value=(min(df.columns), max(df.columns)), step=stepsize, title="Data within date range from ") rcallback = CustomJS(args=dict(source=pdsource, data=jssource, rslid=rslider), code=""" const smin = rslid.value[0] const smax = rslid.value[1] let radius = source.data.radius; const radii = Object.keys(data.data) let lstop = radii.reduce(function(prev, curr) { return (Math.abs(curr - smin) < Math.abs(prev - smin) ? curr : prev); }); let rstop = radii.reduceRight(function(prev, curr) { return (Math.abs(curr - smax) < Math.abs(prev - smax) ? curr : prev); }); let keylist = []; for (let k in data.data) keylist.push(k); let fromkey = keylist.indexOf(lstop); let tokey = keylist.indexOf(rstop); let rangekeys = keylist.slice(fromkey, tokey) var dataavg = Array(36).fill(0) var count = 0; for (let k of rangekeys) { dataavg = dataavg.map(function (num, idx) {return num + data.data[k][idx];}); count += 1 } dataavg = dataavg.map(function (num, idx) {return num/count;}); source.data.radius = dataavg; source.change.emit(); """) rslider.js_on_change('value', rcallback) # create grid rund_perc = ceil(maxhist / sumhist * 100) labels = list(range(0, rund_perc, 2)) labels.append(rund_perc) rad_pos = [i * sumhist / 100 for i in labels] out_rim = rad_pos[-1] label_pos = [sqrt(((i - 1)**2) / 2) for i in rad_pos] mainplot.text(label_pos[1:], label_pos[1:], [str(r) + ' %' for r in labels[1:]], text_font_size="10pt", text_align="left", text_baseline="top") for rad in rad_pos: mainplot.circle(x=0, y=0, radius=rad, fill_color=None, line_color='grey', line_width=0.5, line_alpha=0.8) diagonal = sqrt((out_rim**2) / 2) mainplot.multi_line(xs=[[diagonal, -diagonal], [-diagonal, diagonal], [-out_rim, out_rim], [0, 0]], ys=[[diagonal, -diagonal], [diagonal, -diagonal], [0, 0], [-out_rim, out_rim]], line_color="grey", line_width=0.5, line_alpha=0.8) mainplot.x_range = Range1d(-out_rim * 1.1, out_rim * 1.1) mainplot.y_range = Range1d(-out_rim * 1.1, out_rim * 1.1) mainplot.legend.location = "top_left" mainplot.legend.click_policy = "hide" # plot bars for the number of values in each group as secondary 'by' plot mapper = linear_cmap(field_name='count', palette=Oranges9, low=0, high=max(dataframe['sum'])) bin_width = df.columns[0] - df.columns[1] source = ColumnDataSource({ 'date': dataframe['tstamp'], 'count': dataframe['sum'] }) distriplot = distribution_plot(source, mapper, bin_width, 'Number of values per cluster', 400) script, div = components(column(distriplot, mainplot, slider, rslider), wrap_script=False) return {'div': div, 'script': script}
def bkapp_page(): ###-----------------------------------------------------------------------### ###------------------------PREPARING DATA---------------------------------### ### This section contains getting and preparing data for this plot------ ### ###-----------------------------------------------------------------------### ### Load dataset rat interventions bait_interventions_to_save = pd.read_pickle('development/bait_interventions_to_save_pitch_night.pickle') ### Load indices of rows for each month for bait dataset indices_bait_df = pd.read_pickle('development/indices_bait_df_pitch_night.pickle') #has 2 columns: 'from_index' and 'to_index' ### Load dataset rat sighting dataset_sightings_locations_to_save = pd.read_pickle('development/dataset_sightings_locations_to_save_pitch_night.pickle') ### Load indices of rows for each month for sightings dataset indices_sight_df = pd.read_pickle('development/indices_sight_df_pitch_night.pickle') #has 2 columns: 'from_index' and 'to_index' ### Load dataset by zipcodes by_zipcodes_df = pd.read_pickle('development/dataset_by_zipcodes_pitch_night.pickle') ### PREDICTIONS datasets df = pd.read_pickle('development/locations_prediction_df_pitch_night.pickle') prophet_fit_all_nyc = pd.read_pickle('development/prophet_fit_all_nyc_pitch_night.pickle') prophet_prediction_all_nyc = pd.read_pickle('development/prophet_prediction_all_nyc_pitch_night.pickle') ### Read the nyc map data nyc_df = pd.read_pickle('development/nyc_converted_to_save.pickle') nyc_by_zips = pd.read_pickle('development/dataset_map_by_zipcodes_pitch_night.pickle') zip_hm = pd.read_pickle('development/zip_heatmaps_pitch_night.pickle') ### Read existing year-month strings in dataset with open('development/timepoints_pitch_night.pickle', "rb") as f: timepoints = pickle.load(f) timepoints = timepoints[:-1] ### list of zipcodes in dataset with open('development/all_zips.pickle', "rb") as f: all_zips = pickle.load(f) ### Read predicted months with open('development/predicted_months_pitch_night.pickle', "rb") as f: predicted_months = pickle.load(f) ### prepare data for bokeh bait_source = ColumnDataSource(bait_interventions_to_save) indices_bait_source = ColumnDataSource(indices_bait_df) sight_source = ColumnDataSource(dataset_sightings_locations_to_save) indices_sight_source = ColumnDataSource(indices_sight_df) nyc_source = ColumnDataSource(nyc_df) timepoints_cds = ColumnDataSource(pd.DataFrame({'timepoints':timepoints})) predicted_months_cds = ColumnDataSource(pd.DataFrame({'predicted_months':predicted_months})) by_zipcodes_source = ColumnDataSource(by_zipcodes_df) nyc_by_zips_source = ColumnDataSource(nyc_by_zips) zip_hm_first = ColumnDataSource(zip_hm.loc[:,['ZIPCODE','x','y','sightings']]) zip_hm_original = ColumnDataSource(zip_hm) ### bokeh data source for initial plot rendered: first_source_bait = ColumnDataSource(bait_interventions_to_save.iloc[indices_bait_df['from_index'][51]:indices_bait_df['to_index'][51],:]) first_source_sight = ColumnDataSource(dataset_sightings_locations_to_save.iloc[indices_sight_df['from_index'][51]:indices_sight_df['to_index'][51],:]) ###-----------------------------------------------------------------------### ###----------------------GRAPHICAL USER INTERFACE-------------------------### ### This code defines the Bokeh controls that are used for the user ### ### interface. ---------------------------------------------------------- ### ###-----------------------------------------------------------------------### ### Initialize plot figure p = figure(x_range=(-74.2, -73.7), y_range=(40.53, 40.915), tools= 'box_zoom,pan,save,reset', active_drag="box_zoom", min_border_right = 40, min_border_top = 5, min_border_bottom = 5, border_fill_color = "black", background_fill_color = "black", toolbar_location="left") p.xgrid.grid_line_color = None p.ygrid.grid_line_color = None p.axis.visible = False p.outline_line_color = "black" ###-----------------------------------------------------------------------### ###------------------------PREDICTED locations----------------------------### ###-------------------------heatmap---------------------------------------### ###-----------------------------------------------------------------------### colors = ['#000000'] + brewer["Reds"][9] binsize = 0.5/80 hm_source = ColumnDataSource(pd.read_pickle('development/df_mock_pitch_practice.pickle')) # hm_source = ColumnDataSource(df) hm_source_original = ColumnDataSource(df) ## not nomalized count mapper = LinearColorMapper(palette=colors, low=df.rat_count.min(), high=df.rat_count.max()) prediction_location = p.rect(x="level_0", y="level_1", width=binsize, height=binsize, source=hm_source, fill_color={'field': 'start', 'transform': mapper}, line_color=None) ###-----------------------------------------------------------------------### ###---------------------------NYC map-------------------------------------### ###------------------------and events from data---------------------------### ###-----------------------------------------------------------------------### ### Add nyc map p.patches('x', 'y', source=nyc_source, color='black', line_width=1, fill_color={'field': 'colors'}, fill_alpha = 0.4) ### Add my bait points baits = p.circle('LONGITUDE', 'LATITUDE', source=first_source_bait, fill_color='#4dc6e0', line_color = '#4dc6e0', line_width=3, line_alpha=0.6, legend="Rat Interventions") ### Add my sights points sights = p.circle('LONGITUDE', 'LATITUDE', source=first_source_sight, fill_color = '#d96c82',line_color = '#d96c82',line_width=3, line_alpha=0.6, legend="Rat Sightings") p.legend.location = "top_left" p.legend.label_text_color = 'white' p.legend.border_line_color = "white" p.legend.background_fill_color = "black" ### Add slider date_slider = DateSlider(title="Date", start=dt.date(2010, 1, 1), end=dt.date(2018, 9, 1),value=dt.date(2014, 4, 1), step=1, format = "%B %Y") ### Add hovers bait_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #00BFFF">Location:</span> <span style="font-size: 15px; color: #000000">@HOUSE_NUMBER @STREET_NAME</span><br> <span style="font-size: 14px; font-weight:bold; color: #00BFFF;">Zip Code:</span> <span style="font-size: 15px; color: #000000"> @ZIP_CODE </span><br> <span style="font-size: 14px; font-weight:bold; color: #00BFFF;">Intervention Date: </span> <span style="font-size: 15px; color: #000000">@Inspection_Date</span><br> <span style="font-size: 14px; font-weight:bold; color: #00BFFF;">Intervention Type: </span> <span style="font-size: 15px; color: #000000">@RESULT</span> </div> </div> """, renderers=[baits]) p.add_tools(bait_hover) sight_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #F08080">Location:</span> <span style="font-size: 15px; color: #000000">@ADDRESS</span><br> <span style="font-size: 14px; font-weight:bold; color: #F08080;">Zip Code:</span> <span style="font-size: 15px; color: #000000"> @ZIP_CODE </span><br> <span style="font-size: 14px; font-weight:bold; color: #F08080;">Rat Sighting Date: </span> <span style="font-size: 15px; color: #000000">@Sighting_Date</span> </div> </div> """, renderers=[sights]) p.add_tools(sight_hover) prediction_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #F08080">Longitude:</span> <span style="font-size: 15px; color: #000000">@level_0</span><br> <span style="font-size: 14px; font-weight:bold; color: #F08080;">Latitude:</span> <span style="font-size: 15px; color: #000000"> @level_1 </span><br> <span style="font-size: 14px; font-weight:bold; color: #F08080;">Predicted monthly sightings: </span> <span style="font-size: 15px; color: #000000">@start</span> </div> </div> """, renderers=[prediction_location]) p.add_tools(prediction_hover) ### Add a Zip Code selection option zip_select = Select(title="Selected Zipcode:", value="all zipcodes", options= all_zips) ###-----------------------------------------------------------------------### ###------------------------PLOT of whole----------------------------------### ###----------------------city sightings numbers---------------------------### ###-----------------------------------------------------------------------### fit_source = ColumnDataSource(prophet_fit_all_nyc) prediction_source = ColumnDataSource(prophet_prediction_all_nyc) p_right = figure(title = 'CITY-WIDE MONTHLY PREDICTIONS',tools= 'box_zoom,pan,save,reset', min_border_top = 250, min_border_left = 100, border_fill_color = "black", background_fill_color = "black", width = 600, height = 550, active_drag="box_zoom", x_axis_type="datetime") # interval shading glyph: lowerband = prophet_prediction_all_nyc['yhat_lower'].values upperband = prophet_prediction_all_nyc['yhat_upper'].values band_x = np.append(prophet_prediction_all_nyc['ds'].values, prophet_prediction_all_nyc['ds'].values[::-1]) band_y = np.append(lowerband, upperband[::-1]) p_right.patch(band_x, band_y, color='white', fill_alpha=0.5, alpha = 0.5) p_right.line(x = 'ds', y = 'y', source = fit_source, color = '#d96c82', line_width=2.6, legend = 'monthly rat sightings') p_right.circle(x = 'ds', y = 'y', source = fit_source, color = '#d96c82', size = 7, alpha = 0.5, legend = 'monthly rat sightings') p_right.line(x = 'ds', y = 'yhat', source = prophet_fit_all_nyc, line_width=2, color = 'white', legend = 'FBprophet fit/prediction') p_right.circle(x = 'ds', y = 'yhat', source = prophet_fit_all_nyc, color = 'white', size = 5, alpha = 0.5, legend = 'FBprophet fit/prediction') p_right.line(x = 'ds', y = 'yhat', source = prophet_prediction_all_nyc, line_width=2, color = 'white', line_dash="4 4") p_right.circle(x = 'ds', y = 'yhat', source = prophet_prediction_all_nyc, size = 5, color = 'white', alpha = 0.5, line_dash="4 4") p_right.line([prophet_fit_all_nyc.iloc[-1,0], prophet_prediction_all_nyc.iloc[0,0]], [prophet_fit_all_nyc.iloc[-1,2], prophet_prediction_all_nyc.iloc[0,1]], line_dash="4 4", line_width=2, color='white') p_right.legend.location = "top_left" p_right.xaxis.major_label_text_font_size = "14pt" p_right.yaxis.major_label_text_font_size = "14pt" p_right.title.text_font_size = '16pt' p_right.legend.label_text_font_size = '9pt' p_right.legend.location = "top_left" p_right.xaxis.axis_label = 'Date' p_right.yaxis.axis_label = 'monthly rat sightings' p_right.xaxis.axis_label_text_font_size = "14pt" p_right.yaxis.axis_label_text_font_size = "14pt" p_right.xaxis.axis_label_text_color = '#909090' p_right.xaxis.axis_line_color = '#909090' p_right.xaxis.major_label_text_color = '#909090' p_right.yaxis.axis_label_text_color = '#909090' p_right.yaxis.axis_line_color = '#909090' p_right.yaxis.major_label_text_color = '#909090' p_right.title.text_color = '#909090' p_right.legend.label_text_color = '#909090' p_right.legend.border_line_color = "#909090" p_right.outline_line_color = "#909090" p_right.legend.background_fill_color = "black" ###-----------------------------------------------------------------------### ###----------------------------CALLBACKS----------------------------------### ### This section defines the behavior of the GUI as the user interacts ### ### with the controls. --------------------------------------------------### ###-----------------------------------------------------------------------### ### Slider callback function callback = CustomJS(args=dict(date_slider = date_slider, zip_select = zip_select, first_source_bait = first_source_bait, original_source_bait = bait_source, bait_indices = indices_bait_source, first_source_sight = first_source_sight, original_source_sight = sight_source, hm_source_original = hm_source_original, hm_source = hm_source, sight_indices = indices_sight_source, timepoints_cds = timepoints_cds, predicted_months_cds = predicted_months_cds), code=""" var date_slider = new Date(date_slider.value); var timepoints_cds = timepoints_cds.data; var predicted_months_cds = predicted_months_cds.data; var zip_selected = parseFloat(zip_select.value); var data_bait = first_source_bait.data; var whole_data_bait = original_source_bait.data; var bait_indices = bait_indices.data; var data_sight = first_source_sight.data; var whole_data_sight = original_source_sight.data; var sight_indices = sight_indices.data; var data_hm = hm_source.data; var data_hm_original = hm_source_original.data; const monthNames = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; var year_month = (date_slider.getUTCFullYear()).toString() +'-'+ monthNames[date_slider.getUTCMonth()]; console.log(year_month) var g = timepoints_cds['timepoints'].indexOf(year_month) var test = 0; data_hm['start'] = []; data_hm['level_0'] = []; data_hm['level_1'] = []; if(predicted_months_cds['predicted_months'].indexOf(year_month) >= 0 ) { for (k = 0; k < 80*80; k++) { data_hm['start'].push(data_hm_original['predicted_'+ year_month][k]) data_hm['level_0'].push(data_hm_original['level_0'][k]) data_hm['level_1'].push(data_hm_original['level_1'][k]) test = k; } } console.log(data_hm['start'][test]) data_bait['LONGITUDE'] = [] data_bait['LATITUDE'] = [] data_bait['HOUSE_NUMBER'] = [] data_bait['STREET_NAME'] = [] data_bait['ZIP_CODE'] = [] data_bait['Inspection_Date'] = [] data_bait['RESULT'] = [] for (i = bait_indices['from_index'][g]; i < bait_indices['to_index'][g] + 1; i++) { if(whole_data_bait['ZIP_CODE'][i] == zip_selected || zip_selected == "all zipcodes" || isNaN(zip_selected)) { data_bait['LONGITUDE'].push(whole_data_bait['LONGITUDE'][i]) data_bait['LATITUDE'].push(whole_data_bait['LATITUDE'][i]) data_bait['HOUSE_NUMBER'].push(whole_data_bait['HOUSE_NUMBER'][i]) data_bait['STREET_NAME'].push(whole_data_bait['STREET_NAME'][i]) data_bait['ZIP_CODE'].push(whole_data_bait['ZIP_CODE'][i]) data_bait['Inspection_Date'].push(whole_data_bait['Inspection_Date'][i]) data_bait['RESULT'].push(whole_data_bait['RESULT'][i]) } } data_sight['LONGITUDE'] = [] data_sight['LATITUDE'] = [] data_sight['ADDRESS'] = [] data_sight['ZIP_CODE'] = [] data_sight['Sighting_Date'] = [] for (j = sight_indices['from_index'][g]; j < sight_indices['to_index'][g] + 1; j++) { if(whole_data_sight['ZIP_CODE'][j] == zip_selected || zip_selected == "all zipcodes" || isNaN(zip_selected)) { data_sight['LONGITUDE'].push(whole_data_sight['LONGITUDE'][j]) data_sight['LATITUDE'].push(whole_data_sight['LATITUDE'][j]) data_sight['ADDRESS'].push(whole_data_sight['ADDRESS'][j]) data_sight['ZIP_CODE'].push(whole_data_sight['ZIP_CODE'][j]) data_sight['Sighting_Date'].push(whole_data_sight['Sighting_Date'][j]) } } hm_source.change.emit(); first_source_sight.change.emit(); first_source_bait.change.emit(); """) ### Zip code select callback function zip_callback = CustomJS(args=dict(zip_select = zip_select, nyc_source = nyc_source, date_slider = date_slider, first_source_bait = first_source_bait, original_source_bait = bait_source, bait_indices = indices_bait_source, first_source_sight = first_source_sight, original_source_sight = sight_source, sight_indices = indices_sight_source, timepoints_cds = timepoints_cds), code=""" var zip_selected = parseFloat(zip_select.value); var date_slider = new Date(date_slider.value); var timepoints_cds = timepoints_cds.data; var nyc_source_data = nyc_source.data; var zip_color_selected = "black"; if (zip_selected == "all zipcodes" || isNaN(zip_selected)) { zip_color_selected = "white"; } var zip_color_rest = "white"; var data_bait = first_source_bait.data; var whole_data_bait = original_source_bait.data; var bait_indices = bait_indices.data; var data_sight = first_source_sight.data; var whole_data_sight = original_source_sight.data; var sight_indices = sight_indices.data; nyc_source_data['colors'] = [] for (i = 0; i <nyc_source_data['ZIPCODE'].length; i++) { if (nyc_source_data['ZIPCODE'][i] == zip_selected || zip_selected == "all zipcodes" || isNaN(zip_selected)) { nyc_source_data['colors'].push(zip_color_selected); } else { nyc_source_data['colors'].push(zip_color_rest); } } const monthNames = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; var year_month = (date_slider.getUTCFullYear()).toString() +'-'+ monthNames[date_slider.getUTCMonth()]; console.log(year_month) console.log(zip_selected) var g = timepoints_cds['timepoints'].indexOf(year_month) data_bait['LONGITUDE'] = [] data_bait['LATITUDE'] = [] data_bait['HOUSE_NUMBER'] = [] data_bait['STREET_NAME'] = [] data_bait['ZIP_CODE'] = [] data_bait['Inspection_Date'] = [] data_bait['RESULT'] = [] for (i = bait_indices['from_index'][g]; i < bait_indices['to_index'][g] + 1; i++) { if(whole_data_bait['ZIP_CODE'][i] == zip_selected || zip_selected == "all zipcodes" || isNaN(zip_selected)) { data_bait['LONGITUDE'].push(whole_data_bait['LONGITUDE'][i]) data_bait['LATITUDE'].push(whole_data_bait['LATITUDE'][i]) data_bait['HOUSE_NUMBER'].push(whole_data_bait['HOUSE_NUMBER'][i]) data_bait['STREET_NAME'].push(whole_data_bait['STREET_NAME'][i]) data_bait['ZIP_CODE'].push(whole_data_bait['ZIP_CODE'][i]) data_bait['Inspection_Date'].push(whole_data_bait['Inspection_Date'][i]) data_bait['RESULT'].push(whole_data_bait['RESULT'][i]) } } data_sight['LONGITUDE'] = [] data_sight['LATITUDE'] = [] data_sight['ADDRESS'] = [] data_sight['ZIP_CODE'] = [] data_sight['Sighting_Date'] = [] for (j = sight_indices['from_index'][g]; j < sight_indices['to_index'][g] + 1; j++) { if(whole_data_sight['ZIP_CODE'][j] == zip_selected || zip_selected == "all zipcodes" || isNaN(zip_selected)) { data_sight['LONGITUDE'].push(whole_data_sight['LONGITUDE'][j]) data_sight['LATITUDE'].push(whole_data_sight['LATITUDE'][j]) data_sight['ADDRESS'].push(whole_data_sight['ADDRESS'][j]) data_sight['ZIP_CODE'].push(whole_data_sight['ZIP_CODE'][j]) data_sight['Sighting_Date'].push(whole_data_sight['Sighting_Date'][j]) } } first_source_sight.change.emit(); first_source_bait.change.emit(); nyc_source.change.emit(); """) zip_select.js_on_change('value', zip_callback) date_slider.js_on_change('value', callback) layout = row(column(row(date_slider, zip_select), p), p_right) # layout = gridplot([[p, p_right]]) the_script, the_div = components(layout) ###-----------------------------------------------------------------------### ###-----------------------------------------------------------------------### ###-----------------------BY ZIPCODES PLOT--------------------------------### ###-----------------------------------------------------------------------### ###-----------------------------------------------------------------------### colors = ['#f27991', '#f493a7', '#f7aebd', '#fce4e9'] + ['#FFFFFF'] + ['#ddf8fd','#ccf4fd', '#bbf1fc','#aaedfc','#99eafb','#88e6fa', '#77e3fa', '#66dff9', '#56dcf9'] mapper = LinearColorMapper(palette=colors, low=by_zipcodes_df.dev.min(), high=by_zipcodes_df.dev.max()) color_bar = ColorBar(color_mapper=mapper, location=(0, 0), label_standoff=10, major_label_text_font_size='14pt', major_label_text_color = '#909090', background_fill_color = 'black', scale_alpha = 0.7) mp = figure(x_range=(-74.2, -73.7), y_range=(40.53, 40.915), width = 600, height =630, tools= 'box_zoom,pan,save,reset', active_drag="box_zoom", background_fill_color = "black", min_border_right = 40, min_border_top = 5, min_border_bottom = 5, border_fill_color = "black") mp.xgrid.grid_line_color = None mp.ygrid.grid_line_color = None mp.axis.visible = False mp.outline_line_color = "black" zips = mp.patches('x', 'y', source=nyc_by_zips_source, color='black', line_width=1, fill_color={'field': 'dev', 'transform': mapper}, alpha = 0.7) zips_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #000000;">Zip Code: </span> <span style="font-size: 15px; color: #000000">@ZIPCODE</span><br> <span style="font-size: 14px; font-weight:bold; color: #000000">Average number of rat sightings per year:</span> <span style="font-size: 15px; color: #000000">@sightings</span><br> <span style="font-size: 14px; font-weight:bold; color: #000000;">Average number of interventions per year:</span> <span style="font-size: 15px; color: #000000"> @interventions </span><br> <span style="font-size: 14px; font-weight:bold; color: #000000;">Number of interventions above expectation:</span> <span style="font-size: 15px; color: #000000"> @dev </span> </div> </div> """, renderers=[zips]) mp.add_tools(zips_hover) p_zips = figure(title = 'Sightings and interventions by zipcode',tools= 'box_zoom,pan,save,reset', active_drag="box_zoom", background_fill_color = "black", min_border_right = 40, min_border_top = 5, min_border_bottom = 5, border_fill_color = "black") points = p_zips.circle(x = 'sightings', y = 'interventions', source = by_zipcodes_source, size=12, fill_color={'field': 'dev', 'transform': mapper}, alpha = 0.7, line_color = 'black') p_zips.line(x = 'sightings', y = 'lin_fit', source = by_zipcodes_source, color = 'white') p_zips.add_layout(color_bar, 'left') points_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #000000;">Zip Code: </span> <span style="font-size: 15px; color: #000000">@ZIP_CODE</span><br> <span style="font-size: 14px; font-weight:bold; color: #000000">Average number of rat sightings per year:</span> <span style="font-size: 15px; color: #000000">@sightings</span><br> <span style="font-size: 14px; font-weight:bold; color: #000000;">Average number of interventions per year:</span> <span style="font-size: 15px; color: #000000"> @interventions </span><br> <span style="font-size: 14px; font-weight:bold; color: #000000;">Number of interventions above expectation:</span> <span style="font-size: 15px; color: #000000"> @dev </span> </div> </div> """, renderers=[points]) p_zips.add_tools(points_hover) p_zips.xaxis.major_label_text_font_size = "14pt" p_zips.yaxis.major_label_text_font_size = "14pt" p_zips.title.text_font_size = '16pt' p_zips.xaxis.axis_label = 'average number of rat sightings per year' p_zips.yaxis.axis_label = 'average number of interventions per year' p_zips.xaxis.axis_label_text_font_size = "14pt" p_zips.yaxis.axis_label_text_font_size = "14pt" p_zips.xaxis.axis_label_text_color = '#909090' p_zips.xaxis.axis_line_color = '#909090' p_zips.xaxis.major_label_text_color = '#909090' p_zips.yaxis.axis_label_text_color = '#909090' p_zips.yaxis.axis_line_color = '#909090' p_zips.yaxis.major_label_text_color = '#909090' p_zips.title.text_color = '#909090' layout_zips = row(mp,p_zips) the_script_zips, the_div_zips = components(layout_zips) ###-----------------------------------------------------------------------### ###-----------------------------------------------------------------------### ###--------------------------HEATMAPS PLOT--------------------------------### ###------------------------- BY ZIPCODE----------------------------------### ###-----------------------------------------------------------------------### colors_hm = ['#fdf1f4', '#fce4e9', '#fbd6de', '#f9c9d3', '#f8bcc8', '#f7aebd', '#f5a1b2','#f493a7', '#f3869c', '#f27991'] mapper_hm = LinearColorMapper(palette=colors_hm, low=0, high=50) hm_color_bar = ColorBar(color_mapper=mapper_hm, location=(0, 0), label_standoff=10, major_label_text_font_size='14pt', major_label_text_color = '#909090', background_fill_color = 'black', scale_alpha = 0.7) heatmap = figure(x_range=(-74.2, -73.7), y_range=(40.53, 40.915), width = 500, height =500, tools= 'box_zoom,pan,save,reset', active_drag="box_zoom", background_fill_color = "black", min_border_top = 5, min_border_bottom = 5, border_fill_color = "black", toolbar_location="left") heatmap.xgrid.grid_line_color = None heatmap.ygrid.grid_line_color = None heatmap.axis.visible = False heatmap.outline_line_color = "black" zips_hm = heatmap.patches('x', 'y', source=zip_hm_first, color='black', line_width=1, fill_color={'field': 'sightings', 'transform': mapper_hm}, alpha = 0.7) zips_hm_hover = HoverTool(tooltips = """ <div> <div> <span style="font-size: 14px; font-weight:bold; color: #000000;">Zip Code: </span> <span style="font-size: 15px; color: #000000">@ZIPCODE</span><br> <span style="font-size: 14px; font-weight:bold; color: #000000">Number of sightings this month:</span> <span style="font-size: 15px; color: #000000">@sightings</span><br> </div> </div> """, renderers=[zips_hm]) heatmap.add_tools(zips_hm_hover) dummy = figure(height=500, width=150, toolbar_location=None, min_border=0, outline_line_color=None, background_fill_color = "black", min_border_top = 5, min_border_bottom = 5, border_fill_color = "black") dummy.add_layout(hm_color_bar, 'right') ### Add slider date_slider_hm = DateSlider(title="Date", start=dt.date(2010, 1, 1), end=dt.date(2018, 4, 1),value=dt.date(2014, 7, 1), step=1, format = "%B %Y") ### Slider callback function hm_callback = CustomJS(args=dict(date_slider = date_slider_hm, zip_hm_first = zip_hm_first, zip_hm_original = zip_hm_original), code=""" var date_slider = new Date(date_slider.value); var data_hm = zip_hm_first.data; var data_hm_original = zip_hm_original.data; const monthNames = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; var year_month = (date_slider.getUTCFullYear()).toString() +'-'+ monthNames[date_slider.getUTCMonth()]; console.log(year_month) var test = 0; data_hm['sightings'] = []; for (k = 0; k < 263; k++) { data_hm['sightings'].push(data_hm_original[year_month][k]) } zip_hm_first.change.emit(); """) date_slider_hm.js_on_change('value', hm_callback) layout_hm = column(date_slider_hm,row(heatmap, dummy)) the_script_hm, the_div_hm = components(layout_hm) return render_template('myindex-pitch-night.html', div = the_div, script=the_script, the_script_zips = the_script_zips, the_div_zips = the_div_zips, the_script_hm = the_script_hm, the_div_hm = the_div_hm)
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 plot_visibility(tiles, airmass=1.5, width=800, outfile=None, title=None): ''' Plot tile visibility using bokeh, optionally outputting to html file Args: tiles: np.array of tiles with columns TILEID, RA, DEC, PROGRAM Options: airmass (float): airmass visibility window to highlight width (float): plot width in pixels outfile (str): output filename title (str): plot title If using within a Jupyter notebook, keep outfile=None and it will display within the notebook instead. ''' import bokeh.plotting as bk from bokeh.models.widgets import tables as bktables from bokeh.models import CustomJS, Slider, DateSlider, HoverTool if title is None: title = 'DESI Tile Picker' lat = 31.964 #- KPNO Latitude now = time.localtime() # lst = kpnotime2lst(2019, 1, 1, 16, 0) lst = kpnotime2lst(now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min) ra0_1, dec_1 = visibility(0, lat, airmass, n=350) ra0_2, dec_2 = visibility(0, lat, 2.0, n=350) ra0 = np.concatenate([ra0_1, ra0_2]) dec = np.concatenate([dec_1, dec_2]) # ra0, dec = visibility(0, lat, airmass) ra = (ra0 + lst) % 360 tiledata = dict( ra=tiles['RA'], dec=tiles['DEC'], tileid=tiles['TILEID'], program=tiles['PROGRAM'], selected=np.ones(len(tiles), dtype=bool), ) for colname in ['STAR_DENSITY', 'EBV_MED']: if colname in tiles.dtype.names: tiledata[colname] = tiles[colname] source = bk.ColumnDataSource(data=dict(ra0=ra0, ra=ra, dec=dec)) tile_source = bk.ColumnDataSource(data=tiledata) colformat = bktables.NumberFormatter(format='0,0.00') columns = [ bktables.TableColumn(field='tileid', title='TILEID', width=60), bktables.TableColumn(field='ra', title='RA', formatter=colformat), bktables.TableColumn(field='dec', title='DEC', formatter=colformat), ] for colname in ['STAR_DENSITY', 'EBV_MED']: if colname in tiledata: columns.append(bktables.TableColumn(field=colname, title=colname, formatter=colformat)) columns.append(bktables.TableColumn(field='selected', title='Selected')) tiletable = bktables.DataTable(columns=columns, source=tile_source, width=width) tile_source.selected.js_on_change('indices', CustomJS(args=dict(s1=tile_source), code=""" var inds = cb_obj.indices; var d1 = s1.data; for (var i=0; i<d1['selected'].length; i++) { d1['selected'][i] = false; } for (var i = 0; i < inds.length; i++) { d1['selected'][inds[i]] = true; } s1.change.emit(); """) ) if outfile is not None: if os.path.exists(outfile): os.remove(outfile) bk.output_file(outfile, title=title) else: bk.output_notebook() fig = bk.figure(width=width, height=width//2, tools=['pan', 'box_zoom', 'box_select', 'save', 'undo', 'reset'], active_drag='box_select') #- TODO: adjust size depending upon number of circles drawn circlesize = 2.0 tile_circles = fig.circle('ra', 'dec', source=tile_source, radius=circlesize, alpha=0.8) fig.circle('ra', 'dec', source=source, size=0.1, color='black') fig.xaxis.axis_label = 'RA [degrees]' fig.yaxis.axis_label = 'Declination [degrees]' fig.title.text = '{}; airmass < {:.1f}, 2 @ LST={:.1f} deg'.format( title, airmass, lst) #- KPNO Date and local time sliders start = datetime.datetime(2019, 1, 1, 0, 0, 0) today = datetime.datetime(now.tm_year, now.tm_mon, now.tm_mday, 0, 0, 0) date_slider = DateSlider(start=start, end = start+datetime.timedelta(days=365), value=today, format = "%B %d", title='Date of sunset', width=width) start = datetime.datetime(2019, 1, 1, 16, 0, 0) timenow = datetime.datetime(2019, 1, 1, now.tm_hour, now.tm_min, now.tm_sec) localtime_slider = DateSlider(start = start, end = start+datetime.timedelta(hours=16), value=timenow, format = "%H:%M", title='KPNO local time', width=width) callback = CustomJS( args=dict(source=source, date_slider=date_slider, localtime_slider=localtime_slider, airmass=airmass, fig=fig), code=""" // First set times as if they were UTC var t = new Date(localtime_slider.value); var d = new Date(date_slider.value); if (t.getUTCHours() < 12) { d.setTime(date_slider.value + 24*3600*1000); } else { d.setTime(date_slider.value); } d.setUTCHours(t.getUTCHours()); d.setUTCMinutes(t.getUTCMinutes()); d.setUTCSeconds(0); // Correct to KPNO local time // d object still thinks in UTC, which is 7 hours ahead of KPNO d.setTime(d.getTime() + 7*3600*1000); // noon UT on 2000-01-01 var reftime = new Date(); reftime.setUTCFullYear(2000); reftime.setUTCMonth(0); // Months are 0-11 (!) reftime.setUTCDate(1); // Days are 1-31 (!) reftime.setUTCHours(12); reftime.setUTCMinutes(0); reftime.setUTCSeconds(0); // time difference in days (starting from milliseconds) var dt = (d.getTime() - reftime.getTime()) / (24*3600*1000); // Convert to LST var mayall_longitude_degrees = -(111 + 35/60. + 59.6/3600); var LST_hours = ((18.697374558 + 24.06570982441908 * dt) + mayall_longitude_degrees/15) % 24; var LST_degrees = LST_hours * 15; var data = source.data; var ra = data['ra']; var ra0 = data['ra0']; for (var i = 0; i < ra.length; i++) { ra[i] = (ra0[i] + LST_degrees) % 360; } // DESI tile picker; airmass < {:.1f} @ LST={:.1f} deg fig.title.text = "DESI tile picker; airmass < " + airmass + " @ LST="+LST_degrees.toFixed(2); source.change.emit(); // console.log(d, reftime, LST_degrees); """) date_slider.js_on_change('value', callback) localtime_slider.js_on_change('value', callback) fig.x_range.start = 360 fig.x_range.end = 0 hovertips = [ ("Tile ID", "@tileid"), ("RA,dec", "@ra, @dec"), ("Program", "@program"), ] fig.add_tools(HoverTool(tooltips=hovertips, callback=None, renderers=[tile_circles,])) text_opts = dict(text_align='center', text_baseline='middle', text_alpha=0.3, text_font_size=dict(value='10pt')) fig.text(180, 84, ['North',], **text_opts) fig.text(180, -20, ['South',], **text_opts) fig.text(352, 50, ['East',], angle=np.pi/2, **text_opts) fig.text(7, 50, ['West',], angle=np.pi/2, **text_opts) if outfile is None: bk.show(bk.Column(fig, date_slider, localtime_slider, tiletable)) else: bk.save(bk.Column(fig, date_slider, localtime_slider, tiletable))
def laHexMapPlot(): # TODO: refactor! df = pd.DataFrame(LocalAuthorities.objects.getAll()) df['target_net_zero_year'] = df['target_net_zero_year'].fillna('') start = datetime(2018, 1, 1) end = datetime.now() declaredFromDate = datetime.now() with open(os.path.join(conf.BASE_DIR, 'static/ref/hexmap.geojson'), 'r') as f: data = json.load(f) # For reasons that aren't clear to me, Bokeh plots # the geojson flipped vertically, and every 2nd row # needs shifting one more column to the right. # So a bit of manual effort needed to sort out minR = 1000 maxR = 0 for i in range(len(data['features'])): r = data['features'][i]['properties']['r'] if r > maxR: maxR = r if r < minR: minR = r n = 0 for r in range(minR, maxR + 1): if r % 2 != 0: n += 1 for j in range(len(data['features'])): if data['features'][j]['properties']['r'] == r: data['features'][j]['properties']['q'] += n # Set the colours based on whether they have declared q = [] r = [] la_name = [] declared_date = [] declared_date_str = [] target_net_zero_year = [] source = [] action_plan = [] color = [] for i in range(len(data['features'])): q.append(data['features'][i]['properties']['q']) r.append(data['features'][i]['properties']['r']) la_name.append( list(df.loc[(df.code == data['features'][i]['properties']['c']), 'xr_la_name'])[0]) decDate = \ list(df.loc[ (df.code == data['features'][i]['properties']['c']), 'declaration_date'])[0] decDateStr = \ list(df.loc[ (df.code == data['features'][i]['properties']['c']), 'declaration_date_str'])[0] declared_date.append(decDate) declared_date_str.append(decDateStr) if decDateStr == 'Not declared': color.append(conf.LEMON) elif decDate >= declaredFromDate: color.append(conf.LEMON) else: color.append(conf.PINK) target_net_zero_year.append( list(df.loc[(df.code == data['features'][i]['properties']['c']), 'target_net_zero_year'])[0]) source.append( list(df.loc[(df.code == data['features'][i]['properties']['c']), 'source'])[0]) action_plan.append( list(df.loc[(df.code == data['features'][i]['properties']['c']), 'action_plan'])[0]) q = np.asarray(q) r = np.asarray(r) df_LAs = pd.DataFrame({ 'q': q, 'r': -r, 'la_name': la_name, 'declared_date': declared_date, 'declared_date_str': declared_date_str, 'target_net_zero_year': target_net_zero_year, 'source': source, 'action_plan': action_plan, 'color': color }) tooltips = [ ('la_name'), ('Declared Date', 'declared_date_str'), ('Net Zero Year', 'target_net_zero_year'), ('Source', '/Click for link to data source'), ('Action Plan', '/Click for link to action plan'), ] stickyTooltips = [('la_name'), ('Declared Date', 'declared_date_str'), ('Net Zero Year', 'target_net_zero_year'), ('Source', 'source'), ('Action Plan', 'action_plan')] data = ColumnDataSource(df_LAs) hexMap = chartUtils.hexMap(data, tooltips=tooltips, stickyTooltips=stickyTooltips) slider = DateSlider(start=datetime(2018, 1, 1), end=datetime.now(), step=1, value=datetime.now()) callback = CustomJS(args=dict(source=data), code=""" var declaredFromDate = cb_obj.value; for (var i = 0; i < source.data['declared_date'].length; i++) { if (source.data['declared_date_str'][i] == 'Not declared') { source.data['color'][i] = '#F7EE6A' } else if (source.data['declared_date'][i] > declaredFromDate) { source.data['color'][i] = '#F7EE6A' } else { source.data['color'][i] = '#ED9BC4' } } source.change.emit(); """) slider.js_on_change('value', callback) hexMapLayout = column(slider, hexMap) commentaryDiv = commentary.getCommentary('map_la_declared') backgroundImageDiv = chartUtils.backgroundImage() layout = row(hexMapLayout, commentaryDiv, backgroundImageDiv) return layout