import { ClickAwayListener, Skeleton } from '@mui/material'; import { useAtom } from 'jotai'; import { equals, flatten, isEmpty, isNil, pluck, reject } from 'ramda'; import { type MutableRefObject, type ReactElement, useEffect, useMemo, useRef, useState } from 'react'; import { useDeepCompare } from '../../utils'; import BarGroup from '../BarChart/BarGroup'; import AdditionalLine from '../common/BaseChart/AdditionalLine'; import BaseChart from '../common/BaseChart/BaseChart'; import ChartSvgWrapper from '../common/BaseChart/ChartSvgWrapper'; import { useComputeBaseChartDimensions } from '../common/BaseChart/useComputeBaseChartDimensions'; import { useComputeYAxisMaxCharacters } from '../common/BaseChart/useComputeYAxisMaxCharacters'; import type { Thresholds as ThresholdsModel } from '../common/models'; import Thresholds from '../common/Thresholds/Thresholds'; import { getUnits, getXScale, getXScaleBand, getYScalePerUnit } from '../common/timeSeries'; import type { Line } from '../common/timeSeries/models'; import { useMarginTop } from '../common/useMarginTop'; import Lines from './BasicComponents/Lines'; import { canDisplayThreshold, findLineOfOriginMetricThreshold, lowerLineName, upperLineName } from './BasicComponents/Lines/Threshold/models'; import { useChartStyles } from './Chart.styles'; import { margin } from './common'; import { thresholdTooltipAtom } from './graphAtoms'; import InteractionWithGraph from './InteractiveComponents'; import GraphValueTooltip from './InteractiveComponents/GraphValueTooltip/GraphValueTooltip'; import GraphTooltip from './InteractiveComponents/Tooltip'; import useGraphTooltip from './InteractiveComponents/Tooltip/useGraphTooltip'; import type { Data, GlobalAreaLines, GraphInterval, LineChartProps } from './models'; import { useIntersection } from './useChartIntersection'; interface Props extends LineChartProps { graphData: Data; graphInterval: GraphInterval; graphRef: MutableRefObject; limitLegend?: false | number; shapeLines?: GlobalAreaLines; thresholdUnit?: string; thresholds?: ThresholdsModel; transformMatrix?: { fx?: (pointX: number) => number; fy?: (pointY: number) => number; }; } const filterLines = ( lines: Array, displayThreshold: boolean ): Array => { if (!displayThreshold) { return lines; } const lineOriginMetric = findLineOfOriginMetricThreshold(lines); if (isEmpty(lineOriginMetric)) { return lines; } const findLinesUpperLower = lines.map((line) => equals(line.name, lowerLineName) || equals(line.name, upperLineName) ? line : null ); const linesUpperLower = reject((element) => !element, findLinesUpperLower); return [...lineOriginMetric, ...linesUpperLower] as Array; }; const Chart = ({ graphData, height = 500, width, shapeLines, axis, displayAnchor, zoomPreview, graphInterval, timeShiftZones, annotationEvent, tooltip, legend, graphRef, header, lineStyle, barStyle = { opacity: 1, radius: 0.2 }, thresholds, thresholdUnit, limitLegend, skipIntersectionObserver, transformMatrix, additionalLines, min, max, boundariesUnit }: Props): ReactElement => { const { classes } = useChartStyles(); const maxLeftAxisCharactersRef = useRef(0); const maxRightAxisCharactersRef = useRef(0); const { title, timeSeries, baseAxis, lines } = graphData; const [linesGraph, setLinesGraph] = useState>( filterLines(lines, canDisplayThreshold(shapeLines?.areaThresholdLines)) ); const graphSvgRef = useRef(null); const [thresholdTooltip, setThresholdTooltip] = useAtom(thresholdTooltipAtom); const { isInViewport } = useIntersection({ element: graphRef?.current }); const thresholdValues = flatten([ pluck('value', thresholds?.warning || []), pluck('value', thresholds?.critical || []) ]); const displayedLines = useMemo( () => linesGraph.filter(({ display }) => display), [linesGraph] ); const [firstUnit, secondUnit] = useMemo( () => getUnits(displayedLines), [displayedLines] ); const allUnits = getUnits(linesGraph); const { legendRef, graphWidth, graphHeight, titleRef } = useComputeBaseChartDimensions({ hasSecondUnit: Boolean(secondUnit), height, legendDisplay: legend?.display, legendHeight: legend?.height, legendPlacement: legend?.placement, maxLeftAxisCharacters: maxLeftAxisCharactersRef.current, maxRightAxisCharacters: maxRightAxisCharactersRef.current, title, units: allUnits, width }); const xScale = useMemo( () => getXScale({ dataTime: timeSeries, valueWidth: graphWidth }), [timeSeries, graphWidth] ); const xScaleBand = useMemo( () => getXScaleBand({ dataTime: timeSeries, valueWidth: graphWidth }), [timeSeries, graphWidth] ); const yScalesPerUnit = useMemo( () => getYScalePerUnit({ boundariesUnit, dataLines: linesGraph, dataTimeSeries: timeSeries, isCenteredZero: axis?.isCenteredZero, // @ts-expect-error - suppressing pre-existing type mismatch isFilled: lineStyle?.showArea, max, min, scale: axis?.scale, scaleLogarithmicBase: axis?.scaleLogarithmicBase, thresholds: (thresholds?.enabled && thresholdValues) || [], thresholdUnit, valueGraphHeight: graphHeight - margin.bottom }), [ linesGraph, timeSeries, graphHeight, thresholdValues, thresholds?.enabled, axis?.isCenteredZero, axis?.scale, axis?.scaleLogarithmicBase, boundariesUnit, // @ts-expect-error - suppressing pre-existing type mismatch lineStyle?.showArea, max, min, thresholdUnit ] ); const fallbackLeftUnit = axis?.axisYLeft?.unit ?? firstUnit ?? allUnits[0]; const fallbackRightUnit = axis?.axisYRight?.unit ?? secondUnit ?? allUnits[1]; const leftScale = yScalesPerUnit[fallbackLeftUnit]; const rightScale = yScalesPerUnit[fallbackRightUnit]; const { maxLeftAxisCharacters, maxRightAxisCharacters } = useComputeYAxisMaxCharacters({ axis, base: baseAxis, displayedLines, graphHeight, graphWidth, isHorizontal: false, leftScale, rightScale }); maxRightAxisCharactersRef.current = maxRightAxisCharacters; maxLeftAxisCharactersRef.current = maxLeftAxisCharacters; const linesDisplayedAsLine = useMemo( () => displayedLines.filter( ({ displayAs }) => isNil(displayAs) || equals(displayAs, 'line') ), [displayedLines] ); const linesDisplayedAsBar = useMemo( () => displayedLines.filter(({ displayAs }) => equals(displayAs, 'bar')), [displayedLines] ); useEffect(() => { setLinesGraph( filterLines(lines, canDisplayThreshold(shapeLines?.areaThresholdLines)) ); }, [ ...useDeepCompare([lines, shapeLines?.areaThresholdLines]), lines, shapeLines?.areaThresholdLines ]); const graphTooltipData = useGraphTooltip({ graphWidth, timeSeries, xScale }); const displayLegend = legend?.display ?? true; const displayTooltip = !isNil(tooltip?.renderComponent); const showGridLines = useMemo( () => isNil(axis?.showGridLines) || axis?.showGridLines, [axis?.showGridLines] ); const hasSecondUnit = useMemo(() => Boolean(secondUnit), [secondUnit]); const marginTop = useMarginTop({ title, units: allUnits }); if ((!isInViewport && !skipIntersectionObserver) || !height) { return ( ); } return (
{!isEmpty(linesDisplayedAsBar) && ( )} {!isEmpty(linesDisplayedAsLine) && ( )} {additionalLines?.map((additionalLine) => ( ))} {thresholds?.enabled && ( setThresholdTooltip(null)} showTooltip={({ tooltipData: thresholdLabel }) => setThresholdTooltip({ thresholdLabel }) } thresholds={thresholds as ThresholdsModel} thresholdUnit={thresholdUnit} width={graphWidth} yScalesPerUnit={yScalesPerUnit} /> )}
{displayTooltip && }
); }; export default Chart;