Ejemplo n.º 1
0
    def test_js_on_click_executes(self, bokeh_model_page) -> None:
        button = Toggle(css_classes=['foo'])
        button.js_on_click(CustomJS(code=RECORD("value", "cb_obj.active")))

        page = bokeh_model_page(button)

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': True}

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': False}

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': True}

        assert page.has_no_console_errors()
Ejemplo n.º 2
0
    def test_js_on_click_executes(self, bokeh_model_page: BokehModelPage) -> None:
        button = Toggle()
        button.js_on_click(CustomJS(code=RECORD("value", "cb_obj.active")))

        page = bokeh_model_page(button)

        button_el = find_element_for(page.driver, button, ".bk-btn")
        button_el.click()

        results = page.results
        assert results == {'value': True}

        button_el = find_element_for(page.driver, button, ".bk-btn")
        button_el.click()

        results = page.results
        assert results == {'value': False}

        button_el = find_element_for(page.driver, button, ".bk-btn")
        button_el.click()

        results = page.results
        assert results == {'value': True}

        assert page.has_no_console_errors()
Ejemplo n.º 3
0
    def test_js_on_click_executes(self, bokeh_model_page):
        button = Toggle(css_classes=['foo'])
        button.js_on_click(CustomJS(code=RECORD("value", "cb_obj.active")))

        page = bokeh_model_page(button)

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': True}

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': False}

        button = page.driver.find_element_by_css_selector('.foo .bk-btn')
        button.click()

        results = page.results
        assert results == {'value': True}

        assert page.has_no_console_errors()
Ejemplo n.º 4
0
# Spinner GUI
spinner = Spinner(title="Size", low=0, high=4, step=0.1, value=1, width=300)
# spinner.js_link('value', points.glyph, 'radius')

# Dropdown menu GUI
menu = [("Item 1", "item_1"), ("Item 2", "item_2"), None, ("Item 3", "item_3")]
dropdown = Dropdown(label="Dropdown button", button_type="warning", menu=menu)
dropdown.js_on_event(
    "menu_item_click",
    CustomJS(code="console.log('dropdown: ' + this.item, this.toString())"))

# Toggle button GUI
toggle = Toggle(label="Button", button_type="success")
toggle.js_on_click(
    CustomJS(code="""
    console.log('toggle: active=' + this.active, this.toString())
"""))

# choice menu GUI
OPTIONS = [str(i) for i in range(20)]
multi_choice = MultiChoice(value=["foo", "baz"], options=OPTIONS)
multi_choice.js_on_change(
    "value",
    CustomJS(code="""
    console.log('multi_choice: value=' + this.value, this.toString())
"""))

# # SELECT menu GUI
# selectoptions = ["Postive tested on Covid-19 virus", "Negative tested on Covid-19 virus", "Show both"]
# resultSelect = Select(title="What to show", options=selectoptions)
Ejemplo n.º 5
0
class RoiBrowser(object):
    def __init__(
            self,
            global_map_cells=None,
            global_map_values=None,  # these two args first for bw comp
            roi_tessellations=None,
            roi_map_values=None,
            roi_meta_tessellation=None,
            **kwargs):
        self.global_map_cells = global_map_cells
        self.global_map_values = global_map_values
        self.points = None if roi_meta_tessellation is None else roi_meta_tessellation.points
        tessellation = lambda a: None if a is None else a.tessellation
        self.roi_model = RoiCollection(
            global_tessellation=tessellation(global_map_cells),
            roi_tessellations=roi_tessellations,
            roi_meta_tessellation=tessellation(roi_meta_tessellation),
            **kwargs)
        self.roi_map_values = roi_map_values
        self.first_active_roi = 0

        # options for view elements
        self.full_fov_figure_kwargs = dict(toolbar_location='above',
                                           toolbar_sticky=False,
                                           match_aspect=True,
                                           tools='pan, wheel_zoom, reset')
        self.zooming_in_figure_kwargs = dict(toolbar_location=None,
                                             active_drag=None,
                                             match_aspect=True,
                                             tools='pan, wheel_zoom, reset')
        self.scalar_map_2d_kwargs = {}
        self.points_kwargs = dict(color='r', alpha=.1)
        self.trajectories_kwargs = dict(color='r',
                                        line_width=.5,
                                        line_alpha=.5,
                                        loc_alpha=.1,
                                        loc_size=6)
        self.slider_kwargs = dict(title='roi')

        self.roi_controller = RoiController(self.roi_model, fill_color=None)
        x, y = self.points['x'].values, self.points['y'].values
        self.roi_controller.xlim = [x.min(), x.max()]
        self.roi_controller.ylim = [y.min(), y.max()]

    @property
    def values(self):  # for bw compatibility during code transition
        warnings.warn('deprecated attribute: values', RuntimeWarning)
        return self.global_map_cells

    @property
    def points(self):
        if self._points is None:
            if self.global_map_cells is not None:
                self._points = self.global_map_cells.points
        return self._points

    @points.setter
    def points(self, pts):
        self._points = pts

    @property
    def cells(self):
        warnings.warn('deprecated attribute: cells', RuntimeWarning)
        return self.global_map_cells

    @property
    def roi_tessellations(self):
        return self.roi_model.roi_tessellations

    def full_fov(self):
        ctrl = self.roi_controller
        full_fov_fig = figure(**self.full_fov_figure_kwargs)
        full_fov_fig.add_tools(BoxZoomTool(match_aspect=True))
        if self.global_map_values is not None:
            scalar_map_2d(self.global_map_cells,
                          self.global_map_values,
                          figure=full_fov_fig,
                          **self.scalar_map_2d_kwargs)
        elif False:
            _min, _max = zip(*[(values.min(), values.max())
                               for values in self.roi_map_values])
            clim = (min(_min), max(_max))
            for cells, values in zip(self.roi_tessellations,
                                     self.roi_map_values):
                scalar_map_2d(cells,
                              values,
                              figure=full_fov_fig,
                              clim=clim,
                              **self.scalar_map_2d_kwargs)
        plot_points(self.points, figure=full_fov_fig, **self.points_kwargs)
        full_fov_fig.patches(**ctrl.patches_kwargs)
        full_fov_fig.line(**ctrl.line_kwargs)
        ctrl.unset_active('all')
        ctrl.set_active(self.first_active_roi)
        self.roi_view_full_fov = full_fov_fig
        return full_fov_fig

    def zooming_in(self):
        ctrl = self.roi_controller
        roi_bb = self.roi_model.bounding_box(margin=.1)[self.first_active_roi]
        xlim = roi_bb[[0, 2]].tolist()
        ylim = roi_bb[[1, 3]].tolist()
        xlim, ylim = match_aspect(xlim, ylim)
        zooming_in_fig = figure(**self.zooming_in_figure_kwargs)
        zooming_in_fig.add_tools(BoxZoomTool(match_aspect=True))
        if self.global_map_values is not None:
            scalar_map_2d(self.global_map_cells,
                          self.global_map_values,
                          figure=zooming_in_fig,
                          **self.scalar_map_2d_kwargs)
        else:
            for cells, values in zip(self.roi_tessellations,
                                     self.roi_map_values):
                scalar_map_2d(cells,
                              values,
                              figure=zooming_in_fig,
                              **self.scalar_map_2d_kwargs)
        traj_handles = plot_trajectories(self.points,
                                         figure=zooming_in_fig,
                                         **self.trajectories_kwargs)
        self.trajectory_handles = traj_handles[0::2]
        self.location_handles = traj_handles[1::2]
        zooming_in_fig.x_range = Range1d(*xlim)
        zooming_in_fig.y_range = Range1d(*ylim)
        self.roi_view_zooming_in = zooming_in_fig
        return zooming_in_fig

    def slider(self):
        ctrl = self.roi_controller
        zooming_in_fig = self.roi_view_zooming_in
        slider = Slider(start=1,
                        end=len(self.roi_model),
                        step=1,
                        value=self.first_active_roi + 1,
                        **self.slider_kwargs)
        slider.js_on_change('value_throttled',
                            ctrl.js_callback(zooming_in_fig))
        self.roi_view_slider = slider
        return slider

    def visibility_button1(self):
        self.trajectory_visibility_button = Toggle(label='Hide lines',
                                                   button_type='success')
        assert not self.trajectory_handles[1:]

        def set_visibility(multiline):
            return CustomJS(args=dict(multiline=multiline),
                            code="""
                    multiline.visible=!multiline.visible;
                    if (multiline.visible) {
                        this.label='Hide lines';
                    } else {
                        this.label='Show lines';
                    }
                    """)

        self.trajectory_visibility_button.js_on_click(
            set_visibility(self.trajectory_handles[0]))
        return self.trajectory_visibility_button

    def visibility_button2(self):
        self.location_visibility_button = Toggle(label='Hide points',
                                                 button_type='success')
        assert not self.location_handles[1:]

        def set_visibility(multiline):
            return CustomJS(args=dict(multiline=multiline),
                            code="""
                    multiline.visible=!multiline.visible;
                    if (multiline.visible) {
                        this.label='Hide points';
                    } else {
                        this.label='Show points';
                    }
                    """)

        self.location_visibility_button.js_on_click(
            set_visibility(self.location_handles[0]))
        return self.location_visibility_button

    def make_default_view(self):
        full_fov_map = self.full_fov()
        zooming_in_map = self.zooming_in()
        visibility_button1 = self.visibility_button1()
        visibility_button2 = self.visibility_button2()
        if 1 < len(self.roi_model):
            slider = self.slider()
            self.roi_view = row(
                zooming_in_map,
                column(slider,
                       full_fov_map,
                       row(visibility_button1, visibility_button2),
                       sizing_mode='scale_width'))
        else:
            self.roi_view = row(
                zooming_in_map, column(full_fov_map,
                                       sizing_mode='scale_width'))

    def show(self):
        show(self.roi_view)
Ejemplo n.º 6
0
button = Button(label="Button (enabled) - has click event",
                button_type="primary")
button.js_on_click(
    CustomJS(code="console.log('button: click ', this.toString())"))

button_disabled = Button(label="Button (disabled) - no click event",
                         button_type="primary",
                         disabled=True)
button_disabled.js_on_click(
    CustomJS(code="console.log('button(disabled): click ', this.toString())"))

toggle_inactive = Toggle(label="Toggle button (initially inactive)",
                         button_type="success")
toggle_inactive.js_on_click(
    CustomJS(
        code=
        "console.log('toggle(inactive): active=' + this.active, this.toString())"
    ))

toggle_active = Toggle(label="Toggle button (initially active)",
                       button_type="success",
                       active=True)
toggle_active.js_on_click(
    CustomJS(
        code=
        "console.log('toggle(active): active=' + this.active, this.toString())"
    ))

menu = [("Item 1", "item_1_value"), ("Item 2", "item_2_value"), None,
        ("Item 3", "item_3_value")]
Ejemplo n.º 7
0
    HoverTool(renderers=[points_render],
              tooltips=[('Var1', '@var1'), ('Var2', '@var2'),
                        ('Var3', '@var3')]))

filter_list = {}

for var in ['var1', 'var2', 'var3']:
    min_ = 0
    max_ = 100
    slider = RangeSlider(start=min_,
                         end=max_,
                         step=0.1,
                         value=(min_, max_),
                         title=f'{var} range')
    toggle = Toggle(label="Inactive", button_type="danger", aspect_ratio=3)
    toggle.js_on_click(toggle_callback(toggle))
    filter_list[var] = Filter(var, slider, toggle)


def update_plot(attrname, old, new):
    mask = [True] * len(gdf)
    for key, filter in filter_list.items():
        if filter.toggle_.active:
            mask = mask & (gdf[key] >= filter.slider_.value[0]) & (
                gdf[key] <= filter.slider_.value[1])
    test_view.filters[0] = BooleanFilter(booleans=mask)


for _, filter in filter_list.items():
    filter.slider_.on_change('value', update_plot)
    filter.toggle_.on_change('active', update_plot)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
def lidar2js(path, ncfile, jsfile):
  fname_nc  = join(path,ncfile)
  if isfile(fname_nc):
    if Debug: 
      print "Found file: ", ncfile
    ds      = Dataset(fname_nc,'r',format="NETCDF3_CLASSIC") 
    t_raw   = ds.variables["time"]
    z       = ds.variables["alt1"][:]
    bsc532  = ds.variables["bsc532"][:]
    bsc1064 = ds.variables["bsc1064"][:]
    zb      = ds.variables["zb"][:]
    zpbl    = ds.variables["zpbl"][:]
    t       = num2date(t_raw[:], units = t_raw.units)
    tm      = t_raw[:]
    ds.close()
  else:
    if Debug: 
      print "Not found file: ", ncfile
    return "No Data"

  DX       = t[1]-t[0]
  Dx_sec   = DX.total_seconds()
  Nx       = len(t)
  Nx0      = int(max_days*86400.0/Dx_sec)
  if Nx0<Nx: Nx=Nx0
  tmin     = t[-Nx]
  tmax     = t[-1]+DX
  twidth   = 1000.0*(tmax-tmin).total_seconds()

  zmin = 0
  zmax = z[-1]

  my_cmap,my_norm,my_rgb = build_palette4(levs1,levs2,levs3,levs4)
  img532_raw             = my_norm(bsc532[-Nx:,:])
  img1064_raw            = my_norm(bsc1064[-Nx:,:])

  data1   = {'zbase':      zb[-Nx:], 
             'zpbl':       zpbl[-Nx:], 
             'tcenter':    t[-Nx:] + DX/2,
            }
  data2   = { 'image532':  [np.array(img532_raw.filled().T,  dtype=np.int8)], 
              'image1064': [np.array(img1064_raw.filled().T, dtype=np.int8)], 
            }
  data3   = { 'profile532': bsc532[-1,:],
              'range':      z
            }
  src     = ColumnDataSource(data=data1)
  src_img = ColumnDataSource(data=data2)
  src_z   = ColumnDataSource(data=data3)

  color_mapper = LinearColorMapper(palette=[rgb2hex(item) for item in my_rgb], 
                                   low=0, 
                                   high=my_cmap.N
                                   )

  plot = figure(x_range=(tmin,tmax), y_range=(zmin,zmax), 
              title="Attenuated Backscatter coefficient [/sr /km]",
              toolbar_location="above",
              tools = "pan,wheel_zoom,box_zoom,reset,save",
              active_scroll=None, 
              active_drag=None,
              active_inspect=None,
            #  toolbar_sticky=False,
              y_axis_label = 'Height [km]',
              plot_width=900, 
              plot_height=350, 
              )
  plot.toolbar.logo=None

  plot2 = figure(title="Last Profile at 532 nm - {}".format(t[-1]),
  	             tools = "pan,wheel_zoom,box_zoom,reset,save",
  	             y_axis_label = 'Height [km]',
  	             x_axis_label = 'Attenuated Backscatter coefficient [/sr /km]',
                 # y_axis_type="log",
                 active_inspect=None,
                 plot_width=900, 
                 plot_height=350
  	             )
  plot2.toolbar.logo=None
  
  plot2.line(x='profile532', y='range',
             source=src_z, 
             line_color="black", 
             )

  im = plot.image(image="image532", 
                  source=src_img,
                  color_mapper=color_mapper,
                  dh=zmax, dw=twidth, 
                  x=tmin, y=zmin
                  ) 

  #l1 = plot.line(   x='tcenter', y='zbase', source=source, line_width=2, alpha=0.8, color="black")
  #l2 = plot.line(   x='tcenter', y='zpbl' , source=source, line_width=2, alpha=0.8, color="red")

  r1 = plot.circle( x='tcenter', y='zbase', 
                    source=src, 
                    fill_color="black", 
                    line_color="black", 
                    fill_alpha=0.3,
                    size=4,
                    name="r1"
                    )
  r2 = plot.square( x='tcenter', y='zpbl', 
                    source=src, 
                    fill_color="red", 
                    line_color="red", 
                    fill_alpha=0.3,
                    size=4,
                    name="r2"
                    )
  for item in [r1,r2]: item.visible = False

  color_bar = ColorBar(color_mapper=color_mapper, 
                       ticker=FixedTicker(ticks=[0,5,20,25]),
                       label_standoff=12, 
                       border_line_color=None, 
                       location=(0,0)
                       )
  color_bar.bar_line_color = "black"
  color_bar.major_tick_line_color = "black"
  color_bar.formatter = FuncTickFormatter(code="return {}[tick].toExponential(2)".format(levs))

  legend = Legend(items=[("Cloud Base",[r1]), ("PBL Height",[r2])], 
                  location="top_left"
                  )
  legend.click_policy = "hide"
  legend.visible = False

  hover = HoverTool(names=["r1","r2"])
  hover.tooltips=[("Cloud base", "@zbase km"), 
                  ("PBL height", "@zpbl km"), 
                  ("Time", "@tcenter{%d-%b-%y %H:%M}")]
  hover.formatters = { "tcenter": "datetime"}

  hover2 = HoverTool(tooltips=[
                     ("Signal", "@profile532"), 
                     ("Range", "@range"), 
                     ])

  plot.add_layout(color_bar, 'right')
  plot.add_layout(legend)
  plot.add_tools(hover)

  plot2.add_tools(hover2)
  
  plot.xaxis.formatter = DatetimeTickFormatter(months = ['%Y-%b'],
                                               years  = ['%Y'],
                                               days   = ['%d-%b-%Y']
                                               )

  callback = CustomJS(args=dict(im=im), code="""
      var f = cb_obj.active
      if (f==0){
        im.glyph.image.field = "image532"
      } else {
        im.glyph.image.field = "image1064"
      }
      im.glyph.change.emit();
  """)
  radio_button_group = RadioButtonGroup(labels=["532 nm", "1064 nm"], active=0)
  radio_button_group.js_on_change('active',callback)

  callback2 = CustomJS(args=dict(leg=legend), code="""
    leg.visible = !leg.visible
    console.log("ol")
  """)
  toggle = Toggle(label="Legend", button_type="default")
  toggle.js_on_click(callback2)

  layout = column(
  				children=[ 
  					row(widgetbox(radio_button_group, width=200), widgetbox(toggle, width=80) ), 
  					plot,
  					Spacer(height=50),
  					# row(Spacer(width=50),plot2),
  					plot2, 
					], 
  				)

  			
  #show(plot)
  return create_js(layout,path,jsfile)
Ejemplo n.º 10
0
def plotfits(dirname):
    
    session.modifeid = True
    session['pathname'] = app.config['UPLOAD_FOLDER']+'/'+dirname+'/'
    session['stats'] = {}
    session['date'] = {} # pegar a data para converter em juliana e inserir nas análises

    with open(session['pathname']+'data.json') as f:
        dirdata = json.load(f)

    r = dirdata['r']
    session['r'] = r
    celestial = False
    # Faz logo algumas estatísticas da imagem
    for fil in BANDAS:
        for fname in dirdata[fil]:
            img, header = fits.getdata(session['pathname']+fname, header=True)
            session['stats'][fil+':'+fname] = sigma_clipped_stats(img,sigma=3.0)

            if not celestial:
                celestial = WCS(header).has_celestial
                session['wcs'] = session['pathname']+fname
                
            session['date'][fil+':'+fname] = Time(header['DATE-OBS']).jd # a data de observação de cada imagem

    # Abrindo coordenadas se salvas
    try:
        cordata = pd.read_excel(session['pathname']+'data.xlsx')
        # Dados que serão usados para fazer computação e visualizar os pontos
        source = ColumnDataSource(cordata)

        print('Coordenadas carregadas.')
    except FileNotFoundError:
        print('Não há coordenadas salvas em %s' % session['pathname'])
        # Dados que serão usados para fazer computação e visualizar os pontos
        source = ColumnDataSource(dict(
            ra=[],
            dec=[],
            x=[],
            y=[],
            flux = [],
            j = [],
            k = [],
            tipo=[], # se é obj, src ou sky
            banda=[], # o filtro da imagem e arquivo
            sid=[], # id da estrela copiada
            colors=[], # para colorir de acordo o tipo de objeto
        ))

    # Constrói a tabaela de table que poderá ser usada para designar as posições do objeto, estrela e céu
    tabela = DataTable(source=source,columns=[
        TableColumn(field='x',title='x'),
        TableColumn(field='y',title='y'),
        TableColumn(field='ra',title='ra'),
        TableColumn(field='dec',title='dec'),
        TableColumn(field='j',title='j'),
        TableColumn(field='k',title='k'),
        TableColumn(field='flux',title='flux'),
        TableColumn(field='tipo',title='tipo'),
        TableColumn(field='banda',title='banda'),
        TableColumn(field='sid',title='sid')
    ], editable=True)
    

    P = [] # lista de gráficos para o plot
    Nimg = [] # lista de imagens normalizadas para o contraste
    for fil in BANDAS:
        for fname in dirdata[fil]:
            img = fits.getdata(session['pathname']+fname)
            stretch = HistEqStretch(img) # Histograma, melhor função para granular a imagem
            h,w = img.shape # número de linhas e colunas da matriz da imagem
            nimg = stretch(normal(img)).tolist()
            p = figure(plot_width=700, active_scroll='wheel_zoom')
            p.image(image=[nimg], x=0, y=0, dw=w, dh=h, palette='Greys256', level="image")
            p.x_range.range_padding = p.y_range.range_padding = 0
            p.grid.grid_line_width = 0

            view = CDSView(source=source,filters=[GroupFilter(column_name='banda', group=fil+':'+fname)])
            c = p.circle('x','y', source=source, view=view, color='colors', fill_color=None, radius=r, line_width=2)
            cd = p.circle_dot('x','y', source=source, view=view, color='colors', size=2)
            tool = PointDrawTool(renderers=[c,cd],empty_value='na')
            p.add_tools(tool)
            p.toolbar.active_tap = tool
            p.toolbar.active_inspect = None

            tab = Panel(child=p, title=fil+':'+fname)

            P.append(tab)
            Nimg.append(nimg)
    
    graficos = Tabs(tabs=P)
    graficos.js_on_change('active', CustomJS(code='''
    tabs_onchange(cb_obj);
    '''))

    contrast = Slider(start=-1, end=6, value=1, step=0.05, title="Contraste")
    contrast.js_on_change('value',CustomJS(args = dict(tabs=graficos.tabs, im=Nimg), code = '''
    contrast_onchange(cb_obj,tabs,im);
    '''))

    # Selecionar o tipo de fonte luminosa: obj, src ou sky
    radio_title = Paragraph(text='Escolha o tipo:')
    LABELS = ['obj','src','sky']
    radio_group = RadioGroup(labels=LABELS, active=0)

    # Evento de mudança da tabela de table, para inserir table padrão nas colunas inalteradas
    source.js_on_change('data', CustomJS(args=dict(radio=radio_group, graficos=graficos), code='''
    source_onchange(cb_obj, radio, graficos);
    '''))
    
    # Muda o raio da abertura fotométrica
    spinner = Spinner(title="Raio", low=1, high=40, step=0.5, value=r, width=80)
    spinner.js_on_change('value', CustomJS(args=dict(source=source, tabs=graficos.tabs), code='''
    radius_onchange(cb_obj,source,tabs);
    '''))

    # Coluna de requisição
    text1 = Div(text='<b>Instruções:</b><p>1. Digite a chave do Astrometry.net')
    apikey_input = TextInput(title='Apikey do Astrometry.net', placeholder='digite a chave aqui')

    text2 = Div(text='''<p>2. Selecione qual imagem será usada como referência para o astrometry.net e
    para o cálculo das coordenadas celestes</p>''')
    seletor = Select(title='Escolha a imagem de referência', options=[*session['stats'].keys()])

    text3 = Div(text='3. Clique abaixo pra requisitar a correção WCS')
    send_astrometry = Toggle(label='Solução de placa do astrometry.net', disabled=celestial)
    send_astrometry.js_on_click(CustomJS(args=dict(key=apikey_input, source=source, selected=seletor), code='''
    send_astrometry(cb_obj,key,source,selected);
    '''))

    # o Botão de salvar irá enviar um json para o servidor que irá ler e fazer os procedimentos posteriores
    text4 = Div(text='4. Salve a tabela de table clicando em salvar.')
    salvar = Button(label='Salvar tabela', button_type="success")
    salvar.js_on_click(CustomJS(args=dict(source=source), code='''
    salvar_onclick(source);
    '''))

    reset = Button(label='Limpar', button_type='success')
    reset.js_on_click(CustomJS(args=dict(source=source), code='''
    reset_onclick(source);
    '''))

    copiar = Button(label='Copiar coordenadas', button_type='success')
    copiar.js_on_click(CustomJS(args=dict(source=source, ref=seletor, active=graficos), code='''
    add_data(source,ref,active);
    '''))

    div, script = components(row(column(contrast,spinner,radio_title,radio_group),\
        column(row(reset,copiar,salvar), graficos, tabela, sizing_mode='stretch_both'),
        column(text1,apikey_input,text2,seletor,text3,send_astrometry,text4)))

    return render_template('plot.html', the_div=div, the_script=script,filename=dirdata['name'])
Ejemplo n.º 11
0
def bokeh_plot(node, link, name='NetworkMap'):
    from bokeh.plotting import figure, from_networkx, save
    from bokeh.models import ColumnDataSource, HoverTool
    from bokeh.io import export_png
    from bokeh.models import CustomJS, TextInput, CustomJSFilter, CDSView, TapTool
    from bokeh.layouts import column
    from bokeh.plotting import output_file, show
    from bokeh.tile_providers import get_provider, Vendors
    from bokeh.models import Circle, MultiLine, LabelSet, Toggle, CheckboxGroup
    from bokeh.models.graphs import NodesAndLinkedEdges

    text_input = TextInput(value="", title="Filter Nodes:")

    wgs84_to_web_mercator(node)

    node_source_data = ColumnDataSource(
        data=dict(x=node['MX'], y=node['MY'], desc=node['id']))

    # link
    G = nx.from_pandas_edgelist(link, source='id', target='anode')
    nx.set_node_attributes(G, dict(zip(link.id, link.id)), 'desc')
    n_loc = {k: (x, y) for k, x, y in zip(node['id'], node['MX'], node['MY'])}
    nx.set_node_attributes(G, n_loc, 'pos')
    n_color = {k: 'orange' if 'C' in k else 'green' for k in node['id']}
    nx.set_node_attributes(G, n_color, 'color')
    n_alpha = {k: 1 if 'C' in k else 0 for k in node['id']}
    nx.set_node_attributes(G, n_alpha, 'alpha')
    e_color = {(s, t): 'red' if 'C' in s else 'black'
               for s, t in zip(link['id'], link['anode'])}
    nx.set_edge_attributes(G, e_color, 'color')
    e_line_type = {(s, t): 'dashed' if 'C' in s else 'solid'
                   for s, t in zip(link['id'], link['anode'])}
    nx.set_edge_attributes(G, e_line_type, 'line_type')

    tile_provider = get_provider(Vendors.CARTODBPOSITRON)

    bokeh_plot = figure(title="%s network map" %
                        name.split('/')[-1].split('.')[0],
                        sizing_mode="scale_height",
                        plot_width=1300,
                        x_range=(min(node['MX']), max(node['MX'])),
                        tools='pan,wheel_zoom',
                        active_drag="pan",
                        active_scroll="wheel_zoom")
    bokeh_plot.add_tile(tile_provider)

    # This callback is crucial, otherwise the filter will not be triggered when the slider changes
    callback = CustomJS(args=dict(source=node_source_data),
                        code="""
        source.change.emit();
    """)
    text_input.js_on_change('value', callback)

    # Define the custom filter to return the indices from 0 to the desired percentage of total data rows. You could
    # also compare against values in source.data
    js_filter = CustomJSFilter(args=dict(text_input=text_input),
                               code=f"""
    const z = source.data['desc'];
    var indices = ((() => {{
      var result = [];
      for (let i = 0, end = source.get_length(), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {{
        if (z[i].includes(text_input.value.toString(10))) {{
          result.push(i);
        }}
      }}
      return result;
    }})());
    return indices;""")

    # Use the filter in a view
    view = CDSView(source=node_source_data, filters=[js_filter])

    callback2 = CustomJS(args=dict(x_range=bokeh_plot.x_range,
                                   y_range=bokeh_plot.y_range,
                                   text_input=text_input,
                                   source=node_source_data),
                         code=f"""
    const z = source.data['desc'];
    const x = source.data['x'];
    const y = source.data['y'];
    var result = [];
    for (let i = 0, end = source.get_length(), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {{
      if (z[i].includes(text_input.value.toString(10))) {{
        result.push(i);
      }}
    }}
    var indices = result[0];
    var Xstart = x[indices];
    var Ystart = y[indices];
    y_range.setv({{"start": Ystart-280, "end": Ystart+280}});
    x_range.setv({{"start": Xstart-500, "end": Xstart+500}});
    x_range.change.emit();
    y_range.change.emit();
    """)

    text_input.js_on_change('value', callback2)

    graph = from_networkx(G,
                          nx.get_node_attributes(G, 'pos'),
                          scale=2,
                          center=(0, 0))
    graph.node_renderer.glyph = Circle(radius=15,
                                       fill_color='color',
                                       fill_alpha='alpha')
    graph.node_renderer.hover_glyph = Circle(radius=15, fill_color='red')

    graph.edge_renderer.glyph = MultiLine(
        line_alpha=1, line_color='color', line_width=1,
        line_dash='line_type')  # zero line alpha
    graph.edge_renderer.hover_glyph = MultiLine(line_color='#abdda4',
                                                line_width=5)
    graph.inspection_policy = NodesAndLinkedEdges()

    bokeh_plot.circle('x',
                      'y',
                      source=node_source_data,
                      radius=10,
                      color='green',
                      alpha=0.7,
                      view=view)

    labels = LabelSet(x='x',
                      y='y',
                      text='desc',
                      text_font_size="8pt",
                      text_color='black',
                      x_offset=5,
                      y_offset=5,
                      source=node_source_data,
                      render_mode='canvas')

    code = '''\
    if (toggle.active) {
        box.text_alpha = 0.0;
        console.log('enabling box');
    } else {
        box.text_alpha = 1.0;
        console.log('disabling box');
    }
    '''
    callback3 = CustomJS(code=code, args={})
    toggle = Toggle(label="Annotation", button_type="success")
    toggle.js_on_click(callback3)
    callback3.args = {'toggle': toggle, 'box': labels}

    bokeh_plot.add_tools(HoverTool(tooltips=[("id", "@desc")]), TapTool())
    # Output filepath
    bokeh_plot.renderers.append(graph)
    bokeh_plot.add_layout(labels)
    layout = column(toggle, text_input, bokeh_plot)

    # export_png(p, filename="plot.png")
    output_file(name)
    show(layout)