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 ff3ad203..8aba4dd9 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; @@ -274,16 +275,34 @@ export const HoverButtons = React.memo( )} {data.coordinates.length && !shouldDisplayMetadata && ( - // Show customization button only if plottable - + // Show data manipulation button only if plottable + handleCustomization(data.i)} + aria-label="Data manipulation" + data-testid="data-customization-access-button" + onClick={() => handleCustomization(data.i, 'data')} + className={classes.actionButton} + > + + + + )} + + {data.coordinates.length && !shouldDisplayMetadata && ( + // Show visual customization button only if plottable + + handleCustomization(data.i, 'visual')} className={classes.actionButton} > - diff --git a/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx b/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx index 700d425d..6a845572 100644 --- a/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx +++ b/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx @@ -124,7 +124,7 @@ export const TreeLibrariesAccordion = ({ editedDataPlot={editedDataPlot} checkedNodes={checkedNodes} metadataGridLayout={stableMetadataGridLayout} - customizedGridLayout={stableCustomizedGridLayout} + customizedGridLayout={stableCustomizedGridLayout?.id} previousEditedIdRef={previousEditedIdRef} handleSelectChildren={handleSelectChildren} getCheckedNodes={getNodesChecked} diff --git a/frontend/src/renderer/layout/MainLayout.tsx b/frontend/src/renderer/layout/MainLayout.tsx index 8ae53f07..19d80615 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 93d403af..9247ce6a 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -15,186 +15,22 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { SimplePlotly, Heatmap2D, TabsListCustom } from '../../components'; import { Configuration, + CustomizedGridType, DataGridPlot, 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'; -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); @@ -221,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); @@ -237,7 +74,7 @@ export const DataplotCustomization = () => { if (active?.customizedGridLayout) { const data = structuredClone( active.dataPlot.find( - (item: DataGridPlot) => item.i === active.customizedGridLayout, + (item: DataGridPlot) => item.i === active.customizedGridLayout.id, ), ); if (data) { @@ -293,11 +130,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, ]; @@ -456,6 +293,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: 'Axis range', + component: ( + + ), + }, + { + value: 'Dataplots synchronization', + component: ( + + ), + icon: + customizedDataGrid.synchronizedGrids.color !== '' ? ( + + ) : ( + + ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't be synchronized", + }, + ]; + + const dataAccordions: accordionItemsType[] = [ + { + value: 'Downsampling', + component: ( + + ), + }, + { + value: 'Interpolation', + 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/pages/visualization/VisualizationMetaData.tsx b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx index 643661a1..b8e8953a 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx @@ -22,7 +22,11 @@ import { Axis, PlotDataResponse, } from 'src/renderer/types'; -import { fetchArraySummary, fetchDataPlot } from '../../utils'; +import { + fetchArraySummary, + fetchDataPlot, + getUrisToInterpolate, +} from '../../utils'; interface MetaDataInfosProps { gridLayoutKey: string; @@ -165,11 +169,17 @@ export const MetaDataInfos = ({ (gridLayout) => gridLayout.i === gridLayoutKey, ); + const urisToInterpolate = getUrisToInterpolate( + data.nodeUri, + selectedDataPlot.plot, + ); const response: PlotDataResponse = await fetchDataPlot( data.nodeUri, selectedDataPlot?.downsampled_method, selectedDataPlot?.downsampled_size, selectedDataPlot?.dataType, + urisToInterpolate, + 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 e5abfcb8..e87699cd 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDataRange.tsx @@ -180,6 +180,7 @@ export const CustomizeDataRange = ({ updatedDataPlot?.downsampled_size, updatedDataPlot?.dataType, urisToInterpolate, + 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 38f670a0..8e5149ef 100644 --- a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeDownsampling.tsx @@ -6,6 +6,7 @@ import { fetchErrorBands, getArrayValueFromDependance, getFirstArrayValueFromShape, + getUrisToInterpolate, getVectorData, normalizeIndices, } from '../../../utils'; @@ -45,11 +46,17 @@ export const CustomizeDownsampling = ({ let plotIndex = 0; for (const plot of updatedDataPlot.plot) { // Downsample data + const urisToInterpolate = getUrisToInterpolate( + plot.nodeUri, + updatedDataPlot.plot, + ); const dataPlotDownsampled = await fetchDataPlot( normalizeIndices(plot.nodeUri), downsamplingMethod, downsamplingSize, updatedDataPlot?.dataType, + urisToInterpolate, + 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..2a307c5c --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeInterpolation.tsx @@ -0,0 +1,187 @@ +import { useEffect, useState } from 'react'; +import { DataGridPlot } from '../../../types'; +import { + fetchDataPlot, + fetchErrorBands, + getArrayValueFromDependance, + getFirstArrayValueFromShape, + getVectorData, + normalizeIndices, + getInterpolationMethods, + getUrisToInterpolate, +} 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( + customizedDataGrid.interpolated_method, + ); + 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 urisToInterpolate = getUrisToInterpolate( + plot.nodeUri, + updatedDataPlot.plot, + ); + const dataPlotInterpolated = await fetchDataPlot( + normalizeIndices(plot.nodeUri), + customizedDataGrid?.downsampled_method, + customizedDataGrid?.downsampled_size, + updatedDataPlot?.dataType, + urisToInterpolate, + 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, + ); + } + + 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 getInterpolationOptions = async () => { + const options = await getInterpolationMethods(); + setInterpolationMethods(options); + }; + getInterpolationOptions(); + }, []); + + useEffect(() => { + if (selectedInterpolation !== customizedDataGrid?.interpolated_method) { + getInterpolatedData(); + } + }, [selectedInterpolation]); + + return ( + + +