Source code for forest.state

"""
Application state
-----------------

Although the state in a redux store is defined by reducer functions,
engineers need documentation to extend and understand :class:`State`. Python
dataclasses are a natural fit to make state self-describing.

Expressing data structure as a nested hierarchy of types allows readers
of the code to understand how state is organised. It also allows for
type-checking to simplify functions that manipulate state.

State can be generated programmatically, converted to/from dict
to be compatible with reducers and to make it easier to serialize.

>>> state = forest.state.State()
>>> state.colorbar.name
'Viridis'

Converting to/from dict can be achieved
using :meth:`State.to_dict` and :meth:`State.from_dict`

>>> s1 = forest.state.State()
>>> d = s1.to_dict()
>>> type(d)
<class 'dict'>
>>> s2 = forest.state.State.from_dict(d)
>>> s1 == s2
True

The only caveat to be aware of while mapping to/from dict is that
:class:`State` implements default values for missing entries. A
default State is not equal to an empty dict.

>>> forest.state.State().to_dict() == {}
False

.. note:: State structure may change in future releases, backwards
          compatibility is not guaranteed

"""
import datetime as dt
import bokeh.palettes
from dataclasses import dataclass, field, asdict


[docs]@dataclass class Bokeh: """Additional bokeh state Image streaming behaves inconsistently when data is streamed before the DOM is ready, ``image_shape[t] is undefined`` errors are triggered in the compiled JS .. note:: HTML loaded is merely convenience and may be unnecessary in future bokeh releases """ html_loaded: bool = False
[docs]@dataclass class Limits: """ Color map extent, high and low represent upper and lower limits respectively :param low: lower limit :type low: float :param high: upper limit :type high: float """ low: float = 0.0 high: float = 1.0
[docs]@dataclass class ColorbarLimits: """Define user and column data source limits :param origin: either 'user' or 'column_data_source' :type origin: str :param column_data_source: column_data_source limits :type column_data_source: Limits :param user: user limits :type user: Limits """ origin: str = "column_data_source" column_data_source: Limits = field(default_factory=Limits) user: Limits = field(default_factory=Limits) def __post_init__(self): if isinstance(self.column_data_source, dict): self.column_data_source = Limits(**self.column_data_source) if isinstance(self.user, dict): self.user = Limits(**self.user)
def _names_factory(): return list(sorted(bokeh.palettes.all_palettes.keys())) def _numbers_factory(): return list(sorted(bokeh.palettes.all_palettes["Viridis"].keys()))
[docs]@dataclass class Colorbar: """ Colorbar settings allow users to change palettes and limits based on data or user-specified limits :param name: bokeh palette name :param number: bokeh palette number :param limits: user and column_data_source limits :type limits: ColorbarLimits :param reverse: reverse color palette order :param invisible_min: hide/show values below minimum :param invisible_max: hide/show values above maximum """ name: str = "Viridis" names: list = field(default_factory=_names_factory) number: int = 256 numbers: list = field(default_factory=_numbers_factory) limits: ColorbarLimits = field(default_factory=ColorbarLimits) low: float = 0.0 high: float = 1.0 reverse: bool = False invisible_min: bool = False invisible_max: bool = False def __post_init__(self): if isinstance(self.limits, dict): self.limits = ColorbarLimits(**self.limits) def to_dict(self): return asdict(self)
[docs]@dataclass class LayerMode: """Data to control UI presented to user Contains meta-data to indicate whether a layer is being edited or added. If the layer is being edited an index can be used to specify settings to overwrite. :param state: Edit mode, either 'edit' or 'add' :param index: Index of layer being edited """ state: str = "add" index: int = 0
[docs]@dataclass class Layers: """Layer settings :param figures: Number of figures to display :param index: Map layer index to settings :param active: List of active layers :param mode: Edit/new mode to define UI :type mode: LayerMode """ figures: int = 1 index: dict = field(default_factory=dict) active: list = field(default_factory=list) mode: LayerMode = field(default_factory=LayerMode) def __post_init__(self): if isinstance(self.mode, dict): self.mode = LayerMode(**self.mode) def to_dict(self): return asdict(self)
[docs]@dataclass class Borders: """Cartopy border overlay settings :param line_color: Color of coastlines and country borders :type line_color: str :param visible: Turn all lines on/off :type visible: bool """ line_color: str = "black" visible: bool = False
[docs]@dataclass class Tile: """Web map tiling user-settings :param name: Keyword to specify WMTS source :type name: str :param labels: Turn overlay labels on/off :type labels: bool """ name: str = "Open street map" labels: bool = False
[docs]@dataclass class Position: """X/Y position in WebMercator coordinates related to user interaction :param x: coordinate of tap event :param y: coordinate of tap event """ x: float = 0.0 y: float = -1e9 # South pole
[docs]@dataclass class Tools: """Flags to specify active tools :param time_series: Turn time series widget on/off :type time_series: bool :param profile: Turn profile widget on/off :type time_series: bool """ time_series: bool = False profile: bool = False
[docs]@dataclass class Presets: """Re-usable layer settings Presets are cooked up once and re-used anywhere, they can also be tweaked on the fly and instantly made available to all layers using them. They can also be serialised to disk to store/re-load them as needed :param active: currently chosen preset :type active: int :param labels: map index to label :type active: dict :param meta: data used by user interface :type active: dict """ active: int = 0 labels: dict = field(default_factory=dict) meta: dict = field(default_factory=dict)
[docs]@dataclass class State: """Application State container Nested data structure to define components, views and behaviour. :param pattern: User-selected value :param patterns: Dataset-specific values :param variable: User-selected value :param variables: Dataset-specific values :param initial_time: User-selected value :param initial_times: Dataset-specific values :param valid_time: User-selected value :param valid_times: Dataset-specific values :param pressure: User-selected value :param pressures: Dataset-specific values :param layers: Layer-specific settings :type layers: Layers :param colorbar: Color mapper controls :type colorbar: Colorbar :param tile: Web map tiling configuration :type tile: Tile :param tools: Turn profile/time_series on/off :type tools: Tools :param position: Used by tools to determine geographic position :type position: Position :param presets: Save colorbar settings for later re-use :type presets: Presets :param borders: Cartopy coastline, lakes and border settings :type borders: Borders :param bokeh: Additional bokeh state :type bokeh: Bokeh """ pattern: str = None # TODO: Support empty str as default patterns: list = field(default_factory=list) variable: str = None # TODO: Support empty str as default variables: list = field(default_factory=list) initial_time: dt.datetime = dt.datetime(1970, 1, 1) initial_times: list = field(default_factory=list) valid_time: dt.datetime = dt.datetime(1970, 1, 1) valid_times: list = field(default_factory=list) pressure: float = 0.0 pressures: list = field(default_factory=list) colorbar: Colorbar = field(default_factory=Colorbar) layers: Layers = field(default_factory=Layers) dimension: dict = field(default_factory=dict) # TODO: Find code using it tile: Tile = field(default_factory=Tile) tools: Tools = field(default_factory=Tools) position: Position = field(default_factory=Position) presets: Presets = field(default_factory=Presets) borders: Borders = field(default_factory=Borders) bokeh: Bokeh = field(default_factory=Bokeh) def __post_init__(self): """Type-checking""" if isinstance(self.bokeh, dict): self.bokeh = Bokeh(**self.bokeh) if isinstance(self.borders, dict): self.borders = Borders(**self.borders) if isinstance(self.colorbar, dict): self.colorbar = Colorbar(**self.colorbar) if isinstance(self.tile, dict): self.tile = Tile(**self.tile) if isinstance(self.tools, dict): self.tools = Tools(**self.tools) if isinstance(self.position, dict): self.position = Position(**self.position) if isinstance(self.layers, dict): self.layers = Layers(**self.layers) if isinstance(self.presets, dict): self.presets = Presets(**self.presets)
[docs] @classmethod def from_dict(cls, data: dict): """Factory method to convert from dict to State :returns: State instance :rtype: State """ return cls(**data)
[docs] def to_dict(self): """Map to dict representation of State :returns: dictionary containing nested state data :rtype: dict """ return asdict(self)