1import ChartWrapper from "@/common/components/ChartWrapper";
2import { useMemo, useContext, memo } from "react";
3import { t } from "i18next";
4import { ThemeContext } from "@/common/contexts/Theme";
5import { PreferencesContext } from "@/common/contexts/Preferences";
6import { convertSpeed, getSpeedUnit, TIME_FORMAT_12H } from "@/common/utils/FormatUtil";
7import "./SpeedChart/styles.sass";
8
9const HourlyChart = memo((props) => {
10 const [isDarkMode] = useContext(ThemeContext);
11 const [preferences] = useContext(PreferencesContext);
12 const speedUnit = getSpeedUnit(preferences);
13 const use12h = preferences?.timeFormat === TIME_FORMAT_12H;
14
15 const formatHourLabel = (hour) => {
16 if (use12h) {
17 const suffix = hour >= 12 ? "PM" : "AM";
18 let h = hour % 12;
19 if (h === 0) h = 12;
20 return `${h}:00 ${suffix}`;
21 }
22 return `${hour.toString().padStart(2, '0')}:00`;
23 };
24
25 const chartData = useMemo(() => {
26 if (!props.hourlyAverages) return { labels: [], datasets: [] };
27
28 const labels = props.hourlyAverages.map(h => formatHourLabel(h.hour));
29
30 return {
31 labels,
32 datasets: [
33 {
34 label: t("latest.down"),
35 data: props.hourlyAverages.map(h => convertSpeed(h.download, preferences)),
36 backgroundColor: 'hsla(187, 94%, 43%, 0.75)',
37 borderColor: 'hsl(187, 94%, 43%)',
38 borderWidth: 1.5,
39 borderRadius: 6
40 },
41 {
42 label: t("latest.up"),
43 data: props.hourlyAverages.map(h => convertSpeed(h.upload, preferences)),
44 backgroundColor: 'hsla(258, 90%, 66%, 0.75)',
45 borderColor: 'hsl(258, 90%, 66%)',
46 borderWidth: 1.5,
47 borderRadius: 6
48 }
49 ]
50 };
51 }, [props.hourlyAverages, preferences, use12h]);
52
53 const themeColors = useMemo(() => ({
54 gridColor: isDarkMode ? 'rgba(42, 52, 65, 0.6)' : 'rgba(203, 213, 225, 0.8)',
55 tickColor: isDarkMode ? 'hsl(215, 20%, 50%)' : 'hsl(215, 25%, 40%)',
56 tooltipBg: isDarkMode ? 'hsl(215, 28%, 10%)' : 'hsl(0, 0%, 100%)',
57 tooltipTitle: isDarkMode ? 'hsl(210, 40%, 96%)' : 'hsl(215, 25%, 20%)',
58 tooltipBody: isDarkMode ? 'hsl(215, 20%, 65%)' : 'hsl(215, 15%, 40%)',
59 tooltipBorder: isDarkMode ? 'hsl(215, 25%, 22%)' : 'hsl(215, 20%, 85%)'
60 }), [isDarkMode]);
61
62 const chartOptions = useMemo(() => ({
63 responsive: true,
64 maintainAspectRatio: false,
65 resizeDelay: 100,
66 animation: {
67 duration: 250,
68 easing: 'easeOutQuart'
69 },
70 animations: {
71 colors: false,
72 x: false
73 },
74 transitions: {
75 active: {
76 animation: {
77 duration: 100
78 }
79 },
80 resize: {
81 animation: {
82 duration: 0
83 }
84 }
85 },
86 plugins: {
87 tooltip: {
88 backgroundColor: themeColors.tooltipBg,
89 titleColor: themeColors.tooltipTitle,
90 bodyColor: themeColors.tooltipBody,
91 borderColor: themeColors.tooltipBorder,
92 borderWidth: 1,
93 padding: 14,
94 cornerRadius: 10,
95 displayColors: true,
96 boxPadding: 8,
97 callbacks: {
98 label: (item) => `${item.dataset.label}: ${item.formattedValue} ${speedUnit}`,
99 afterBody: (items) => {
100 const hourIndex = items[0].dataIndex;
101 const count = props.hourlyAverages[hourIndex]?.count || 0;
102 return `\n${t("statistics.hourly.sample_count")}: ${count}`;
103 }
104 }
105 },
106 legend: {
107 position: "bottom",
108 labels: {
109 usePointStyle: true,
110 pointStyle: 'rect',
111 padding: 20,
112 color: themeColors.tickColor,
113 font: {
114 size: 12,
115 weight: 500
116 }
117 }
118 }
119 },
120 scales: {
121 x: {
122 grid: {
123 color: themeColors.gridColor,
124 drawBorder: false
125 },
126 border: {
127 display: false
128 },
129 ticks: {
130 color: themeColors.tickColor,
131 maxRotation: 0
132 }
133 },
134 y: {
135 beginAtZero: true,
136 grid: {
137 color: themeColors.gridColor,
138 drawBorder: false
139 },
140 border: {
141 display: false
142 },
143 ticks: {
144 color: themeColors.tickColor
145 }
146 }
147 }
148 }), [themeColors, props.hourlyAverages, speedUnit]);
149
150 return (
151 <div className="chart-container" onClick={props.onClick}>
152 <div className="chart-header">
153 <h3 className="chart-title">{t("statistics.hourly.title")}</h3>
154 </div>
155 <div className="chart-body">
156 <ChartWrapper type="bar" data={chartData} options={chartOptions} />
157 </div>
158 </div>
159 );
160});
161
162export default HourlyChart;
163