diff --git a/package-lock.json b/package-lock.json index 2c371a777e..a12ff8e52d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", "@mui/material": "^5.16.14", + "@mui/x-charts": "^7.25.0", "@mui/x-tree-view": "^6.17.0", "@powsybl/network-viewer": "1.7.0", "@reduxjs/toolkit": "^2.5.1", @@ -4823,6 +4824,105 @@ } } }, + "node_modules/@mui/x-charts": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.27.0.tgz", + "integrity": "sha512-EIT5zbClc8n14qBvCD7jYSVI4jWAWajY7g8gznf5rggCJuv08IHfmi23q6afax73q6yTAi30qeUmcqttqXV4DQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-charts-vendor": "7.20.0", + "@mui/x-internals": "7.26.0", + "@react-spring/rafz": "^9.7.5", + "@react-spring/web": "^9.7.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", + "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "@types/d3-time": "^3.0.3", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-charts-vendor/node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mui/x-charts-vendor/node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-tree-view": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-6.17.0.tgz", @@ -5258,6 +5358,78 @@ "react-querybuilder": "8.2.0" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", @@ -6208,6 +6380,12 @@ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, "node_modules/@types/d3-drag": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", @@ -6226,12 +6404,42 @@ "@types/d3-color": "*" } }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, "node_modules/@types/d3-selection": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, "node_modules/@types/d3-transition": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", @@ -9147,6 +9355,18 @@ "node": ">=12" } }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -9642,6 +9862,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", diff --git a/package.json b/package.json index 3865297e4a..925f723f74 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", "@mui/material": "^5.16.14", + "@mui/x-charts": "^7.25.0", "@mui/x-tree-view": "^6.17.0", "@powsybl/network-viewer": "1.7.0", "@reduxjs/toolkit": "^2.5.1", diff --git a/src/components/dialogs/limits/limits-side-pane.tsx b/src/components/dialogs/limits/limits-side-pane.tsx index 45a0063b45..1e1701d8d1 100644 --- a/src/components/dialogs/limits/limits-side-pane.tsx +++ b/src/components/dialogs/limits/limits-side-pane.tsx @@ -24,6 +24,7 @@ import { TemporaryLimit } from '../../../services/network-modification-types'; import DndTable from '../../utils/dnd-table/dnd-table'; import TemporaryLimitsTable from './temporary-limits-table'; import { CurrentTreeNode } from '../../../redux/reducer'; +import LimitsChart from './limitsChart'; export interface LimitsSidePaneProps { limitsGroupFormName: string; @@ -198,6 +199,9 @@ export function LimitsSidePane({ return ( + + + {permanentCurrentLimitField} diff --git a/src/components/dialogs/limits/limitsChart.tsx b/src/components/dialogs/limits/limitsChart.tsx new file mode 100644 index 0000000000..652644d276 --- /dev/null +++ b/src/components/dialogs/limits/limitsChart.tsx @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { BarChart } from '@mui/x-charts/BarChart'; +import { useCallback, useMemo } from 'react'; +import { useWatch } from 'react-hook-form'; +import { useIntl } from 'react-intl'; +import { CurrentLimits, Limit } from '../../../services/network-modification-types'; +import { BarSeriesType } from '@mui/x-charts/models/seriesType/bar'; +import { AxisValueFormatterContext } from '@mui/x-charts/models/axis'; + +export interface LimitsGraphProps { + limitsGroupFormName: string; +} +const colorPermanentLimit = '#58d058'; +const colors: string[] = ['#ffc019', '#e47400', '#cc5500', '#ff5757', '#ff0000']; +const colorForbidden: string = '#b10303'; + +interface Ticks { + label: string; + position: number | null; + incoherent: boolean; +} + +const formatTempo = (tempo: number | null) => { + if (!tempo) { + return ''; + } + const min = Math.floor(tempo / 60); + const sec = tempo % 60; + if (min > 0 && sec === 0) { + return `${min}min`; + } + if (min === 0 && sec > 0) { + return `${sec}s`; + } + return `${min}min${sec}s`; +}; + +export default function LimitsChart({ limitsGroupFormName }: Readonly) { + const currentLimits: CurrentLimits = useWatch({ name: `${limitsGroupFormName}` }); + const intl = useIntl(); + + const isIncoherent = useCallback( + (maxValuePermanentLimit: number, item: Limit, previousItem?: Limit) => { + // Incoherent cases : + // threshold without tempo that is not permanent limit when permanent limit exists + // threshold with biggest tempo and biggest value than the previous threshold + // more than one threshold without value + const isPermanentLimit = + (item.name === intl.formatMessage({ id: 'IST' }) && currentLimits.permanentLimit) || + (!currentLimits.permanentLimit && !item.acceptableDuration); + + const permanentLimitValue = currentLimits.permanentLimit + ? currentLimits.permanentLimit + : maxValuePermanentLimit; + + const itemTempoGreaterThanPrevious: boolean = + (item?.acceptableDuration && + previousItem?.acceptableDuration && + item.acceptableDuration > previousItem.acceptableDuration) || + false; + + const atLeastTwoItemsWithNotempo: boolean = + (previousItem?.acceptableDuration && !item.acceptableDuration) || false; + + return ( + (!item.acceptableDuration && currentLimits.permanentLimit && !isPermanentLimit) || + (!isPermanentLimit && item.value && item.value < permanentLimitValue) || + itemTempoGreaterThanPrevious || + atLeastTwoItemsWithNotempo || + false + ); + }, + [currentLimits, intl] + ); + + const { series, ticks } = useMemo(() => { + const thresholds: Limit[] = []; + let noValueThresholdFound = false; + let maxValuePermanentLimit: number = 0; + + if (currentLimits?.permanentLimit) { + thresholds.push({ + name: intl.formatMessage({ id: 'IST' }), + value: currentLimits.permanentLimit, + acceptableDuration: null, + }); + maxValuePermanentLimit = currentLimits.permanentLimit; + } + + if (currentLimits?.temporaryLimits) { + currentLimits.temporaryLimits + .filter((field) => field.name && (field.acceptableDuration || field.value)) + .forEach((field) => { + if (!field.value) { + noValueThresholdFound = true; + } + if (!field.acceptableDuration && field.value) { + maxValuePermanentLimit = Math.max(maxValuePermanentLimit, field.value); + } + thresholds.push({ + name: field.name, + value: field.value, + acceptableDuration: field.acceptableDuration, + }); + }); + } + + // Sort by value, if no value put at the end + thresholds.sort((a, b) => { + if (a.value && !b.value) { + return -1; + } + if (!a.value && !b.value && a.acceptableDuration && b.acceptableDuration) { + return a.acceptableDuration - b.acceptableDuration; + } + if (!a.value && b.value) { + return 1; + } + if (a.value && b.value) { + return a.value - b.value; + } + return 0; + }); + + const maxValue = Math.max(...thresholds.map((item) => item.value ?? 0)); + let colorIndex = 0; + let previousSum = 0; + + return thresholds.reduce<{ series: BarSeriesType[]; ticks: Ticks[] }>( + (acc, item, index) => { + const isPermanentLimit = + (item.name === intl.formatMessage({ id: 'IST' }) && currentLimits.permanentLimit) || + (!currentLimits.permanentLimit && + !item.acceptableDuration && + item.value === maxValuePermanentLimit); + const difference = item.value ? item.value - previousSum : undefined; + + const color = + isPermanentLimit || + !item.acceptableDuration || + (item.acceptableDuration && item.value && maxValuePermanentLimit >= item.value) + ? colorPermanentLimit + : colors?.[colorIndex] ?? colors[colors.length - 1]; + + const incoherent = isIncoherent( + maxValuePermanentLimit, + item, + index > 0 ? thresholds[index - 1] : undefined + ); + + if (item.value && item.value >= maxValuePermanentLimit) { + previousSum = item.value; + } + if (item.value && item.value > maxValuePermanentLimit) { + colorIndex++; + } + + const updatedSeries: BarSeriesType[] = + (item.acceptableDuration && !item.value) || (item.value && item.value >= maxValuePermanentLimit) + ? [ + ...acc.series, + { + type: 'bar', + label: + isPermanentLimit || item.value === maxValuePermanentLimit + ? intl.formatMessage({ id: 'unlimited' }) + : formatTempo(item.acceptableDuration), + data: [difference ?? maxValue * 0.15], + color: item.value ? color : colorForbidden, + stack: 'total', + }, + ] + : [...acc.series]; + const updatedTicks: Ticks[] = [ + ...acc.ticks, + { position: item.value, label: item.name, incoherent: incoherent }, + ]; + + if ( + index === thresholds.length - 1 && + updatedTicks[updatedTicks.length - 1].position && + !noValueThresholdFound + ) { + updatedSeries.push({ + type: 'bar', + label: intl.formatMessage({ id: 'forbidden' }), + data: [(updatedTicks?.[updatedTicks?.length - 1]?.position ?? 0.0) * 0.15], + color: colorForbidden, + stack: 'total', + }); + } + return { + series: updatedSeries, + ticks: updatedTicks, + }; + }, + { series: [], ticks: [] } + ); + }, [currentLimits, intl, isIncoherent]); + + const config = { + id: 'topAxis', + valueFormatter: (value: number, context: AxisValueFormatterContext) => + ticks.find((item: Ticks) => item.position === value)?.label, + }; + + return ( + 0 + ? series + : [{ label: intl.formatMessage({ id: 'unlimited' }), data: [100], color: colorPermanentLimit }] + } + layout="horizontal" + leftAxis={null} + bottomAxis={{ + tickInterval: [...ticks.map((item) => item.position)], + disableLine: true, + tickLabelStyle: { fontSize: 10 }, + position: 'bottom', + }} + topAxis={{ + tickInterval: [...ticks.map((item) => item.position)], + tickLabelStyle: { fontSize: 10 }, + disableLine: true, + position: 'top', + ...config, + }} + sx={{ pointerEvents: 'none' }} + /> + ); +} diff --git a/src/services/network-modification-types.ts b/src/services/network-modification-types.ts index 2d502c0922..14b32243f2 100644 --- a/src/services/network-modification-types.ts +++ b/src/services/network-modification-types.ts @@ -257,12 +257,14 @@ export interface OperationalLimitsGroup { currentLimits: CurrentLimits; } -export interface TemporaryLimit { - value: number | null; +export interface Limit { + name: string; acceptableDuration: number | null; + value: number | null; +} +export interface TemporaryLimit extends Limit { modificationType: string | null; selected: boolean; - name: string; } export interface CurrentLimits { diff --git a/src/translations/en.json b/src/translations/en.json index 22f826ca28..f8d78ecfef 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1487,6 +1487,10 @@ "rootNetworkDirectoryFetchingError": "An error occurred while fetching root directory", "rootNetworkNotFound": "No root network was found for the study with ID \"{studyUuid}\"", "createRootNetworksError": "An error occurred while creating the root network", + + "unlimited": "Unlimited", + "forbidden": "Forbidden", + "StateEstimationParametersGeneralTabLabel": "General", "StateEstimationParametersWeightsTabLabel": "Weights", "StateEstimationParametersQualityTabLabel": "Thresholds", diff --git a/src/translations/fr.json b/src/translations/fr.json index b0bcadf15d..d5d53e123f 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -1484,6 +1484,10 @@ "rootNetworkDirectoryFetchingError": "Une erreur est survenue lors de la lecture du dossier du réseau", "rootNetworkNotFound": "Aucun réseau racine n'a été trouvé pour l'étude avec l'identifiant \"{studyUuid}\"", "createRootNetworksError": "Une erreur est survenue lors de la création du réseau racine", + + "unlimited": "Illimité", + "forbidden": "Interdit", + "createRootNetworksError": "An error occurred while creating the root network", "StateEstimationParametersGeneralTabLabel": "Général", "StateEstimationParametersWeightsTabLabel": "Pondération",