Skip to content
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
22 changes: 17 additions & 5 deletions src/easydynamics/utils/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import plopp as pp
import scipp as sc
from plopp.backends.matplotlib.figure import WidgetFigure
from plopp.backends.matplotlib.utils import make_figure
from plopp.plotting._slicer import SlicerPlot
from plopp.plotting._slicer import _maybe_reduce_dim
from plopp.widgets import slice_dims
Expand Down Expand Up @@ -76,10 +77,23 @@ def slicerplot_with_residuals(
if residuals_key not in dg:
raise ValueError(f"Residuals key '{residuals_key}' not found in DataGroup")

# Draw the data and the residuals as two axes of one Matplotlib figure with a shared x axis.
# Two separate figures are laid out independently, so their axes only line up when their y
# tick labels and y labels happen to be equally wide.
figsize = kwargs.pop('figsize', None)
mpl_fig = make_figure(figsize=(6.0, 6.0) if figsize is None else figsize)
main_ax, res_ax = mpl_fig.subplots(
nrows=2,
ncols=1,
sharex=True,
gridspec_kw={'height_ratios': [2, 1]},
)

sp = SlicerPlot(
{k: da for k, da in dg.items() if k != residuals_key},
keep=keep,
operation=operation,
ax=main_ax,
**kwargs,
)

Expand All @@ -92,15 +106,13 @@ def slicerplot_with_residuals(
if 'autoscale' in kwargs:
res_fig_kwargs['autoscale'] = kwargs['autoscale']

res_fig = pp.linefigure(reduce_res, figsize=(6, 2), **res_fig_kwargs)

res_fig.layout.margin = '0px'
res_fig.layout.padding = '0px'
# The log buttons of the main plot also apply to the shared x axis, and a log y axis makes
# no sense for residuals, which can be negative.
res_fig = pp.linefigure(reduce_res, ax=res_ax, hide_log_buttons=True, **res_fig_kwargs)

fig1 = sp.figure
for widget in fig1.bottom_bar[0].controls.values():
widget.slider_toggler.value = '-o-'
fig1.bottom_bar.children = [pp.widgets.VBar([res_fig, fig1.bottom_bar.children[0]])]

res_fig.autoscale()

Expand Down
76 changes: 76 additions & 0 deletions tests/unit/easydynamics/utils/test_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,79 @@ def test_slicerplot_with_residuals_hides_top_x_axis(self, datagroup):
mock_fig.ax.xaxis.label.set_visible.assert_called_once_with(False)
mock_fig.ax.tick_params.assert_called_once_with(labelbottom=False)
mock_fig.autoscale.assert_called_once()

@pytest.mark.parametrize(
('figsize', 'expected_figsize'),
[
(None, (6.0, 6.0)),
((8.0, 5.0), (8.0, 5.0)),
],
ids=['Default', 'Custom'],
)
def test_slicerplot_with_residuals_draws_both_panels_in_one_figure(
self, datagroup, figsize, expected_figsize
):
# WHEN
mock_sp = MagicMock()
mock_sp.slicer.keep = ['x']
kwargs = {} if figsize is None else {'figsize': figsize}

with (
patch(
'easydynamics.utils.plotting.SlicerPlot',
return_value=mock_sp,
) as slicer_plot,
patch(
'easydynamics.utils.plotting.pp.linefigure', return_value=MagicMock()
) as linefigure,
patch('easydynamics.utils.plotting.slice_dims'),
patch('easydynamics.utils.plotting.pp.Node'),
):
# THEN
slicerplot_with_residuals(datagroup, **kwargs)

# EXPECT the data and residuals axes belong to one figure and share their x axis
main_ax = slicer_plot.call_args.kwargs['ax']
res_ax = linefigure.call_args.kwargs['ax']

assert main_ax.get_figure() is res_ax.get_figure()
assert main_ax.get_shared_x_axes().joined(main_ax, res_ax)
assert tuple(main_ax.get_figure().get_size_inches()) == expected_figsize
assert 'figsize' not in slicer_plot.call_args.kwargs
assert linefigure.call_args.kwargs['hide_log_buttons'] is True

def test_slicerplot_with_residuals_aligns_data_and_residuals_axes(self):
# WHEN the plopp slicer needs an interactive matplotlib backend, switched in for the
# test, and the two panels have y tick labels of different widths
import matplotlib.pyplot as plt

energy = sc.linspace('energy', -1.0, 1.0, 11, unit='meV')
Q = sc.array(dims=['Q'], values=[0.5, 1.0], unit='1/angstrom')

def data_array(values):
return sc.DataArray(
sc.array(dims=['Q', 'energy'], values=[values, values]),
coords={'energy': energy, 'Q': Q},
)

datagroup = sc.DataGroup(
Data=data_array([1000.0 * (i + 1) for i in range(11)]),
Residuals=data_array([0.1 * (-1) ** i for i in range(11)]),
)

plt.switch_backend('module://ipympl.backend_nbagg')
try:
# THEN
fig = slicerplot_with_residuals(datagroup, keep='energy')
fig.canvas.to_widget()

# EXPECT the axes span the same x range on screen and in data
main_ax, res_ax = fig.canvas.fig.axes
main_box = main_ax.get_window_extent()
res_box = res_ax.get_window_extent()

assert main_box.x0 == pytest.approx(res_box.x0)
assert main_box.x1 == pytest.approx(res_box.x1)
assert main_ax.get_xlim() == res_ax.get_xlim()
finally:
plt.switch_backend('Agg')
Loading