Skip to content

Commit

Permalink
feat(itool): add copy code to PlotItem vb menu
Browse files Browse the repository at this point in the history
For each plot in imagetool, a new 'copy selection code' button has been added to the right-click menu that copies the code that can slice the data to recreate the data shown in the plot.
  • Loading branch information
kmnhan committed Apr 8, 2024
1 parent 4992251 commit 7b4f30a
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 11 deletions.
15 changes: 12 additions & 3 deletions src/erlab/interactive/imagetool/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import collections
import functools
import gc
import inspect
import os
import time
Expand All @@ -25,7 +24,7 @@
pg_colormap_powernorm,
)
from erlab.interactive.imagetool.slicer import ArraySlicer
from erlab.interactive.utilities import BetterAxisItem
from erlab.interactive.utilities import BetterAxisItem, copy_to_clipboard

if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence
Expand Down Expand Up @@ -452,7 +451,6 @@ def on_close(self):
if hasattr(self, "_data"):
self._data.close()
del self._data
gc.collect()

def connect_axes_signals(self):
for ax in self.axes:
Expand Down Expand Up @@ -1152,6 +1150,9 @@ def __init__(
save_action = self.vb.menu.addAction("Save data as HDF5")
save_action.triggered.connect(lambda: self.save_current_data())

copy_code_action = self.vb.menu.addAction("Copy selection code")
copy_code_action.triggered.connect(self.copy_selection_code)

for i in (0, 1):
self.getViewBoxMenu().ctrl[i].linkCombo.setVisible(False)
self.getViewBoxMenu().ctrl[i].label.setVisible(False)
Expand Down Expand Up @@ -1509,6 +1510,14 @@ def save_current_data(self, fileName=None):
fileName,
)

@QtCore.Slot()
def copy_selection_code(self):
copy_to_clipboard(
self.array_slicer.qsel_code(
self.slicer_area.current_cursor, self.display_axis
)
)

@property
def display_axis(self) -> tuple[int, ...]:
return self._display_axis
Expand Down
79 changes: 71 additions & 8 deletions src/erlab/interactive/imagetool/slicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,18 +576,81 @@ def index_of_value(
else:
return _index_of_value_nonuniform(self.coords[axis], value)

def gen_selection_code(self, cursor: int, disp: Sequence[int]) -> str:
def isel_args(
self, cursor: int, disp: Sequence[int], int_if_one: bool = False
) -> dict[str, slice | int]:
axis = sorted(set(range(self._obj.ndim)) - set(disp))
slice_dict = {
self._obj.dims[ax]: self._bin_slice(cursor, ax, int_if_one=True)
for ax in axis
return {
self._obj.dims[ax]: self._bin_slice(cursor, ax, int_if_one) for ax in axis
}
return f".isel(**{slice_dict!s}).squeeze()"

def qsel_args(self, cursor: int, disp: Sequence[int]) -> dict:
out: dict[str, float] = {}
binned = self.get_binned(cursor)

for dim, selector in self.isel_args(cursor, disp, int_if_one=True).items():
inc = self.incs[self._obj.dims.index(dim)]
order = int(-np.floor(np.log10(inc)) + 1)

if binned[self._obj.dims.index(dim)]:
coord = self._obj[dim][selector].values

out[dim] = np.round(coord.mean(), order)
width = np.round(abs(coord[-1] - coord[0]) + inc, order)

if not np.allclose(
self._obj[dim]
.sel({dim: slice(out[dim] - width / 2, out[dim] + width / 2)})
.values,
coord,
):
raise ValueError(
"Bin does not contain the same values as the original data."
)

out[dim + "_width"] = width

else:
out[dim] = np.round(self._obj[dim].values[selector], order)

return out

def qsel_code(self, cursor: int, disp: Sequence[int]) -> str:
if self._nonuniform_axes:
# Has non-uniform axes, fallback to isel
return self.isel_code(cursor, disp)

try:
qsel_kw = self.qsel_args(cursor, disp)
except ValueError:
return self.isel_code(cursor, disp)

dict_repr: str = ""
for k, v in qsel_kw.items():
dict_repr += f"{k}={v!s}, "
dict_repr = dict_repr.rstrip(", ")
return f".qsel({dict_repr})"

def isel_code(self, cursor: int, disp: Sequence[int]) -> str:
dict_repr: str = ""
for k, v in self.isel_args(cursor, disp, int_if_one=True).items():
dict_repr += f"{k}={v!s}, "
dict_repr = dict_repr.rstrip(", ")
return f".isel({dict_repr})"

def xslice(self, cursor: int, disp: Sequence[int]) -> xr.DataArray:
axis = sorted(set(range(self._obj.ndim)) - set(disp))
slices = {self._obj.dims[ax]: self._bin_slice(cursor, ax) for ax in axis}
return self._obj.isel(**slices).squeeze()
isel_kw: dict[str, slice] = self.isel_args(cursor, disp, int_if_one=False)
binned_coord_average: dict[str, xr.DataArray] = {
k: self._obj[k][isel_kw[k]].mean()
for k, v in zip(self._obj.dims, self.get_binned(cursor))
if v
}
return (
self._obj.isel(**isel_kw)
.squeeze()
.mean(binned_coord_average.keys())
.assign_coords(**binned_coord_average)
)

@QtCore.Slot(int, tuple, result=np.ndarray)
def slice_with_coord(
Expand Down

0 comments on commit 7b4f30a

Please sign in to comment.