Skip to content

Commit 47b1413

Browse files
committed
feat: ux ui improvements on analysis charts
1 parent 61e4e57 commit 47b1413

17 files changed

Lines changed: 622 additions & 201 deletions

src/components/AnalysisPageSections/MethodologyAccordion.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ const { title, subtitle, content } = Astro.props;
2323
viewBox="0 0 24 24"
2424
stroke-width="1.5"
2525
stroke="currentColor"
26-
class="w-5 h-5"
26+
class="size-6"
2727
>
2828
<path
2929
stroke-linecap="round"
3030
stroke-linejoin="round"
31-
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
31+
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
3232
></path>
3333
</svg>
3434
</div>

src/components/MCDAADashboardPage.astro

Lines changed: 86 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
MCDA_PERSPECTIVES,
2222
resolveMcdaPerspectiveLabel,
2323
} from "../lib/helpers";
24-
import { InfoAlert, InfoCard, RButton } from "./react";
24+
import { FAQAccordion, InfoAlert, InfoCard, RButton } from "./react";
2525
2626
const { perspective, breadcrumbs, backHref } = Astro.props;
2727
const api = new ApiClient(Astro.request);
@@ -38,7 +38,9 @@ if (perspective) {
3838
pageBreadcrumbs.push({
3939
label: resolveMcdaPerspectiveLabel(perspective, MCDA_PERSPECTIVES),
4040
});
41-
const latestRun = await api.getLatestJobRun(`mcda_analysis_${perspective}`);
41+
const latestRun = await api.getLatestJobRun(
42+
`mcda_analysis_qualitative_${perspective}`,
43+
);
4244
4345
if (latestRun) {
4446
formattedDate = formatDateWithTime(latestRun.created_at);
@@ -108,6 +110,39 @@ const howToUseSteps = [
108110
"Examine the PROMETHEE scores, rankings, and GAIA plane visualization to understand trade-offs and recommendations.",
109111
},
110112
];
113+
114+
const methodologyQuestions = [
115+
{
116+
question: "What is PROMETHEE?",
117+
answer: `PROMETHEE (Preference Ranking Organisation METHod for Enrichment
118+
Evaluations) is a Multi-Criteria Decision Analysis (MCDA) method
119+
used to evaluate and rank alternatives when multiple, and sometimes conflicting, criteria must be considered simultaneously.
120+
It handles both quantitative data (e.g. costs, travel times) and qualitative
121+
factors (e.g. environmental impact, social acceptability), making it
122+
well-suited for complex decisions like urban transport policy evaluation.
123+
It also incorporates the diverse perspectives of multiple stakeholders
124+
(citizens, businesses, public authorities) by building preference relations
125+
between alternatives based on how each group values the criteria.`,
126+
},
127+
{
128+
question: "What is GAIA?",
129+
answer: `GAIA (Graphical Analysis for Interactive Assistance) is the
130+
visual, interactive companion to PROMETHEE. It displays evaluation
131+
results graphically on a GAIA plane, mapping both the policy
132+
alternatives and the evaluation criteria together. This visual
133+
representation helps stakeholders (regardless of their technical
134+
background) better understand the analysis, communicate results,
135+
and identify trade-offs and areas where preferences converge or
136+
conflict.`,
137+
},
138+
{
139+
question: "How to read the PROMETHEE-GAIA results?",
140+
answer: `PROMETHEE-GAIA results combine two complementary outputs: PROMETHEE and GAIA.
141+
Together, they allow users to quickly identify the strongest
142+
policy options, understand key trade-offs, and assess the overall
143+
robustness and consensus behind the recommended solutions.`,
144+
},
145+
];
111146
---
112147

113148
<Layout role="visitor" breadcrumbs={pageBreadcrumbs} backHref={backHref}>
@@ -141,76 +176,20 @@ const howToUseSteps = [
141176
>
142177
<!-- How to Use Guide -->
143178
<HowToUseAccordion
144-
title="How to use this tool"
179+
title="How to use this tool ?"
145180
subtitle="3 simple steps to get started"
146181
steps={howToUseSteps}
147182
/>
148183

149184
<!-- Methodology -->
150185
<MethodologyAccordion
151-
title="Understanding the Methodology"
186+
title="Understanding the methodology"
152187
subtitle="Multi-criteria decision analysis approach with PROMETHEE and GAIA visualization"
153188
content=""
154189
>
155-
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-2">
156-
<InfoAlert title="What is PROMETHEE?" variant="question">
157-
<p>
158-
PROMETHEE (Preference Ranking Organisation METHod for Enrichment
159-
Evaluations) is a Multi-Criteria Decision Analysis (MCDA) method
160-
used to evaluate and rank alternatives when multiple, <i
161-
>and sometimes conflicting,</i
162-
> criteria must be considered simultaneously. <br />
163-
It handles both quantitative data (e.g. costs, travel times) and qualitative
164-
factors (e.g. environmental impact, social acceptability), making it
165-
well-suited for complex decisions like urban transport policy evaluation.
166-
It also incorporates the diverse perspectives of multiple stakeholders
167-
(citizens, businesses, public authorities) by building preference relations
168-
between alternatives based on how each group values the criteria.
169-
</p>
170-
</InfoAlert>
171-
<InfoAlert title="What is GAIA?" variant="question">
172-
<p>
173-
GAIA (Graphical Analysis for Interactive Assistance) is the
174-
visual, interactive companion to PROMETHEE. It displays evaluation
175-
results graphically on a GAIA plane, mapping both the policy
176-
alternatives and the evaluation criteria together. This visual
177-
representation helps stakeholders — regardless of their technical
178-
background — better understand the analysis, communicate results,
179-
and identify trade-offs and areas where preferences converge or
180-
conflict.
181-
</p>
182-
</InfoAlert>
183-
<InfoAlert
184-
title="How to read the PROMETHEE-GAIA results?"
185-
className=""
186-
variant="question"
187-
>
188-
<p>
189-
PROMETHEE-GAIA results combine two complementary outputs:
190-
<ul class="list-disc list-inside my-2">
191-
<li class="text-sm">
192-
PROMETHEE provides a ranked order of alternatives, reflecting
193-
how well each performs across all weighted criteria and
194-
stakeholder preferences.
195-
</li>
196-
<li class="text-sm">
197-
The GAIA plane visually maps these alternatives alongside the
198-
criteria, revealing which policies excel in specific areas and
199-
where tensions between different objectives lie.
200-
</li>
201-
</ul>
202-
</p>
203-
<p>
204-
Together, they allow users to quickly identify the strongest
205-
policy options, understand key trade-offs, and assess the overall
206-
robustness and consensus behind the recommended solutions.
207-
</p>
208-
</InfoAlert>
209-
<InfoAlert
210-
title="Pro tip for interpretation"
211-
className="md:col-span-2"
212-
variant="success"
213-
>
190+
<div class="grid grid-cols-1 gap-3 mt-2">
191+
<FAQAccordion questions={methodologyQuestions} client:load />
192+
<InfoAlert title="Pro tip for interpretation" variant="success">
214193
<p>
215194
Compare results across different perspectives to identify measures
216195
that work well for multiple stakeholders.
@@ -270,6 +249,50 @@ const howToUseSteps = [
270249
</div>
271250
</div>
272251
</MethodologyAccordion>
252+
253+
<MethodologyAccordion
254+
title="How to interpret the results ?"
255+
subtitle="Understanding Multi-Criteria Analysis outputs and model accuracy"
256+
content=""
257+
>
258+
<p class="mb-4">
259+
PROMETHEE-GAIA results combine two complementary outputs that work
260+
together to support decision-making. PROMETHEE produces a ranked order
261+
of the transport policy alternatives, reflecting how each city's
262+
interventions perform across all weighted criteria and stakeholder
263+
perspectives. A higher rank indicates a policy that better satisfies
264+
the overall set of criteria as valued by the stakeholders involved.
265+
The GAIA plane then brings a visual dimension to these rankings,
266+
mapping both the policy alternatives and the evaluation criteria onto
267+
a two-dimensional graph. On this plane, criteria pointing in similar
268+
directions indicate aligned objectives, while criteria pointing in
269+
opposite directions reveal trade-offs. Policies positioned close to a
270+
criterion's axis perform well on that specific criterion. A decision
271+
axis summarizes the overall PROMETHEE ranking direction, helping users
272+
quickly identify the most globally preferred alternatives.
273+
</p>
274+
<p class="mb-6">Together, these two outputs allow users to:</p>
275+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
276+
<InfoCard
277+
title="Rank Alternatives"
278+
description="Compare transport policies based on their overall performance across all criteria and stakeholder groups"
279+
showIcon={false}
280+
textAlign="center"
281+
/>
282+
<InfoCard
283+
title="Identify Trade-offs"
284+
description="Spot tensions between competing objectives (e.g. cost reduction vs. environmental impact) that cannot be simultaneously optimized"
285+
showIcon={false}
286+
textAlign="center"
287+
/>
288+
<InfoCard
289+
title="Explore Consensus"
290+
description="Identify areas where different stakeholders' preferences align, supporting more inclusive and robust decision-making"
291+
showIcon={false}
292+
textAlign="center"
293+
/>
294+
</div>
295+
</MethodologyAccordion>
273296
</section>
274297

275298
<!-- Dashboard Section -->

src/components/react/ImpactAnalysis/KpiGroupVariationCharts.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ export const KpiGroupVariationCharts: React.FC<
4545
labs.map((l) => l.id),
4646
);
4747

48+
const normalizeTransportMode = (mode?: string | null): string | null => {
49+
if (!mode) return null;
50+
const trimmed = mode.trim();
51+
return trimmed.length > 0 ? trimmed : null;
52+
};
53+
54+
const buildKpiModeKey = (kpiId: string, mode?: string | null): string => {
55+
const normalizedMode = normalizeTransportMode(mode);
56+
return normalizedMode ? `${kpiId}__${normalizedMode}` : kpiId;
57+
};
58+
4859
const chartsByName = new Map<string, KpiVariationChartData>();
4960
chartsByName.set(`Variation per Lab for all KPIs in ${groupName} group`, {
5061
labs,
@@ -68,39 +79,62 @@ export const KpiGroupVariationCharts: React.FC<
6879
],
6980
});
7081

71-
const uniqueParentKpiIds = new Set<string>();
82+
const chartGroups = new Map<
83+
string,
84+
{
85+
parentName: string;
86+
transportMode: string | null;
87+
kpis: IKpiVariation[];
88+
}
89+
>();
90+
7291
globalKpiVariations.forEach((kpi) => {
7392
const parentId = kpi.kpiParentId ?? kpi.kpiId;
74-
uniqueParentKpiIds.add(parentId);
75-
});
93+
const parentName = kpi.kpiParentName ?? kpi.kpiName ?? "Unnamed KPI";
94+
const transportMode = normalizeTransportMode(kpi.transportModeName);
95+
const groupKey = `${parentId}__${transportMode ?? "no-mode"}`;
7696

77-
uniqueParentKpiIds.forEach((parentKpiId) => {
78-
const kpisForParent = globalKpiVariations.filter((kpi) => {
79-
// const pid = kpi.kpiParentId ?? kpi.kpiId;
80-
return [kpi.kpiParentId, kpi.kpiId].includes(parentKpiId);
81-
});
97+
if (!chartGroups.has(groupKey)) {
98+
chartGroups.set(groupKey, {
99+
parentName,
100+
transportMode,
101+
kpis: [],
102+
});
103+
}
104+
105+
chartGroups.get(groupKey)!.kpis.push(kpi);
106+
});
82107

108+
chartGroups.forEach((group) => {
83109
const kpiMap = new Map<string, IKpiVariation>();
84-
for (const kpi of kpisForParent) {
85-
if (!kpiMap.has(kpi.kpiId)) {
86-
kpiMap.set(kpi.kpiId, kpi);
110+
for (const kpi of group.kpis) {
111+
const kpiModeKey = buildKpiModeKey(kpi.kpiId, kpi.transportModeName);
112+
if (!kpiMap.has(kpiModeKey)) {
113+
kpiMap.set(kpiModeKey, kpi);
87114
}
88115
}
89116

90117
const kpis = Array.from(kpiMap.values())
91118
.map((globalKpi) => {
92-
// Build variation by lab for this KPI
119+
const globalMode = normalizeTransportMode(globalKpi.transportModeName);
120+
121+
// Build variation by lab for this KPI and transport mode
93122
const byLab: Record<string, number> = {};
94123
for (const lab of livingLabVariations) {
95-
const labKpi = lab.kpis.find((k) => k.kpiId === globalKpi.kpiId);
124+
const labKpi = lab.kpis.find(
125+
(k) =>
126+
k.kpiId === globalKpi.kpiId &&
127+
normalizeTransportMode(k.transportModeName) === globalMode,
128+
);
129+
96130
byLab[lab.labId] = toPercent(
97131
labKpi?.ratioVariation,
98132
labKpi?.ratioVariationPercentage,
99133
);
100134
}
101135

102136
return {
103-
id: globalKpi.kpiId,
137+
id: buildKpiModeKey(globalKpi.kpiId, globalKpi.transportModeName),
104138
name: globalKpi.kpiName,
105139
variation: {
106140
global: toPercent(
@@ -113,11 +147,11 @@ export const KpiGroupVariationCharts: React.FC<
113147
})
114148
.sort((a, b) => a.name.localeCompare(b.name));
115149

116-
const parentKpiName =
117-
kpisForParent[0]?.kpiParentName ??
118-
kpisForParent[0]?.kpiName ??
119-
"Unnamed KPI";
120-
chartsByName.set(parentKpiName, { labs, kpis });
150+
const chartTitle = group.transportMode
151+
? `${group.parentName} (${group.transportMode})`
152+
: group.parentName;
153+
154+
chartsByName.set(chartTitle, { labs, kpis });
121155
});
122156

123157
//actions
@@ -169,7 +203,6 @@ export const KpiGroupVariationCharts: React.FC<
169203
color={isSelected ? "primary" : "light"}
170204
className="cursor-pointer border transition-all hover:shadow-md"
171205
onClick={() => toggleLab(lab.id)}
172-
title={lab.name}
173206
>
174207
{displayName}
175208
</Badge>
@@ -187,7 +220,9 @@ export const KpiGroupVariationCharts: React.FC<
187220
data={data}
188221
selectedLabIds={selectedLabIds}
189222
/>
190-
<h3 className="text-center">Variations per KPI in {groupName} group</h3>
223+
<h3 className="text-center">
224+
Variations per KPI in {groupName} group
225+
</h3>
191226
</div>
192227
) : (
193228
<KpiVariationChart

src/components/react/ImpactAnalysis/MeasureImpactCard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const MeasureImpactCard: React.FC<MeasureImpactCardProps> = ({
1717
const isPositive = coefficient >= 0;
1818

1919
// Determine colors based on impact
20-
const colorClasses = isPositive
21-
? "from-secondary/20 to-secondary/40 border-secondary text-secondary"
22-
: "from-danger/20 to-danger/40 border-danger text-danger";
20+
const colorClasses = ""; //isPositive
21+
// ? "from-secondary/20 to-secondary/40 border-secondary text-dark"
22+
// : "from-danger/20 to-danger/40 border-danger text-dark";
2323

2424
const badgeClasses = isPositive
2525
? "bg-secondary text-white"
@@ -42,7 +42,7 @@ export const MeasureImpactCard: React.FC<MeasureImpactCardProps> = ({
4242
</div>
4343

4444
{/* Measure Name */}
45-
<h5 className={`font-bold mb-2 pr-5 leading-tight`}>{measure.name}</h5>
45+
<h6 className={`font-bold mb-2 pr-5 leading-tight`}>{measure.name}</h6>
4646

4747
{/* Coefficient Value */}
4848
<div className="flex items-baseline gap-2 mb-2">

0 commit comments

Comments
 (0)