feat: start of charts

This commit is contained in:
tresabhi committed 2024-12-23 12:48:52 -06:00
1 parent b6fd5eaa66
commit eb95230a5f
11 files changed
+192 -165

No files matched your search

@@ -0,0 +1,32 @@
import { Flex, Select, Text } from '@radix-ui/themes';
import { Charts, type ChartsPeriod } from '../../stores/charts';
export function ChartsControl() {
const period = Charts.use((state) => state.period);
const mutateCharts = Charts.useMutation();
return (
<Flex justify="center" align="center" gap="2">
<Text>Player count</Text>
<Select.Root
value={period}
onValueChange={(period) => {
mutateCharts((draft) => {
draft.period = period as ChartsPeriod;
});
}}
>
<Select.Trigger />
<Select.Content>
<Select.Item value="total">Total</Select.Item>
<Select.Item value="d_120">Past 120 days</Select.Item>
<Select.Item value="d_90">Past 90 days</Select.Item>
<Select.Item value="d_60">Past 60 days</Select.Item>
<Select.Item value="d_30">This month</Select.Item>
<Select.Item value="d_7">This week</Select.Item>
<Select.Item value="d_1">Yesterday</Select.Item>
</Select.Content>
</Select.Root>
</Flex>
);
}
@@ -0,0 +1,99 @@
import {
TANK_CLASSES,
TIERS,
TIER_ROMAN_NUMERALS,
TankClass,
formatCompact,
} from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import { useStore } from '@nanostores/react';
import { Box, Flex, Heading } from '@radix-ui/themes';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
import { filterTank } from '../../core/blitzkit/filterTank';
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
import { Charts } from '../../stores/charts';
import { $tankFilters } from '../../stores/tankFilters';
import { ThemedBar } from '../Nivo/ThemedBar';
const [tankDefinitions, averageDefinitions] = await Promise.all([
awaitableTankDefinitions,
awaitableAverageDefinitions,
]);
const averageDefinitionsArray = Object.entries(averageDefinitions.averages);
export function PlayersInPeriod() {
const period = Charts.use((state) => state.period);
const ratio = useAveragesExclusionRatio();
const filters = useStore($tankFilters);
const data = TIERS.map((tier) => {
const players: Record<TankClass, number> = {
0: 0,
1: 0,
2: 0,
3: 0,
};
averageDefinitionsArray.forEach(([idString, entry]) => {
const id = Number(idString);
const tank = tankDefinitions.tanks[id];
if (
tank === undefined ||
tank.tier !== tier ||
!filterTank(filters, tank)
) {
return;
}
players[tank.class] += entry.samples[period] * ratio;
});
const bar: Record<string, number | string> = {
tier: TIER_ROMAN_NUMERALS[tier],
};
TANK_CLASSES.forEach((c) => {
bar[strings.common.tank_class_short[c]] = players[c];
});
return bar;
});
return (
<Flex direction="column" gap="4">
<Heading align="center">Players by tier & class</Heading>
<Box height="25rem">
<ThemedBar
data={data}
keys={TANK_CLASSES.map((c) => strings.common.tank_class_short[c])}
indexBy="tier"
margin={{ right: 90, bottom: 50, left: 90 }}
axisBottom={{
legend: 'Tier',
legendPosition: 'middle',
legendOffset: 32,
}}
axisLeft={{
legend: 'Players',
legendPosition: 'middle',
legendOffset: -60,
format: formatCompact,
}}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom-right',
direction: 'column',
translateX: 120,
itemWidth: 100,
itemHeight: 20,
},
]}
valueFormat={formatCompact}
/>
</Box>
</Flex>
);
}
@@ -1,100 +0,0 @@
import {
formatCompact,
TANK_CLASSES,
TankClass,
TIER_ROMAN_NUMERALS,
TIERS,
} from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import { useStore } from '@nanostores/react';
import { Box, Flex, Heading } from '@radix-ui/themes';
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
import { filterTank } from '../../../../core/blitzkit/filterTank';
import { useAveragesExclusionRatio } from '../../../../hooks/useAveragesExclusionRatio';
import { $tankFilters } from '../../../../stores/tankFilters';
import { TankPerformanceEphemeral } from '../../../../stores/tankPerformanceEphemeral';
import { ThemedBar } from '../../../Nivo/ThemedBar';
const [tankDefinitions, averageDefinitions] = await Promise.all([
awaitableTankDefinitions,
awaitableAverageDefinitions,
]);
const averageDefinitionsArray = Object.entries(averageDefinitions.averages);
export function PlayersByTierAndClass() {
const playerCountPeriod = TankPerformanceEphemeral.use(
(state) => state.playerCountPeriod,
);
const ratio = useAveragesExclusionRatio();
const filters = useStore($tankFilters);
return (
<Flex direction="column" gap="4">
<Heading align="center">Players by tier & class</Heading>
<Box height="25rem">
<ThemedBar
data={TIERS.map((tier) => {
const players: Record<TankClass, number> = {
0: 0,
1: 0,
2: 0,
3: 0,
};
averageDefinitionsArray.forEach(([idString, entry]) => {
const id = Number(idString);
const tank = tankDefinitions.tanks[id];
if (
tank === undefined ||
tank.tier !== tier ||
!filterTank(filters, tank)
) {
return;
}
players[tank.class] += entry.samples[playerCountPeriod] * ratio;
});
const bar: Record<string, number | string> = {
tier: TIER_ROMAN_NUMERALS[tier],
};
TANK_CLASSES.forEach((c) => {
bar[strings.common.tank_class_short[c]] = players[c];
});
return bar;
})}
keys={TANK_CLASSES.map((c) => strings.common.tank_class_short[c])}
indexBy="tier"
margin={{ right: 90, bottom: 50, left: 90 }}
axisBottom={{
legend: 'Tier',
legendPosition: 'middle',
legendOffset: 32,
}}
axisLeft={{
legend: 'Players',
legendPosition: 'middle',
legendOffset: -60,
format: formatCompact,
}}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom-right',
direction: 'column',
translateX: 120,
itemWidth: 100,
itemHeight: 20,
},
]}
valueFormat={formatCompact}
/>
</Box>
</Flex>
);
}
@@ -1,12 +0,0 @@
import { Flex } from '@radix-ui/themes';
import { PlayersByTierAndClass } from './components/PlayersByTierAndClass';
export function Charts() {
return (
<Flex justify="center">
<Flex direction="column" maxWidth="45rem" flexGrow="1" py="6" gap="9">
<PlayersByTierAndClass />
</Flex>
</Flex>
);
}
@@ -1,31 +0,0 @@
import { Tabs } from '@radix-ui/themes';
import {
TankPerformanceEphemeral,
TankPerformanceMode,
} from '../../stores/tankPerformanceEphemeral';
export function ModeSwitcher() {
const mode = TankPerformanceEphemeral.use((state) => state.mode);
const mutateTankPerformanceMode = TankPerformanceEphemeral.useMutation();
return (
<Tabs.Root
value={`${mode}`}
onValueChange={(value) => {
mutateTankPerformanceMode((draft) => {
draft.mode = Number(value) as TankPerformanceMode;
});
}}
mb="5"
>
<Tabs.List justify="center">
<Tabs.Trigger value={`${TankPerformanceMode.Table}`}>
Table
</Tabs.Trigger>
<Tabs.Trigger value={`${TankPerformanceMode.Charts}`}>
Charts
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
);
}
+4 -3
View File
@@ -179,14 +179,14 @@ export const tierListTool: Tool = {
export const chartsTool: Tool = { export const chartsTool: Tool = {
id: 'charts', id: 'charts',
title: 'Charts', title: 'Charts',
description: 'Lots of Blitz charts', description: 'Lots of graphs',
pageDescription: pageDescription:
'Charts and graphs on anything and everything about tanks, players, and events in World of Tanks Blitz', 'Charts and graphs on anything and everything about tanks, players, and events in World of Tanks Blitz',
button: { button: {
text: 'Nerd out', text: 'Nerd out',
color: 'yellow', color: 'bronze',
}, },
image: '9s68NwR', image: '6fhkljA',
branches: ['main', 'dev', 'opentest', 'preview'], branches: ['main', 'dev', 'opentest', 'preview'],
}; };
@@ -194,6 +194,7 @@ export const TOOLS: Tool[] = [
tankopediaTool, tankopediaTool,
compareTool, compareTool,
performanceTool, performanceTool,
chartsTool,
galleryTool, galleryTool,
sessionTool, sessionTool,
tierListTool, tierListTool,
@@ -0,0 +1,29 @@
import { Flex } from '@radix-ui/themes';
import { ChartsControl } from '../../../components/Charts/ChartsControl';
import { PlayersInPeriod } from '../../../components/Charts/PlayersInPeriod';
import { PageWrapper } from '../../../components/PageWrapper';
import { FilterControl } from '../../../components/TankSearch/components/FilterControl';
import { Charts } from '../../../stores/charts';
export function Page() {
return (
<Charts.Provider>
<Content />
</Charts.Provider>
);
}
function Content() {
return (
<PageWrapper maxWidth="unset">
<FilterControl />
<ChartsControl />
<Flex justify="center">
<Flex direction="column" maxWidth="45rem" flexGrow="1" py="6" gap="9">
<PlayersInPeriod />
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,9 @@
---
import { chartsTool } from '../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { Page } from './_index';
---
<ToolLayout tool={chartsTool}>
<Page client:load />
</ToolLayout>
@@ -1,12 +1,9 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from '../../../components/PageWrapper';
import { Charts } from '../../../components/Performance/Charts'; import { PerformanceInfo } from '../../../components/Performance/PerformanceInfo';
import { PlayerCountControl } from '../../../components/Performance/PlayerCountControl'; import { PlayerCountControl } from '../../../components/Performance/PlayerCountControl';
import { TankTable } from '../../../components/Performance/Table'; import { TankTable } from '../../../components/Performance/Table';
import { FilterControl } from '../../../components/TankSearch/components/FilterControl'; import { FilterControl } from '../../../components/TankSearch/components/FilterControl';
import { import { TankPerformanceEphemeral } from '../../../stores/tankPerformanceEphemeral';
TankPerformanceEphemeral,
TankPerformanceMode,
} from '../../../stores/tankPerformanceEphemeral';
import { TankPerformanceSort } from '../../../stores/tankPerformanceSort'; import { TankPerformanceSort } from '../../../stores/tankPerformanceSort';
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
@@ -21,18 +18,13 @@ export function Page({ skeleton }: MaybeSkeletonComponentProps) {
} }
function Content({ skeleton }: MaybeSkeletonComponentProps) { function Content({ skeleton }: MaybeSkeletonComponentProps) {
const mode = TankPerformanceEphemeral.use((state) => state.mode);
return ( return (
<PageWrapper color="jade" maxWidth="100%"> <PageWrapper color="jade" maxWidth="100%">
{/* <ModeSwitcher /> */} <PerformanceInfo skeleton={skeleton} />
{/* <PerformanceInfo skeleton={skeleton} /> */}
<FilterControl /> <FilterControl />
<PlayerCountControl /> <PlayerCountControl />
{mode === TankPerformanceMode.Table && <TankTable skeleton={skeleton} />} <TankTable skeleton={skeleton} />
{mode === TankPerformanceMode.Charts && <Charts />}
</PageWrapper> </PageWrapper>
); );
} }
+15
View File
@@ -0,0 +1,15 @@
import type { Samples } from '@blitzkit/core';
import { create } from 'zustand';
import { createContextualStore } from '../core/zustand/createContextualStore';
export interface Charts {
period: ChartsPeriod;
}
export type ChartsPeriod = keyof Samples;
export const Charts = createContextualStore(() =>
create<Charts>()(() => ({
period: 'd_30',
})),
);
@@ -4,12 +4,6 @@ import { createContextualStore } from '../core/zustand/createContextualStore';
export interface TankPerformanceEphemeral { export interface TankPerformanceEphemeral {
playerCountPeriod: PlayerCountPeriod; playerCountPeriod: PlayerCountPeriod;
mode: TankPerformanceMode;
}
export enum TankPerformanceMode {
Table,
Charts,
} }
export type PlayerCountPeriod = keyof Samples; export type PlayerCountPeriod = keyof Samples;
@@ -17,6 +11,5 @@ export type PlayerCountPeriod = keyof Samples;
export const TankPerformanceEphemeral = createContextualStore(() => export const TankPerformanceEphemeral = createContextualStore(() =>
create<TankPerformanceEphemeral>()(() => ({ create<TankPerformanceEphemeral>()(() => ({
playerCountPeriod: 'd_30', playerCountPeriod: 'd_30',
mode: TankPerformanceMode.Table,
})), })),
); );