Skip to content

Commit

Permalink
Merge pull request #290 from dataforgoodfr/feat/D4G-289-show-most-imp…
Browse files Browse the repository at this point in the history
…actful-actions

feat(SynthesisPage): Implement most impactful actions
  • Loading branch information
Baboo7 authored Jun 26, 2023
2 parents 2d813a3 + 84763cb commit 01abd65
Show file tree
Hide file tree
Showing 17 changed files with 516 additions and 15 deletions.
8 changes: 5 additions & 3 deletions packages/client/src/lib/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const indexArrayBy = <
>(
arr: T[],
key: U
): Record<T[U], T> => {
): Record<KeyUnknownGuard<T[U]>, T> => {
return Object.fromEntries(arr.map((item) => [get(item, key), item]));
};

Expand Down Expand Up @@ -83,10 +83,12 @@ type DeepPathRecursive<

type TemplatableTypes = string | number | bigint | boolean | null | undefined;

type Head<T extends any[]> = T extends [infer THead, ...infer _]
type Head<T extends unknown[]> = T extends [infer THead, ...infer _]
? THead
: never;

type Tail<T extends any[]> = T extends [infer _, ...infer TTail]
type Tail<T extends unknown[]> = T extends [infer _, ...infer TTail]
? TTail
: never;

type KeyUnknownGuard<T> = T extends string | number | symbol ? T : any;
22 changes: 22 additions & 0 deletions packages/client/src/lib/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { userLocale } from "../modules/translations";
export {
formatBudget,
formatCarbonFootprint,
formatConsumption,
formatPercentage,
formatPoints,
formatProduction,
formatProductionGw,
Expand All @@ -26,6 +28,26 @@ function formatProductionGw(value?: number) {
return value?.toFixed(1) || "";
}

function formatConsumption(
value: number,
{ fractionDigits = 0 }: { fractionDigits?: number } = {}
) {
return formatNumber(value, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
});
}

function formatPercentage(
value?: number,
{ fractionDigits = 1 }: { fractionDigits?: number } = {}
) {
return formatNumber(value, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
});
}

function formatProduction({
fractionDigits = 2,
}: { fractionDigits?: number } = {}) {
Expand Down
12 changes: 12 additions & 0 deletions packages/client/src/modules/common/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Box, styled } from "@mui/material";

export { Card };

const Card = styled(Box)(({ theme }) => ({
borderRadius: "10px",
border: "2px solid",
borderColor: "#ffffff",
backgroundColor: theme.palette.primary.main,
color: "#ffffff",
overflow: "hidden",
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Card } from "./Card";
10 changes: 10 additions & 0 deletions packages/client/src/modules/common/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import { SvgIconProps } from "@mui/material";
import PersonPinRounded from "@mui/icons-material/PersonPinRounded";
import {
Badge,
Bolt,
Close,
Computer,
ConnectingAirports,
ContentCopy,
DirectionsCar,
DoNotDisturb,
DryCleaning,
HistoryEdu,
Home,
Expand All @@ -41,10 +43,13 @@ import {
LockOpen,
LunchDining,
Microwave,
North,
PieChart,
Shield,
South,
TipsAndUpdates,
Train,
Whatshot,
} from "@mui/icons-material";

export { Icon };
Expand All @@ -70,19 +75,24 @@ const ICONS = {
consumption: ShoppingCart,
copy: ContentCopy,
draft: HistoryEdu,
energy: Bolt,
"form-draft": HistoryEdu,
"form-pending-validation": SettingsSuggestIcon,
"form-unfilled": Cancel,
"form-validated": CheckCircle,
food: LunchDining,
helper: HelpIcon,
house: Home,
impactful: Whatshot,
information: Info,
"info-card": IconImgFactory({ asset: "info_icon.svg" }),
lock: Lock,
"lock-open": LockOpen,
"mark-circle": Cancel,
microwave: Microwave,
missed: DoNotDisturb,
"number-increase": North,
"number-decrease": South,
"open-in-new-tab": OpenInNew,
plane: ConnectingAirports,
"player-finished": HowToReg,
Expand Down
47 changes: 47 additions & 0 deletions packages/client/src/modules/common/components/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { styled } from "@mui/material";
import { ReactNode } from "react";

export { Tag };

function Tag({
type,
color,
icon,
children,
}: {
type?: "success" | "error" | "secondary";
color?: string;
icon?: ReactNode;
children: ReactNode;
}) {
const className = type || "";

return (
<TagStyled className={className} sx={{ backgroundColor: color }}>
{icon}
{children}
</TagStyled>
);
}

const TagStyled = styled("span")(({ theme }) => {
return {
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
flexGrow: 0,
flexShrink: 0,
padding: theme.spacing(0.5),
borderRadius: "10px",
color: "#ffffff",
"&.success": {
backgroundColor: theme.palette.status.success,
},
"&.error": {
backgroundColor: theme.palette.status.error,
},
"&.secondary": {
backgroundColor: "hsl(0, 50%, 100%)",
},
};
});
1 change: 1 addition & 0 deletions packages/client/src/modules/common/components/Tag/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Tag } from "./Tag";
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from "react";
import { Icon } from "../Icon";
import { Tag } from "../Tag";
import { Typography } from "../Typography";

export { TagNumber };

function TagNumber({
value,
successDirection = "increase",
formatter,
}: {
value: number;
successDirection?: "increase" | "decrease";
formatter: (nb: number) => string;
}) {
const tagType = useMemo(() => {
const directionFactor = successDirection === "increase" ? 1 : -1;
if (value * directionFactor > 0) {
return "success";
}
if (value * directionFactor < 0) {
return "error";
}
return "secondary";
}, [successDirection, value]);

const icon = useMemo(() => {
if (value > 0) {
return (
<Icon name="number-increase" sx={{ width: "1rem", height: "1rem" }} />
);
}
if (value < 0) {
return (
<Icon name="number-decrease" sx={{ width: "1rem", height: "1rem" }} />
);
}
return "•";
}, [value]);

const sign = useMemo(() => {
if (value > 0) {
return "+";
}
if (value < 0) {
return "-";
}
return "";
}, [value]);

return (
<Tag type={tagType} icon={icon}>
<Typography as="span">
{sign}
{formatter(Math.abs(value))}
</Typography>
</Tag>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TagNumber } from "./TagNumber";
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const Typography = styled(TypographyMui)(({ theme }) => ({
"&:is(h6)": {
fontSize: 16,
},
"&:is(p)": {
"&:is(p), &:is(span)": {
marginRight: 0,
fontSize: 14,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useCurrentPlayer";
export * from "./useMostImpactfulActions";
export * from "./usePersona";
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function useCurrentPlayer() {
return {
player,
profile: player.profile,
personalization: player.profile.personalization,
playerActions: player.actions,
actionPointsAvailableAtCurrentStep: STEPS[game.step].availableActionPoints,
teamActions: team.actions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { sumBy } from "lodash";
import { computeNewConsumptionData } from "../../../utils/consumption";
import { sumFor } from "../../../../persona";
import {
ConsumptionDatum,
ConsumptionType,
} from "../../../../persona/consumption";
import { indexArrayBy } from "../../../../../lib/array";
import { fromEntries } from "../../../../../lib/object";
import { useMemo } from "react";
import { Action } from "../../../../../utils/types";
import { useCurrentPlayer } from "./useCurrentPlayer";
import { usePersona } from "./usePersona";
import { usePlay } from "../../playContext";

export { useMostImpactfulActions };
export type { ImpactfulAction };

type ImpactfulAction = {
action: Action;
isPerformed: boolean;
consumptionImpacts: {
type: ConsumptionType;
initial: number;
final: number;
absolute: number;
relative: number;
}[];
};

function useMostImpactfulActions({ limit = 5 }: { limit?: number } = {}) {
const { consumptionActionById } = usePlay();
const { personalization, playerActions } = useCurrentPlayer();
const { personaBySteps } = usePersona();

const mostImpactfulActions = useMemo(() => {
const initialPersona = personaBySteps[0];
const actions = playerActions.map(
(playerAction) => consumptionActionById[playerAction.actionId]
);
const PlayerActionByActionId = indexArrayBy(playerActions, "actionId");

const initialConsumptionByType = computeConsumptionByType(
initialPersona.consumption
);

const mostImpactfulActions: ImpactfulAction[] = actions
.map((action) => {
const consumptionData = computeNewConsumptionData(
[action.name],
personalization
);

return {
action,
consumptionData,
totalConsumptionKwh: sumBy(consumptionData, "value"),
};
})
.sort(
(consoA, consoB) =>
consoA.totalConsumptionKwh - consoB.totalConsumptionKwh
)
.slice(0, limit)
.map(({ action, consumptionData }) => {
const consumptionByType = computeConsumptionByType(consumptionData);
const consumptionImpacts = computeConsumptionDifference(
consumptionByType,
initialConsumptionByType
);

return {
action,
isPerformed: PlayerActionByActionId[action.id].isPerformed,
consumptionImpacts,
};
});

return mostImpactfulActions;
}, [
consumptionActionById,
limit,
personaBySteps,
personalization,
playerActions,
]);

return {
mostImpactfulActions,
};
}

function computeConsumptionByType(
consumptionData: readonly ConsumptionDatum[]
): Record<ConsumptionType, number> {
const consumptionTypes = consumptionData.map((c) => c.type);

const consumptionByType = fromEntries(
consumptionTypes.map((type) => [type, sumFor(consumptionData, type)])
);

return consumptionByType;
}

function computeConsumptionDifference(
consumptionByType: Record<ConsumptionType, number>,
refConsumptionByType: Record<ConsumptionType, number>
): {
type: ConsumptionType;
initial: number;
final: number;
absolute: number;
relative: number;
}[] {
const consumptionTypes = Object.keys(
refConsumptionByType
) as ConsumptionType[];

const consumptionImpacts = consumptionTypes
.map((type) => {
const absoluteDifference =
consumptionByType[type] - refConsumptionByType[type];

return {
type,
initial: refConsumptionByType[type],
final: consumptionByType[type],
absolute: absoluteDifference,
relative: absoluteDifference / refConsumptionByType[type],
};
})
.filter((difference) => difference.absolute !== 0);

return consumptionImpacts;
}
Loading

0 comments on commit 01abd65

Please sign in to comment.