import { Box, Grid2 as Grid, Grid2Props as GridProps } from "@mui/material"; import { useAtomValue } from "jotai"; import { Area, AreaChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import type { DotProps } from "recharts"; import { boardAtom, currentPositionAtom, gameAtom, gameEvalAtom, } from "../../states"; import { useCallback, useMemo } from "react"; import type { ReactElement } from "react"; import CustomTooltip from "./tooltip"; import { ChartItemData } from "./types"; import { PositionEval } from "@/types/eval"; import { CLASSIFICATION_COLORS } from "@/constants"; import CustomDot from "./dot"; import { MoveClassification } from "@/types/enums"; import { useChessActions } from "@/hooks/useChessActions"; export default function GraphTab(props: GridProps) { const gameEval = useAtomValue(gameEvalAtom); const currentPosition = useAtomValue(currentPositionAtom); const { goToMove } = useChessActions(boardAtom); const game = useAtomValue(gameAtom); const chartData: ChartItemData[] = useMemo( () => gameEval?.positions.map(formatEvalToChartData) ?? [], [gameEval] ); const bestDotIndices = useMemo(() => { const bestItems = chartData.filter( (item) => item.moveClassification === MoveClassification.Best ); const count = Math.ceil(bestItems.length * 0.15); const indices = bestItems.map((item) => item.moveNb); for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [indices[i], indices[j]] = [indices[j], indices[i]]; } return new Set(indices.slice(0, count)); }, [chartData]); const boardMoveColor = currentPosition.eval?.moveClassification ? CLASSIFICATION_COLORS[currentPosition.eval.moveClassification] : "grey"; // Render a dot only on selected classifications (always returns an element) const renderDot = useCallback( ( props: DotProps & { payload?: ChartItemData } ): ReactElement => { const payload = props.payload; const moveClass = payload?.moveClassification; if (!moveClass) return ; if ( [ MoveClassification.Splendid, MoveClassification.Perfect, MoveClassification.Blunder, MoveClassification.Mistake, ].includes(moveClass) || (moveClass === MoveClassification.Best && bestDotIndices.has(payload.moveNb)) ) { return ; } return ; }, [bestDotIndices] ); if (!gameEval) return null; return ( { const payload = e?.activePayload?.[0]?.payload as | ChartItemData | undefined; if (!payload) return; goToMove(payload.moveNb, game); }} style={{ cursor: "pointer" }} > } isAnimationActive={false} cursor={{ stroke: "grey", strokeWidth: 2, strokeOpacity: 0.3, }} /> } isAnimationActive={false} /> ); } const formatEvalToChartData = ( position: PositionEval, index: number ): ChartItemData => { const line = position.lines[0]; const chartItem: ChartItemData = { moveNb: index, value: 10, cp: line.cp, mate: line.mate, moveClassification: position.moveClassification, }; if (line.mate) { return { ...chartItem, value: line.mate > 0 ? 20 : 0, }; } if (line.cp) { return { ...chartItem, value: Math.max(Math.min(line.cp / 100, 10), -10) + 10, }; } return chartItem; };