Skip to content

Commit 73bdb61

Browse files
committed
feat: support mixed signal axis groups
Add a transactional axis organizer for shared and separate signal plots. Persist group assignments by object UUID and restore them across selections. Synchronize compatible X axes and share layouts with detached plot views. Preserve unique object UUIDs across creation, duplication, and HDF5 reloads. Add French translations, release notes, and unit, Python, and E2E coverage. Closes #16
1 parent 713769c commit 73bdb61

20 files changed

Lines changed: 1524 additions & 59 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66

77
## [Unreleased]
88

9+
### Added in Unreleased
10+
11+
- **Mixed signal axis groups**: selected signals may now be organized so related curves share one axis while other curves remain on separate vertical or horizontal axes. Compatible X axes stay synchronized, and each selection's organization is restored locally when the same workspace objects are selected again.
12+
913
## [0.7.0] - 2026-08-03
1014

1115
### Added in 0.7.0

‎src/App.tsx‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { MultiImagePlot, MULTI_IMAGE_LIMIT } from "./components/MultiImagePlot";
6868
import {
6969
bundleSignalResults,
7070
normalizeSignalLayoutMode,
71+
type SignalAxisGroup,
7172
type SignalLayoutMode,
7273
type SignalResultBundle,
7374
} from "./components/signalPlotLayout";
@@ -79,9 +80,11 @@ import {
7980
LazyProfileDefinitionDialog as ProfileDefinitionDialog,
8081
LazyRoiGridDialog as RoiGridDialog,
8182
LazySeparateViewDialog as SeparateViewDialog,
83+
LazySignalAxisGroupsDialog as SignalAxisGroupsDialog,
8284
LazySignalPlot as SignalPlot,
8385
} from "./components/lazyPlotComponents";
8486
import { useSelectionView } from "./hooks/useSelectionView";
87+
import { useSignalAxisGroups } from "./hooks/useSignalAxisGroups";
8588
import { DataSetDialog } from "./components/DataSetDialog";
8689
import type { ProfileFeatureId } from "./components/ProfileDefinitionDialog";
8790
import { OperandPicker } from "./components/OperandPicker";
@@ -677,6 +680,33 @@ export default function App() {
677680
/** Other signals selected alongside ``currentId`` and rendered according
678681
* to the persisted multi-signal layout preference. */
679682
const [extraSignals, setExtraSignals] = useState<SignalData[]>([]);
683+
const selectedSignalData = useMemo(
684+
() => (data ? [data, ...extraSignals] : []),
685+
[data, extraSignals],
686+
);
687+
const { groups: signalAxisGroups, applyGroups: applySignalAxisGroups } =
688+
useSignalAxisGroups(selectedSignalData);
689+
const [signalAxisGroupsDialogOpen, setSignalAxisGroupsDialogOpen] =
690+
useState(false);
691+
const openSignalAxisGroupsDialog = useCallback(
692+
() => setSignalAxisGroupsDialogOpen(true),
693+
[],
694+
);
695+
const closeSignalAxisGroupsDialog = useCallback(
696+
() => setSignalAxisGroupsDialogOpen(false),
697+
[],
698+
);
699+
const handleApplySignalAxisGroups = useCallback(
700+
(
701+
groups: SignalAxisGroup[],
702+
layoutMode: Exclude<SignalLayoutMode, "overlay">,
703+
) => {
704+
applySignalAxisGroups(groups);
705+
setSignalLayoutMode(layoutMode);
706+
setSignalAxisGroupsDialogOpen(false);
707+
},
708+
[applySignalAxisGroups, setSignalLayoutMode],
709+
);
680710
const [imageData, setImageData] = useState<ImageData | null>(null);
681711
const [extraImages, setExtraImages] = useState<ImageData[]>([]);
682712
/** Multi-image view mode: ``false`` = thumbnail grid (default),
@@ -3985,6 +4015,9 @@ export default function App() {
39854015
signalLayoutMode,
39864016
signalLayoutAvailable: treeKind === "signal",
39874017
onSetSignalLayoutMode: setSignalLayoutMode,
4018+
signalAxisGroupsAvailable:
4019+
treeKind === "signal" && selectedSignalData.length > 1,
4020+
onOrganizeSignalAxes: openSignalAxisGroupsDialog,
39884021
onOpenSeparateView: openSeparateView,
39894022
hasSelection: selectedIds.length > 0 || currentId !== null,
39904023
notebookFloating,
@@ -4120,6 +4153,8 @@ export default function App() {
41204153
toggleGraphicalTitles,
41214154
signalLayoutMode,
41224155
setSignalLayoutMode,
4156+
selectedSignalData.length,
4157+
openSignalAxisGroupsDialog,
41234158
openSeparateView,
41244159
aiPanelVisible,
41254160
toggleAIPanel,
@@ -4496,6 +4531,8 @@ export default function App() {
44964531
extraResults={extraResults}
44974532
layoutMode={signalLayoutMode}
44984533
onLayoutModeChange={setSignalLayoutMode}
4534+
axisGroups={signalAxisGroups}
4535+
onEditAxisGroups={openSignalAxisGroupsDialog}
44994536
/>
45004537
</Suspense>
45014538
)}
@@ -4999,11 +5036,28 @@ export default function App() {
49995036
showGraphicalTitles={showGraphicalTitles}
50005037
signalLayoutMode={signalLayoutMode}
50015038
onSignalLayoutModeChange={setSignalLayoutMode}
5039+
signalAxisGroups={signalAxisGroups}
5040+
onEditSignalAxisGroups={openSignalAxisGroupsDialog}
50025041
onClose={closeSeparateView}
50035042
/>
50045043
</Suspense>
50055044
);
50065045
})()}
5046+
{signalAxisGroupsDialogOpen && selectedSignalData.length > 1 && (
5047+
<Suspense fallback={null}>
5048+
<SignalAxisGroupsDialog
5049+
key={selectedSignalData
5050+
.map((signal) => `${signal.uuid ?? ""}:${signal.id}`)
5051+
.sort()
5052+
.join("|")}
5053+
signals={selectedSignalData}
5054+
groups={signalAxisGroups}
5055+
layoutMode={signalLayoutMode}
5056+
onApply={handleApplySignalAxisGroups}
5057+
onCancel={closeSignalAxisGroupsDialog}
5058+
/>
5059+
</Suspense>
5060+
)}
50075061
<DialogBridge />
50085062
{contextMenu && (
50095063
<ContextMenu

‎src/actions/registry.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ export interface ViewActionCallbacks {
458458
signalLayoutAvailable: boolean;
459459
/** Select and persist the multi-signal plot arrangement. */
460460
onSetSignalLayoutMode: (mode: SignalLayoutMode) => void;
461+
/** Whether more than one signal is available for axis grouping. */
462+
signalAxisGroupsAvailable: boolean;
463+
/** Open the selected-signal axis grouping editor. */
464+
onOrganizeSignalAxes: () => void;
461465
/** Open the current selection in a full-screen popout dialog. */
462466
onOpenSeparateView: () => void;
463467
/** True when the active panel has at least one object selected
@@ -548,6 +552,15 @@ export function buildViewActions(cb: ViewActionCallbacks): ActionDescriptor[] {
548552
if (cb.signalLayoutMode !== mode) cb.onSetSignalLayoutMode(mode);
549553
},
550554
})),
555+
{
556+
id: "view.signal_axes.organize",
557+
label: t("Organize axes…"),
558+
menuPath: "View/Signal plot layout/Organize axes…",
559+
beginGroup: true,
560+
enabled: (state) =>
561+
state.status === "ready" && !state.busy && cb.signalAxisGroupsAvailable,
562+
run: cb.onOrganizeSignalAxes,
563+
},
551564
{
552565
id: "view.notebook_floating",
553566
label: `${notebookPrefix}${t("Detach Notebooks panel")}`,

‎src/components/SeparateViewDialog.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import { useEffect } from "react";
1717
import { ImagePlot } from "./ImagePlot";
1818
import { SignalPlot } from "./SignalPlot";
19-
import type { SignalLayoutMode, SignalResultBundle } from "./signalPlotLayout";
19+
import type {
20+
SignalAxisGroup,
21+
SignalLayoutMode,
22+
SignalResultBundle,
23+
} from "./signalPlotLayout";
2024
import type {
2125
ImageData,
2226
PlotlyAnnotations,
@@ -53,6 +57,8 @@ interface Props {
5357
showGraphicalTitles: boolean;
5458
signalLayoutMode: SignalLayoutMode;
5559
onSignalLayoutModeChange: (mode: SignalLayoutMode) => void;
60+
signalAxisGroups: readonly SignalAxisGroup[];
61+
onEditSignalAxisGroups: () => void;
5662
onClose: () => void;
5763
}
5864

@@ -62,6 +68,8 @@ export function SeparateViewDialog({
6268
showGraphicalTitles,
6369
signalLayoutMode,
6470
onSignalLayoutModeChange,
71+
signalAxisGroups,
72+
onEditSignalAxisGroups,
6573
onClose,
6674
}: Props) {
6775
// Close on Escape — same convention as the existing ConfirmDialog.
@@ -117,6 +125,8 @@ export function SeparateViewDialog({
117125
extraResults={content.extraResults}
118126
layoutMode={signalLayoutMode}
119127
onLayoutModeChange={onSignalLayoutModeChange}
128+
axisGroups={signalAxisGroups}
129+
onEditAxisGroups={onEditSignalAxisGroups}
120130
/>
121131
) : (
122132
<ImagePlot

0 commit comments

Comments
 (0)