Skip to content

Commit

Permalink
Merge pull request #2279 from Carifio24/proj-angle-units
Browse files Browse the repository at this point in the history
Angle units in full-sphere projections
  • Loading branch information
astrofrog committed May 19, 2022
2 parents c6fda7b + 5c64fe2 commit bc93828
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ v1.4.0 (unreleased)
* Modify profile viewer so that when in 'Sum' mode, parts of profiles
with no valid values are NaN rather than zero.

* Add support for using degrees in full-sphere projections. [#2279]

v1.3.0 (2022-04-22)
-------------------

Expand Down
12 changes: 8 additions & 4 deletions glue/core/roi_pretransforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ def __setgluestate__(cls, rec, context):
class RadianTransform(object):
# We define 'next_transform' so that this pre-transform can
# be chained together with another transformation, if desired
def __init__(self, next_transform=None):
def __init__(self, coords=[], next_transform=None):
self._next_transform = next_transform
self._state = {"next_transform": next_transform}
self._coords = coords
self._state = {"coords": coords, "next_transform": next_transform}

def __call__(self, x, y):
x = np.deg2rad(x)
if 'x' in self._coords:
x = np.deg2rad(x)
if 'y' in self._coords:
y = np.deg2rad(y)
if self._next_transform is not None:
return self._next_transform(x, y)
else:
Expand All @@ -52,4 +56,4 @@ def __gluestate__(self, context):
@classmethod
def __setgluestate__(cls, rec, context):
state = context.object(rec['state'])
return cls(state['next_transform'])
return cls(state['coords'], state['next_transform'])
12 changes: 12 additions & 0 deletions glue/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,3 +1247,15 @@ def apply_inplace_patches(rec):
load_log = rec[rec[comp]['log']]
if 'force_coords' not in load_log:
load_log['force_coords'] = True

# The following accounts for the addition of the degree mode to the
# full-sphere projection. Originally, this was only used for polar mode
# and so the `coords` parameter was not needed. If this is not present,
# the plot is polar and we can set coords to be ['x']
if value['_type'] == 'glue.core.roi_pretransforms.RadianTransform':
if 'state' in value and value['state'] is not None:
state = value['state']
if 'contents' in state and state['contents'] is not None:
contents = state['contents']
if 'st__coords' not in contents:
contents['st__coords'] = ['x']
12 changes: 8 additions & 4 deletions glue/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from matplotlib.projections.polar import ThetaFormatter, ThetaLocator
import matplotlib.ticker as mticker

__all__ = ["ThetaRadianFormatter", "relim", "split_component_view", "join_component_view",
__all__ = ["ThetaRadianFormatter", "ThetaDegreeFormatter", "PolarRadiusFormatter",
"relim", "split_component_view", "join_component_view",
"facet_subsets", "colorize_subsets", "disambiguate",
'small_view', 'small_view_array', 'visible_limits',
'tick_linker', 'update_ticks']
Expand Down Expand Up @@ -57,7 +58,8 @@ def rad_fn(cls, x, digits=2):
if n is None or d is None:
return "{value:0.{digits}f}".format(value=x, digits=digits)

ns = "" if n == 1 else str(n)
absn = abs(n)
ns = "" if absn == 1 else str(absn)
if n == 0:
return "0"
elif d == 1:
Expand Down Expand Up @@ -473,8 +475,7 @@ def update_ticks(axes, coord, kinds, is_log, categories, projection='rectilinear
Currently only the scatter viewer supports different projections.
"""

# Short circuit the full-sphere projections
if projection in ['aitoff', 'hammer', 'mollweide', 'lambert']:
if projection == 'lambert' and coord == 'y':
return

if coord == 'x':
Expand Down Expand Up @@ -515,6 +516,9 @@ def update_ticks(axes, coord, kinds, is_log, categories, projection='rectilinear
axis.set_major_formatter(PolarRadiusFormatter(label))
for lbl in axis.get_majorticklabels():
lbl.set_fontstyle("italic")
elif projection in ['aitoff', 'hammer', 'mollweide', 'lambert']:
formatter_type = ThetaRadianFormatter if radians else ThetaDegreeFormatter
axis.set_major_formatter(formatter_type())
else:
axis.set_major_locator(AutoLocator())
axis.set_major_formatter(ScalarFormatter())
6 changes: 5 additions & 1 deletion glue/viewers/scatter/layer_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,12 @@ def _update_data(self):
self.scatter_artist.set_offsets(np.zeros((0, 2)))
else:

if getattr(self._viewer_state, 'using_degrees', False):
full_sphere = getattr(self._viewer_state, 'using_full_sphere', False)
degrees = getattr(self._viewer_state, 'using_degrees', False)
if degrees:
x = np.radians(x)
if full_sphere:
y = np.radians(y)

self.density_artist.set_label(None)
if self._use_plot_artist():
Expand Down
21 changes: 16 additions & 5 deletions glue/viewers/scatter/python_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ def python_export_scatter_layer(layer, *args):
imports = ["import numpy as np"]

polar = layer._viewer_state.using_polar
degrees = polar and getattr(layer._viewer_state, 'using_degrees', False)
full_sphere = layer._viewer_state.using_full_sphere
degrees = layer._viewer_state.using_degrees
theta_formatter = 'ThetaDegreeFormatter' if degrees else 'ThetaRadianFormatter'
if polar or full_sphere:
imports += [
"from glue.core.util import {0}".format(theta_formatter),
]
if polar:
theta_formatter = 'ThetaDegreeFormatter' if degrees else 'ThetaRadianFormatter'
imports += [
"from glue.core.util import polar_tick_alignment, PolarRadiusFormatter, {0}".format(theta_formatter),
"from matplotlib.projections.polar import ThetaFormatter, ThetaLocator",
"from glue.core.util import PolarRadiusFormatter, polar_tick_alignment",
"from matplotlib.projections.polar import ThetaLocator",
"from matplotlib.ticker import AutoLocator"
]

Expand All @@ -24,8 +29,10 @@ def python_export_scatter_layer(layer, *args):
script += "# Get main data values\n"
x_transform_open = "np.radians(" if degrees else ""
x_transform_close = ")" if degrees else ""
y_transform_open = "np.radians(" if degrees and full_sphere else ""
y_transform_close = ")" if degrees and full_sphere else ""
script += "x = {0}layer_data['{1}']{2}\n".format(x_transform_open, layer._viewer_state.x_att.label, x_transform_close)
script += "y = layer_data['{0}']\n".format(layer._viewer_state.y_att.label)
script += "y = {0}layer_data['{1}']{2}\n".format(y_transform_open, layer._viewer_state.y_att.label, y_transform_close)
script += "keep = ~np.isnan(x) & ~np.isnan(y)\n\n"
if polar:
script += 'ax.xaxis.set_major_locator(ThetaLocator(AutoLocator()))\n'
Expand All @@ -37,6 +44,10 @@ def python_export_scatter_layer(layer, *args):
script += 'ax.yaxis.set_major_formatter(PolarRadiusFormatter("{0}"))\n'.format(layer._viewer_state.y_axislabel)
script += 'for lbl in ax.yaxis.get_majorticklabels():\n'
script += '\tlbl.set_fontstyle(\'italic\')\n\n'
elif full_sphere:
script += 'ax.xaxis.set_major_formatter({0}())\n'.format(theta_formatter)
if layer._viewer_state.plot_mode != 'lambert':
script += 'ax.yaxis.set_major_formatter({0}())\n'.format(theta_formatter)

if layer.state.cmap_mode == 'Linear':

Expand Down
9 changes: 5 additions & 4 deletions glue/viewers/scatter/qt/options_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _get_labels(proj):
elif proj == 'polar':
return 'theta', 'radius'
elif proj in ['aitoff', 'hammer', 'lambert', 'mollweide']:
return 'long (rad)', 'lat (rad)'
return 'long', 'lat'
else:
return 'axis 1', 'axis 2'

Expand Down Expand Up @@ -85,16 +85,17 @@ def _update_plot_mode(self, *args):
self.ui.y_lab.setText(y_label)
self.ui.y_lab_2.setText(y_label)
lim_enabled = self.viewer_state.plot_mode not in ['aitoff', 'hammer', 'lambert', 'mollweide']
is_polar = self.viewer_state.plot_mode == 'polar'
is_polar = self.viewer_state.using_polar
is_rect = self.viewer_state.using_rectilinear
self.ui.valuetext_x_min.setEnabled(lim_enabled)
self.ui.button_flip_x.setEnabled(lim_enabled)
self.ui.valuetext_x_max.setEnabled(lim_enabled)
self.ui.valuetext_y_min.setEnabled(lim_enabled)
self.ui.button_flip_y.setEnabled(lim_enabled)
self.ui.valuetext_y_max.setEnabled(lim_enabled)
self.ui.button_full_circle.setVisible(False)
self.ui.angle_unit_lab.setVisible(is_polar)
self.ui.combosel_angle_unit.setVisible(is_polar)
self.ui.angle_unit_lab.setVisible(not is_rect)
self.ui.combosel_angle_unit.setVisible(not is_rect)
self.ui.x_lab_2.setVisible(not is_polar)
self.ui.valuetext_x_min.setVisible(not is_polar)
self.ui.button_flip_x.setVisible(not is_polar)
Expand Down
22 changes: 21 additions & 1 deletion glue/viewers/scatter/qt/tests/test_python_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,27 @@ def test_simple_polar_plot_radians(self, tmpdir):
self.viewer.state.y_att = self.data.id['d']
self.assert_same(tmpdir)

def test_full_sphere(self, tmpdir):
def test_full_sphere_degrees(self, tmpdir):
self.viewer.state.angle_unit = 'degrees'
self.viewer.state.plot_mode = 'aitoff'
self.viewer.state.x_att = self.data.id['c']
self.viewer.state.y_att = self.data.id['d']
self.assert_same(tmpdir)
self.viewer.state.plot_mode = 'hammer'
self.viewer.state.x_att = self.data.id['e']
self.viewer.state.y_att = self.data.id['f']
self.assert_same(tmpdir)
self.viewer.state.plot_mode = 'lambert'
self.viewer.state.x_att = self.data.id['g']
self.viewer.state.y_att = self.data.id['h']
self.assert_same(tmpdir)
self.viewer.state.plot_mode = 'mollweide'
self.viewer.state.x_att = self.data.id['a']
self.viewer.state.y_att = self.data.id['b']
self.assert_same(tmpdir)

def test_full_sphere_radians(self, tmpdir):
self.viewer.state.angle_unit = 'radians'
self.viewer.state.plot_mode = 'aitoff'
self.viewer.state.x_att = self.data.id['c']
self.viewer.state.y_att = self.data.id['d']
Expand Down
16 changes: 12 additions & 4 deletions glue/viewers/scatter/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ScatterViewerState(MatplotlibDataViewerState):
y_att = DDSCProperty(docstring='The attribute to show on the y-axis', default_index=1)
dpi = DDCProperty(72, docstring='The resolution (in dots per inch) of density maps, if present')
plot_mode = DDSCProperty(docstring="Whether to plot the data in cartesian, polar or another projection")
angle_unit = DDSCProperty(docstring="When plotting in polar mode, whether to use radians or degrees for the angles")
angle_unit = DDSCProperty(docstring="Whether to use radians or degrees for any angular coordinates")

def __init__(self, **kwargs):

Expand Down Expand Up @@ -96,17 +96,25 @@ def flip_y(self):
"""
self.y_lim_helper.flip_limits()

@property
def using_rectilinear(self):
return self.plot_mode == 'rectilinear'

@property
def using_polar(self):
return self.plot_mode == 'polar'

@property
def using_full_sphere(self):
return self.plot_mode in ['aitoff', 'hammer', 'mollweide', 'lambert']

@property
def using_degrees(self):
return self.using_polar and self.angle_unit == 'degrees'
return (self.using_polar or self.using_full_sphere) and self.angle_unit == 'degrees'

@property
def using_radians(self):
return self.using_polar and self.angle_unit == 'radians'
return not self.using_rectilinear and self.angle_unit == 'radians'

def full_circle(self):
if not self.using_polar:
Expand Down Expand Up @@ -348,7 +356,7 @@ def __init__(self, viewer_state=None, layer=None, **kwargs):
self.update_from_dict(kwargs)

def _update_points_mode(self, *args):
if getattr(self.viewer_state, 'using_polar', False):
if getattr(self.viewer_state, 'using_polar', False) or getattr(self.viewer_state, 'using_full_sphere', False):
self.points_mode_helper.choices = ['markers']
self.points_mode_helper.select = 'markers'
else:
Expand Down
14 changes: 9 additions & 5 deletions glue/viewers/scatter/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ def setup_callbacks(self):
self._update_axes()

def _update_ticks(self, *args):
radians = hasattr(self.state, 'angle_unit') and self.state.angle_unit == 'radians'
if self.state.x_att is not None:
# Update ticks, which sets the labels to categories if components are categorical
radians = hasattr(self.state, 'angle_unit') and self.state.angle_unit == 'radians'
update_ticks(self.axes, 'x', self.state.x_kinds, self.state.x_log,
self.state.x_categories, projection=self.state.plot_mode, radians=radians,
label=self.state.x_axislabel)

if self.state.y_att is not None:
# Update ticks, which sets the labels to categories if components are categorical
update_ticks(self.axes, 'y', self.state.y_kinds, self.state.y_log,
self.state.y_categories, projection=self.state.plot_mode, label=self.state.y_axislabel)
self.state.y_categories, projection=self.state.plot_mode, radians=radians, label=self.state.y_axislabel)

def _update_axes(self, *args):

Expand Down Expand Up @@ -99,6 +99,9 @@ def _update_projection(self, *args):

self.figure.canvas.draw_idle()

def using_rectilinear(self):
return self.state.plot_mode == 'rectilinear'

def using_polar(self):
return self.state.plot_mode == 'polar'

Expand Down Expand Up @@ -149,7 +152,7 @@ def apply_roi(self, roi, override_mode=None):
roi = roi.transformed(xfunc=mpl_to_datetime64 if x_date else None,
yfunc=mpl_to_datetime64 if y_date else None)

use_transform = self.state.plot_mode != 'rectilinear'
use_transform = not self.using_rectilinear()
subset_state = roi_to_subset_state(roi,
x_att=self.state.x_att, x_categories=self.state.x_categories,
y_att=self.state.y_att, y_categories=self.state.y_categories,
Expand All @@ -162,8 +165,9 @@ def apply_roi(self, roi, override_mode=None):
self.axes.get_yscale())

# If we're using degrees, we need to staple on the degrees -> radians conversion beforehand
if self.using_polar() and self.state.angle_unit == 'degrees':
transform = RadianTransform(next_transform=transform)
if self.state.using_degrees:
coords = ['x'] if self.using_polar() else ['x', 'y']
transform = RadianTransform(coords=coords, next_transform=transform)
subset_state.pretransform = transform

self.apply_subset_state(subset_state, override_mode=override_mode)
Expand Down

0 comments on commit bc93828

Please sign in to comment.