import { useMemo, useCallback } from 'react'; import { ResponsiveLine } from '@nivo/line'; import type { DefaultSeries, LineCustomSvgLayerProps } from '@nivo/line'; import { formatAxisLabel, getChartXAxisTickValues } from '@/utils/formatting'; import type { RevisionMarker } from '@/api/getPageRevisionsData'; interface PageRevisionsGraphProps { timestamps: number[]; engagementSeries: number[]; revisions: RevisionMarker[]; selectedRevisionId: number | null; onRevisionSelect: ( id: number | null ) => void; } type RevisionLayerProps = LineCustomSvgLayerProps & { revisions: RevisionMarker[]; selectedRevisionId: number | null; onRevisionSelect: ( id: number | null ) => void; }; const CIRCLE_R = 6; const CIRCLE_TOP = -20; /** 7 days in milliseconds — half-width of the shaded highlight region. */ const SHADE_MS = 7 * 24 * 60 * 60 * 1000; /** * Custom Nivo SVG layer that renders revision markers: * - A shaded green region + vertical line for the selected revision. * - A clickable circle at the top of the chart for every revision. * * @param {RevisionLayerProps} props - Nivo layer context + revision props. * @return {JSX.Element|null} SVG layer content. */ // fallow-ignore-next-line complexity function RevisionMarkersLayer( props: RevisionLayerProps ) { const { xScale, innerHeight, revisions, selectedRevisionId, onRevisionSelect } = props; if ( ! revisions.length || ! xScale ) { return null; } return ( <> { // fallow-ignore-next-line complexity revisions.map( ( revision ) => { const tsMs = revision.timestamp * 1000; const x = xScale( new Date( tsMs ) as never ); const isSelected = revision.id === selectedRevisionId; if ( isNaN( x ) ) { return null; } const shadeLeft = xScale( new Date( tsMs - SHADE_MS ) as never ); const shadeRight = xScale( new Date( tsMs + SHADE_MS ) as never ); return ( {isSelected && ( )} {isSelected && ( )} onRevisionSelect( isSelected ? null : revision.id )} /> ); })} ); } /** * PageRevisionsGraph renders the engagement score line chart with revision markers. * Built on top of Nivo ResponsiveLine, following InsightsGraph.js patterns. * * @param {PageRevisionsGraphProps} props - Component props. * @return {JSX.Element} The engagement score chart. */ // fallow-ignore-next-line complexity const PageRevisionsGraph = ({ timestamps, engagementSeries, revisions, selectedRevisionId, onRevisionSelect }: PageRevisionsGraphProps ) => { const nivoData = useMemo( () => { if ( ! timestamps.length ) { return []; } return [ { id: 'engagement', color: 'var(--color-blue-500)', data: timestamps.map( ( ts, i ) => ({ x: new Date( ts * 1000 ), y: engagementSeries?.[i] ?? 0 }) ) } ]; }, [ timestamps, engagementSeries ]); const xTickValues = useMemo( () => getChartXAxisTickValues( timestamps.map( ( ts ) => new Date( ts * 1000 ) ) ), [ timestamps ] ); const formatTick = useCallback( ( value: Date | number ) => { const ts = value instanceof Date ? value.getTime() / 1000 : Number( value ) / 1000; return formatAxisLabel( ts, 'day', false ); }, []); const revisionLayer = useCallback( ( layerProps: LineCustomSvgLayerProps ) => ( ), [ revisions, selectedRevisionId, onRevisionSelect ] ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const layers: any[] = useMemo( () => [ 'grid', 'axes', 'areas', revisionLayer, 'lines', 'points', 'mesh' ], [ revisionLayer ] ); return ( ); }; export default PageRevisionsGraph;