class BokehDocument(InstancesMixin): def __init__(self, title='Live Trending'): self.document = Document(title=title) self.plots = [] self.widgets = [ None, ] self._log = logging.getLogger("bokeh").setLevel(logging.INFO) def add_plot(self, new_plot_and_widget, linked_x_axis=True, infos=None): self.document.clear() new_plot, widget = new_plot_and_widget new_plot.x_range.bounds = None new_plot.y_range.bounds = None for key, plot in enumerate(self.plots): if new_plot.title == plot.title: self.plots.pop(key) self.plots.append(new_plot) break else: self.plots.append(new_plot) if self.widgets: pass else: # For now, let's deal with only one widget self.widgets.append(widget) self.widgets[0] = widget if linked_x_axis: for plot in self.plots[1:]: plot.x_range = self.plots[0].x_range plot.x_range.bounds = None plot.y_range.bounds = None div_header_doc = Div(text="""<div class="header"> <H1> BAC0 Trending Tools </H1> <h2>For %s (Address : %s | Device ID : %s)</h2></div>""" % (infos.name, infos.address, infos.device_id)) div_header_notes = Div( text= """<div class="TableTitle"><H1> Notes from controller</H1></div>""" ) div_footer = Div( text= """<div class="footer"><p> <a href="http://www.servisys.com">Servisys inc.</a> | <a href="https://pythoninthebuilding.wordpress.com/">Python in the building</a></p></div>""" ) layout = column(widgetbox(div_header_doc), gridplot(self.plots, ncols=2), widgetbox(div_header_notes), row(self.widgets), widgetbox(div_footer)) self.document.add_root(layout) #curdoc().add_root(layout) def add_periodic_callback(self, cb, update=100): self.document.add_periodic_callback(cb, update)
def modify_doc(doc: Document): source = ColumnDataSource(data=data_holder.get_data()) _keys = data_holder.get_keys() plot = figure(plot_height=600, plot_width=1200, title='Sequence Diagram', x_axis_type='datetime', y_range=_keys, tools="xwheel_zoom,xpan,box_zoom,hover,reset", active_scroll="xwheel_zoom", active_drag="xpan") glyph = HBar(y="who", left="t_start_ms", right="t_end_ms", height=0.4, fill_color="green", fill_alpha=0.2) plot.add_glyph(source, glyph) labels = LabelSet(x="t_start_ms", y="who", text="func", text_font_size="8pt", source=source, text_align='left') plot.add_layout(labels) plot.hover.tooltips = [ ("func", "@func"), ("in", "@in"), ("out", "@out"), # ("msg", "@msg"), ] def callback(): #source.stream(new_data=data_holder.get_data(), rollover=1000) # todo: how to use stream source.data = ColumnDataSource(data=data_holder.get_data()).data doc.add_periodic_callback(callback, 1000) doc.add_root(plot)
class BokehDocument(InstancesMixin): def __init__(self, title='Live Trending'): self.document = Document(title=title) self.plots = [] logging.getLogger("requests").setLevel(logging.INFO) logging.getLogger("bokeh").setLevel(logging.INFO) def add_plot(self, new_plot, linked_x_axis=True): self.document.clear() new_plot.x_range.bounds = None new_plot.y_range.bounds = None for key, plot in enumerate(self.plots): if new_plot.title == plot.title: self.plots.pop(key) self.plots.append(new_plot) break else: self.plots.append(new_plot) if linked_x_axis: for plot in self.plots[1:]: plot.x_range = self.plots[0].x_range plot.x_range.bounds = None plot.y_range.bounds = None if len(self.plots) > 1: number_of_rows = int(round(len(self.plots) / 2)) rows = [] number_of_columns = 2 i = 0 for each in range(number_of_rows): rows.append( list(plot for plot in self.plots[i:i + number_of_columns])) i += number_of_columns layout = gridplot(rows) else: print('Layout...') layout = VBox(children=self.plots) self.document.add_root(layout) def add_periodic_callback(self, cb, update=100): self.document.add_periodic_callback(cb, update)
class BokehDocument(InstancesMixin): def __init__(self, title = 'Live Trending'): self.document = Document(title = title) self.plots = [] logging.getLogger("requests").setLevel(logging.INFO) logging.getLogger("bokeh").setLevel(logging.INFO) def add_plot(self, new_plot, linked_x_axis = True): self.document.clear() new_plot.x_range.bounds = None new_plot.y_range.bounds = None for key, plot in enumerate(self.plots): if new_plot.title == plot.title: self.plots.pop(key) self.plots.append(new_plot) break else: self.plots.append(new_plot) if linked_x_axis: for plot in self.plots[1:]: plot.x_range = self.plots[0].x_range plot.x_range.bounds = None plot.y_range.bounds = None if len(self.plots) > 1: number_of_rows = int(round(len(self.plots) / 2)) rows = [] number_of_columns = 2 i = 0 for each in range(number_of_rows): rows.append(list(plot for plot in self.plots[i:i+number_of_columns])) i += number_of_columns layout = gridplot(rows) else: print('Layout...') layout = VBox(children=self.plots) self.document.add_root(layout) def add_periodic_callback(self, cb, update = 100): self.document.add_periodic_callback(cb,update)
def modify_doc(doc: Document): plot = figure() plot.circle('t', 'busy', source=_source) doc.add_periodic_callback(callback, 200) doc.add_root(plot)
def define_plot( doc: Document, rt_client: _StreamingClient, channels: list, tribe: RealTimeTribe, inventory: Inventory, detections: list, map_options: dict, plot_options: dict, plot_length: float, update_interval: int, data_color: str = "grey", lowcut: float = 1.0, highcut: float = 10.0, offline: bool = False, ): """ Set up a bokeh plot for real-time plotting. Defines a moving data stream and a map. Parameters ---------- doc Bokeh document to edit - usually called as a partial rt_client RealTimeClient streaming data channels Channels to plot tribe Tribe to plot inventory Inventory to plot detections Detections to plot - should be a list that is updated in place. map_options Dictionary of options for the map plot_options Dictionary of options for plotting in general plot_length Length of data plot update_interval Update frequency in seconds data_color Colour to data stream lowcut Lowcut for filtering data stream highcut Highcut for filtering data stream offline Flag to set time-stamps to data time-stamps if True, else timestamps will be real-time """ # Set up the data source Logger.info("Getting stream to define plot") stream = rt_client.stream.copy().split().detrend() if lowcut and highcut: stream.filter("bandpass", freqmin=lowcut, freqmax=highcut) title = "Streaming data: {0}-{1} Hz bandpass".format(lowcut, highcut) elif lowcut: stream.filter("highpass", lowcut) title = "Streaming data: {0} Hz highpass".format(lowcut) elif highcut: stream.filter("lowpass", highcut) title = "Streaming data: {0} Hz lowpass".format(highcut) else: title = "Raw streaming data" stream.merge() Logger.info(f"Have the stream: \n{stream}") template_lats, template_lons, template_alphas, template_ids = ( [], [], [], []) for template in tribe: try: origin = (template.event.preferred_origin() or template.event.origins[0]) except IndexError: continue template_lats.append(origin.latitude) template_lons.append(origin.longitude % 360) template_alphas.append(0) template_ids.append(template.event.resource_id.id.split("/")[-1]) station_lats, station_lons, station_ids = ([], [], []) for network in inventory: for station in network: station_lats.append(station.latitude) station_lons.append(station.longitude % 360) station_ids.append(station.code) # Get plot bounds in web mercator Logger.info("Defining map") transformer = Transformer.from_crs( "epsg:4326", "epsg:3857", always_xy=True) try: min_lat, min_lon, max_lat, max_lon = ( min(template_lats + station_lats), min(template_lons + station_lons), max(template_lats + station_lats), max(template_lons + station_lons)) except ValueError as e: Logger.error(e) Logger.info("Setting map bounds to NZ") min_lat, min_lon, max_lat, max_lon = (-47., 165., -34., 179.9) Logger.info(f"Map bounds: {min_lon}, {min_lat} - {max_lon}, {max_lat}") bottom_left = transformer.transform(min_lon, min_lat) top_right = transformer.transform(max_lon, max_lat) map_x_range = (bottom_left[0], top_right[0]) map_y_range = (bottom_left[1], top_right[1]) template_x, template_y = ([], []) for lon, lat in zip(template_lons, template_lats): _x, _y = transformer.transform(lon, lat) template_x.append(_x) template_y.append(_y) station_x, station_y = ([], []) for lon, lat in zip(station_lons, station_lats): _x, _y = transformer.transform(lon, lat) station_x.append(_x) station_y.append(_y) template_source = ColumnDataSource({ 'y': template_y, 'x': template_x, 'lats': template_lats, 'lons': template_lons, 'template_alphas': template_alphas, 'id': template_ids}) station_source = ColumnDataSource({ 'y': station_y, 'x': station_x, 'lats': station_lats, 'lons': station_lons, 'id': station_ids}) Logger.info("Allocated data sources") trace_sources = {} trace_data_range = {} # Allocate empty arrays for channel in channels: tr = stream.select(id=channel)[0] times = np.arange( tr.stats.starttime.datetime, (tr.stats.endtime + tr.stats.delta).datetime, step=dt.timedelta(seconds=tr.stats.delta)) data = tr.data trace_sources.update( {channel: ColumnDataSource({'time': times, 'data': data})}) trace_data_range.update({channel: (data.min(), data.max())}) # Set up the map to go on the left side Logger.info("Adding features to map") map_plot = figure( title="Template map", x_range=map_x_range, y_range=map_y_range, x_axis_type="mercator", y_axis_type="mercator", **map_options) url = 'http://a.basemaps.cartocdn.com/rastertiles/voyager/{Z}/{X}/{Y}.png' attribution = "Tiles by Carto, under CC BY 3.0. Data by OSM, under ODbL" map_plot.add_tile(WMTSTileSource(url=url, attribution=attribution)) map_plot.circle( x="x", y="y", source=template_source, fill_color="firebrick", line_color="grey", line_alpha=.2, fill_alpha="template_alphas", size=10) map_plot.triangle( x="x", y="y", size=10, source=station_source, color="blue", alpha=1.0) # Set up the trace plots Logger.info("Setting up streaming plot") trace_plots = [] if not offline: now = dt.datetime.utcnow() else: now = max([tr.stats.endtime for tr in stream]).datetime p1 = figure( y_axis_location="right", title=title, x_range=[now - dt.timedelta(seconds=plot_length), now], plot_height=int(plot_options["plot_height"] * 1.2), **{key: value for key, value in plot_options.items() if key != "plot_height"}) p1.yaxis.axis_label = None p1.xaxis.axis_label = None p1.min_border_bottom = 0 p1.min_border_top = 0 if len(channels) != 1: p1.xaxis.major_label_text_font_size = '0pt' p1_line = p1.line( x="time", y='data', source=trace_sources[channels[0]], color=data_color, line_width=1) legend = Legend(items=[(channels[0], [p1_line])]) p1.add_layout(legend, 'right') datetick_formatter = DatetimeTickFormatter( days=["%m/%d"], months=["%m/%d"], hours=["%m/%d %H:%M:%S"], minutes=["%m/%d %H:%M:%S"], seconds=["%m/%d %H:%M:%S"], hourmin=["%m/%d %H:%M:%S"], minsec=["%m/%d %H:%M:%S"]) p1.xaxis.formatter = datetick_formatter # Add detection lines Logger.info("Adding detection artists") detection_source = _get_pick_times(detections, channels[0]) detection_source.update( {"pick_values": [[ int(min(stream.select(id=channels[0])[0].data) * .9), int(max(stream.select(id=channels[0])[0].data) * .9)] for _ in detection_source['picks']]}) detection_sources = {channels[0]: ColumnDataSource(detection_source)} detection_lines = MultiLine( xs="picks", ys="pick_values", line_color="red", line_dash="dashed", line_width=1) p1.add_glyph(detection_sources[channels[0]], detection_lines) trace_plots.append(p1) if len(channels) > 1: for i, channel in enumerate(channels[1:]): p = figure( x_range=p1.x_range, y_axis_location="right", **plot_options) p.yaxis.axis_label = None p.xaxis.axis_label = None p.min_border_bottom = 0 # p.min_border_top = 0 p_line = p.line( x="time", y="data", source=trace_sources[channel], color=data_color, line_width=1) legend = Legend(items=[(channel, [p_line])]) p.add_layout(legend, 'right') p.xaxis.formatter = datetick_formatter # Add detection lines detection_source = _get_pick_times(detections, channel) detection_source.update( {"pick_values": [[ int(min(stream.select(id=channel)[0].data) * .9), int(max(stream.select(id=channel)[0].data) * .9)] for _ in detection_source['picks']]}) detection_sources.update({ channel: ColumnDataSource(detection_source)}) detection_lines = MultiLine( xs="picks", ys="pick_values", line_color="red", line_dash="dashed", line_width=1) p.add_glyph(detection_sources[channel], detection_lines) trace_plots.append(p) if i != len(channels) - 2: p.xaxis.major_label_text_font_size = '0pt' plots = gridplot([[map_plot, column(trace_plots)]]) previous_timestamps = { channel: stream.select(id=channel)[0].stats.endtime for channel in channels} def update(): Logger.debug("Plot updating") _stream = rt_client.stream.split().detrend() if lowcut and highcut: _stream.filter("bandpass", freqmin=lowcut, freqmax=highcut) elif lowcut: _stream.filter("highpass", lowcut) elif highcut: _stream.filter("lowpass", highcut) _stream.merge() for _i, _channel in enumerate(channels): try: _tr = _stream.select(id=_channel)[0] except IndexError: Logger.debug("No channel for {0}".format(_channel)) continue new_samples = int(_tr.stats.sampling_rate * ( previous_timestamps[_channel] - _tr.stats.endtime)) if new_samples == 0: Logger.debug("No new data for {0}".format(_channel)) continue _new_data = _tr.slice( starttime=previous_timestamps[_channel]) new_times = np.arange( _new_data.stats.starttime.datetime, (_tr.stats.endtime + _tr.stats.delta).datetime, step=dt.timedelta(seconds=_tr.stats.delta)) new_data = {'time': new_times[1:], 'data': _new_data.data[1:]} Logger.debug("Channl: {0}\tNew times: {1}\t New data: {2}".format( _tr.id, new_data["time"].shape, new_data["data"].shape)) trace_sources[_channel].stream( new_data=new_data, rollover=int(plot_length * _tr.stats.sampling_rate)) new_picks = _get_pick_times(detections, _channel) new_picks.update({ 'pick_values': [ [int(np.nan_to_num( trace_sources[_channel].data['data']).max() * .9), int(np.nan_to_num( trace_sources[_channel].data['data']).min() * .9)] for _ in new_picks['picks']]}) detection_sources[_channel].data = new_picks previous_timestamps.update({_channel: _tr.stats.endtime}) Logger.debug("New data plotted for {0}".format(_channel)) if not offline: now = dt.datetime.utcnow() else: try: now = max([tr.stats.endtime for tr in _stream]).datetime except ValueError: return trace_plots[0].x_range.start = now - dt.timedelta(seconds=plot_length) trace_plots[0].x_range.end = now _update_template_alphas( detections, tribe, decay=plot_length, now=now, datastream=template_source) Logger.info("Adding callback") doc.add_periodic_callback(update, update_interval) doc.title = "EQcorrscan Real-time plotter" doc.add_root(plots) Logger.info("Plot defined")