Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analytic: Feature to change Air Quality standards on charts #2383

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions platform/src/common/components/Charts/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SkeletonLoader from './components/SkeletonLoader';
import { setOpenModal, setModalType } from '@/lib/store/services/downloadModal';
import CustomToast from '../Toast/CustomToast';
import useOutsideClick from '@/core/hooks/useOutsideClick';
import StandardsMenu from './components/StandardsMenu';

const ChartContainer = ({
chartType,
Expand Down Expand Up @@ -155,7 +156,7 @@ const ChartContainer = ({
onClick={handleRefreshChart}
className="flex justify-between items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Refresh
<span>Refresh</span>
</button>
<hr className="border-gray-200" />
{['png', 'jpg', 'pdf'].map((format) => (
Expand All @@ -166,12 +167,11 @@ const ChartContainer = ({
>
<span>Export as {format.toUpperCase()}</span>
<span className="-mr-2 flex items-center">
{loadingFormat === format && (
{loadingFormat === format ? (
<div className="animate-spin h-4 w-4 border-2 border-t-blue-500 border-gray-300 rounded-full"></div>
)}
{downloadComplete === format && (
) : downloadComplete === format ? (
<CheckIcon fill="#1E40AF" width={20} height={20} />
)}
) : null}
</span>
</button>
))}
Expand All @@ -180,8 +180,9 @@ const ChartContainer = ({
onClick={() => handleOpenModal('inSights', [], userSelectedSites)}
className="flex justify-between items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
More insights
<span>More insights</span>
</button>
<StandardsMenu />
</>
),
[
Expand Down
10 changes: 6 additions & 4 deletions platform/src/common/components/Charts/MoreInsightsChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import {
} from './components';

import { parseAndValidateISODate } from '@/core/utils/dateUtils';
import { WHO_STANDARD_VALUES } from './constants';
import { formatYAxisTick, CustomizedAxisTick } from './utils';
import useResizeObserver from '@/core/utils/useResizeObserver';
import { useSelector } from 'react-redux';

/**
* MoreInsightsChart Component
Expand Down Expand Up @@ -60,6 +60,7 @@ const MoreInsightsChart = ({

const containerRef = useRef(null);
const { width: containerWidth } = useResizeObserver(containerRef);
const aqStandard = useSelector((state) => state.chart.aqStandard);

/**
* Processes raw chart data by validating dates and organizing data by time and site.
Expand Down Expand Up @@ -162,8 +163,8 @@ const MoreInsightsChart = ({
* Memoized WHO standard value based on pollutant type.
*/
const WHO_STANDARD_VALUE = useMemo(
() => WHO_STANDARD_VALUES[pollutantType] || 0,
[pollutantType],
() => aqStandard.value[pollutantType] || 0,
[pollutantType, aqStandard],
);

/**
Expand Down Expand Up @@ -352,7 +353,7 @@ const MoreInsightsChart = ({
{WHO_STANDARD_VALUE && (
<ReferenceLine
y={WHO_STANDARD_VALUE}
label={<CustomReferenceLabel />}
label={<CustomReferenceLabel name={aqStandard.name} />}
ifOverflow="extendDomain"
stroke="red"
strokeOpacity={1}
Expand Down Expand Up @@ -380,6 +381,7 @@ const MoreInsightsChart = ({
frequency,
siteIdToName,
refreshChart,
aqStandard.name,
]);

return (
Expand Down
142 changes: 142 additions & 0 deletions platform/src/common/components/Charts/components/StandardsMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useState, useRef, useEffect } from 'react';
import { MdKeyboardArrowRight, MdKeyboardArrowDown } from 'react-icons/md';
import { useDispatch, useSelector } from 'react-redux';
import { setAqStandard } from '@/lib/store/services/charts/ChartSlice';
import { IoMdCheckmark } from 'react-icons/io';

const aqStandards = [
{
name: 'WHO',
value: {
pm2_5: 10,
pm10: 20,
no2: 40,
},
},
{
name: 'NEMA',
value: {
pm2_5: 15,
pm10: 45,
no2: 25,
},
},
{
name: 'KCCA',
value: {
pm2_5: 20,
pm10: 50,
no2: 30,
},
},
{
name: 'LASEPA',
value: {
pm2_5: 25,
pm10: 55,
no2: 35,
},
},
];

const StandardsMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const menuRef = useRef(null);
const dispatch = useDispatch();
const selectedStandard = useSelector((state) => state.chart.aqStandard);

useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setIsOpen(false);
}
};

const handleResize = () => {
setIsMobile(window.innerWidth < 640); // Adjust this breakpoint as needed
};

document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('resize', handleResize);
handleResize(); // Initial check

return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('resize', handleResize);
};
}, []);

const handleSelectStandard = (standard) => {
dispatch(setAqStandard(standard));
setIsOpen(false);
};

const currentStandard =
aqStandards.find((s) => s.name === selectedStandard?.name) ||
aqStandards[0];

Comment on lines +59 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for missing standards.

The current standard selection logic should handle cases where the standard is not found in the array.

 const currentStandard =
-  aqStandards.find((s) => s.name === selectedStandard?.name) ||
-  aqStandards[0];
+  aqStandards.find((s) => s.name === selectedStandard?.name) ??
+  aqStandards[0] ??
+  { name: 'Unknown', value: { pm2_5: 0, pm10: 0, no2: 0 } };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentStandard =
aqStandards.find((s) => s.name === selectedStandard?.name) ||
aqStandards[0];
const currentStandard =
aqStandards.find((s) => s.name === selectedStandard?.name) ??
aqStandards[0] ??
{ name: 'Unknown', value: { pm2_5: 0, pm10: 0, no2: 0 } };

return (
<div
className="relative w-full border-t border-gray-200"
ref={menuRef}
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
<button
className="flex justify-between items-center w-full px-4 py-2 text-sm text-gray-600 hover:bg-gray-50"
aria-haspopup="true"
aria-expanded={isOpen}
>
<span>Air Quality Standard</span>
<span className="text-gray-400 ml-2 transition-transform duration-200">
{isMobile ? (
<MdKeyboardArrowDown
size={18}
className={`transform ${isOpen ? 'rotate-180' : ''}`}
/>
) : (
<MdKeyboardArrowRight
size={18}
className={`transform ${isOpen ? 'rotate-180' : ''}`}
/>
)}
</span>
</button>
<div
className={`
absolute bg-white shadow-md p-1 border border-gray-200 rounded-md
transition-all duration-200 ease-in-out overflow-hidden z-10
${isOpen ? 'opacity-100 visible' : 'opacity-0 invisible'}
${isMobile ? 'left-0 right-0 top-full mt-1' : 'right-full top-0 mt-0'}
`}
style={{
width: '100%',
transform: isMobile ? 'none' : 'translateX(-8px)',
}}
>
{aqStandards.map((standard, index) => (
<button
key={standard.name}
className={`
flex flex-row justify-between items-center w-full px-4 py-2
text-sm text-gray-600 hover:bg-gray-50
${standard.name === currentStandard.name ? 'bg-gray-100' : ''}
${index !== 0 ? 'border-t border-gray-200' : ''}
`}
onClick={() => handleSelectStandard(standard)}
>
<span className="font-medium">{standard.name}</span>
{standard.name === currentStandard.name && (
<span className="text-green-500 ml-2">
<IoMdCheckmark size={18} />
</span>
)}
</button>
))}
</div>
</div>
);
};

export default StandardsMenu;
4 changes: 2 additions & 2 deletions platform/src/common/components/Charts/components/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ const renderCustomizedLegend = ({ payload }) => {
* @param {Object} props
*/
const CustomReferenceLabel = (props) => {
const { viewBox } = props;
const { viewBox, name } = props;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update PropTypes to include the new name prop.

The name prop should be added to the PropTypes validation.

 CustomReferenceLabel.propTypes = {
   viewBox: PropTypes.object,
+  name: PropTypes.string.isRequired,
 };

Also applies to: 283-283

const x = viewBox.width + viewBox.x - 10;
const y = viewBox.y + 3;

Expand All @@ -280,7 +280,7 @@ const CustomReferenceLabel = (props) => {
style={{ backgroundColor: 'red' }}
className="rounded-[2px] py-[4px] px-[6px] flex justify-center text-center text-white text-[14px] tracking-[0.16px] font-normal leading-[16px]"
>
WHO
{name}
</div>
</foreignObject>
</g>
Expand Down
Loading
Loading