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 9ff4b8eb..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 || '', @@ -53,40 +55,49 @@ 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) { + 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, + 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 +109,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 +152,7 @@ export const GridLayoutPlot = ({ }); return { - ...data, + ...item, coordinates: updatedCoordinatesValue, plot: updatedPlot, xAxisData: updatedXAxisData, @@ -216,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]); @@ -250,6 +253,27 @@ 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.list) { + const dataPlotToUpdate = newDataPlot.find( + (dp) => synchronizedId === dp.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..1d48b54a 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,22 @@ export const Heatmap2D = ({ /> ), )} + {itemDataGrid.synchronizedGrids.list.length && ( +
2 + ? { right: -20 } + : { left: 0 }), + }} + > + +
+ )} diff --git a/frontend/src/renderer/components/plot/SimplePlotly.tsx b/frontend/src/renderer/components/plot/SimplePlotly.tsx index de72da27..f08fd6d1 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,20 @@ export const SimplePlotly = ({ /> ), )} + {itemDataGrid.synchronizedGrids?.list.length && ( +
+ +
+ )} )} 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..b530030a 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -18,11 +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; @@ -131,8 +134,23 @@ const Customization = ({ }, { value: 'Dataplots synchronization', - component: <>, - disabled: true, + component: ( + + ), + icon: + customizedDataGrid.synchronizedGrids.color !== '' ? ( + + ) : ( + + ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't be synchronized", }, ]; @@ -315,6 +333,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, @@ -322,6 +343,90 @@ export const DataplotCustomization = () => { customizedDataGrid, ]; + // Update synchronized grids dependencies + if ( + JSON.parse(JSON.stringify(oldDataGrid.synchronizedGrids.list)) + .sort() + .toString() !== + JSON.parse(JSON.stringify(customizedDataGrid.synchronizedGrids.list)) + .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.list.includes( + dataPlotDependency.i, + ) && + customizedDataGrid.synchronizedGrids.list.includes( + 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.list) { + const indexDPProbablyDesync = updatedActive.dataPlot.findIndex( + (dp) => oldSyncIdFromNewDep === dp.i, + ); + if ( + !newSynchronizedList.list.includes( + updatedActive.dataPlot[indexDPProbablyDesync].i, + ) + ) { + updatedActive.dataPlot[ + indexDPProbablyDesync + ].synchronizedGrids = { color: '', list: [] }; + } + } + + // Add in other grid the synchronized list and include the customized grid + updatedActive.dataPlot[index].synchronizedGrids = + newSynchronizedList; + } else if ( + oldDataGrid.synchronizedGrids.list.includes(dataPlotDependency.i) && + !customizedDataGrid.synchronizedGrids.list.includes( + dataPlotDependency.i, + ) + ) { + // Remove synchronization for deleted dependencies + updatedActive.dataPlot[index].synchronizedGrids = { + color: '', + list: [], + }; + } else if ( + customizedDataGrid.synchronizedGrids.list.includes( + 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, + ), + ], + }; + } + } + } + } + updatedConfiguration({ ...updatedActive, dataPlot: updatedDataPlot }); }, [active, customizedDataGrid]); 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 new file mode 100644 index 00000000..7839a729 --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeSynchronization.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import { DataGridPlot, synchronizedList } from '../../../types'; +import { MultiSelect, Stack } from '@mantine/core'; +import { useIbexStore } from '../../../stores'; +import { getColorRandom } from '../../../utils'; + +interface CustomizeSynchronizationProps { + customizedDataGrid: DataGridPlot; + setCustomizedDataGrid: React.Dispatch>; +} +export const CustomizeSynchronization = ({ + customizedDataGrid, + setCustomizedDataGrid, +}: CustomizeSynchronizationProps) => { + const { active } = useIbexStore(); + 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 && dataGrid.coordinates.length > 1, + ) + .map((dataGrid) => ({ value: dataGrid.i, label: dataGrid.title })); + + useEffect(() => { + setCustomizedDataGrid({ + ...customizedDataGrid, + synchronizedGrids: synchronizedList, + }); + }, [synchronizedList]); + + const handleSynchronizedListUpdate = (newSynchronizedList: string[]) => { + const newDepencyAdded = active.dataPlot.find( + (dp) => dp.i === newSynchronizedList[newSynchronizedList.length - 1], + ); + + if (newSynchronizedList.length < synchronizedList.list.length) { + // Remove an element + setSynchronizedList({ + color: !newSynchronizedList.length ? '' : synchronizedList.color, + list: newSynchronizedList, + }); + } else { + // Add an element + if (newDepencyAdded?.synchronizedGrids.list.length) { + // Add also these dependencies + setSynchronizedList({ + color: newDepencyAdded.synchronizedGrids.color, + list: Array.from( + new Set([ + ...newSynchronizedList, + ...newDepencyAdded.synchronizedGrids.list.filter( + (dataGridId) => dataGridId !== customizedDataGrid.i, + ), + ]), + ), + }); + } else { + setSynchronizedList({ + color: !synchronizedList.list.length + ? getColorRandom() + : synchronizedList.color, + list: newSynchronizedList, + }); + } + } + }; + + return ( + + + + ); +}; diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index 8b52ebef..74a7b69d 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: synchronizedList; downsampled_method?: string; downsampled_size?: number; xAxisData?: Axis; @@ -106,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 5655746b..16c8a13e 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: { color: '', list: [] }, i: generateUuid(), isEditing: true, static: true, diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 09f9ecb1..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; }; @@ -756,6 +764,9 @@ export function formatConfigBeforeLoadingURIs( unit: '', } as DataPlotly; }), + synchronizedGrids: data?.synchronizedGrids + ? data.synchronizedGrids + : { color: '', list: [] }, }), ); @@ -953,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,