Skip to content

Commit

Permalink
Add ability to plot tables in PlotViewer
Browse files Browse the repository at this point in the history
- basically combines the column selection logic from PlotTable
  plugin into the PlotViewer.
- When presented with an AstroTable, you can choose between opening
  it with the TableViewer or the PlotViewer
  • Loading branch information
ejeschke committed Jun 7, 2024
1 parent d4f5454 commit 067bff9
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 23 deletions.
7 changes: 4 additions & 3 deletions doc/WhatsNew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ What's New
Ver 5.2.0 (unreleased)
======================
- Substituted puremagic package for python-magic (works better across
platforms)
- Fixed an issue with the mouse wheel event scrolling MDI workspaces
platforms).
- Fixed an issue with the mouse wheel event scrolling MDI workspaces.
- Fixed a spurious warning when moving the cursor in the Pan plugin
window and a table or plot viewer is running in the channel
window and a table or plot viewer is running in the channel.
- Enhanced PlotViewer can plot from 1D images or (new) tables.

Ver 5.1.0 (2024-05-22)
======================
Expand Down
181 changes: 161 additions & 20 deletions ginga/gw/PlotView.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import logging
import numpy as np

from ginga.misc import Callback, Settings
from ginga.misc import Callback, Settings, Bunch
from ginga import AstroImage
from ginga.table import AstroTable

from ginga.gw import Widgets
try:
Expand All @@ -24,11 +25,14 @@ class PlotViewGw(Callback.Callbacks):
"""

vname = 'Ginga Plot'
vtypes = [AstroImage.AstroImage]
vtypes = [AstroImage.AstroImage, AstroTable.AstroTable]

@classmethod
def viewable(cls, dataobj):
"""Test whether `dataobj` is viewable by this viewer."""
if isinstance(dataobj, AstroTable.AstroTable):
# TODO: check for at least 2 columns?
return True
if not isinstance(dataobj, AstroImage.AstroImage):
return False
shp = list(dataobj.shape)
Expand All @@ -44,11 +48,21 @@ def __init__(self, logger=None, settings=None):
else:
self.logger = logging.Logger('PlotView')

# To store all active table info
self.tab = None
self.cols = []
self._idx = []
self._idxname = '_idx'
# To store selected columns names of active table
self.x_col = ''
self.y_col = ''

# Create settings and set defaults
if settings is None:
settings = Settings.SettingGroup(logger=self.logger)
self.settings = settings
self.settings.add_defaults(plot_bg='white', show_marker=False,
x_index=1, y_index=2,
linewidth=1, linestyle='-',
linecolor='blue', markersize=6,
markerwidth=0.5, markercolor='red',
Expand Down Expand Up @@ -80,7 +94,9 @@ def __init__(self, logger=None, settings=None):

top.add_widget(self.plot_w, stretch=1)

captions = (('Log X', 'checkbutton', 'Log Y', 'checkbutton',
captions = (('X:', 'label', 'x_combo', 'combobox',
'Y:', 'label', 'y_combo', 'combobox'),
('Log X', 'checkbutton', 'Log Y', 'checkbutton',
'Show Marker', 'checkbutton'),
('X Low:', 'label', 'x_lo', 'entry',
'X High:', 'label', 'x_hi', 'entry',
Expand All @@ -96,6 +112,20 @@ def __init__(self, logger=None, settings=None):

top.add_widget(w, stretch=0)

# Controls for X-axis column listing
combobox = b.x_combo
combobox.add_callback('activated', self.x_select_cb)
self.w.xcombo = combobox
combobox.set_enabled(False)
combobox.set_tooltip('Select a column to plot on X-axis')

# Controls for Y-axis column listing
combobox = b.y_combo
combobox.add_callback('activated', self.y_select_cb)
self.w.ycombo = combobox
combobox.set_enabled(False)
combobox.set_tooltip('Select a column to plot on Y-axis')

b.log_x.set_state(self.line_plot.logx)
b.log_x.add_callback('activated', self.log_x_cb)
b.log_x.set_tooltip('Plot X-axis in log scale')
Expand Down Expand Up @@ -161,6 +191,31 @@ def set_dataobj(self, dataobj):

self._dataobj = dataobj

if isinstance(self._dataobj, AstroTable.AstroTable):
# <-- dealing with a table
self.clear_data()
self.w.x_combo.set_enabled(True)
self.w.y_combo.set_enabled(True)
# Generate column indices
self.tab = dataobj.get_data()
self._idx = np.arange(len(self.tab))

# Populate combobox with table column names
self.cols = [self._idxname] + self.tab.colnames
self.x_col = self._set_combobox('xcombo', self.cols,
default=self.settings.get('x_index', 1))
self.y_col = self._set_combobox('ycombo', self.cols,
default=self.settings.get('y_index', 2))

elif isinstance(self._dataobj, AstroImage.AstroImage):
# <-- dealing with a 1D image
self.clear_data()
self.w.x_combo.set_enabled(False)
self.w.y_combo.set_enabled(False)

else:
raise ValueError("I don't know how to handle type '{}'".format(type(self._dataobj)))

self.do_plot(reset_xlimits=True, reset_ylimits=True)

self.make_callback('image-set', dataobj)
Expand All @@ -170,6 +225,13 @@ def get_dataobj(self):

def clear_data(self):
"""Clear comboboxes and columns."""
self.tab = None
self.cols = []
self._idx = []
self.x_col = ''
self.y_col = ''
self.w.xcombo.clear()
self.w.ycombo.clear()
self.w.x_lo.set_text('')
self.w.x_hi.set_text('')
self.w.y_lo.set_text('')
Expand Down Expand Up @@ -198,13 +260,12 @@ def do_plot(self, reset_xlimits=True, reset_ylimits=True):
plt_kw['mec'] = plt_kw['mfc']

try:
x_data, y_data = self.get_plot_data()
marker = self.get_marker()
p_dat = self.get_plot_data()

self.line_plot.plot(
x_data, y_data,
xtitle=self.get_label('x'), ytitle=self.get_label('y'),
marker=marker, **plt_kw)
p_dat.x_data, p_dat.y_data,
xtitle=p_dat.x_label, ytitle=p_dat.y_label,
marker=p_dat.marker, **plt_kw)

if not reset_xlimits:
self.set_xlim_cb()
Expand All @@ -215,10 +276,21 @@ def do_plot(self, reset_xlimits=True, reset_ylimits=True):
self.set_ylimits_widgets()

except Exception as e:
self.logger.error(str(e))
self.logger.error(str(e), exc_info=True)
else:
self.save_plot.set_enabled(True)

def _set_combobox(self, attrname, vals, default=0):
"""Populate combobox with given list."""
combobox = getattr(self.w, attrname)
for val in vals:
combobox.append_text(val)
if default > len(vals):
default = 0
val = vals[default]
combobox.show_text(val)
return val

def set_xlimits_widgets(self, set_min=True, set_max=True):
"""Populate axis limits GUI with current plot values."""
xmin, xmax = self.line_plot.ax.get_xlim()
Expand All @@ -244,10 +316,71 @@ def limits_cb(self, plot, dct):

def get_plot_data(self):
"""Extract only good data point for plotting."""
y_data = self._dataobj.get_data()
x_data = np.arange(len(y_data))
if isinstance(self._dataobj, AstroImage.AstroImage):
y_data = self._dataobj.get_data()
x_data = np.arange(len(y_data))
x_label = 'Index'
y_label = 'Value'

elif isinstance(self._dataobj, AstroTable.AstroTable):
if self.x_col == self._idxname:
x_data = self._idx
else:
x_data = self.tab[self.x_col].data
x_label = self._get_label('x')

if self.y_col == self._idxname:
y_data = self._idx
else:
y_data = self.tab[self.y_col].data
y_label = self._get_label('y')

if self.tab.masked:
if self.x_col == self._idxname:
x_mask = np.ones_like(self._idx, dtype=bool)
else:
x_mask = ~self.tab[self.x_col].mask

if self.y_col == self._idxname:
y_mask = np.ones_like(self._idx, dtype=bool)
else:
y_mask = ~self.tab[self.y_col].mask

mask = x_mask & y_mask
x_data = x_data[mask]
y_data = y_data[mask]

if len(x_data) > 1:
i = np.argsort(x_data) # Sort X-axis to avoid messy line plot
x_data = x_data[i]
y_data = y_data[i]

return x_data, y_data
else:
raise ValueError("I don't know how to get plot data from type '{}'".format(type(self._dataobj)))

marker = self.get_marker()

return Bunch.Bunch(x_data=x_data, y_data=y_data, marker=marker,
x_label=x_label, y_label=y_label)

def _get_label(self, axis):
"""Return plot label for column for the given axis."""

if axis == 'x':
colname = self.x_col
else: # y
colname = self.y_col

if colname == self._idxname:
label = 'Index'
else:
col = self.tab[colname]
if col.unit:
label = '{0} ({1})'.format(col.name, col.unit)
else:
label = col.name

return label

def get_marker(self):
_marker_type = self.settings.get('markerstyle', 'o')
Expand All @@ -257,15 +390,23 @@ def get_marker(self):

return _marker_type

def get_label(self, axis):
"""Return plot label for the given axis."""

if axis == 'x':
label = 'Index'
if axis == 'y':
label = 'Value'
def x_select_cb(self, w, index):
"""Callback to set X-axis column."""
try:
self.x_col = self.cols[index]
except IndexError as e:
self.logger.error(str(e))
else:
self.do_plot(reset_xlimits=True, reset_ylimits=False)

return label
def y_select_cb(self, w, index):
"""Callback to set Y-axis column."""
try:
self.y_col = self.cols[index]
except IndexError as e:
self.logger.error(str(e))
else:
self.do_plot(reset_xlimits=False, reset_ylimits=True)

def log_x_cb(self, w, val):
"""Toggle linear/log scale for X-axis."""
Expand Down

0 comments on commit 067bff9

Please sign in to comment.