diff --git a/package-lock.json b/package-lock.json index e0a3961186631738dbb50a3ce01b8100f2bcde32..7493aec7b1496c5c64df9f62a50e9d20cc8660e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "sanitize-filename": "^1.6.3", "scroll-into-view-if-needed": "^3.0.10", "slug": "^10.0.0", - "svelte": "^5.9.0", + "svelte": "^5.17.3", "svelte-check": "^4.0.1", "svelte-dnd-action": "^0.9.8", "svelte-floating-ui": "^1.5.3", @@ -3116,9 +3116,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.79", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.79.tgz", - "integrity": "sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==", + "version": "1.5.80", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", + "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", "dev": true, "license": "ISC" }, @@ -3490,9 +3490,9 @@ } }, "node_modules/esrap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz", - "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.2.tgz", + "integrity": "sha512-FhVlJzvTw7ZLxYZ7RyHwQCFE64dkkpzGNNnphaGCLwjqGk1SQcqzbgdx9FowPCktx6NOSHkzvcZ3vsvdH54YXA==", "dev": true, "license": "MIT", "dependencies": { @@ -6527,9 +6527,9 @@ } }, "node_modules/svelte": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.17.1.tgz", - "integrity": "sha512-HitqD0XhU9OEytPuux/XYzxle4+7D8+fIb1tHbwMzOtBzDZZO+ESEuwMbahJ/3JoklfmRPB/Gzp74L87Qrxfpw==", + "version": "5.17.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.17.3.tgz", + "integrity": "sha512-eLgtpR2JiTgeuNQRCDcLx35Z7Lu9Qe09GPOz+gvtR9nmIZu5xgFd6oFiLGQlxLD0/u7xVyF5AUkjDVyFHe6Bvw==", "dev": true, "license": "MIT", "dependencies": { @@ -7017,9 +7017,9 @@ } }, "node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.0.tgz", + "integrity": "sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7237,9 +7237,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.4.tgz", - "integrity": "sha512-IzL6VtTTYcAhA/oghbFJ1Dkmqev+FpQWnCBaKq/gUluLxliWvO8DPFWfIviRmYbtaavtSQe4WBL++rFjdcGWEg==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", diff --git a/package.json b/package.json index e6150509714571c6cdc8082813ac929fc5188e09..ad53dac59d98dea5e669c3eb070de4aa0025d28b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "sanitize-filename": "^1.6.3", "scroll-into-view-if-needed": "^3.0.10", "slug": "^10.0.0", - "svelte": "^5.9.0", + "svelte": "^5.17.3", "svelte-check": "^4.0.1", "svelte-dnd-action": "^0.9.8", "svelte-floating-ui": "^1.5.3", diff --git a/src/lib/components/WaterfallView.svelte b/src/lib/components/WaterfallView.svelte index 174c43f1c11fb203efa0bd9d5858f4e857fe7e83..8155632d5d63897d1eb0266ef41cf5277c2f56ed 100644 --- a/src/lib/components/WaterfallView.svelte +++ b/src/lib/components/WaterfallView.svelte @@ -186,11 +186,19 @@ } function removeSituationSlider() { - situation = { ...situation } - delete situation.slider + if (shared.savedSituation !== undefined) { + situation = shared.savedSituation + delete shared.savedSituation + if (shared.savedSituationIndex !== undefined) + shared.testCases[shared.savedSituationIndex] = situation + delete shared.savedSituationIndex + } } function requestAxesCalculation() { + shared.savedSituation = situation + situation = structuredClone($state.snapshot(situation)) + shared.testCases[situationIndex] = situation if ( situation.sliders?.length === undefined || situation.sliders?.length <= 0 diff --git a/src/lib/components/piece_of_cake/DragSelect.svelte b/src/lib/components/piece_of_cake/DragSelect.svelte index 61bc573c367a36f03d457ce5dd2839c35dad6f72..577c633364487c06a3b64d8b13125dfb2cd7ba08 100644 --- a/src/lib/components/piece_of_cake/DragSelect.svelte +++ b/src/lib/components/piece_of_cake/DragSelect.svelte @@ -1,16 +1,14 @@ <script lang="ts"> - import { createEventDispatcher } from "svelte" - import type { CurveModel } from "$lib/components/piece_of_cake/model" + import type { GraphDomain } from "$lib/components/piece_of_cake/types" interface Props { - modelGroups: CurveModel[][] children?: import("svelte").Snippet<[any]> + modelGroups: CurveModel[][] + onZoom?: (domain: GraphDomain) => void } - let { modelGroups, children }: Props = $props() - - const dispatch = createEventDispatcher() + let { children, modelGroups, onZoom }: Props = $props() let clientWidth: number = $state() let clientHeight: number = $state() @@ -60,20 +58,20 @@ Math.round((start[0] / clientWidth) * xRange) + xScale.domain()[0] const xEnd = Math.round((end[0] / clientWidth) * xRange) + xScale.domain()[0] - const x = !xInverted ? [xStart, xEnd] : [xEnd, xStart] + const x: AxisDomain = !xInverted ? [xStart, xEnd] : [xEnd, xStart] const yRange = yScale.domain()[1] - yScale.domain()[0] const yStart = Math.round((1 - start[1] / clientHeight) * yRange) + yScale.domain()[0] const yEnd = Math.round((1 - end[1] / clientHeight) * yRange) + yScale.domain()[0] - const y = !yInverted ? [yEnd, yStart] : [yStart, yEnd] + const y: AxisDomain = !yInverted ? [yEnd, yStart] : [yStart, yEnd] - const domain = { + const domain: GraphDomain = { x, y, } - dispatch("zoom", domain) + onZoom?.(domain) start = undefined end = undefined } diff --git a/src/lib/components/piece_of_cake/types.ts b/src/lib/components/piece_of_cake/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e31f09dc21b57b1f36939d6b20b23a6ca991919 --- /dev/null +++ b/src/lib/components/piece_of_cake/types.ts @@ -0,0 +1,6 @@ +export type AxisDomain = [number, number] // [min, max] + +export interface GraphDomain { + x: AxisDomain + y: AxisDomain +} diff --git a/src/lib/components/test_cases/TestCaseGraph.svelte b/src/lib/components/test_cases/TestCaseGraph.svelte index 08f9f6c2b73c205e7898b9f83141d3524ad8eb64..16367f96a22eb639a5bc9ab6a9141a9bd9c8d815 100644 --- a/src/lib/components/test_cases/TestCaseGraph.svelte +++ b/src/lib/components/test_cases/TestCaseGraph.svelte @@ -30,6 +30,7 @@ import PieceOfCake from "$lib/components/piece_of_cake/PieceOfCake.svelte" import SharedTooltip from "$lib/components/piece_of_cake/SharedTooltip.svelte" import Svg from "$lib/components/piece_of_cake/Svg.svelte" + import type { GraphDomain } from "$lib/components/piece_of_cake/types" import TestCaseGraphXlsxExport from "$lib/components/test_cases/TestCaseGraphXlsxExport.svelte" import Tooltip from "$lib/components/Tooltip.svelte" import ValueChangeGraph from "$lib/components/ValueChangeGraph.svelte" @@ -97,6 +98,11 @@ year, }: Props = $props() + let domain: GraphDomain = $state({ + x: [situation.slider?.min ?? 0, situation.slider?.max ?? 100000], + y: [situation.slider?.yMin ?? 0, situation.slider?.yMax ?? 100000], + }) + const familyEntity = entityByKey[familyEntityKey] const formatCurrency = valueFormatter(0, "currency-EUR", false) const formatLongOrdinalSup = (n: number) => { @@ -113,6 +119,7 @@ ["other", "ème"], ["one", "er"], ]) + let maxVariableValue: number | undefined = $state() const ordinalPluralRules = new Intl.PluralRules("fr-FR", { type: "ordinal" }) const personEntity = entityByKey[personEntityKey] const shortOrdinalSuffixes = new Map([ @@ -122,7 +129,6 @@ let svgPadding = { bottom: 20, left: 8, right: 20, top: 20 } let grapheExplanationOpen = $state(false) - let maxVariableValue: number | undefined = $state() let variableCustomizations = $state(variableCustomizationsBase) let variableValues: VariableGraph[][] = $state([]) let variableGroups: { @@ -133,451 +139,82 @@ variables: VariableGraph[] }[] = $state() - function calculateDeciles(decilesNiveauDeVie, niveauDeVie) { - if (niveauDeVie === undefined || niveauDeVie?.length <= 1) { - return [] - } + let visibleDecompositionsGraph: VisibleDecompositionForGraph[] | undefined = + $state(undefined) - const niveauDeVieStart = niveauDeVie[0] - const niveauDeVieEnd = niveauDeVie[niveauDeVie.length - 1] + $effect(() => { + visibleDecompositionsGraph = + situation.slider !== undefined + ? buildVisibleDecompositionsForGraph( + shared.decompositionByName, + entityByKey, + evaluationByName, + situation, + variableSummaryByName, + waterfall, + false, + useRevaluationInsteadOfLaw, + vectorLength, + year, + ) + : undefined + }) - let sumPreviousWidthPercentages = 0 + $effect(() => { + if ( + situation.slider !== undefined && + evaluationByNameArray !== undefined && + visibleDecompositionsGraph !== undefined + ) { + untrack(() => { + const ressourcesBrutes = [ + "remuneration_brute", + "chomage_brut", + "retraite_brute", + ] - return [ - ...decilesNiveauDeVie, - { rate: { value: 10 }, threshold: { value: niveauDeVieEnd } }, - ].reduce( - ( - result: [number, number, number, number, boolean, boolean][], - current: RateBracket, - index, - array: RateBracketAtInstant[], - ) => { - const rate = current.rate.value as number - const start = index > 0 ? array[index - 1].threshold.value + 1 : 0 - const end = parseInt(`${current.threshold.value}`) - if (end - niveauDeVieStart >= 0 && start <= niveauDeVieEnd) { - const nextDecileIndex = niveauDeVie.findIndex((val) => val > end) - const widthPercentage = - (nextDecileIndex === -1 ? 101 : nextDecileIndex) - - 1 - - sumPreviousWidthPercentages - const cutStart = - start < niveauDeVieStart && (domain.x.min > 0 || index > 0) - const cutEnd = niveauDeVieEnd < end || rate === 10 - result.push([rate, end, widthPercentage, cutStart, cutEnd]) - sumPreviousWidthPercentages += widthPercentage - } - return result - }, - [], - ) - } + if (waterfall.name === "brut_to_disponible") { + const decompositions = visibleDecompositionsGraph.filter( + (decomposition) => + !decomposition.trunk || + ressourcesBrutes.includes(decomposition.decomposition.name) || + decomposition.decomposition.name === "revenu_disponible", + ) - function generateValues( - decompositions: VisibleDecompositionForGraph[], - reverse: boolean = false, - ): VariableGraph[] { - if (reverse) { - decompositions.reverse() - } - const values = decompositions.map((decomposition) => { - const open = decomposition.decomposition.open ?? false - const trunk = decomposition.trunk - return { - depth: decomposition.depth, - label: - decomposition.decomposition.short_label ?? - decomposition.decomposition.label, - name: decomposition.decomposition.name, - open, - parent: decomposition.parent, - rows: decomposition.rows.map((row) => ({ - ...row, - delta: (row.delta ?? []).map((val) => (isNaN(val) ? 0 : val)), - ignore: - (open && !trunk) || - variableCustomizations[ - getVariableCustomizationName( - decomposition.decomposition.name, - row.calculationName, - ) - ]?.selected === "false", - })), - trunk, - } - }) - const sums: VariableGraph[] = [] - for (let i = 0; i < values.length; i++) { - const actualArr = sums.filter(({ rows }) => - rows.some((row) => !row.ignore), - ) - const decomposition = values[i] + const versementBrutIndex = decompositions.findIndex((decomposition) => + ressourcesBrutes.includes(decomposition.decomposition.name), + ) + const prestationsIndex = decompositions.findIndex( + (decomposition) => + decomposition.decomposition.name === "prestations_sociales", + ) + const revenuIndex = decompositions.findIndex( + (decomposition) => + decomposition.decomposition.name === "revenu_disponible", + ) - // If we're not doing the sums in reverse, it all works normally. + const prelevementsDecompositions = [ + ...decompositions.slice(0, versementBrutIndex), + ...decompositions + .slice(versementBrutIndex, prestationsIndex) + .filter( + (decomposition) => + ressourcesBrutes.includes(decomposition.decomposition.name) || + getLatestVisibleCalculation(decomposition.rows)?.isNegative, + ), + ] - // If we're calculating in reverse, this means that the negative - // values won't be substracted from the previous ones, but from the - // upcoming ones. This means we have to iterate twice to calculate them. + const complementsDeRessourcesDecompositions = decompositions + .slice(versementBrutIndex, prestationsIndex) + .filter( + (decomposition) => + !ressourcesBrutes.includes(decomposition.decomposition.name) && + !getLatestVisibleCalculation(decomposition.rows)?.isNegative, + ) - if (!reverse) { - sums.push({ - ...decomposition, - rows: decomposition.rows.map((row) => ({ - ...row, - summedValues: - actualArr.length > 0 && !decomposition.trunk - ? getLatestVisibleCalculation( - actualArr[actualArr.length - 1].rows, - )!.summedValues.map( - (value, index) => value + row.delta[index], - ) - : row.delta, - })), - }) - } else { - // In reverse: - if (getLatestVisibleCalculation(decomposition.rows)?.isNegative) { - // If the variable is negative, just push it as is for now, - // we'll iterate over it again later. - sums.push({ - ...decomposition, - rows: decomposition.rows.map((row) => ({ - ...row, - summedValues: row.delta, - })), - }) - } else { - // If the variable is positive, look for negative values that substract from it. - // 1) Add the latest positive value to it: - const latestPositive = actualArr.findLast( - (d) => !getLatestVisibleCalculation(d.rows)?.isNegative, - ) - sums.push({ - ...decomposition, - rows: decomposition.rows.map((row) => ({ - ...row, - summedValues: - latestPositive !== undefined && !decomposition.trunk - ? getLatestVisibleCalculation( - latestPositive.rows, - )!.summedValues.map( - (value, index) => value + row.delta[index], - ) - : row.delta, - })), - }) - // 2) If the previous variable is negative, add it (and others if there are many): - if ( - sums.length > 1 && - getLatestVisibleCalculation(sums[i - 1].rows)?.isNegative - ) { - let currentSummedValues = getLatestVisibleCalculation(sums[i].rows) - let lastPositiveIndex = sums - .slice(0, i) - .findLastIndex( - (d) => !getLatestVisibleCalculation(d.rows)?.isNegative, - ) - if (lastPositiveIndex === -1) { - lastPositiveIndex = 0 - } - for (let j = i - 1; j > lastPositiveIndex; j--) { - sums[j].rows = sums[j].rows.map((row) => ({ - ...row, - summedValues: - latestPositive !== undefined && - !decomposition.trunk && - currentSummedValues !== undefined - ? currentSummedValues.summedValues.map( - (value, index) => value + row.delta[index], - ) - : row.delta, - })) - currentSummedValues = getLatestVisibleCalculation(sums[j].rows) - } - } - } - } - } - if (reverse) { - return sums.reverse() - } else { - return sums - } - } - - function getLatestCalculation<T extends { calculationName: CalculationName }>( - list: T[], - ): T { - for (const calculationName of [ - "amendment", - "bill", - "revaluation", - "law", - ] as CalculationName[]) { - const calculation = list?.find( - (item) => item.calculationName === calculationName, - ) - if (calculation !== undefined) { - return calculation - } - } - } - - function getLatestVisibleCalculation< - T extends { - calculationName: CalculationName - ignore?: boolean - }, - >(list: T[]): T | undefined { - for (const calculationName of [ - "amendment", - "bill", - "revaluation", - "law", - ] as CalculationName[]) { - const calculation = list?.find( - (item) => - item.calculationName === calculationName && (!item.ignore ?? true), - ) - if (calculation !== undefined) { - return calculation - } - } - } - - function getVariableCustomizationName(name: string, calculationName: string) { - return Object.hasOwn(variableCustomizations, `${name}_${calculationName}`) - ? `${name}_${calculationName}` - : name - } - - function getVariableValue( - situation: Situation, - variableName: string, - populationId: string, - ): VariableValue | undefined { - const variable = variableSummaryByName[variableName] - if (variable === undefined) { - return undefined - } - return getSituationVariableValue(situation, variable, populationId, year) - } - - function requestAxesCalculation() { - if ( - situation.sliders?.length === undefined || - situation.sliders?.length <= 0 - ) { - console.error( - "requestAxesCalculation", - "Situation sliders list is undefined", - ) - return - } - - // Get situation - const variable = variableSummaryByName[situation.sliders[0].name] - const slider = situation.sliders.find( - (slider) => - slider.entity === variable.entity && slider.name === variable.name, - ) as ActiveSlider | undefined - - if (slider === undefined) { - console.error("requestAxesCalculation", "Slider is undefined") - return - } - - const updatedMin = domain.x.min ?? slider.min - const updatedMax = domain.x.max ?? slider.max - const updatedStepValue = (updatedMax - updatedMin) / 100 ?? slider.stepValue - - const value = getSituationVariableValue( - situation, - variable, - slider.id, - year, - ) as number - - const vectorIndex = Math.max( - 0, - Math.min(100, Math.round(value / updatedStepValue)), - ) - - // Update situation - setSituationVariableValue( - entityByKey, - situation, - variable, - slider.id, - year, - Math.round(updatedStepValue * vectorIndex), - ) - situation.slider = { - ...slider, - min: updatedMin, - max: updatedMax, - stepValue: updatedStepValue, - vectorIndex: vectorIndex, - } - if (domain.y?.min !== undefined) { - situation.slider.yMin = domain.y?.min - } - if (domain.y?.max !== undefined) { - situation.slider.yMax = domain.y?.max - } - } - - function stackValues(...groups: VariableGraph[][]) { - const newGroups: VariableGraph[][] = [] - for (const group of groups.toReversed()) { - if (newGroups.length === 0) { - newGroups.push(group) - } else { - if ( - !group.some( - ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, - ) - ) { - newGroups.push(group) - } else { - const previousGroup = newGroups.findLast((group) => - group.some( - ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, - ), - ) - if (previousGroup === undefined) { - return groups - } - const previousGroupTopVariable = previousGroup.find( - ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, - ) - if (previousGroupTopVariable === undefined) { - return groups - } - const previousGroupTopVariableLatestVisibleCalculation = - getLatestVisibleCalculation( - previousGroupTopVariable.rows, - ) as VariableGraphRow - newGroups.push( - group.map((value) => ({ - ...value, - rows: value.rows.map((row) => ({ - ...row, - summedValues: row.summedValues.map( - (value, index) => - value + - previousGroupTopVariableLatestVisibleCalculation - .summedValues[index], - ), - })), - })), - ) - } - } - } - return newGroups.toReversed() - } - - function updateDomain({ target }: Event, axis: "x" | "y", minMax: string) { - let { value }: { value: VariableValue } = target as - | HTMLInputElement - | HTMLSelectElement - const numberValue = value.replace(/\D/g, "") - if (numberValue !== "") { - domain[axis] = { - ...domain?.[axis], - [minMax]: Number(numberValue), - } - requestAxesCalculation() - } - } - - function updateAllDomain(newDomain) { - domain.x = { - min: newDomain.x[0], - max: newDomain.x[1], - } - domain.y = { - min: newDomain.y[0], - max: newDomain.y[1], - } - requestAxesCalculation() - } - let visibleDecompositionsGraph: VisibleDecompositionForGraph[] | undefined = - $state(undefined) - $effect(() => { - visibleDecompositionsGraph = - situation.slider !== undefined - ? buildVisibleDecompositionsForGraph( - shared.decompositionByName, - entityByKey, - evaluationByName, - situation, - variableSummaryByName, - waterfall, - false, - useRevaluationInsteadOfLaw, - vectorLength, - year, - ) - : undefined - }) - $effect(() => { - if ( - situation.slider !== undefined && - evaluationByNameArray !== undefined && - visibleDecompositionsGraph !== undefined - ) { - untrack(() => { - const ressourcesBrutes = [ - "remuneration_brute", - "chomage_brut", - "retraite_brute", - ] - - if (waterfall.name === "brut_to_disponible") { - const decompositions = visibleDecompositionsGraph.filter( - (decomposition) => - !decomposition.trunk || - ressourcesBrutes.includes(decomposition.decomposition.name) || - decomposition.decomposition.name === "revenu_disponible", - ) - - const versementBrutIndex = decompositions.findIndex((decomposition) => - ressourcesBrutes.includes(decomposition.decomposition.name), - ) - const prestationsIndex = decompositions.findIndex( - (decomposition) => - decomposition.decomposition.name === "prestations_sociales", - ) - const revenuIndex = decompositions.findIndex( - (decomposition) => - decomposition.decomposition.name === "revenu_disponible", - ) - - const prelevementsDecompositions = [ - ...decompositions.slice(0, versementBrutIndex), - ...decompositions - .slice(versementBrutIndex, prestationsIndex) - .filter( - (decomposition) => - ressourcesBrutes.includes(decomposition.decomposition.name) || - getLatestVisibleCalculation(decomposition.rows)?.isNegative, - ), - ] - - const complementsDeRessourcesDecompositions = decompositions - .slice(versementBrutIndex, prestationsIndex) - .filter( - (decomposition) => - !ressourcesBrutes.includes(decomposition.decomposition.name) && - !getLatestVisibleCalculation(decomposition.rows)?.isNegative, - ) - - const prestationsDecompositions = decompositions.slice( - prestationsIndex, - revenuIndex, + const prestationsDecompositions = decompositions.slice( + prestationsIndex, + revenuIndex, ) const revenuDisponibleDecomposition = decompositions[revenuIndex] @@ -782,17 +419,7 @@ }) } }) - let domain = $state({ - x: { - min: situation.slider?.min, - max: situation.slider?.max, - }, - y: { - min: situation.slider?.yMin ?? 0, - max: situation.slider?.yMax ?? (() => maxVariableValue)() ?? 100000, - }, - }) - updateAllDomain((() => domain)()) + // Note: A reform parameters tree is always more complete than a parameters tree before reform. // And the children of a reform node parameter always contain the children of the node parameter // before reform (albeit with some different value parameters). @@ -892,6 +519,367 @@ ) let smicLatestInstantValueCouple = $derived(smicInstantValueCouplesArray[0]) let smicValue = $derived(smicLatestInstantValueCouple?.[1] as NumberValue) + + function calculateDeciles(decilesNiveauDeVie, niveauDeVie) { + if (niveauDeVie === undefined || niveauDeVie?.length <= 1) { + return [] + } + + const niveauDeVieStart = niveauDeVie[0] + const niveauDeVieEnd = niveauDeVie[niveauDeVie.length - 1] + + let sumPreviousWidthPercentages = 0 + + return [ + ...decilesNiveauDeVie, + { rate: { value: 10 }, threshold: { value: niveauDeVieEnd } }, + ].reduce( + ( + result: [number, number, number, number, boolean, boolean][], + current: RateBracket, + index, + array: RateBracketAtInstant[], + ) => { + const rate = current.rate.value as number + const start = index > 0 ? array[index - 1].threshold.value + 1 : 0 + const end = parseInt(`${current.threshold.value}`) + if (end - niveauDeVieStart >= 0 && start <= niveauDeVieEnd) { + const nextDecileIndex = niveauDeVie.findIndex((val) => val > end) + const widthPercentage = + (nextDecileIndex === -1 ? 101 : nextDecileIndex) - + 1 - + sumPreviousWidthPercentages + const cutStart = + start < niveauDeVieStart && (domain.x[0] > 0 || index > 0) + const cutEnd = niveauDeVieEnd < end || rate === 10 + result.push([rate, end, widthPercentage, cutStart, cutEnd]) + sumPreviousWidthPercentages += widthPercentage + } + return result + }, + [], + ) + } + + function generateValues( + decompositions: VisibleDecompositionForGraph[], + reverse: boolean = false, + ): VariableGraph[] { + if (reverse) { + decompositions.reverse() + } + const values = decompositions.map((decomposition) => { + const open = decomposition.decomposition.open ?? false + const trunk = decomposition.trunk + return { + depth: decomposition.depth, + label: + decomposition.decomposition.short_label ?? + decomposition.decomposition.label, + name: decomposition.decomposition.name, + open, + parent: decomposition.parent, + rows: decomposition.rows.map((row) => ({ + ...row, + delta: (row.delta ?? []).map((val) => (isNaN(val) ? 0 : val)), + ignore: + (open && !trunk) || + variableCustomizations[ + getVariableCustomizationName( + decomposition.decomposition.name, + row.calculationName, + ) + ]?.selected === "false", + })), + trunk, + } + }) + const sums: VariableGraph[] = [] + for (let i = 0; i < values.length; i++) { + const actualArr = sums.filter(({ rows }) => + rows.some((row) => !row.ignore), + ) + const decomposition = values[i] + + // If we're not doing the sums in reverse, it all works normally. + + // If we're calculating in reverse, this means that the negative + // values won't be substracted from the previous ones, but from the + // upcoming ones. This means we have to iterate twice to calculate them. + + if (!reverse) { + sums.push({ + ...decomposition, + rows: decomposition.rows.map((row) => ({ + ...row, + summedValues: + actualArr.length > 0 && !decomposition.trunk + ? getLatestVisibleCalculation( + actualArr[actualArr.length - 1].rows, + )!.summedValues.map( + (value, index) => value + row.delta[index], + ) + : row.delta, + })), + }) + } else { + // In reverse: + if (getLatestVisibleCalculation(decomposition.rows)?.isNegative) { + // If the variable is negative, just push it as is for now, + // we'll iterate over it again later. + sums.push({ + ...decomposition, + rows: decomposition.rows.map((row) => ({ + ...row, + summedValues: row.delta, + })), + }) + } else { + // If the variable is positive, look for negative values that substract from it. + // 1) Add the latest positive value to it: + const latestPositive = actualArr.findLast( + (d) => !getLatestVisibleCalculation(d.rows)?.isNegative, + ) + sums.push({ + ...decomposition, + rows: decomposition.rows.map((row) => ({ + ...row, + summedValues: + latestPositive !== undefined && !decomposition.trunk + ? getLatestVisibleCalculation( + latestPositive.rows, + )!.summedValues.map( + (value, index) => value + row.delta[index], + ) + : row.delta, + })), + }) + // 2) If the previous variable is negative, add it (and others if there are many): + if ( + sums.length > 1 && + getLatestVisibleCalculation(sums[i - 1].rows)?.isNegative + ) { + let currentSummedValues = getLatestVisibleCalculation(sums[i].rows) + let lastPositiveIndex = sums + .slice(0, i) + .findLastIndex( + (d) => !getLatestVisibleCalculation(d.rows)?.isNegative, + ) + if (lastPositiveIndex === -1) { + lastPositiveIndex = 0 + } + for (let j = i - 1; j > lastPositiveIndex; j--) { + sums[j].rows = sums[j].rows.map((row) => ({ + ...row, + summedValues: + latestPositive !== undefined && + !decomposition.trunk && + currentSummedValues !== undefined + ? currentSummedValues.summedValues.map( + (value, index) => value + row.delta[index], + ) + : row.delta, + })) + currentSummedValues = getLatestVisibleCalculation(sums[j].rows) + } + } + } + } + } + if (reverse) { + return sums.reverse() + } else { + return sums + } + } + + function getLatestCalculation<T extends { calculationName: CalculationName }>( + list: T[], + ): T { + for (const calculationName of [ + "amendment", + "bill", + "revaluation", + "law", + ] as CalculationName[]) { + const calculation = list?.find( + (item) => item.calculationName === calculationName, + ) + if (calculation !== undefined) { + return calculation + } + } + } + + function getLatestVisibleCalculation< + T extends { + calculationName: CalculationName + ignore?: boolean + }, + >(list: T[]): T | undefined { + for (const calculationName of [ + "amendment", + "bill", + "revaluation", + "law", + ] as CalculationName[]) { + const calculation = list?.find( + (item) => + item.calculationName === calculationName && (!item.ignore ?? true), + ) + if (calculation !== undefined) { + return calculation + } + } + } + + function getVariableCustomizationName(name: string, calculationName: string) { + return Object.hasOwn(variableCustomizations, `${name}_${calculationName}`) + ? `${name}_${calculationName}` + : name + } + + function getVariableValue( + situation: Situation, + variableName: string, + populationId: string, + ): VariableValue | undefined { + const variable = variableSummaryByName[variableName] + if (variable === undefined) { + return undefined + } + return getSituationVariableValue(situation, variable, populationId, year) + } + + function requestAxesCalculation() { + if ( + situation.sliders?.length === undefined || + situation.sliders?.length <= 0 + ) { + console.error( + "requestAxesCalculation", + "Situation sliders list is undefined", + ) + return + } + + // Get situation + const variable = variableSummaryByName[situation.sliders[0].name] + const slider = situation.sliders.find( + (slider) => + slider.entity === variable.entity && slider.name === variable.name, + ) as ActiveSlider | undefined + + if (slider === undefined) { + console.error("requestAxesCalculation", "Slider is undefined") + return + } + + const updatedMin = domain.x[0] ?? slider.min + const updatedMax = domain.x[1] ?? slider.max + const updatedStepValue = (updatedMax - updatedMin) / 100 + + const value = getSituationVariableValue( + situation, + variable, + slider.id, + year, + ) as number + + const vectorIndex = Math.max( + 0, + Math.min(100, Math.round(value / updatedStepValue)), + ) + + // Update situation + setSituationVariableValue( + entityByKey, + situation, + variable, + slider.id, + year, + Math.round(updatedStepValue * vectorIndex), + ) + situation.slider = { + ...slider, + min: updatedMin, + max: updatedMax, + stepValue: updatedStepValue, + vectorIndex: vectorIndex, + } + if (domain.y[0] !== undefined) { + situation.slider.yMin = domain.y[0] + } + if (domain.y[1] !== undefined) { + situation.slider.yMax = domain.y[1] + } + } + + function stackValues(...groups: VariableGraph[][]) { + const newGroups: VariableGraph[][] = [] + for (const group of groups.toReversed()) { + if (newGroups.length === 0) { + newGroups.push(group) + } else { + if ( + !group.some( + ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, + ) + ) { + newGroups.push(group) + } else { + const previousGroup = newGroups.findLast((group) => + group.some( + ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, + ), + ) + if (previousGroup === undefined) { + return groups + } + const previousGroupTopVariable = previousGroup.find( + ({ rows }) => getLatestVisibleCalculation(rows) !== undefined, + ) + if (previousGroupTopVariable === undefined) { + return groups + } + const previousGroupTopVariableLatestVisibleCalculation = + getLatestVisibleCalculation( + previousGroupTopVariable.rows, + ) as VariableGraphRow + newGroups.push( + group.map((value) => ({ + ...value, + rows: value.rows.map((row) => ({ + ...row, + summedValues: row.summedValues.map( + (value, index) => + value + + previousGroupTopVariableLatestVisibleCalculation + .summedValues[index], + ), + })), + })), + ) + } + } + } + return newGroups.toReversed() + } + + function updateDomainAxis( + { target }: Event, + axis: "x" | "y", + minMax: string, + ) { + let { value }: { value: VariableValue } = target as + | HTMLInputElement + | HTMLSelectElement + const numberValue = value.replace(/\D/g, "") + if (numberValue !== "") { + domain[axis][minMax === "min" ? 0 : 1] = Number(numberValue) + requestAxesCalculation() + } + } </script> {#if situation.slider !== undefined && visibleDecompositionsGraph !== undefined} @@ -903,7 +891,7 @@ <div class="flex flex-col gap-2"> <div class="h-4 w-24 bg-neutral-300"></div> <div class="flex gap-4"> - {#each Array(row) as _} + {#each Array(row)} <div class="flex h-9 items-center overflow-hidden rounded-full border bg-white text-neutral-600" > @@ -929,7 +917,7 @@ <div class="h-9 w-32 bg-neutral-300"></div> </div> <div class="flex gap-2"> - {#each Array(10) as _} + {#each Array(10)} <div class="h-1 w-full bg-neutral-300"></div> {/each} </div> @@ -1142,10 +1130,10 @@ class="w-24 rounded-t border-b-2 border-black bg-neutral-200 px-3 outline-none md:w-28" min="0" max={maxVariableValue} - onchange={(event) => updateDomain(event, "y", "max")} + onchange={(event) => updateDomainAxis(event, "y", "max")} step="100" type="text" - value={domain.y?.max ?? maxVariableValue} + value={domain.y[1] ?? maxVariableValue} /> <span class="pointer-events-none absolute right-1 bg-neutral-200 px-2" @@ -1159,10 +1147,10 @@ class="w-28 rounded-t border-b-2 border-black bg-neutral-200 px-3 outline-none" min="0" max={maxVariableValue} - onchange={(event) => updateDomain(event, "y", "min")} + onchange={(event) => updateDomainAxis(event, "y", "min")} step="100" type="text" - value={situation.slider.yMin ?? domain.y?.min ?? 0} + value={situation.slider.yMin ?? domain.y[0] ?? 0} /> <span class="pointer-events-none absolute right-1 bg-neutral-200 px-2" @@ -1205,8 +1193,8 @@ trunk, xDomain: [situation.slider.min, situation.slider.max], yDomain: [ - domain?.y?.min ?? 0, - domain?.y?.max ?? maxVariableValue, + domain?.y[0] ?? 0, + domain?.y[1] ?? maxVariableValue, ], }, ), @@ -1240,8 +1228,10 @@ {#snippet children({ modelGroups })} <DragSelect {modelGroups} - on:zoom={({ detail }) => { - updateAllDomain(detail) + onZoom={(newDomain: GraphDomain) => { + domain.x = [newDomain.x[0], newDomain.x[1]] + domain.y = [newDomain.y[0], newDomain.y[1]] + requestAxesCalculation() }} > {#snippet children({ modelGroups })} @@ -1494,7 +1484,7 @@ class="w-24 rounded-t border-b-2 border-black bg-neutral-200 px-3 outline-none md:w-28" min="0" max="100000" - onchange={(event) => updateDomain(event, "x", "min")} + onchange={(event) => updateDomainAxis(event, "x", "min")} step="100" type="text" value={situation.slider.min} @@ -1546,7 +1536,7 @@ class="w-24 rounded-t border-b-2 border-black bg-neutral-200 px-3 outline-none md:w-28" min="0" max="100000" - onchange={(event) => updateDomain(event, "x", "max")} + onchange={(event) => updateDomainAxis(event, "x", "max")} step="100" type="text" value={situation.slider.max} diff --git a/src/lib/components/test_cases/TestCaseGraphXlsxExport.svelte b/src/lib/components/test_cases/TestCaseGraphXlsxExport.svelte index b9bc42bb3c103300b5a070296c5522452e8245ea..f541e5a079a2c7a02a39237dabf3203ec84084cc 100644 --- a/src/lib/components/test_cases/TestCaseGraphXlsxExport.svelte +++ b/src/lib/components/test_cases/TestCaseGraphXlsxExport.svelte @@ -12,6 +12,7 @@ import XLSX from "xlsx-js-style" import { page } from "$app/stores" + import type { GraphDomain } from "$lib/components/piece_of_cake/types" import { decompositionCoreByName } from "$lib/decompositions" import type { DisplayMode } from "$lib/displays" import { entityByKey } from "$lib/entities" @@ -43,10 +44,7 @@ interface Props { displayMode: DisplayMode - domain: { - x: { min?: number; max?: number } - y: { min: number; max: number } - } + domain: GraphDomain situation: Situation situationIndex: number variableSummaryByName: VariableByName @@ -157,7 +155,7 @@ [ undefined, "Fourchette de l'axe des X :", - `${domain.x.min}-${domain.x.max}`, + `${domain.x[0]}-${domain.x[1]}`, ], [undefined, "Date d'export :", dateFormatter(new Date())], [ diff --git a/src/lib/shared.svelte.ts b/src/lib/shared.svelte.ts index 43d007bedb0c69ee5d9812d205d9fa8326d44890..153b30f7584f91606d39ffc45641b0d01e008b22 100644 --- a/src/lib/shared.svelte.ts +++ b/src/lib/shared.svelte.ts @@ -41,6 +41,8 @@ export interface Shared { requestedSimulationEmail?: string // Budget simulation requests requestedSimulationSent: boolean // Budget simulation requests requestedVariablesNameToCalculate?: Set<string> // Budget simulation requests + savedSituation?: Situation + savedSituationIndex?: number searchActive: boolean // Search searchVariableName?: string // Search showNulls: boolean