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);")
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
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
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
def pi_formatter(): return FuncTickFormatter.from_py_func(pi_formatter_func)
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 = []
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", ) ]])
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()
# 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,
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)), )
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_)
def double_tap(event): """Двойной клик для перемещения отсчёта времени для подгонки""" zone_of_interest.location = event.x sec_axis.formatter = FuncTickFormatter( code=f"return ((tick - {event.x}) / {time_coef}).toFixed(1);")
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)