Skip to content

Commit 9386362

Browse files
authored
Merge pull request #1077 from MTES-MCT/feature/data-calculation-details
Ajout du détail de calcul aux charts
2 parents 7e506ff + 7847e71 commit 9386362

File tree

9 files changed

+230
-116
lines changed

9 files changed

+230
-116
lines changed

assets/scripts/components/charts/ChartDataSource.tsx

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import React from 'react';
22
import styled from 'styled-components';
33

4-
const TagGroup = styled.div`
4+
const Container = styled.div<{ $displayMode: 'tag' | 'text' }>`
5+
display: flex;
6+
align-items: ${props => props.$displayMode === 'tag' ? 'center' : 'flex-start'};
7+
flex-direction: ${props => props.$displayMode === 'tag' ? 'row' : 'column'};
8+
gap: ${props => props.$displayMode === 'tag' ? '0' : '0.3rem'};
9+
`;
10+
11+
const SourceLabel = styled.div<{ $displayMode: 'tag' | 'text' }>`
12+
margin: 0;
13+
font-size: ${props => props.$displayMode === 'tag' ? '0.75rem' : ''};
14+
`;
15+
16+
const TagsContainer = styled.div`
517
display: flex;
618
align-items: center;
19+
`;
20+
21+
const TextContainer = styled.div`
22+
display: flex;
23+
flex-direction: column;
724
gap: 0.5rem;
825
`;
926

@@ -40,31 +57,40 @@ const SOURCES_DETAILS: Record<string, { label: string; html: string }> = {
4057

4158
interface ChartDataSourceProps {
4259
sources: (keyof typeof SOURCES_DETAILS)[];
43-
chartId: string;
60+
displayMode?: 'tag' | 'text';
4461
}
4562

46-
const ChartDataSource: React.FC<ChartDataSourceProps> = ({ sources, chartId }) => {
63+
const ChartDataSource: React.FC<ChartDataSourceProps> = ({ sources, displayMode = 'tag' }) => {
4764
if (!sources || sources.length === 0) return null;
4865
return (
49-
<div style={{ display: 'flex', alignItems: 'center' }}>
50-
<span className="fr-text--xs fr-mb-0 fr-mr-1w">Source de données :</span>
51-
<TagGroup>
52-
{sources.map((src) => {
53-
const source = SOURCES_DETAILS[src];
54-
if (!source) return null;
55-
const tooltipId = `tooltip-${chartId}-${src}`;
56-
const tagId = `tag-${chartId}-${src}`;
57-
return (
58-
<React.Fragment key={src}>
59-
<span className="fr-tag fr-tag--sm fr-tag--blue fr-text--bold" role="button" id={tagId} aria-describedby={tooltipId} tabIndex={0}>
60-
{source.label}
66+
<Container $displayMode={displayMode}>
67+
<SourceLabel as={displayMode === 'text' ? 'h6' : 'span'} $displayMode={displayMode}>
68+
Source de données :
69+
</SourceLabel>
70+
{displayMode === 'text' ? (
71+
<TextContainer>
72+
{sources.map((src) => {
73+
const source = SOURCES_DETAILS[src];
74+
if (!source) return null;
75+
return (
76+
<p key={src} className="fr-text--xs fr-mb-0" dangerouslySetInnerHTML={{ __html: source.html }} />
77+
);
78+
})}
79+
</TextContainer>
80+
) : (
81+
<TagsContainer>
82+
{sources.map((src) => {
83+
const source = SOURCES_DETAILS[src];
84+
if (!source) return null;
85+
return (
86+
<span key={src} className="fr-tag fr-tag--sm fr-tag--blue fr-text--bold fr-ml-1w">
87+
{source.label}
6188
</span>
62-
<span className="fr-tooltip fr-placement" id={tooltipId} role="tooltip" aria-hidden="true" dangerouslySetInnerHTML={{ __html: source.html }} />
63-
</React.Fragment>
64-
);
65-
})}
66-
</TagGroup>
67-
</div>
89+
);
90+
})}
91+
</TagsContainer>
92+
)}
93+
</Container>
6894
);
6995
};
7096

assets/scripts/components/charts/ChartDataTable.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import React from 'react';
22
import { formatNumber } from "@utils/formatUtils";
3-
import { ChartDataTableProps } from 'scripts/types/chart';
3+
interface ChartData {
4+
headers: string[];
5+
rows: Array<{
6+
name: string;
7+
data: any[];
8+
}>;
9+
}
410

5-
const ChartDataTable: React.FC<ChartDataTableProps> = ({ data }) => {
11+
interface ChartDataTableProps {
12+
data: ChartData;
13+
title: string;
14+
}
15+
16+
const ChartDataTable: React.FC<ChartDataTableProps> = ({ data, title }) => {
617
if (!data || !data.headers || !data.rows) return null;
718

819
const { headers, rows } = data;
920
return (
10-
<div className="fr-table--sm fr-table fr-table--bordered fr-mb-0">
21+
<div className="fr-table--sm fr-table fr-table--bordered fr-mb-0 fr-mt-0">
1122
<div className="fr-table__wrapper">
1223
<div className="fr-table__container">
1324
<div className="fr-table__content">
1425
<table>
26+
<caption>{title}</caption>
1527
<thead>
1628
<tr>
1729
{headers.map((header: string, index: number) => (
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { useState } from 'react';
2+
import styled from 'styled-components';
3+
import ChartDataSource from './ChartDataSource';
4+
import ChartDataTable from './ChartDataTable';
5+
6+
const Container = styled.div`
7+
margin-top: 1rem;
8+
border: 1px solid #EEF2F7;
9+
`;
10+
11+
const Header = styled.div`
12+
display: flex;
13+
justify-content: space-between;
14+
align-items: center;
15+
padding: 1rem;
16+
`;
17+
18+
const DataContainer = styled.div<{ $isVisible: boolean }>`
19+
max-height: ${props => props.$isVisible ? 'auto' : '0'};
20+
overflow: hidden;
21+
transition: max-height 0.3s ease;
22+
padding: ${props => props.$isVisible ? '1rem' : '0'};
23+
visibility: ${props => props.$isVisible ? 'visible' : 'hidden'};
24+
display: flex;
25+
flex-direction: column;
26+
gap: 1rem;
27+
`;
28+
29+
type ChartDetailsProps = {
30+
sources: string[];
31+
showDataTable?: boolean;
32+
children?: React.ReactNode;
33+
chartId: string;
34+
dataTable?: {
35+
headers: string[];
36+
rows: any[];
37+
};
38+
chartTitle?: string;
39+
}
40+
41+
const ChartDetails: React.FC<ChartDetailsProps> = ({
42+
sources,
43+
showDataTable = false,
44+
children,
45+
chartId,
46+
dataTable,
47+
chartTitle
48+
}) => {
49+
const [isVisible, setIsVisible] = useState(false);
50+
51+
if (!(sources.length > 0 || showDataTable)) {
52+
return null;
53+
}
54+
55+
return (
56+
<Container>
57+
<Header>
58+
{sources.length > 0 && (
59+
<ChartDataSource
60+
sources={sources}
61+
displayMode="tag"
62+
/>
63+
)}
64+
{(sources.length > 0 || showDataTable) && (
65+
<button
66+
className="fr-btn fr-btn--tertiary fr-btn--sm fr-btn--icon-right fr-icon-arrow-down-s-fill"
67+
onClick={() => setIsVisible(!isVisible)}
68+
title={isVisible ? "Masquer les données" : "Afficher les données"}
69+
aria-expanded={isVisible}
70+
aria-controls={`${chartId}-details`}
71+
>
72+
Détails données et calcul
73+
</button>
74+
)}
75+
</Header>
76+
{(sources.length > 0 || showDataTable) && (
77+
<DataContainer
78+
$isVisible={isVisible}
79+
id={`${chartId}-details`}
80+
role="region"
81+
aria-label="Détails des données et calculs"
82+
aria-hidden={!isVisible}
83+
>
84+
{sources.length > 0 && (
85+
<ChartDataSource
86+
sources={sources}
87+
displayMode="text"
88+
/>
89+
)}
90+
{children}
91+
{showDataTable && dataTable && (
92+
<ChartDataTable data={dataTable} title={chartTitle} />
93+
)}
94+
</DataContainer>
95+
)}
96+
</Container>
97+
);
98+
};
99+
100+
export default ChartDetails;

assets/scripts/components/charts/GenericChart.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Exporting from 'highcharts/modules/exporting';
1010
import Fullscreen from 'highcharts/modules/full-screen';
1111

1212
import Loader from '@components/ui/Loader';
13-
import ChartDataWrapper from '@components/ui/ChartDataWrapper';
13+
import ChartDetails from './ChartDetails';
1414

1515
// Initialize the modules
1616
HighchartsHeatmap(Highcharts);
@@ -29,6 +29,8 @@ type GenericChartProps = {
2929
error?: any;
3030
showToolbar?: boolean;
3131
sources?: string[]; // ['insee', 'majic', 'gpu', 'lovac', 'ocsge', 'rpls', 'sitadel']
32+
children?: React.ReactNode;
33+
showDataTable?: boolean;
3234
}
3335

3436
const LoaderContainer = styled.div`
@@ -46,7 +48,9 @@ const GenericChart = ({
4648
isLoading = false,
4749
error = null,
4850
showToolbar = true,
49-
sources = []
51+
sources = [],
52+
children,
53+
showDataTable = false
5054
} : GenericChartProps) => {
5155
const chartRef = useRef<any>(null);
5256

@@ -81,7 +85,7 @@ const GenericChart = ({
8185
*/
8286
const mutableChartOptions = JSON.parse(JSON.stringify(chartOptions.highcharts_options || {}))
8387

84-
// Génère un ID basé sur le titre du graphique
88+
// Génère un ID basé sur le titre du graphique (utilisé pour l'accessibilité des dataTable)
8589
const chartId = `chart-${String(mutableChartOptions.title?.text || '')
8690
.toLowerCase()
8791
.replace(/[^a-z0-9]+/g, '-')
@@ -95,6 +99,11 @@ const GenericChart = ({
9599
style: { height: "400px", width: "100%", marginBottom: "2rem" }
96100
};
97101

102+
const dataTable = showDataTable ? {
103+
headers: chartOptions.data_table?.headers,
104+
rows: chartOptions.data_table?.rows
105+
} : undefined;
106+
98107
return (
99108
<div>
100109
{showToolbar && (
@@ -119,11 +128,15 @@ const GenericChart = ({
119128
containerProps={{ ...defaultContainerProps, ...containerProps }}
120129
constructorType={isMap ? 'mapChart' : 'chart'}
121130
/>
122-
<ChartDataWrapper
131+
<ChartDetails
123132
sources={sources}
124-
data={chartOptions.data_table}
133+
showDataTable={showDataTable}
125134
chartId={chartId}
126-
/>
135+
dataTable={dataTable}
136+
chartTitle={mutableChartOptions.title?.text}
137+
>
138+
{children}
139+
</ChartDetails>
127140
</div>
128141
)
129142
}

assets/scripts/components/charts/ocsge/OcsgeGraph.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ type OcsgeGraphProps = {
1111
params?: object;
1212
containerProps?: object;
1313
sources?: string[];
14+
children?: React.ReactNode;
15+
showDataTable?: boolean;
1416
};
1517

1618
export const OcsgeGraph = ({
@@ -20,18 +22,23 @@ export const OcsgeGraph = ({
2022
isMap = false,
2123
params,
2224
containerProps,
23-
sources = []
25+
sources = [],
26+
children,
27+
showDataTable = false
2428
} : OcsgeGraphProps) => {
2529
const { data, isLoading, error } = useGetChartConfigQuery({ id, land_id, land_type, ...params })
2630

2731
return (
2832
<GenericChart
2933
isMap={isMap}
30-
chartOptions={data}
34+
chartOptions={data}
3135
containerProps={containerProps}
3236
isLoading={isLoading}
3337
error={error}
3438
sources={sources}
35-
/>
39+
showDataTable={showDataTable}
40+
>
41+
{children}
42+
</GenericChart>
3643
)
3744
}

assets/scripts/components/features/ocsge/ArtificialisationZonage.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { formatNumber } from "@utils/formatUtils";
33
import { ZonageType } from "scripts/types/ZonageType";
4-
import ChartDataWrapper from "@components/ui/ChartDataWrapper";
4+
import ChartDetails from "@components/charts/ChartDetails";
55
import { MillesimeDisplay } from "@components/features/ocsge/MillesimeDisplay";
66
import { LandArtifStockIndex } from "@services/types/landartifstockindex";
77

@@ -91,7 +91,12 @@ export const ArtificialisationZonage: React.FC<ArtificialisationZonageProps> = (
9191
</div>
9292
</div>
9393
</div>
94-
<ChartDataWrapper sources={['ocsge', 'gpu']} chartId="artificialisation-zonage-tableau" />
94+
<ChartDetails sources={['ocsge', 'gpu']} chartId="artificialisation-zonage-tableau">
95+
<div>
96+
<h6 className="fr-mb-0">Calcul</h6>
97+
<p className="fr-text--sm">Qualifier l'artificialisation de chaque parcelle OCS GE via la matrice d'artficialisation. Puis comparer la surface totale des parcelles artificialisées dans chaque zonage d'urbanisme à la surface de la zone pour connaître le taux d'occupation.</p>
98+
</div>
99+
</ChartDetails>
95100
</div>
96101
</div>
97102
);

0 commit comments

Comments
 (0)