Source code for shyft.dashboard.time_series.axes

from enum import Enum
from typing import Optional, Tuple
from pint.errors import UndefinedUnitError
import logging

import bokeh.models

from shyft.dashboard.base.app import update_value_factory
from shyft.dashboard.time_series.state import Unit, UnitRegistry
from shyft.dashboard.base.hashable import Hashable
from shyft.dashboard.time_series.bindable import Bindable
from shyft.dashboard.time_series.attr_callback_manager import AttributeCallbackManager


[docs] class YAxisError(RuntimeError): pass
[docs] class YAxisSide(Enum): LEFT = 'left' RIGHT = 'right'
[docs] class YAxis(Hashable, Bindable, AttributeCallbackManager): """ This object represents the y axis definition """
[docs] def __init__(self, *, label: str, unit: Unit, color: str='black', side: YAxisSide=YAxisSide.LEFT, default_y_range: Optional[Tuple[float, float]]=None, dynamic_unit_prefix: bool=False, auto_unit_change: bool=True) -> None: """ Representation of a y-axis for a figure. Parameters ---------- label: y axis lable unit: unit of the axis color: color of the axis incl. tixs and numbers side: YAxisSide either YAxisSide.LEFT or YAxisSide.RIGHT default_y_range: set the default range to use if no renderer on axis dynamic_unit_prefix: NOT IN USE, used dynamic axis scaling auto_unit_change: allow that the figure changes the axis units if the axis is empty """ AttributeCallbackManager.__init__(self) Hashable.__init__(self) Bindable.__init__(self) self.label = label self.unit = unit self.color = color self.y_range_pad = 0.1 self.auto_unit_change = auto_unit_change self.__dynamic_unit_prefix = False # dynamic_unit_prefix self.__default_y_range = default_y_range or (0, 1) self.__side = side
@property def side(self) -> YAxisSide: return self.__side @property def default_y_range(self) -> Tuple[float, float]: return self.__default_y_range @property def dynamic_unit_prefix(self) -> bool: return self.__dynamic_unit_prefix def __repr__(self) -> str: return f"YAxis(label='{self.label}', unit='{self.unit}', color='{self.color}', side={self.side})"
[docs] class FigureYAxis(Bindable): """ This Object represents actual y axis for a Figure class """
[docs] def __init__(self, *, axis: YAxis, unit_registry: UnitRegistry, bokeh_axis: Optional[bokeh.models.LinearAxis]=None, y_axis_format: Optional[str]=None, logger: Optional['logging.Logger']=None) -> None: """ Figure Y Axis for the figure view container Parameters ---------- axis: YAxis object which to represent unit_registry: unit registry to use for unit converison bokeh_axis: bokeh axis to use """ Bindable.__init__(self) if axis.bound: raise YAxisError(f"{axis} is already bound to {axis.parent}") try: unit_registry.Unit(axis.unit) except UndefinedUnitError as u: raise YAxisError(f"{axis}: Incompatible unit!: {u}") axis.bind(parent=self) self.logger = logger or logging.getLogger() self.axis_view = axis self.unit_registry = unit_registry self.bokeh_range = bokeh.models.Range1d(axis.default_y_range[0], axis.default_y_range[1]) self.bokeh_range.on_change('end', self.check_axis_dimensions) self.bokeh_range.on_change('start', self.check_axis_dimensions) self.set_bokeh_range_start = update_value_factory(self.bokeh_range, 'start') self.set_bokeh_range_end = update_value_factory(self.bokeh_range, 'end') self.is_default_axis = False if bokeh_axis: self.bokeh_axis = bokeh_axis self.is_default_axis = True else: self.bokeh_axis = bokeh.models.LinearAxis() self.bokeh_axis.y_range_name = str(self.uid) self.bokeh_axis.axis_label_text_font = 'monospace' self.bokeh_axis.axis_label_text_font_style = 'normal' f = self.axis_format(self.unit_registry.Unit(self.axis_view.unit)) self.bokeh_axis.formatter = bokeh.models.NumeralTickFormatter(rounding='nearest', language='en', format=f) self.axis_view.on_change(obj=self, attr='label', callback=self.label_callback) self.axis_view.on_change(obj=self, attr='unit', callback=self.unit_callback) self.axis_view.on_change(obj=self, attr='color', callback=self.color_callback) # init the axis self.label_callback(self.axis_view, 'label', '', self.axis_view.label) self.unit_callback(self.axis_view, 'unit', '', self.axis_view.unit) self.color_callback(self.axis_view, 'color', '', self.axis_view.color)
[docs] def label_callback(self, obj, attr, old_value, new_value) -> None: """ Sets the label for the bokeh Axis """ if attr != 'label': return if obj != self.axis_view: obj.remove_all_callbacks(self) return self.bokeh_axis.axis_label = ' '.join([new_value, self.unit_view_format(self.unit_registry.Unit(self.axis_view.unit))])
[docs] def unit_callback(self, obj, attr, old_value, new_value) -> None: """ Sets the color for the bokeh axis """ if attr != 'unit': return if obj != self.axis_view: obj.remove_all_callbacks(self) return try: self.unit_registry.Unit(new_value) except UndefinedUnitError as u: self.logger.error(f"{self.axis_view}: Incompatible unit!: {u}") if self.parent and self.parent.has_renderer_on_y_axis(self.axis_view): view_dimensionality = self.unit_registry.Unit(old_value).dimensionality axis_dimensionality = self.unit_registry.Unit(new_value).dimensionality if view_dimensionality != axis_dimensionality: raise YAxisError(f"{self.axis_view}: Cannot change unit!: to {new_value} Since Renderer with different unit are " f"defined on this axis!") self.bokeh_axis.axis_label = ' '.join([self.axis_view.label, self.unit_view_format(self.unit_registry.Unit(new_value))]) self.bokeh_axis.formatter.format = self.axis_format(self.unit_registry.Unit(new_value))
[docs] def color_callback(self, obj, attr, old_value, new_value) -> None: """ This function sets the color for the bokeh axis """ if attr != 'color': return if obj != self.axis_view: obj.remove_all_callbacks(self) return self.bokeh_axis.axis_line_color = new_value self.bokeh_axis.major_label_text_color = new_value self.bokeh_axis.axis_label_text_color = new_value self.bokeh_axis.major_tick_line_color = new_value self.bokeh_axis.minor_tick_line_color = new_value
[docs] @staticmethod def axis_format(unit: Unit) -> str: """ This functions sets the y axis number format """ if str(unit) == 'percent': return '0.[00]' else: return '0.[000]'
[docs] @staticmethod def unit_view_format(unit: Unit) -> str: """ This function converts the Unit of the y axis """ return '%' if str(unit) == 'percent' else f'{unit:~P}'
@property def uid(self) -> str: """ This function returns the uid of the axis_view, used to identify this axis """ if self.is_default_axis: return 'default' return str(self.axis_view.uid) @property def side(self) -> str: """ This function returns the side of the axis """ if self.is_default_axis: return YAxisSide.LEFT.value return self.axis_view.side.value
[docs] def set_y_range(self, start: float, end: float) -> None: """ This function sets the axis range to start and end """ #self.bokeh_axis.visible = True if abs(end) < 1.e-4 and abs(start) < 1.e-4: end = 1 start = -1 if start == end: start *= 0.8 end *= 1.2 if start < 0: start, end = end, start if self.axis_view.dynamic_unit_prefix: start, end, axis_unit = self.get_dynamic_axis_unit(start, end) self.axis_view.unit = axis_unit extra_pad = (end - start)*self.axis_view.y_range_pad self.set_bokeh_range_start(start - extra_pad) self.set_bokeh_range_end(end + extra_pad)
#self.bokeh_range.start = start - extra_pad #self.bokeh_range.end = end + extra_pad
[docs] def check_axis_dimensions(self, attr, old, new) -> None: """ This function checks if y range has changed enough order of magnitude to trigger unit change """ if self.axis_view.dynamic_unit_prefix: start, end, unit = self.get_dynamic_axis_unit(self.bokeh_range.start, self.bokeh_range.end) if unit != self.unit_registry.Unit(self.axis_view.unit): self.set_bokeh_range_start(start) self.set_bokeh_range_end(end) self.axis_view.unit = unit # TODO: is not updating the graph correctly self.parent.draw_figure(y_axis=self.axis_view)
[docs] def get_dynamic_axis_unit(self, start: float, end: float) -> Tuple[float, float, str]: """ This function checks if the order of magnitude of the axis should change and returns the changes """ # TODO check if unit gets outside pints unit prefix definitions > yotta, < yocto axis_length = abs(start) + abs(end) axis_unit = (self.unit_registry.Unit(self.axis_view.unit) * axis_length).to_compact().u start = (start*self.unit_registry.Unit(self.axis_view.unit)).to(axis_unit).magnitude end = (end*self.unit_registry.Unit(self.axis_view.unit)).to(axis_unit).magnitude return start, end, str(axis_unit)
[docs] def reset_y_range(self) -> None: """ This function resets the y range to the default y range defined by YAxis """ self.set_bokeh_range_start(self.axis_view.default_y_range[0]) self.set_bokeh_range_end(self.axis_view.default_y_range[1])
#self.bokeh_range.start = self.axis_view.default_y_range[0] #self.bokeh_range.end = self.axis_view.default_y_range[1] #self.bokeh_axis.visible = False