From 3c8a7cea13e73b24779e1f5717b0f90a61fec5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 19 May 2026 13:59:27 +0200 Subject: [PATCH 01/12] Change ux way to select the type of plot to display --- .../components/grid/GridLayoutPlot.tsx | 12 +- .../renderer/components/grid/HoverButtons.tsx | 114 ++++++++++++------ .../renderer/components/plot/Heatmap2D.tsx | 10 +- frontend/src/renderer/types/plot.ts | 4 +- 4 files changed, 100 insertions(+), 40 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 7939f26e..e5daabc9 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -34,7 +34,10 @@ export const GridLayoutPlot = ({ ); const [widthGrid, setWidthGrid] = useState(Math.floor(data.w * colWidth)); const [is3DView, setIs3DView] = useState( - data?.selectedPlotMode === 'Heatmap' ? true : false, + data?.selectedPlotMode === 'Heatmap' || + data?.selectedPlotMode === 'Geometry' + ? true + : false, ); const [active3DTab, setActive3DTab] = useState('0'); const [metadataTabsValue, setMetadataTabsValue] = useState( @@ -214,7 +217,12 @@ export const GridLayoutPlot = ({ }, [data.plot]); useEffect(() => { - setIs3DView(data?.selectedPlotMode === 'Heatmap' ? true : false); + setIs3DView( + data?.selectedPlotMode === 'Heatmap' || + data?.selectedPlotMode === 'Geometry' + ? true + : false, + ); }, [data.selectedPlotMode]); /** diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 83cbe706..7fb060d1 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -1,5 +1,5 @@ import classes from './HoverButtons.module.css'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Group, Tooltip, @@ -8,21 +8,19 @@ import { Tabs, Switch, ScrollArea, + Menu, } from '@mantine/core'; import { IconBrandDatabricks, IconCheck, IconEdit, + IconGeometry, IconPalette, IconTrash, } from '@tabler/icons-react'; import { useHover } from '@mantine/hooks'; -import { Configuration, DataGridPlot } from '../../types'; -import { - applyRange, - containsFloat, - fetchErrorBandsInConfig, -} from '../../utils'; +import { Configuration, DataGridPlot, PlotType } from '../../types'; +import { applyRange, fetchErrorBandsInConfig } from '../../utils'; import { useIbexStore } from '../../stores'; interface HoverButtonsProps { @@ -54,9 +52,16 @@ export const HoverButtons = React.memo( const previousValueDisplayErrorBands = useRef( undefined, ); + const [plotMode, setPlotMode] = useState( + active.dataPlot.find((dataPlot) => dataPlot.i === data.i) + ?.selectedPlotMode, + ); + const [plotTypeMenuOpened, setPlotTypeMenuOpened] = useState(false); + const [forcePlotTypeMenuOpened, setForcePlotTypeMenuOpened] = + useState(false); const heatmapLogo = ( - + @@ -71,6 +76,15 @@ export const HoverButtons = React.memo( ); + const modes: { + value: PlotType; + icon?: React.ReactNode; + }[] = [ + { value: '1D' }, + { value: 'Heatmap', icon: heatmapLogo }, + { value: 'Geometry', icon: }, + ]; + const updateDisplayErrorBands = useCallback( (newValue: boolean) => { const updatedActive = JSON.parse( @@ -161,7 +175,7 @@ export const HoverButtons = React.memo( }, [data.displayErrorBand]); const updateSelectedPlotMode = ( - is3DView: boolean, + plotTypeWanted: PlotType, active: Configuration, ) => { const updatedDataPlot: DataGridPlot[] = JSON.parse( @@ -170,17 +184,9 @@ export const HoverButtons = React.memo( const selectedDataPlot = updatedDataPlot.find( (dataPlot) => dataPlot.i === data.i, ); - if (selectedDataPlot?.selectedPlotMode) { - selectedDataPlot.selectedPlotMode = is3DView ? 'Heatmap' : '1D'; - } else { - selectedDataPlot.selectedPlotMode = - data.coordinates.length >= 2 && - containsFloat( - data.coordinates.find((coord) => coord.axeIndex === 1)?.data, - ) - ? 'Heatmap' - : '1D'; - } + + // Update plot type + selectedDataPlot.selectedPlotMode = plotTypeWanted; const updatedActive: Configuration = { ...active, @@ -189,6 +195,13 @@ export const HoverButtons = React.memo( updatedConfiguration(updatedActive); }; + const updateTypeOfPlot = (wantedType: PlotType) => { + setPlotMode(wantedType); + updateSelectedPlotMode(wantedType, active); + setPlotTypeMenuOpened(false); + setForcePlotTypeMenuOpened(false); + }; + return (
@@ -228,7 +241,10 @@ export const HoverButtons = React.memo(
)} - {hovered || data.isEditing ? ( + {hovered || + data.isEditing || + plotTypeMenuOpened || + forcePlotTypeMenuOpened ? ( {!is3DView && data.isEditing && !shouldDisplayMetadata && ( = 2 && !shouldDisplayMetadata && ( - - - updateSelectedPlotMode( - !(data.selectedPlotMode === 'Heatmap'), - active, - ) - } - className={classes.actionButton} - > - {is3DView ? 1D : heatmapLogo} - - + + + + setForcePlotTypeMenuOpened((o) => !o)} + variant="filled" + aria-label="Select plot mode" + className={classes.actionButton} + > + {modes.find((m) => m.value === plotMode)?.icon || ( + {plotMode} + )} + + + + + + {modes.map((mode) => ( + { + updateTypeOfPlot(mode.value); + }} + leftSection={mode.icon} + rightSection={ + plotMode === mode.value ? ( + + ) : null + } + > + {mode.value} + + ))} + + )} {data.coordinates.length && !shouldDisplayMetadata && ( diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index d693ddc1..001ccfac 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -415,7 +415,15 @@ export const Heatmap2D = ({ ref={plotRef} data={[ { - type: 'heatmap', + type: + itemDataGrid.selectedPlotMode === 'Heatmap' + ? 'heatmap' + : itemDataGrid.selectedPlotMode === 'Geometry' + ? 'contour' + : plotRef.current.props.data[0].type, + contours: { + coloring: 'lines', + }, colorscale: itemDataGrid.plot[parseInt(plotIndex)]?.customPreferences ?.colorscale || 'Viridis', diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index 8e7635b5..83e8657e 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -103,7 +103,7 @@ export interface DataGridPlot extends Layout, BaseDataGridPlot { coordinates?: Coordinates[]; downsampled_method?: string; downsampled_size?: number; - selectedPlotMode?: 'Heatmap' | '1D'; + selectedPlotMode?: PlotType; } export interface DataGridPlotToSave extends BaseDataGridPlot { @@ -116,3 +116,5 @@ export type synchronizedList = { color: string; list: string[]; }; + +export type PlotType = '1D' | 'Heatmap' | 'Geometry'; From 42d600daf3de98d73423c3af6176167f7536d157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Thu, 28 May 2026 13:20:10 +0200 Subject: [PATCH 02/12] Add contour visualization, overlay outline geometry and adapt features to works with geometries --- .../components/grid/GridLayoutPlot.tsx | 29 ++++- .../renderer/components/grid/HoverButtons.tsx | 68 ++++++++--- .../renderer/components/plot/Heatmap2D.tsx | 19 ++- .../renderer/components/tree/TreeLibrary.tsx | 5 + frontend/src/renderer/types/nodes.ts | 1 + frontend/src/renderer/types/plot.ts | 18 +++ frontend/src/renderer/utils/plot.ts | 112 +++++++++++++++++- 7 files changed, 222 insertions(+), 30 deletions(-) diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index e5daabc9..369568ae 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -148,8 +148,8 @@ export const GridLayoutPlot = ({ return { ...plotItem, - x: newXData, - y: newYData, + x: [...newXData], + y: [...newYData], customdata: customdata, error_bands: updated_error_bands, nodeUri: updatedNodeUri, @@ -158,8 +158,8 @@ export const GridLayoutPlot = ({ } else { return { ...plotItem, - x: newXData, - y: newYData, + x: [...newXData], + y: [...newYData], nodeUri: updatedNodeUri, path: updatedPath, }; @@ -314,6 +314,27 @@ export const GridLayoutPlot = ({ } } } + + if (findPlot?.geometrie) { + // Check geometries in tree + for (const geometry of findPlot.geometrie) { + for (const uriOfGeo of geometry.nodeUris) { + const newCheckedNode = { + name: findPlot.plot[0].labelUri, + uri: normalizeIndices(uriOfGeo), + type: 'GEO', + } as URITreeNodeData; + const exists = checkedNodeURI.some( + (node) => + node.name === newCheckedNode.name && + node.uri === newCheckedNode.uri, + ); + if (!exists) { + checkedNodeURI.push(newCheckedNode); + } + } + } + } } const updatedActive: Configuration = { diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 7fb060d1..601fe7bf 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -20,8 +20,13 @@ import { } from '@tabler/icons-react'; import { useHover } from '@mantine/hooks'; import { Configuration, DataGridPlot, PlotType } from '../../types'; -import { applyRange, fetchErrorBandsInConfig } from '../../utils'; +import { + applyRange, + fetchErrorBandsInConfig, + fetchGeometries, +} from '../../utils'; import { useIbexStore } from '../../stores'; +import { showNotification } from '@mantine/notifications'; interface HoverButtonsProps { data: DataGridPlot; @@ -174,34 +179,61 @@ export const HoverButtons = React.memo( updateErrorBands(); }, [data.displayErrorBand]); - const updateSelectedPlotMode = ( - plotTypeWanted: PlotType, - active: Configuration, - ) => { - const updatedDataPlot: DataGridPlot[] = JSON.parse( - JSON.stringify(active.dataPlot), - ); + const updateTypeOfPlot = async (wantedType: PlotType) => { + setPlotTypeMenuOpened(false); + setForcePlotTypeMenuOpened(false); + const updatedDataPlot: DataGridPlot[] = structuredClone(active.dataPlot); const selectedDataPlot = updatedDataPlot.find( (dataPlot) => dataPlot.i === data.i, ); + // Control the coordinates order to prevent from adding geometry when coordinates are incompatible with geometries + if ( + wantedType === 'Geometry' && + !( + selectedDataPlot.coordinates.length >= 2 && + (selectedDataPlot.coordinates[0].axeIndex === 0 || + selectedDataPlot.coordinates[0].axeIndex === 1) && + (selectedDataPlot.coordinates[1].axeIndex === 0 || + selectedDataPlot.coordinates[1].axeIndex === 1) + ) + ) { + console.warn( + 'Axis are not matching with geometries: the default axis should be restored to get geometries.', + ); + showNotification({ + title: 'Axis are not matching with geometries', + message: 'The default axis should be restored to get geometries.', + color: 'yellow', + }); + return; + } + setPlotMode(wantedType); + + const checkedNodeURI = structuredClone(active.checkedNodeURI); + // Update plot type - selectedDataPlot.selectedPlotMode = plotTypeWanted; + selectedDataPlot.selectedPlotMode = wantedType; + + // Handle geometries in contour type + if (wantedType === 'Geometry') { + const fetchedGeometrie = await fetchGeometries( + selectedDataPlot, + checkedNodeURI, + ); + selectedDataPlot.geometrie = fetchedGeometrie.geometrie; + } else { + delete selectedDataPlot.geometrie; + } const updatedActive: Configuration = { ...active, dataPlot: updatedDataPlot, + checkedNodeURI: checkedNodeURI, }; updatedConfiguration(updatedActive); }; - const updateTypeOfPlot = (wantedType: PlotType) => { - setPlotMode(wantedType); - updateSelectedPlotMode(wantedType, active); - setPlotTypeMenuOpened(false); - setForcePlotTypeMenuOpened(false); - }; - return (
@@ -283,8 +315,8 @@ export const HoverButtons = React.memo( {modes.map((mode) => ( { - updateTypeOfPlot(mode.value); + onClick={async () => { + await updateTypeOfPlot(mode.value); }} leftSection={mode.icon} rightSection={ diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 001ccfac..617ddba9 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -316,9 +316,13 @@ export const Heatmap2D = ({ coord.axeIndex === (targetAxis === 'y' ? 1 : 0), ).name } - data={itemDataGrid.coordinates.map( - (coord: Coordinates) => coord.name, - )} + data={(itemDataGrid.geometrie // In contour plot, allow to transpose only x & y to keep compatibles coordinates with geometries + ? itemDataGrid.coordinates.filter( + (coord) => + coord.axeIndex === 0 || coord.axeIndex === 1, + ) + : itemDataGrid.coordinates + ).map((coord: Coordinates) => coord.name)} w={`${width * 0.2}px`} onChange={(value) => value && @@ -439,10 +443,13 @@ export const Heatmap2D = ({ }, hovertemplate: 'x: %{x}
' + 'y: %{y}
' + 'z: %{z:,.6g}', - x: x, - y: y, - z: z, + x: [...x], + y: [...y], + z: z.map((row) => [...row]), }, + + // Add geometries in contour type + ...(itemDataGrid?.geometrie ?? []), ]} config={{ autosizable: false, diff --git a/frontend/src/renderer/components/tree/TreeLibrary.tsx b/frontend/src/renderer/components/tree/TreeLibrary.tsx index 810ce080..9a49df85 100644 --- a/frontend/src/renderer/components/tree/TreeLibrary.tsx +++ b/frontend/src/renderer/components/tree/TreeLibrary.tsx @@ -18,6 +18,7 @@ import { IconHash, IconRipple, IconTypography, + IconGeometry, } from '@tabler/icons-react'; import classes from './TreeLibrary.module.css'; import { @@ -314,6 +315,10 @@ function NodeIcon({ [NodeInfoTypeEnum.COMPLEX]: getCheckboxIcon( , ), + [NodeInfoTypeEnum.GEOMETRY]: getCheckboxIcon( + // TODO : use geometry type from BE + , + ), }; return ( diff --git a/frontend/src/renderer/types/nodes.ts b/frontend/src/renderer/types/nodes.ts index ee4d005a..81870802 100644 --- a/frontend/src/renderer/types/nodes.ts +++ b/frontend/src/renderer/types/nodes.ts @@ -5,6 +5,7 @@ export enum NodeInfoTypeEnum { FLOAT = 'FLT', STRING = 'STR', COMPLEX = 'CPX', + GEOMETRY = 'GEO', // TODO : use geometry type from BE } export type NodeInfoChildrenResponse = { diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index 83e8657e..bd760d98 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -46,6 +46,22 @@ export interface BaseDataPlotly { mode?: string; } +export type Geometry = { + type: 'scatter'; + mode: 'lines'; + x: number[]; + y: number[]; + line: GeometryLine; + nodeUris: string[]; + fill?: 'toself'; + name?: string; +}; + +type GeometryLine = { + color: string; + width: number; +}; + export type DataPlotly = BaseDataPlotly & Data & { x: (string | number)[]; @@ -104,12 +120,14 @@ export interface DataGridPlot extends Layout, BaseDataGridPlot { downsampled_method?: string; downsampled_size?: number; selectedPlotMode?: PlotType; + geometrie?: Geometry[]; } export interface DataGridPlotToSave extends BaseDataGridPlot { dataType: NodeInfoTypeEnum; plot: BaseDataPlotly[]; coordinates: BaseCoordinates[]; + geometrie?: Geometry[]; } export type synchronizedList = { diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 4bb5be4b..19e6dfc0 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -9,6 +9,7 @@ import { DataGridPlot, DataPlotly, ErrorBandData, + Geometry, PlotCoordinatesResponse, PlotDataResponse, PlotLine, @@ -575,6 +576,104 @@ export const fetchErrorBandsInConfig = async ( } }; +function closeContourGeometrie(data: AxisData): AxisData { + if (typeof data[0] === 'number') { + const vector = data as number[]; + + // Add first element in the end of the vector + return [...vector, vector[0]] as AxisData; + } + + // Go through last depth + //eslint-disable-next-line @typescript-eslint/no-explicit-any + return (data as any[]).map(closeContourGeometrie) as AxisData; +} + +export const fetchGeometries = async ( + dataPlot: DataGridPlot, + updatedCheckedNodeURI: URITreeNodeData[], +) => { + try { + const uri = dataPlot.plot[0].nodeUri.split('#')[0]; // ? Need a rule in the case we have plots from different URIs (at the moment we get geometries from first URI plotted) + + // TODO replace constrained paths by them provided by BE + const rPath = '#wall:0/description_2d[:]/limiter/unit[:]/outline/r'; + const zPath = '#wall:0/description_2d[:]/limiter/unit[:]/outline/z'; + + // Get r + const rResponse = await fetchDataPlot(normalizeIndices(uri + rPath)); + rResponse.data.value = closeContourGeometrie(rResponse.data.value); + const rFormattedCoordinates = formatCoordinates( + rResponse.data.coordinates, + normalizeIndices(uri + rPath), + 0, + ); + const rVector = getVectorData(rFormattedCoordinates, rResponse.data.value); + + // Get z + const zResponse = await fetchDataPlot(normalizeIndices(uri + zPath)); + zResponse.data.value = closeContourGeometrie(zResponse.data.value); + const zFormattedCoordinates = formatCoordinates( + zResponse.data.coordinates, + normalizeIndices(uri + zPath), + 0, + ); + const zVector = getVectorData(zFormattedCoordinates, zResponse.data.value); + + let x, y: number[]; + const shouldSwitchAxis = + dataPlot.coordinates.findIndex((coord) => coord.axeIndex === 0) === 1; + + if (shouldSwitchAxis) { + x = zVector; + y = rVector; + } else { + x = rVector; + y = zVector; + } + // Get geometries // ? (contour case) + const contourGeometry: Geometry[] = [ + { + x: [...x], + y: [...y], + type: 'scatter', + mode: 'lines', + line: { color: 'black', width: 2 }, + nodeUris: [uri + rPath, uri + zPath], + name: 'contour', + }, + ]; + + dataPlot.geometrie = contourGeometry; + + if (dataPlot.isEditing && dataPlot?.geometrie) { + // Check geometries in tree + for (const geometry of dataPlot.geometrie) { + for (const uriOfGeo of geometry.nodeUris) { + const newCheckedNode = { + name: dataPlot.plot[0].labelUri, + uri: normalizeIndices(uriOfGeo), + type: 'GEO', // ? geometry type, (contour at the moment) + } as URITreeNodeData; + const exists = updatedCheckedNodeURI.some( + (node) => + node.name === newCheckedNode.name && + node.uri === newCheckedNode.uri, + ); + if (!exists) { + updatedCheckedNodeURI.push(newCheckedNode); + } + } + } + } + + // Return dataPlot list with the plot which includes error bands + return dataPlot; + } catch (error) { + console.error('Error getting geometries:', error); + } +}; + /** * Get & return error bands of provided uri & dataPlot id * @param dataPlot Datagrid containing the targeted uri @@ -719,8 +818,8 @@ const formatErrorBandLayout = ( } } const errBandPartPlot: Partial = { - x: mainPlot.x, - y: yErrBandPart, + x: [...mainPlot.x], + y: [...yErrBandPart], type: 'scatter', mode: 'lines', line: { width: 0, shape: lineShape }, @@ -1537,6 +1636,15 @@ export const swapAxis = async ( // Limit coordinate sliders to the max of their new shape limitSlidersToMaxLength(updatedDataPlot.coordinates); + if (updatedDataPlot.geometrie) { + // Swap geometrie x & y in the case we swap x & y coordinates + for (const geo of updatedDataPlot.geometrie) { + const tempX = geo.x; + geo.x = geo.y; + geo.y = tempX; + } + } + if (active && updatedConfiguration) { const updatedActive = { ...active, From 19dab76329bbb5f7fb2d3712008e6a9f7c783701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Thu, 28 May 2026 14:49:13 +0200 Subject: [PATCH 03/12] Control if there is no geometry to display --- .../src/renderer/components/grid/HoverButtons.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 601fe7bf..fcee4c5c 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -208,7 +208,6 @@ export const HoverButtons = React.memo( }); return; } - setPlotMode(wantedType); const checkedNodeURI = structuredClone(active.checkedNodeURI); @@ -221,10 +220,20 @@ export const HoverButtons = React.memo( selectedDataPlot, checkedNodeURI, ); + if (!fetchedGeometrie) { + // Prevent from displaying contour plot when no geometry are available + showNotification({ + title: 'No geometry available', + message: 'This dataset does not contain any geometry to display.', + color: 'yellow', + }); + return; + } selectedDataPlot.geometrie = fetchedGeometrie.geometrie; } else { delete selectedDataPlot.geometrie; } + setPlotMode(wantedType); const updatedActive: Configuration = { ...active, From c6135448b1820530bdf9ff3ed857047575f1af4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Mon, 1 Jun 2026 16:11:26 +0200 Subject: [PATCH 04/12] Display rectangle geometries --- .../renderer/components/grid/HoverButtons.tsx | 3 +- .../renderer/components/plot/Heatmap2D.tsx | 1 + frontend/src/renderer/types/plot.ts | 2 +- frontend/src/renderer/utils/plot.ts | 206 ++++++++++++++---- 4 files changed, 166 insertions(+), 46 deletions(-) diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index fcee4c5c..93b98dfd 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -216,6 +216,7 @@ export const HoverButtons = React.memo( // Handle geometries in contour type if (wantedType === 'Geometry') { + // Get geometries const fetchedGeometrie = await fetchGeometries( selectedDataPlot, checkedNodeURI, @@ -231,7 +232,7 @@ export const HoverButtons = React.memo( } selectedDataPlot.geometrie = fetchedGeometrie.geometrie; } else { - delete selectedDataPlot.geometrie; + selectedDataPlot.geometrie = []; } setPlotMode(wantedType); diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 617ddba9..01e36263 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -78,6 +78,7 @@ export const Heatmap2D = ({ modebar: { orientation: 'v', }, + legend: { x: 1.3, y: 1 }, }); // Custom hook used for trigger some useEffects to update the layout usePlotLayout({ diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index bd760d98..d8373300 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -120,7 +120,7 @@ export interface DataGridPlot extends Layout, BaseDataGridPlot { downsampled_method?: string; downsampled_size?: number; selectedPlotMode?: PlotType; - geometrie?: Geometry[]; + geometrie: Geometry[]; } export interface DataGridPlotToSave extends BaseDataGridPlot { diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 19e6dfc0..9470f5ca 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -589,6 +589,140 @@ function closeContourGeometrie(data: AxisData): AxisData { return (data as any[]).map(closeContourGeometrie) as AxisData; } +const fetchGeometryOutline = async ( + uri: string, + path: string, + shouldSwitchAxis: boolean, +) => { + // TODO replace constrained paths by them provided by BE + const rPath = path + '/r'; + const zPath = path + '/z'; + + // Get r + const rResponse = await fetchDataPlot(normalizeIndices(uri + rPath)); + rResponse.data.value = closeContourGeometrie(rResponse.data.value); + const rFormattedCoordinates = formatCoordinates( + rResponse.data.coordinates, + normalizeIndices(uri + rPath), + 0, + ); + const rVector = getVectorData(rFormattedCoordinates, rResponse.data.value); + + // Get z + const zResponse = await fetchDataPlot(normalizeIndices(uri + zPath)); + zResponse.data.value = closeContourGeometrie(zResponse.data.value); + const zFormattedCoordinates = formatCoordinates( + zResponse.data.coordinates, + normalizeIndices(uri + zPath), + 0, + ); + const zVector = getVectorData(zFormattedCoordinates, zResponse.data.value); + + let x, y: number[]; + + if (shouldSwitchAxis) { + x = zVector; + y = rVector; + } else { + x = rVector; + y = zVector; + } + // Get outline + const outlineGeometry: Geometry = { + x: [...x], + y: [...y], + type: 'scatter', + mode: 'lines', + line: { color: 'black', width: 2 }, + nodeUris: [uri + rPath, uri + zPath], + name: 'outline', + }; + return outlineGeometry; +}; + +const fetchGeometryRectangle = async ( + uri: string, + path: string, + shouldSwitchAxis: boolean, +) => { + // TODO replace constrained paths by them provided by BE + const rPath = path + '/r'; + const zPath = path + '/z'; + const widthPath = path + '/width'; + const heightPath = path + '/height'; + + // Get r + const rResponse = await fetchDataPlot(normalizeIndices(uri + rPath)); + const rVector = rResponse.data.value as number[][]; + + // Get z + const zResponse = await fetchDataPlot(normalizeIndices(uri + zPath)); + const zVector = zResponse.data.value as number[][]; + + // Get width + const widthResponse = await fetchDataPlot(normalizeIndices(uri + widthPath)); + const widthVector = widthResponse.data.value as number[][]; + + // Get height + const heightResponse = await fetchDataPlot( + normalizeIndices(uri + heightPath), + ); + const heightVector = heightResponse.data.value as number[][]; + + const rectangleGeometry: Geometry[] = []; + const lastCoord = + zResponse.data.coordinates[zResponse.data.coordinates.length - 1]; + for (const [coordIndex, coordValue] of lastCoord.value.entries()) { + if ( + rVector[coordIndex][0] === -9e40 || + zVector[coordIndex][0] === -9e40 || + widthVector[coordIndex][0] === -9e40 || + heightVector[coordIndex][0] === -9e40 + ) { + // Data equals to -9e+40 are unexpected + continue; + } + + // Format (x,y) points with rectangle rule + let x, y: number[]; + x = [ + rVector[coordIndex][0] - widthVector[coordIndex][0] / 2, + rVector[coordIndex][0] + widthVector[coordIndex][0] / 2, + rVector[coordIndex][0] + widthVector[coordIndex][0] / 2, + rVector[coordIndex][0] - widthVector[coordIndex][0] / 2, + ]; + y = [ + zVector[coordIndex][0] - heightVector[coordIndex][0] / 2, + zVector[coordIndex][0] - heightVector[coordIndex][0] / 2, + zVector[coordIndex][0] + heightVector[coordIndex][0] / 2, + zVector[coordIndex][0] + heightVector[coordIndex][0] / 2, + ]; + + // Close x & y vectors + x.push(x[0]); + y.push(y[0]); + + if (shouldSwitchAxis) { + const tempX = x; + x = y; + y = tempX; + } + // Add a rectangle + rectangleGeometry.push({ + x: [...x], + y: [...y], + type: 'scatter', + mode: 'lines', + line: { color: 'orange', width: 2 }, + nodeUris: [uri + rPath, uri + zPath], + name: coordValue, + fill: 'toself', + } as Geometry); + } + // Return rectangle list + return rectangleGeometry; +}; + export const fetchGeometries = async ( dataPlot: DataGridPlot, updatedCheckedNodeURI: URITreeNodeData[], @@ -596,55 +730,38 @@ export const fetchGeometries = async ( try { const uri = dataPlot.plot[0].nodeUri.split('#')[0]; // ? Need a rule in the case we have plots from different URIs (at the moment we get geometries from first URI plotted) - // TODO replace constrained paths by them provided by BE - const rPath = '#wall:0/description_2d[:]/limiter/unit[:]/outline/r'; - const zPath = '#wall:0/description_2d[:]/limiter/unit[:]/outline/z'; - - // Get r - const rResponse = await fetchDataPlot(normalizeIndices(uri + rPath)); - rResponse.data.value = closeContourGeometrie(rResponse.data.value); - const rFormattedCoordinates = formatCoordinates( - rResponse.data.coordinates, - normalizeIndices(uri + rPath), - 0, - ); - const rVector = getVectorData(rFormattedCoordinates, rResponse.data.value); - - // Get z - const zResponse = await fetchDataPlot(normalizeIndices(uri + zPath)); - zResponse.data.value = closeContourGeometrie(zResponse.data.value); - const zFormattedCoordinates = formatCoordinates( - zResponse.data.coordinates, - normalizeIndices(uri + zPath), - 0, - ); - const zVector = getVectorData(zFormattedCoordinates, zResponse.data.value); + // TODO : get from BE all geometries to display + const paths: string[] = [ + '#wall:0/description_2d[:]/limiter/unit[:]/outline', + '#pf_active/coil[0]/element[0]/geometry/rectangle', + ]; - let x, y: number[]; const shouldSwitchAxis = dataPlot.coordinates.findIndex((coord) => coord.axeIndex === 0) === 1; - if (shouldSwitchAxis) { - x = zVector; - y = rVector; - } else { - x = rVector; - y = zVector; - } - // Get geometries // ? (contour case) - const contourGeometry: Geometry[] = [ - { - x: [...x], - y: [...y], - type: 'scatter', - mode: 'lines', - line: { color: 'black', width: 2 }, - nodeUris: [uri + rPath, uri + zPath], - name: 'contour', - }, - ]; + for (const path of paths) { + const pathSplitted = path.split('/'); + const typeOfGeometry = pathSplitted[pathSplitted.length - 1]; + if (typeOfGeometry === 'outline') { + // Get outline + const contourGeometry = await fetchGeometryOutline( + uri, + path, + shouldSwitchAxis, + ); + dataPlot.geometrie.push(contourGeometry); + } - dataPlot.geometrie = contourGeometry; + if (typeOfGeometry === 'rectangle') { + // Get rectangle + const rectangleGeometry = await fetchGeometryRectangle( + uri, + path, + shouldSwitchAxis, + ); + dataPlot.geometrie = [...dataPlot.geometrie, ...rectangleGeometry]; + } + } if (dataPlot.isEditing && dataPlot?.geometrie) { // Check geometries in tree @@ -1020,6 +1137,7 @@ export function formatConfigBeforeLoadingURIs( unit: '', } as DataPlotly; }), + geometrie: [], synchronizedGrids: data?.synchronizedGrids ? data.synchronizedGrids : { color: '', list: [] }, From 6cd1894395a45134f6e4269efb631690212cc1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Tue, 2 Jun 2026 14:52:18 +0200 Subject: [PATCH 05/12] Display oblique geometry and group the legend --- .../renderer/components/plot/Heatmap2D.tsx | 7 +- frontend/src/renderer/types/plot.ts | 4 +- frontend/src/renderer/utils/plot.ts | 176 +++++++++++++++++- 3 files changed, 180 insertions(+), 7 deletions(-) diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 01e36263..94e66841 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -78,7 +78,12 @@ export const Heatmap2D = ({ modebar: { orientation: 'v', }, - legend: { x: 1.3, y: 1 }, + legend: { + x: 1.3, + y: 1, + groupclick: 'togglegroup', + tracegroupgap: 0, + }, }); // Custom hook used for trigger some useEffects to update the layout usePlotLayout({ diff --git a/frontend/src/renderer/types/plot.ts b/frontend/src/renderer/types/plot.ts index d8373300..06b0c184 100644 --- a/frontend/src/renderer/types/plot.ts +++ b/frontend/src/renderer/types/plot.ts @@ -53,8 +53,10 @@ export type Geometry = { y: number[]; line: GeometryLine; nodeUris: string[]; + name: string; + legendgroup: string; + showlegend: boolean; fill?: 'toself'; - name?: string; }; type GeometryLine = { diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 9470f5ca..4692ee42 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -627,6 +627,12 @@ const fetchGeometryOutline = async ( x = rVector; y = zVector; } + + // Get legend name + const firstPart = path.slice(1).split('/')[0]; + const secondPart = path.slice(1).split('/unit')[0].split('/'); + const groupLegend = firstPart + '/' + secondPart[secondPart.length - 1]; + // Get outline const outlineGeometry: Geometry = { x: [...x], @@ -635,7 +641,9 @@ const fetchGeometryOutline = async ( mode: 'lines', line: { color: 'black', width: 2 }, nodeUris: [uri + rPath, uri + zPath], - name: 'outline', + name: groupLegend, + legendgroup: groupLegend, + showlegend: false, }; return outlineGeometry; }; @@ -672,7 +680,7 @@ const fetchGeometryRectangle = async ( const rectangleGeometry: Geometry[] = []; const lastCoord = zResponse.data.coordinates[zResponse.data.coordinates.length - 1]; - for (const [coordIndex, coordValue] of lastCoord.value.entries()) { + for (const [coordIndex] of lastCoord.value.entries()) { if ( rVector[coordIndex][0] === -9e40 || zVector[coordIndex][0] === -9e40 || @@ -707,6 +715,11 @@ const fetchGeometryRectangle = async ( x = y; y = tempX; } + + // Get legend name + const groupLegend = + path.slice(1).split('/')[0] + '/' + path.slice(1).split('/')[1]; + // Add a rectangle rectangleGeometry.push({ x: [...x], @@ -715,7 +728,9 @@ const fetchGeometryRectangle = async ( mode: 'lines', line: { color: 'orange', width: 2 }, nodeUris: [uri + rPath, uri + zPath], - name: coordValue, + name: groupLegend, + legendgroup: groupLegend, + showlegend: false, fill: 'toself', } as Geometry); } @@ -723,6 +738,135 @@ const fetchGeometryRectangle = async ( return rectangleGeometry; }; +const fetchGeometryOblique = async ( + uri: string, + path: string, + shouldSwitchAxis: boolean, +) => { + // TODO replace constrained paths by them provided by BE + const rPath = path + '/r'; + const zPath = path + '/z'; + const lengthAlphaPath = path + '/length_alpha'; + const lengthBetaPath = path + '/length_beta'; + const alphaPath = path + '/alpha'; + const betaPath = path + '/beta'; + + // Get r + const rResponse = await fetchDataPlot(normalizeIndices(uri + rPath)); + const rVector = rResponse.data.value as number[][]; + + // Get z + const zResponse = await fetchDataPlot(normalizeIndices(uri + zPath)); + const zVector = zResponse.data.value as number[][]; + + // Get length alpha + const lengthAlphaResponse = await fetchDataPlot( + normalizeIndices(uri + lengthAlphaPath), + ); + const lengthAlphaVector = lengthAlphaResponse.data.value as number[][]; + + // Get length beta + const lengthBetaResponse = await fetchDataPlot( + normalizeIndices(uri + lengthBetaPath), + ); + const lengthBetaVector = lengthBetaResponse.data.value as number[][]; + + // Get alpha + const alphaResponse = await fetchDataPlot(normalizeIndices(uri + alphaPath)); + const alphaVector = alphaResponse.data.value as number[][]; + + // Get beta + const betaResponse = await fetchDataPlot(normalizeIndices(uri + betaPath)); + const betaVector = betaResponse.data.value as number[][]; + const obliqueGeometry: Geometry[] = []; + const lastCoord = + zResponse.data.coordinates[zResponse.data.coordinates.length - 1]; + for (const [coordIndex] of lastCoord.value.entries()) { + if ( + rVector[coordIndex][0] === -9e40 || + zVector[coordIndex][0] === -9e40 || + lengthAlphaVector[coordIndex][0] === -9e40 || + lengthBetaVector[coordIndex][0] === -9e40 || + alphaVector[coordIndex][0] === -9e40 || + betaVector[coordIndex][0] === -9e40 + ) { + // Data equals to -9e+40 are unexpected + continue; + } + + // Format (x,y) points with oblique rule + let x, y: number[]; + + const r = rVector[coordIndex][0]; + const z = zVector[coordIndex][0]; + + const alphaLength = lengthAlphaVector[coordIndex][0]; + const betaLength = lengthBetaVector[coordIndex][0]; + + const alpha = alphaVector[coordIndex][0]; + const beta = betaVector[coordIndex][0]; + + // alpha: R axis reference + const vxAlpha = alphaLength * Math.cos(alpha); + const vyAlpha = alphaLength * Math.sin(alpha); + + // beta: Z axis reference + const vxBeta = -betaLength * Math.sin(beta); + const vyBeta = betaLength * Math.cos(beta); + + const p0 = { x: r, y: z }; + + const p1 = { + x: r + vxAlpha, + y: z + vyAlpha, + }; + + const p2 = { + x: r + vxBeta, + y: z + vyBeta, + }; + + const p3 = { + x: p1.x + vxBeta, + y: p1.y + vyBeta, + }; + + x = [p0.x, p1.x, p3.x, p2.x, p0.x]; + + y = [p0.y, p1.y, p3.y, p2.y, p0.y]; + + // Close x & y vectors + x.push(x[0]); + y.push(y[0]); + + if (shouldSwitchAxis) { + const tempX = x; + x = y; + y = tempX; + } + + // Get legend name + const groupLegend = + path.slice(1).split('/')[0] + '/' + path.slice(1).split('/')[1]; + + // Add a oblique + obliqueGeometry.push({ + x: [...x], + y: [...y], + type: 'scatter', + mode: 'lines', + line: { color: 'orange', width: 2 }, + nodeUris: [uri + rPath, uri + zPath], + name: groupLegend, + legendgroup: groupLegend, + showlegend: false, + fill: 'toself', + } as Geometry); + } + // Return oblique list + return obliqueGeometry; +}; + export const fetchGeometries = async ( dataPlot: DataGridPlot, updatedCheckedNodeURI: URITreeNodeData[], @@ -733,7 +877,8 @@ export const fetchGeometries = async ( // TODO : get from BE all geometries to display const paths: string[] = [ '#wall:0/description_2d[:]/limiter/unit[:]/outline', - '#pf_active/coil[0]/element[0]/geometry/rectangle', + '#pf_active/coil[:]/element[:]/geometry/rectangle', + '#pf_active/coil[:]/element[:]/geometry/oblique', ]; const shouldSwitchAxis = @@ -761,6 +906,27 @@ export const fetchGeometries = async ( ); dataPlot.geometrie = [...dataPlot.geometrie, ...rectangleGeometry]; } + + if (typeOfGeometry === 'oblique') { + // Get oblique + const obliqueGeometry = await fetchGeometryOblique( + uri, + path, + shouldSwitchAxis, + ); + dataPlot.geometrie = [...dataPlot.geometrie, ...obliqueGeometry]; + } + } + + if (dataPlot?.geometrie) { + const displayedLegendGroups: string[] = []; + // Show each group in legend + for (const geometry of dataPlot.geometrie) { + if (!displayedLegendGroups.find((lg) => lg === geometry.legendgroup)) { + displayedLegendGroups.push(geometry.legendgroup); + geometry.showlegend = true; + } + } } if (dataPlot.isEditing && dataPlot?.geometrie) { @@ -784,7 +950,7 @@ export const fetchGeometries = async ( } } - // Return dataPlot list with the plot which includes error bands + // Return dataPlot list with all geometries return dataPlot; } catch (error) { console.error('Error getting geometries:', error); From 1778c20127c9b3651badd06e356616320b695267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Thu, 4 Jun 2026 13:03:13 +0200 Subject: [PATCH 06/12] Fix DataGrid type --- frontend/src/renderer/components/plot/Heatmap2D.tsx | 2 +- frontend/src/renderer/utils/grid.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index 94e66841..9dc5ffa4 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -430,7 +430,7 @@ export const Heatmap2D = ({ ? 'heatmap' : itemDataGrid.selectedPlotMode === 'Geometry' ? 'contour' - : plotRef.current.props.data[0].type, + : 'heatmap', contours: { coloring: 'lines', }, diff --git a/frontend/src/renderer/utils/grid.ts b/frontend/src/renderer/utils/grid.ts index 16c8a13e..aed93fea 100644 --- a/frontend/src/renderer/utils/grid.ts +++ b/frontend/src/renderer/utils/grid.ts @@ -44,5 +44,6 @@ export const generateNewGrid = ( y: findNextAvailableY(existingPlots), w: 6, h: 12, + geometrie: [], }; }; From 727ec23e348233f4b33c1320f200ddd57c2f343d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Fiaudrin?= Date: Mon, 8 Jun 2026 13:20:11 +0200 Subject: [PATCH 07/12] Decorrelate contour and geometries --- .../components/grid/GridLayoutPlot.tsx | 5 +- .../renderer/components/grid/HoverButtons.tsx | 68 +------ .../renderer/components/plot/Heatmap2D.tsx | 11 +- .../visualization/DataplotCustomization.tsx | 31 ++- .../CustomizeGeometry.tsx | 181 ++++++++++++++++++ .../customizableElements/index.ts | 1 + frontend/src/renderer/types/plot.ts | 2 +- frontend/src/renderer/utils/plot.ts | 94 +++++---- 8 files changed, 275 insertions(+), 118 deletions(-) create mode 100644 frontend/src/renderer/pages/visualization/customizableElements/CustomizeGeometry.tsx diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 406627b2..f192e84a 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -35,8 +35,7 @@ export const GridLayoutPlot = ({ ); const [widthGrid, setWidthGrid] = useState(Math.floor(data.w * colWidth)); const [is3DView, setIs3DView] = useState( - data?.selectedPlotMode === 'Heatmap' || - data?.selectedPlotMode === 'Geometry' + data?.selectedPlotMode === 'Heatmap' || data?.selectedPlotMode === 'Contour' ? true : false, ); @@ -220,7 +219,7 @@ export const GridLayoutPlot = ({ useEffect(() => { setIs3DView( data?.selectedPlotMode === 'Heatmap' || - data?.selectedPlotMode === 'Geometry' + data?.selectedPlotMode === 'Contour' ? true : false, ); diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 3058226e..157645bc 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -15,19 +15,19 @@ import { IconCheck, IconDatabaseEdit, IconEdit, - IconGeometry, IconEyeEdit, IconTrash, + IconTarget, } from '@tabler/icons-react'; import { useHover } from '@mantine/hooks'; -import { Configuration, CustomizedGridType, DataGridPlot, PlotType } from '../../types'; import { - applyRange, - fetchErrorBandsInConfig, - fetchGeometries, -} from '../../utils'; + Configuration, + CustomizedGridType, + DataGridPlot, + PlotType, +} from '../../types'; +import { applyRange, fetchErrorBandsInConfig } from '../../utils'; import { useIbexStore } from '../../stores'; -import { showNotification } from '@mantine/notifications'; interface HoverButtonsProps { data: DataGridPlot; @@ -88,7 +88,7 @@ export const HoverButtons = React.memo( }[] = [ { value: '1D' }, { value: 'Heatmap', icon: heatmapLogo }, - { value: 'Geometry', icon: }, + { value: 'Contour', icon: }, ]; const updateDisplayErrorBands = useCallback( @@ -179,64 +179,18 @@ export const HoverButtons = React.memo( const updateTypeOfPlot = async (wantedType: PlotType) => { setPlotTypeMenuOpened(false); setForcePlotTypeMenuOpened(false); + setPlotMode(wantedType); const updatedDataPlot: DataGridPlot[] = structuredClone(active.dataPlot); const selectedDataPlot = updatedDataPlot.find( (dataPlot) => dataPlot.i === data.i, ); - // Control the coordinates order to prevent from adding geometry when coordinates are incompatible with geometries - if ( - wantedType === 'Geometry' && - !( - selectedDataPlot.coordinates.length >= 2 && - (selectedDataPlot.coordinates[0].axeIndex === 0 || - selectedDataPlot.coordinates[0].axeIndex === 1) && - (selectedDataPlot.coordinates[1].axeIndex === 0 || - selectedDataPlot.coordinates[1].axeIndex === 1) - ) - ) { - console.warn( - 'Axis are not matching with geometries: the default axis should be restored to get geometries.', - ); - showNotification({ - title: 'Axis are not matching with geometries', - message: 'The default axis should be restored to get geometries.', - color: 'yellow', - }); - return; - } - - const checkedNodeURI = structuredClone(active.checkedNodeURI); - // Update plot type selectedDataPlot.selectedPlotMode = wantedType; - // Handle geometries in contour type - if (wantedType === 'Geometry') { - // Get geometries - const fetchedGeometrie = await fetchGeometries( - selectedDataPlot, - checkedNodeURI, - ); - if (!fetchedGeometrie) { - // Prevent from displaying contour plot when no geometry are available - showNotification({ - title: 'No geometry available', - message: 'This dataset does not contain any geometry to display.', - color: 'yellow', - }); - return; - } - selectedDataPlot.geometrie = fetchedGeometrie.geometrie; - } else { - selectedDataPlot.geometrie = []; - } - setPlotMode(wantedType); - const updatedActive: Configuration = { ...active, dataPlot: updatedDataPlot, - checkedNodeURI: checkedNodeURI, }; updatedConfiguration(updatedActive); }; @@ -322,9 +276,7 @@ export const HoverButtons = React.memo( {modes.map((mode) => ( { - await updateTypeOfPlot(mode.value); - }} + onClick={() => updateTypeOfPlot(mode.value)} leftSection={mode.icon} rightSection={ plotMode === mode.value ? ( diff --git a/frontend/src/renderer/components/plot/Heatmap2D.tsx b/frontend/src/renderer/components/plot/Heatmap2D.tsx index ad345574..b8ed5abc 100644 --- a/frontend/src/renderer/components/plot/Heatmap2D.tsx +++ b/frontend/src/renderer/components/plot/Heatmap2D.tsx @@ -32,6 +32,7 @@ interface Heatmap2DProps { height: number; plotIndex: string; showSliders: boolean; + forcedPlotType?: 'heatmap' | 'contour'; handleUpdateCoordinate?: ( coordinate: Coordinates, valueIndex: number, @@ -44,6 +45,7 @@ export const Heatmap2D = ({ height, plotIndex, showSliders, + forcedPlotType, handleUpdateCoordinate, }: Heatmap2DProps) => { const { active, updatedConfiguration } = useIbexStore(); @@ -324,7 +326,7 @@ export const Heatmap2D = ({ coord.axeIndex === (targetAxis === 'y' ? 1 : 0), ).name } - data={(itemDataGrid.geometrie // In contour plot, allow to transpose only x & y to keep compatibles coordinates with geometries + data={(itemDataGrid.geometrie.length // In contour plot, allow to transpose only x & y to keep compatibles coordinates with geometries ? itemDataGrid.coordinates.filter( (coord) => coord.axeIndex === 0 || coord.axeIndex === 1, @@ -427,10 +429,11 @@ export const Heatmap2D = ({ ref={plotRef} data={[ { - type: - itemDataGrid.selectedPlotMode === 'Heatmap' + type: forcedPlotType + ? forcedPlotType + : itemDataGrid.selectedPlotMode === 'Heatmap' ? 'heatmap' - : itemDataGrid.selectedPlotMode === 'Geometry' + : itemDataGrid.selectedPlotMode === 'Contour' ? 'contour' : 'heatmap', contours: { diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index 9247ce6a..02541835 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -28,8 +28,9 @@ import { CustomizeDataRange, CustomizeSynchronization, CustomizeInterpolation, + CustomizeGeometry, } from './customizableElements'; -import { IconLink } from '@tabler/icons-react'; +import { IconGeometry, IconLink } from '@tabler/icons-react'; import { initPlotColors } from '../../utils'; export const DataplotCustomization = () => { @@ -271,7 +272,8 @@ export const DataplotCustomization = () => { {tabsValue === item.name && ( - {selectedAccordion === 'Heatmap' ? ( + {selectedAccordion === 'Heatmap' || + selectedAccordion === 'Geometry' ? ( { .findIndex((data) => data.name === item.name) .toString()} showSliders={false} + forcedPlotType={ + selectedAccordion === 'Heatmap' + ? 'heatmap' + : 'contour' + } /> ) : ( + ), + icon: ( + + + + ), + disabled: customizedDataGrid.coordinates.length < 2, + tooltip: "This grid can't have geometries", + }, { value: 'Axis range', component: ( diff --git a/frontend/src/renderer/pages/visualization/customizableElements/CustomizeGeometry.tsx b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeGeometry.tsx new file mode 100644 index 00000000..be2bd87d --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeGeometry.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; +import { DataGridPlot } from '../../../types'; +import { MultiSelect, Select, Stack } from '@mantine/core'; +import { useIbexStore } from '../../../stores'; +import { fetchGeometries } from '../../../utils'; +import { showNotification } from '@mantine/notifications'; + +interface CustomizeGeometryProps { + customizedDataGrid: DataGridPlot; + setCustomizedDataGrid: React.Dispatch>; +} +export const CustomizeGeometry = ({ + customizedDataGrid, + setCustomizedDataGrid, +}: CustomizeGeometryProps) => { + const { active } = useIbexStore(); + const [selectedUri, setSelectedUri] = useState(null); + const [geometriesSelectable, setGeometriesSelectable] = useState( + [], + ); + const [selectedGeometries, setSelectedGeometries] = useState(() => { + return [ + ...new Set( + customizedDataGrid.geometrie.map((geo) => { + const fullUri = geo.nodeUris[0]; + + // URI before '#' + const uri = fullUri.split('#')[0]; + + const uriName = active.dataURI.find((u) => u.uri === uri)?.name; + + // Path after '#' + const path = fullUri.split('#')[1]; + + // Remove everything after last '/' + const pathWithoutLastSegment = path.substring( + 0, + path.lastIndexOf('/') + 1, + ); + + // Format geometry path + return `${uriName}#${pathWithoutLastSegment}`; + }), + ), + ]; + }); + + const handleSelectedUri = (value: string) => { + setSelectedUri(value); + // TODO : call BE endpoint to get geometries and fill multiselect data + const rawData = [ + { + geometryNode: 'URI-0#wall/description_2d[:]/limiter/unit[:]/outline/', + parameters: ['r', 'z'], + }, + { + geometryNode: + 'URI-0#pf_active:0/coil[:]/element[:]/geometry/rectangle/', + parameters: ['r', 'z'], + }, + { + geometryNode: 'URI-0#pf_active:0/coil[:]/element[:]/geometry/oblique/', + parameters: ['r', 'z', 'width', 'height'], + }, + ]; + setGeometriesSelectable(rawData?.map((d) => d.geometryNode)); + }; + + /** + * Update geometry list + * @param values - selected paths + */ + const handleSelectGeometrie = async (values: string[]) => { + // Control the coordinates order to prevent from adding geometry when coordinates are incompatible with geometries + if ( + !( + customizedDataGrid.coordinates.length >= 2 && + (customizedDataGrid.coordinates[0].axeIndex === 0 || + customizedDataGrid.coordinates[0].axeIndex === 1) && + (customizedDataGrid.coordinates[1].axeIndex === 0 || + customizedDataGrid.coordinates[1].axeIndex === 1) + ) + ) { + console.warn( + 'Axis are not matching with geometries: the default axis should be restored to get geometries.', + ); + showNotification({ + title: 'Axis are not matching with geometries', + message: 'The default axis should be restored to get geometries.', + color: 'yellow', + }); + return; + } + + const checkedNodeURI = structuredClone(active.checkedNodeURI); + + if (!values.length) { + // Remove all geometries + customizedDataGrid.geometrie = []; + } else { + if (values.length < selectedGeometries.length) { + // Remove deselected geometry + const selectUriToRemove = selectedGeometries.find( + (value) => !values.includes(value), + ); // Get selected geometrie + const uri = active.dataURI.find( + (uri) => uri.name === selectUriToRemove.split('#')[0], + )?.uri; + const pathToRemove = '#' + selectUriToRemove.split('#')[1]; + const fullPathToRemove = uri + pathToRemove; + customizedDataGrid.geometrie = customizedDataGrid.geometrie.filter( + (geoToRemove) => + !fullPathToRemove.includes( + geoToRemove.nodeUris[0].substring( + 0, + geoToRemove.nodeUris[0].lastIndexOf('/') + 1, + ), + ), + ); + } else if (values.length > selectedGeometries.length) { + // Add selected geometry + const added = + '#' + + values + .find((value) => !selectedGeometries.includes(value)) + .split('#')[1]; // Get selected geometrie + + // Get geometries + const fetchedGeometrie = await fetchGeometries( + selectedUri + added, + customizedDataGrid, + checkedNodeURI, + ); + + if (!fetchedGeometrie) { + // Prevent from displaying contour plot when no geometry are available + showNotification({ + title: 'No geometry available', + message: 'This dataset does not contain any geometry to display.', + color: 'yellow', + }); + return; + } + + customizedDataGrid.geometrie = fetchedGeometrie.geometrie; + } + } + setCustomizedDataGrid({ + ...customizedDataGrid, + }); + + setSelectedGeometries(...[values]); + }; + + return ( + +