Skip to content

Commit

Permalink
Add ability to expose View controls (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Apr 15, 2021
1 parent 94f4fa0 commit 0b29e67
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 22 deletions.
1 change: 1 addition & 0 deletions lumen/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def _render_dashboard(self):
self._materialize_specification()
self._rerender()
except Exception as e:
self.param.warning(f'Rendering dashboard raised following error:\n\n {type(e).__name__}: {e}')
self._main.loading = False
tb = html.escape(traceback.format_exc())
alert = pn.pane.HTML(
Expand Down
28 changes: 25 additions & 3 deletions lumen/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,15 @@ class Target(param.Parameterized):
A list or dictionary of views to be displayed.""")

def __init__(self, **params):
if 'facet' not in params:
params['facet'] = Facet()
self._application = params.pop('application', None)
self._cards = []
self._cache = {}
self._cb = None
self._stale = False
self._updates = {}
self._view_controls = pn.Column(sizing_mode='stretch_width')
self.kwargs = {k: v for k, v in params.items() if k not in self.param}
super().__init__(**{k: v for k, v in params.items() if k in self.param})

Expand Down Expand Up @@ -319,7 +322,7 @@ def _get_card(self, filters, facet_filters, invalidate_cache=True, update_views=
update_card = update_card or view_stale

if not any(view for view in views):
return None, None
return None, None, views

sort_key = self.facet.get_sort_key(views)

Expand All @@ -335,7 +338,7 @@ def _get_card(self, filters, facet_filters, invalidate_cache=True, update_views=
card.title = title
if update_card:
self._updates[card] = views
return sort_key, card
return sort_key, card, views

def get_filter_panel(self, skip=None):
skip = skip or []
Expand All @@ -359,6 +362,7 @@ def get_filter_panel(self, skip=None):
self.facet._reverse_widget
])
views.append(pn.layout.Divider())
views.append(self._view_controls)
self._reload_button = pn.widgets.Button(
name='↻', width=50, css_classes=['reload'], margin=0
)
Expand All @@ -375,17 +379,35 @@ def get_filter_panel(self, skip=None):
# Rendering API
##################################################################

def _sync_view(self, view, *events):
view.param.set_param(**{event.name: event.new for event in events})

def _update_views(self, invalidate_cache=True, update_views=True, events=[]):
cards = []
view_controls = []
prev_views = None
for facet_filters in self.facet.filters:
key, card = self._get_card(
key, card, views = self._get_card(
self.filters, facet_filters, invalidate_cache, update_views,
events=events
)
if prev_views:
for v1, v2 in zip(prev_views, views):
v1.param.watch(partial(self._sync_view, v2), v1.controls)
else:
for view in views:
if not view.controls:
continue
view_controls.append(view.control_panel)
cb = partial(self._rerender, invalidate_cache=False)
view.param.watch(cb, view.controls)
prev_views = views
if card is None:
continue
cards.append((key, card))

self._view_controls[:] = view_controls

if self.facet.sort:
cards = sorted(cards, key=lambda x: x[0])
if self.facet.reverse:
Expand Down
75 changes: 75 additions & 0 deletions lumen/tests/test_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from pathlib import Path

import holoviews as hv

from panel.param import Param

from lumen.sources import FileSource
from lumen.target import Target


def test_view_controls():
source = FileSource(tables={'test': 'sources/test.csv'}, root=str(Path(__file__).parent))
views = {
'test': {
'type': 'hvplot', 'table': 'test', 'controls': ['x', 'y'],
'x': 'A', 'y': 'B', 'kind': 'scatter'
}
}
target = Target(source=source, views=views)

filter_panel = target.get_filter_panel()
param_pane = filter_panel[0][0]
assert isinstance(param_pane, Param)
assert param_pane.parameters == ['x', 'y']

assert len(target._cards) == 1
card = target._cards[0]
hv_pane = card[0][0]
isinstance(hv_pane.object, hv.Scatter)
assert hv_pane.object.kdims == ['A']
assert hv_pane.object.vdims == ['B']

param_pane._widgets['x'].value = 'C'
param_pane._widgets['y'].value = 'D'

isinstance(hv_pane.object, hv.Scatter)
assert hv_pane.object.kdims == ['C']
assert hv_pane.object.vdims == ['D']


def test_view_controls_facetted():
source = FileSource(tables={'test': 'sources/test.csv'}, root=str(Path(__file__).parent))
views = {
'test': {
'type': 'hvplot', 'table': 'test', 'controls': ['x', 'y'],
'x': 'A', 'y': 'B', 'kind': 'scatter'
}
}
spec = {
'source': 'test',
'facet': {'by': 'C'},
'views': views
}
target = Target.from_spec(spec, sources={'test': source})

filter_panel = target.get_filter_panel()
param_pane = filter_panel[4][0]
assert isinstance(param_pane, Param)
assert param_pane.parameters == ['x', 'y']

assert len(target._cards) == 5
for card in target._cards:
hv_pane = card[0][0]
isinstance(hv_pane.object, hv.Scatter)
assert hv_pane.object.kdims == ['A']
assert hv_pane.object.vdims == ['B']

param_pane._widgets['x'].value = 'C'
param_pane._widgets['y'].value = 'D'

for card in target._cards:
hv_pane = card[0][0]
isinstance(hv_pane.object, hv.Scatter)
assert hv_pane.object.kdims == ['C']
assert hv_pane.object.vdims == ['D']
75 changes: 56 additions & 19 deletions lumen/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from panel.pane.perspective import (
THEMES as _PERSPECTIVE_THEMES, Plugin as _PerspectivePlugin
)
from panel.param import Param
from ..filters import ParamFilter
from ..sources import Source
from ..transforms import Transform
Expand All @@ -34,6 +35,9 @@ class View(param.Parameterized):
a Viewable Panel object in the `get_panel` method.
"""

controls = param.List(default=[], doc="""
Parameters that should be exposed as widgets in the UI.""")

filters = param.List(constant=True, doc="""
A list of Filter object providing the query parameters for the
Source.""")
Expand All @@ -51,10 +55,13 @@ class View(param.Parameterized):

table = param.String(doc="The table being visualized.")

field = param.String(doc="The field being visualized.")
field = param.Selector(doc="The field being visualized.")

view_type = None

# Parameters which reference fields in the table
_field_params = ['field']

_selections = WeakKeyDictionary()

_supports_selections = False
Expand All @@ -67,7 +74,19 @@ def __init__(self, **params):
self._panel = None
self._updates = None
self.kwargs = {k: v for k, v in params.items() if k not in self.param}
super().__init__(**{k: v for k, v in params.items() if k in self.param})

# Populate field selector parameters
params = {k: v for k, v in params.items() if k in self.param}
source, table = params.pop('source', None), params.pop('table', None)
if source is None:
raise ValueError("Views must declare a Source.")
if table is None:
raise ValueError("Views must reference a table on the declared Source.")
fields = list(source.get_schema(table))
for fp in self._field_params:
self.param[fp].objects = fields

super().__init__(source=source, table=table, **params)
if self.selection_group:
self._init_link_selections()

Expand Down Expand Up @@ -275,6 +294,10 @@ def update(self, *events, invalidate_cache=True):
def _get_params(self):
return None

@property
def control_panel(self):
return Param(self.param, parameters=self.controls, sizing_mode='stretch_width')

@property
def panel(self):
if isinstance(self._panel, PaneBase):
Expand Down Expand Up @@ -353,19 +376,27 @@ class hvPlotView(View):

kind = param.String(doc="The kind of plot, e.g. 'scatter' or 'line'.")

x = param.String(doc="The column to render on the x-axis.")
x = param.Selector(doc="The column to render on the x-axis.")

y = param.String(doc="The column to render on the y-axis.")
y = param.Selector(doc="The column to render on the y-axis.")

opts = param.Dict(default={}, doc="HoloViews option to apply on the plot.")
by = param.ListSelector(doc="The column(s) to facet the plot by.")

groupby = param.ListSelector(doc="The column(s) to group by.")

opts = param.Dict(default={}, doc="HoloViews options to apply on the plot.")

streaming = param.Boolean(default=False, doc="""
Whether to stream new data to the plot or rerender the plot.""")

selection_expr = param.Parameter()
selection_expr = param.Parameter(doc="""
A selection expression caputirng the current selection applied
on the plot.""")

view_type = 'hvplot'

_field_params = ['x', 'y', 'by', 'groupby']

_supports_selections = True

def __init__(self, **params):
Expand All @@ -375,6 +406,10 @@ def __init__(self, **params):
import hvplot.dask # noqa
except Exception:
pass
if 'by' in params and isinstance(params['by'], str):
params['by'] = [params['by']]
if 'groupby' in params and isinstance(params['groupby'], str):
params['groupby'] = [params['groupby']]
self._stream = None
self._linked_objs = []
super().__init__(**params)
Expand Down Expand Up @@ -519,37 +554,39 @@ def _get_params(self):
class PerspectiveView(View):

aggregates = param.Dict(None, doc="""
How to aggregate. For example {x: "distinct count"}""")
How to aggregate. For example {x: "distinct count"}""")

columns = param.List(default=None, doc="""
columns = param.ListSelector(default=None, doc="""
A list of source columns to show as columns. For example ["x", "y"]""")

computed_columns = param.List(default=None, doc="""
A list of computed columns. For example [""x"+"index""]""")
computed_columns = param.ListSelector(default=None, doc="""
A list of computed columns. For example [""x"+"index""]""")

column_pivots = param.List(None, doc="""
A list of source columns to pivot by. For example ["x", "y"]""")
column_pivots = param.ListSelector(None, doc="""
A list of source columns to pivot by. For example ["x", "y"]""")

filters = param.List(default=None, doc="""
How to filter. For example [["x", "<", 3],["y", "contains", "abc"]]""")
How to filter. For example [["x", "<", 3],["y", "contains", "abc"]]""")

row_pivots = param.List(default=None, doc="""
A list of source columns to group by. For example ["x", "y"]""")
row_pivots = param.ListSelector(default=None, doc="""
A list of source columns to group by. For example ["x", "y"]""")

selectable = param.Boolean(default=True, allow_None=True, doc="""
Whether items are selectable.""")
Whether items are selectable.""")

sort = param.List(default=None, doc="""
How to sort. For example[["x","desc"]]""")
How to sort. For example[["x","desc"]]""")

plugin = param.ObjectSelector(default=_PerspectivePlugin.GRID.value, objects=_PerspectivePlugin.options(), doc="""
The name of a plugin to display the data. For example hypergrid or d3_xy_scatter.""")
The name of a plugin to display the data. For example hypergrid or d3_xy_scatter.""")

theme = param.ObjectSelector(default='material', objects=_PERSPECTIVE_THEMES, doc="""
The style of the PerspectiveViewer. For example material-dark""")
The style of the PerspectiveViewer. For example material-dark""")

view_type = 'perspective'

_field_params = ['columns', 'computed_columns', 'column_pivots', 'row_pivots']

def _get_params(self):
df = self.get_data()
param_values = dict(self.param.get_param_values())
Expand Down

0 comments on commit 0b29e67

Please sign in to comment.