Skip to content

Commit

Permalink
Merge pull request #1084 from astrofrog/table-improvements-2
Browse files Browse the repository at this point in the history
Improvements to table viewer
  • Loading branch information
astrofrog authored Sep 1, 2016
2 parents bd1dcb5 + 2c97cc5 commit 95161e3
Show file tree
Hide file tree
Showing 22 changed files with 575 additions and 195 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ v0.9.0 (unreleased)
* Refactored code related to toolbars in order to make it easier to define
toolbars and toolbar modes that aren't Matplotlib-specific. [#1085]

* Added a new table viewer. [#1084]

* Fix saving/loading of categorical components. [#1084]

* Make it possible for tools to define a status bar message. [#1084]

v0.8.3 (unreleased)
-------------------

Expand Down
11 changes: 7 additions & 4 deletions doc/customizing_guide/toolbar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ The class-level variables set at the start of the class are as follows:
tool that has the same ``tool_id`` as an existing tool already implemented in
glue, you will get an error.

* ``action_text``: a string describing the tool. This is shown in the status bar
at the bottom of the viewer whenever the button is active.
* ``action_text``: a string describing the tool. This is not currently used,
but would be the text that would appear if the tool was accessible by a menu.

* ``tool_tip``: this should be a string that will be shown when the user hovers
above the button in the toolbar. This can include instructions on how to use
Expand Down Expand Up @@ -78,7 +78,7 @@ Checkable tools
^^^^^^^^^^^^^^^

The basic structure for a checkable tool is similar to the above, but with an
additional ``deactivate`` method:
additional ``deactivate`` method, and a ``status_tip`` attribute:

.. code:: python
Expand All @@ -92,6 +92,7 @@ additional ``deactivate`` method:
tool_id = 'custom_tool'
action_text = 'Does cool stuff'
tool_tip = 'Does cool stuff'
status_tip = 'Instructions on what to do now'
shortcut = 'D'
def __init__(self, viewer):
Expand All @@ -109,7 +110,9 @@ additional ``deactivate`` method:
When the tool icon is pressed, the ``activate`` method is called, and when the
button is unchecked (either by clicking on it again, or if the user clicks on
another tool icon), the ``deactivate`` method is called. As before, when the
viewer is closed, the ``close`` method is called.
viewer is closed, the ``close`` method is called. The ``status_tip`` is a
message shown in the status bar of the viewer when the tool is active. This can
be used to provide instructions to the user as to what they should do next.

Drop-down menus
^^^^^^^^^^^^^^^
Expand Down
6 changes: 6 additions & 0 deletions glue/core/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections import OrderedDict

import uuid
import numpy as np
import pandas as pd

Expand Down Expand Up @@ -100,6 +101,11 @@ def __init__(self, label="", **kwargs):

self._key_joins = {}

# To avoid circular references when saving objects with references to
# the data, we make sure that all Data objects have a UUID that can
# uniquely identify them.
self.uuid = str(uuid.uuid4())

@property
def subsets(self):
"""
Expand Down
32 changes: 30 additions & 2 deletions glue/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ def load(rec, context)

from glue.external import six
from glue import core
from glue.core.data import (Data, Component, ComponentID, DerivedComponent,
CoordinateComponent)
from glue.core.data import Data
from glue.core.component_id import ComponentID
from glue.core.component import (Component, CategoricalComponent,
DerivedComponent, CoordinateComponent)
from glue.core.subset import (OPSYM, SYMOP, CompositeSubsetState,
SubsetState, Subset, RoiSubsetState,
InequalitySubsetState, RangeSubsetState)
Expand Down Expand Up @@ -713,6 +715,7 @@ def save_cid_tuple(cids):
return tuple(context.id(cid) for cid in cids)
result['_key_joins'] = [[context.id(k), save_cid_tuple(v0), save_cid_tuple(v1)]
for k, (v0, v1) in data._key_joins.items()]
result['uuid'] = data.uuid
return result


Expand All @@ -724,6 +727,8 @@ def load_cid_tuple(cids):
return tuple(context.object(cid) for cid in cids)
result._key_joins = dict((context.object(k), (load_cid_tuple(v0), load_cid_tuple(v1)))
for k, v0, v1 in rec['_key_joins'])
result.uuid = rec['uuid']


@saver(ComponentID)
def _save_component_id(cid, context):
Expand Down Expand Up @@ -755,6 +760,29 @@ def _load_component(rec, context):
return Component(data=context.object(rec['data']),
units=rec['units'])

@saver(CategoricalComponent)
def _save_categorical_component(component, context):

if not context.include_data and hasattr(component, '_load_log'):
log = component._load_log
return dict(log=context.id(log),
log_item=log.id(component))

return dict(categorical_data=context.do(component.labels),
categories=context.do(component.categories),
jitter_method=context.do(component._jitter_method),
units=component.units)


@loader(CategoricalComponent)
def _load_categorical_component(rec, context):
if 'log' in rec:
return context.object(rec['log']).component(rec['log_item'])

return CategoricalComponent(categorical_data=context.object(rec['categorical_data']),
categories=context.object(rec['categories']),
jitter=context.object(rec['jitter_method']),
units=rec['units'])

@saver(DerivedComponent)
def _save_derived_component(component, context):
Expand Down
34 changes: 23 additions & 11 deletions glue/core/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,29 +879,41 @@ def __setgluestate__(cls, rec, context):

class ElementSubsetState(SubsetState):

def __init__(self, indices=None):
def __init__(self, indices=None, data=None):
super(ElementSubsetState, self).__init__()
self._indices = indices
if data is None:
self._data_uuid = None
else:
self._data_uuid = data.uuid

@memoize
def to_mask(self, data, view=None):
# XXX this is inefficient for views
result = np.zeros(data.shape, dtype=bool)
if self._indices is not None:
result.flat[self._indices] = True
if view is not None:
result = result[view]
return result
if data.uuid == self._data_uuid or self._data_uuid is None:
# XXX this is inefficient for views
result = np.zeros(data.shape, dtype=bool)
if self._indices is not None:
result.flat[self._indices] = True
if view is not None:
result = result[view]
return result
else:
raise IncompatibleAttribute()

def copy(self):
return ElementSubsetState(self._indices)
state = ElementSubsetState(indices=self._indices)
state._data_uuid = self._data_uuid
return state

def __gluestate__(self, context):
return dict(indices=context.do(self._indices))
return dict(indices=context.do(self._indices),
data_uuid=self._data_uuid)

@classmethod
def __setgluestate__(cls, rec, context):
return cls(indices=context.object(rec['indices']))
state = cls(indices=context.object(rec['indices']))
state._data_uuid = rec['data_uuid']
return state


class InequalitySubsetState(SubsetState):
Expand Down
10 changes: 10 additions & 0 deletions glue/core/tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from glue.external import six
from glue import core
from glue.core.component import CategoricalComponent
from glue.tests.helpers import requires_astropy, make_file

from ..data_factories import load_data
Expand Down Expand Up @@ -233,6 +234,15 @@ def test_matplotlib_cmap():
assert clone(cm.gist_heat) is cm.gist_heat


def test_categorical_component():
c = CategoricalComponent(['a','b','c','a','b'], categories=['a','b','c'])
c2 = clone(c)
assert isinstance(c2, CategoricalComponent)
np.testing.assert_array_equal(c.data, [0, 1, 2, 0, 1])
np.testing.assert_array_equal(c.labels, ['a','b','c','a','b'])
np.testing.assert_array_equal(c.categories, ['a','b','c'])


class DummyClass(object):
pass

Expand Down
1 change: 1 addition & 0 deletions glue/core/tests/test_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ class TestSubsetIo(object):
def setup_method(self, method):
self.data = MagicMock(spec=Data)
self.data.shape = (4, 4)
self.data.uuid = 'abcde'
self.subset = Subset(self.data)
inds = np.array([1, 2, 3])
self.subset.subset_state = ElementSubsetState(indices=inds)
Expand Down
Binary file added glue/icons/glue_row_select.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions glue/plugins/tools/spectrum_tool/qt/spectrum_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,8 +834,8 @@ def _setup_toolbar(self):
tb = MatplotlibViewerToolbar(self.widget)

# disable ProfileViewer mouse processing during mouse modes
tb.mode_activated.connect(self.profile.disconnect)
tb.mode_deactivated.connect(self.profile.connect)
tb.tool_activated.connect(self.profile.disconnect)
tb.tool_deactivated.connect(self.profile.connect)

self._menu_toggle_action = QtWidgets.QAction("Options", tb)
self._menu_toggle_action.setCheckable(True)
Expand Down
3 changes: 2 additions & 1 deletion glue/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
from .array import *
from .matplotlib import *
from .misc import *
from .geometry import *
from .geometry import *
from .colors import *
29 changes: 29 additions & 0 deletions glue/utils/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import absolute_import, division, print_function

from matplotlib.colors import ColorConverter

__all__ = ['alpha_blend_colors']

COLOR_CONVERTER = ColorConverter()


def alpha_blend_colors(colors, additional_alpha=1.0):
"""
Given a sequence of colors, return the alpha blended color.
This assumes the last color is the one in front.
"""

srcr, srcg, srcb, srca = COLOR_CONVERTER.to_rgba(colors[0])
srca *= additional_alpha

for color in colors[1:]:
dstr, dstg, dstb, dsta = COLOR_CONVERTER.to_rgba(color)
dsta *= additional_alpha
outa = srca + dsta * (1 - srca)
outr = (srcr * srca + dstr * dsta * (1 - srca)) / outa
outg = (srcg * srca + dstg * dsta * (1 - srca)) / outa
outb = (srcb * srca + dstb * dsta * (1 - srca)) / outa
srca, srcr, srcg, srcb = outa, outr, outg, outb

return srcr, srcg, srcb, srca
9 changes: 5 additions & 4 deletions glue/utils/qt/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
'QColormapCombo']


def mpl_to_qt4_color(color, alpha=1.0):
def mpl_to_qt4_color(color, alpha=None):
"""
Convert a matplotlib color stirng into a Qt QColor object
Expand All @@ -37,9 +37,10 @@ def mpl_to_qt4_color(color, alpha=1.0):
return QtGui.QColor(0, 0, 0, 0)

cc = ColorConverter()
r, g, b = cc.to_rgb(color)
alpha = max(0, min(255, int(256 * alpha)))
return QtGui.QColor(r * 255, g * 255, b * 255, alpha)
r, g, b, a = cc.to_rgba(color)
if alpha is not None:
a = alpha
return QtGui.QColor(r * 255, g * 255, b * 255, a * 255)


def qt4_to_mpl_color(qcolor):
Expand Down
4 changes: 4 additions & 0 deletions glue/viewers/common/qt/data_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,7 @@ def window_title(self):

def update_window_title(self):
self.setWindowTitle(self.window_title)

def set_status(self, message):
sb = self.statusBar()
sb.showMessage(message)
16 changes: 12 additions & 4 deletions glue/viewers/common/qt/mouse_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,23 +361,31 @@ class PolyMode(ClickRoiMode):
tool_tip = ('Lasso a region of interest\n'
' ENTER accepts the path\n'
' ESCAPE clears the path')
status_tip = ('CLICK and DRAG to define lasso, CLICK multiple times to '
'define polygon, ENTER to finalize, ESCAPE to cancel')
shortcut = 'G'

def __init__(self, viewer, **kwargs):
super(PolyMode, self).__init__(viewer, **kwargs)
self._roi_tool = qt_roi.QtPolygonalROI(self._axes)


# TODO: determine why LassoMode exists since it's the same as PolyMode?

@viewer_tool
class LassoMode(RoiMode):
"""
Defines a Polygonal ROI, accessible via the :meth:`~LassoMode.roi` method
"""

icon = 'glue_lasso'
tool_id = 'Lasso'
tool_id = 'select:lasso'
action_text = 'Polygonal ROI'
tool_tip = 'Lasso a region of interest'
tool_tip = ('Lasso a region of interest\n'
' ENTER accepts the path\n'
' ESCAPE clears the path')
status_tip = ('CLICK and DRAG to define lasso, CLICK multiple times to '
'define polygon, ENTER to finalize, ESCAPE to cancel')
shortcut = 'L'

def __init__(self, viewer, **kwargs):
Expand All @@ -395,7 +403,7 @@ class HRangeMode(RoiMode):

icon = 'glue_xrange_select'
tool_id = 'select:xrange'
action_text = 'select:xrange'
action_text = 'X range'
tool_tip = 'Select a range of x values'
shortcut = 'X'

Expand All @@ -414,7 +422,7 @@ class VRangeMode(RoiMode):

icon = 'glue_yrange_select'
tool_id = 'select:yrange'
action_text = 'select:yrange'
action_text = 'Y range'
tool_tip = 'Select a range of y values'
shortcut = 'Y'

Expand Down
8 changes: 4 additions & 4 deletions glue/viewers/common/qt/mpl_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,16 @@ def setup_default_modes(self):

self._connections = []

def activate_mode(self, mode):
def activate_tool(self, mode):
if isinstance(mode, MouseMode):
self._connections.append(self.canvas.mpl_connect('button_press_event', mode.press))
self._connections.append(self.canvas.mpl_connect('motion_notify_event', mode.move))
self._connections.append(self.canvas.mpl_connect('button_release_event', mode.release))
self._connections.append(self.canvas.mpl_connect('key_press_event', mode.key))
super(MatplotlibViewerToolbar, self).activate_mode(mode)
super(MatplotlibViewerToolbar, self).activate_tool(mode)

def deactivate_mode(self, mode):
def deactivate_tool(self, mode):
for connection in self._connections:
self.canvas.mpl_disconnect(connection)
self._connections = []
super(MatplotlibViewerToolbar, self).deactivate_mode(mode)
super(MatplotlibViewerToolbar, self).deactivate_tool(mode)
Loading

0 comments on commit 95161e3

Please sign in to comment.