Beispiel #1
0
def configure_plot(plot):
    plot.toolbar.logo = None
    plot.xaxis.axis_label_text_font_style = "normal"
    plot.yaxis.axis_label_text_font_style = "normal"
    plot.xaxis.major_label_text_font_size = "1rem"
    plot.yaxis.major_label_text_font_size = "1rem"
    plot.xaxis.axis_label_text_font_size = "1rem"
    plot.yaxis.axis_label_text_font_size = "1rem"
    plot.xaxis.formatter = FuncTickFormatter(
        code="return format_exponential(tick);")
    plot.yaxis.formatter = FuncTickFormatter(
        code="return format_exponential(tick);")
Beispiel #2
0
def wrap_formatter(formatter, axis):
    """
    Wraps formatting function or string in
    appropriate bokeh formatter type.
    """
    if isinstance(formatter, TickFormatter):
        pass
    elif isinstance(formatter, FunctionType):
        msg = ('%sformatter could not be '
               'converted to tick formatter. ' % axis)
        jsfunc = py2js_tickformatter(formatter, msg)
        if jsfunc:
            formatter = FuncTickFormatter(code=jsfunc)
        else:
            formatter = None
    else:
        formatter = PrintfTickFormatter(format=formatter)
    return formatter
Beispiel #3
0
def create_annotation_hbar(annotation, data, col_index=0):
    '''Data in the format [('Label 1',52), ('Label 2, 148'), ...] in ascending order.'''
    labels = [k for k, v in data]
    values = [v for k, v in data]
    xmax = max(values) * 1.05
    p = figure(x_range=(0, xmax),
               y_range=labels,
               sizing_mode='stretch_both',
               toolbar_location=None,
               tools=['tap'],
               plot_height=180)
    p.xaxis.formatter = FuncTickFormatter(code='return tick? tick : "";')
    p.yaxis.visible = False
    p.min_border_left = 0
    p.min_border_right = 0
    p.min_border_top = 0
    p.min_border_bottom = 0
    p.ygrid.grid_line_color = None
    colors = [('#87cded', '#a7edfd'), ('#c0c610', '#d0ff14'),
              ('#ff7034', '#ff9044'), ('#ff9889', '#ffb8a9'),
              ('#f4bfff', '#f8dfff'), ('#bff4bb', '#dff8dd'),
              ('#c0c0c0', '#b0b0b0'), ('#ff80a0', '#ff90b0')]
    cmap = colors[col_index % len(colors)] * int(len(labels) / 2 + 1)
    cmap = cmap[:len(labels)][::-1]
    ds = ColumnDataSource(dict(labels=labels, value=values, color=cmap))
    p.hbar(y='labels', right='value', height=0.9, color='color', source=ds)
    p.text(y='labels',
           text='labels',
           text_baseline='middle',
           x=0,
           x_offset=3,
           text_font_size='9px',
           source=ds)
    ds.selected.js_on_change(
        'indices',
        CustomJS(args=dict(labels=labels),
                 code='''var label = labels[cb_obj.indices[0]];
                            var url = new URL(window.location.href);
                            var filter = JSON.parse(url.searchParams.get("filter")) || [{}];
                            filter[0]["%s"] = (filter[0]["%s"] || []).concat(label);
                            url.searchParams.set("filter", JSON.stringify(filter));
                            window.location.assign(url);''' %
                 (annotation, annotation)))
    return p
Beispiel #4
0
    def __update_category_barplot(self):
        """Function updates Category Barplot and it's corresponding ColumnDataSource.

            Function first calculates aggregated DataFrame, grouped by .monthyear column and then sorts it by
            .price column in a descending order. Data from this df is then pulled to update both ColumnDataSource
            and a Plot:
                - "x" and "top" values in ColumnDataSource are updated with Category/Price values, appropriately,
                - Plot x_range.factors is updated with new Category values

            Additionally, X axis JavaScript formatter is pinned to the Plot - if the length of categories exceeds
            25, then every second entry in the X axis is changed to ''. This is done to prevent overcrowding of
            labels in the X axis when there are too many Elements present.

            Grid Element .g_category_expenses and Grid Source Element .g_category_expenses are updated.
        """

        agg_df = self.chosen_month_expense_df.groupby(
            [self.category]).sum().reset_index().sort_values(by=[self.price],
                                                             ascending=False)

        fig = self.grid_elem_dict[self.g_category_expenses]
        source = self.grid_source_dict[self.g_category_expenses]

        fig.x_range.factors = agg_df[self.category].tolist()

        new_data = {"x": agg_df[self.category], "top": agg_df[self.price]}

        source.data.update(new_data)

        if len(agg_df[self.category]) >= 25:
            formatter = FuncTickFormatter(code="""
                var return_tick;
                return_tick = tick;
                if ((((index + 1) % 2) == 0) || (tick.length > 35)) {
                    return_tick = '';
                }
                return return_tick;
            """)

            fig.xaxis.formatter = formatter
Beispiel #5
0
def pi_formatter():
    return FuncTickFormatter.from_py_func(pi_formatter_func)
Beispiel #6
0
plot.legend.background_fill_alpha = 0.2

plot.title.text = "Interactive GW Spectrum"
plot.title.align = "right"
plot.title.text_color = "white"
plot.title.text_font_size = "23px"
plot.xgrid.visible = True
plot.ygrid.visible = True
plot.outline_line_width = 3

plot.add_layout(info_box_k_sw)


plot.yaxis[0].formatter = FuncTickFormatter(code="""
return 10 + (Math.log10(tick).toString()
             .split('')
             .map(function (d) { return d === '-' ? '⁻' : '⁰¹²³⁴⁵⁶⁷⁸⁹'[+d]; })
             .join(''));
""")
plot.xaxis[0].formatter = FuncTickFormatter(code="""
return 10 + (Math.log10(tick).toString()
             .split('')
             .map(function (d) { return d === '-' ? '⁻' : '⁰¹²³⁴⁵⁶⁷⁸⁹'[+d]; })
             .join(''));
""")

# NANOGrav data
plot.circle(freq_bins[:5], h2omega_median[:5], color='snow', size=8, line_alpha=0)

err_xs = []
err_ys = []
Beispiel #7
0
    def __init__(
        self,
        params,
        gp_callback,
        limits=(-90, 90),
        xticks=[-90, -60, -30, 0, 30, 60, 90],
        funcs=[],
        labels=[],
        xlabel="",
        ylabel="",
        distribution=False,
        legend_location="bottom_right",
        npts=300,
    ):
        # Store
        self.params = params
        self.limits = limits
        self.funcs = funcs
        self.gp_callback = gp_callback
        self.last_run = 0.0
        self.throttle_time = 0.0
        self.distribution = distribution

        # Arrays
        if len(self.funcs):
            xmin, xmax = self.limits
            xs = [np.linspace(xmin, xmax, npts) for f in self.funcs]
            ys = [
                f(x, *[self.params[p]["value"] for p in self.params.keys()])
                for x, f in zip(xs, self.funcs)
            ]
            colors = Category10[10][:len(xs)]
            lws = np.append([3], np.ones(10))[:len(xs)]
            self.source = ColumnDataSource(
                data=dict(xs=xs, ys=ys, colors=colors, lws=lws))

            # Plot them
            dx = (xmax - xmin) * 0.01
            self.plot = figure(
                plot_width=400,
                plot_height=600,
                toolbar_location=None,
                x_range=(xmin - dx, xmax + dx),
                title=xlabel,
                sizing_mode="stretch_both",
            )
            self.plot.title.align = "center"
            self.plot.title.text_font_size = "14pt"
            self.plot.multi_line(
                xs="xs",
                ys="ys",
                line_color="colors",
                source=self.source,
                line_width="lws",
                line_alpha=0.6,
            )
            self.plot.xaxis.axis_label_text_font_style = "normal"
            self.plot.xaxis.axis_label_text_font_size = "12pt"
            self.plot.xaxis.ticker = FixedTicker(ticks=xticks)
            self.plot.yaxis[0].formatter = FuncTickFormatter(
                code="return '  ';")
            self.plot.yaxis.axis_label = ylabel
            self.plot.yaxis.axis_label_text_font_style = "normal"
            self.plot.yaxis.axis_label_text_font_size = "12pt"
            self.plot.outline_line_width = 1
            self.plot.outline_line_alpha = 1
            self.plot.outline_line_color = "black"
            self.plot.toolbar.active_drag = None
            self.plot.toolbar.active_scroll = None
            self.plot.toolbar.active_tap = None

            # Legend
            for j, label in enumerate(labels):
                self.plot.line(
                    [0, 0],
                    [0, 0],
                    legend_label=label,
                    line_color=Category10[10][j],
                )
            self.plot.legend.location = legend_location
            self.plot.legend.title_text_font_style = "bold"
            self.plot.legend.title_text_font_size = "8pt"
            self.plot.legend.label_text_font_size = "8pt"
            self.plot.legend.spacing = 0
            self.plot.legend.label_height = 5
            self.plot.legend.glyph_height = 15

        else:

            self.plot = None

        # Sliders
        self.sliders = []
        for p in self.params.keys():
            slider = Slider(
                start=self.params[p]["start"],
                end=self.params[p]["stop"],
                step=self.params[p]["step"],
                value=self.params[p]["value"],
                orientation="horizontal",
                format="0.3f",
                css_classes=["custom-slider"],
                sizing_mode="stretch_width",
                height=10,
                show_value=False,
                title=self.params[p]["label"],
            )
            slider.on_change("value_throttled", self.callback)
            slider.on_change("value", self.callback_throttled)
            self.sliders.append(slider)

        # HACK: Add a hidden slider to get the correct
        # amount of padding below the graph
        if len(self.params.keys()) < 2:
            slider = Slider(
                start=0,
                end=1,
                step=0.1,
                value=0.5,
                orientation="horizontal",
                css_classes=["custom-slider", "hidden-slider"],
                sizing_mode="stretch_width",
                height=10,
                show_value=False,
                title="s",
            )
            self.hidden_sliders = [slider]
        else:
            self.hidden_sliders = []

        # Show mean and std. dev.?
        if self.distribution:
            self.mean_vline = Span(
                location=self.sliders[0].value,
                dimension="height",
                line_color="black",
                line_width=1,
                line_dash="dashed",
            )
            self.std_vline1 = Span(
                location=self.sliders[0].value - self.sliders[1].value,
                dimension="height",
                line_color="black",
                line_width=1,
                line_dash="dotted",
            )
            self.std_vline2 = Span(
                location=self.sliders[0].value + self.sliders[1].value,
                dimension="height",
                line_color="black",
                line_width=1,
                line_dash="dotted",
            )
            self.plot.renderers.extend(
                [self.mean_vline, self.std_vline1, self.std_vline2])

        # Full layout
        if self.plot is not None:
            self.layout = grid([
                [self.plot],
                [
                    column(
                        *self.sliders,
                        *self.hidden_sliders,
                        sizing_mode="stretch_width",
                    )
                ],
            ])
        else:
            self.layout = grid([[
                column(
                    *self.sliders,
                    *self.hidden_sliders,
                    sizing_mode="stretch_width",
                )
            ]])
Beispiel #8
0
def update():
    """This updates the axis labels and circles after changing the axes or selection sliders."""
    for p in [p1, p2, p3, p4]:
        p.xaxis.axis_label = x_axis.value + ' ' + unit_map[x_axis.value]
        p.yaxis.axis_label = y_axis.value + ' ' + unit_map[y_axis.value]
    x_name = axis_map[x_axis.value]
    y_name = axis_map[y_axis.value]
    #This is where the actual conversion between the exoplanet input table and the dataframe read by Bokeh is done.
    datatable.data = dict(
        x=DF[x_name],
        y=DF[y_name],
        P=DF["pl_orbper"],
        Mp=DF["planetmass"],
        Rp=DF["planetradius"],
        T_eq=np.round(DF["teq"], 0),
        Gmag=np.round(DF["gaia_gmag"], 1),
        Jmag=np.round(DF["st_j"], 1),
        color=DF["colour"],
        alpha=DF["alpha"],
        Name=DF["pl_name"],
        rho=DF['pl_dens'],
        ecc=DF['pl_orbeccen'],
        FeH=DF["st_metfe"]
    )  #All this additional info is needed ONLY for the tooltip. Just sayin.

    #This fixes the horribly looking superscripts native to Bokeh, kindly adopted from jdbocarsly on issue 6031; https://github.com/bokeh/bokeh/issues/6031
    fix_substring_JS = """
        var str = Math.log10(tick).toString(); //get exponent
        var newStr = "";
        for (var i=0; i<str.length;i++)
        {
            var code = str.charCodeAt(i);
            switch(code) {
                case 45: // "-"
                    newStr += "⁻";
                    break;
                case 49: // "1"
                    newStr +="¹";
                    break;
                case 50: // "2"
                    newStr +="²";
                    break;
                case 51: // "3"
                    newStr +="³"
                    break;
                default: // all digit superscripts except 1, 2, and 3 can be generated by adding 8256
                    newStr += String.fromCharCode(code+8256)
            }
        }
        return 10+newStr;
    """
    normal_logstring_JS = "return tick"
    if x_name == 'planetmass' or x_name == 'planetradius' or x_name == 'pl_orbper':
        p4.xaxis[0].formatter = FuncTickFormatter(code=fix_substring_JS)
        p2.xaxis[0].formatter = FuncTickFormatter(code=fix_substring_JS)
    else:
        p4.xaxis[0].formatter = FuncTickFormatter(code=normal_logstring_JS)
        p2.xaxis[0].formatter = FuncTickFormatter(code=normal_logstring_JS)
    if y_name == 'planetmass' or y_name == 'planetradius' or y_name == 'pl_orbper':
        p3.yaxis[0].formatter = FuncTickFormatter(code=fix_substring_JS)
        p4.yaxis[0].formatter = FuncTickFormatter(code=fix_substring_JS)
    else:
        p3.yaxis[0].formatter = FuncTickFormatter(code=normal_logstring_JS)
        p4.yaxis[0].formatter = FuncTickFormatter(code=normal_logstring_JS)
    #Wow. And it all still runs smoothly.
    update_selection()
Beispiel #9
0
#    return v[tick]
#    """
#radius_ticker="""
#    var v=['0 Re','0.5 Re','1 Re','1.6 Re','2 Re','3 Re (0.27 Rj)','4 Re (0.36 Rj)','5 Re (0.45 Rj)','0.5 Rj','0.8 Rj','1 Rj','1.2 Rj','1.5 Rj','1.8 Rj','2 Rj','2.5 Rj','3 Rj','5 Rj'];
#    return v[tick]
#    """

# Create sliders and other controls
# mass = RangeSlider(start=lim_mass[0], end=lim_mass[1], value=lim_mass,value_throttled=lim_mass, step=.1, title=list(axis_map.keys())[0])
mass = RangeSlider(start=0,
                   end=20,
                   value=(0, 20),
                   value_throttled=(0, 20),
                   step=1,
                   title=list(axis_map.keys())[0],
                   format=FuncTickFormatter(code=mass_ticker))
# rad  = RangeSlider(start=lim_rad[0], end=lim_rad[1], value=lim_rad,value_throttled=lim_rad, step=.1, title=list(axis_map.keys())[1])
rad = RangeSlider(start=0,
                  end=16,
                  value=(0, 16),
                  value_throttled=(0, 16),
                  step=1,
                  title=list(axis_map.keys())[1],
                  format=FuncTickFormatter(code=radius_ticker))
# per  = RangeSlider(start=lim_per[0], end=lim_per[1], value=lim_per, step=.1, title=list(axis_map.keys())[2])
per = RangeSlider(
    start=-1.5,
    end=4,
    value=[-1.5, 4],
    value_throttled=[-1.5, 4],
    step=.01,
Beispiel #10
0
    def __init__(
        self,
        name,
        xmin,
        xmax,
        mu,
        sigma,
        pdf,
        gp_callback,
        throttle,
    ):
        # Store
        self.pdf = pdf
        self.gp_callback = gp_callback

        # Arrays
        x = np.linspace(xmin, xmax, 300)
        y = self.pdf(x, mu["value"], sigma["value"])
        self.source = ColumnDataSource(data=dict(x=x, y=y))

        # Plot them
        dx = (xmax - xmin) * 0.01
        self.plot = figure(
            plot_width=400,
            plot_height=400,
            sizing_mode="stretch_height",
            toolbar_location=None,
            x_range=(xmin - dx, xmax + dx),
            title="{} distribution".format(name),
        )
        self.plot.title.align = "center"
        self.plot.title.text_font_size = "14pt"
        self.plot.line("x",
                       "y",
                       source=self.source,
                       line_width=3,
                       line_alpha=0.6)
        self.plot.xaxis.axis_label = name
        self.plot.xaxis.axis_label_text_font_style = "normal"
        self.plot.xaxis.axis_label_text_font_size = "12pt"
        self.plot.yaxis[0].formatter = FuncTickFormatter(code="return '  ';")
        self.plot.yaxis.axis_label = "probability"
        self.plot.yaxis.axis_label_text_font_style = "normal"
        self.plot.yaxis.axis_label_text_font_size = "12pt"
        self.plot.outline_line_width = 1
        self.plot.outline_line_alpha = 1
        self.plot.outline_line_color = "black"

        # Sliders
        self.slider_mu = Slider(
            start=mu["start"],
            end=mu["stop"],
            step=mu["step"],
            value=mu["value"],
            orientation="vertical",
            format="0.3f",
            css_classes=["custom-slider"],
            callback_policy="throttle",
            callback_throttle=throttle,
        )
        self.slider_mu.on_change("value_throttled", self.callback)
        self.slider_sigma = Slider(
            start=sigma["start"],
            end=sigma["stop"],
            step=sigma["step"],
            value=sigma["value"],
            orientation="vertical",
            format="0.3f",
            css_classes=["custom-slider"],
            name="sigma",
            callback_policy="throttle",
            callback_throttle=throttle,
        )
        self.slider_sigma.on_change("value_throttled", self.callback)

        # Show mean and std. dev.
        self.mean_vline = Span(
            location=self.slider_mu.value,
            dimension="height",
            line_color="black",
            line_width=1,
            line_dash="dashed",
        )
        self.std_vline1 = Span(
            location=self.slider_mu.value - self.slider_sigma.value,
            dimension="height",
            line_color="black",
            line_width=1,
            line_dash="dotted",
        )
        self.std_vline2 = Span(
            location=self.slider_mu.value + self.slider_sigma.value,
            dimension="height",
            line_color="black",
            line_width=1,
            line_dash="dotted",
        )
        self.plot.renderers.extend(
            [self.mean_vline, self.std_vline1, self.std_vline2])

        # Full layout
        self.layout = row(
            self.plot,
            column(svg_mu(), self.slider_mu, margin=(10, 10, 10, 10)),
            column(svg_sigma(), self.slider_sigma, margin=(10, 10, 10, 10)),
        )
Beispiel #11
0
def app(doc, hist_storage_, data_storage_, freq_storage_, depolarizer, names):

    # вспомогательные глобальные

    data_source = ColumnDataSource({key: [] for key in names})
    fit_handler = {
        "fit_line": None,
        "input_fields": {},
        "fit_indices": tuple()
    }
    utc_plus_7h = 7 * 3600
    time_coef = 10**3  # Пересчёт времени в мс для формата datetime Bokeh
    fit_line_points_amount = 300  # Количество точек для отрисовки подгоночной кривой
    depol_list = []

    datetime_formatter = DatetimeTickFormatter(
        milliseconds=['%M:%S:%3Nms'],
        seconds=['%H:%M:%S'],
        minsec=['%H:%M:%S'],
        minutes=['%H:%M:%S'],
        hourmin=['%H:%M:%S'],
        hours=['%H:%M:%S'],
        days=["%d.%m"],
        months=["%Y-%m-%d"],
    )

    # Гистограмма пятна
    img, img_x_std, img_y_std = hist_storage_.get_hist_with_std()
    hist_source = ColumnDataSource(data=dict(image=[img]))
    width_ = config.GEM_X * 5
    hist_height_ = config.GEM_Y * 5
    hist_fig = figure(plot_width=width_,
                      plot_height=hist_height_,
                      x_range=(0, config.GEM_X),
                      y_range=(0, config.GEM_Y))

    hist_fig.image(image='image',
                   x=0,
                   y=0,
                   dw=config.GEM_X,
                   dh=config.GEM_Y,
                   palette="Spectral11",
                   source=hist_source)

    hist_label = Label(
        x=0,
        y=0,
        x_units='screen',
        y_units='screen',
        text=f"x_std={'%.2f' % img_x_std},y_std={'%.2f' % img_y_std}",
        render_mode='css',
        border_line_color='black',
        border_line_alpha=1.0,
        background_fill_color='white',
        background_fill_alpha=1.0)

    hist_fig.add_layout(hist_label)

    hist_buffer_len = config.hist_buffer_len - 1
    hist_slider = RangeSlider(start=0,
                              end=hist_buffer_len,
                              value=(0, hist_buffer_len),
                              step=1,
                              title="Срез пятна (от..до) сек назад")

    def hist_update():
        img, img_x_std, img_y_std = hist_storage_.get_hist_with_std(
            hist_buffer_len - hist_slider.value[1],
            hist_buffer_len - hist_slider.value[0])
        hist_label.text = f"x_std={'%.2f' % img_x_std},y_std={'%.2f' % img_y_std}"
        hist_source.data = {'image': [img]}

    # График асимметрии

    asym_fig = figure(
        plot_width=width_,
        plot_height=400,
        tools="box_zoom, xbox_select, wheel_zoom, pan, save, reset",
        active_scroll="wheel_zoom",
        active_drag="pan",
        toolbar_location="below",
        lod_threshold=100,
        x_axis_location=None,
        x_range=DataRange1d())

    asym_fig.yaxis.axis_label = "мм"
    asym_fig.extra_x_ranges = {
        "time_range": asym_fig.x_range,
        "depolarizer": asym_fig.x_range,
        "sec": asym_fig.x_range
    }

    depol_axis = LinearAxis(x_range_name="depolarizer",
                            axis_label='Деполяризатор',
                            major_label_overrides={},
                            major_label_orientation=pi / 2)

    asym_fig.add_layout(
        LinearAxis(x_range_name="time_range",
                   axis_label='Время',
                   formatter=datetime_formatter), 'below')

    # Прямая, с которой идёт отсчёт времени для подгонки
    zone_of_interest = Span(location=0,
                            dimension='height',
                            line_color='green',
                            line_dash='dashed',
                            line_width=3)

    sec_axis = LinearAxis(
        x_range_name='sec',
        axis_label='Секунды')  # Секундная ось сверху (настр. диапазон)
    sec_axis.formatter = FuncTickFormatter(
        code=
        f"return ((tick - {zone_of_interest.location}) / {time_coef}).toFixed(1);"
    )

    def double_tap(event):
        """Двойной клик для перемещения отсчёта времени для подгонки"""
        zone_of_interest.location = event.x
        sec_axis.formatter = FuncTickFormatter(
            code=f"return ((tick - {event.x}) / {time_coef}).toFixed(1);")

    asym_fig.add_layout(depol_axis, 'below')
    asym_fig.add_layout(sec_axis, 'above')
    asym_fig.add_layout(zone_of_interest)
    asym_fig.on_event(DoubleTap, double_tap)

    def draw_selected_area(attr, old, new):
        """Подсветка выделенной для подгонки области"""

        # Удаляет предыдущую выделенную область
        asym_fig.renderers = [
            r for r in asym_fig.renderers if r.name != 'fit_zone'
        ]

        if not new.indices:
            fit_handler["fit_indices"] = tuple()
            return

        l_time_ = data_source.data['time'][min(new.indices)]
        r_time_ = data_source.data['time'][max(new.indices)]

        if l_time_ != r_time_:
            fit_handler["fit_indices"] = (l_time_, r_time_)
            box_select = BoxAnnotation(left=l_time_,
                                       right=r_time_,
                                       name="fit_zone",
                                       fill_alpha=0.1,
                                       fill_color='red')
            asym_fig.add_layout(box_select)

    asym_box_select_overlay = asym_fig.select_one(BoxSelectTool).overlay
    asym_box_select_overlay.line_color = "firebrick"

    data_source.on_change('selected', draw_selected_area)

    def create_whisker(data_name: str):
        """ Создает усы для data_name от time

        :param data_name: имя поля данных из data_storage
                (у данных должны быть поля '_up_error', '_down_error')
        :return: Bokeh Whisker
        """
        return Whisker(source=data_source,
                       base="time",
                       upper=data_name + "_up_error",
                       lower=data_name + "_down_error")

    def create_render(data_name: str, glyph: str, color: str):
        """ Рисует data_name от time

        :param data_name: имя поля данных из data_storage
        :param glyph: ['circle', 'square']
        :param color: цвет
        :return: Bokeh fig
        """
        if glyph == 'circle':
            func = asym_fig.circle
        elif glyph == 'square':
            func = asym_fig.square
        else:
            raise ValueError('Неверное значение glyph')
        return func('time',
                    data_name,
                    source=data_source,
                    name=data_name,
                    color=color,
                    nonselection_alpha=1,
                    nonselection_color=color)

    # Список линий на графике асимметрии: data_name, name, glyph, color
    asym_renders_name = [('y_one_asym', 'ΔY ONE', 'circle', 'black'),
                         ('y_cog_asym', 'ΔY COG', 'circle', 'green'),
                         ('x_one_asym', 'ΔX ONE', 'square', 'black'),
                         ('x_cog_asym', 'ΔX COG', 'square', 'green')]

    pretty_names = dict([(data_name, name)
                         for data_name, name, *_ in asym_renders_name])
    asym_renders = [
        create_render(data_name, glyph, color)
        for data_name, _, glyph, color in asym_renders_name
    ]
    asym_error_renders = [
        create_whisker(data_name) for data_name, *_ in asym_renders_name
    ]

    for render, render_error in zip(asym_renders, asym_error_renders):
        asym_fig.add_layout(render_error)
        render.js_on_change(
            'visible',
            CustomJS(args=dict(x=render_error),
                     code="x.visible = cb_obj.visible"))

    asym_fig.add_layout(
        Legend(items=[(pretty_names[r.name], [r]) for r in asym_renders],
               click_policy="hide",
               location="top_left",
               background_fill_alpha=0.2,
               orientation="horizontal"))

    # Вывод информации о точке при наведении мыши
    asym_fig.add_tools(
        HoverTool(
            renderers=asym_renders,
            formatters={"time": "datetime"},
            mode='vline',
            tooltips=[
                ("Время", "@time{%F %T}"),
                *[(pretty_names[r.name],
                   f"@{r.name}{'{0.000}'} ± @{r.name + '_error'}{'{0.000}'}")
                  for r in asym_renders],
                ("Деполяризатор", f"@depol_energy{'{0.000}'}")
            ]))

    # Окно ввода периода усреднения
    period_input = TextInput(value='300', title="Время усреднения (с):")

    # Глобальный список параметров, для сохранения результатов запросов к data_storage
    params = {'last_time': 0, 'period': int(period_input.value)}

    def update_data():
        """
        Обновляет данные для пользовательского интерфейса, собирая их у data_storage
        """
        if params['period'] != int(period_input.value):
            data_source.data = {name: [] for name in names}
            params['period'] = int(period_input.value)
            params['last_time'] = 0
            depol_axis.ticker = []
            depol_axis.major_label_overrides.clear()
            depol_list.clear()

        points, params['last_time'] = data_storage_.get_mean_from(
            params['last_time'], params['period'])

        if not points['time']:
            return

        points['time'] = [(i + utc_plus_7h) * time_coef for i in points['time']
                          ]  # Учёт сдвижки UTC+7 для отрисовки

        for time, energy in zip(points['time'], points['depol_energy']):
            if energy == 0:
                continue
            depol_axis.major_label_overrides[time] = str(energy)
            depol_list.append(time)

        depol_axis.ticker = depol_list  # TODO: оптимизировать
        data_source.stream({key: np.array(val)
                            for key, val in points.items()},
                           rollover=250)

    def period_correction_func(attr, old, new):
        """Проверка введенного значения на целое число больше нуля"""
        if not new.isdigit() or int(new) <= 0:
            period_input.value = old

    period_input.on_change('value', period_correction_func)

    # Создание панели графиков (вкладок)

    def create_fig(data_names: list,
                   colors: list,
                   y_axis_name: str,
                   ers: str = None):
        """Создаёт график data_names : time. Если в data_names несколько имён,
        то они будут на одном графике. Возвращает fig.

        :param data_names: список с именами полей данных из data_storage
        :param colors: список цветов, соотв. элементам из fig_names
        :param y_axis_name: имя оси Y
        :param ers: 'err', 'pretty' --- вид усов (у данных должны быть поля '_up_error', '_down_error'),
                       'err' --- усы обыкновенные
                       'pretty' --- усы без шляпки и цветом совпадающим с цветом точки
        :return fig --- Bokeh figure
        """

        if len(data_names) != len(colors):
            raise IndexError('Кол-во цветов и графиков не совпадает')

        fig = figure(plot_width=width_,
                     plot_height=300,
                     tools="box_zoom, wheel_zoom, pan, save, reset",
                     active_scroll="wheel_zoom",
                     lod_threshold=100,
                     x_axis_type="datetime")

        for fig_name, color in zip(data_names, colors):

            if ers == 'err':
                fig.add_layout(
                    Whisker(source=data_source,
                            base="time",
                            upper=fig_name + '_up_error',
                            lower=fig_name + '_down_error'))
            elif ers == 'pretty':
                fig.add_layout(
                    Whisker(source=data_source,
                            base="time",
                            upper=fig_name + '_up_error',
                            lower=fig_name + '_down_error',
                            line_color=color,
                            lower_head=None,
                            upper_head=None))

            fig.circle('time',
                       fig_name,
                       source=data_source,
                       size=5,
                       color=color,
                       nonselection_alpha=1,
                       nonselection_color=color)

        fig.yaxis.axis_label = y_axis_name
        fig.xaxis.axis_label = 'Время'
        fig.xaxis.formatter = datetime_formatter
        fig.x_range = asym_fig.x_range

        return fig

    figs = [(create_fig(['y_one_l'], ['black'], 'Y [мм]', 'err'), 'Y ONE L'),
            (create_fig(['y_one_r'], ['black'], 'Y [мм]', 'err'), 'Y ONE R'),
            (create_fig(['y_cog_l'], ['black'], 'Y [мм]', 'err'), 'Y COG L'),
            (create_fig(['y_cog_r'], ['black'], 'Y [мм]', 'err'), 'Y COG R'),
            (create_fig(['rate' + i for i in ['_l', '_r']], ['red', 'blue'],
                        'Усл. ед.', 'pretty'), 'Rate'),
            (create_fig(['corrected_rate' + i for i in ['_l', '_r']],
                        ['red', 'blue'], 'Усл. ед.', 'pretty'), 'Corr. rate'),
            (create_fig(['delta_rate'], ['black'], 'Корр. лев. - корр. пр.',
                        'err'), 'Delta corr. rate'),
            (create_fig(['charge'], ['blue'], 'Ед.'), 'Charge')]

    tab_handler = Tabs(
        tabs=[Panel(child=fig, title=fig_name) for fig, fig_name in figs],
        width=width_)

    # Окно статуса деполяризатора

    depol_status_window = Div(text="Инициализация...", width=500, height=500)

    depol_start_stop_buttons = RadioButtonGroup(
        labels=["Старт", "Стоп"], active=(0 if depolarizer.is_scan else 1))

    fake_depol_button = Button(label="Деполяризовать", width=200)
    fake_depol_button.on_click(GEM.depolarize)

    depol_input_harmonic_number = TextInput(value=str(
        '%.1f' % depolarizer.harmonic_number),
                                            title=f"Номер гармоники",
                                            width=150)

    depol_input_attenuation = TextInput(value=str('%.1f' %
                                                  depolarizer.attenuation),
                                        title=f"Аттенюатор (дБ)",
                                        width=150)

    depol_input_speed = TextInput(
        value=str(depolarizer.frequency_to_energy(depolarizer.speed, n=0)),
        title=f"Скорость ({'%.1f' % depolarizer.speed} Гц):",
        width=150)

    depol_input_step = TextInput(
        value=str(depolarizer.frequency_to_energy(depolarizer.step, n=0)),
        title=f"Шаг ({'%.1f' % depolarizer.step} Гц):",
        width=150)

    depol_input_initial = TextInput(
        value=str(depolarizer.frequency_to_energy(depolarizer.initial)),
        title=f"Начало ({'%.1f' % depolarizer.initial} Гц):",
        width=150)

    depol_input_final = TextInput(
        value=str(depolarizer.frequency_to_energy(depolarizer.final)),
        title=f"Конец ({'%.1f' % depolarizer.final} Гц):",
        width=150)

    depol_dict = {
        "speed": (depol_input_speed, depolarizer.set_speed),
        "step": (depol_input_step, depolarizer.set_step),
        "initial": (depol_input_initial, depolarizer.set_initial),
        "final": (depol_input_final, depolarizer.set_final),
        "harmonic_number":
        (depol_input_harmonic_number, depolarizer.set_harmonic_number),
        "attenuation": (depol_input_attenuation, depolarizer.set_attenuation)
    }

    def change_value_generator(value_name):
        """Возвращает callback функцию для параметра value_name деполяризатора"""
        def change_value(attr, old, new):
            if float(old) == float(new):
                return

            depol_input, depol_set = depol_dict[value_name]
            depol_current = depolarizer.get_by_name(value_name)
            try:
                if value_name in ['harmonic_number', 'attenuation']:
                    new_val = float(new)
                elif value_name in ['speed', 'step']:
                    new_val = depolarizer.energy_to_frequency(float(new), n=0)
                else:
                    new_val = depolarizer.energy_to_frequency(float(new))

                if depol_current == new_val:
                    return

                depol_set(new_val)
                if value_name not in ['harmonic_number', 'attenuation']:
                    name = depol_input.title.split(' ')[0]
                    depol_input.title = name + f" ({'%.1f' % new_val} Гц):"

            except ValueError as e:
                if value_name in ['harmonic_number', 'attenuation']:
                    depol_input.value = str(depol_current)
                elif value_name in ['speed', 'step']:
                    depol_input.value = str(
                        depolarizer.frequency_to_energy(depol_current, n=0))
                else:
                    depol_input.value = str(
                        depolarizer.frequency_to_energy(depol_current))
                print(e)

        return change_value

    depol_input_harmonic_number.on_change(
        'value', change_value_generator('harmonic_number'))
    depol_input_attenuation.on_change('value',
                                      change_value_generator("attenuation"))
    depol_input_speed.on_change('value', change_value_generator("speed"))
    depol_input_step.on_change('value', change_value_generator("step"))
    depol_input_initial.on_change('value', change_value_generator("initial"))
    depol_input_final.on_change('value', change_value_generator("final"))

    def update_depol_status(
    ):  # TODO: самому пересчитывать начало и конец сканирования по частотам
        """Обновляет статус деполяризатора,
        если какое-то значение поменялось другим пользователем"""
        depol_start_stop_buttons.active = 0 if depolarizer.is_scan else 1

        depol_status_window.text = f"""
<p>Сканирование: 
<font color={'"green">включено' if depolarizer.is_scan else '"red">выключено'}</font></p>
<p/>Частота {"%.1f" % depolarizer.current_frequency} (Гц)</p>
<p/>Энергия {"%.3f" % depolarizer.current_energy} МэВ</p>"""

        for value_name in ['speed', 'step']:
            depol_input, _ = depol_dict[value_name]
            depol_value = depolarizer.frequency_to_energy(
                depolarizer.get_by_name(value_name), n=0)
            if float(depol_input.value) != depol_value:
                depol_input.value = str(depol_value)

        for value_name in ['initial', 'final']:
            depol_input, _ = depol_dict[value_name]
            freq = depolarizer.get_by_name(value_name)
            energy = depolarizer.frequency_to_energy(freq)
            if float(depol_input.value) != energy:
                depol_input.value = str(energy)
            else:
                name = depol_input.title.split(' ')[0]
                depol_input.title = name + f" ({'%.1f' % freq} Гц):"

        for value_name in ['attenuation', 'harmonic_number']:
            depol_input, _ = depol_dict[value_name]
            depol_value = depolarizer.get_by_name(value_name)
            if float(depol_input.value) != depol_value:
                depol_input.value = str(int(depol_value))

    depol_start_stop_buttons.on_change(
        "active", lambda attr, old, new:
        (depolarizer.start_scan() if new == 0 else depolarizer.stop_scan()))

    # Подгонка

    fit_line_selection_widget = Select(title="Fitting line:",
                                       width=200,
                                       value=asym_renders[0].name,
                                       options=[(render.name,
                                                 pretty_names[render.name])
                                                for render in asym_renders])

    options = [name for name in fit.function_handler.keys()]
    if not options:
        raise IndexError("Пустой function_handler в fit.py")

    fit_function_selection_widget = Select(title="Fitting function:",
                                           value=options[0],
                                           options=options,
                                           width=200)

    fit_button = Button(label="FIT", width=200)

    def make_parameters_table():
        """Создание поля ввода данных для подгонки: начальное значение, fix и т.д."""
        name = fit_function_selection_widget.value

        t_width = 10
        t_height = 12

        rows = [
            row(Paragraph(text="name", width=t_width, height=t_height),
                Paragraph(text="Fix", width=t_width, height=t_height),
                Paragraph(text="Init value", width=t_width, height=t_height),
                Paragraph(text="step (error)", width=t_width, height=t_height),
                Paragraph(text="limits", width=t_width, height=t_height),
                Paragraph(text="lower_limit", width=t_width, height=t_height),
                Paragraph(text="upper_limit", width=t_width, height=t_height))
        ]

        fit_handler["input_fields"] = {}

        for param, value in fit.get_function_params(name):
            fit_handler["input_fields"][param] = {}
            fit_handler["input_fields"][param]["fix"] = CheckboxGroup(
                labels=[""], width=t_width, height=t_height)
            fit_handler["input_fields"][param]["Init value"] = TextInput(
                width=t_width, height=t_height, value=str(value))
            fit_handler["input_fields"][param]["step (error)"] = TextInput(
                width=t_width, height=t_height, value='1')
            fit_handler["input_fields"][param]["limits"] = CheckboxGroup(
                labels=[""], width=t_width, height=t_height)
            fit_handler["input_fields"][param]["lower_limit"] = TextInput(
                width=t_width, height=t_height)
            fit_handler["input_fields"][param]["upper_limit"] = TextInput(
                width=t_width, height=t_height)

            rows.append(
                row(Paragraph(text=param, width=t_width, height=t_height),
                    fit_handler["input_fields"][param]["fix"],
                    fit_handler["input_fields"][param]["Init value"],
                    fit_handler["input_fields"][param]["step (error)"],
                    fit_handler["input_fields"][param]["limits"],
                    fit_handler["input_fields"][param]["lower_limit"],
                    fit_handler["input_fields"][param]["upper_limit"]))

        return column(rows)

    def clear_fit():
        """Удаление подогнанной кривой"""
        if fit_handler["fit_line"] in asym_fig.renderers:
            asym_fig.renderers.remove(fit_handler["fit_line"])

    energy_window = Div(text="Частота: , энергия: ")
    clear_fit_button = Button(label="Clear", width=200)
    clear_fit_button.on_click(clear_fit)

    def fit_callback():
        if not fit_handler["fit_indices"]:
            return

        name = fit_function_selection_widget.value
        line_name = fit_line_selection_widget.value

        left_time_, right_time_ = fit_handler["fit_indices"]

        left_ind_ = bisect.bisect_left(data_source.data['time'], left_time_)
        right_ind_ = bisect.bisect_right(data_source.data['time'],
                                         right_time_,
                                         lo=left_ind_)

        if left_ind_ == right_ind_:
            return

        clear_fit()

        x_axis = data_source.data['time'][left_ind_:right_ind_]
        y_axis = data_source.data[line_name][left_ind_:right_ind_]
        y_errors = data_source.data[line_name +
                                    '_up_error'][left_ind_:right_ind_] - y_axis

        init_vals = {
            name: float(val["Init value"].value)
            for name, val in fit_handler["input_fields"].items()
        }

        steps = {
            "error_" + name: float(val["step (error)"].value)
            for name, val in fit_handler["input_fields"].items()
        }

        fix_vals = {
            "fix_" + name: True
            for name, val in fit_handler["input_fields"].items()
            if val["fix"].active
        }

        limit_vals = {
            "limit_" + name:
            (float(val["lower_limit"].value), float(val["upper_limit"].value))
            for name, val in fit_handler["input_fields"].items()
            if val["limits"].active
        }

        kwargs = {}
        kwargs.update(init_vals)
        kwargs.update(steps)
        kwargs.update(fix_vals)
        kwargs.update(limit_vals)

        # Предобработка времени, перевод в секунды, вычитание сдвига (для лучшей подгонки)
        left_ = zone_of_interest.location
        x_time = x_axis - left_  # Привёл время в интервал от 0
        x_time /= time_coef  # Перевёл в секунды

        # Создание точек, которые передадутся в подогнанную функцию с параметрами,
        # и точек, которые соответсвуют реальным временам на графике (т.е. без смещения к 0)

        fit_line_real_x_axis = np.linspace(left_time_, right_time_,
                                           fit_line_points_amount)
        fit_line_x_axis = fit_line_real_x_axis - left_
        fit_line_x_axis /= time_coef

        m = fit.create_fit_func(name, x_time, y_axis, y_errors, kwargs)

        fit.fit(m)
        params_ = m.get_param_states()
        for param in params_:
            fit_handler["input_fields"][
                param['name']]["Init value"].value = "%.3f" % param['value']
            fit_handler["input_fields"][
                param['name']]["step (error)"].value = "%.3f" % param['error']
            if param['name'] == "depol_time":
                freq = freq_storage_.find_closest_freq(param['value'] +
                                                       left_ / time_coef -
                                                       utc_plus_7h)
                freq_error = abs(depolarizer.speed * param['error'])
                energy = depolarizer.frequency_to_energy(
                    freq) if freq != 0 else 0
                energy_error = depolarizer.frequency_to_energy(
                    freq_error, depolarizer._F0, 0)
                energy_window.text = "<p>Частота: %8.1f +- %.1f Hz,</p> <p>Энергия: %7.3f +- %.1f МэВ</p>" % (
                    freq, freq_error, energy, energy_error)

        fit_handler["fit_line"] = asym_fig.line(
            fit_line_real_x_axis,
            fit.get_line(name, fit_line_x_axis, [x['value'] for x in params_]),
            color="red",
            line_width=2)

    fit_button.on_click(fit_callback)

    # Инициализация bokeh app, расположение виджетов
    column_1 = column(gridplot([tab_handler], [asym_fig], merge_tools=False),
                      period_input,
                      width=width_ + 50)
    widgets_ = WidgetBox(depol_start_stop_buttons, depol_input_harmonic_number,
                         depol_input_attenuation, depol_input_speed,
                         depol_input_step, depol_input_initial,
                         depol_input_final, depol_status_window)

    row_21 = column(hist_fig, hist_slider)
    column_21 = column(widgets_)
    if config.GEM_idle:
        column_22 = column(fit_button, clear_fit_button, fake_depol_button,
                           fit_line_selection_widget,
                           fit_function_selection_widget, energy_window,
                           make_parameters_table())
        make_parameters_table_id = 6
    else:
        column_22 = column(fit_button, clear_fit_button,
                           fit_line_selection_widget,
                           fit_function_selection_widget, energy_window,
                           make_parameters_table())
        make_parameters_table_id = 5

    def rebuild_table(attr, old, new):
        column_22.children[make_parameters_table_id] = make_parameters_table()

    fit_function_selection_widget.on_change("value", rebuild_table)

    row_22 = row(column_21, column_22)
    column_2 = column(row_21, row_22, width=width_ + 50)
    layout_ = layout([[column_1, column_2]])

    # Настройка документа Bokeh

    update_data()
    doc.add_periodic_callback(hist_update,
                              1000)  # TODO запихнуть в один callback
    doc.add_periodic_callback(update_data, 1000)  # TODO: подобрать периоды
    doc.add_periodic_callback(update_depol_status, 1000)
    doc.title = "Laser polarimeter"
    doc.add_root(layout_)
Beispiel #12
0
 def double_tap(event):
     """Двойной клик для перемещения отсчёта времени для подгонки"""
     zone_of_interest.location = event.x
     sec_axis.formatter = FuncTickFormatter(
         code=f"return ((tick - {event.x}) / {time_coef}).toFixed(1);")
Beispiel #13
0
def make_plots(df_full, df_plot):
    """Builds the Bokeh plot dashboard.

    Uses the Bokeh package to create the plots in the dashboard.
    Client-side html interactivity is provided by Bokeh's Javascript callbacks.

    Required arguments:
    df_full -- dict or pd.dataframe -- containing the whole NYT dataset
    df_plot -- dict or pd.dataframe -- contains a plottable subset of dataset
    Both objects will be passed as inputs to ColumnDataSources
    The expected keys/columns of df_full are:
    date, state, county, cases, deaths
    The expected keys/columns in each element of df_plot are:
    date, cases, deaths, cobweb_cases, cobweb_deaths

    Returns:
    bokeh.layouts.grid -- can be saved or used in jupyter notebook
    """
    ### Begin Shared objects

    # Column Data Source (which can be filtered later)
    CDS_full = ColumnDataSource(df_full)

    # Shared options
    states = sorted(list(set(CDS_full.data['state'])))
    counties = sorted(list(set(CDS_full.data['county'])))
    metrics = ['cases', 'deaths']

    # Shared Widgets
    button = Button(label='Synchronize', button_type="success", sizing_mode='stretch_width')
    roll_avg = Slider(title='Rolling Average', value=1, start=1, end=14, step=1, sizing_mode='stretch_width')

    ### End shared objects


    ### Begin combined plots

    # Create lists indexed by plot

    CDS_plots = []

    scale_menus = []
    state_menus = []
    county_menus = []
    metric_menus = []
    method_menus = []
    widget_lists = []
    widget_layouts = []

    linear_plots = []
    log_plots = []
    cobweb_plots = []
    linear_panels = []
    log_panels = []
    cobweb_panels = []
    plot_lists = []
    panel_lists = []
    tab_lists = []

    update_menus = []
    update_datas = []

    # Create a plot for the desired number of plots
    N = 2
    # If N != 2, the following items are affected:
    # metrics, since loop excepts len(metrics) >= N
    # CustomJS for the synchronize button
    # since it adds no functionality if N=1
    # and if N > 2 needs to make more updates for other plots
    for i in range(N):

        # Initial plot data

        CDS_plots.append(
            ColumnDataSource(
                {
                'date' : df_plot['date'],
                'metric' : df_plot[metrics[i]],
                'cobweb' : df_plot['cobweb_' + metrics[i]],
                }
            )
        )

        # Widgets

        scale_menus.append(
            Select(
                title='Scale ' + str(i + 1),
                value='national',
                options=['national', 'state', 'county'],
            )
        )
        state_menus.append(
            Select(
                title='State ' + str(i + 1),
                value=states[0],
                options=states,
                visible=False,
            )
        )
        county_menus.append(
            Select(
                title='County ' + str(i + 1),
                value=counties[0],
                options=counties,
                visible=False,
            )
        )
        metric_menus.append(
            Select(
                title="Metric " + str(i + 1),
                value=metrics[i],
                options=metrics,
            )
        )
        method_menus.append(
            Select(
                title="Method " + str(i + 1),
                value='cumulative',
                options=['cumulative', 'difference'],
            )
        )
        widget_lists.append(
            [
            scale_menus[i],
            state_menus[i],
            county_menus[i],
            metric_menus[i],
            method_menus[i],
            ]
        )
        widget_layouts.append(
            [
            [method_menus[i], metric_menus[i]],
            [scale_menus[i], state_menus[i], county_menus[i]],
            ]
        )

        # Create plot layout
        # linear plot
        linear_plots.append(
            figure(
                title='NYT COVID-19 data: National',
                x_axis_label='Date',
                y_axis_label='Cumulative cases',
                x_axis_type='datetime',
                y_axis_type='linear',
            )
        )
        linear_plots[i].yaxis.formatter = FuncTickFormatter(code="return tick.toExponential();")
        linear_plots[i].line(x='date', y='metric', source=CDS_plots[i])
        linear_panels.append(
            Panel(
                child=linear_plots[i],
                title='linear',
            )
        )
        # log plot
        log_plots.append(
            figure(
                title='NYT COVID-19 data: National',
                x_axis_label='Date',
                y_axis_label='Cumulative cases',
                x_axis_type='datetime',
                y_axis_type='log',
            )
        )
        log_plots[i].line(x='date', y='metric', source=CDS_plots[i])
        log_panels.append(
            Panel(
                child=log_plots[i],
                title='log',
            )
        )
        # cobweb plot
        cobweb_plots.append(
            figure(
                title='NYT COVID-19 data: National',
                x_axis_label='Cumulative cases today',
                y_axis_label='Cumulative cases tomorrow',
                x_axis_type='linear',
                y_axis_type='linear',
            )
        )
        cobweb_plots[i].xaxis.formatter = FuncTickFormatter(code="return tick.toExponential();")
        cobweb_plots[i].yaxis.formatter = FuncTickFormatter(code="return tick.toExponential();")
        cobweb_plots[i].step(x='cobweb', y='metric', source=CDS_plots[i])
        cobweb_plots[i].line(x='cobweb', y='cobweb', source=CDS_plots[i], line_color='red')
        cobweb_panels.append(
            Panel(
                child=cobweb_plots[i],
                title='cobweb',
            )
        )
        # collect plots, panels, tabs
        plot_lists.append(
            [
            linear_plots[i],
            log_plots[i],
            cobweb_plots[i],
            ]
        )
        panel_lists.append(
            [
            linear_panels[i],
            log_panels[i],
            cobweb_panels[i],
            ]
        )
        tab_lists.append(
            Tabs(tabs=panel_lists[i])
        )

        # Construct callback functions
        update_menus.append(
            CustomJS(
                args=dict(
                    scale=scale_menus[i],
                    state=state_menus[i],
                    county=county_menus[i],
                    source=CDS_full,
                ),
                code="""
                    if (scale.value === 'national') {
                        state.visible = false
                        county.visible = false
                    }

                    else if (scale.value === 'state') {
                        state.visible = true
                        county.visible = false
                    }

                    else if (scale.value === 'county') {
                        state.visible = true
                        county.visible = true

                        // filter the state and then unique counties
                        function oneState(value, index, self) {
                            return source.data['state'][index] === state.value
                        }

                        function onlyUnique(value, index, self) {
                            return self.indexOf(value) === index;
                        }

                        let counties_in_state = source.data['county'].filter(oneState).filter(onlyUnique).sort()

                        if (counties_in_state.indexOf(county.value) === -1) {
                            county.value = counties_in_state[0]
                        }
                        county.options = counties_in_state
                    }
                """
            )
        )
        update_datas.append(
            CustomJS(
                args=dict(
                    metric=metric_menus[i],
                    method=method_menus[i],
                    scale=scale_menus[i],
                    state=state_menus[i],
                    county=county_menus[i],
                    plot=CDS_plots[i],
                    source=CDS_full,
                    avg=roll_avg,
                    linear_title=linear_plots[i].title,
                    linear_x=linear_plots[i].xaxis[0],
                    linear_y=linear_plots[i].yaxis[0],
                    log_title=log_plots[i].title,
                    log_x=log_plots[i].xaxis[0],
                    log_y=log_plots[i].yaxis[0],
                    cobweb_title=cobweb_plots[i].title,
                    cobweb_x=cobweb_plots[i].xaxis[0],
                    cobweb_y=cobweb_plots[i].yaxis[0],
                ),
                code="""
                    let plot_x = []
                    let plot_y = []
                    let plot_z = []
                    let nDays = -1
                    let yesterdate = 0

                    function mask (x) {
                        if (scale.value === 'national') {
                            return true
                        }
                        else if (scale.value === 'state') {
                            return source.data['state'][x] === state.value
                        }
                        else { // if (scale.value === 'county') {
                            return source.data['county'][x] === county.value
                        }
                    }

                    // this works because it knows the dates are in increasing order
                    for (let i=0; i < source.data['date'].length; i++) {
                        if (mask(i)) { // filter by scale
                            if (yesterdate < source.data['date'][i]) {
                                plot_x.push(source.data['date'][i])
                                plot_y.push(source.data[metric.value][i])
                                yesterdate = source.data['date'][i]
                                nDays += 1
                            }
                            else { // aggregate values with the same date
                                plot_y[nDays] += source.data[metric.value][i]
                            }
                        }
                    }

                    // Extra transformations (edge cases are the first few days)
                    // Except for edge cases, you can show that the order of
                    // difference and average doesn't matter
                    if (method.value === 'difference') {
                        // Converts from raw cumulative data
                        for (let i=plot_x.length-1; i > 0; i--) {
                            plot_y[i] -= plot_y[i-1]
                        }
                    }
                    // Rolling Average (uniform backwards window (avg over last x days))
                    if (avg.value > 1) {
                        for (let i=plot_x.length-1; i > avg.value-1; i--) {
                            plot_y[i] = plot_y.slice(i-avg.value, i+1).reduce((a, b) => a + b, 0) / (avg.value+1)
                        }
                    }

                    // cobweb plotting
                    plot_z = plot_y.slice()
                    plot_z.pop()
                    plot_z.unshift(0)

                    // update ColumnDataSource
                    plot.data['date'] = plot_x
                    plot.data['metric'] = plot_y
                    plot.data['cobweb'] = plot_z
                    plot.change.emit()

                    // Update plot labels
                    if (scale.value === 'national') {
                        linear_title.text = 'NYT COVID-19 data: National'
                        log_title.text = 'NYT COVID-19 data: National'
                        cobweb_title.text = 'NYT COVID-19 data: National'
                    }
                    else if (scale.value === 'state') {
                        linear_title.text = 'NYT COVID-19 data: State: '+ state.value
                        log_title.text = 'NYT COVID-19 data: State: ' + state.value
                        cobweb_title.text = 'NYT COVID-19 data: State: ' + state.value
                    }
                    else { // if (scale.value === 'county') {
                        linear_title.text = 'NYT COVID-19 data: County: ' + county.value
                        log_title.text = 'NYT COVID-19 data: County: ' + county.value
                        cobweb_title.text = 'NYT COVID-19 data: County: ' + state.value
                    }

                    let method_name =''
                    if (method.value === 'difference') {
                        method_name = 'New '
                    }
                    else { // if (method.value === 'cumulative')
                        method_name = 'Cumulative '
                    }
                    linear_y.axis_label = method_name + metric.value
                    log_y.axis_label = method_name + metric.value
                    cobweb_x.axis_label = method_name + metric.value + ' today'
                    cobweb_y.axis_label = method_name + metric.value + ' tomorrow'
                """
            )
        )

        # Callbacks
        scale_menus[i].js_on_change('value', update_menus[i])
        state_menus[i].js_on_change('value', update_menus[i])

        scale_menus[i].js_on_change('value', update_datas[i])
        state_menus[i].js_on_change('value', update_datas[i])
        county_menus[i].js_on_change('value', update_datas[i])
        metric_menus[i].js_on_change('value', update_datas[i])
        method_menus[i].js_on_change('value', update_datas[i])


    ### End combined plots


    # Shared Callbacks
    menu_dict = {}
    for i in range(N):
        roll_avg.js_on_change('value', update_datas[i])
        # store all menus to later be synchronized
        menu_dict['scale_' + str(i + 1)] = scale_menus[i]
        menu_dict['state_' + str(i + 1)] = state_menus[i]
        menu_dict['county_' + str(i + 1)] = county_menus[i]
    button.js_on_click(
        CustomJS(
            args=menu_dict,
            code="""
                scale_2.value = scale_1.value
                state_2.value = state_1.value
                county_2.value = county_1.value
            """
        )
    )

    # Display options
    for i in range(N):
        for e in widget_lists[i]:
            e.height = 50
            e.width = 100
            e.sizing_mode = 'fixed'
        for e in plot_lists[i]:
            e.sizing_mode = 'scale_both'
            e.min_border_bottom = 80
    for e in tab_lists:
        e.aspect_ratio = 1
        e.sizing_mode = 'scale_height'
    for e in [button, roll_avg]:
        e.sizing_mode = 'stretch_width'
    # Display arrangement
    display = grid(
        [
        gridplot([tab_lists]),
        [button, roll_avg],
        widget_layouts,
        ],
        sizing_mode='stretch_both',
    )
    return(display)