diff --git a/src/lib/calculations.svelte.ts b/src/lib/calculations.svelte.ts index 0b84a3f4cec546e643406e6844c497a71ef1ad3d..3e4e8df8cf02f816d49d74eceeb939ad202f97ac 100644 --- a/src/lib/calculations.svelte.ts +++ b/src/lib/calculations.svelte.ts @@ -13,16 +13,8 @@ import { updateEvaluations, waterfalls, } from "$lib/decompositions" -import { - type Axis, - type Calculation, - type CalculationByName, - getPopulationReservedKeys, - type Situation, - type SituationWithAxes, - type TestCasesCalculationInput, -} from "$lib/situations" import { entityByKey } from "$lib/entities" +import { metadata } from "$lib/metadata" import type { ParametricReform } from "$lib/reforms" import { billActive, @@ -30,8 +22,23 @@ import { revaluationName, shared, year, + yearPLF, } from "$lib/shared.svelte" import { + type Axis, + type Calculation, + type CalculationByName, + getPopulationReservedKeys, + type Situation, + type SituationWithAxes, + type TestCasesCalculationInput, +} from "$lib/situations" +import { + budgetEditableParametersName, + type BudgetVariable, + budgetVariableNameByVariableName, + budgetVariablesConfig, + budgetVariablesName, otherCalculatedVariablesName, summaryCalculatedVariablesName, variableSummaryByName, @@ -64,6 +71,8 @@ export const requestedCalculations = $state({ situationIndexByCalculationName: {}, }) as RequestedCalculations +let budgetSimulationAbortController = new AbortController() + const testCasesAbortControllers: { [calculationName: string]: AbortController } = Object.fromEntries( @@ -73,11 +82,87 @@ const testCasesAbortControllers: { ]), ) -export async function calculateTestCases( +export function buildBudgetDemandBody( + variableName: string, + outputVariables: string[], + quantileBaseVariable: string[], + quantileCompareVariables: string[], + includeDisplay = true, +) { + const budgetParametricReform = Object.fromEntries( + Object.entries(shared.parametricReform).filter(([parameterName]) => + budgetEditableParametersName.has(parameterName), + ), + ) + return { + amendement: budgetParametricReform, + base: yearPLF, + displayMode: includeDisplay ? shared.displayMode : undefined, + metadata, + output_variables: outputVariables, + quantile_base_variable: quantileBaseVariable, + quantile_compare_variables: quantileCompareVariables, + winners_loosers_variable: variableName, + quantile_nb: 10, + plf: billName === undefined ? undefined : yearPLF, + } +} + +export async function calculateBudget( + budgetVariableName: string, + // budgetCalculationNames: Set<CalculationName>, +): Promise<void> { + budgetSimulationAbortController.abort() + budgetSimulationAbortController = new AbortController() + if (!budgetVariablesName.has(budgetVariableName)) { + console.error( + `Budget calculation for variable ${budgetVariableName} is not available`, + ) + shared.budgetSimulation = undefined + return + } + const variableName = budgetVariableNameByVariableName[budgetVariableName] + const variableConfig: BudgetVariable = budgetVariablesConfig[variableName] + const body = JSON.stringify( + buildBudgetDemandBody( + variableName, + variableConfig.outputVariables, + variableConfig.quantileBaseVariable, + variableConfig.quantileCompareVariables, + ), + ) + shared.budgetSimulation = undefined + const response = await fetch("/budgets", { + body, + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + method: "POST", + signal: budgetSimulationAbortController.signal, + }) + if (!response.ok) { + console.error( + `"/budgets": Error while calculating budget:\n${body}\n${response.status} ${response.statusText}`, + ) + console.error(await response.text()) + shared.budgetSimulation = { + errors: [ + `Une erreur inattendue (${response.status} ${response.statusText}) s'est produite et les impacts budgétaires ne sont pas disponibles pour le moment. Écrivez-nous à leximpact@assemblee-nationale.fr.`, + ], + isPublic: true, + } + shared.budgetSimulation = undefined + return + } + shared.budgetSimulation = await response.json() +} + +export function calculateTestCases( situationIndexByCalculationName: RequestedSituationIndexByCalculationName, ) { const aggregatedSituation: SituationWithAxes = {} - const axesData: { situationIndex: number; variables: string[] } = + const axesData: { situationIndex?: number; variables: string[] } = shared.axes.reduce( ( result: { situationIndex?: number; variables: string[] }, @@ -108,7 +193,7 @@ export async function calculateTestCases( shared.inputInstantsByVariableNameArray[situationIndex] const situationPrefix = `Cas type n°${situationIndex + 1} | ` for (const entity of entities) { - let entitySituation = situation[entity.key_plural as string] + const entitySituation = situation[entity.key_plural as string] if (entitySituation === undefined) { continue } @@ -392,9 +477,9 @@ function cleanSituation( situationToClean: Situation, axes: Axis[][], ): SituationWithAxes { - let situation: SituationWithAxes = {} + const situation: SituationWithAxes = {} for (const entity of Object.values(entityByKey)) { - let entitySituation = situationToClean[entity.key_plural as string] + const entitySituation = situationToClean[entity.key_plural as string] if (entitySituation === undefined) { continue } @@ -431,9 +516,9 @@ export function requestAllBudgetCalculations(variableName: string): void { } } -export async function requestAllTestCasesCalculations( +export function requestAllTestCasesCalculations( requestedSituationIndex: number | null, -): Promise<void> { +): void { if ( calculationNames.some((calculationName) => { const situationIndex = @@ -488,6 +573,61 @@ export function requestTestCasesCalculation( } } +export async function sendBudgetSimulationDemand() { + const budgetVariableName = shared.displayMode?.parametersVariableName + if ( + budgetVariableName === undefined || + !budgetVariablesName.has(budgetVariableName) + ) { + console.error( + `Budget calculation for variable ${budgetVariableName} is not available`, + ) + shared.budgetSimulation = undefined + return + } + const variableName = budgetVariableNameByVariableName[budgetVariableName] + const variableConfig: BudgetVariable = budgetVariablesConfig[variableName] + const simulationBody = buildBudgetDemandBody( + variableName, + variableConfig.outputVariables, + variableConfig.quantileBaseVariable, + variableConfig.quantileCompareVariables, + false, + ) + const urlString = "/budgets/demands" + const res = await fetch(urlString, { + body: JSON.stringify( + { + displayMode: shared.displayMode, + email: shared.requestedSimulationEmail, + request: Object.fromEntries( + Object.entries(shared.parametricReform).filter(([parameterName]) => + budgetEditableParametersName.has(parameterName), + ), + ), + simulation: simulationBody, + }, + null, + 2, + ), + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + method: "POST", + }) + if (!res.ok) { + shared.requestedSimulationEmail = undefined + console.error( + `Error ${ + res.status + } while sending a simulation request at ${urlString}\n\n${await res.text()}`, + ) + return + } + shared.requestedSimulationEmail = undefined +} + async function sendTestCasesSimulation( calculationName: CalculationName, { @@ -641,49 +781,60 @@ async function sendTestCasesSimulation( // Variable has been computed for all test cases. // First, update delta and values of evaluations. - updatedEvaluationByNameArray = updatedEvaluationByNameArray.map( - (evaluationByName, situationIndex): EvaluationByName => { - const situation = shared.testCases[situationIndex] - const values = valuesByCalculationNameByVariableNameArray[ - situationIndex - ][variableName][calculationName] as VariableValues - const entitySituation = - situation[entity.key_plural as string] ?? {} - const situationPopulationCount = - Object.keys(entitySituation).length - let delta = new Array(shared.vectorLength).fill(0) - for (const situationPersonIndex of Object.keys( - entitySituation, - ).keys()) { - for ( - let index = situationPersonIndex, vectorIndex = 0; - vectorIndex < shared.vectorLength; - index += situationPopulationCount, vectorIndex++ - ) { - delta[vectorIndex] += values[index] - } - } - const evaluation = evaluationByName[variableName] - const calculationEvaluationByName = - evaluation?.calculationEvaluationByName ?? {} - return { - ...evaluationByName, - [variableName]: { - ...(evaluation ?? {}), - calculationEvaluationByName: { - ...calculationEvaluationByName, - [calculationName]: { - ...(calculationEvaluationByName[calculationName] ?? {}), - delta, - deltaAtVectorIndex: - delta[situation.slider?.vectorIndex ?? 0] ?? 0, - }, - }, - fromOpenFisca: true, - }, + for (const [ + situationIndex, + evaluationByName, + ] of updatedEvaluationByNameArray.entries()) { + const situation = shared.testCases[situationIndex] + const values = valuesByCalculationNameByVariableNameArray[ + situationIndex + ][variableName][calculationName] as VariableValues + const entitySituation = situation[entity.key_plural as string] ?? {} + const situationPopulationCount = Object.keys(entitySituation).length + const delta = new Array(shared.vectorLength).fill(0) + for (const situationPersonIndex of Object.keys( + entitySituation, + ).keys()) { + for ( + let index = situationPersonIndex, vectorIndex = 0; + vectorIndex < shared.vectorLength; + index += situationPopulationCount, vectorIndex++ + ) { + delta[vectorIndex] += values[index] } - }, - ) + } + + if (evaluationByName[variableName] === undefined) { + evaluationByName[variableName] = {} + } + + if ( + evaluationByName[variableName].calculationEvaluationByName === + undefined + ) { + evaluationByName[variableName].calculationEvaluationByName = {} + } + + if ( + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ] === undefined + ) { + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ] = {} + } + + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ]!.delta = delta + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ]!.deltaAtVectorIndex = + delta[situation.slider?.vectorIndex ?? 0] ?? 0 + + evaluationByName[variableName].fromOpenFisca = true + } } else { // Variable has been computed for a single test case. @@ -694,7 +845,7 @@ async function sendTestCasesSimulation( ][variableName][calculationName] as VariableValues const entitySituation = situation[entity.key_plural as string] ?? {} const situationPopulationCount = Object.keys(entitySituation).length - let delta = new Array(shared.vectorLength).fill(0) + const delta = new Array(shared.vectorLength).fill(0) for (const situationPersonIndex of Object.keys( entitySituation, ).keys()) { @@ -709,22 +860,36 @@ async function sendTestCasesSimulation( const evaluationByName = { ...updatedEvaluationByNameArray[calculation.situationIndex], } - const evaluation = evaluationByName[variableName] - const calculationEvaluationByName = - evaluation?.calculationEvaluationByName ?? {} - evaluationByName[variableName] = { - ...(evaluation ?? {}), - calculationEvaluationByName: { - ...calculationEvaluationByName, - [calculationName]: { - ...(calculationEvaluationByName[calculationName] ?? {}), - delta, - deltaAtVectorIndex: - delta[situation.slider?.vectorIndex ?? 0] ?? 0, - }, - }, - fromOpenFisca: true, + + if (evaluationByName[variableName] === undefined) { + evaluationByName[variableName] = {} } + + if ( + evaluationByName[variableName].calculationEvaluationByName === + undefined + ) { + evaluationByName[variableName].calculationEvaluationByName = {} + } + + if ( + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ] === undefined + ) { + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ] = {} + } + + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ]!.delta = delta + evaluationByName[variableName].calculationEvaluationByName[ + calculationName + ]!.deltaAtVectorIndex = delta[situation.slider?.vectorIndex ?? 0] ?? 0 + + evaluationByName[variableName].fromOpenFisca = true } } } diff --git a/src/lib/components/test_cases/TestCaseGraph.svelte b/src/lib/components/test_cases/TestCaseGraph.svelte index d82aea47f6af8978b014cc9207a5893acef17632..17b5e397cf1eecaffe4306d553dfa0b893d1214c 100644 --- a/src/lib/components/test_cases/TestCaseGraph.svelte +++ b/src/lib/components/test_cases/TestCaseGraph.svelte @@ -1,6 +1,4 @@ <script lang="ts"> - import { run } from "svelte/legacy" - import { type NumberValue, type PopulationWithoutId, @@ -11,10 +9,9 @@ type RateBracketAtInstant, scaleByInstantFromBrackets, } from "@openfisca/json-model" - import { createEventDispatcher } from "svelte" + import { createEventDispatcher, untrack } from "svelte" import { fade } from "svelte/transition" - import { page } from "$app/stores" import type { CalculationName } from "$lib/calculations.svelte" import PersistentPopover from "$lib/components/PersistentPopover.svelte" import PictoBigAdulteRetraite from "$lib/components/pictos/PictoBigAdulteRetraite.svelte" @@ -517,287 +514,293 @@ } requestAxesCalculation() } - let visibleDecompositionsGraph = $derived( - situation.slider !== undefined - ? buildVisibleDecompositionsForGraph( - shared.decompositionByName, - entityByKey, - evaluationByName, - situation, - variableSummaryByName, - waterfall, - false, - useRevaluationInsteadOfLaw, - vectorLength, - year, - ) - : undefined, - ) - run(() => { + 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 ) { - 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, - ), + untrack(() => { + const ressourcesBrutes = [ + "remuneration_brute", + "chomage_brut", + "retraite_brute", ] - const complementsDeRessourcesDecompositions = decompositions - .slice(versementBrutIndex, prestationsIndex) - .filter( + if (waterfall.name === "brut_to_disponible") { + const decompositions = visibleDecompositionsGraph.filter( (decomposition) => - !ressourcesBrutes.includes(decomposition.decomposition.name) && - !getLatestVisibleCalculation(decomposition.rows)?.isNegative, + !decomposition.trunk || + ressourcesBrutes.includes(decomposition.decomposition.name) || + decomposition.decomposition.name === "revenu_disponible", ) - const prestationsDecompositions = decompositions.slice( - prestationsIndex, - revenuIndex, - ) + 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 revenuDisponibleDecomposition = decompositions[revenuIndex] - const revenuDisponibleValues = generateValues([ - revenuDisponibleDecomposition, - ]) - - const [ - prelevementsValues, - complementsDeRessourcesValues, - prestationsValues, - ] = stackValues( - generateValues(prelevementsDecompositions), - generateValues(complementsDeRessourcesDecompositions), - generateValues(prestationsDecompositions, true), - ) + const prelevementsDecompositions = [ + ...decompositions.slice(0, versementBrutIndex), + ...decompositions + .slice(versementBrutIndex, prestationsIndex) + .filter( + (decomposition) => + ressourcesBrutes.includes(decomposition.decomposition.name) || + getLatestVisibleCalculation(decomposition.rows)?.isNegative, + ), + ] - variableValues = [ - prelevementsValues, - complementsDeRessourcesValues, - prestationsValues, - revenuDisponibleValues, - ] + const complementsDeRessourcesDecompositions = decompositions + .slice(versementBrutIndex, prestationsIndex) + .filter( + (decomposition) => + !ressourcesBrutes.includes(decomposition.decomposition.name) && + !getLatestVisibleCalculation(decomposition.rows)?.isNegative, + ) - const splitIndex = - prelevementsValues.findIndex( - (variable) => variable.name === "remuneration_brute", - ) + 1 - - variableGroups = [ - { - variables: [ - ...prelevementsValues.slice(0, splitIndex).reverse(), - ...complementsDeRessourcesValues, - ...revenuDisponibleValues, - ], - }, - { - css: "text-red-600", - label: "Prélèvements", - name: "prelevements", - hideOpen: true, - variables: prelevementsValues.slice(splitIndex), - }, - { - css: "text-green-800", - label: "Prestations", - name: "prestations", - hideOpen: true, - variables: prestationsValues, - }, - ] + const prestationsDecompositions = decompositions.slice( + prestationsIndex, + revenuIndex, + ) - for (const [groupIndex, group] of variableGroups.entries()) { - for (const variable of group.variables) { - for (const row of variable.rows) { - const key = `${variable.name}_${row.calculationName}` - if ( - !Object.keys(variableCustomizations).includes(key) && - ((row.calculationName !== "law" && - row.calculationName !== "revaluation") || - !Object.keys(variableCustomizations).includes(variable.name)) - ) { - const types = ["base", "prelevement", "prestation"] - variableCustomizations = { - ...variableCustomizations, - [key]: structuredClone( - variableCustomizations[ - getVariableCustomizationName( - variable.parent, - row.calculationName, - ) - ] ?? - defaultCustomization[waterfall.name][types[groupIndex]][ - row.calculationName === "revaluation" - ? "law" - : row.calculationName - ], - ), + const revenuDisponibleDecomposition = decompositions[revenuIndex] + const revenuDisponibleValues = generateValues([ + revenuDisponibleDecomposition, + ]) + + const [ + prelevementsValues, + complementsDeRessourcesValues, + prestationsValues, + ] = stackValues( + generateValues(prelevementsDecompositions), + generateValues(complementsDeRessourcesDecompositions), + generateValues(prestationsDecompositions, true), + ) + + variableValues = [ + prelevementsValues, + complementsDeRessourcesValues, + prestationsValues, + revenuDisponibleValues, + ] + + const splitIndex = + prelevementsValues.findIndex( + (variable) => variable.name === "remuneration_brute", + ) + 1 + + variableGroups = [ + { + variables: [ + ...prelevementsValues.slice(0, splitIndex).reverse(), + ...complementsDeRessourcesValues, + ...revenuDisponibleValues, + ], + }, + { + css: "text-red-600", + label: "Prélèvements", + name: "prelevements", + hideOpen: true, + variables: prelevementsValues.slice(splitIndex), + }, + { + css: "text-green-800", + label: "Prestations", + name: "prestations", + hideOpen: true, + variables: prestationsValues, + }, + ] + + for (const [groupIndex, group] of variableGroups.entries()) { + for (const variable of group.variables) { + for (const row of variable.rows) { + const key = `${variable.name}_${row.calculationName}` + if ( + !Object.keys(variableCustomizations).includes(key) && + ((row.calculationName !== "law" && + row.calculationName !== "revaluation") || + !Object.keys(variableCustomizations).includes( + variable.name, + )) + ) { + const types = ["base", "prelevement", "prestation"] + variableCustomizations[key] = JSON.parse( + JSON.stringify( + variableCustomizations[ + getVariableCustomizationName( + variable.parent, + row.calculationName, + ) + ] ?? + defaultCustomization[waterfall.name][types[groupIndex]][ + row.calculationName === "revaluation" + ? "law" + : row.calculationName + ], + ), + ) } } } } - } - } else { - const decompositions = visibleDecompositionsGraph.filter( - (decomposition) => - !decomposition.trunk || - decomposition.decomposition.name === "remuneration_brute" || - decomposition.decomposition.name === "salaire_super_brut", - ) - - const allValues = generateValues(decompositions) - - const versementBrutIndex = allValues.findIndex( - (value) => value.name === "remuneration_brute", - ) - const allegementGeneralIndex = allValues.findIndex( - (value) => value.name === "allegement_general", - ) + } else { + const decompositions = visibleDecompositionsGraph.filter( + (decomposition) => + !decomposition.trunk || + decomposition.decomposition.name === "remuneration_brute" || + decomposition.decomposition.name === "salaire_super_brut", + ) - const versementBrutValues = allValues.slice(0, versementBrutIndex + 1) + const allValues = generateValues(decompositions) - const superBrutValues = allValues.slice(-1) + const versementBrutIndex = allValues.findIndex( + (value) => value.name === "remuneration_brute", + ) + const allegementGeneralIndex = allValues.findIndex( + (value) => value.name === "allegement_general", + ) - const cotisationsValues = allValues.slice( - versementBrutIndex + 1, - allegementGeneralIndex, - ) + const versementBrutValues = allValues.slice(0, versementBrutIndex + 1) - const allegementsValues = allValues.slice(allegementGeneralIndex, -1) + const superBrutValues = allValues.slice(-1) - variableValues = [ - versementBrutValues, - cotisationsValues, - allegementsValues, - superBrutValues, - ] + const cotisationsValues = allValues.slice( + versementBrutIndex + 1, + allegementGeneralIndex, + ) - variableGroups = [ - { - variables: [ - ...allValues.slice(0, versementBrutIndex + 1), - ...allValues.slice(-1), - ], - }, - { - css: "text-green-800", - label: "Cotisations employeur", - name: "cotisations_employeur", - hideOpen: true, - variables: allValues.slice( - allValues.findIndex( - (value) => - value.name === "cotisations_employeur_securite_sociale", + const allegementsValues = allValues.slice(allegementGeneralIndex, -1) + + variableValues = [ + versementBrutValues, + cotisationsValues, + allegementsValues, + superBrutValues, + ] + + variableGroups = [ + { + variables: [ + ...allValues.slice(0, versementBrutIndex + 1), + ...allValues.slice(-1), + ], + }, + { + css: "text-green-800", + label: "Cotisations employeur", + name: "cotisations_employeur", + hideOpen: true, + variables: allValues.slice( + allValues.findIndex( + (value) => + value.name === "cotisations_employeur_securite_sociale", + ), + allegementGeneralIndex, ), - allegementGeneralIndex, - ), - }, - { - css: "text-red-600", - label: "Allègements", - name: "allegements", - hideOpen: true, - variables: allValues.slice(allegementGeneralIndex, -1), - }, - ] - - for (const [groupIndex, group] of variableGroups.entries()) { - for (const variable of group.variables) { - for (const row of variable.rows) { - const key = `${variable.name}_${row.calculationName}` - if ( - !Object.keys(variableCustomizations).includes(key) && - ((row.calculationName !== "law" && - row.calculationName !== "revaluation") || - !Object.keys(variableCustomizations).includes(variable.name)) - ) { - const types = ["base", "cotisation", "allegement"] - variableCustomizations = { - ...variableCustomizations, - [key]: structuredClone( - variableCustomizations[ - getVariableCustomizationName( - variable.parent, - row.calculationName, - ) - ] ?? - defaultCustomization[waterfall.name][types[groupIndex]][ - row.calculationName === "revaluation" - ? "law" - : row.calculationName - ], - ), + }, + { + css: "text-red-600", + label: "Allègements", + name: "allegements", + hideOpen: true, + variables: allValues.slice(allegementGeneralIndex, -1), + }, + ] + + for (const [groupIndex, group] of variableGroups.entries()) { + for (const variable of group.variables) { + for (const row of variable.rows) { + const key = `${variable.name}_${row.calculationName}` + if ( + !Object.keys(variableCustomizations).includes(key) && + ((row.calculationName !== "law" && + row.calculationName !== "revaluation") || + !Object.keys(variableCustomizations).includes( + variable.name, + )) + ) { + const types = ["base", "cotisation", "allegement"] + variableCustomizations[key] = JSON.parse( + JSON.stringify( + variableCustomizations[ + getVariableCustomizationName( + variable.parent, + row.calculationName, + ) + ] ?? + defaultCustomization[waterfall.name][types[groupIndex]][ + row.calculationName === "revaluation" + ? "law" + : row.calculationName + ], + ), + ) } } } } } - } - maxVariableValue = variableValues.reduce( - (max: number, values: VariableGraph[]) => { - max = Math.max( - max, - Math.max( - ...values.map(({ rows }) => - Math.max( - ...rows.map(({ summedValues }) => Math.max(...summedValues)), + maxVariableValue = variableValues.reduce( + (max: number, values: VariableGraph[]) => { + max = Math.max( + max, + Math.max( + ...values.map(({ rows }) => + Math.max( + ...rows.map(({ summedValues }) => + Math.max(...summedValues), + ), + ), ), ), - ), - ) - return max - }, - 0, - ) + ) + return max + }, + 0, + ) + }) } }) - let domain - run(() => { - domain = { - x: { - min: situation.slider?.min, - max: situation.slider?.max, - }, - y: { - min: situation.slider?.yMin ?? 0, - max: situation.slider?.yMax ?? maxVariableValue ?? 100000, - }, - } + let domain = $state({ + x: { + min: situation.slider?.min, + max: situation.slider?.max, + }, + y: { + min: situation.slider?.yMin ?? 0, + max: situation.slider?.yMax ?? maxVariableValue ?? 100000, + }, }) // 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 @@ -900,7 +903,7 @@ let smicValue = $derived(smicLatestInstantValueCouple?.[1] as NumberValue) </script> -{#if situation.slider !== undefined} +{#if situation.slider !== undefined && visibleDecompositionsGraph !== undefined} <div class="relative"> {#if isCalculationRunning} <div class="absolute inset-0 z-20 bg-white" out:fade> diff --git a/src/lib/components/variables/VariableInput.svelte b/src/lib/components/variables/VariableInput.svelte index ff05e9d7c665915187efde449b64d3aad3d391de..7e88820ba183ce8ba7d6d0e7a846e7622dea679a 100644 --- a/src/lib/components/variables/VariableInput.svelte +++ b/src/lib/components/variables/VariableInput.svelte @@ -44,7 +44,7 @@ inputInstantsByVariableName = $bindable(), situation = $bindable(), situationIndex, - valuesByCalculationNameByVariableName, + valuesByCalculationNameByVariableName = $bindable(), variable, year, }: Props = $props() diff --git a/src/lib/shared.svelte.ts b/src/lib/shared.svelte.ts index f6e037ad79ce8750a1b6ee612cef29d51ff9e4aa..706090873bee86e848c9c70d4be26e5e17613509 100644 --- a/src/lib/shared.svelte.ts +++ b/src/lib/shared.svelte.ts @@ -4,40 +4,27 @@ import { type Waterfall } from "@openfisca/json-model" import "iconify-icon" import type { BudgetSimulation } from "$lib/budgets" -import { requestAllTestCasesCalculations } from "$lib/calculations.svelte" import { buildDecompositionByNameFromCore, decompositionCoreByName, decompositionCoreByNameByReformName, - updateEvaluations, - updateEvaluationsVectorIndex, waterfalls, type EvaluationByName, type DecompositionByName, } from "$lib/decompositions" import type { DisplayMode } from "$lib/displays" -import { entityByKey } from "$lib/entities" import { trackTestCaseShowAllVariables } from "$lib/matomo" -import { metadata } from "$lib/metadata" import { getNavbarConfig, type NavbarConfig } from "$lib/navbar" import publicConfig from "$lib/public_config" import { type ParametricReform, reformMetadataByName } from "$lib/reforms" import { extractInputInstantsFromTestCases, - indexOfSituationPopulationId, testCasesCore, type Axis, type CalculationByName, type Situation, } from "$lib/situations" -import { - budgetEditableParametersName, - budgetVariableNameByVariableName, - budgetVariablesConfig, - budgetVariablesName, - type BudgetVariable, - type ValuesByCalculationNameByVariableName, -} from "$lib/variables" +import { type ValuesByCalculationNameByVariableName } from "$lib/variables" export interface Shared { axes: Axis[][] @@ -66,15 +53,8 @@ export interface Shared { waterfall: Waterfall } -const { - baseUrl, - reformName: reformBillName, - revaluationName: reformRevaluationName, -} = publicConfig - -let axisBySituationIndex: { [situationIndex: string]: Axis } = {} - -let budgetSimulationAbortController = new AbortController() +const { reformName: reformBillName, revaluationName: reformRevaluationName } = + publicConfig /** * Bill (PLF) and revaluation (contrefactuel) @@ -136,266 +116,6 @@ export const shared: Shared = $state({ waterfall: waterfalls[0], }) -const vectorIndexes = new Array(testCasesCore.length).fill(0) - if (shared.showNulls) { trackTestCaseShowAllVariables() } - -function buildBudgetDemandBody( - variableName: string, - outputVariables: string[], - quantileBaseVariable: string[], - quantileCompareVariables: string[], - includeDisplay = true, -) { - const budgetParametricReform = Object.fromEntries( - Object.entries(shared.parametricReform).filter(([parameterName]) => - budgetEditableParametersName.has(parameterName), - ), - ) - return { - amendement: budgetParametricReform, - base: yearPLF, - displayMode: includeDisplay ? shared.displayMode : undefined, - metadata, - output_variables: outputVariables, - quantile_base_variable: quantileBaseVariable, - quantile_compare_variables: quantileCompareVariables, - winners_loosers_variable: variableName, - quantile_nb: 10, - plf: billName === undefined ? undefined : yearPLF, - } -} - -export async function calculateBudget( - budgetVariableName: string, - // budgetCalculationNames: Set<CalculationName>, -): Promise<void> { - budgetSimulationAbortController.abort() - budgetSimulationAbortController = new AbortController() - if (!budgetVariablesName.has(budgetVariableName)) { - console.error( - `Budget calculation for variable ${budgetVariableName} is not available`, - ) - shared.budgetSimulation = undefined - return - } - const variableName = budgetVariableNameByVariableName[budgetVariableName] - const variableConfig: BudgetVariable = budgetVariablesConfig[variableName] - const body = JSON.stringify( - buildBudgetDemandBody( - variableName, - variableConfig.outputVariables, - variableConfig.quantileBaseVariable, - variableConfig.quantileCompareVariables, - ), - ) - shared.budgetSimulation = undefined - const response = await fetch("/budgets", { - body, - headers: { - Accept: "application/json", - "Content-Type": "application/json; charset=utf-8", - }, - method: "POST", - signal: budgetSimulationAbortController.signal, - }) - if (!response.ok) { - console.error( - `${baseUrl}: Error while calculating budget:\n${body}\n${response.status} ${response.statusText}`, - ) - console.error(await response.text()) - shared.budgetSimulation = { - errors: [ - `Une erreur inattendue (${response.status} ${response.statusText}) s'est produite et les impacts budgétaires ne sont pas disponibles pour le moment. Écrivez-nous à leximpact@assemblee-nationale.fr.`, - ], - isPublic: true, - } - shared.budgetSimulation = undefined - return - } - shared.budgetSimulation = await response.json() -} - -export async function sendBudgetSimulationDemand() { - const budgetVariableName = shared.displayMode?.parametersVariableName - if ( - budgetVariableName === undefined || - !budgetVariablesName.has(budgetVariableName) - ) { - console.error( - `Budget calculation for variable ${budgetVariableName} is not available`, - ) - shared.budgetSimulation = undefined - return - } - const variableName = budgetVariableNameByVariableName[budgetVariableName] - const variableConfig: BudgetVariable = budgetVariablesConfig[variableName] - const simulationBody = buildBudgetDemandBody( - variableName, - variableConfig.outputVariables, - variableConfig.quantileBaseVariable, - variableConfig.quantileCompareVariables, - false, - ) - const urlString = "/budgets/demands" - const res = await fetch(urlString, { - body: JSON.stringify( - { - displayMode: shared.displayMode, - email: shared.requestedSimulationEmail, - request: Object.fromEntries( - Object.entries(shared.parametricReform).filter(([parameterName]) => - budgetEditableParametersName.has(parameterName), - ), - ), - simulation: simulationBody, - }, - null, - 2, - ), - headers: { - Accept: "application/json", - "Content-Type": "application/json; charset=utf-8", - }, - method: "POST", - }) - if (!res.ok) { - shared.requestedSimulationEmail = undefined - console.error( - `Error ${ - res.status - } while sending a simulation request at ${urlString}\n\n${await res.text()}`, - ) - return - } - shared.requestedSimulationEmail = undefined -} - -export function updateOnSliderChange(situations: Situation[]): void { - let changedAxesSituationIndex: number[] = [] - let evaluationByNameArrayChanged = false - const updatedEvaluationByNameArray = [...shared.evaluationByNameArray] - for (const [situationIndex, situation] of situations.entries()) { - const slider = situation.slider - if (slider === undefined) { - if (axisBySituationIndex[situationIndex] !== undefined) { - axisBySituationIndex = { ...axisBySituationIndex } - delete axisBySituationIndex[situationIndex] - changedAxesSituationIndex.push(situationIndex) - - // Change only vectorIndexes content, because vectorIndexes reactivity - // is not needed. - vectorIndexes[situationIndex] = 0 - } - } else { - const currentAxis = axisBySituationIndex[situationIndex] - if ( - currentAxis === undefined || - currentAxis.name !== slider.name || - currentAxis.min !== slider.min || - currentAxis.max !== slider.max - ) { - const entity = entityByKey[slider.entity] - - let previousSituationsPopulationCount = 0 - for (const situation of shared.testCases.slice(0, situationIndex)) { - const entitySituation = situation[entity.key_plural as string] ?? {} - const populationCount = Object.keys(entitySituation).length - previousSituationsPopulationCount += populationCount - } - const axis = { - count: 101, - index: - previousSituationsPopulationCount + - indexOfSituationPopulationId(entity, situation, slider.id), - max: slider.max, - min: slider.min, - name: slider.name, - period: year.toString(), // Previous years are added later. - situationIndex: previousSituationsPopulationCount, - } - axisBySituationIndex = { - ...axisBySituationIndex, - [situationIndex]: axis, - } - changedAxesSituationIndex.push(situationIndex) - } - - const vectorIndex = slider.vectorIndex - if (vectorIndex !== vectorIndexes[situationIndex]) { - // Change only vectorIndexes content, because vectorIndexes reactivity - // is not needed. - vectorIndexes[situationIndex] = vectorIndex - - // Update evaluations. - let evaluationByName = updatedEvaluationByNameArray[situationIndex] - const updatedEvaluationByName = updateEvaluationsVectorIndex( - evaluationByName, - vectorIndex, - ) - if (updatedEvaluationByName !== evaluationByName) { - updatedEvaluationByNameArray[situationIndex] = updatedEvaluationByName - evaluationByNameArrayChanged = true - } - } - } - } - - if (changedAxesSituationIndex.length > 0) { - const parallelAxes = Object.entries(axisBySituationIndex) - .sort(([situationIndex1], [situationIndex2]) => - situationIndex1.localeCompare(situationIndex2), - ) - .map(([, axis]) => axis) - .reduce((parallelAxes, axis) => { - // Add previous years of each axis. - const year = parseInt(axis.period as string) - parallelAxes.push( - { - ...axis, - period: (year - 2).toString(), - }, - { - ...axis, - period: (year - 1).toString(), - }, - axis, - ) - return parallelAxes - }, [] as Axis[]) - - shared.axes = parallelAxes.length === 0 ? [] : [parallelAxes] - - let updatedVectorLength = 1 - for (const parallelAxes of shared.axes) { - // All the parallel axes have the same count. - const axis = parallelAxes[0] - updatedVectorLength *= axis.count - } - shared.vectorLength = updatedVectorLength - - for (const situationIndex of changedAxesSituationIndex) { - updatedEvaluationByNameArray[situationIndex] = updateEvaluations( - shared.decompositionByName, - updatedEvaluationByNameArray[situationIndex], - vectorIndexes[situationIndex], - shared.vectorLength, - waterfalls, - ) - evaluationByNameArrayChanged = true - } - - // Launch calculations if the axes have not been deleted (ie shared.vectorLength > 1). - const situationIndex = - changedAxesSituationIndex.length === 1 - ? changedAxesSituationIndex[0] - : undefined - requestAllTestCasesCalculations(situationIndex ?? null) - } - - if (evaluationByNameArrayChanged) { - shared.evaluationByNameArray = updatedEvaluationByNameArray - } -} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b3c1cca51a2533aff7af5e285fd46302e18b3ef0..2716dbae8a69f0e1ce391018c645f7e19c779b93 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,21 +11,30 @@ import { onNavigate } from "$app/navigation" import { page } from "$app/stores" import { + calculateBudget, calculateTestCases, calculateTestCasesAdditionalVariables, requestAllTestCasesCalculations, requestedCalculations, + sendBudgetSimulationDemand, } from "$lib/calculations.svelte" import NavBar from "$lib/components/NavBar.svelte" import { trackPageView, trackParametricReform } from "$lib/matomo" import { getNavbarConfig } from "$lib/navbar" import publicConfig from "$lib/public_config" + import { shared, year } from "$lib/shared.svelte" import { - calculateBudget, - sendBudgetSimulationDemand, - shared, - updateOnSliderChange, - } from "$lib/shared.svelte" + type Axis, + indexOfSituationPopulationId, + type Situation, + testCasesCore, + } from "$lib/situations" + import { entityByKey } from "$lib/entities" + import { + updateEvaluations, + updateEvaluationsVectorIndex, + waterfalls, + } from "$lib/decompositions" interface Props { data: LayoutData @@ -38,6 +47,8 @@ let { url } = $derived($page) + let axisBySituationIndex: { [situationIndex: string]: Axis } = {} + const customOpenGraphRoutes = [ /\/fonctionnement/, /\/test_cases\/simulations\/.*/, @@ -47,6 +58,8 @@ shared.navbarConfig = getNavbarConfig($page.route.id) } + const vectorIndexes = new Array(testCasesCore.length).fill(0) + $effect(() => { if (matomoConfig !== undefined) { matomo(matomoConfig) @@ -108,30 +121,15 @@ })() } - $effect(() => { - updateOnSliderChange(shared.testCases) - }) onMount(() => { // Launch first simulation. requestAllTestCasesCalculations(null) + + // Initialize showTutorial from localStorage + shared.showTutorial = localStorage.getItem("hideTutorial") === null }) - $effect(() => { - if ( - requestedCalculations.budgetVariableName !== undefined && - requestedCalculations.budgetCalculationNames !== undefined - ) { - delete requestedCalculations.budgetCalculationNames - calculateBudget( - requestedCalculations.budgetVariableName /*, budgetCalculationNames */, - ) // Don't await - shared.requestedSimulationSent = false - } - }) - $effect(() => { - if (shared.requestedSimulationEmail !== undefined) { - sendBudgetSimulationDemand() - } - }) + + // Test cases calculations $effect(() => { if ( Object.keys(requestedCalculations.situationIndexByCalculationName) @@ -152,14 +150,158 @@ } }) $effect(() => { - shared.showTutorial = localStorage.getItem("hideTutorial") === null + updateOnSliderChange(shared.testCases) }) + // Budget calculations + $effect(() => { + if ( + requestedCalculations.budgetVariableName !== undefined && + requestedCalculations.budgetCalculationNames !== undefined + ) { + delete requestedCalculations.budgetCalculationNames + calculateBudget( + requestedCalculations.budgetVariableName /*, budgetCalculationNames */, + ) // Don't await + shared.requestedSimulationSent = false + } + }) + $effect(() => { + if (shared.requestedSimulationEmail !== undefined) { + sendBudgetSimulationDemand() + } + }) + + // Change navbar layout when navigating onNavigate((navigation) => { if (navigation.to?.route?.id !== navigation.from?.route?.id) { shared.navbarConfig = getNavbarConfig(navigation.to?.route?.id ?? "/") } }) + + function updateOnSliderChange(situations: Situation[]): void { + let changedAxesSituationIndex: number[] = [] + let evaluationByNameArrayChanged = false + const updatedEvaluationByNameArray = [...shared.evaluationByNameArray] + for (const [situationIndex, situation] of situations.entries()) { + const slider = situation.slider + if (slider === undefined) { + if (axisBySituationIndex[situationIndex] !== undefined) { + axisBySituationIndex = { ...axisBySituationIndex } + delete axisBySituationIndex[situationIndex] + changedAxesSituationIndex.push(situationIndex) + + // Change only vectorIndexes content, because vectorIndexes reactivity + // is not needed. + vectorIndexes[situationIndex] = 0 + } + } else { + const currentAxis = axisBySituationIndex[situationIndex] + if ( + currentAxis === undefined || + currentAxis.name !== slider.name || + currentAxis.min !== slider.min || + currentAxis.max !== slider.max + ) { + const entity = entityByKey[slider.entity] + + let previousSituationsPopulationCount = 0 + for (const situation of shared.testCases.slice(0, situationIndex)) { + const entitySituation = situation[entity.key_plural as string] ?? {} + const populationCount = Object.keys(entitySituation).length + previousSituationsPopulationCount += populationCount + } + axisBySituationIndex[situationIndex] = { + count: 101, + index: + previousSituationsPopulationCount + + indexOfSituationPopulationId(entity, situation, slider.id), + max: slider.max, + min: slider.min, + name: slider.name, + period: year.toString(), // Previous years are added later. + situationIndex: previousSituationsPopulationCount, + } + changedAxesSituationIndex.push(situationIndex) + } + + const vectorIndex = slider.vectorIndex + if (vectorIndex !== vectorIndexes[situationIndex]) { + // Change only vectorIndexes content, because vectorIndexes reactivity + // is not needed. + vectorIndexes[situationIndex] = vectorIndex + + // Update evaluations. + let evaluationByName = updatedEvaluationByNameArray[situationIndex] + const updatedEvaluationByName = updateEvaluationsVectorIndex( + evaluationByName, + vectorIndex, + ) + if (updatedEvaluationByName !== evaluationByName) { + updatedEvaluationByNameArray[situationIndex] = + updatedEvaluationByName + evaluationByNameArrayChanged = true + } + } + } + } + + if (changedAxesSituationIndex.length > 0) { + const parallelAxes = Object.entries(axisBySituationIndex) + .sort(([situationIndex1], [situationIndex2]) => + situationIndex1.localeCompare(situationIndex2), + ) + .map(([, axis]) => axis) + .reduce((parallelAxes, axis) => { + // Add previous years of each axis. + const year = parseInt(axis.period as string) + parallelAxes.push( + { + ...axis, + period: (year - 2).toString(), + }, + { + ...axis, + period: (year - 1).toString(), + }, + axis, + ) + return parallelAxes + }, [] as Axis[]) + + shared.axes = parallelAxes.length === 0 ? [] : [parallelAxes] + + let updatedVectorLength = 1 + for (const parallelAxes of shared.axes) { + // All the parallel axes have the same count. + const axis = parallelAxes[0] + updatedVectorLength *= axis.count + } + shared.vectorLength = updatedVectorLength + + for (const situationIndex of changedAxesSituationIndex) { + updatedEvaluationByNameArray[situationIndex] = updateEvaluations( + shared.decompositionByName, + updatedEvaluationByNameArray[situationIndex], + vectorIndexes[situationIndex], + shared.vectorLength, + waterfalls, + ) + evaluationByNameArrayChanged = true + } + + // Launch calculations if the axes have not been deleted (ie shared.vectorLength > 1). + const situationIndex = + changedAxesSituationIndex.length === 1 + ? changedAxesSituationIndex[0] + : undefined + requestAllTestCasesCalculations(situationIndex ?? null) + } + + if (evaluationByNameArrayChanged) { + shared.evaluationByNameArray = updatedEvaluationByNameArray + } + } </script> <svelte:head> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 095dfc7d0860a00a04acdfb299f9819ecd5f5b00..30eaf8c4f9a5ae2bf3e556f721d4863a92188f54 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -467,9 +467,7 @@ } function changeSituation(situationIndex: number, situation: Situation): void { - const situations = [...shared.testCases] - situations[situationIndex] = situation - shared.testCases = situations + shared.testCases[situationIndex] = situation } function changeTestCasesIndex(testCasesIndex: number[]): void {