1
- import React , { useEffect , useState , useCallback } from 'react' ;
1
+ import React , { useEffect , useState } from 'react' ;
2
2
import {
3
3
LineChart ,
4
4
Line ,
@@ -12,250 +12,23 @@ import {
12
12
Label ,
13
13
Legend ,
14
14
} from 'recharts' ;
15
- import GoodAir from '@/icons/Charts/GoodAir' ;
16
- import Hazardous from '@/icons/Charts/Hazardous' ;
17
- import Moderate from '@/icons/Charts/Moderate' ;
18
- import Unhealthy from '@/icons/Charts/Unhealthy' ;
19
- import UnhealthySG from '@/icons/Charts/UnhealthySG' ;
20
- import VeryUnhealthy from '@/icons/Charts/VeryUnhealthy' ;
21
15
import { useSelector , useDispatch } from 'react-redux' ;
22
16
import Spinner from '@/components/Spinner' ;
23
17
import { setRefreshChart } from '@/lib/store/services/charts/ChartSlice' ;
24
18
import { fetchAnalyticsData } from '@/lib/store/services/charts/ChartData' ;
25
-
26
- const colors = [ '#11225A' , '#0A46EB' , '#297EFF' , '#B8D9FF' ] ;
27
-
28
- const reduceDecimalPlaces = ( num ) => {
29
- return Math . round ( ( num + Number . EPSILON ) * 10000 ) / 10000 ;
30
- } ;
31
-
32
- const truncate = ( str ) => {
33
- return str . length > 10 ? str . substr ( 0 , 10 - 1 ) + '...' : str ;
34
- } ;
35
-
36
- const getAirQualityLevelText = ( value ) => {
37
- let airQualityText = '' ;
38
- let AirQualityIcon = null ;
39
- let airQualityColor = '' ;
40
-
41
- if ( value >= 0 && value <= 12 ) {
42
- airQualityText = 'Air Quality is Good' ;
43
- AirQualityIcon = GoodAir ;
44
- airQualityColor = 'text-green-500' ;
45
- } else if ( value > 12 && value <= 35.4 ) {
46
- airQualityText = 'Air Quality is Moderate' ;
47
- AirQualityIcon = Moderate ;
48
- airQualityColor = 'text-yellow-500' ;
49
- } else if ( value > 35.4 && value <= 55.4 ) {
50
- airQualityText = 'Air Quality is Unhealthy for Sensitive Groups' ;
51
- AirQualityIcon = UnhealthySG ;
52
- airQualityColor = 'text-orange-500' ;
53
- } else if ( value > 55.4 && value <= 150.4 ) {
54
- airQualityText = 'Air Quality is Unhealthy' ;
55
- AirQualityIcon = Unhealthy ;
56
- airQualityColor = 'text-red-500' ;
57
- } else if ( value > 150.4 && value <= 250.4 ) {
58
- airQualityText = 'Air Quality is Very Unhealthy' ;
59
- AirQualityIcon = VeryUnhealthy ;
60
- airQualityColor = 'text-purple-500' ;
61
- } else if ( value > 250.4 && value <= 500 ) {
62
- airQualityText = 'Air Quality is Hazardous' ;
63
- AirQualityIcon = Hazardous ;
64
- airQualityColor = 'text-gray-500' ;
65
- }
66
-
67
- return { airQualityText, AirQualityIcon, airQualityColor } ;
68
- } ;
69
-
70
- const CustomTooltipLineGraph = ( { active, payload } ) => {
71
- if ( active && payload && payload . length ) {
72
- const hoveredPoint = payload [ 0 ] ;
73
- const otherPoints = payload . slice ( 1 ) ;
74
-
75
- const { airQualityText, AirQualityIcon, airQualityColor } = getAirQualityLevelText (
76
- hoveredPoint . value ,
77
- ) ;
78
-
79
- return (
80
- < div className = 'bg-white border border-gray-200 rounded-md shadow-lg w-72 outline-none' >
81
- < div className = 'flex flex-col space-y-1' >
82
- < div className = 'flex flex-col items-start justify-between w-full h-auto p-2' >
83
- < span className = 'text-sm text-gray-300' >
84
- { new Date ( hoveredPoint . payload . time ) . toLocaleDateString ( 'en-US' , {
85
- year : 'numeric' ,
86
- month : 'long' ,
87
- day : 'numeric' ,
88
- } ) }
89
- </ span >
90
- < p className = 'flex justify-between w-full mb-1 mt-2' >
91
- < div className = 'flex items-center text-xs font-medium leading-[14px] text-gray-600' >
92
- < div className = 'w-[10px] h-[10px] bg-blue-700 rounded-xl mr-2' > </ div >
93
- { truncate ( hoveredPoint . name ) }
94
- </ div >
95
- < div className = 'text-xs font-medium leading-[14px] text-gray-600' >
96
- { reduceDecimalPlaces ( hoveredPoint . value ) + ' μg/m3' }
97
- </ div >
98
- </ p >
99
- < div className = 'flex justify-between items-center w-full' >
100
- < div className = { `${ airQualityColor } text-xs font-medium leading-[14px] ` } >
101
- { airQualityText }
102
- </ div >
103
- < AirQualityIcon width = { 30 } height = { 30 } />
104
- </ div >
105
- </ div >
106
- { otherPoints . length > 0 && (
107
- < >
108
- < div className = 'w-full h-[2px] bg-transparent my-1 border-t border-dotted border-gray-300' />
109
- < div className = 'p-2 space-y-1' >
110
- { otherPoints . map ( ( point , index ) => (
111
- < p key = { index } className = 'flex justify-between w-full mb-1' >
112
- < div className = 'flex items-center text-xs font-medium leading-[14px] text-gray-400' >
113
- < div className = 'w-[10px] h-[10px] bg-gray-400 rounded-xl mr-2' > </ div >
114
- { truncate ( point . name ) }
115
- </ div >
116
- < div className = 'text-xs font-medium leading-[14px] text-gray-400' >
117
- { reduceDecimalPlaces ( point . value ) + ' μg/m3' }
118
- </ div >
119
- </ p >
120
- ) ) }
121
- </ div >
122
- </ >
123
- ) }
124
- </ div >
125
- </ div >
126
- ) ;
127
- }
128
- return '' ;
129
- } ;
130
-
131
- const CustomTooltipBarGraph = ( { active, payload } ) => {
132
- if ( active && payload && payload . length ) {
133
- const hoveredPoint = payload [ 0 ] ;
134
-
135
- const { airQualityText, AirQualityIcon, airQualityColor } = getAirQualityLevelText (
136
- hoveredPoint . value ,
137
- ) ;
138
-
139
- return (
140
- < div className = 'bg-white border border-gray-200 rounded-md shadow-lg w-72 outline-none' >
141
- < div className = 'flex flex-col space-y-1' >
142
- < div className = 'flex flex-col items-start justify-between w-full h-auto p-2' >
143
- < span className = 'text-sm text-gray-300' >
144
- { new Date ( hoveredPoint . payload . time ) . toLocaleDateString ( 'en-US' , {
145
- year : 'numeric' ,
146
- month : 'long' ,
147
- day : 'numeric' ,
148
- } ) }
149
- </ span >
150
- < p className = 'flex justify-between w-full mb-1 mt-2' >
151
- < div className = 'flex items-center text-xs font-medium leading-[14px] text-gray-600' >
152
- < div className = 'w-[10px] h-[10px] bg-blue-700 rounded-xl mr-2' > </ div >
153
- { truncate ( hoveredPoint . name ) }
154
- </ div >
155
- < div className = 'text-xs font-medium leading-[14px] text-gray-600' >
156
- { reduceDecimalPlaces ( hoveredPoint . value ) + ' μg/m3' }
157
- </ div >
158
- </ p >
159
- < div className = 'flex justify-between items-center w-full' >
160
- < div className = { `${ airQualityColor } text-xs font-medium leading-[14px] ` } >
161
- { airQualityText }
162
- </ div >
163
- < AirQualityIcon width = { 30 } height = { 30 } />
164
- </ div >
165
- </ div >
166
- </ div >
167
- </ div >
168
- ) ;
169
- }
170
- return '' ;
171
- } ;
172
-
173
- const CustomizedAxisTick = ( { x, y, payload } ) => {
174
- return (
175
- < g transform = { `translate(${ x } ,${ y } )` } >
176
- < text x = { 0 } y = { 0 } dy = { 16 } textAnchor = 'middle' fill = '#666' fontSize = { 12 } >
177
- { new Date ( payload . value ) . toLocaleDateString ( 'en-US' , {
178
- month : 'short' ,
179
- day : 'numeric' ,
180
- } ) }
181
- </ text >
182
- </ g >
183
- ) ;
184
- } ;
185
-
186
- const CustomDot = ( props ) => {
187
- const { cx, cy, fill, payload } = props ;
188
-
189
- if ( ! payload . active ) {
190
- return null ;
191
- }
192
-
193
- return < circle cx = { cx } cy = { cy } r = { 6 } fill = { fill } /> ;
194
- } ;
195
-
196
- const CustomLegendTooltip = ( { tooltipText, children, direction, themeClass } ) => {
197
- const [ visible , setVisible ] = useState ( false ) ;
198
-
199
- const tooltipClass = {
200
- top : 'bottom-full mb-3' ,
201
- bottom : 'top-full mt-3' ,
202
- } [ direction ] ;
203
-
204
- return (
205
- < div
206
- className = 'relative'
207
- onMouseEnter = { ( ) => setVisible ( true ) }
208
- onMouseLeave = { ( ) => setVisible ( false ) } >
209
- { children }
210
- { visible && (
211
- < div
212
- className = { `absolute ${ tooltipClass } ${
213
- themeClass ? themeClass : 'bg-white text-center text-gray-700'
214
- } p-2 w-48 rounded-md shadow-lg z-10`} >
215
- < p className = 'text-sm' > { tooltipText } </ p >
216
- </ div >
217
- ) }
218
- </ div >
219
- ) ;
220
- } ;
221
-
222
- const renderCustomizedLegend = ( props ) => {
223
- const { payload } = props ;
224
-
225
- // Sort the payload array from darkest to lightest color
226
- const sortedPayload = payload . sort ( ( a , b ) => {
227
- const colorToGrayscale = ( color ) => {
228
- if ( color ) {
229
- const hex = color . replace ( '#' , '' ) ;
230
- const r = parseInt ( hex . slice ( 0 , 2 ) , 16 ) ;
231
- const g = parseInt ( hex . slice ( 2 , 4 ) , 16 ) ;
232
- const b = parseInt ( hex . slice ( 4 , 6 ) , 16 ) ;
233
- return 0.2126 * r + 0.7152 * g + 0.0722 * b ; // ITU-R BT.709 formula
234
- }
235
- return 0 ;
236
- } ;
237
- return colorToGrayscale ( a . color ) - colorToGrayscale ( b . color ) ;
238
- } ) ;
239
-
240
- return (
241
- < div className = 'p-2 md:p-0 flex flex-wrap flex-col md:flex-row md:justify-end mt-2 space-y-2 md:space-y-0 md:space-x-4 outline-none' >
242
- { sortedPayload . map ( ( entry , index ) => (
243
- < CustomLegendTooltip key = { `item-${ index } ` } tooltipText = { entry . value } direction = 'top' >
244
- < div
245
- style = { { color : '#485972' } }
246
- className = 'flex w-full items-center text-sm outline-none' >
247
- < span
248
- className = 'w-[10px] h-[10px] rounded-xl mr-1 ml-1 outline-none'
249
- style = { { backgroundColor : entry . color } } > </ span >
250
- { truncate ( entry . value ) }
251
- </ div >
252
- </ CustomLegendTooltip >
253
- ) ) }
254
- </ div >
255
- ) ;
256
- } ;
257
-
258
- // Custom hook to fetch analytics data
19
+ import {
20
+ renderCustomizedLegend ,
21
+ CustomDot ,
22
+ CustomizedAxisTick ,
23
+ CustomTooltipLineGraph ,
24
+ CustomTooltipBarGraph ,
25
+ colors ,
26
+ } from './components' ;
27
+
28
+ /**
29
+ * @description Custom hook to fetch analytics data
30
+ * @returns {Object } analyticsData, isLoading, error, loadingTime
31
+ */
259
32
const useAnalytics = ( ) => {
260
33
const dispatch = useDispatch ( ) ;
261
34
const chartData = useSelector ( ( state ) => state . chart ) ;
@@ -303,6 +76,13 @@ const useAnalytics = () => {
303
76
return { analyticsData, isLoading, error, loadingTime } ;
304
77
} ;
305
78
79
+ /**
80
+ * @description Charts component
81
+ * @param {String } chartType - Type of chart to render
82
+ * @param {String } width - Width of the chart
83
+ * @param {String } height - Height of the chart
84
+ * @returns {React.Component } Charts
85
+ */
306
86
const Charts = ( { chartType = 'line' , width = '100%' , height = '100%' } ) => {
307
87
const chartData = useSelector ( ( state ) => state . chart ) ;
308
88
const { analyticsData, isLoading, error, loadingTime } = useAnalytics ( ) ;
@@ -325,7 +105,8 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
325
105
return (
326
106
< div className = 'ml-10 flex justify-center text-center items-center w-full h-full' >
327
107
< p className = 'text-red-500' >
328
- Oops! Something went wrong. Please try again later or contact support.
108
+ An error has occurred. Please try again later or reach out to our support team for
109
+ assistance.
329
110
</ p >
330
111
</ div >
331
112
) ;
@@ -335,14 +116,14 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
335
116
if ( isLoading || ! hasLoaded ) {
336
117
return (
337
118
< div className = 'ml-10 flex justify-center text-center items-center w-full h-full' >
338
- < p className = 'text-blue-500' >
119
+ < div className = 'text-blue-500' >
339
120
< Spinner />
340
121
{ showLoadingMessage && (
341
122
< span className = 'text-yellow-500 mt-2' >
342
- The data is taking longer than expected to load. Please hang on a bit longer .
123
+ The data is currently being processed. We appreciate your patience .
343
124
</ span >
344
125
) }
345
- </ p >
126
+ </ div >
346
127
</ div >
347
128
) ;
348
129
}
@@ -351,7 +132,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
351
132
if ( hasLoaded && ( analyticsData === null || analyticsData . length === 0 ) ) {
352
133
return (
353
134
< div className = 'ml-10 flex justify-center items-center w-full h-full' >
354
- No data for this time range
135
+ There is no data available for the selected time range.
355
136
</ div >
356
137
) ;
357
138
}
@@ -373,6 +154,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
373
154
allKeys = new Set ( Object . keys ( dataForChart [ 0 ] ) ) ;
374
155
}
375
156
157
+ // Render the chart
376
158
const renderChart = ( ) => {
377
159
if ( chartType === 'line' ) {
378
160
return (
@@ -484,7 +266,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
484
266
wrapperStyle = { { bottom : 0 , right : 0 , position : 'absolute' } }
485
267
/>
486
268
< Tooltip
487
- content = { < CustomTooltipBarGraph /> }
269
+ content = { < CustomTooltipLineGraph /> }
488
270
cursor = { { fill : '#eee' , fillOpacity : 0.3 } }
489
271
/>
490
272
</ BarChart >
0 commit comments