Skip to content

Commit

Permalink
Merge f503a4a into 40b2208
Browse files Browse the repository at this point in the history
  • Loading branch information
Carifio24 authored Feb 3, 2022
2 parents 40b2208 + f503a4a commit 2e99024
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 31 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Full changelog
v1.3.0 (unreleased)
-------------------

* Modify scatter viewer so that axis labels are shown in tick marks
when in polar mode. [#2267]

* Remove bundled version of pvextractor and include it as a proper
dependency. [#2252]

Expand Down
88 changes: 81 additions & 7 deletions glue/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class ThetaRadianFormatter(mticker.Formatter):
max_den = 20
limit_den = 10000

def __init__(self, axis_label=None):
super().__init__()
self.axis_label = axis_label

@classmethod
def _check_valid(cls, num, den):
return den != 0 and abs(num) < cls.max_num and den < cls.max_den
Expand Down Expand Up @@ -62,13 +66,64 @@ def rad_fn(cls, x, digits=2):
sgn = "-" if n < 0 else ""
return r"${0}{1}\pi/{2}$".format(sgn, ns, d)

def format_ticks(self, values):
ticks = super().format_ticks(values)
if self.axis_label:
ticks[0] = "{label}={value}".format(label=self.axis_label, value=ticks[0])
return ticks

def __call__(self, x, pos=None):
vmin, vmax = self.axis.get_view_interval()
d = abs(vmax - vmin)
digits = max(-int(np.log10(d) - 1.5), 0)
return ThetaRadianFormatter.rad_fn(x, digits=digits)


class ThetaDegreeFormatter(ThetaFormatter):

def __init__(self, axis_label=None):
super().__init__()
self.axis_label = axis_label

def format_ticks(self, values):
ticks = super().format_ticks(values)
if self.axis_label:
ticks[0] = "{label}={value}".format(label=self.axis_label, value=ticks[0])
return ticks


class PolarRadiusFormatter(ScalarFormatter):

def __init__(self, axis_label=None):
super().__init__()
self.axis_label = axis_label

def format_ticks(self, values):
ticks = super().format_ticks(values)
vmin, vmax = self.axis.get_view_interval()

# Not every tick will necessarily be shown
# We don't want to include ticks outside the view interval

# Which direction are the ticks running?
forwards = vmax > vmin
if forwards:
delta, index = -1, -1

def test(idx):
return values[idx] > vmax and idx >= -len(values)
else:
delta, index = 1, 0

def test(idx):
return values[idx] < vmax and idx < len(values)
while test(index):
index += delta
if self.axis_label:
ticks[index] = "{label}={value}".format(label=self.axis_label, value=ticks[index])
return ticks


def relim(lo, hi, log=False):
logging.getLogger(__name__).debug("Inputs to relim: %r %r", lo, hi)
x, y = lo, hi
Expand Down Expand Up @@ -368,7 +423,6 @@ def visible_limits(artists, axis):


def tick_linker(all_categories, pos, *args):

# We need to take care to ignore negative indices since these would actually
# 'work' 'when accessing all_categories, but we need to avoid that.
if pos < 0 or pos >= len(all_categories):
Expand All @@ -385,7 +439,19 @@ def tick_linker(all_categories, pos, *args):
return ''


def update_ticks(axes, coord, kinds, is_log, categories, projection='rectilinear', radians=True):
def polar_tick_alignment(value, radians):
if radians:
value = value * 180 / np.pi

if value < 90 or 270 < value:
return "left"
elif 90 < value < 270:
return "right"
else:
return "center"


def update_ticks(axes, coord, kinds, is_log, categories, projection='rectilinear', radians=True, label=None):
"""
Changes the axes to have the proper tick formatting based on the type of
component.
Expand Down Expand Up @@ -436,11 +502,19 @@ def update_ticks(axes, coord, kinds, is_log, categories, projection='rectilinear
formatter = FuncFormatter(format_func)
axis.set_major_locator(locator)
axis.set_major_formatter(formatter)
# Have to treat the theta axis of polar plots differently
elif projection == 'polar' and coord == 'x':
axis.set_major_locator(ThetaLocator(AutoLocator()))
formatter = ThetaRadianFormatter if radians else ThetaFormatter
axis.set_major_formatter(formatter())
# Have to treat the axes for polar plots differently
elif projection == 'polar':
if coord == 'x':
axis.set_major_locator(ThetaLocator(AutoLocator()))
formatter_type = ThetaRadianFormatter if radians else ThetaDegreeFormatter
axis.set_major_formatter(formatter_type(label))
for lbl, loc in zip(axis.get_majorticklabels(), axis.get_majorticklocs()):
lbl.set_horizontalalignment(polar_tick_alignment(loc, radians))
else:
axis.set_major_locator(AutoLocator())
axis.set_major_formatter(PolarRadiusFormatter(label))
for lbl in axis.get_majorticklabels():
lbl.set_fontstyle("italic")
else:
axis.set_major_locator(AutoLocator())
axis.set_major_formatter(ScalarFormatter())
5 changes: 4 additions & 1 deletion glue/viewers/matplotlib/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,12 @@ def _script_footer(self):
if mode not in ['aitoff', 'hammer', 'lambert', 'mollweide', 'polar']:
x_log_str = 'log' if self.state.x_log else 'linear'
temp_str += "ax.set_xscale('" + x_log_str + "')\n"
if mode not in ['aitoff', 'hammer', 'lambert', 'mollweide']:
if mode not in ['aitoff', 'hammer', 'lambert', 'mollweide', 'polar']:
y_log_str = 'log' if self.state.y_log else 'linear'
temp_str += "ax.set_yscale('" + y_log_str + "')\n"
if mode == 'polar':
state_dict['x_axislabel'] = ''
state_dict['y_axislabel'] = ''
state_dict['scale_script'] = "# Set scale (log or linear)\n" + temp_str if temp_str else ''
full_sphere = ['aitoff', 'hammer', 'lambert', 'mollweide']
state_dict['limit_script'] = '' if mode in full_sphere else LIMIT_SCRIPT.format(**state_dict)
Expand Down
21 changes: 15 additions & 6 deletions glue/viewers/scatter/python_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ def python_export_scatter_layer(layer, *args):
polar = layer._viewer_state.using_polar
degrees = polar and getattr(layer._viewer_state, 'using_degrees', False)
if polar:
imports += ["from glue.core.util import ThetaRadianFormatter",
"from matplotlib.projections.polar import ThetaFormatter, ThetaLocator",
"from matplotlib.ticker import AutoLocator"]
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 matplotlib.ticker import AutoLocator"
]

script += "layer_handles = [] # for legend"

Expand All @@ -23,11 +26,17 @@ def python_export_scatter_layer(layer, *args):
x_transform_close = ")" if degrees 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 += "keep = ~np.isnan(x) & ~np.isnan(y)\n"
script += "keep = ~np.isnan(x) & ~np.isnan(y)\n\n"
if polar:
script += 'ax.xaxis.set_major_locator(ThetaLocator(AutoLocator()))\n'
formatter = 'ThetaFormatter' if degrees else 'ThetaRadianFormatter'
script += 'ax.xaxis.set_major_formatter({0}())\n\n'.format(formatter)
script += 'ax.xaxis.set_major_formatter({0}("{1}"))\n'.format(theta_formatter, layer._viewer_state.x_axislabel)
script += "for lbl, loc in zip(ax.xaxis.get_majorticklabels(), ax.xaxis.get_majorticklocs()):\n"
script += "\tlbl.set_horizontalalignment(polar_tick_alignment(loc, {0}))\n\n".format(not degrees)

script += 'ax.yaxis.set_major_locator(AutoLocator())\n'
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'

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

Expand Down
15 changes: 15 additions & 0 deletions glue/viewers/scatter/qt/options_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,20 @@ def _update_plot_mode(self, *args):
self.ui.button_flip_x.setVisible(not is_polar)
self.ui.valuetext_x_max.setVisible(not is_polar)
self.ui.bool_x_log.setVisible(not is_polar)

# In polar mode, the axis labels are shown in ticks rather than using the plot axis
# so we adjust the axes editor to account for this
axes_ui = self.ui.axes_editor.ui
axes_ui.label_3.setVisible(not is_polar)
axes_ui.label_4.setVisible(not is_polar)
axes_ui.value_x_axislabel_size.setVisible(not is_polar)
axes_ui.value_y_axislabel_size.setVisible(not is_polar)
axes_ui.combosel_x_axislabel_weight.setVisible(not is_polar)
axes_ui.combosel_y_axislabel_weight.setVisible(not is_polar)
axes_ui.label_2.setText("Θlabel" if is_polar else "xlabel")
axes_ui.label_6.setText("rlabel" if is_polar else "ylabel")
axes_ui.label.setText("Θ" if is_polar else "x")
axes_ui.label_10.setText("r" if is_polar else "y")

self._update_x_attribute()
self._update_y_attribute()
51 changes: 34 additions & 17 deletions glue/viewers/scatter/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,48 @@ def setup_callbacks(self):
self.state.add_callback('x_max', self._x_limits_to_mpl)
self.state.add_callback('y_min', self.limits_to_mpl)
self.state.add_callback('y_max', self.limits_to_mpl)
self.state.add_callback('x_axislabel', self._update_polar_ticks)
self.state.add_callback('y_axislabel', self._update_polar_ticks)
self._update_axes()

def _update_axes(self, *args):

def _update_ticks(self, *args):
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)
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)

def _update_axes(self, *args):

self._update_ticks(args)

if self.state.x_att is not None:

if self.state.x_log:
self.state.x_axislabel = 'Log ' + self.state.x_att.label
elif self.using_polar():
self.state.x_axislabel = ""
else:
self.state.x_axislabel = self.state.x_att.label

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)

if self.state.y_log:
self.state.y_axislabel = 'Log ' + self.state.y_att.label
elif self.using_polar():
self.state.y_axislabel = ""
else:
self.state.y_axislabel = self.state.y_att.label

self.axes.figure.canvas.draw_idle()

def _update_polar_ticks(self, *args):
if self.using_polar():
self._update_ticks(args)
self.axes.figure.canvas.draw_idle()

def _update_projection(self, *args):
self.figure.delaxes(self.axes)
_, self.axes = init_mpl(self.figure, projection=self.state.plot_mode)
Expand Down Expand Up @@ -103,14 +112,22 @@ def _x_limits_to_mpl(self, *args, **kwargs):
self.state.full_circle()
self.limits_to_mpl()

def update_x_axislabel(self, *event):
if self.using_polar():
self.axes.set_xlabel("")
else:
super().update_x_axislabel()

# Because of how the polar plot is drawn, we need to give the y-axis label more padding
# Since this mixin is first in the MRO, we can 'override' the MatplotlibDataViewer method
def update_y_axislabel(self, *event):
labelpad = 25 if self.using_polar() else None
self.axes.set_ylabel(self.state.y_axislabel,
weight=self.state.y_axislabel_weight,
size=self.state.y_axislabel_size,
labelpad=labelpad)
if self.using_polar():
self.axes.set_ylabel("")
else:
self.axes.set_ylabel(self.state.y_axislabel,
weight=self.state.y_axislabel_weight,
size=self.state.y_axislabel_size,
labelpad=None)
self.redraw()

def apply_roi(self, roi, override_mode=None):
Expand Down

0 comments on commit 2e99024

Please sign in to comment.