Skip to content

Commit d3482be

Browse files
authored
Merge pull request #1937 from airqo-platform/ft-charts
[platform] Optimize and Document Chart Component
2 parents ee94340 + 19387ba commit d3482be

File tree

5 files changed

+319
-263
lines changed

5 files changed

+319
-263
lines changed

platform/src/common/components/AQNumberCard/index.jsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import WindIcon from '@/icons/Common/wind.svg';
88
import CustomTooltip from '../Tooltip';
99
import { useWindowSize } from '@/lib/windowSize';
1010

11-
const AQNumberCard = ({ reading, location, keyValue, pollutant, count }) => {
11+
const AQNumberCard = ({ reading, location, pollutant, count }) => {
1212
let airQualityText = '';
1313
let AirQualityIcon = null;
1414
let airQualityColor = '';
@@ -44,18 +44,15 @@ const AQNumberCard = ({ reading, location, keyValue, pollutant, count }) => {
4444
<div
4545
className={`${
4646
count <= 2 ? 'w-full md:min-w-[200px] md:max-w-[50%] float-left' : 'w-full'
47-
} relative h-[164.48px] bg-white flex-col justify-start items-center inline-flex`}
48-
key={keyValue}
49-
>
47+
} relative h-[164.48px] bg-white flex-col justify-start items-center inline-flex`}>
5048
<div className='border border-gray-200 rounded-lg overflow-hidden w-full'>
5149
<div className='self-stretch w-full h-[68.48px] px-4 pt-3.5 pb-[10.48px] bg-white border-b border-b-gray-200 flex-col justify-start items-start flex'>
5250
<div className='self-stretch justify-between items-center inline-flex'>
5351
<div className='flex-col justify-start items-start inline-flex'>
5452
{location !== '--' ? (
5553
<div
5654
className='text-gray-700 text-base font-medium leading-normal whitespace-nowrap overflow-ellipsis'
57-
title={location}
58-
>
55+
title={location}>
5956
{location.length > 17 ? location.slice(0, 17) + '...' : location}
6057
</div>
6158
) : (

platform/src/common/components/Calendar/CustomCalendar.jsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,14 @@ const CustomCalendar = ({ initialStartDate, initialEndDate, Icon, dropdown, clas
142142
<button
143143
onClick={handleClick}
144144
type='button'
145-
className='relative border border-grey-750 rounded flex items-center justify-between gap-2 px-4 py-3'
146-
>
145+
className='relative border border-grey-750 rounded flex items-center justify-between gap-2 px-4 py-3'>
147146
{Icon ? <Icon /> : <CalendarIcon />}
148147
<span className='hidden sm:inline-block text-sm font-medium'>
149148
{chartData.chartDataRange.label}
150149
</span>
151150
{dropdown && <ChevronDownIcon />}
152151
</button>
153-
<div
154-
className={`absolute top-[50px] z-[900] ${className} ${
155-
openDatePicker ? 'block' : 'hidden'
156-
}`}
157-
>
152+
<div className={`absolute top-[50px] ${className} ${openDatePicker ? 'block' : 'hidden'}`}>
158153
<DatePickerHiddenInput />
159154
</div>
160155
</div>

platform/src/common/components/Charts/Charts.js

+29-247
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useCallback } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import {
33
LineChart,
44
Line,
@@ -12,250 +12,23 @@ import {
1212
Label,
1313
Legend,
1414
} 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';
2115
import { useSelector, useDispatch } from 'react-redux';
2216
import Spinner from '@/components/Spinner';
2317
import { setRefreshChart } from '@/lib/store/services/charts/ChartSlice';
2418
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+
*/
25932
const useAnalytics = () => {
26033
const dispatch = useDispatch();
26134
const chartData = useSelector((state) => state.chart);
@@ -303,6 +76,13 @@ const useAnalytics = () => {
30376
return { analyticsData, isLoading, error, loadingTime };
30477
};
30578

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+
*/
30686
const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
30787
const chartData = useSelector((state) => state.chart);
30888
const { analyticsData, isLoading, error, loadingTime } = useAnalytics();
@@ -325,7 +105,8 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
325105
return (
326106
<div className='ml-10 flex justify-center text-center items-center w-full h-full'>
327107
<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.
329110
</p>
330111
</div>
331112
);
@@ -335,14 +116,14 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
335116
if (isLoading || !hasLoaded) {
336117
return (
337118
<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'>
339120
<Spinner />
340121
{showLoadingMessage && (
341122
<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.
343124
</span>
344125
)}
345-
</p>
126+
</div>
346127
</div>
347128
);
348129
}
@@ -351,7 +132,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
351132
if (hasLoaded && (analyticsData === null || analyticsData.length === 0)) {
352133
return (
353134
<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.
355136
</div>
356137
);
357138
}
@@ -373,6 +154,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
373154
allKeys = new Set(Object.keys(dataForChart[0]));
374155
}
375156

157+
// Render the chart
376158
const renderChart = () => {
377159
if (chartType === 'line') {
378160
return (
@@ -484,7 +266,7 @@ const Charts = ({ chartType = 'line', width = '100%', height = '100%' }) => {
484266
wrapperStyle={{ bottom: 0, right: 0, position: 'absolute' }}
485267
/>
486268
<Tooltip
487-
content={<CustomTooltipBarGraph />}
269+
content={<CustomTooltipLineGraph />}
488270
cursor={{ fill: '#eee', fillOpacity: 0.3 }}
489271
/>
490272
</BarChart>

0 commit comments

Comments
 (0)