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

Analytics: Download toast added #2200

Merged
merged 13 commits into from
Oct 27, 2024
9 changes: 4 additions & 5 deletions platform/public/icons/Analytics/checkCircleIcon.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React from 'react';

const checkCircleIcon = (props) => {
const CheckCircleIcon = ({ width, height }) => {
return (
<svg
width={20}
height={20}
width={width || 20}
height={height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M18.334 9.237v.767a8.334 8.334 0 11-4.942-7.617m4.942.946L10 11.674l-2.5-2.5"
Expand All @@ -21,4 +20,4 @@ const checkCircleIcon = (props) => {
);
};

export default checkCircleIcon;
export default CheckCircleIcon;
72 changes: 30 additions & 42 deletions platform/src/common/components/Charts/ChartContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// src/components/Charts/ChartContainer.jsx

import React, { useRef, useCallback, useEffect, memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { jsPDF } from 'jspdf';
import html2canvas from 'html2canvas';
import CheckIcon from '@/icons/tickIcon';
import CustomDropdown from '@/components/Dropdowns/CustomDropdown';
// import PrintReportModal from '@/components/Modal/PrintReportModal';
// import PrintReportModal from '@/components/Modal/PrintReportModal'; // Retained as per request
import MoreInsightsChart from './MoreInsightsChart';
import SkeletonLoader from './components/SkeletonLoader';
import { setOpenModal, setModalType } from '@/lib/store/services/downloadModal';
import useFetchAnalyticsData from '@/core/utils/useFetchAnalyticsData';
// import { toast } from 'sonner';
// import checkCircleIcon from '@/icons/Analytics/checkCircleIcon';
import CustomToast from '../Toast/CustomToast';

const ChartContainer = memo(
({
Expand All @@ -21,43 +21,28 @@ const ChartContainer = memo(
width = '100%',
id,
showTitle = true,
data = [],
chartLoading,
error,
refetch,
}) => {
const dispatch = useDispatch();
const chartRef = useRef(null);
const dropdownRef = useRef(null);

// Extract necessary data from Redux store
const {
chartDataRange,
chartSites,
timeFrame,
organizationName,
pollutionType,
} = useSelector((state) => state.chart);

const { chartSites, timeFrame, pollutionType } = useSelector(
(state) => state.chart,
);
const preferencesData = useSelector(
(state) => state.defaults.individual_preferences,
);
const user_selected_sites = preferencesData?.[0]?.selected_sites || [];

// State for handling sharing and exporting
// const [openShare, setOpenShare] = useState(false);
// const [shareFormat, setShareFormat] = useState(null);
const [loadingFormat, setLoadingFormat] = React.useState(null);
const [downloadComplete, setDownloadComplete] = React.useState(null);

// Fetch analytics data using the custom hook
const { allSiteData, chartLoading, error, refetch } = useFetchAnalyticsData(
{
selectedSiteIds: chartSites,
dateRange: chartDataRange,
chartType,
frequency: timeFrame,
pollutant: pollutionType,
organisationName: organizationName,
},
);

// Handle click outside for dropdown
useEffect(() => {
const handleClickOutside = (event) => {
Expand Down Expand Up @@ -97,9 +82,9 @@ const ChartContainer = memo(
const link = document.createElement('a');
link.href = imgData;
link.download = `airquality-data.${format}`;
document.body.appendChild(link); // Append to body to make it clickable in Firefox
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // Remove after clicking
document.body.removeChild(link);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using URL.createObjectURL for downloads

Instead of manipulating the DOM directly, consider using URL.createObjectURL for a cleaner approach to handling downloads.

- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ const url = URL.createObjectURL(new Blob([imgData]));
+ link.href = url;
+ link.click();
+ URL.revokeObjectURL(url);

Committable suggestion was skipped due to low confidence.

} else if (format === 'pdf') {
const pdf = new jsPDF({
orientation: 'landscape',
Expand All @@ -114,12 +99,7 @@ const ChartContainer = memo(
}

setDownloadComplete(format);
// toast('Download complete', {
// className: 'bg-black text-white p-2 rounded-md max-w-xs',
// duration: 5000,
// position: 'bottom-center',
// icon: <checkCircleIcon width={20} height={20} />,
// });
CustomToast();
} catch (error) {
console.error('Error exporting chart:', error);
} finally {
Expand Down Expand Up @@ -182,15 +162,17 @@ const ChartContainer = memo(
>
More insights
</button>
{/* {['csv', 'pdf'].map((format) => (
{/*
{['csv', 'pdf'].map((format) => (
<button
key={format}
onClick={() => shareReport(format)}
className="flex justify-between items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<span>Share report as {format.toUpperCase()}</span>
</button>
))} */}
))}
*/}
</>
),
[
Expand All @@ -204,7 +186,7 @@ const ChartContainer = memo(
);

const ErrorOverlay = () => (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-gray-400 bg-opacity-50 z-10">
<p className="text-red-500 font-semibold">
Something went wrong. Please try again.
</p>
Expand Down Expand Up @@ -249,9 +231,9 @@ const ChartContainer = memo(

{!chartLoading && error && <ErrorOverlay />}

{!chartLoading && !error && allSiteData?.length > 0 ? (
{!chartLoading && !error && data?.length > 0 && (
<MoreInsightsChart
data={allSiteData}
data={data}
selectedSites={chartSites}
chartType={chartType}
frequency={timeFrame}
Expand All @@ -261,11 +243,12 @@ const ChartContainer = memo(
pollutantType={pollutionType}
isLoading={chartLoading}
/>
) : null}
)}
</div>
</div>

{/* <PrintReportModal
{/*
<PrintReportModal
title="Share Report"
btnText="Send"
shareModel
Expand All @@ -277,7 +260,8 @@ const ChartContainer = memo(
endDate: chartDataRange.endDate,
sites: chartSites,
}}
/> */}
/>
*/}
</div>
);
},
Expand All @@ -290,6 +274,10 @@ ChartContainer.propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
id: PropTypes.string.isRequired,
showTitle: PropTypes.bool,
data: PropTypes.array.isRequired,
chartLoading: PropTypes.bool.isRequired,
error: PropTypes.string,
refetch: PropTypes.func.isRequired,
// defaultBody: PropTypes.object, // Commented out as per your request
};

Expand Down
73 changes: 33 additions & 40 deletions platform/src/common/components/Charts/MoreInsightsChart.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
// MoreInsightsChart.jsx
import React, { useState, useCallback, useMemo, useRef } from 'react';
import {
LineChart,
Line,
Expand Down Expand Up @@ -27,24 +28,30 @@ import { parseAndValidateISODate } from '@/core/utils/dateUtils';
import { WHO_STANDARD_VALUES } from './constants';
import { formatYAxisTick, CustomizedAxisTick } from './utils';
import SkeletonLoader from './components/SkeletonLoader';
import useResizeObserver from '@/core/utils/useResizeObserver';

/**
* MoreInsightsChart Component
*/
const MoreInsightsChart = React.memo(
({
data = [],
selectedSites = [], // Array of site IDs; if empty, include all sites
selectedSites = [],
chartType = 'line',
frequency = 'daily',
width = '100%',
height = '300px',
id,
pollutantType,
isLoading = false,
isLoading,
}) => {
const [activeIndex, setActiveIndex] = useState(null);

// Reference to the chart container
const containerRef = useRef(null);
// Use the custom hook to get container dimensions
const { width: containerWidth } = useResizeObserver(containerRef);

/**
* Processes raw chart data by validating dates and organizing data by time and site.
*/
Expand Down Expand Up @@ -152,40 +159,31 @@ const MoreInsightsChart = React.memo(
const DataComponent = chartType === 'line' ? Line : Bar;

/**
* Calculates the interval for the X-axis ticks based on screen width.
* Calculate step based on container width and number of ticks.
* Assume each label requires a minimum width
*/
const calculateXAxisInterval = useCallback(() => {
const screenWidth = window.innerWidth;
if (screenWidth < 768) return Math.ceil(chartData.length / 4);
if (screenWidth < 1024) return Math.ceil(chartData.length / 6);
return Math.ceil(chartData.length / 8);
}, [chartData.length]);
const calculateStep = useCallback(() => {
const minLabelWidth = 40;
if (containerWidth === 0) return 1;
const maxLabels = Math.floor(containerWidth / minLabelWidth);
const step = Math.ceil(chartData.length / maxLabels);
return step;
}, [containerWidth, chartData.length]);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Smart approach to dynamic step calculation! 🎯

The calculateStep function elegantly handles the label spacing based on container width. The magic number 40 for minLabelWidth could be made more maintainable.

Consider extracting the configuration:

+const CHART_CONFIG = {
+  MIN_LABEL_WIDTH: 40,
+  DEFAULT_STEP: 1
+};

 const calculateStep = useCallback(() => {
-  const minLabelWidth = 40;
+  const minLabelWidth = CHART_CONFIG.MIN_LABEL_WIDTH;
   if (containerWidth === 0) return 1;
   const maxLabels = Math.floor(containerWidth / minLabelWidth);
   const step = Math.ceil(chartData.length / maxLabels);
   return step;
 }, [containerWidth, chartData.length]);

Committable suggestion was skipped due to low confidence.

/**
* Memoized X-axis interval
* Memoized step for labels
*/
const xAxisInterval = useMemo(
() => calculateXAxisInterval(),
[calculateXAxisInterval],
);

/**
* Effect to update X-axis interval on window resize for responsiveness
*/
useEffect(() => {
const handleResize = () => {
// Force recalculation by updating a state or triggering a re-render
// Here, we do nothing because xAxisInterval is recalculated on dependency change
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const step = useMemo(() => calculateStep(), [calculateStep]);

/**
* Render the chart or appropriate messages based on state
*/
const renderChart = useMemo(() => {
if (chartData.length === 0 && !isLoading) {
if (isLoading) {
return <SkeletonLoader width={width} height={height} />;
}

if (chartData.length === 0) {
return (
<div className="w-full flex flex-col justify-center items-center h-[380px] text-gray-500">
<p className="text-lg font-medium mb-2">No Data Available</p>
Expand All @@ -200,7 +198,7 @@ const MoreInsightsChart = React.memo(
<ResponsiveContainer width={width} height={height}>
<ChartComponent
data={chartData}
margin={{ top: 38, right: 10, left: -15, bottom: 10 }}
margin={{ top: 38, right: 10, left: -15, bottom: 20 }}
style={{ cursor: 'pointer' }}
>
{/* Grid */}
Expand All @@ -213,7 +211,7 @@ const MoreInsightsChart = React.memo(
{/* X-Axis */}
<XAxis
dataKey="time"
tickLine
tickLine={false}
tick={({ x, y, payload, fill, index }) => (
<CustomizedAxisTick
x={x}
Expand All @@ -222,12 +220,11 @@ const MoreInsightsChart = React.memo(
fill={fill}
frequency={frequency}
index={index}
numTicks={chartData.length}
step={step}
/>
)}
interval={xAxisInterval}
axisLine={false}
scale="point"
scale="auto"
padding={{ left: 30, right: 30 }}
/>

Expand Down Expand Up @@ -318,7 +315,7 @@ const MoreInsightsChart = React.memo(
chartType,
width,
height,
xAxisInterval,
step,
getColor,
handleMouseLeave,
activeIndex,
Expand All @@ -332,12 +329,8 @@ const MoreInsightsChart = React.memo(
]);

return (
<div id={id} className="pt-4">
{isLoading ? (
<SkeletonLoader width={width} height={height} />
) : (
renderChart
)}
<div id={id} ref={containerRef} className="pt-4">
{renderChart}
</div>
);
},
Expand Down
20 changes: 2 additions & 18 deletions platform/src/common/components/Charts/utils/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,8 @@ export const CustomizedAxisTick = ({
fill,
frequency,
index,
numTicks,
step,
}) => {
/**
* Formats the date based on the frequency.
* @param {string} value - The raw ISO date string.
* @returns {string} - Formatted date string.
*/
const formatDate = (value) => {
const date = parseAndValidateISODate(value);
if (!date) {
Expand All @@ -62,21 +57,10 @@ export const CustomizedAxisTick = ({
}
};

/**
* Controls the visibility of the X-axis ticks to avoid overcrowding.
* If there are too many ticks, we hide some based on the index.
*/
const shouldDisplayTick = () => {
if (frequency === 'weekly') {
return index % 2 === 0; // Show every second tick for weekly frequency
}
if (frequency === 'daily') {
return index % Math.floor(numTicks / 5) === 0; // Show fewer ticks for daily frequency
}
return true; // Show all ticks for other frequencies
return step > 1 ? index % step === 0 : true;
};

// If the tick should not be displayed, return null.
if (!shouldDisplayTick()) {
return null;
}
Expand Down
Loading
Loading