mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: start of charts
This commit is contained in:
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
-100
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
Reference in new issue
Block a user