Source code for shyft.dashboard.widgets.date_selector

from typing import Optional, Tuple

import shyft.time_series as sa
from bokeh.layouts import column
from bokeh.models import TextInput, LayoutDOM

from shyft.dashboard.base.app import LayoutComponents, Widget
from shyft.dashboard.base import constants

from shyft.dashboard.base.ports import Sender, Receiver, StatePorts, States

from shyft.dashboard.widgets.logger_box import LoggerBox


[docs] class DateSelector(Widget):
[docs] def __init__(self, title: str = '', width: int = 200, height: Optional[int]=None, padding: Optional[int] = None, sizing_mode: Optional[str] = None, max_date: Optional[int] = None, min_date: Optional[int] = None, time_zone: Optional[str] = 'Europe/Oslo', logger: Optional[LoggerBox] = None) -> None: super().__init__(logger=logger) padding = padding or constants.widget_padding sizing_mode = sizing_mode or constants.sizing_mode self.text_input = TextInput(placeholder='YYYY.MM.DD', title=title, width=width, height=height) self.text_input.on_change('value', self._evaluate_date) self._set_date = self.update_value_factory(self.text_input, 'value') self._layout = column(self.text_input, width=width + padding, height=height, sizing_mode=sizing_mode) self.cal = sa.Calendar(time_zone) self.current_date_int = None self.max_date: int = max_date self.min_date: int = min_date self.send_selected_date = Sender(parent=self, name='send selected date', signal_type=int) self.receive_selected_date = Receiver(parent=self, name='receive selected date', func=self._receive_date, signal_type=int) self.receive_min_date = Receiver(parent=self, name='receive min date', func=self._receive_min_date, signal_type=int) self.receive_max_date = Receiver(parent=self, name='receive max date', func=self._receive_max_date, signal_type=int) self.state_port = StatePorts(parent=self, _receive_state=self._receive_state) self._state = States.ACTIVE
@property def layout(self) -> LayoutDOM: return self._layout @property def layout_components(self) -> LayoutComponents: """ Property to return all layout.dom components of an visualisation app such that they can be arranged by the parent layout obj as desired. Returns ------- dict layout_components as: {'widgets': [], 'figures': []} """ return {'widgets': [self.text_input], 'figures': []} def _evaluate_date(self, attr, old, new): if not new: return date = self.convert_str_to_date(new) error = f"Error: Cannot convert given input date: '{new}' please check format is YYYY.MM.DD and date is vaild" date, error = self._evaluate_date_range(date=date) if date: self._receive_state(States.ACTIVE) self.current_date_int = date self.send_selected_date(date) else: self._receive_state(States.INVALID) self.logger.error(error) def _evaluate_date_range(self, *, date: int) -> Tuple[int, str]: date_str = self.convert_date_to_str(date) error = '' if date and self.max_date: if self.max_date < date: date = None error = f"Error: date: '{date_str}' is larger than max_date '{self.convert_date_to_str(self.max_date)}'" if date and self.min_date: if self.min_date > date: date = None error = f"Error: date: '{date_str}' is smaller than min_date '{self.convert_date_to_str(self.min_date)}'" return date, error
[docs] def convert_str_to_date(self, date_str: str) -> Optional[int]: if date_str.count('.') != 2: return None yyyy, mm, dd, = date_str.split('.') if len(yyyy) != 4 or len(mm) not in [1, 2] or len(dd) not in [1, 2]: return None def convert_to_int(v): try: return int(v) except ValueError as e: return e yyyy = convert_to_int(yyyy) if not isinstance(yyyy, int): return None mm = convert_to_int(mm) if not isinstance(mm, int): return None dd = convert_to_int(dd) if not isinstance(dd, int): return None if 0 < mm > 12 or 0 < dd > 31: return None return self.cal.time(Y=yyyy, M=mm, D=dd)
[docs] def convert_date_to_str(self, date: Optional[int]) -> Optional[str]: # noinspection PyBroadException try: date = int(date) # allow this to fail, for None '', allow it to succeed for anything convertible to time as int [s] except: return '' u = self.cal.calendar_units(date) return f'{u.year}.{u.month}.{u.day}'
def _receive_date(self, date: int) -> None: self.current_date_int, error = self._evaluate_date_range(date=date) if error: self.logger.error(error) self.text_input.value = self.convert_date_to_str(self.current_date_int) return date_str = self.convert_date_to_str(date) self.text_input.value = date_str def _receive_min_date(self, date: int) -> None: """ Receive and set the minimum date which can be chosen. Parameters ---------- date: int epoch time """ self.min_date = date if self.current_date_int and self.current_date_int < self.min_date: self.current_date_int = self.min_date + self.cal.DAY self._set_date(self.convert_date_to_str(self.current_date_int)) def _receive_max_date(self, date: int) -> None: """ Receive and set the maximum date. The chosen date must be smaller then that Parameters ---------- date: int epoch time """ self.max_date = date if self.current_date_int and self.current_date_int > self.max_date: self.current_date_int = self.max_date - self.cal.DAY self._set_date(self.convert_date_to_str(self.current_date_int)) def _receive_state(self, state: States) -> None: if state == States.ACTIVE: self._state = state self.text_input.disabled = False # Not sending active state since this only done if we can send data to the next widget elif state == States.DEACTIVE: self._state = state self.text_input.disabled = False self.state_port.send_state(state) elif state == States.INVALID: self._state = state self._set_date(self.convert_date_to_str(self.current_date_int)) self.text_input.disabled = False self.state_port.send_state(state) else: self.logger.error(f"ERROR: {self} - not handel for received state {state} implemented") self.state_port.send_state(state)