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

feat: [CGC-65] Automatic time interval selection #83

Merged
merged 11 commits into from
Sep 20, 2024
15 changes: 11 additions & 4 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-07-31T11:25:42.922Z\n"
"PO-Revision-Date: 2024-07-31T11:25:42.922Z\n"
"POT-Creation-Date: 2024-09-20T08:45:15.376Z\n"
"PO-Revision-Date: 2024-09-20T08:45:15.376Z\n"

msgid ""
"Failed to load data. Please check that you have selected the correct "
"program stage ID in the configuration."
"programStageId in the configuration."
msgstr ""
"Failed to load data. Please check that you have selected the correct "
"program stage ID in the configuration."
"programStageId in the configuration."

msgid "Growth Chart"
msgstr "Growth Chart"
Expand All @@ -38,6 +38,13 @@ msgstr "The default indicator"
msgid "is not a valid. Please select a valid indicator in the configuration"
msgstr "is not a valid. Please select a valid indicator in the configuration"

msgid ""
"There was an error fetching the tracked entity for the growth chart. Please "
"check the configuration in Datastore Management and try again."
msgstr ""
"There was an error fetching the tracked entity for the growth chart. Please "
"check the configuration in Datastore Management and try again."

msgid "Print"
msgstr "Print"

Expand Down
9 changes: 8 additions & 1 deletion src/Plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GenericLoading } from './UI/GenericLoading';
import { useCustomReferences } from './utils/DataFetching/Hooks/useCustomReferences';
import { chartData } from './DataSets/WhoStandardDataSets/ChartData';
import { ConfigError, CustomReferenceError, DefaultIndicatorError } from './UI/FeedbackComponents';
import { TrackedEntityError } from './UI/FeedbackComponents/TrackedEntityError';
import { GenericError } from './UI/GenericError';

const queryClient = new QueryClient();
Expand Down Expand Up @@ -84,7 +85,7 @@ const PluginInner = (propsFromParent: EnrollmentOverviewProps) => {
);
}

if (isErrorTei || isErrorEvents) {
if (isErrorEvents) {
return (
<GenericError
errorMessage={i18n.t('Failed to load data. Please check that you have selected the correct programStageId in the configuration.')}
Expand All @@ -104,6 +105,12 @@ const PluginInner = (propsFromParent: EnrollmentOverviewProps) => {
);
}

if (isErrorTei) {
return (
<TrackedEntityError />
);
}

return (
<QueryClientProvider client={queryClient}>
<div className='bg-white w-screen flex m-0 p-0'>
Expand Down
10 changes: 10 additions & 0 deletions src/UI/FeedbackComponents/TrackedEntityError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import { GenericError } from '../GenericError';

export const TrackedEntityError = () => (
<GenericError
/* eslint-disable-next-line max-len */
errorMessage={i18n.t('There was an error fetching the tracked entity for the growth chart. Please check the configuration in Datastore Management and try again.')}
/>
);
54 changes: 28 additions & 26 deletions src/components/GrowthChart/GrowthChart.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { useEffect, useMemo, useState } from 'react';
import { differenceInMonths, differenceInWeeks } from 'date-fns';
import { GrowthChartBuilder } from './GrowthChartBuilder';
import { ChartSelector } from './GrowthChartSelector';
import { CategoryCodes, ChartData, GenderCodes, MeasurementData } from '../../types/chartDataTypes';
import { ChartData, GenderCodes, MeasurementData } from '../../types/chartDataTypes';
import { useCalculateMinMaxValues } from '../../utils/Hooks/Calculations';
import { ChartSettingsButton } from './ChartSettingsButton';
import { useChartDataForGender } from '../../utils/DataFetching/Sorting';
import { MappedEntityValues } from '../../utils/DataFetching/Sorting/useMappedTrackedEntity';
import { useAppropriateChartData } from '../../utils/Hooks/Calculations/useAppropriateChartData';

interface GrowthChartProps {
trackedEntity: MappedEntityValues;
Expand All @@ -25,38 +27,38 @@ export const GrowthChart = ({
setDefaultIndicatorError,
}: GrowthChartProps) => {
const trackedEntityGender = trackedEntity?.gender;
const dateOfBirth = useMemo(() => new Date(trackedEntity.dateOfBirth), [trackedEntity.dateOfBirth]);
edvinstava marked this conversation as resolved.
Show resolved Hide resolved
const childAgeInWeeks = useMemo(() => differenceInWeeks(new Date(), dateOfBirth), [dateOfBirth]);
const childAgeInMonths = useMemo(() => differenceInMonths(new Date(), dateOfBirth), [dateOfBirth]);

const [gender, setGender] = useState<string>(trackedEntityGender !== undefined ? trackedEntityGender : GenderCodes.CGC_Female);
const { chartDataForGender } = useChartDataForGender({
gender,
chartData,
});

const [category, setCategory] = useState<keyof typeof CategoryCodes>();
const [dataset, setDataset] = useState<string>();

const isKeyOfCategoryCodes = (key: string): key is keyof typeof CategoryCodes => key in CategoryCodes;

useEffect(() => {
const key = `${defaultIndicator}_${gender.charAt(0).toLowerCase()}`;
if (!isKeyOfCategoryCodes(key)) {
setDefaultIndicatorError(true);
}
if (isKeyOfCategoryCodes(key) && chartDataForGender[key]) {
const newCategory = CategoryCodes[key];
setCategory(newCategory);
const newDataset = Object.keys(chartDataForGender[newCategory].datasets)[0];
setDataset(newDataset);
}
}, [chartDataForGender, defaultIndicator, gender, setDefaultIndicatorError]);
const {
selectedCategory,
selectedDataset,
setSelectedCategory: setCategory,
setSelectedDataset: setDataset,
} = useAppropriateChartData(
chartDataForGender,
defaultIndicator,
gender,
setDefaultIndicatorError,
childAgeInWeeks,
childAgeInMonths,
);

useEffect(() => {
if (trackedEntity && Object.values(GenderCodes).includes(trackedEntity.gender)) {
if (trackedEntity && Object.values(GenderCodes)
.includes(trackedEntity.gender)) {
setGender(trackedEntity.gender);
}
}, [trackedEntity]);

const dataSetEntry = chartDataForGender[category]?.datasets[dataset];
const dataSetEntry = chartDataForGender[selectedCategory]?.datasets[selectedDataset];

const dataSetValues = isPercentiles ? dataSetEntry?.percentileDatasetValues : dataSetEntry?.zScoreDatasetValues;
const dataSetMetadata = dataSetEntry?.metadata;
Expand All @@ -82,8 +84,8 @@ export const GrowthChart = ({
<>
<div className='flex justify-between px-14'>
<ChartSelector
category={category}
dataset={dataset}
category={selectedCategory}
dataset={selectedDataset}
setCategory={setCategory}
setDataset={setDataset}
chartData={chartDataForGender}
Expand All @@ -93,8 +95,8 @@ export const GrowthChart = ({
/>
<div>
<ChartSettingsButton
category={category}
dataset={dataset}
category={selectedCategory}
dataset={selectedDataset}
gender={gender}
trackedEntity={trackedEntity}
/>
Expand All @@ -109,8 +111,8 @@ export const GrowthChart = ({
yAxisValues={yAxisValues}
keysDataSet={keysDataSet}
dateOfBirth={new Date(trackedEntity?.dateOfBirth)}
category={category}
dataset={dataset}
category={selectedCategory}
dataset={selectedDataset}
isPercentiles={isPercentiles}
/>
</div>
Expand Down
83 changes: 83 additions & 0 deletions src/utils/Hooks/Calculations/useAppropriateChartData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect, useRef, useState } from 'react';
import { CategoryCodes, ChartData, MeasurementTypeCodesLabel, TimeUnitCodes } from '../../../types/chartDataTypes';

export const useAppropriateChartData = (
chartDataForGender: ChartData,
defaultIndicator: string,
gender: string,
setDefaultIndicatorError: (value: boolean) => void,
childAgeInWeeks: number,
childAgeInMonths: number,
) => {
const [selectedCategory, setSelectedCategory] = useState<keyof typeof CategoryCodes>();
const [selectedDataset, setSelectedDataset] = useState<string>();

const selectDatasetForCategoryRef = useRef<(category: keyof typeof CategoryCodes) => void>();
selectDatasetForCategoryRef.current = (category: keyof typeof CategoryCodes) => {
const { datasets } = chartDataForGender[category];

const isMeasurementType = (xAxis: string) =>
Object.values(MeasurementTypeCodesLabel)
.includes(xAxis);

const isWeeksInRange = (xAxis: string) =>
xAxis === TimeUnitCodes.weeks && childAgeInWeeks < 13;

const isMonthsInRange = (xAxis: string, range: { start: number, end: number }) =>
xAxis === TimeUnitCodes.months && childAgeInMonths >= range.start && childAgeInMonths < range.end;
edvinstava marked this conversation as resolved.
Show resolved Hide resolved

const getMaxRangeDataset = (datasets: ChartData[0]['datasets']) =>
Object.entries(datasets)
.reduce((max, [key, value]) =>
((!max || value.metadata.range.end > max[1].metadata.range.end) ? [key, value] : max));

const isAboveRange = (xAxis: string, range: { start: number, end: number }) =>
xAxis === TimeUnitCodes.months && childAgeInMonths >= range.end;
Object.entries(datasets)
.some(([key, value]) => {
const { range } = value.metadata;
const xAxis = value.metadata.xAxisLabel;

if (isMeasurementType(xAxis) || isWeeksInRange(xAxis) || isMonthsInRange(xAxis, range)) {
setSelectedDataset((prevDataset) => (prevDataset !== key ? key : prevDataset));
return true;
}

if (isAboveRange(xAxis, range)) {
const [newDatasetKey] = getMaxRangeDataset(datasets);
setSelectedDataset(newDatasetKey);
return true;
}
return false;
});
};

useEffect(() => {
if (selectedCategory && chartDataForGender[selectedCategory]) {
selectDatasetForCategoryRef.current?.(selectedCategory);
}
}, [selectedCategory, chartDataForGender]);

const isKeyOfCategoryCodes = (key: string): key is keyof typeof CategoryCodes => key in CategoryCodes;

useEffect(() => {
const key = `${defaultIndicator}_${gender.charAt(0)
.toLowerCase()}`;
if (!isKeyOfCategoryCodes(key)) {
setDefaultIndicatorError(true);
}
if (isKeyOfCategoryCodes(key) && chartDataForGender[key]) {
const newCategory = CategoryCodes[key];
setSelectedCategory(newCategory);
const newDataset = Object.keys(chartDataForGender[newCategory].datasets)[0];
setSelectedDataset(newDataset);
}
}, [chartDataForGender, defaultIndicator, gender, setDefaultIndicatorError]);

return {
selectedCategory,
selectedDataset,
setSelectedCategory,
setSelectedDataset,
};
};
Loading