Esempio n. 1
0
    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()
Esempio n. 2
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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}
Esempio n. 6
0
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)
Esempio n. 8
0
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