Skip to content

Commit f051656

Browse files
committed
fix(outcomes): add dismissible tag for filters
1 parent 4ff0ddf commit f051656

5 files changed

Lines changed: 78 additions & 17 deletions

File tree

client/src/boards/outcomes/pages/flux/charts/sankey/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { type OutcomesFluxLink } from "../../../../api";
55
import { createSankeyOptions } from "./options";
66

77
interface SankeyChartProps {
8+
hideTitle?: boolean;
89
links: OutcomesFluxLink[];
910
}
1011

11-
export default function SankeyChart({ links }: SankeyChartProps) {
12+
export default function SankeyChart({ hideTitle, links }: SankeyChartProps) {
1213
const options = useMemo(() => {
1314
if (!links?.length) return null;
1415
return createSankeyOptions(links);
@@ -18,6 +19,7 @@ export default function SankeyChart({ links }: SankeyChartProps) {
1819

1920
return (
2021
<ChartWrapper
22+
hideTitle={hideTitle}
2123
config={{
2224
id: "outcomes-flux-sankey",
2325
title: { fr: "Parcours des néo-bacheliers inscrits en L1 en 2019", look: "h4" as const },

client/src/boards/outcomes/pages/flux/index.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Button, Col, Container, Row, Title } from "@dataesr/dsfr-plus";
2-
import { useMemo } from "react";
1+
import { Button, Col, Container, DismissibleTag, Row, TagGroup, Title } from "@dataesr/dsfr-plus";
2+
import { useMemo, useState } from "react";
33
import { useSearchParams } from "react-router-dom";
44

55
import DefaultSkeleton from "../../../../components/charts-skeletons/default";
6+
import ChartWrapper from "../../../../components/chart-wrapper";
67
import Callout from "../../../../components/callout.tsx";
78
import { type OutcomesFilterField, useOutcomesFlux } from "../../api";
89
import SankeyChart from "./charts/sankey";
@@ -110,21 +111,36 @@ export default function FluxPage() {
110111
const cohortSituation = searchParams.get("cohorte_situation") || DEFAULT_COHORT_SITUATION;
111112

112113
const minValue = Number.parseInt(searchParams.get("min_value") || "", 10) || DEFAULT_MIN_VALUE;
114+
const [sliderValue, setSliderValue] = useState(minValue);
113115
const relativeYears = useMemo(() => {
114116
const raw = searchParams.get("annee_rel");
115117
if (!raw) return ALL_RELATIVE_YEARS;
116118
const parsed = raw.split(",").map(Number).filter((n) => ALL_RELATIVE_YEARS.includes(n));
117119
return parsed.length >= 2 ? parsed : ALL_RELATIVE_YEARS;
118120
}, [searchParams]);
119121

120-
const { data, error, isLoading } = useOutcomesFlux({
122+
const { data, error, isFetching, isLoading } = useOutcomesFlux({
121123
cohorteAnnee: cohortYear,
122124
cohorteSituation: cohortSituation,
123125
filters,
124126
minValue,
125127
relativeYears,
126128
});
127129

130+
const activeFiltersElement = (() => {
131+
const tags = FILTER_SECTIONS.flatMap(s =>
132+
s.fields.filter(f => filters[f.field]).map(f => ({ field: f.field, label: f.label, value: filters[f.field]! }))
133+
);
134+
if (!tags.length) return null;
135+
return (
136+
<TagGroup className="fr-mt-1w fr-mb-1w">
137+
{tags.map(({ field, label, value }) => (
138+
<DismissibleTag key={field} size="sm" onClick={() => updateFilter(field, null)}>{label} : {value}</DismissibleTag>
139+
))}
140+
</TagGroup>
141+
);
142+
})();
143+
128144
const updateFilter = (field: OutcomesFilterField, value: string | null) => {
129145
const nextParams = new URLSearchParams(searchParams);
130146
if (value) {
@@ -193,19 +209,26 @@ export default function FluxPage() {
193209
</Col>
194210
<Col lg={8}>
195211
<div className="outcomes-flux-page__content">
196-
{isLoading && <DefaultSkeleton height="540px" />}
197-
{!isLoading && error && (
212+
<ChartWrapper.Title config={{ id: "outcomes-flux-sankey", title: { fr: "Parcours des néo-bacheliers inscrits en L1 en 2019", look: "h4" as const } }} />
213+
{activeFiltersElement}
214+
{(isLoading || (isFetching && !data)) && <DefaultSkeleton height="540px" />}
215+
{!isLoading && isFetching && data && (
216+
<div style={{ opacity: 0.5, transition: "opacity 0.2s" }}>
217+
<SankeyChart hideTitle links={data.links} />
218+
</div>
219+
)}
220+
{!isLoading && !isFetching && error && (
198221
<Callout colorFamily="pink-macaron" icon="fr-icon-error-warning-line" title="Erreur de chargement">
199222
Impossible de récupérer les flux pour cette cohorte.
200223
</Callout>
201224
)}
202-
{!isLoading && !error && data && !data.links?.length && (
225+
{!isLoading && !isFetching && data && !data.links?.length && (
203226
<Callout title="Aucune transition visible" icon="fr-icon-information-line">
204227
Aucun flux ne dépasse le seuil d'affichage avec les filtres actuellement sélectionnés.
205228
</Callout>
206229
)}
207-
{!isLoading && !error && (data?.links?.length ?? 0) > 0 && (
208-
<SankeyChart links={data!.links} />
230+
{!isLoading && !isFetching && (data?.links?.length ?? 0) > 0 && (
231+
<SankeyChart hideTitle links={data!.links} />
209232
)}
210233

211234
<div className="outcomes-flux-page__params fr-mt-3w fr-mb-3w">
@@ -236,23 +259,24 @@ export default function FluxPage() {
236259
<Col md={6}>
237260
<Title as="h3" look="h6" className="fr-mb-1w">Affichage des flux</Title>
238261
<p className="fr-text--xs fr-mb-1w">
239-
Afficher les flux regroupant au minimum {minValue} étudiants.
262+
Afficher les flux regroupant au minimum {sliderValue} étudiants.
240263
</p>
241264
<div className="fr-range-group">
242265
<div className="outcomes-flux-page__slider-labels">
243266
<span className="fr-text--xs">0</span>
244-
<span className="fr-text--sm fr-text--bold">{minValue}</span>
267+
<span className="fr-text--sm fr-text--bold">{sliderValue}</span>
245268
<span className="fr-text--xs">1 000</span>
246269
</div>
247270
<input
248271
aria-label="Seuil minimum d'étudiants"
249-
className="fr-range"
250272
max={1000}
251273
min={0}
252-
onChange={(e) => updateMinValue(Number(e.target.value))}
274+
onChange={(e) => setSliderValue(Number(e.target.value))}
275+
onMouseUp={() => updateMinValue(sliderValue)}
276+
onTouchEnd={() => updateMinValue(sliderValue)}
253277
step={10}
254278
type="range"
255-
value={minValue}
279+
value={sliderValue}
256280
/>
257281
</div>
258282
</Col>

client/src/boards/outcomes/pages/plus-haut-diplome/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Col, Container, Row, Title } from "@dataesr/dsfr-plus";
1+
import { Button, Col, Container, DismissibleTag, Row, TagGroup, Title } from "@dataesr/dsfr-plus";
22
import { useSearchParams } from "react-router-dom";
33

44
import DefaultSkeleton from "../../../../components/charts-skeletons/default";
@@ -117,6 +117,20 @@ export default function PlusHautDiplomePage() {
117117
filters,
118118
});
119119

120+
const activeFiltersElement = (() => {
121+
const tags = FILTER_SECTIONS.flatMap(s =>
122+
s.fields.filter(f => filters[f.field]).map(f => ({ field: f.field, label: f.label, value: filters[f.field]! }))
123+
);
124+
if (!tags.length) return null;
125+
return (
126+
<TagGroup className="fr-mt-1w fr-mb-1w">
127+
{tags.map(({ field, label, value }) => (
128+
<DismissibleTag key={field} size="sm" onClick={() => updateFilter(field, null)}>{label} : {value}</DismissibleTag>
129+
))}
130+
</TagGroup>
131+
);
132+
})();
133+
120134
const updateFilter = (field: OutcomesFilterField, value: string | null) => {
121135
const nextParams = new URLSearchParams(searchParams);
122136
if (value) {
@@ -164,6 +178,7 @@ export default function PlusHautDiplomePage() {
164178
<Title as="h1" look="h3">
165179
Le plus haut diplôme obtenu atteint en {diplomaYearLabel}
166180
</Title>
181+
{activeFiltersElement}
167182

168183
{isLoading && <DefaultSkeleton height="400px" />}
169184
{!isLoading && error && (

client/src/boards/outcomes/pages/repartition/charts/repartition-column/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import ChartWrapper from "../../../../../../components/chart-wrapper";
44
import { createRepartitionOptions } from "./options";
55

66
interface RepartitionChartProps {
7+
hideTitle?: boolean;
78
distribution: Array<{ annee_rel: number; situation: string; count: number }>;
89
relativeYears: number[];
910
yearLabels: Record<number, string>;
1011
}
1112

12-
export default function RepartitionChart({ distribution, relativeYears, yearLabels }: RepartitionChartProps) {
13+
export default function RepartitionChart({ hideTitle, distribution, relativeYears, yearLabels }: RepartitionChartProps) {
1314
const options = useMemo(() => {
1415
if (!distribution?.length) return null;
1516
return createRepartitionOptions(distribution, relativeYears, yearLabels);
@@ -19,6 +20,7 @@ export default function RepartitionChart({ distribution, relativeYears, yearLabe
1920

2021
return (
2122
<ChartWrapper
23+
hideTitle={hideTitle}
2224
config={{
2325
id: "outcomes-repartition",
2426
title: { fr: "Répartition selon les inscriptions (en %)", look: "h4" as const },

client/src/boards/outcomes/pages/repartition/index.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Button, Col, Container, Row, Title } from "@dataesr/dsfr-plus";
1+
import { Button, Col, Container, DismissibleTag, Row, TagGroup, Title } from "@dataesr/dsfr-plus";
22
import { useMemo } from "react";
33
import { useSearchParams } from "react-router-dom";
44

55
import DefaultSkeleton from "../../../../components/charts-skeletons/default";
6+
import ChartWrapper from "../../../../components/chart-wrapper";
67
import Callout from "../../../../components/callout.tsx";
78
import { type OutcomesFilterField, useOutcomesRepartition } from "../../api";
89
import RepartitionChart from "./charts/repartition-column";
@@ -123,6 +124,20 @@ export default function RepartitionPage() {
123124
relativeYears,
124125
});
125126

127+
const activeFiltersElement = (() => {
128+
const tags = FILTER_SECTIONS.flatMap(s =>
129+
s.fields.filter(f => filters[f.field]).map(f => ({ field: f.field, label: f.label, value: filters[f.field]! }))
130+
);
131+
if (!tags.length) return null;
132+
return (
133+
<TagGroup className="fr-mt-1w fr-mb-1w">
134+
{tags.map(({ field, label, value }) => (
135+
<DismissibleTag key={field} size="sm" onClick={() => updateFilter(field, null)}>{label} : {value}</DismissibleTag>
136+
))}
137+
</TagGroup>
138+
);
139+
})();
140+
126141
const updateFilter = (field: OutcomesFilterField, value: string | null) => {
127142
const nextParams = new URLSearchParams(searchParams);
128143
if (value) {
@@ -180,6 +195,7 @@ export default function RepartitionPage() {
180195
</Col>
181196
<Col lg={8}>
182197
<div className="outcomes-flux-page__content">
198+
<ChartWrapper.Title config={{ id: "outcomes-repartition", title: { fr: "Répartition selon les inscriptions (en %)", look: "h4" as const } }} />
183199
{isLoading && <DefaultSkeleton height="540px" />}
184200
{!isLoading && error && (
185201
<Callout colorFamily="pink-macaron" icon="fr-icon-error-warning-line" title="Erreur de chargement">
@@ -191,8 +207,10 @@ export default function RepartitionPage() {
191207
Aucune donnée disponible avec les filtres actuellement sélectionnés.
192208
</Callout>
193209
)}
210+
{activeFiltersElement}
194211
{!isLoading && !error && (data?.distribution?.length ?? 0) > 0 && (
195212
<RepartitionChart
213+
hideTitle
196214
distribution={data!.distribution}
197215
relativeYears={relativeYears}
198216
yearLabels={YEAR_LABELS}

0 commit comments

Comments
 (0)