From 47a1a0e978f40c668c0a8389c63d1c9f6bf6339c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Fri, 22 May 2026 10:27:13 +0200 Subject: [PATCH 1/7] Distinction between visual changes and data manipulations --- .../components/grid/GridLayoutPlot.tsx | 5 +- .../renderer/components/grid/HoverButtons.tsx | 31 +- .../renderer/components/tree/TreeLibrary.tsx | 7 +- .../visualization/DataplotCustomization.tsx | 356 +++++++++--------- frontend/src/renderer/types/configuration.ts | 9 +- 5 files changed, 225 insertions(+), 183 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 7939f26e..5f997432 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -3,6 +3,7 @@ import { Axis, Configuration, Coordinates, + CustomizedGridType, DataGridPlot, DataPlotly, GridLayoutPlotProps, @@ -338,10 +339,10 @@ export const GridLayoutPlot = ({ * Customize plot */ const handleCustomization = useCallback( - (id: string) => { + (id: string, typeOfEdition: CustomizedGridType) => { const updatedActive: Configuration = { ...active, - customizedGridLayout: id, + customizedGridLayout: { id: id, type: typeOfEdition }, }; updatedConfiguration(updatedActive); }, diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 83cbe706..76f2ad8b 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -12,12 +12,13 @@ import { import { IconBrandDatabricks, IconCheck, + IconDatabaseEdit, IconEdit, - IconPalette, + IconEyeEdit, IconTrash, } from '@tabler/icons-react'; import { useHover } from '@mantine/hooks'; -import { Configuration, DataGridPlot } from '../../types'; +import { Configuration, CustomizedGridType, DataGridPlot } from '../../types'; import { applyRange, containsFloat, @@ -30,7 +31,7 @@ interface HoverButtonsProps { shouldDisplayMetadata: boolean; handleEditGrid: (id: string) => void; handleInspectMetadata: (id: string) => void; - handleCustomization: (id: string) => void; + handleCustomization: (id: string, typeOfEdition: CustomizedGridType) => void; handleDeleteGrid: (id: string) => void; is3DView: boolean; active3DTab: string; @@ -280,15 +281,33 @@ export const HoverButtons = React.memo( {data.coordinates.length && !shouldDisplayMetadata && ( // Show customization button only if plottable - + handleCustomization(data.i)} + onClick={() => handleCustomization(data.i, 'data')} className={classes.actionButton} > - + + + )} + + {data.coordinates.length && !shouldDisplayMetadata && ( + // Show customization button only if plottable + + handleCustomization(data.i, 'visual')} + className={classes.actionButton} + > + diff --git a/frontend/src/renderer/components/tree/TreeLibrary.tsx b/frontend/src/renderer/components/tree/TreeLibrary.tsx index 810ce080..809ab3f9 100644 --- a/frontend/src/renderer/components/tree/TreeLibrary.tsx +++ b/frontend/src/renderer/components/tree/TreeLibrary.tsx @@ -460,8 +460,11 @@ export const TreeLibrary = ({ }, [isEditingPlot]); useEffect(() => { - handleDisableTree(active?.metadataGridLayout, active?.customizedGridLayout); - }, [active?.metadataGridLayout, active?.customizedGridLayout]); + handleDisableTree( + active?.metadataGridLayout, + active?.customizedGridLayout?.id, + ); + }, [active?.metadataGridLayout, active?.customizedGridLayout?.id]); return ( diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index abfdc686..e4a23e8f 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -15,6 +15,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { SimplePlotly, Heatmap2D, TabsListCustom } from '../../components'; import { Configuration, + CustomizedGridType, DataGridPlot, DataPlotly, synchronizedList, @@ -26,175 +27,6 @@ import { CustomizeDataRange } from './customizableElements/CustomizeDataRange'; import { CustomizeSynchronization } from './customizableElements/CustomizeSynchronization'; import { IconLink } from '@tabler/icons-react'; import { initPlotColors } from '../../utils'; -interface CustomizationProps { - customizedDataGrid: DataGridPlot; - selectedAccordion: string | null; - selectedPlot: DataPlotly | null; - applyToAllHeatmap: boolean; - customContainerRef: React.MutableRefObject; - setCustomizedDataGrid: React.Dispatch>; - setSelectedAccordion: React.Dispatch>; - setApplyToAllHeatmap: React.Dispatch>; -} -const Customization = ({ - customizedDataGrid, - selectedAccordion, - selectedPlot, - applyToAllHeatmap, - customContainerRef, - setCustomizedDataGrid, - setSelectedAccordion, - setApplyToAllHeatmap, -}: CustomizationProps) => { - type accordionItemsType = { - value: string; - component: JSX.Element; - icon?: JSX.Element; - disabled?: boolean; - tooltip?: string; - }; - const accordionItems: accordionItemsType[] = [ - { - value: 'Global', - component: ( - - ), - }, - { - value: '1D plots', - component: ( - - ), - icon: ( - - 1D - - ), - }, - { - value: 'Heatmap', - component: ( - - ), - icon: ( - - - - - - - - - - - - - - - - ), - disabled: customizedDataGrid.coordinates.length < 2, - tooltip: "This grid can't display heatmap", - }, - { - value: 'Axis range', - component: ( - - ), - }, - { - value: 'Downsampling', - component: ( - - ), - }, - { - value: 'Dataplots synchronization', - component: ( - - ), - icon: - customizedDataGrid.synchronizedGrids.color !== '' ? ( - - ) : ( - - ), - disabled: customizedDataGrid.coordinates.length < 2, - tooltip: "This grid can't be synchronized", - }, - ]; - - const items = accordionItems.map((item) => ( - - - - {item.value} - - {item.component} - - - )); - - return ( - - - Customize plot parameters - - - - {items} - - - - ); -}; export const DataplotCustomization = () => { const customContainerRef = useRef(null); @@ -238,7 +70,7 @@ export const DataplotCustomization = () => { const data = JSON.parse( JSON.stringify( active.dataPlot.find( - (item: DataGridPlot) => item.i === active.customizedGridLayout, + (item: DataGridPlot) => item.i === active.customizedGridLayout.id, ), ), ); @@ -295,11 +127,11 @@ export const DataplotCustomization = () => { saved: false, }; const oldDataGrid = updatedActive.dataPlot.find( - (dp) => dp.i === active.customizedGridLayout, + (dp) => dp.i === active.customizedGridLayout.id, ); const updatedDataPlot: DataGridPlot[] = [ ...updatedActive.dataPlot.filter( - (dp) => dp.i !== active.customizedGridLayout, + (dp) => dp.i !== active.customizedGridLayout.id, ), customizedDataGrid, ]; @@ -460,6 +292,7 @@ export const DataplotCustomization = () => { { ); }; + +interface CustomizationProps { + customizedDataGrid: DataGridPlot; + customizedType: CustomizedGridType; + selectedAccordion: string | null; + selectedPlot: DataPlotly | null; + applyToAllHeatmap: boolean; + customContainerRef: React.MutableRefObject; + setCustomizedDataGrid: React.Dispatch>; + setSelectedAccordion: React.Dispatch>; + setApplyToAllHeatmap: React.Dispatch>; +} +const Customization = ({ + customizedDataGrid, + customizedType, + selectedAccordion, + selectedPlot, + applyToAllHeatmap, + customContainerRef, + setCustomizedDataGrid, + setSelectedAccordion, + setApplyToAllHeatmap, +}: CustomizationProps) => { + type accordionItemsType = { + value: string; + component: JSX.Element; + icon?: JSX.Element; + disabled?: boolean; + tooltip?: string; + }; + const visualAccordions: accordionItemsType[] = [ + { + value: 'Global', + component: ( + + ), + }, + { + value: '1D plots', + component: ( + + ), + icon: ( + + 1D + + ), + }, + { + value: 'Heatmap', + component: ( + + ), + icon: ( + + + + + + + + + + + + + + + + ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't display heatmap", + }, + { + value: 'Dataplots synchronization', + component: ( + + ), + icon: + customizedDataGrid.synchronizedGrids.color !== '' ? ( + + ) : ( + + ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't be synchronized", + }, + ]; + + const dataAccordions: accordionItemsType[] = [ + { + value: 'Axis range', + component: ( + + ), + }, + { + value: 'Downsampling', + component: ( + + ), + }, + ]; + + const items = ( + customizedType === 'visual' ? visualAccordions : dataAccordions + ).map((item) => ( + + + + {item.value} + + {item.component} + + + )); + + return ( + + + {customizedType === 'visual' + ? 'Visual customization' + : 'Data manipulation'} + + + + {items} + + + + ); +}; diff --git a/frontend/src/renderer/types/configuration.ts b/frontend/src/renderer/types/configuration.ts index 88aa51b4..6e204da0 100644 --- a/frontend/src/renderer/types/configuration.ts +++ b/frontend/src/renderer/types/configuration.ts @@ -9,6 +9,13 @@ export interface BaseConfiguration { lastURIInput?: string; } +export type CustomizedGridType = 'data' | 'visual'; + +export type CustomizedGrid = { + id: string; + type: CustomizedGridType; +}; + export interface Configuration extends BaseConfiguration { checkedNodeURI: URITreeNodeData[]; customDataTree: CustomTreeData[]; @@ -16,7 +23,7 @@ export interface Configuration extends BaseConfiguration { path?: string; saved?: boolean; metadataGridLayout?: string | null; - customizedGridLayout?: string | null; + customizedGridLayout?: CustomizedGrid | null; } export interface ConfigurationToSave extends BaseConfiguration { From 7d7faed5da90652ab70f8f668fa3935f0fb36224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Fri, 22 May 2026 15:03:51 +0200 Subject: [PATCH 2/7] Allow the user to select interpolation method in data manipulation --- frontend/src/renderer/layout/MainLayout.tsx | 1 + .../visualization/DataplotCustomization.tsx | 24 ++- .../visualization/VisualizationMetaData.tsx | 2 + .../CustomizeDataRange.tsx | 2 + .../CustomizeDownsampling.tsx | 2 + .../CustomizeInterpolation.tsx | 196 ++++++++++++++++++ .../customizableElements/index.ts | 5 + frontend/src/renderer/types/data.ts | 23 ++ frontend/src/renderer/types/plot.ts | 1 + frontend/src/renderer/utils/fetchData.ts | 27 ++- frontend/src/renderer/utils/plot.ts | 19 ++ 11 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 frontend/src/renderer/pages/visualization/customizableElements/CustomizeInterpolation.tsx diff --git a/frontend/src/renderer/layout/MainLayout.tsx b/frontend/src/renderer/layout/MainLayout.tsx index 8ae53f07..3fbb53ed 100644 --- a/frontend/src/renderer/layout/MainLayout.tsx +++ b/frontend/src/renderer/layout/MainLayout.tsx @@ -105,6 +105,7 @@ export function MainLayout() { synchronizedGrids: dataGrid.synchronizedGrids, downsampled_method: dataGrid?.downsampled_method, downsampled_size: dataGrid?.downsampled_size, + interpolated_method: dataGrid?.interpolated_method, xAxisData: dataGrid.xAxisData, yAxisData: dataGrid.yAxisData, y2AxisData: dataGrid?.y2AxisData, diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index e4a23e8f..ac0fd376 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -20,11 +20,15 @@ import { DataPlotly, 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 { + CustomizeDownsampling, + CustomizeGlobal, + CustomizeHeatmap, + Customize1DPlot, + CustomizeDataRange, + CustomizeSynchronization, + CustomizeInterpolation, +} from './customizableElements'; import { IconLink } from '@tabler/icons-react'; import { initPlotColors } from '../../utils'; @@ -53,6 +57,7 @@ export const DataplotCustomization = () => { ...dataGridLayout, title: customizedDataGrid?.title, downsampled_method: customizedDataGrid?.downsampled_method, + interpolated_method: customizedDataGrid?.interpolated_method, plot: customizedDataGrid?.plot, } as DataGridPlot; setDataGridLayout(updatedDataGridLayout); @@ -444,6 +449,15 @@ const Customization = ({ /> ), }, + { + value: 'Interpolation', + component: ( + + ), + }, ]; const items = ( diff --git a/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx index 31aeaf57..b1e904cf 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx @@ -170,6 +170,8 @@ export const MetaDataInfos = ({ selectedDataPlot?.downsampled_method, selectedDataPlot?.downsampled_size, selectedDataPlot?.dataType, + undefined, + selectedDataPlot?.interpolated_method, ); setCoordinates(response.data.coordinates); diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx index 1b676bc9..843664a5 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx @@ -174,6 +174,8 @@ export const CustomizeDataRange = ({ updatedDataPlot?.downsampled_method, updatedDataPlot?.downsampled_size, updatedDataPlot?.dataType, + undefined, + updatedDataPlot?.interpolated_method, ); if (plot?.error_bands?.length > 0) { diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx index 9949754e..383f9ec0 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx @@ -50,6 +50,8 @@ export const CustomizeDownsampling = ({ downsamplingMethod, downsamplingSize, updatedDataPlot?.dataType, + undefined, + updatedDataPlot?.interpolated_method, ); if (plot?.error_bands?.length) { diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeInterpolation.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeInterpolation.tsx new file mode 100644 index 00000000..0efdfe73 --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeInterpolation.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState } from 'react'; +import { DataGridPlot } from '../../../types'; +import { + fetchDataPlot, + fetchDataManipulationMethods, + fetchErrorBands, + getArrayValueFromDependance, + getFirstArrayValueFromShape, + getVectorData, + normalizeIndices, +} from '../../../utils'; +import { showNotification } from '@mantine/notifications'; +import { Group, Loader, Select, Stack } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { OptionWithTooltip } from '../../../types/components/select'; +import { RenderSelectOption } from '../../../components/select'; + +interface CustomizeInterpolationProps { + customizedDataGrid: DataGridPlot; + setCustomizedDataGrid: React.Dispatch>; +} +export const CustomizeInterpolation = ({ + customizedDataGrid, + setCustomizedDataGrid, +}: CustomizeInterpolationProps) => { + const [interpolationMethods, setInterpolationMethods] = useState< + OptionWithTooltip[] + >([]); + const [selectedInterpolation, setSelectedInterpolation] = useState< + string | null + >(null); + const [loading, { open, close }] = useDisclosure(); + + /** + * Update configuration with interpolated data (changes coordinates, plots & error bands) + */ + const getInterpolatedData = async () => { + try { + open(); + const updatedDataPlot = structuredClone( + customizedDataGrid, + ) as DataGridPlot; + + let plotIndex = 0; + for (const plot of updatedDataPlot.plot) { + // Interpolated data + const dataPlotInterpolated = await fetchDataPlot( + normalizeIndices(plot.nodeUri), + customizedDataGrid?.downsampled_method, + customizedDataGrid?.downsampled_size, + updatedDataPlot?.dataType, + undefined, + selectedInterpolation, + ); + + if (plot?.error_bands?.length) { + // Interpolate error bands with provided parameters if error bands exists for this plot + await fetchErrorBands( + updatedDataPlot, + plot.nodeUri, + undefined, + undefined, + // selectedInterpolation, // TODO : force the interpolation with the selected one + ); + } + + if (plotIndex === 0) { + // Update coordinates with interpolated data only once because each plots have same coordinates + let coordinateIndex = 0; + for (const coordinate of updatedDataPlot.coordinates) { + // Apply new shape + coordinate.shape = + dataPlotInterpolated.data.coordinates[ + coordinateIndex + ].downsampled_shape; + // Apply new data + coordinate.data = + dataPlotInterpolated.data.coordinates[coordinateIndex].value; + coordinateIndex++; + // Apply new range + coordinate.range = [ + 0, + coordinate.shape[coordinate.shape.length - 1] - 1, + ]; + const firstArrayValueFromCoord = getFirstArrayValueFromShape( + coordinate.data, + coordinate.shape, + ); + + coordinate.rangeValues = [ + firstArrayValueFromCoord[0], + firstArrayValueFromCoord[firstArrayValueFromCoord.length - 1], + ]; + } + + // Update interpolated method + updatedDataPlot.interpolated_method = + dataPlotInterpolated.data.interpolated_method; + } + + // Update plot with interpolated data + plot.shape = dataPlotInterpolated.data.downsampled_shape; + // Get x axis switch coordinates dependances + plot.x = getArrayValueFromDependance(updatedDataPlot.coordinates, 0); + plot.yData = dataPlotInterpolated.data.value; + // Get y axis + const vectorData = getVectorData( + updatedDataPlot.coordinates, + plot.yData, + ); + plot.y = vectorData; + + plotIndex++; + } + + // Save new configuration with interpolated data + setCustomizedDataGrid({ + ...customizedDataGrid, + coordinates: updatedDataPlot.coordinates, + interpolated_method: updatedDataPlot.interpolated_method, + plot: updatedDataPlot.plot, + }); + } catch (error) { + console.error('Error getting interpolated data: ', error); + showNotification({ + title: 'Error', + message: `Unable to get interpolated data.`, + color: 'red', + }); + } finally { + close(); + } + }; + + /* + * Get interpolated methods to show in select + */ + useEffect(() => { + const getInterpolationMethods = async () => { + const methodsRes = await fetchDataManipulationMethods(); + const options: OptionWithTooltip[] = methodsRes.data_manipulation_methods + .find((data_manip) => data_manip.name === 'Data interpolation') + .method_parameters.find( + (param) => param.name === 'interpolation_method', + ) + .possible_values.map((item) => ({ + value: item.value, + tooltip: item.description, + })); + setInterpolationMethods(options); + }; + getInterpolationMethods(); + }, []); + + useEffect(() => { + // Update interpolated method after a timeout + if (customizedDataGrid.interpolated_method) { + setSelectedInterpolation(customizedDataGrid.interpolated_method); + } + }, [customizedDataGrid.interpolated_method]); + + useEffect(() => { + if (selectedInterpolation) { + getInterpolatedData(); + } + }, [selectedInterpolation]); + + return ( + + +