diff --git a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx index 5f997432..d4357e75 100644 --- a/frontend/src/renderer/components/grid/GridLayoutPlot.tsx +++ b/frontend/src/renderer/components/grid/GridLayoutPlot.tsx @@ -7,6 +7,7 @@ import { DataGridPlot, DataPlotly, GridLayoutPlotProps, + NodeInfoTypeEnum, URITreeNodeData, } from '../../../renderer/types'; import { Center, Container, Text } from '@mantine/core'; @@ -35,7 +36,9 @@ 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 === 'Contour' + ? true + : false, ); const [active3DTab, setActive3DTab] = useState('0'); const [metadataTabsValue, setMetadataTabsValue] = useState( @@ -146,8 +149,8 @@ export const GridLayoutPlot = ({ return { ...plotItem, - x: newXData, - y: newYData, + x: [...newXData], + y: [...newYData], customdata: customdata, error_bands: updated_error_bands, nodeUri: updatedNodeUri, @@ -156,8 +159,8 @@ export const GridLayoutPlot = ({ } else { return { ...plotItem, - x: newXData, - y: newYData, + x: [...newXData], + y: [...newYData], nodeUri: updatedNodeUri, path: updatedPath, }; @@ -180,6 +183,8 @@ export const GridLayoutPlot = ({ }; useEffect(() => { + let forceToDisplayMetadata = false; + // Rule to force to show metadata when y data is of type string let isYDataString = false; for (const plot of data.plot) { @@ -190,7 +195,15 @@ export const GridLayoutPlot = ({ } } } - setShouldDisplayMetadata(isYDataString); + + // Rule to force to show metadata when y data is a geometry + let isGeometry = false; + if (data.is_geometry_node === true) { + isGeometry = true; + } + + forceToDisplayMetadata = isYDataString || isGeometry; + setShouldDisplayMetadata(forceToDisplayMetadata); }, [data.plot.length]); /** @@ -215,7 +228,12 @@ export const GridLayoutPlot = ({ }, [data.plot]); useEffect(() => { - setIs3DView(data?.selectedPlotMode === 'Heatmap' ? true : false); + setIs3DView( + data?.selectedPlotMode === 'Heatmap' || + data?.selectedPlotMode === 'Contour' + ? true + : false, + ); }, [data.selectedPlotMode]); /** @@ -281,6 +299,7 @@ export const GridLayoutPlot = ({ uri: normalizeIndices(item.nodeUri), name: item.labelUri, type: findPlot.dataType, + is_geometry_node: findPlot.is_geometry_node, })) : []; @@ -295,6 +314,7 @@ export const GridLayoutPlot = ({ name: plot.labelUri, uri: normalizeIndices(error_band.path), type: findPlot.dataType, + is_geometry_node: findPlot.is_geometry_node, }; const exists = checkedNodeURI.some( (node) => @@ -307,6 +327,28 @@ export const GridLayoutPlot = ({ } } } + + if (findPlot?.geometries) { + // Check geometries in tree + for (const geometry of findPlot.geometries) { + for (const uriOfGeo of geometry.nodeUris) { + const newCheckedNode = { + name: findPlot.plot[0].labelUri, + uri: normalizeIndices(uriOfGeo), + type: NodeInfoTypeEnum.FLOAT, + is_geometry_node: true, + } as URITreeNodeData; + const exists = checkedNodeURI.some( + (node) => + node.name === newCheckedNode.name && + node.uri === newCheckedNode.uri, + ); + if (!exists) { + checkedNodeURI.push(newCheckedNode); + } + } + } + } } const updatedActive: Configuration = { @@ -376,6 +418,7 @@ export const GridLayoutPlot = ({ return ( index.toString() === active3DTab && ( ( 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 = ( - + @@ -72,6 +82,15 @@ export const HoverButtons = React.memo( ); + const modes: { + value: PlotType; + icon?: React.ReactNode; + }[] = [ + { value: '1D' }, + { value: 'Heatmap', icon: heatmapLogo }, + { value: 'Contour', icon: }, + ]; + const updateDisplayErrorBands = useCallback( (newValue: boolean) => { const updatedActive = structuredClone(active) as Configuration; @@ -157,25 +176,17 @@ export const HoverButtons = React.memo( updateErrorBands(); }, [data.displayErrorBand]); - const updateSelectedPlotMode = ( - is3DView: boolean, - active: Configuration, - ) => { + 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, ); - 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 = wantedType; const updatedActive: Configuration = { ...active, @@ -223,7 +234,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 257bbd2b..a80ed3fd 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(); @@ -80,6 +82,12 @@ export const Heatmap2D = ({ modebar: { orientation: 'v', }, + legend: { + x: 1.3, + y: 1, + groupclick: 'togglegroup', + tracegroupgap: 0, + }, }); // Custom hook used for trigger some useEffects to update the layout usePlotLayout({ @@ -318,9 +326,13 @@ export const Heatmap2D = ({ coord.axeIndex === (targetAxis === 'y' ? 1 : 0), ).name } - data={itemDataGrid.coordinates.map( - (coord: Coordinates) => coord.name, - )} + data={(itemDataGrid.geometries.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, + ) + : itemDataGrid.coordinates + ).map((coord: Coordinates) => coord.name)} w={`${width * 0.2}px`} onChange={(value) => value && @@ -417,7 +429,16 @@ export const Heatmap2D = ({ ref={plotRef} data={[ { - type: 'heatmap', + type: forcedPlotType + ? forcedPlotType + : itemDataGrid.selectedPlotMode === 'Heatmap' + ? 'heatmap' + : itemDataGrid.selectedPlotMode === 'Contour' + ? 'contour' + : 'heatmap', + contours: { + coloring: 'lines', + }, colorscale: itemDataGrid.plot[parseInt(plotIndex)]?.customPreferences ?.colorscale || 'Viridis', @@ -433,10 +454,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?.geometries ?? []), ]} config={{ autosizable: false, diff --git a/frontend/src/renderer/components/tree/TreeLibrary.tsx b/frontend/src/renderer/components/tree/TreeLibrary.tsx index b73af922..30da8b43 100644 --- a/frontend/src/renderer/components/tree/TreeLibrary.tsx +++ b/frontend/src/renderer/components/tree/TreeLibrary.tsx @@ -189,7 +189,11 @@ function NodeIcon({ if ( shouldDisableTree || node.label.toString().endsWith('_error_lower') || - node.label.toString().endsWith('_error_upper') + node.label.toString().endsWith('_error_upper') || + (node.is_geometry_node === true && + checkedNodes.length && + checkedNodes[0].is_geometry_node !== true && + checkedNodes[0]?.type !== 'STR') ) { return; } @@ -232,6 +236,7 @@ function NodeIcon({ name: uriLabel, uri: node.value, type: node.type, + is_geometry_node: node.is_geometry_node, }; checkedNodes.push(newCheckedNode); } @@ -273,7 +278,11 @@ function NodeIcon({ cursor: shouldDisableTree || node.label.toString().endsWith('_error_lower') || - node.label.toString().endsWith('_error_upper') + node.label.toString().endsWith('_error_upper') || + (node.is_geometry_node === true && + checkedNodes.length && + checkedNodes[0].is_geometry_node !== true && + checkedNodes[0]?.type !== 'STR') ? 'not-allowed' : 'pointer', }} @@ -293,7 +302,11 @@ function NodeIcon({ disabled={ shouldDisableTree || node.label.toString().endsWith('_error_lower') || - node.label.toString().endsWith('_error_upper') + node.label.toString().endsWith('_error_upper') || + (node.is_geometry_node === true && + checkedNodes.length && + checkedNodes[0].is_geometry_node !== true && + checkedNodes[0]?.type !== 'STR') } /> {IconComponent} diff --git a/frontend/src/renderer/layout/MainLayout.tsx b/frontend/src/renderer/layout/MainLayout.tsx index 19d80615..98e1e35c 100644 --- a/frontend/src/renderer/layout/MainLayout.tsx +++ b/frontend/src/renderer/layout/MainLayout.tsx @@ -20,6 +20,7 @@ import { plotNodeUriLoaded, updateCustomDataTree, readIbexConfig, + formatGeometriesToSave, } from '../utils'; import { VisualizationURIModal } from '../pages'; import { showNotification } from '@mantine/notifications'; @@ -106,6 +107,11 @@ export function MainLayout() { downsampled_method: dataGrid?.downsampled_method, downsampled_size: dataGrid?.downsampled_size, interpolated_method: dataGrid.interpolated_method, + is_geometry_node: dataGrid.is_geometry_node, + geometries: formatGeometriesToSave( + dataGrid?.geometries, + active.dataURI, + ), xAxisData: dataGrid.xAxisData, yAxisData: dataGrid.yAxisData, y2AxisData: dataGrid?.y2AxisData, @@ -236,7 +242,10 @@ export function MainLayout() { dataURI: newIbexState.dataURI, customDataTree: updateCustomDataTree([], newIbexState.dataURI), checkedNodeURI: [], - dataPlot: await plotNodeUriLoaded(newListDataGridPlot), + dataPlot: await plotNodeUriLoaded( + newListDataGridPlot, + newIbexState.dataURI, + ), saved: true, path: path, }; 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/VisualizationTree.tsx b/frontend/src/renderer/pages/visualization/VisualizationTree.tsx index 0e18b5a2..1ab7ca1c 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationTree.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationTree.tsx @@ -228,6 +228,7 @@ export const VisualizationTree = ({ children: [], seeErrorBars: showErrorBars, uriLabel: dataUri.name, + is_geometry_node: false, }); } } @@ -275,11 +276,7 @@ export const VisualizationTree = ({ (item) => item.uri === dataUri.uri, ).data; - const dataTree = buildTree( - customDataTreeUri, - dataUri, - searchResults.paths, - ); + const dataTree = buildTree(customDataTreeUri, dataUri, searchResults); const updatedActive: Configuration = { ...active, @@ -366,6 +363,7 @@ export const VisualizationTree = ({ type: child.type, children: [] as CustomTreeNodeData[], uriLabel: uriSelectedRef.current.name, + is_geometry_node: child.is_geometry_node, }; }, ); diff --git a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx index 3d6a0a15..21c964aa 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx @@ -334,7 +334,10 @@ export const VisualizationURIModal = ({ // Get new data from BE const newListDataGridPlot = formatConfigBeforeLoadingURIs(active); - const wantedDataPlot = await plotNodeUriLoaded(newListDataGridPlot); + const wantedDataPlot = await plotNodeUriLoaded( + newListDataGridPlot, + active.dataURI, + ); active.dataPlot = wantedDataPlot; const updatedActive: Configuration = { @@ -429,7 +432,6 @@ export const VisualizationURIModal = ({ * */ async function fetchDataIDSFromURI() { - // ? function write/paste if (!formURI.values.uri) { console.error('URI is empty.'); formURI.setFieldError('uri', 'Please provide a valid URI'); 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..370fc1d6 --- /dev/null +++ b/frontend/src/renderer/pages/visualization/customizableElements/CustomizeGeometry.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react'; +import { DataGridPlot, GeometryInfos } from '../../../types'; +import { MultiSelect, Select, Stack } from '@mantine/core'; +import { useIbexStore } from '../../../stores'; +import { fetchGeometries, formatGeometriesToSave } 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 [geometriesAvailable, setGeometriesAvailable] = useState< + GeometryInfos[] + >([]); + const [geometriesSelectable, setGeometriesSelectable] = useState( + [], + ); + const [selectedGeometries, setSelectedGeometries] = useState(() => { + return [ + ...new Set( + formatGeometriesToSave( + customizedDataGrid.geometries, + active.dataURI, + ).map((geo) => geo.geometryUri), + ), + ]; + }); + + const handleSelectedUri = (value: string) => { + setSelectedUri(value); + // TODO : call BE endpoint to get geometries and fill multiselect data + const rawData: GeometryInfos[] = [ + { + geometryUri: 'URI-0#wall:0/description_2d[:]/limiter/unit[:]/outline/', + parameters: ['r', 'z'], + }, + { + geometryUri: 'URI-0#pf_active:0/coil[:]/element[:]/geometry/', + parameters: [ + 'rectangle/r', + 'rectangle/z', + 'rectangle/width', + 'rectangle/height', + + 'oblique/r', + 'oblique/z', + 'oblique/length_alpha', + 'oblique/length_beta', + 'oblique/alpha', + 'oblique/beta', + ], + }, + ]; + setGeometriesAvailable(rawData); + setGeometriesSelectable(rawData?.map((d) => d.geometryUri)); + }; + + /** + * 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.geometries = []; + } else { + if (values.length < selectedGeometries.length) { + // Remove selected geometry + const selectUriToRemove = selectedGeometries.find( + (value) => !values.includes(value), + ); // Get selected geometries + const uri = active.dataURI.find( + (uri) => uri.name === selectUriToRemove.split('#')[0], + )?.uri; + const pathToRemove = '#' + selectUriToRemove.split('#')[1]; + const fullPathToRemove = uri + pathToRemove; + customizedDataGrid.geometries = customizedDataGrid.geometries.filter( + (geoToRemove) => !geoToRemove.geometryUri.includes(fullPathToRemove), + ); + } else if (values.length > selectedGeometries.length) { + // Add selected geometry + const added = + '#' + + values + .find((value) => !selectedGeometries.includes(value)) + .split('#')[1]; // Get selected geometries + + const wantedGeometryInfos: GeometryInfos = geometriesAvailable.find( + (geo) => geo.geometryUri.includes(added), + ); + + // Get geometries + const fetchedGeometrie = await fetchGeometries( + selectedUri + added, + customizedDataGrid, + checkedNodeURI, + wantedGeometryInfos, + ); + + if (!fetchedGeometrie) { + // Prevent from displaying contour plot when no geometry are available + showNotification({ + title: 'No geometry available', + message: 'No geometry to display.', + color: 'yellow', + }); + return; + } + + customizedDataGrid.geometries = fetchedGeometrie.geometries; + } + } + setCustomizedDataGrid({ + ...customizedDataGrid, + }); + + setSelectedGeometries(...[values]); + }; + + return ( + +