""" ``xrview.timeseries.base`` """
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from bokeh.document import without_document_lock
from bokeh.events import Reset
from tornado import gen
from xrview.core import BaseViewer
from xrview.elements import ResamplingElement
from xrview.handlers import ResamplingDataHandler
class TimeseriesViewer(BaseViewer):
""" Interactive viewer for large time series datasets."""
element_type = ResamplingElement
handler_type = ResamplingDataHandler
def __init__(
self,
data,
x,
overlay="dims",
glyphs="line",
tooltips=None,
tools=None,
figsize=(600, 300),
ncols=1,
palette=None,
ignore_index=False,
resolution=4,
max_workers=10,
lowpass=False,
verbose=0,
**fig_kwargs,
):
""" Constructor.
Parameters
----------
data : xarray DataArray or Dataset
The data to display.
x : str
The name of the dimension in ``data`` that contains the x-axis
values.
glyphs : str, BaseGlyph or iterable, default 'line'
The glyph to use for plotting.
figsize : iterable, default (600, 300)
The size of the figure in pixels.
ncols : int, default 1
The number of columns of the layout.
overlay : 'dims' or 'data_vars', default 'dims'
If 'dims', make one figure for each data variable and overlay the
dimensions. If 'data_vars', make one figure for each dimension and
overlay the data variables. In the latter case, all variables must
have the same dimensions.
tooltips : dict, optional
Names of tooltips mapping to glyph properties or source columns,
e.g. {'datetime': '@index{%F %T.%3N}'}.
tools : str, optional
bokeh tool string.
palette : iterable, optional
The palette to use when overlaying multiple glyphs.
ignore_index : bool, default Falseh
If True, replace the x-axis values of the data by an appropriate
evenly spaced index.
resolution : int, default 4
The number of points to render for each pixel.
max_workers : int, default 10
The maximum number of workers in the thread pool to perform the
down-sampling.
lowpass : bool, default False
If True, filter the values with a low-pass filter before
down-sampling.
verbose : int, default 0
The level of verbosity.
"""
super(TimeseriesViewer, self).__init__(
data,
x,
overlay=overlay,
glyphs=glyphs,
tooltips=tooltips,
tools=tools,
figsize=figsize,
ncols=ncols,
palette=palette,
ignore_index=ignore_index,
**fig_kwargs,
)
# sub-sampling parameters
self.resolution = resolution
self.thread_pool = ThreadPoolExecutor(max_workers)
self.lowpass = lowpass
self.verbose = verbose
@without_document_lock
@gen.coroutine
def update_handler(self, handler):
""" Update a single handler. """
yield self.thread_pool.submit(
partial(
handler.update,
start=self.figures[0].x_range.start,
end=self.figures[0].x_range.end,
)
)
@without_document_lock
@gen.coroutine
def reset_handlers(self):
""" Reset handlers. """
for h in self.handlers:
yield self.thread_pool.submit(h.reset)
def on_xrange_change(self, attr, old, new):
""" Callback for xrange change event. """
self.doc.add_next_tick_callback(self.update_handlers)
def _make_handlers(self):
""" Make handlers. """
self.handlers = [
self.handler_type(
self._collect(coords=self.coords),
self.resolution * self.figsize[0],
context=self,
lowpass=self.lowpass,
)
]
for element in self.added_figures + self.added_overlays:
self.handlers.append(element.handler)
def _update_handlers(self, hooks=None):
""" Update handlers. """
if hooks is None:
# TODO: check if this breaks co-dependent hooks
hooks = [i.collect_hook for i in self.added_interactions]
element_list = self.added_figures + self.added_overlays
for h_idx, h in enumerate(self.handlers):
if h_idx == 0:
h.data = self._collect(hooks)
else:
h.data = element_list[h_idx - 1]._collect(hooks)
start, end = h.get_range(
self.figures[0].x_range.start, self.figures[0].x_range.end
)
h.update_data(start, end)
h.update_source()
if h.source.selected is not None:
h.source.selected.indices = []
def _add_callbacks(self):
""" Add callbacks. """
self.figures[0].x_range.on_change("start", self.on_xrange_change)
self.figures[0].x_range.on_change("end", self.on_xrange_change)
self.figures[0].on_event(Reset, self.on_reset)
def add_figure(
self, data, glyphs="line", coords=None, name=None, resolution=None
):
""" Add a figure to the layout.
Parameters
----------
data : xarray.DataArray
The data to display.
glyphs : str, BaseGlyph or iterable thereof, default 'line'
The glyph (or glyphs) to display.
coords : iterable of str, optional
The coordinates of the DataArray to include. This is necessary
for composite glyphs such as BoxWhisker.
name : str, optional
The name of the DataArray which will be used as the title of the
figure. If not provided, the name of the DataArray will be used.
resolution : float, optional
The number of points to render for each pixel of this figure. If
not specified, the resolution of the viewer is used.
"""
element = self.element_type(glyphs, data, coords, name, resolution)
self.added_figures.append(element)
def add_overlay(
self,
data,
glyphs="line",
coords=None,
name=None,
onto=None,
resolution=None,
):
""" Add an overlay to a figure in the layout.
Parameters
----------
data : xarray.DataArray
The data to display.
glyphs : str, BaseGlyph or iterable thereof, default 'line'
The glyph (or glyphs) to display.
coords : iterable of str, optional
The coordinates of the DataArray to include. This is necessary
for composite glyphs such as BoxWhisker.
name : str, optional
The name of the DataArray which will be used as the title of the
figure. If not provided, the name of the DataArray will be used.
onto : str or int, optional
Title or index of the figure on which the element will be
overlaid. By default, the element is overlaid on all figures.
resolution : float, optional
The number of points to render for each pixel of this figure. If
not specified, the resolution of the viewer is used.
"""
element = self.element_type(glyphs, data, coords, name, resolution)
self.added_overlays.append(element)
self.added_overlay_figures.append(onto)