|
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"; |
3 | 3 | import { useSearchParams } from "react-router-dom"; |
4 | 4 |
|
5 | 5 | import DefaultSkeleton from "../../../../components/charts-skeletons/default"; |
| 6 | +import ChartWrapper from "../../../../components/chart-wrapper"; |
6 | 7 | import Callout from "../../../../components/callout.tsx"; |
7 | 8 | import { type OutcomesFilterField, useOutcomesFlux } from "../../api"; |
8 | 9 | import SankeyChart from "./charts/sankey"; |
@@ -110,21 +111,36 @@ export default function FluxPage() { |
110 | 111 | const cohortSituation = searchParams.get("cohorte_situation") || DEFAULT_COHORT_SITUATION; |
111 | 112 |
|
112 | 113 | const minValue = Number.parseInt(searchParams.get("min_value") || "", 10) || DEFAULT_MIN_VALUE; |
| 114 | + const [sliderValue, setSliderValue] = useState(minValue); |
113 | 115 | const relativeYears = useMemo(() => { |
114 | 116 | const raw = searchParams.get("annee_rel"); |
115 | 117 | if (!raw) return ALL_RELATIVE_YEARS; |
116 | 118 | const parsed = raw.split(",").map(Number).filter((n) => ALL_RELATIVE_YEARS.includes(n)); |
117 | 119 | return parsed.length >= 2 ? parsed : ALL_RELATIVE_YEARS; |
118 | 120 | }, [searchParams]); |
119 | 121 |
|
120 | | - const { data, error, isLoading } = useOutcomesFlux({ |
| 122 | + const { data, error, isFetching, isLoading } = useOutcomesFlux({ |
121 | 123 | cohorteAnnee: cohortYear, |
122 | 124 | cohorteSituation: cohortSituation, |
123 | 125 | filters, |
124 | 126 | minValue, |
125 | 127 | relativeYears, |
126 | 128 | }); |
127 | 129 |
|
| 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 | + |
128 | 144 | const updateFilter = (field: OutcomesFilterField, value: string | null) => { |
129 | 145 | const nextParams = new URLSearchParams(searchParams); |
130 | 146 | if (value) { |
@@ -193,19 +209,26 @@ export default function FluxPage() { |
193 | 209 | </Col> |
194 | 210 | <Col lg={8}> |
195 | 211 | <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 && ( |
198 | 221 | <Callout colorFamily="pink-macaron" icon="fr-icon-error-warning-line" title="Erreur de chargement"> |
199 | 222 | Impossible de récupérer les flux pour cette cohorte. |
200 | 223 | </Callout> |
201 | 224 | )} |
202 | | - {!isLoading && !error && data && !data.links?.length && ( |
| 225 | + {!isLoading && !isFetching && data && !data.links?.length && ( |
203 | 226 | <Callout title="Aucune transition visible" icon="fr-icon-information-line"> |
204 | 227 | Aucun flux ne dépasse le seuil d'affichage avec les filtres actuellement sélectionnés. |
205 | 228 | </Callout> |
206 | 229 | )} |
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} /> |
209 | 232 | )} |
210 | 233 |
|
211 | 234 | <div className="outcomes-flux-page__params fr-mt-3w fr-mb-3w"> |
@@ -236,23 +259,24 @@ export default function FluxPage() { |
236 | 259 | <Col md={6}> |
237 | 260 | <Title as="h3" look="h6" className="fr-mb-1w">Affichage des flux</Title> |
238 | 261 | <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. |
240 | 263 | </p> |
241 | 264 | <div className="fr-range-group"> |
242 | 265 | <div className="outcomes-flux-page__slider-labels"> |
243 | 266 | <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> |
245 | 268 | <span className="fr-text--xs">1 000</span> |
246 | 269 | </div> |
247 | 270 | <input |
248 | 271 | aria-label="Seuil minimum d'étudiants" |
249 | | - className="fr-range" |
250 | 272 | max={1000} |
251 | 273 | 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)} |
253 | 277 | step={10} |
254 | 278 | type="range" |
255 | | - value={minValue} |
| 279 | + value={sliderValue} |
256 | 280 | /> |
257 | 281 | </div> |
258 | 282 | </Col> |
|
0 commit comments