Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add table viewer based in ipyvuetify #129

Merged
merged 2 commits into from
Oct 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions glue_jupyter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,45 @@ def new_data_viewer(self, *args, **kwargs):
viewer.show()
return viewer

def table(self, *, data=None, x=None, widget='ipyvuetify', viewer_state=None, layer_state=None, show=True):
"""
Open an interactive table viewer.

Parameters
----------
data : str or `~glue.core.data.Data`, optional
The dataset to show in the viewer.
widget : {'ipyvuetify', 'matplotlib'}
Whether to use ipyvuetify or ... as table viewer
viewer_state : `~glue.viewers.common.state.ViewerState`
The initial state for the viewer (advanced).
layer_state : `~glue.viewers.common.state.LayerState`
The initial state for the data layer (advanced).
show : bool, optional
Whether to show the view immediately (`True`) or whether to only
show it later if the ``show()`` method is called explicitly
(`False`).
"""

if widget == 'ipyvuetify':
from .table import TableViewer
viewer_cls = TableViewer
else:
raise ValueError("Widget type should be 'ipyvuetify'")

data = validate_data_argument(self.data_collection, data)

viewer_state_obj = viewer_cls._state_cls()
viewer_state = viewer_state or {}

viewer_state_obj.update_from_dict(viewer_state)

view = self.new_data_viewer(viewer_cls, data=data,
state=viewer_state_obj, show=show)
layer_state = layer_state or {}
view.layers[0].state.update_from_dict(layer_state)
return view

def histogram1d(self, *, data=None, x=None, widget='bqplot', color=None,
x_min=None, x_max=None, n_bin=None, normalize=False,
cumulative=False, viewer_state=None, layer_state=None,
Expand Down
1 change: 1 addition & 0 deletions glue_jupyter/table/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .viewer import TableViewer
63 changes: 63 additions & 0 deletions glue_jupyter/table/table.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<v-slide-x-transition appear>
<v-data-table
dense
hide-default-header
:headers="[...headers]"
:items="items"
:footer-props="{'items-per-page-options': [10,20,50,100]}"
:options.sync="options"
:items_per_page.sync="items_per_page"
:server-items-length="total_length"
class="elevation-1"
>
<template v-slot:header="props">
<thead>
<tr>
<th style="padding: 0 10px">#</th>
<th style="padding: 0 1px" v-if="selection_enabled">
<v-btn icon color="primary" text small @click="apply_filter">
<v-icon>filter_list</v-icon>
</v-btn>
</th>
<th style="padding: 0 1px" v-for="(header, index) in headers_selections" :key="header.text">
<v-icon style="padding: 0 1px" :key="index" :color="selection_colors[index]">brightness_1</v-icon>
</th>
<v-slide-x-transition :key="header.text" v-for="header in headers">
<th >{{ header.text }}</th>
</v-slide-x-transition>
</tr>
</thead>
</template>
<template v-slot:item="props">
<tr>
<td style="padding: 0 10px" class="text-xs-left">
<i>{{ props.item.__row__ }}</i>
</td>
<td style="padding: 0 1px" class="text-xs-left" v-if="selection_enabled">
<v-checkbox
hide-details style="margin-top: 0; padding-top: 0"
:input-value="checked.indexOf(props.item.__row__) != -1"
:key="props.item.__row__"
@change="(value) => select({checked: value, row: props.item.__row__})"
/>
</td>
<td style="padding: 0 1px" :key="header.text" v-for="(header, index) in headers_selections">
<v-fade-transition leave-absolute>
<v-icon
v-if="props.item[header.value]"
v-model="props.item[header.value]"
:color="selection_colors[index]"
>brightness_1</v-icon>
</v-fade-transition>
</td>
<td v-for="header in headers" class="text-xs-right" :key="header.text">
<v-slide-x-transition appear>
<span class="text-truncate" style="display: inline-block">{{ props.item[header.value] }}</span>
</v-slide-x-transition>
</td>
</tr>
</template>
</v-data-table>
</v-slide-x-transition>
</template>
Empty file.
15 changes: 15 additions & 0 deletions glue_jupyter/table/tests/test_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
def test_table(app, dataxyz):
table = app.table(data=dataxyz)
assert len(table.layers) == 1
assert table.widget_table is not None
table.widget_table.checked = [1]
table.apply_filter()
assert len(table.layers) == 2
subset = table.layers[1].layer
assert table.widget_table.selections == [subset.label]
assert [k['text'] for k in table.widget_table.headers_selections] == [subset.label]
assert table.widget_table.selection_colors == [subset.style.color]

app.subset('test', dataxyz.id['x'] > 1)
assert len(table.layers) == 3
assert len(table.widget_table.selections) == 2
202 changes: 202 additions & 0 deletions glue_jupyter/table/viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import os

import traitlets
import ipywidgets as widgets
import ipyvuetify as v

from glue.core.subset import ElementSubsetState
from glue.core.data import Subset
from glue.viewers.common.layer_artist import LayerArtist
from glue.config import viewer_tool
from ..view import IPyWidgetView


with open(os.path.join(os.path.dirname(__file__), "table.vue")) as f:
TEMPLATE = f.read()


class TableBase(v.VuetifyTemplate):
total_length = traitlets.CInt().tag(sync=True)
checked = traitlets.List([]).tag(sync=True) # indices of which rows are selected
items = traitlets.Any().tag(sync=True) # the data, a list of dict
headers = traitlets.Any().tag(sync=True)
headers_selections = traitlets.Any().tag(sync=True)
options = traitlets.Any().tag(sync=True)
items_per_page = traitlets.CInt(11).tag(sync=True)
selections = traitlets.Any([]).tag(sync=True)
selection_colors = traitlets.Any([]).tag(sync=True)
selection_enabled = traitlets.Bool(True).tag(sync=True)

def _update(self):
self._update_columns()

def _update_columns(self):
self.headers = self._get_headers()
self._update_items()

@traitlets.observe('items_per_page')
def _items_per_page(self, change):
self._update_items()

@traitlets.default('headers')
def _headers(self):
return self._get_headers()

@traitlets.observe('selections')
def _on_headers(self, change):
self.headers_selections = self._get_headers_selections()

@traitlets.default('headers_selections')
def _headers_selections(self):
return self._get_headers_selections()

def _get_headers_selections(self):
return [{'text': name, 'value': name, 'sortable': False} for name in self.selections]

@traitlets.default('total_length')
def _total_length(self):
return len(self)

@traitlets.default('options')
def _options(self):
return {'descending': False, 'page': 1, 'itemsPerPage': 10, 'sortBy': None, 'totalItems': len(self)}

def format(self, value):
return value

def _update_items(self):
self.items = self._get_items()

@traitlets.default('items')
def _items(self):
return self._get_items()

@traitlets.observe('options')
def _on_change_options(self, change):
self.items = self._get_items()

@traitlets.default('template')
def _template(self):
with open(os.path.join(os.path.dirname(__file__), "table.vue")) as f:
return f.read()

def vue_apply_filter(self, data):
pass

def vue_select(self, data):
is_checked, row = data['checked'], data['row']
if not is_checked and row in self.checked:
copy = self.checked[:]
del copy[copy.index(row)]
self.checked = copy
if is_checked and row not in self.checked:
self.checked = self.checked + [row]


class TableGlue(TableBase):
data = traitlets.Any() # Glue data object
apply_filter = traitlets.Any() # callback

def __len__(self):
return self.data.shape[0]

def _get_headers(self):
components = [str(k) for k in self.data.main_components + self.data.derived_components]
return [{'text': k, 'value': k, 'sortable': False} for k in components]

def _get_items(self):
page = self.options['page'] - 1
page_size = self.options['itemsPerPage']
i1 = page * page_size
i2 = min(len(self), (page + 1) * page_size)

view = slice(i1, i2)
masks = {k.label: k.to_mask(view) for k in self.data.subsets}

items = []
for i in range(i2 - i1):
item = {'__row__': i + i1} # special key for the row number
for selection in self.selections:
selected = masks[selection][i]
item[selection] = bool(selected)
for j, component in enumerate(self.data.main_components + self.data.derived_components):
item[str(component)] = self.format(self.data[component][i + i1])
items.append(item)
return items

def vue_apply_filter(self, data):
self.apply_filter()


class TableLayerArtist(LayerArtist):
def __init__(self, table_viewer, viewer_state, layer_state=None, layer=None):
self._table_viewer = table_viewer
super(TableLayerArtist, self).__init__(viewer_state, layer_state=layer_state, layer=layer)
self.redraw()

def _refresh(self):
self._table_viewer.redraw()

def redraw(self):
self._refresh()

def update(self):
self._refresh()

def clear(self):
self._refresh()


class TableLayerStateWidget(widgets.VBox):
def __init__(self, layer_state):
super(TableLayerStateWidget, self).__init__()
self.state = layer_state


class TableViewerStateWidget(widgets.VBox):
def __init__(self, viewer_state):
super(TableViewerStateWidget, self).__init__()
self.state = viewer_state


class TableViewer(IPyWidgetView):
allow_duplicate_data = False
allow_duplicate_subset = False
large_data_size = 1e100 # Basically infinite (a googol)

_options_cls = TableViewerStateWidget
_data_artist_cls = TableLayerArtist
_subset_artist_cls = TableLayerArtist
_layer_style_widget_cls = TableLayerStateWidget

tools = []

def __init__(self, session, state=None):
super(TableViewer, self).__init__(session, state=state)
self.widget_table = None

def create_table(self, data):
self.widget_table = TableGlue(data=data, apply_filter=self.apply_filter)
self.create_layout()

def redraw(self):
subsets = [k.layer for k in self.layers if isinstance(k.layer, Subset)]
with self.widget_table.hold_sync():
self.widget_table.selections = [subset.label for subset in subsets]
self.widget_table.selection_colors = [subset.style.color for subset in subsets]
self.widget_table._update()

def apply_filter(self):
selected_rows = self.widget_table.checked
subset_state = ElementSubsetState(indices=selected_rows, data=self.layers[0].layer)
mode = self.session.edit_subset_mode
mode.update(self._data, subset_state, focus_data=self.layers[0].layer)

def add_data(self, data, color=None, alpha=None, **layer_state):
self.create_table(data)
result = super().add_data(data, color=color, alpha=alpha, **layer_state)
return result

@property
def figure_widget(self):
return self.widget_table