Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heat map #82

Merged
merged 4 commits into from
Dec 4, 2023
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
66 changes: 66 additions & 0 deletions web/src/components/playing/HeatMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { Box, Flex, Grid, Text } from "@chakra-ui/react";
import { getColorByFactor } from "../../utils/colors";

const leftBorder = [6, 11, 16];
const topBorder = [6, 7, 8];
const rightBorder = [8, 13, 18];
const bottomBorder = [16, 17, 18];

const HeatMap = ({
rates,
counts,
isPitcher,
showStrikeZone = false,
}: {
rates: number[];
counts: number[];
showStrikeZone?: boolean;
isPitcher: boolean;
}) => {
const [showMode, setShowMode] = useState(0);

const generateCell = (index: number) => (
<Box
key={index}
border={"1px solid #aaa"}
borderLeftStyle={leftBorder.includes(index) && showStrikeZone ? "solid" : "none"}
borderRightStyle={rightBorder.includes(index) && showStrikeZone ? "solid" : "none"}
borderTopStyle={topBorder.includes(index) && showStrikeZone ? "solid" : "none"}
borderBottomStyle={bottomBorder.includes(index) && showStrikeZone ? "solid" : "none"}
>
<Box
height="20px"
width="20px"
display="flex"
alignItems="center"
justifyContent="center"
cursor={"pointer"}
bg={getColorByFactor(rates, rates[index] ?? "#111111")}
onClick={() => setShowMode(showMode === 2 ? 0 : showMode + 1)}
>
{showMode !== 0 && (
<Text fontSize={"6px"} color={"black"} fontWeight={"400"}>
{showMode === 1 ? (rates[index] * 100).toFixed(2) : counts[index]}
</Text>
)}
</Box>
</Box>
);

return (
<Flex direction={"column"} alignItems={"center"} gap={"10px"} minH={"150px"}>
<Grid templateColumns="repeat(5, 1fr)" w={"fit-content"}>
{Array.from({ length: 25 }).map((_, i) => generateCell(i))}
</Grid>
{showMode !== 0 && (
<Text fontSize={"10px"}>
Total: {counts.reduce((acc, c) => acc + c)}
{isPitcher ? " pitches" : " swings"}
</Text>
)}
</Flex>
);
};

export default HeatMap;
266 changes: 180 additions & 86 deletions web/src/components/playing/PlayView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import PitcherView from "./PitcherView";
import { Box, Flex, Image, Text } from "@chakra-ui/react";
import Timer from "./Timer";
import { useQuery } from "react-query";
import { useContext, useState } from "react";
import { useContext, useEffect, useState } from "react";
import Web3Context from "../../contexts/Web3Context/context";
import { Token } from "../../types";
import { PitchLocation, SwingLocation, Token } from "../../types";
import { CloseIcon } from "@chakra-ui/icons";
import Outcome from "./Outcome";
import BatterView2 from "./BatterView2";
Expand All @@ -19,6 +19,7 @@ const tokenABI = require("../../web3/abi/BLBABI.json");
import styles from "./PlayView.module.css";
import axios from "axios";
import MainStat from "./MainStat";
import HeatMap from "./HeatMap";

const FullcountABI = FullcountABIImported as unknown as AbiItem[];

Expand Down Expand Up @@ -107,6 +108,10 @@ const PlayView = ({ selectedToken }: { selectedToken: Token }) => {
const tokenContract = new web3ctx.web3.eth.Contract(tokenABI) as any;
const isPitcher = (token?: Token) => selectedSession?.pair.pitcher?.id === token?.id;
const [opponent, setOpponent] = useState<Token | undefined>(undefined);
const [gameOver, setGameOver] = useState(false);

const [pitcher, setPitcher] = useState<Token | undefined>(undefined);
const [batter, setBatter] = useState<Token | undefined>(undefined);

const sessionStatus = useQuery(
["session", selectedSession],
Expand All @@ -116,6 +121,9 @@ const PlayView = ({ selectedToken }: { selectedToken: Token }) => {
const progress = Number(
await gameContract.methods.sessionProgress(selectedSession.sessionID).call(),
);
if (progress < 2 || progress > 4) {
setGameOver(true);
}
const pitcherAddress = session.pitcherNFT.nftAddress;
const pitcherTokenID = session.pitcherNFT.tokenID;
const batterAddress = session.batterNFT.nftAddress;
Expand Down Expand Up @@ -193,25 +201,91 @@ const PlayView = ({ selectedToken }: { selectedToken: Token }) => {
};
},
{
refetchInterval: 3 * 1000,
refetchInterval: () => (gameOver ? false : 3000),
},
);

const pitcherStats = useQuery(
["pitcher_stat", pitcher],
async () => {
if (!pitcher) {
return;
}
const API_URL = "https://api.fullcount.xyz/stats";
const stat = await axios.get(`${API_URL}/${pitcher.address}/${pitcher.id}`);
return stat.data;
},
{
enabled: !!pitcher,
},
);

const opponentStats = useQuery(
["opponent", opponent],
const batterStats = useQuery(
["batter_stat", batter],
async () => {
if (!opponent) {
if (!batter) {
return;
}
const API_URL = "https://api.fullcount.xyz/stats";
const stat = await axios.get(`${API_URL}/${opponent.address}/${opponent.id}`);
const stat = await axios.get(`${API_URL}/${batter.address}/${batter.id}`);
return stat.data;
},
{
enabled: !!opponent,
enabled: !!batter,
},
);

const mockLocations = [
19, 11, 5, 1, 2, 45, 29, 13, 8, 6, 70, 59, 47, 23, 12, 40, 35, 40, 32, 31, 11, 12, 23, 24, 34,
];

const pitchDistributions = useQuery(
["pitch_distribution", pitcher],
async () => {
if (!pitcher) {
return;
}
const API_URL = "https://api.fullcount.xyz/pitch_distribution";
const res = await axios.get(`${API_URL}/${pitcher.address}/${pitcher.id}`);
const counts = new Array(25).fill(0);
res.data.pitch_distribution.forEach(
(l: PitchLocation) => (counts[l.pitch_vertical * 5 + l.pitch_horizontal] = l.count),
);
const total = counts.reduce((acc, value) => acc + value);
const rates = counts.map((value) => value / total);
return { rates, counts };
},
{
enabled: !!pitcher,
},
);

const swingDistributions = useQuery(
["swing_distribution", batter],
async () => {
if (!batter) {
return;
}
const API_URL = "https://api.fullcount.xyz/swing_distribution";
const res = await axios.get(`${API_URL}/${batter.address}/${batter.id}`);
const counts = new Array(25).fill(0);
res.data.swing_distribution.forEach(
(l: SwingLocation) => (counts[l.swing_vertical * 5 + l.swing_horizontal] = l.count),
);
const total = counts.reduce((acc, value) => acc + value);
const rates = counts.map((value) => value / total);
return { rates, counts };
},
{
enabled: !!batter,
},
);

useEffect(() => {
setPitcher(isPitcher(selectedToken) ? selectedToken : opponent);
setBatter(isPitcher(selectedToken) ? opponent : selectedToken);
}, [selectedToken, opponent]);

return (
<Flex direction={"column"} gap={"20px"} minW={"100%"}>
<Flex justifyContent={"space-between"} minW={"100%"} alignItems={"center"}>
Expand All @@ -234,45 +308,55 @@ const PlayView = ({ selectedToken }: { selectedToken: Token }) => {
</Flex>
</Flex>
<Flex alignItems={"center"} justifyContent={"space-between"}>
{isPitcher(selectedToken) ? (
<>
{selectedToken && (
<Flex direction={"column"} gap="10px" alignItems={"center"}>
<Image
src={selectedToken?.image}
aspectRatio={1}
w={"300px"}
alt={selectedToken?.name}
/>
<Text fontSize={"14px"} fontWeight={"700"}>
{selectedToken.name}
</Text>
</Flex>
)}
</>
) : (
<>
{opponent ? (
<Flex direction={"column"} gap="10px" alignItems={"center"} w={"300px"}>
<Image src={opponent?.image} h={"150px"} w={"150px"} alt={opponent?.name} />
<Text fontSize={"14px"} fontWeight={"700"}>
{opponent?.name}
</Text>
{opponentStats.data && <MainStat stats={opponentStats.data} isPitcher={true} />}
</Flex>
) : (
<Flex
direction={"column"}
gap="10px"
alignItems={"center"}
className={styles.pitcherGrid}
>
<Box w={"300px"} h={"300px"} bg={"#4D4D4D"} border={"1px solid #F1E3BF"} />
<Box h={"21px"} w="300px" bg={"transparent"} />
</Flex>
)}
</>
)}
<Flex direction={"column"} gap={"10px"}>
{isPitcher(selectedToken) ? (
<>
{selectedToken && (
<Flex direction={"column"} gap="10px" alignItems={"center"}>
<Image
src={selectedToken?.image}
h={"300px"}
w={"300px"}
alt={selectedToken?.name}
/>
<Text fontSize={"14px"} fontWeight={"700"}>
{selectedToken.name}
</Text>
</Flex>
)}
</>
) : (
<>
{opponent ? (
<Flex direction={"column"} gap="10px" alignItems={"center"} w={"300px"}>
<Image src={opponent?.image} h={"150px"} w={"150px"} alt={opponent?.name} />
<Text fontSize={"14px"} fontWeight={"700"}>
{opponent?.name}
</Text>
</Flex>
) : (
<Flex
direction={"column"}
gap="10px"
alignItems={"center"}
className={styles.pitcherGrid}
>
<Box w={"300px"} h={"300px"} bg={"#4D4D4D"} border={"1px solid #F1E3BF"} />
<Box h={"21px"} w="300px" bg={"transparent"} />
</Flex>
)}
</>
)}
{pitcherStats.data && <MainStat stats={pitcherStats.data} isPitcher={true} />}

{pitchDistributions.data && (
<HeatMap
rates={pitchDistributions.data.rates}
counts={pitchDistributions.data.counts}
isPitcher
/>
)}
</Flex>

{sessionStatus.data?.progress === 2 && selectedSession && selectedToken && (
<InviteLink session={selectedSession} token={selectedToken} />
Expand Down Expand Up @@ -302,45 +386,55 @@ const PlayView = ({ selectedToken }: { selectedToken: Token }) => {
}}
/>
)}
{!isPitcher(selectedToken) ? (
<>
{selectedToken && (
<Flex direction={"column"} gap="10px" alignItems={"center"}>
<Image
src={selectedToken?.image}
aspectRatio={1}
w={"300px"}
alt={selectedToken?.name}
/>
<Text fontSize={"14px"} fontWeight={"700"}>
{selectedToken.name}
</Text>
</Flex>
)}
</>
) : (
<>
{opponent ? (
<Flex direction={"column"} gap="10px" alignItems={"center"} w={"300px"}>
<Image src={opponent?.image} h={"150px"} w={"150px"} alt={opponent?.name} />
<Text fontSize={"14px"} fontWeight={"700"}>
{opponent?.name}
</Text>
{opponentStats.data && <MainStat stats={opponentStats.data} isPitcher={false} />}
</Flex>
) : (
<Flex
direction={"column"}
gap="10px"
alignItems={"center"}
className={styles.pitcherGrid}
>
<Box w={"300px"} h={"300px"} bg={"#4D4D4D"} border={"1px solid #F1E3BF"} />
<Box h={"21px"} w="300px" bg={"transparent"} />
</Flex>
)}
</>
)}
<Flex direction={"column"} gap={"20px"}>
{!isPitcher(selectedToken) ? (
<>
{selectedToken && (
<Flex direction={"column"} gap="10px" alignItems={"center"}>
<Image
src={selectedToken?.image}
h={"300px"}
w={"300px"}
alt={selectedToken?.name}
/>
<Text fontSize={"14px"} fontWeight={"700"}>
{selectedToken.name}
</Text>
</Flex>
)}
</>
) : (
<>
{opponent ? (
<Flex direction={"column"} gap="10px" alignItems={"center"} w={"300px"}>
<Image src={opponent?.image} h={"150px"} w={"150px"} alt={opponent?.name} />
<Text fontSize={"14px"} fontWeight={"700"}>
{opponent?.name}
</Text>
</Flex>
) : (
<Flex
direction={"column"}
gap="10px"
alignItems={"center"}
className={styles.pitcherGrid}
>
<Box w={"300px"} h={"300px"} bg={"#4D4D4D"} border={"1px solid #F1E3BF"} />
<Box h={"21px"} w="300px" bg={"transparent"} />
</Flex>
)}
</>
)}
{batterStats.data && <MainStat stats={batterStats.data} isPitcher={false} />}

{swingDistributions.data && (
<HeatMap
rates={swingDistributions.data.rates}
counts={swingDistributions.data.counts}
isPitcher={false}
/>
)}
</Flex>
</Flex>
</Flex>
);
Expand Down
Loading