Skip to content

Commit

Permalink
Merge pull request #277 from informatics-lab/tidy-ui
Browse files Browse the repository at this point in the history
Tidy UI
  • Loading branch information
andrewgryan authored Feb 11, 2020
2 parents 5a4cd2b + e624ad8 commit 9d37f2c
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 157 deletions.
2 changes: 1 addition & 1 deletion forest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
.. automodule:: forest.presets
"""
__version__ = '0.7.2'
__version__ = '0.8.0'

from .config import *
from . import (
Expand Down
1 change: 1 addition & 0 deletions forest/components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .time import TimeUI
from .colorbar import ColorbarUI
from .headline import Headline
19 changes: 19 additions & 0 deletions forest/components/headline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import bokeh.models


class Headline:
"""Summarise selected state"""
def __init__(self):
self._template = "<h3>{}</h3>"
self.div = bokeh.models.Div(text=self._template.format(""),
sizing_mode="stretch_width")
self.layout = self.div

def connect(self, store):
store.add_subscriber(self.render)
return self

def render(self, state):
labels = state.get("layers", {}).get("labels", [])
content = ", ".join([label for label in labels if label is not None])
self.div.text = self._template.format(content)
68 changes: 57 additions & 11 deletions forest/components/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,53 @@
import numpy as np


class _Axis:
"""Helper to find datetimes
Maps all datetime variants using str(datetime.datetime) as a key
"""
def __init__(self):
self._mapping = {}
self._values = []

@property
def datetimes(self):
return [_to_datetime(t) for t in self.times]

@property
def times(self):
return self._values

@times.setter
def times(self, values):
"""Intercept assignment to populate mapping"""
self._mapping = {self._key(v): i for i, v in enumerate(values)}
self._values = values

def index(self, time):
"""Map random datetime object to index"""
return self._mapping[self._key(time)]

def value(self, i):
"""Recover original value at index"""
return self._values[i]

@staticmethod
def _key(t):
return str(_to_datetime(t))



class TimeUI(Observable):
"""Allow navigation through time"""
def __init__(self):
self._axis = _Axis()
self.source = bokeh.models.ColumnDataSource(dict(
x=[],
y=[],
))
self.figure = bokeh.plotting.figure(
plot_height=100,
plot_height=80,
plot_width=800,
border_fill_alpha=0,
tools='xpan',
Expand Down Expand Up @@ -52,6 +90,7 @@ def __init__(self):
self.figure.toolbar_location = None
self.figure.xaxis.fixed_location = 0
self.figure.title.text = "Select time"
self.figure.title.align = "center"

# Hover interaction
hover_tool = bokeh.models.HoverTool(tooltips=None)
Expand Down Expand Up @@ -191,22 +230,25 @@ def __init__(self):
self.buttons["next"].js_on_click(custom_js)

self.layout = bokeh.layouts.column(
self.figure,
bokeh.layouts.row(
self.figure, sizing_mode="stretch_width"),
bokeh.layouts.row(
self.buttons["previous"],
self.buttons["play"],
self.buttons["pause"],
self.buttons["next"],
))
sizing_mode="stretch_width"
),
sizing_mode="stretch_width")

super().__init__()

def on_selected(self, attr, old, new):
"""Notify store of set valid time action"""
if len(new) > 0:
i = new[0]
value = self.source.data["x"][i]
print("on_selected", value)
value = self._axis.value(i)
print(value, self.source.data["x"][i])
self.notify(forest.db.control.set_value('valid_time', value))

def connect(self, store):
Expand All @@ -228,20 +270,24 @@ def to_props(self, state):
"""Convert state to properties needed by component"""
if ('valid_time' not in state) or ('valid_times' not in state):
return
time = _to_datetime(state['valid_time'])
times = sorted([_to_datetime(t) for t in state['valid_times']])
return times.index(time), times
return state['valid_time'], sorted(state['valid_times'])

def render(self, index, times):
def render(self, time, times):
"""React to state changes"""
self._axis.times = times
self.source.data = {
"x": times,
"x": self._axis.datetimes,
"y": np.zeros(len(times))
}

try:
index = self._axis.index(time)
except KeyError:
return
self.source.selected.indices = [index]

# Title
time = times[index]
time = self._axis.datetimes[index]
self.figure.title.text = f"{time:%A %d %B %Y %H:%M}"

# Band
Expand Down
22 changes: 17 additions & 5 deletions forest/eida50.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import datetime as dt
import netCDF4
import numpy as np
from functools import lru_cache


def infinite_cache(f):
"""Unbounded cache to reduce navigation I/O
.. note:: This information would be better saved in a database
or file to reduce round-trips to disk
"""
cache = {}
def wrapped(self, path, variable):
if path not in cache:
cache[path] = f(self, path, variable)
return cache[path]
return wrapped


class Coordinates(object):
"""Coordinate system related to EIDA50 file(s)"""
def initial_time(self, path):
return min(self._cached_times(path))
return dt.datetime(1970, 1, 1) # Placeholder for missing dimension

@infinite_cache
def valid_times(self, path, variable):
return self._cached_times(path)

@lru_cache()
def _cached_times(self, path):
with netCDF4.Dataset(path) as dataset:
var = dataset.variables["time"]
values = netCDF4.num2date(var[:], units=var.units)
Expand Down
3 changes: 2 additions & 1 deletion forest/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ class FigureRow:
def __init__(self, figures):
self.figures = figures
self.layout = bokeh.layouts.row(*figures,
sizing_mode="stretch_both")
sizing_mode="stretch_both",
name="figures")
self.layout.children = [self.figures[0]] # Trick to keep correct sizing modes

def connect(self, store):
Expand Down
55 changes: 14 additions & 41 deletions forest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ def is_image(renderer):
time_ui = forest.components.TimeUI()
time_ui.connect(store)

# Add snippet of text for user
headline = forest.components.Headline()
headline.connect(store)

# Connect renderer.visible states to store
artist = layers.Artist(renderers)
artist.connect(store)
Expand Down Expand Up @@ -315,12 +319,6 @@ def old_world(state):
layers_ui.layout),
title="Control"
),
bokeh.models.Panel(
child=bokeh.layouts.column(
tools_panel.buttons["toggle_time_series"],
tools_panel.buttons["toggle_profile"],
),
title="Tools"),
bokeh.models.Panel(
child=bokeh.layouts.column(
border_row,
Expand Down Expand Up @@ -382,54 +380,29 @@ def old_world(state):
f.on_event(bokeh.events.Tap, tap_listener.update_xy)
marker = screen.MarkDraw(f).connect(store)


# Minimise controls to ease navigation
compact_button = bokeh.models.Button(
label="Compact")
compact_minus = bokeh.models.Button(label="-", width=50)
compact_plus = bokeh.models.Button(label="+", width=50)
compact_navigation = bokeh.layouts.column(
compact_button,
bokeh.layouts.row(
compact_minus,
compact_plus,
width=100))
control_root = bokeh.layouts.column(
compact_button,
tabs,
name="controls")

display = "large"
def on_compact():
nonlocal display
if display == "large":
control_root.height = 100
control_root.width = 120
compact_button.width = 100
compact_button.label = "Expand"
control_root.children = [
compact_navigation]
display = "compact"
else:
control_root.height = 500
control_root.width = 300
compact_button.width = 300
compact_button.label = "Compact"
control_root.children = [compact_button, tabs]
display = "large"

compact_button.on_click(on_compact)

# Add key press support
key_press = keys.KeyPress()
key_press.add_subscriber(store.dispatch)

document = bokeh.plotting.curdoc()
document.title = "FOREST"
document.add_root(control_root)
document.add_root(tool_layout.figures_row)
document.add_root(
bokeh.layouts.column(
tools_panel.buttons["toggle_time_series"],
tools_panel.buttons["toggle_profile"],
tool_layout.layout,
width=400,
name="series"))
document.add_root(
bokeh.layouts.row(time_ui.layout, name="time"))
document.add_root(
bokeh.layouts.column(headline.layout, name="headline",
sizing_mode="stretch_width"))
document.add_root(
bokeh.layouts.row(colorbar_ui.layout, name="colorbar"))
document.add_root(figure_row.layout)
Expand Down
3 changes: 2 additions & 1 deletion forest/rdt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bokeh.palettes import GnBu3, OrRd3
import itertools
import math
from forest.gridded_forecast import _to_datetime


class RenderGroup(object):
Expand Down Expand Up @@ -128,7 +129,7 @@ def __init__(self, loader):
def render(self, state):
"""Gets called when a menu button is clicked (or when application state changes)"""
if state.valid_time is not None:
date = dt.datetime.strptime(state.valid_time, '%Y-%m-%d %H:%M:%S')
date = _to_datetime(state.valid_time)
try:
(self.source.geojson,
self.tail_line_source.data,
Expand Down
6 changes: 6 additions & 0 deletions forest/static/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let openId = function(id) {
document.getElementById(id).style.width = "400px";
}
let closeId = function(id) {
document.getElementById(id).style.width = "0";
}
Loading

0 comments on commit 9d37f2c

Please sign in to comment.