From 153f763c60021f902ed7964f8cf2bfe72cee9615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Thu, 5 Feb 2026 13:33:46 +0100 Subject: [PATCH 1/9] in-progress: add component in customization to select graphs to synchronize --- frontend/src/renderer/layout/MainLayout.tsx | 1 + .../visualization/DataplotCustomization.tsx | 9 ++- .../CustomizeSynchronization.tsx | 81 +++++++++++++++++++ frontend/src/renderer/types/plot.ts | 1 + frontend/src/renderer/utils/grid.ts | 1 + 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx diff --git a/frontend/src/renderer/layout/MainLayout.tsx b/frontend/src/renderer/layout/MainLayout.tsx index 11642138..cf697194 100644 --- a/frontend/src/renderer/layout/MainLayout.tsx +++ b/frontend/src/renderer/layout/MainLayout.tsx @@ -102,6 +102,7 @@ export function MainLayout() { isTitleOverwritten: dataGrid.isTitleOverwritten, displayErrorBand: dataGrid.displayErrorBand, displayGrid: dataGrid.displayGrid, + synchronizedGrids: dataGrid.synchronizedGrids, downsampled_method: dataGrid?.downsampled_method, downsampled_size: dataGrid?.downsampled_size, xAxisData: dataGrid.xAxisData, diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index b35d70ee..99a91c04 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -23,6 +23,7 @@ import { CustomizeDownsampling, CustomizeGlobal } from './customizableElements'; import { CustomizeHeatmap } from './customizableElements/CustomizeHeatmap'; import { Customize1DPlot } from './customizableElements/Customize1DPlot'; import { CustomizeDataRange } from './customizableElements/CustomizeDataRange'; +import { CustomizeSynchronization } from './customizableElements/CustomizeSynchronization'; interface CustomizationProps { customizedDataGrid: DataGridPlot; selectedAccordion: string | null; @@ -131,8 +132,12 @@ const Customization = ({ }, { value: 'Dataplots synchronization', - component: <>, - disabled: true, + component: ( + + ), }, ]; diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx new file mode 100644 index 00000000..8c30f369 --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +import { DataGridPlot } from '../../../types'; +import { + fetchDataPlot, + fetchDownsamplingMethods, + fetchErrorBands, + getArrayValueFromDependance, + getFirstArrayValueFromShape, + getVectorData, + normalizeIndices, +} from '../../../utils'; +import { MultiSelect, Stack } from '@mantine/core'; +import { useIbexStore } from '../../../stores'; + +interface CustomizeSynchronizationProps { + customizedDataGrid: DataGridPlot; + setCustomizedDataGrid: React.Dispatch>; +} +export const CustomizeSynchronization = ({ + customizedDataGrid, + setCustomizedDataGrid, +}: CustomizeSynchronizationProps) => { + const { active, updatedConfiguration } = useIbexStore(); + const [synchronizedList, setSynchronizedList] = useState([]); + const fullDataGridList: { value: string; label: string }[] = active.dataPlot + .filter((dataGrid) => dataGrid.i !== customizedDataGrid.i) + .map((dataGrid) => ({ value: dataGrid.i, label: dataGrid.title })); + + /* + * Get downsampling methods to show in select + */ + useEffect(() => { + console.log('active : ', active); + console.log('customizedDataGrid : ', customizedDataGrid); + console.log('fullDataGridList : ', fullDataGridList); + }, []); + + useEffect(() => { + console.log('synchronizedList : ', synchronizedList); + // TODO : MAJ customizedDataGrid.synchronizedGrids here + // setCustomizedDataGrid({ + // ...customizedDataGrid, + // synchronizedGrids: synchronizedList + // }); + + console.log('active : ', active); + + const updatedActive = JSON.parse(JSON.stringify(active)); + for (const [index, dataPlot] of updatedActive.dataPlot.entries()) { + if (dataPlot.i === customizedDataGrid.i) { + // Add in customized grid the synchronized list + updatedActive.dataPlot[index].synchronizedGrids = synchronizedList; + } else if (synchronizedList.includes(dataPlot.i)) { + // Add in other grid the synchronized list and include the customized grid + updatedActive.dataPlot[index].synchronizedGrids = [ + customizedDataGrid.i, + ...synchronizedList.filter((i) => i !== dataPlot.i), + ]; + } + } + updatedConfiguration(updatedActive); + console.log('updatedActive (SYNC) : ', updatedActive); + }, [synchronizedList]); + + return ( + + + + ); +}; diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index 8b52ebef..6604c39c 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -84,6 +84,7 @@ export interface BaseDataGridPlot { isTitleOverwritten: boolean; displayErrorBand: boolean; displayGrid: boolean; + synchronizedGrids: string[]; downsampled_method?: string; downsampled_size?: number; xAxisData?: Axis; diff --git a/frontend/src/renderer/utils/grid.ts b/frontend/src/renderer/utils/grid.ts index 5655746b..261f8f87 100644 --- a/frontend/src/renderer/utils/grid.ts +++ b/frontend/src/renderer/utils/grid.ts @@ -35,6 +35,7 @@ export const generateNewGrid = ( isTitleOverwritten: false, displayErrorBand: true, displayGrid: true, + synchronizedGrids: [], i: generateUuid(), isEditing: true, static: true, From 82934de50c128b5d1dbd2d149fcc673d1d336b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Fri, 13 Feb 2026 15:39:26 +0100 Subject: [PATCH 2/9] finalize synchronization/desynchronizaztion between dataGrids --- .../components/grid/GridLayoutPlot.tsx | 13 ++++ .../visualization/DataplotCustomization.tsx | 69 +++++++++++++++++ .../CustomizeSynchronization.tsx | 74 ++++++++----------- 3 files changed, 114 insertions(+), 42 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 9ff4b8eb..18e32c8a 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -250,6 +250,19 @@ export const GridLayoutPlot = ({ dataPlot: newDataPlot, checkedNodeURI: checkedNodeURI, }; + + // Remove from synchronized relations deleted dataGrid + const oldDataPlot = active.dataPlot.find( + (item: DataGridPlot) => item.i === id, + ); + for (const synchronizedId of oldDataPlot.synchronizedGrids) { + const dataPlotToUpdate = newDataPlot.find( + (dp) => synchronizedId === dp.i, + ); + dataPlotToUpdate.synchronizedGrids = + dataPlotToUpdate.synchronizedGrids.filter((id) => id !== oldDataPlot.i); + } + updatedConfiguration(newActive); }, []); diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index 99a91c04..95a89b45 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -320,6 +320,9 @@ export const DataplotCustomization = () => { customizedGridLayout: null, saved: false, }; + const oldDataGrid = updatedActive.dataPlot.find( + (dp) => dp.i === active.customizedGridLayout, + ); const updatedDataPlot: DataGridPlot[] = [ ...updatedActive.dataPlot.filter( (dp) => dp.i !== active.customizedGridLayout, @@ -327,6 +330,72 @@ export const DataplotCustomization = () => { customizedDataGrid, ]; + // Update synchronized grids dependencies + if ( + JSON.parse(JSON.stringify(oldDataGrid.synchronizedGrids)) + .sort() + .toString() !== + JSON.parse(JSON.stringify(customizedDataGrid.synchronizedGrids)) + .sort() + .toString() + ) { + for (const [ + index, + dataPlotDependency, + ] of updatedActive.dataPlot.entries()) { + if (dataPlotDependency.i !== customizedDataGrid.i) { + // Add & remove automatically dataPlots excepted the updated one + if ( + !oldDataGrid.synchronizedGrids.includes(dataPlotDependency.i) && + customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + ) { + const newSynchronizedList = [ + customizedDataGrid.i, + ...customizedDataGrid.synchronizedGrids.filter( + (i) => i !== dataPlotDependency.i, + ), + ]; + + // Reset synchronized list for deleted dependencies + for (const oldSyncIdFromNewDep of dataPlotDependency.synchronizedGrids) { + const indexDPProbablyDesync = updatedActive.dataPlot.findIndex( + (dp) => oldSyncIdFromNewDep === dp.i, + ); + if ( + !newSynchronizedList.includes( + updatedActive.dataPlot[indexDPProbablyDesync].i, + ) + ) { + updatedActive.dataPlot[ + indexDPProbablyDesync + ].synchronizedGrids = []; + } + } + + // Add in other grid the synchronized list and include the customized grid + updatedActive.dataPlot[index].synchronizedGrids = + newSynchronizedList; + } else if ( + oldDataGrid.synchronizedGrids.includes(dataPlotDependency.i) && + !customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + ) { + // Remove synchronization for deleted dependencies + updatedActive.dataPlot[index].synchronizedGrids = []; + } else if ( + customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + ) { + // Update relations of unchanged dataGrids + updatedActive.dataPlot[index].synchronizedGrids = [ + customizedDataGrid.i, + ...customizedDataGrid.synchronizedGrids.filter( + (i) => i !== dataPlotDependency.i, + ), + ]; + } + } + } + } + updatedConfiguration({ ...updatedActive, dataPlot: updatedDataPlot }); }, [active, customizedDataGrid]); diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx index 8c30f369..796c3811 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx @@ -1,14 +1,5 @@ import { useEffect, useState } from 'react'; import { DataGridPlot } from '../../../types'; -import { - fetchDataPlot, - fetchDownsamplingMethods, - fetchErrorBands, - getArrayValueFromDependance, - getFirstArrayValueFromShape, - getVectorData, - normalizeIndices, -} from '../../../utils'; import { MultiSelect, Stack } from '@mantine/core'; import { useIbexStore } from '../../../stores'; @@ -20,47 +11,46 @@ export const CustomizeSynchronization = ({ customizedDataGrid, setCustomizedDataGrid, }: CustomizeSynchronizationProps) => { - const { active, updatedConfiguration } = useIbexStore(); - const [synchronizedList, setSynchronizedList] = useState([]); + const { active } = useIbexStore(); + const [synchronizedList, setSynchronizedList] = useState( + customizedDataGrid?.synchronizedGrids, + ); const fullDataGridList: { value: string; label: string }[] = active.dataPlot .filter((dataGrid) => dataGrid.i !== customizedDataGrid.i) .map((dataGrid) => ({ value: dataGrid.i, label: dataGrid.title })); - /* - * Get downsampling methods to show in select - */ - useEffect(() => { - console.log('active : ', active); - console.log('customizedDataGrid : ', customizedDataGrid); - console.log('fullDataGridList : ', fullDataGridList); - }, []); - useEffect(() => { - console.log('synchronizedList : ', synchronizedList); - // TODO : MAJ customizedDataGrid.synchronizedGrids here - // setCustomizedDataGrid({ - // ...customizedDataGrid, - // synchronizedGrids: synchronizedList - // }); + setCustomizedDataGrid({ + ...customizedDataGrid, + synchronizedGrids: synchronizedList, + }); + }, [synchronizedList]); - console.log('active : ', active); + const handleSynchronizedListUpdate = (newSynchronizedList: string[]) => { + const newDepencyAdded = active.dataPlot.find( + (dp) => dp.i === newSynchronizedList[newSynchronizedList.length - 1], + ); - const updatedActive = JSON.parse(JSON.stringify(active)); - for (const [index, dataPlot] of updatedActive.dataPlot.entries()) { - if (dataPlot.i === customizedDataGrid.i) { - // Add in customized grid the synchronized list - updatedActive.dataPlot[index].synchronizedGrids = synchronizedList; - } else if (synchronizedList.includes(dataPlot.i)) { - // Add in other grid the synchronized list and include the customized grid - updatedActive.dataPlot[index].synchronizedGrids = [ - customizedDataGrid.i, - ...synchronizedList.filter((i) => i !== dataPlot.i), - ]; + if (newSynchronizedList.length < synchronizedList.length) { + // Remove an element + setSynchronizedList(newSynchronizedList); + } else { + // Add an element + if (newDepencyAdded?.synchronizedGrids.length) { + // Add also these dependencies + setSynchronizedList( + Array.from( + new Set([ + ...newSynchronizedList, + ...newDepencyAdded.synchronizedGrids, + ]), + ), + ); + } else { + setSynchronizedList(newSynchronizedList); } } - updatedConfiguration(updatedActive); - console.log('updatedActive (SYNC) : ', updatedActive); - }, [synchronizedList]); + }; return ( @@ -72,7 +62,7 @@ export const CustomizeSynchronization = ({ data={fullDataGridList} // defaultValue={['React']} value={synchronizedList} - onChange={setSynchronizedList} + onChange={handleSynchronizedListUpdate} searchable nothingFoundMessage="Nothing found..." /> From 81713dd0d0ee9a31d29650471b5c7fe1383fb334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Mon, 16 Feb 2026 13:38:02 +0100 Subject: [PATCH 3/9] improve ux by discerning synchronized grids --- .../components/grid/GridLayoutPlot.tsx | 14 +++- .../renderer/components/plot/Heatmap2D.tsx | 10 +++ .../renderer/components/plot/SimplePlotly.tsx | 10 +++ .../visualization/DataplotCustomization.tsx | 79 +++++++++++++------ .../visualization/VisualizationURIModal.tsx | 5 +- .../CustomizeSynchronization.tsx | 35 +++++--- frontend/src/renderer/types/plot.ts | 7 +- frontend/src/renderer/utils/functions.ts | 6 ++ frontend/src/renderer/utils/grid.ts | 2 +- 9 files changed, 122 insertions(+), 46 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 18e32c8a..3bed73e6 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -255,12 +255,20 @@ export const GridLayoutPlot = ({ const oldDataPlot = active.dataPlot.find( (item: DataGridPlot) => item.i === id, ); - for (const synchronizedId of oldDataPlot.synchronizedGrids) { + for (const synchronizedId of oldDataPlot.synchronizedGrids.list) { const dataPlotToUpdate = newDataPlot.find( (dp) => synchronizedId === dp.i, ); - dataPlotToUpdate.synchronizedGrids = - dataPlotToUpdate.synchronizedGrids.filter((id) => id !== oldDataPlot.i); + const updatedList = dataPlotToUpdate.synchronizedGrids.list.filter( + (id) => id !== oldDataPlot.i, + ); + dataPlotToUpdate.synchronizedGrids = { + color: + updatedList.length > 0 + ? dataPlotToUpdate.synchronizedGrids.color + : '', + list: updatedList, + }; } updatedConfiguration(newActive); diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 0ed708e6..4422a2ca 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -24,6 +24,7 @@ import classes from './Heatmap2D.module.css'; import { useIbexStore } from '../../stores'; import { NoDataForURI } from '.'; import { usePlotLayout } from './hooks/usePlotLayout'; +import { IconLink } from '@tabler/icons-react'; interface Heatmap2DProps { itemDataGrid: DataGridPlot; @@ -344,6 +345,7 @@ export const Heatmap2D = ({ w={`${width * 0.2}px`} miw={`${(itemDataGrid.coordinates.length - coordsUsedInAxes) * 50}px`} align="flex-end" + pos="relative" > {JSON.parse(JSON.stringify(itemDataGrid.coordinates)) .sort(compareByAxeIndex) @@ -375,6 +377,14 @@ export const Heatmap2D = ({ /> ), )} + {itemDataGrid.synchronizedGrids.list.length && ( +
+ +
+ )} diff --git a/frontend/src/renderer/components/plot/SimplePlotly.tsx b/frontend/src/renderer/components/plot/SimplePlotly.tsx index de72da27..0b3ab6cc 100644 --- a/frontend/src/renderer/components/plot/SimplePlotly.tsx +++ b/frontend/src/renderer/components/plot/SimplePlotly.tsx @@ -15,6 +15,7 @@ import { import classes from './SimplePlotly.module.css'; import { NoDataForURI } from '../plot'; import { usePlotLayout } from './hooks/usePlotLayout'; +import { IconLink } from '@tabler/icons-react'; interface SimplePlotlyProps { itemDataGrid: DataGridPlot; width: number; @@ -361,6 +362,7 @@ export const SimplePlotly = ({ w={`${width * 0.2}px`} miw={`${(itemDataGrid.coordinates.length - coordsUsedInAxes) * 50}px`} align="flex-end" + pos="relative" > {JSON.parse(JSON.stringify(itemDataGrid.coordinates)) .sort(compareByAxeIndex) @@ -395,6 +397,14 @@ export const SimplePlotly = ({ /> ), )} + {itemDataGrid.synchronizedGrids?.list.length && ( +
+ +
+ )} )} diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index 95a89b45..42810a0d 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -18,12 +18,14 @@ import { DataGridPlot, DataPlotly, PlotLine, -} from 'src/renderer/types'; + synchronizedList, +} from '../../types'; import { CustomizeDownsampling, CustomizeGlobal } from './customizableElements'; import { CustomizeHeatmap } from './customizableElements/CustomizeHeatmap'; import { Customize1DPlot } from './customizableElements/Customize1DPlot'; import { CustomizeDataRange } from './customizableElements/CustomizeDataRange'; import { CustomizeSynchronization } from './customizableElements/CustomizeSynchronization'; +import { IconLink } from '@tabler/icons-react'; interface CustomizationProps { customizedDataGrid: DataGridPlot; selectedAccordion: string | null; @@ -138,6 +140,15 @@ const Customization = ({ setCustomizedDataGrid={setCustomizedDataGrid} /> ), + icon: + customizedDataGrid.synchronizedGrids.color !== '' ? ( + + ) : ( + + ), }, ]; @@ -332,10 +343,10 @@ export const DataplotCustomization = () => { // Update synchronized grids dependencies if ( - JSON.parse(JSON.stringify(oldDataGrid.synchronizedGrids)) + JSON.parse(JSON.stringify(oldDataGrid.synchronizedGrids.list)) .sort() .toString() !== - JSON.parse(JSON.stringify(customizedDataGrid.synchronizedGrids)) + JSON.parse(JSON.stringify(customizedDataGrid.synchronizedGrids.list)) .sort() .toString() ) { @@ -346,29 +357,37 @@ export const DataplotCustomization = () => { if (dataPlotDependency.i !== customizedDataGrid.i) { // Add & remove automatically dataPlots excepted the updated one if ( - !oldDataGrid.synchronizedGrids.includes(dataPlotDependency.i) && - customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + !oldDataGrid.synchronizedGrids.list.includes( + dataPlotDependency.i, + ) && + customizedDataGrid.synchronizedGrids.list.includes( + dataPlotDependency.i, + ) ) { - const newSynchronizedList = [ - customizedDataGrid.i, - ...customizedDataGrid.synchronizedGrids.filter( - (i) => i !== dataPlotDependency.i, - ), - ]; + const newSynchronizedList = { + color: customizedDataGrid.synchronizedGrids.color, + list: [ + customizedDataGrid.i, + ...customizedDataGrid.synchronizedGrids.list.filter( + (i) => i !== dataPlotDependency.i, + ), + ], + } as synchronizedList; // Reset synchronized list for deleted dependencies - for (const oldSyncIdFromNewDep of dataPlotDependency.synchronizedGrids) { + for (const oldSyncIdFromNewDep of dataPlotDependency + .synchronizedGrids.list) { const indexDPProbablyDesync = updatedActive.dataPlot.findIndex( (dp) => oldSyncIdFromNewDep === dp.i, ); if ( - !newSynchronizedList.includes( + !newSynchronizedList.list.includes( updatedActive.dataPlot[indexDPProbablyDesync].i, ) ) { updatedActive.dataPlot[ indexDPProbablyDesync - ].synchronizedGrids = []; + ].synchronizedGrids = { color: '', list: [] }; } } @@ -376,21 +395,31 @@ export const DataplotCustomization = () => { updatedActive.dataPlot[index].synchronizedGrids = newSynchronizedList; } else if ( - oldDataGrid.synchronizedGrids.includes(dataPlotDependency.i) && - !customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + oldDataGrid.synchronizedGrids.list.includes(dataPlotDependency.i) && + !customizedDataGrid.synchronizedGrids.list.includes( + dataPlotDependency.i, + ) ) { // Remove synchronization for deleted dependencies - updatedActive.dataPlot[index].synchronizedGrids = []; + updatedActive.dataPlot[index].synchronizedGrids = { + color: '', + list: [], + }; } else if ( - customizedDataGrid.synchronizedGrids.includes(dataPlotDependency.i) + customizedDataGrid.synchronizedGrids.list.includes( + dataPlotDependency.i, + ) ) { - // Update relations of unchanged dataGrids - updatedActive.dataPlot[index].synchronizedGrids = [ - customizedDataGrid.i, - ...customizedDataGrid.synchronizedGrids.filter( - (i) => i !== dataPlotDependency.i, - ), - ]; + // Update relations of unchanged dataGrids + updatedActive.dataPlot[index].synchronizedGrids = { + color: customizedDataGrid.synchronizedGrids.color, + list: [ + customizedDataGrid.i, + ...customizedDataGrid.synchronizedGrids.list.filter( + (i) => i !== dataPlotDependency.i, + ), + ], + }; } } } diff --git a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx index 39777798..01f0f810 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx @@ -26,6 +26,7 @@ import { fetchURIExists, fetchURIFromPath, formatConfigBeforeLoadingURIs, + getColorRandom, plotNodeUriLoaded, updateCustomDataTree, } from '../../utils'; @@ -41,10 +42,6 @@ interface FormIDS { uri: string; } -function getColorRandom(): string { - return `#${Math.floor(Math.random() * 16777215).toString(16)}`; -} - export const VisualizationURIModal = ({ opened, close, diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx index 796c3811..05c9ca18 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; -import { DataGridPlot } from '../../../types'; +import { DataGridPlot, synchronizedList } from '../../../types'; import { MultiSelect, Stack } from '@mantine/core'; import { useIbexStore } from '../../../stores'; +import { getColorRandom } from '../../../utils'; interface CustomizeSynchronizationProps { customizedDataGrid: DataGridPlot; @@ -12,7 +13,7 @@ export const CustomizeSynchronization = ({ setCustomizedDataGrid, }: CustomizeSynchronizationProps) => { const { active } = useIbexStore(); - const [synchronizedList, setSynchronizedList] = useState( + const [synchronizedList, setSynchronizedList] = useState( customizedDataGrid?.synchronizedGrids, ); const fullDataGridList: { value: string; label: string }[] = active.dataPlot @@ -31,23 +32,34 @@ export const CustomizeSynchronization = ({ (dp) => dp.i === newSynchronizedList[newSynchronizedList.length - 1], ); - if (newSynchronizedList.length < synchronizedList.length) { + if (newSynchronizedList.length < synchronizedList.list.length) { // Remove an element - setSynchronizedList(newSynchronizedList); + setSynchronizedList({ + color: !newSynchronizedList.length ? '' : synchronizedList.color, + list: newSynchronizedList, + }); } else { // Add an element - if (newDepencyAdded?.synchronizedGrids.length) { + if (newDepencyAdded?.synchronizedGrids.list.length) { // Add also these dependencies - setSynchronizedList( - Array.from( + setSynchronizedList({ + color: newDepencyAdded.synchronizedGrids.color, + list: Array.from( new Set([ ...newSynchronizedList, - ...newDepencyAdded.synchronizedGrids, + ...newDepencyAdded.synchronizedGrids.list.filter( + (dataGridId) => dataGridId !== customizedDataGrid.i, + ), ]), ), - ); + }); } else { - setSynchronizedList(newSynchronizedList); + setSynchronizedList({ + color: !synchronizedList.list.length + ? getColorRandom() + : synchronizedList.color, + list: newSynchronizedList, + }); } } }; @@ -60,8 +72,7 @@ export const CustomizeSynchronization = ({ label="Graphs to synchronyze" placeholder="Pick a graph" data={fullDataGridList} - // defaultValue={['React']} - value={synchronizedList} + value={synchronizedList.list} onChange={handleSynchronizedListUpdate} searchable nothingFoundMessage="Nothing found..." diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index 6604c39c..74a7b69d 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -84,7 +84,7 @@ export interface BaseDataGridPlot { isTitleOverwritten: boolean; displayErrorBand: boolean; displayGrid: boolean; - synchronizedGrids: string[]; + synchronizedGrids: synchronizedList; downsampled_method?: string; downsampled_size?: number; xAxisData?: Axis; @@ -107,3 +107,8 @@ export interface DataGridPlotToSave extends BaseDataGridPlot { plot: BaseDataPlotly[]; coordinates: BaseCoordinates[]; } + +export type synchronizedList = { + color: string; + list: string[]; +}; diff --git a/frontend/src/renderer/utils/functions.ts b/frontend/src/renderer/utils/functions.ts index bcd084e2..859b9b76 100644 --- a/frontend/src/renderer/utils/functions.ts +++ b/frontend/src/renderer/utils/functions.ts @@ -1,5 +1,11 @@ import { Complex } from '../types'; +export function getColorRandom(): string { + return `#${Math.floor(Math.random() * 0xffffff) + .toString(16) + .padStart(6, '0')}`; +} + export function removeSuffix(str: string, suffix: string): string { return str?.endsWith(suffix) ? str.slice(0, -suffix.length) : str; } diff --git a/frontend/src/renderer/utils/grid.ts b/frontend/src/renderer/utils/grid.ts index 261f8f87..16c8a13e 100644 --- a/frontend/src/renderer/utils/grid.ts +++ b/frontend/src/renderer/utils/grid.ts @@ -35,7 +35,7 @@ export const generateNewGrid = ( isTitleOverwritten: false, displayErrorBand: true, displayGrid: true, - synchronizedGrids: [], + synchronizedGrids: { color: '', list: [] }, i: generateUuid(), isEditing: true, static: true, From 8da807ff15495c1dcba9587de0cd5eebac632f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 17 Feb 2026 11:29:44 +0100 Subject: [PATCH 4/9] Update each coordinates from synchronized sliders when updating sliders --- .../components/grid/GridLayoutPlot.tsx | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 3bed73e6..91aec91c 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -53,40 +53,39 @@ export const GridLayoutPlot = ({ if (!lastTargetLastName) return console.warn('No indexed field found in target'); - // Update coordinates targets & paths with new valueIndex - const updatedCoordinatesValue = data.coordinates.map((item) => { - const lastTargetLastName = getLastIndexedField(coordinate.target); - - const updatedPath = updateIndexFieldName( - item.path, - lastTargetLastName, - valueIndex, - ); - const updatedTarget = updateIndexFieldName( - item.target, - lastTargetLastName, - valueIndex, - ); - - return { - ...item, - path: updatedPath, - target: updatedTarget, - valueIndex: - item.name === coordinate.name ? valueIndex : item.valueIndex, - }; - }) as Coordinates[]; - - limitSlidersToMaxLength(updatedCoordinatesValue); - const updatedActive: Configuration = { ...active, dataPlot: active.dataPlot.map((item: DataGridPlot) => { - if (item.i === data.i) { + if (item.i === data.i || data.synchronizedGrids.list.includes(item.i)) { + // Update targets & paths from each coordinates of synchronized sliders with new valueIndex + const updatedCoordinatesValue = item.coordinates.map((coordItem) => { + const updatedPath = updateIndexFieldName( + coordItem.path, + lastTargetLastName, + valueIndex, + ); + const updatedTarget = updateIndexFieldName( + coordItem.target, + lastTargetLastName, + valueIndex, + ); + + return { + ...coordItem, + path: updatedPath, + target: updatedTarget, + valueIndex: + coordItem.name === coordinate.name + ? valueIndex + : coordItem.valueIndex, + }; + }) as Coordinates[]; + limitSlidersToMaxLength(updatedCoordinatesValue); + const updatedXAxisData: Axis = { - ...data.xAxisData, + ...item.xAxisData, path: updateIndexFieldName( - data.xAxisData?.path || '', + item.xAxisData?.path || '', lastTargetLastName, valueIndex, ), @@ -98,7 +97,7 @@ export const GridLayoutPlot = ({ 0, ); - const updatedPlot = data.plot.map((plotItem) => { + const updatedPlot = item.plot.map((plotItem) => { const updatedNodeUri = updateIndexFieldName( plotItem.nodeUri, lastTargetLastName, @@ -141,7 +140,7 @@ export const GridLayoutPlot = ({ }); return { - ...data, + ...item, coordinates: updatedCoordinatesValue, plot: updatedPlot, xAxisData: updatedXAxisData, From 0b57c1c143923f03b691734eaaf677cd5608dd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 17 Feb 2026 11:31:56 +0100 Subject: [PATCH 5/9] Format loaded configuration to have by default empty synchronized grids --- frontend/src/renderer/utils/plot.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 09f9ecb1..42f0f734 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -756,6 +756,9 @@ export function formatConfigBeforeLoadingURIs( unit: '', } as DataPlotly; }), + synchronizedGrids: data?.synchronizedGrids + ? data.synchronizedGrids + : { color: '', list: [] }, }), ); From b7e50afa154c01c357a77088115f8542212c29fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 17 Feb 2026 13:47:57 +0100 Subject: [PATCH 6/9] Updates only the synchronized sliders with the same coordinates --- .../renderer/components/grid/GridLayoutPlot.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 91aec91c..187e7b1c 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -56,8 +56,18 @@ export const GridLayoutPlot = ({ const updatedActive: Configuration = { ...active, dataPlot: active.dataPlot.map((item: DataGridPlot) => { - if (item.i === data.i || data.synchronizedGrids.list.includes(item.i)) { - // Update targets & paths from each coordinates of synchronized sliders with new valueIndex + const mainDataGrid = item.i === data.i; + const isSynchronized = data.synchronizedGrids.list.includes(item.i); + const coordWithSameName = item.coordinates.find( + (ic) => ic.name === coordinate.name, + ); + const sameCoordinate = + coordWithSameName && + JSON.stringify(coordinate.data) === + JSON.stringify(coordWithSameName.data); + + if (mainDataGrid || (isSynchronized && sameCoordinate)) { + // Update main slider with new valueIndex & update synchronized ones matching with the same coordinate const updatedCoordinatesValue = item.coordinates.map((coordItem) => { const updatedPath = updateIndexFieldName( coordItem.path, From 7dd61a3fd54d5263cfa2632da6ab8bace1b435ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 17 Feb 2026 14:57:05 +0100 Subject: [PATCH 7/9] synchronize only grids having at least 2 coordinates --- .../renderer/pages/visualization/DataplotCustomization.tsx | 2 ++ .../customizableElements/CustomizeSynchronization.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index 42810a0d..b530030a 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -149,6 +149,8 @@ const Customization = ({ ) : ( ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't be synchronized", }, ]; diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx index 05c9ca18..7839a729 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx @@ -16,8 +16,13 @@ export const CustomizeSynchronization = ({ const [synchronizedList, setSynchronizedList] = useState( customizedDataGrid?.synchronizedGrids, ); + + // Get a list of dataGrids having at least 2 coordinates const fullDataGridList: { value: string; label: string }[] = active.dataPlot - .filter((dataGrid) => dataGrid.i !== customizedDataGrid.i) + .filter( + (dataGrid) => + dataGrid.i !== customizedDataGrid.i && dataGrid.coordinates.length > 1, + ) .map((dataGrid) => ({ value: dataGrid.i, label: dataGrid.title })); useEffect(() => { From 1b72713542ba13d57b9be67ada69c2f3d48aabda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 17 Feb 2026 16:05:52 +0100 Subject: [PATCH 8/9] update the icon position when having no slider --- frontend/src/renderer/components/plot/Heatmap2D.tsx | 10 +++++++++- frontend/src/renderer/components/plot/SimplePlotly.tsx | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 4422a2ca..1d48b54a 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -378,7 +378,15 @@ export const Heatmap2D = ({ ), )} {itemDataGrid.synchronizedGrids.list.length && ( -
+
2 + ? { right: -20 } + : { left: 0 }), + }} + > +
Date: Thu, 26 Feb 2026 11:58:15 +0100 Subject: [PATCH 9/9] preserve selected plot mode --- frontend/src/main/backend-manager.ts | 2 +- .../components/grid/GridLayoutPlot.tsx | 22 +++++++------------ frontend/src/renderer/utils/plot.ts | 17 +++++++++++++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/frontend/src/main/backend-manager.ts b/frontend/src/main/backend-manager.ts index dd64cd19..5a160798 100644 --- a/frontend/src/main/backend-manager.ts +++ b/frontend/src/main/backend-manager.ts @@ -231,7 +231,7 @@ export class BackendManager { console.info('Remote backend configured, not stopping it.'); return; } - + if (this.backendProcess) { console.info('Stopping backend process...'); this.backendProcess.kill(); diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 187e7b1c..8ad45549 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Axis, Configuration, @@ -34,7 +34,9 @@ export const GridLayoutPlot = ({ data.h * rowHeight + (23 * (data.h * rowHeight)) / 100, ); const [widthGrid, setWidthGrid] = useState(Math.floor(data.w * colWidth)); - const [is3DView, setIs3DView] = useState(false); + const [is3DView, setIs3DView] = useState( + data?.selectedPlotMode === 'Heatmap' ? true : false, + ); const [active3DTab, setActive3DTab] = useState('0'); const [metadataTabsValue, setMetadataTabsValue] = useState( data.plot[0]?.path || '', @@ -225,20 +227,12 @@ export const GridLayoutPlot = ({ updatedConfiguration(updatedActive); }; - useLayoutEffect(() => { - if (data?.selectedPlotMode) { - setIs3DView(data.selectedPlotMode === 'Heatmap'); - } else { - setIs3DView( - data.coordinates.length >= 2 && - containsFloat( - data.coordinates.find((coord) => coord.axeIndex === 1)?.data, - ), - ); + useEffect(() => { + if ((data.selectedPlotMode === 'Heatmap') === is3DView) { + // Prevent from triggering updateSelectedPlotMode when initialize is3DView + return; } - }, []); - useEffect(() => { updateSelectedPlotMode(is3DView, active); }, [is3DView]); diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 42f0f734..17cd78b2 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -28,7 +28,7 @@ import { } from './matrix'; import * as tf from '@tensorflow/tfjs'; import { ErrorBar } from 'plotly.js'; -import { removeSuffix } from './functions'; +import { containsFloat, removeSuffix } from './functions'; /** * @description Generates a new DataGridPlot with the provided coordinates, xAxis, and yAxis. @@ -178,6 +178,14 @@ export const handleNewPlot = async ( response.data.description, ); updatedPlot.dataType = nodes[0].type; + + // Rule to define the default plot mode + updatedPlot.selectedPlotMode = + updatedPlot.coordinates.length >= 2 && + containsFloat(updatedPlot.coordinates[1]?.data) + ? 'Heatmap' + : '1D'; + updatedActive.dataPlot.push(updatedPlot); return updatedActive; }; @@ -956,6 +964,13 @@ export async function plotNodeUriLoaded( } } + // Rule to define the default plot mode + dataGrid.selectedPlotMode = + dataGrid.coordinates.length >= 2 && + containsFloat(dataGrid.coordinates[1]?.data) + ? 'Heatmap' + : '1D'; + const dataGridUpdated = { ...dataGrid, xAxisData: updatedXAxisData,