Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Analysis/AnalysisSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const AnalysisSidebar: React.FC<Props> = ({
const movesByRatingProps = {
moves: analysisEnabled ? controller.movesByRating : undefined,
colorSanMapping: analysisEnabled ? controller.colorSanMapping : {},
positionKey: analysisEnabled ? controller.currentNode?.fen : undefined,
}

const renderHeader = (
Expand Down
82 changes: 53 additions & 29 deletions src/components/Analysis/MovesByRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,53 @@ import {
CartesianGrid,
ResponsiveContainer,
} from 'recharts'
import { useContext } from 'react'
import { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { ColorSanMapping } from 'src/types'
import { WindowSizeContext } from 'src/contexts'

interface Props {
moves: { [key: string]: number }[] | undefined
colorSanMapping: ColorSanMapping
isHomePage?: boolean
positionKey?: string
}

export const MovesByRating: React.FC<Props> = ({
moves,
colorSanMapping,
isHomePage = false,
positionKey,
}: Props) => {
const { isMobile } = useContext(WindowSizeContext)
const [displayedMoves, setDisplayedMoves] = useState(moves)
const [displayedColorSanMapping, setDisplayedColorSanMapping] =
useState(colorSanMapping)
const [displayedPositionKey, setDisplayedPositionKey] = useState(positionKey)
const lastRenderedPositionKeyRef = useRef<string | undefined>(positionKey)
const shouldAnimateSeries =
!!displayedPositionKey &&
displayedPositionKey !== lastRenderedPositionKeyRef.current

const maxValue = moves
useEffect(() => {
if (!moves?.length || !positionKey) return
setDisplayedMoves(moves)
setDisplayedColorSanMapping(colorSanMapping)
setDisplayedPositionKey(positionKey)
}, [moves, colorSanMapping, positionKey])

useEffect(() => {
if (!displayedPositionKey) return
lastRenderedPositionKeyRef.current = displayedPositionKey
}, [displayedPositionKey])

const moveKeys = useMemo(() => {
if (!displayedMoves?.length) return []
return Object.keys(displayedMoves[0]).filter((move) => move !== 'rating')
}, [displayedMoves])

const maxValue = displayedMoves
? Math.max(
...moves.flatMap((move) =>
...displayedMoves.flatMap((move) =>
Object.entries(move)
.filter(([key]) => key !== 'rating')
.map(([, value]) => value as number),
Expand All @@ -48,7 +75,7 @@ export const MovesByRating: React.FC<Props> = ({
</h2>
<ResponsiveContainer width="100%" height="90%">
<AreaChart
data={moves}
data={displayedMoves}
margin={{
left: 0,
right: isMobile ? 40 : 50,
Expand Down Expand Up @@ -99,11 +126,8 @@ export const MovesByRating: React.FC<Props> = ({
tickFormatter={(value) => `${value}%`}
/>
<defs>
{moves &&
Object.keys(moves[0]).map((move, i) => {
if (move === 'rating') {
return null
}
{displayedMoves &&
moveKeys.map((move) => {
return (
<linearGradient
key={`color${move}`}
Expand All @@ -115,22 +139,26 @@ export const MovesByRating: React.FC<Props> = ({
>
<stop
offset="5%"
stopColor={colorSanMapping[move]?.color ?? '#fff'}
stopColor={
displayedColorSanMapping[move]?.color ?? '#fff'
}
stopOpacity={0.5}
/>
<stop
offset="95%"
stopColor={colorSanMapping[move]?.color ?? '#fff'}
stopColor={
displayedColorSanMapping[move]?.color ?? '#fff'
}
stopOpacity={0}
/>
</linearGradient>
)
})}
</defs>
{moves &&
{displayedMoves &&
// First, collect all the end points and sort them by y-position
(() => {
const lastIndex = moves.length - 1
const lastIndex = displayedMoves.length - 1

// Define the type for end points
interface EndPoint {
Expand All @@ -142,42 +170,38 @@ export const MovesByRating: React.FC<Props> = ({
adjustment?: number
}

const endPoints = Object.keys(moves[0])
.filter((move) => move !== 'rating')
const endPoints = moveKeys
.map((move) => {
const value = moves[lastIndex][move] as number
const value = displayedMoves[lastIndex][move] as number
return {
move,
value,
san: colorSanMapping[move]?.san || move,
color: colorSanMapping[move]?.color ?? '#fff',
san: displayedColorSanMapping[move]?.san || move,
color: displayedColorSanMapping[move]?.color ?? '#fff',
} as EndPoint
})
.sort((a, b) => a.value - b.value) // Sort by value (y-position)

// Return the original map function with adjusted positions
return Object.keys(moves[0]).map((move, i) => {
if (move === 'rating') {
return null
}

return moveKeys.map((move, index) => {
const endPoint = endPoints.find((ep) => ep.move === move)
const san = endPoint?.san || move

return (
<Area
key={i}
key={index}
yAxisId="left"
dataKey={move}
dot={{
r: isMobile ? 2 : 3,
stroke: colorSanMapping[move]?.color ?? '#fff',
stroke: displayedColorSanMapping[move]?.color ?? '#fff',
strokeWidth: isMobile ? 2 : 3,
}}
stroke={colorSanMapping[move]?.color ?? '#fff'}
stroke={displayedColorSanMapping[move]?.color ?? '#fff'}
fill={`url(#color${move})`}
strokeWidth={isMobile ? 2 : 3}
animationDuration={300}
isAnimationActive={shouldAnimateSeries}
name={san}
label={(props: {
x: number
Expand Down Expand Up @@ -231,7 +255,7 @@ export const MovesByRating: React.FC<Props> = ({
dy={isMobile ? 3 : 4}
fontSize={11}
fontWeight={600}
fill={colorSanMapping[move]?.color ?? '#fff'}
fill={displayedColorSanMapping[move]?.color ?? '#fff'}
textAnchor="start"
>
{san}
Expand Down Expand Up @@ -298,8 +322,8 @@ export const MovesByRating: React.FC<Props> = ({
fontSize: 14,
}}
iconSize={0}
formatter={(value) => {
return colorSanMapping[value as string]?.san ?? value
formatter={(value: string) => {
return displayedColorSanMapping[value as string]?.san ?? value
}}
/>
</AreaChart>
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useAnalysisController/useAnalysisController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@ export const useAnalysisController = (
}

if (moveEvaluation?.stockfish) {
const bestMove = Object.entries(moveEvaluation.stockfish.cp_vec)[0]
const bestMove = moveEvaluation.stockfish.model_move
if (bestMove) {
arrows.push({
brush: 'blue',
orig: bestMove[0].slice(0, 2) as Key,
dest: bestMove[0].slice(2, 4) as Key,
orig: bestMove.slice(0, 2) as Key,
dest: bestMove.slice(2, 4) as Key,
modifiers: { lineWidth: 8 },
} as DrawShape)
}
Expand Down
15 changes: 12 additions & 3 deletions src/hooks/useAnalysisController/useMoveRecommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { Chess } from 'chess.ts'
import { MAIA_MODELS } from 'src/constants/common'
import { GameNode, MaiaEvaluation, StockfishEvaluation } from 'src/types'
import { sortStockfishMoves } from './utils'

export const useMoveRecommendations = (
currentNode: GameNode | null,
Expand All @@ -23,6 +24,7 @@ export const useMoveRecommendations = (
cp: number
winrate?: number
winrate_loss?: number
cp_relative?: number
}[]
isBlackTurn?: boolean
} = {
Expand All @@ -44,10 +46,14 @@ export const useMoveRecommendations = (
const cp_relative_vec = moveEvaluation.stockfish.cp_relative_vec || {}
const winrate_vec = moveEvaluation.stockfish.winrate_vec || {}
const winrate_loss_vec = moveEvaluation.stockfish.winrate_loss_vec || {}
const sortedMoves = sortStockfishMoves(
moveEvaluation.stockfish,
Object.keys(cp_vec),
)

const stockfish = Object.entries(cp_vec).map(([move, cp]) => ({
const stockfish = sortedMoves.map((move) => ({
move,
cp,
cp: cp_vec[move] || 0,
winrate: winrate_vec[move] || 0,
winrate_loss: winrate_loss_vec[move] || 0,
cp_relative: cp_relative_vec[move] || 0,
Expand Down Expand Up @@ -80,7 +86,10 @@ export const useMoveRecommendations = (

// Get top 3 Stockfish moves
if (stockfish) {
for (const move of Object.keys(stockfish.cp_vec).slice(0, 3)) {
for (const move of sortStockfishMoves(
stockfish,
Object.keys(stockfish.cp_vec),
).slice(0, 3)) {
if (candidates.find((c) => c[0] === move)) continue
candidates.push([move, move])
}
Expand Down
Loading
Loading