Skip to content

Commit

Permalink
Meilleur affichage du graphe + ajout de textes
Browse files Browse the repository at this point in the history
  • Loading branch information
martinratinaud committed Feb 11, 2025
1 parent 33759c0 commit fc300db
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 58 deletions.
176 changes: 127 additions & 49 deletions src/components/ComparateurPublicodes/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { type SimulatorEngine } from './useSimulatorEngine';
import Button from '../ui/Button';
import Notice from '../ui/Notice';

const precisionDisplay = 10 / 100;
const COST_PRECISION = 10;
const CO2_PRECISION = 5;
const costPrecisionPercentage = COST_PRECISION / 100;
const co2PrecisionPercentage = CO2_PRECISION / 100;

type GraphProps = React.HTMLAttributes<HTMLDivElement> & {
engine: SimulatorEngine;
advancedMode?: boolean;
Expand Down Expand Up @@ -143,18 +147,31 @@ const useFixLegendOpacity = (coutsRef?: React.RefObject<HTMLDivElement | null>)
});
};

const formatPrecisionRange = (value: number) => {
// as calculations are approximations, give a +-10% range
const lowerBound = Math.round((value * (1 - precisionDisplay)) / 10) * 10;
const upperBound = Math.round((value * (1 + precisionDisplay)) / 10) * 10;
const getCostPrecisionRange = (value: number) => {
const lowerBound = Math.round((value * (1 - costPrecisionPercentage)) / 10) * 10;
const upperBound = Math.round((value * (1 + costPrecisionPercentage)) / 10) * 10;

const lowerBoundString = lowerBound.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
const upperBoundString = upperBound.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
return { lowerBound, upperBound, lowerBoundString, upperBoundString };
};

const lowerBoundStr = lowerBound.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
const upperBoundStr = upperBound.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
return `${lowerBoundStr} - ${upperBoundStr}`;
const formatCostPrecisionRange = (value: number) => {
const { lowerBoundString, upperBoundString } = getCostPrecisionRange(value);
return `${lowerBoundString} - ${upperBoundString}`;
};

const formatEmissionsCO2 = (value: number) =>
`${(Math.round(value / 10) * 10).toLocaleString('fr-FR', { maximumFractionDigits: 0 })} kgCO2e`;
const getEmissionsCO2PrecisionRange = (value: number) => {
const lowerBound = Math.round((value * (1 - co2PrecisionPercentage)) / 10) * 10;
const upperBound = Math.round((value * (1 + co2PrecisionPercentage)) / 10) * 10;

const lowerBoundString = formatEmissionsCO2(lowerBound, ''); // no suffix as it takes too much space
const upperBoundString = formatEmissionsCO2(upperBound, '');
return { lowerBound, lowerBoundString, upperBound, upperBoundString };
};

const formatEmissionsCO2 = (value: number, suffix = 'kgCO2e') =>
[`${(Math.round(value / 10) * 10).toLocaleString('fr-FR', { maximumFractionDigits: 0 })}`, suffix].filter(Boolean).join(' ');

const formatCost = (value: number) =>
`${(Math.round(value / 10) * 10).toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 })}`;
Expand Down Expand Up @@ -224,69 +241,69 @@ const Graph: React.FC<GraphProps> = ({ advancedMode, engine, className, captureI
title: `P1 abonnement${typeInstallation.label === 'Réseau de chaleur' ? ' (R2 du réseau de chaleur)' : ''}`,
amount: amountP1Abo,
color: colorP1Abo,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: `P1 consommation${typeInstallation.label === 'Réseau de chaleur' ? ' (R1 du réseau de chaleur)' : ''}`,
amount: amountP1Conso,
color: colorP1Conso,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({ title: 'P1 ECS', amount: amountP1ECS, color: colorP1ECS, valueFormatter: formatPrecisionRange }),
...getRow({ title: "P1'", amount: amountP1prime, color: colorP1prime, valueFormatter: formatPrecisionRange }),
...getRow({ title: 'P1 ECS', amount: amountP1ECS, color: colorP1ECS, valueFormatter: formatCostPrecisionRange }),
...getRow({ title: "P1'", amount: amountP1prime, color: colorP1prime, valueFormatter: formatCostPrecisionRange }),
...getRow({
title: 'P1 consommation froid',
amount: amountP1Consofroid,
color: colorP1Consofroid,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({ title: 'P2', amount: amountP2, color: colorP2, valueFormatter: formatPrecisionRange }),
...getRow({ title: 'P3', amount: amountP3, color: colorP3, valueFormatter: formatPrecisionRange }),
...getRow({ title: 'P2', amount: amountP2, color: colorP2, valueFormatter: formatCostPrecisionRange }),
...getRow({ title: 'P3', amount: amountP3, color: colorP3, valueFormatter: formatCostPrecisionRange }),
...getRow({
title: 'P4 moins aides',
amount: amountP4SansAides,
color: colorP4SansAides,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: tooltipAides,
amount: amountAides,
color: colorP4Aides,
bordered: true,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
]
: [
...getRow({
title: `Abonnement${typeInstallation.label === 'Réseau de chaleur' ? ' (R2 du réseau de chaleur)' : ''}`,
amount: amountP1Abo,
color: colorP1Abo,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: `Consommation${typeInstallation.label === 'Réseau de chaleur' ? ' (R1 du réseau de chaleur)' : ''}`,
amount: amountP1Conso + amountP1ECS,
color: colorP1Conso,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: 'Maintenance',
amount: amountP1prime + amountP2 + amountP3,
color: colorP1prime,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: 'Investissement',
amount: amountP4SansAides,
color: colorP4SansAides,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
...getRow({
title: tooltipAides,
amount: amountAides,
color: colorP4Aides,
bordered: true,
valueFormatter: formatPrecisionRange,
valueFormatter: formatCostPrecisionRange,
}),
];

Expand All @@ -295,7 +312,7 @@ const Graph: React.FC<GraphProps> = ({ advancedMode, engine, className, captureI
0
);
const totalAmount = totalAmountWithAides - amountAides;
const precisionRange = formatPrecisionRange(totalAmount);
const precisionRange = formatCostPrecisionRange(totalAmount);
maxCoutValue = Math.max(maxCoutValue, totalAmount);
totalCoutsEtEmissions[index] = [getLabel(typeInstallation), totalAmount, -1];
return [
Expand Down Expand Up @@ -374,19 +391,27 @@ const Graph: React.FC<GraphProps> = ({ advancedMode, engine, className, captureI

const chartHeight = modesDeChauffageFiltres.length * estimatedRowHeightPx + estimatedBaseGraphHeightPx;

const maxExistingEmissionsCO2Value = totalCoutsEtEmissions.reduce((acc, [, , co2]) => Math.max(acc, co2), 0);
const maxExistingCostValue = totalCoutsEtEmissions.reduce((acc, [, cost]) => Math.max(acc, cost), 0);
const scaleTickEmissionsCO2 = maxExistingEmissionsCO2Value > 10000 ? 10000 : 500;
const maxExistingEmissionsCO2Value =
totalCoutsEtEmissions.reduce((acc, [, , co2]) => Math.max(acc, co2), 0) * (1 + co2PrecisionPercentage);
const maxExistingCostValue = totalCoutsEtEmissions.reduce((acc, [, cost]) => Math.max(acc, cost), 0) * (1 + costPrecisionPercentage);
const scaleTickCost = 500;
const scaleEmissionsCO2Value = Math.ceil(maxExistingEmissionsCO2Value / scaleTickEmissionsCO2) * scaleTickEmissionsCO2;
const scaleCostMaxValue = Math.ceil(maxExistingCostValue / scaleTickCost) * scaleTickCost;
const gridValueEmissionsCO2 = (scaleTickEmissionsCO2 / scaleEmissionsCO2Value) * 100;
const gridValueCost = (scaleTickCost / scaleCostMaxValue) * 100;
const scaleTickEmissionsCO2 = maxExistingEmissionsCO2Value > 10000 ? 10000 : 500;
const scaleEmissionsCO2maxValue = Math.ceil(maxExistingEmissionsCO2Value / scaleTickEmissionsCO2) * scaleTickEmissionsCO2;

const getGrid = (value: number) => {
if (value % 500 === 0 && value <= 3000) return 100 / (value / 500);
if (value % 1000 === 0 && value <= 10000) return 100 / (value / 1000);
if (value % 10000 === 0 && value > 10000) return 100 / (value / 10000);
return 50;
};

const titleItems = ['chauffage', inclusClimatisation && 'froid', inclusECS && 'ECS'].filter(Boolean);
const titleItemsString =
titleItems.length > 1 ? titleItems.slice(0, -1).join(', ') + ' et ' + titleItems[titleItems.length - 1] : titleItems[0] || '';

let graphSectionTitle = '';

return (
<>
<Box textAlign="right" my="4w">
Expand Down Expand Up @@ -428,50 +453,102 @@ const Graph: React.FC<GraphProps> = ({ advancedMode, engine, className, captureI
<div className="relative py-2">
<div className="absolute inset-0 -z-10 flex h-full w-full [&>*]:flex-1">
<div
className="mx-12"
style={{
backgroundImage: `repeating-linear-gradient(to right,#EEE 0,#EEE 1px,transparent 1px,transparent ${getGrid(scaleEmissionsCO2maxValue)}%)`,
// Goal here is to give a grid that is relevent for a user
// when % is infinite (16.666666% for example), grid might appear inaccurate and we rather display only one understandable line instead
backgroundImage: `repeating-linear-gradient(to right,#EEE 0,#EEE 1px,transparent 1px,transparent ${gridValueEmissionsCO2 % 1 === 0 ? gridValueEmissionsCO2 : '50'}%)`,
borderRight: '1px solid #EEE',
}}
></div>
<div
className="mx-12"
style={{
backgroundImage: `repeating-linear-gradient(to right,#EEE 0,#EEE 1px,transparent 1px,transparent ${gridValueCost % 1 === 0 ? gridValueCost : '50'}%)`,
backgroundImage: `repeating-linear-gradient(to right,#EEE 0,#EEE 1px,transparent 1px,transparent ${getGrid(scaleCostMaxValue)}%)`,
borderRight: '1px solid #EEE',
}}
></div>
</div>
<div className="flex justify-between text-sm font-bold text-faded">
<span>{formatEmissionsCO2(scaleEmissionsCO2Value)}</span>
<span>{formatCost(scaleCostMaxValue)}</span>
<div className="flex-1 px-1 mr-12 flex items-center justify-between">
<span>{formatEmissionsCO2(scaleEmissionsCO2maxValue)}</span>
<span>{formatEmissionsCO2(0)}</span>
</div>
<div className="flex-1 px-1 ml-12 flex items-center justify-between">
<span>{formatCost(0)}</span>
<span>{formatCost(scaleCostMaxValue)}</span>
</div>
</div>
{totalCoutsEtEmissions.map(([name, cost, co2]) => {
const co2Percent = Math.round((co2 / scaleEmissionsCO2Value) * 100);
const maxCostPercent = Math.round(((cost * 1.1) / scaleCostMaxValue) * 100);
const {
lowerBound: co2LowerBound,
upperBound: co2UpperBound,
lowerBoundString: co2LowerBoundString,
upperBoundString: co2UpperBoundString,
} = getEmissionsCO2PrecisionRange(co2);
const co2LowerPercent = Math.max(0, Math.round((co2LowerBound / scaleEmissionsCO2maxValue) * 100));
const co2UpperPercent = Math.min(100, Math.round((co2UpperBound / scaleEmissionsCO2maxValue) * 100));
const co2Width = co2UpperPercent - co2LowerPercent;

const {
lowerBound: costLowerBound,
upperBound: costUpperBound,
lowerBoundString,
upperBoundString,
} = getCostPrecisionRange(cost);
const costLowerPercent = Math.max(0, Math.round((costLowerBound / scaleCostMaxValue) * 100));
const costUpperPercent = Math.min(100, Math.round((costUpperBound / scaleCostMaxValue) * 100));
const costWidth = costUpperPercent - costLowerPercent;
const graphSectionType: string = name.includes(' individuel') ? 'Chauffage individuel' : 'Chauffage collectif';

let showSectionTitle = false;
if (graphSectionTitle !== (graphSectionType as string)) {
showSectionTitle = true;
graphSectionTitle = graphSectionType;
}

return (
<>
{showSectionTitle && (
<div className="relative mb-1 mt-12 text-center text-xl font-bold bg-white/50">{graphSectionTitle}</div>
)}
<div key={name} className="relative mb-1 mt-2 flex items-center justify-center text-base font-bold">
<span className="bg-white">{name}</span>
</div>
<div className="stretch flex items-center">
<div className="flex flex-1 border-r border-solid border-white bg-fcu-orange">
<div className="bg-white/80" style={{ flex: 100 - co2Percent }}></div>
<div className="group stretch flex items-center">
<div className="px-12 flex flex-1 border-r border-solid border-white">
<div
className="relative bg-fcu-orange-light/10 whitespace-nowrap py-0.5 tracking-tighter text-left font-extrabold text-fcu-orange-light sm:text-xs md:text-sm flex items-center justify-end"
style={{ flex: 100 - co2UpperPercent }}
>
<span className="pr-0.5 absolute right-[12px]">{co2UpperBoundString}</span>
<div className="border-solid border-l-fcu-orange-light border-l-[12px] border-y-transparent border-y-[5px] my-1 border-r-0"></div>
</div>
<div className="relative bg-fcu-orange-light" style={{ flex: co2Width }}></div>
<div
className="relative overflow-hidden whitespace-nowrap px-2 py-0.5 font-extrabold text-white hover:overflow-visible sm:text-xs md:text-sm"
style={{ flex: co2Percent }}
className="relative bg-fcu-orange-light/30 whitespace-nowrap tracking-tighter py-0.5 text-right font-extrabold text-fcu-orange-light sm:text-xs md:text-sm flex items-center justify-start"
style={{ flex: co2LowerPercent }}
>
{formatEmissionsCO2(co2)}
<div className="border-solid border-r-fcu-orange-light border-r-[12px] border-y-transparent border-y-[5px] my-1 border-l-0"></div>
<span className="absolute left-[12px] pl-0.5">{co2LowerBoundString}</span>
</div>
</div>
<div className="flex flex-1 border-l border-solid border-white bg-fcu-blue">
<div className="px-12 flex flex-1 border-l border-solid border-white">
<div
className="relative bg-fcu-purple/30 whitespace-nowrap tracking-tighter py-0.5 text-right font-extrabold text-fcu-purple sm:text-xs md:text-sm flex items-center justify-end"
style={{ flex: costLowerPercent }}
>
<span className="pr-0.5 absolute right-[12px]">{lowerBoundString}</span>
<div className="border-solid border-l-fcu-purple border-l-[12px] border-y-transparent border-y-[5px] my-1 border-r-0"></div>
</div>
<div className="relative bg-fcu-purple" style={{ flex: costWidth }}></div>
<div
className="relative overflow-hidden whitespace-nowrap px-2 py-0.5 text-right font-extrabold text-white hover:overflow-visible sm:text-xs md:text-sm"
style={{ flex: maxCostPercent }}
className="relative bg-fcu-purple/10 whitespace-nowrap py-0.5 tracking-tighter text-left font-extrabold text-fcu-purple sm:text-xs md:text-sm flex items-center justify-start"
style={{ flex: 100 - costUpperPercent }}
>
<div className="absolute right-0 top-0 h-full w-[20%] bg-white/40"></div>
{formatPrecisionRange(cost)}
<div className="border-solid border-r-fcu-purple border-r-[12px] border-y-transparent border-y-[5px] my-1 border-l-0"></div>
<span className="pl-0.5 absolute left-[12px]">{upperBoundString}</span>
</div>
<div className="bg-white/80" style={{ flex: 100 - maxCostPercent }}></div>
</div>
</div>
</>
Expand Down Expand Up @@ -515,6 +592,7 @@ const Graph: React.FC<GraphProps> = ({ advancedMode, engine, className, captureI
<DisclaimerButton className="!mb-5" />
{typeDeBatiment === 'résidentiel' && (
<SegmentedControl
className="!mt-2"
hideLegend
small
segments={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const ModesDeChauffageAComparerForm: React.FC<ModesDeChauffageAComparerFormProps
return (
<div {...props}>
<p className="fr-text--sm">Sélectionnez les modes de chauffage et de refroidissement que vous souhaitez comparer.</p>
<DisclaimerButton />
{
// in advanced mode, fields are shown at the previous step to be able to fine tune its info
!advancedMode && (
Expand All @@ -56,6 +55,7 @@ const ModesDeChauffageAComparerForm: React.FC<ModesDeChauffageAComparerFormProps
}
{/* This is because the Text component has a weird 0 bottom border */}
<div className="fr-mt-4w" />
<DisclaimerButton className="!mb-5" />
<Heading as="h3" size="h6">
Chauffage Collectif
</Heading>
Expand Down
Loading

0 comments on commit fc300db

Please sign in to comment.