import React, { type ComponentProps, useState, useCallback, useMemo, useLayoutEffect, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, InlineLoading, SkeletonPlaceholder, SkeletonText } from '@carbon/react'; import { LineChart, ScaleTypes, TickRotations } from '@carbon/charts-react'; import { ArrowLeftIcon, ConfigurableLink, formatDate } from '@openmrs/esm-framework'; import { EmptyState, type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import { testResultsBasePath } from '../helpers'; import { useObstreeData } from './trendline-resource'; import { formatReferenceRange } from '../grouped-timeline/reference-range-helpers'; import CommonDataTable from '../overview/common-datatable.component'; import RangeSelector from './range-selector.component'; import styles from './trendline.scss'; interface TrendlineProps { conceptUuid: string; patientUuid: string; hideTrendlineHeader?: boolean; showBackToTimelineButton?: boolean; } interface TrendLineBackgroundProps { children?: React.ReactNode; } const TrendLineBackground: React.FC = ({ ...props }) => (
); interface TrendlineHeaderProps { isValidating: boolean; patientUuid: string; showBackToTimelineButton: boolean; title: string; } const TrendlineHeader: React.FC = ({ patientUuid, title, isValidating, showBackToTimelineButton, }) => { const { t } = useTranslation(); return (
{showBackToTimelineButton && ( )}
{title}
{isValidating && }
); }; const Trendline: React.FC = ({ patientUuid, conceptUuid, hideTrendlineHeader = false, showBackToTimelineButton = false, }) => { const { t } = useTranslation(); const { trendlineData, isLoading, isValidating } = useObstreeData(patientUuid, conceptUuid); const { obs, display: chartTitle, hiNormal, lowNormal, units: leftAxisTitle } = trendlineData; const bottomAxisTitle = t('date', 'Date'); const [range, setRange] = useState<[Date, Date]>(); const [showResultsTable, setShowResultsTable] = useState(false); const [upperRange, lowerRange] = useMemo(() => { if (obs.length === 0) { return [new Date(), new Date()]; } return [new Date(), new Date(Date.parse(obs[obs.length - 1].obsDatetime))]; }, [obs]); const setLowerRange = useCallback( (selectedLowerRange: Date) => { setRange([selectedLowerRange > lowerRange ? selectedLowerRange : lowerRange, upperRange]); }, [setRange, upperRange, lowerRange], ); /** * reorder svg element to bring line in front of the area */ useLayoutEffect(() => { const graph = document.querySelector('g.cds--cc--area')?.parentElement; if (obs && obs.length && graph) { graph.insertBefore(graph.children[3], graph.childNodes[2]); } }, [obs]); useEffect(() => { setRange(undefined); }, [conceptUuid]); const { data, tableData } = useMemo(() => { const chartData: Array<{ date: Date; value: number; group: string; min?: number; max?: number; rangeLabel?: string; }> = []; const table: Array<{ id: string; dateTime: string; range: string; value: { value: number; interpretation: OBSERVATION_INTERPRETATION; }; }> = []; obs.forEach((observation, idx) => { const resolvedLow = observation.lowNormal ?? lowNormal; const resolvedHigh = observation.hiNormal ?? hiNormal; const hasRange = resolvedLow !== undefined && resolvedHigh !== undefined; const normalRange = hasRange ? { max: resolvedHigh, min: resolvedLow, } : {}; const rangeLabel = formatReferenceRange( hasRange ? { lowNormal: resolvedLow, hiNormal: resolvedHigh, units: leftAxisTitle, } : null, leftAxisTitle, ); chartData.push({ date: new Date(Date.parse(observation.obsDatetime)), value: parseFloat(observation.value), group: chartTitle, ...normalRange, rangeLabel, }); table.push({ id: `${idx}`, dateTime: observation.obsDatetime, range: rangeLabel, value: { value: parseFloat(observation.value), interpretation: observation.interpretation, }, }); }); return { data: chartData, tableData: table }; }, [obs, chartTitle, hiNormal, leftAxisTitle, lowNormal]); const chartOptions = useMemo( () => ({ bounds: { lowerBoundMapsTo: 'min', upperBoundMapsTo: 'max', }, axes: { bottom: { title: bottomAxisTitle, mapsTo: 'date', scaleType: ScaleTypes.TIME, ticks: { rotation: TickRotations.ALWAYS, }, domain: range, }, left: { mapsTo: 'value', title: leftAxisTitle, scaleType: ScaleTypes.LINEAR, includeZero: false, }, }, height: '400px', color: { scale: { [chartTitle]: '#6929c4', }, }, points: { radius: 4, enabled: true, }, legend: { enabled: false, }, tooltip: { customHTML: ([{ date, value, rangeLabel }]) => { const valueLabel = t('trendlineTooltipValue', 'Value'); const dateLabel = t('trendlineTooltipDate', 'Date'); const rangeLabelText = t('trendlineTooltipRange', 'Range'); return `
${valueLabel}: ${value} ${leftAxisTitle}
${rangeLabelText}: ${rangeLabel || '--'}
${dateLabel}: ${formatDate(date)}
`; }, }, toolbar: { enabled: true, numberOfIcons: 4, controls: [ { type: 'Zoom in', }, { type: 'Zoom out', }, { type: 'Reset zoom', }, { type: 'Export as CSV', }, { type: 'Export as PNG', }, { type: 'Make fullscreen', }, ], }, zoomBar: { top: { enabled: true, }, }, }), [bottomAxisTitle, leftAxisTitle, range, chartTitle, t], ); const tableHeaderData = useMemo( () => [ { header: t('dateTime', 'Date and time'), key: 'dateTime', }, { header: leftAxisTitle ? `${t('value', 'Value')} (${leftAxisTitle})` : t('value', 'Value'), key: 'value', }, { header: t('referenceRange', 'Reference range'), key: 'range', }, ], [leftAxisTitle, t], ); if (isLoading) { return (
); } if (obs.length === 0) { return (
); } return (
{!hideTrendlineHeader && ( )}
{showResultsTable ? ( <> ) : ( )}
); }; interface DrawTableProps { tableData: Array<{ id: string; dateTime: string; range: string; value: { value: number; interpretation: OBSERVATION_INTERPRETATION; }; }>; tableHeaderData: Array<{ header: string; key: string; }>; } const DrawTable = React.memo(({ tableData, tableHeaderData }) => { return ; }); DrawTable.displayName = 'DrawTable'; export default Trendline;