Skip to content

Commit 8409be1

Browse files
committed
feat: WIP charts
1 parent 39b3193 commit 8409be1

File tree

16 files changed

+1435
-9
lines changed

16 files changed

+1435
-9
lines changed

assets/@types/stats.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export interface StatsRequestDto {
2+
start: string;
3+
end: string;
4+
details: boolean;
5+
page: number;
6+
limit: number;
7+
}
8+
9+
export interface HitsDto {
10+
begin_date: string;
11+
end_date: string;
12+
data_transfer: number;
13+
hits: number;
14+
}
15+
16+
export interface HitStatisticsDto {
17+
total: HitsDto;
18+
details: HitsDto[];
19+
}
20+
21+
export interface StatsHits {
22+
begin_date: Date;
23+
end_date: Date;
24+
data_transfer: number;
25+
hits: number;
26+
}
27+
28+
export interface Stats {
29+
total: StatsHits;
30+
details: StatsHits[];
31+
}
32+
33+
export interface IBarChartData {
34+
x: [string[]];
35+
y: [number[]];
36+
}
37+
38+
export enum StatsType {
39+
DATA_TRANSFERT = "data_transfer",
40+
HITS = "hits",
41+
}
42+
43+
export enum StatsAggregation {
44+
DAY = 24 * 60 * 60 * 1000, // 1 day
45+
MINUTES = 5 * 60 * 1000, // 5 minutes
46+
}

assets/hooks/useBarChart.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect, useMemo, useRef, useState } from "react";
2+
import { BarChartProps } from "@codegouvfr/react-dsfr/Chart/BarChart";
3+
4+
import { HitStatisticsDto, StatsAggregation, StatsType } from "@/@types/stats";
5+
import { formatBarChartData, formatStats } from "@/utils/stats";
6+
7+
export interface IUseBarChartOptions extends BarChartProps {
8+
data: HitStatisticsDto;
9+
type?: StatsType;
10+
aggregation?: StatsAggregation;
11+
startDate?: Date;
12+
endDate?: Date;
13+
}
14+
15+
export function useBarChart(options: IUseBarChartOptions) {
16+
const { data, type = StatsType.DATA_TRANSFERT, aggregation = StatsAggregation.DAY, startDate, endDate, ...rest } = options;
17+
const ref = useRef<HTMLDivElement>(null);
18+
const [barsize, setBarsize] = useState(24);
19+
const barChartProps = useMemo(
20+
() => formatBarChartData(formatStats(data), type, aggregation, startDate, endDate),
21+
[data, type, aggregation, startDate, endDate]
22+
);
23+
const totalItems = barChartProps.x[0].length;
24+
25+
useEffect(() => {
26+
const element = ref.current;
27+
if (element && totalItems) {
28+
const { width } = element.getBoundingClientRect();
29+
const barSize = Math.max(Math.floor(width / totalItems) - 4, 2);
30+
if (barSize < 24) {
31+
setBarsize(barSize);
32+
} else {
33+
setBarsize(24);
34+
}
35+
}
36+
}, [totalItems]);
37+
38+
return { barChartProps: { ...rest, ...barChartProps, barsize }, ref };
39+
}

assets/pages/Chart.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { BarChart } from "@codegouvfr/react-dsfr/Chart/BarChart";
2+
import Button from "@codegouvfr/react-dsfr/Button";
3+
import { tss } from "tss-react";
4+
5+
import { IUseBarChartOptions, useBarChart } from "@/hooks/useBarChart";
6+
7+
export interface IChartProps extends IUseBarChartOptions {
8+
page?: number;
9+
onPageChange?: (page: number) => void;
10+
totalPage?: number;
11+
}
12+
13+
export default function Chart(props: IChartProps) {
14+
const { page = 1, onPageChange, totalPage = 1, ...options } = props;
15+
const { barChartProps, ref } = useBarChart(options);
16+
const { classes } = useStyles();
17+
18+
function handlePageChange(page) {
19+
return () => onPageChange?.(Math.max(Math.min(page, totalPage), 1));
20+
}
21+
22+
return (
23+
<div ref={ref} className={classes.root}>
24+
{totalPage > 2 && (
25+
<Button
26+
className={classes.prev}
27+
iconId="fr-icon-arrow-left-s-line"
28+
onClick={handlePageChange(page - 1)}
29+
priority="tertiary no outline"
30+
title="Page précédente"
31+
disabled={page === 1}
32+
/>
33+
)}
34+
<BarChart className={classes.chart} {...barChartProps} />
35+
{totalPage > 2 && (
36+
<Button
37+
className={classes.next}
38+
iconId="fr-icon-arrow-right-s-line"
39+
onClick={handlePageChange(page + 1)}
40+
priority="tertiary no outline"
41+
title="Page suivante"
42+
disabled={page === totalPage}
43+
/>
44+
)}
45+
</div>
46+
);
47+
}
48+
49+
const useStyles = tss.create(() => ({
50+
root: {
51+
display: "flex",
52+
alignItems: "center",
53+
},
54+
chart: {
55+
flex: 1,
56+
},
57+
prev: {},
58+
next: {},
59+
}));

assets/pages/Test.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useMemo, useState } from "react";
2+
import { useQuery } from "@tanstack/react-query";
3+
import Select from "@codegouvfr/react-dsfr/SelectNext";
4+
import LoadingIcon from "@/components/Utils/LoadingIcon";
5+
import { fr } from "@codegouvfr/react-dsfr";
6+
7+
import Main from "@/components/Layout/Main";
8+
import { HitStatisticsDto, StatsAggregation, StatsType } from "@/@types/stats";
9+
10+
import dataMonth from "./stats-month.json";
11+
import dataDay from "./stats-day.json";
12+
import dataDay1 from "./stats-day-1.json";
13+
import dataDay2 from "./stats-day-2.json";
14+
import dataDay3 from "./stats-day-3.json";
15+
import dataDay4 from "./stats-day-4.json";
16+
import dataDay5 from "./stats-day-5.json";
17+
import Chart from "./Chart";
18+
19+
// const dataRangeOptions = Object.entries(StatsAggregation)
20+
// .filter(([, value]) => typeof value === "number")
21+
// .map(([key, value]) => ({ value: String(value), label: key }));
22+
const dataRangeOptions = [
23+
{ value: "month", label: "Février 2025" },
24+
{ value: "day", label: "3 mars 2025" },
25+
{ value: "paginated", label: "3 mars 2025 paginée" },
26+
];
27+
const dataTypeOptions = Object.entries(StatsType).map(([key, value]) => ({ value, label: key }));
28+
29+
function delay(data: HitStatisticsDto): Promise<HitStatisticsDto> {
30+
return new Promise((resolve) => setTimeout(() => resolve(data), 500));
31+
}
32+
33+
export default function Test() {
34+
const [dataRange, setDataRange] = useState("month");
35+
const [dataType, setDataType] = useState(StatsType.DATA_TRANSFERT);
36+
const [page, setPage] = useState(1);
37+
38+
const { data } = useQuery<HitStatisticsDto>({
39+
queryKey: ["test", dataRange, page],
40+
queryFn: () => {
41+
if (dataRange === "month") {
42+
return delay(dataMonth);
43+
} else if (dataRange === "day") {
44+
return delay(dataDay);
45+
} else if (page === 1) {
46+
return delay(dataDay1);
47+
} else if (page === 2) {
48+
return delay(dataDay2);
49+
} else if (page === 3) {
50+
return delay(dataDay3);
51+
} else if (page === 4) {
52+
return delay(dataDay4);
53+
}
54+
return delay(dataDay5);
55+
},
56+
});
57+
58+
const chartProps = useMemo(() => {
59+
if (!data) {
60+
return null;
61+
}
62+
if (dataRange === "month") {
63+
return {
64+
aggregation: StatsAggregation.DAY,
65+
data,
66+
endDate: new Date(2025, 2, 1),
67+
startDate: new Date(2025, 1, 1),
68+
type: dataType,
69+
};
70+
}
71+
if (dataRange === "paginated") {
72+
return { aggregation: StatsAggregation.MINUTES, data, page, totalPage: 5, type: dataType };
73+
}
74+
return { aggregation: StatsAggregation.MINUTES, data, type: dataType };
75+
}, [data, dataRange, dataType, page]);
76+
77+
function onDateRangeChange(event) {
78+
setDataRange(event.target.value);
79+
setPage(1);
80+
}
81+
82+
function onDateTypeChange(event) {
83+
setDataType(event.target.value as StatsType);
84+
}
85+
86+
return (
87+
<Main title="Test">
88+
<Select
89+
label="Data"
90+
options={dataRangeOptions}
91+
nativeSelectProps={{
92+
value: String(dataRange),
93+
onChange: onDateRangeChange,
94+
}}
95+
/>
96+
<Select
97+
label="Type"
98+
options={dataTypeOptions}
99+
nativeSelectProps={{
100+
value: dataType,
101+
onChange: onDateTypeChange,
102+
}}
103+
/>
104+
{!chartProps ? <LoadingIcon className={fr.cx("fr-ml-2w")} largeIcon={true} /> : <Chart {...chartProps} onPageChange={setPage} />}
105+
</Main>
106+
);
107+
}

assets/pages/stats-day-1.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"total": {
3+
"begin_date": "2025-03-03T09:29:33.351Z",
4+
"end_date": "2025-03-03T18:52:12.406Z",
5+
"data_transfer": 167127,
6+
"hits": 367
7+
},
8+
"details": [
9+
{
10+
"begin_date": "2025-03-03T09:29:33.351Z",
11+
"end_date": "2025-03-03T09:29:38.410Z",
12+
"data_transfer": 3205,
13+
"hits": 4
14+
},
15+
{
16+
"begin_date": "2025-03-03T09:30:52.924Z",
17+
"end_date": "2025-03-03T09:34:56.278Z",
18+
"data_transfer": 4124,
19+
"hits": 5
20+
},
21+
{
22+
"begin_date": "2025-03-03T09:35:24.967Z",
23+
"end_date": "2025-03-03T09:39:23.645Z",
24+
"data_transfer": 4905,
25+
"hits": 13
26+
},
27+
{
28+
"begin_date": "2025-03-03T09:42:04.660Z",
29+
"end_date": "2025-03-03T09:44:01.483Z",
30+
"data_transfer": 3243,
31+
"hits": 9
32+
},
33+
{
34+
"begin_date": "2025-03-03T09:45:39.153Z",
35+
"end_date": "2025-03-03T09:49:35.197Z",
36+
"data_transfer": 7881,
37+
"hits": 22
38+
},
39+
{
40+
"begin_date": "2025-03-03T09:50:49.896Z",
41+
"end_date": "2025-03-03T09:54:46.281Z",
42+
"data_transfer": 6118,
43+
"hits": 14
44+
},
45+
{
46+
"begin_date": "2025-03-03T09:55:27.101Z",
47+
"end_date": "2025-03-03T09:59:55.715Z",
48+
"data_transfer": 5352,
49+
"hits": 14
50+
},
51+
{
52+
"begin_date": "2025-03-03T10:00:11.325Z",
53+
"end_date": "2025-03-03T10:04:51.625Z",
54+
"data_transfer": 6959,
55+
"hits": 18
56+
},
57+
{
58+
"begin_date": "2025-03-03T10:05:03.363Z",
59+
"end_date": "2025-03-03T10:06:09.848Z",
60+
"data_transfer": 2255,
61+
"hits": 6
62+
},
63+
{
64+
"begin_date": "2025-03-03T10:11:14.505Z",
65+
"end_date": "2025-03-03T10:13:54.149Z",
66+
"data_transfer": 5647,
67+
"hits": 15
68+
}
69+
]
70+
}

assets/pages/stats-day-2.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"total": {
3+
"begin_date": "2025-03-03T09:29:33.351Z",
4+
"end_date": "2025-03-03T18:52:12.406Z",
5+
"data_transfer": 167127,
6+
"hits": 367
7+
},
8+
"details": [
9+
{
10+
"begin_date": "2025-03-03T10:15:23.246Z",
11+
"end_date": "2025-03-03T10:19:10.725Z",
12+
"data_transfer": 4529,
13+
"hits": 10
14+
},
15+
{
16+
"begin_date": "2025-03-03T10:20:41.665Z",
17+
"end_date": "2025-03-03T10:22:07.312Z",
18+
"data_transfer": 2816,
19+
"hits": 5
20+
},
21+
{
22+
"begin_date": "2025-03-03T10:28:05.372Z",
23+
"end_date": "2025-03-03T10:29:45.510Z",
24+
"data_transfer": 2522,
25+
"hits": 4
26+
},
27+
{
28+
"begin_date": "2025-03-03T13:17:42.953Z",
29+
"end_date": "2025-03-03T13:19:50.931Z",
30+
"data_transfer": 6006,
31+
"hits": 19
32+
},
33+
{
34+
"begin_date": "2025-03-03T13:21:27.366Z",
35+
"end_date": "2025-03-03T13:23:24.398Z",
36+
"data_transfer": 3434,
37+
"hits": 7
38+
},
39+
{
40+
"begin_date": "2025-03-03T13:25:05.711Z",
41+
"end_date": "2025-03-03T13:27:36.012Z",
42+
"data_transfer": 3636,
43+
"hits": 9
44+
},
45+
{
46+
"begin_date": "2025-03-03T13:37:58.346Z",
47+
"end_date": "2025-03-03T13:39:47.894Z",
48+
"data_transfer": 2841,
49+
"hits": 5
50+
},
51+
{
52+
"begin_date": "2025-03-03T13:41:26.731Z",
53+
"end_date": "2025-03-03T13:44:53.524Z",
54+
"data_transfer": 5053,
55+
"hits": 10
56+
},
57+
{
58+
"begin_date": "2025-03-03T13:45:17.856Z",
59+
"end_date": "2025-03-03T13:49:06.256Z",
60+
"data_transfer": 4419,
61+
"hits": 8
62+
},
63+
{
64+
"begin_date": "2025-03-03T13:50:27.497Z",
65+
"end_date": "2025-03-03T13:53:39.309Z",
66+
"data_transfer": 5306,
67+
"hits": 11
68+
}
69+
]
70+
}

0 commit comments

Comments
 (0)