mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 12:09:51 +02:00
feat: graphs
This commit is contained in:
1 parent
973d24a615
commit
dee0dc6a14
14 files changed
+377
-17
No files matched your search
Vendored
+1
@@ -63,6 +63,7 @@
|
|||||||
"DVPLCRC",
|
"DVPLCRC",
|
||||||
"dvpm",
|
"dvpm",
|
||||||
"dxgi",
|
"dxgi",
|
||||||
|
"echarts",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"FASTNAME",
|
"FASTNAME",
|
||||||
"FOURCC",
|
"FOURCC",
|
||||||
|
|||||||
@@ -55,7 +55,18 @@ export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
|
|||||||
[TankClass.MEDIUM]: 'https://i.imgur.com/u8YDMBh.png',
|
[TankClass.MEDIUM]: 'https://i.imgur.com/u8YDMBh.png',
|
||||||
[TankClass.HEAVY]: 'https://i.imgur.com/8xRf3nc.png',
|
[TankClass.HEAVY]: 'https://i.imgur.com/8xRf3nc.png',
|
||||||
};
|
};
|
||||||
export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const;
|
export const TIERS = [
|
||||||
|
Tier.I,
|
||||||
|
Tier.II,
|
||||||
|
Tier.III,
|
||||||
|
Tier.IV,
|
||||||
|
Tier.V,
|
||||||
|
Tier.VI,
|
||||||
|
Tier.VII,
|
||||||
|
Tier.VIII,
|
||||||
|
Tier.IX,
|
||||||
|
Tier.X,
|
||||||
|
];
|
||||||
export const TIER_ROMAN_NUMERALS: Record<Tier, string> = {
|
export const TIER_ROMAN_NUMERALS: Record<Tier, string> = {
|
||||||
[Tier.I]: 'I',
|
[Tier.I]: 'I',
|
||||||
[Tier.II]: 'II',
|
[Tier.II]: 'II',
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
"@nivo/core": "^0.87.0",
|
|
||||||
"@radix-ui/colors": "^3.0.0",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/themes": "^3.1.3",
|
"@radix-ui/themes": "^3.1.3",
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
"@vanilla-extract/css": "^1.15.5",
|
"@vanilla-extract/css": "^1.15.5",
|
||||||
"@vanilla-extract/dynamic": "^2.1.2",
|
"@vanilla-extract/dynamic": "^2.1.2",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"markdown-to-jsx": "^7.5.0",
|
"markdown-to-jsx": "^7.5.0",
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Box, Flex, Heading, type BoxProps } from '@radix-ui/themes';
|
||||||
|
import type { ECharts, EChartsOption, SetOptionOpts } from 'echarts';
|
||||||
|
import { getInstanceByDom, init } from 'echarts';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { echartsTheme } from './theme';
|
||||||
|
|
||||||
|
export type EChartProps = BoxProps & {
|
||||||
|
option: EChartsOption;
|
||||||
|
style?: CSSProperties;
|
||||||
|
settings?: SetOptionOpts;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thank you Maneet Goyal! Heavily modified.
|
||||||
|
*
|
||||||
|
* https://dev.to/manufac/using-apache-echarts-with-react-and-typescript-353k
|
||||||
|
*/
|
||||||
|
export function EChart({
|
||||||
|
option,
|
||||||
|
settings,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: EChartProps): JSX.Element {
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize chart
|
||||||
|
let chart: ECharts | undefined;
|
||||||
|
if (chartRef.current !== null) {
|
||||||
|
chart = init(chartRef.current, echartsTheme, { renderer: 'svg' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add chart resize listener
|
||||||
|
// ResizeObserver is leading to a bit janky UX
|
||||||
|
function resizeChart() {
|
||||||
|
chart?.resize();
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', resizeChart);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
chart?.dispose();
|
||||||
|
window.removeEventListener('resize', resizeChart);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chartRef.current === null) return undefined;
|
||||||
|
|
||||||
|
const chart = getInstanceByDom(chartRef.current);
|
||||||
|
|
||||||
|
if (chart === undefined) return undefined;
|
||||||
|
|
||||||
|
chart.setOption(option, settings);
|
||||||
|
}, [option, settings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="center">
|
||||||
|
{title && <Heading align="center">{title}</Heading>}
|
||||||
|
<Box {...props} ref={chartRef} width="100%" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { Var } from '../../core/radix/var';
|
||||||
|
|
||||||
|
export const echartsTheme = {
|
||||||
|
seriesCnt: '3',
|
||||||
|
backgroundColor: Var('color-background'),
|
||||||
|
titleColor: '#ff0000',
|
||||||
|
subtitleColor: '#dddddd',
|
||||||
|
textColorShow: false,
|
||||||
|
textColor: '#333',
|
||||||
|
markTextColor: '#293441',
|
||||||
|
color: [
|
||||||
|
Var('accent-indicator'),
|
||||||
|
'#fc97af',
|
||||||
|
'#87f7cf',
|
||||||
|
'#f7f494',
|
||||||
|
'#72ccff',
|
||||||
|
'#f7c5a0',
|
||||||
|
'#d4a4eb',
|
||||||
|
'#d2f5a6',
|
||||||
|
'#76f2f2',
|
||||||
|
],
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 0,
|
||||||
|
visualMapColor: ['#fc97af', '#87f7cf'],
|
||||||
|
legendTextColor: '#999999',
|
||||||
|
kColor: '#fc97af',
|
||||||
|
kColor0: 'transparent',
|
||||||
|
kBorderColor: '#fc97af',
|
||||||
|
kBorderColor0: '#87f7cf',
|
||||||
|
kBorderWidth: '2',
|
||||||
|
lineWidth: '3',
|
||||||
|
symbolSize: '0',
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolBorderWidth: '4',
|
||||||
|
lineSmooth: true,
|
||||||
|
graphLineWidth: '1',
|
||||||
|
graphLineColor: '#ffffff',
|
||||||
|
mapLabelColor: '#893448',
|
||||||
|
mapLabelColorE: 'rgb(137,52,72)',
|
||||||
|
mapBorderColor: '#999999',
|
||||||
|
mapBorderColorE: '#eb8146',
|
||||||
|
mapBorderWidth: 0.5,
|
||||||
|
mapBorderWidthE: 1,
|
||||||
|
mapAreaColor: '#f3f3f3',
|
||||||
|
mapAreaColorE: 'rgba(255,178,72,1)',
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
type: 'all',
|
||||||
|
name: '通用坐标轴',
|
||||||
|
axisLineShow: true,
|
||||||
|
axisLineColor: '#666666',
|
||||||
|
axisTickShow: false,
|
||||||
|
axisTickColor: '#333',
|
||||||
|
axisLabelShow: true,
|
||||||
|
axisLabelColor: '#aaaaaa',
|
||||||
|
splitLineShow: false,
|
||||||
|
splitLineColor: ['#e6e6e6'],
|
||||||
|
splitAreaShow: false,
|
||||||
|
splitAreaColor: ['rgba(250,250,250,0.05)', 'rgba(200,200,200,0.02)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
name: '类目坐标轴',
|
||||||
|
axisLineShow: true,
|
||||||
|
axisLineColor: '#333',
|
||||||
|
axisTickShow: true,
|
||||||
|
axisTickColor: '#333',
|
||||||
|
axisLabelShow: true,
|
||||||
|
axisLabelColor: '#333',
|
||||||
|
splitLineShow: false,
|
||||||
|
splitLineColor: ['#ccc'],
|
||||||
|
splitAreaShow: false,
|
||||||
|
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '数值坐标轴',
|
||||||
|
axisLineShow: true,
|
||||||
|
axisLineColor: '#333',
|
||||||
|
axisTickShow: true,
|
||||||
|
axisTickColor: '#333',
|
||||||
|
axisLabelShow: true,
|
||||||
|
axisLabelColor: '#333',
|
||||||
|
splitLineShow: true,
|
||||||
|
splitLineColor: ['#ccc'],
|
||||||
|
splitAreaShow: false,
|
||||||
|
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'log',
|
||||||
|
name: '对数坐标轴',
|
||||||
|
axisLineShow: true,
|
||||||
|
axisLineColor: '#333',
|
||||||
|
axisTickShow: true,
|
||||||
|
axisTickColor: '#333',
|
||||||
|
axisLabelShow: true,
|
||||||
|
axisLabelColor: '#333',
|
||||||
|
splitLineShow: true,
|
||||||
|
splitLineColor: ['#ccc'],
|
||||||
|
splitAreaShow: false,
|
||||||
|
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
name: '时间坐标轴',
|
||||||
|
axisLineShow: true,
|
||||||
|
axisLineColor: '#333',
|
||||||
|
axisTickShow: true,
|
||||||
|
axisTickColor: '#333',
|
||||||
|
axisLabelShow: true,
|
||||||
|
axisLabelColor: '#333',
|
||||||
|
splitLineShow: true,
|
||||||
|
splitLineColor: ['#ccc'],
|
||||||
|
splitAreaShow: false,
|
||||||
|
splitAreaColor: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
axisSeperateSetting: false,
|
||||||
|
toolboxColor: '#999999',
|
||||||
|
toolboxEmphasisColor: '#666666',
|
||||||
|
tooltipAxisColor: '#cccccc',
|
||||||
|
tooltipAxisWidth: 1,
|
||||||
|
timelineLineColor: '#87f7cf',
|
||||||
|
timelineLineWidth: 1,
|
||||||
|
timelineItemColor: '#87f7cf',
|
||||||
|
timelineItemColorE: '#f7f494',
|
||||||
|
timelineCheckColor: '#fc97af',
|
||||||
|
timelineCheckBorderColor: '#fc97af',
|
||||||
|
timelineItemBorderWidth: 1,
|
||||||
|
timelineControlColor: '#87f7cf',
|
||||||
|
timelineControlBorderColor: '#87f7cf',
|
||||||
|
timelineControlBorderWidth: 0.5,
|
||||||
|
timelineLabelColor: '#87f7cf',
|
||||||
|
datazoomBackgroundColor: 'rgba(255,255,255,0)',
|
||||||
|
datazoomDataColor: 'rgba(114,204,255,1)',
|
||||||
|
datazoomFillColor: 'rgba(114,204,255,0.2)',
|
||||||
|
datazoomHandleColor: '#72ccff',
|
||||||
|
datazoomHandleWidth: '100',
|
||||||
|
datazoomLabelColor: '#333333',
|
||||||
|
};
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function Charts() {
|
|
||||||
return 'amogos';
|
|
||||||
}
|
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
TIERS,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
fetchAverageDefinitions,
|
||||||
|
fetchTankDefinitions,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import { TankPerformanceEphemeral } from '../../../../stores/tankPerformanceEphemeral';
|
||||||
|
import { EChart } from '../../../EChart';
|
||||||
|
|
||||||
|
const averageDefinitions = await fetchAverageDefinitions();
|
||||||
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
|
const averageDefinitionsArray = Object.entries(averageDefinitions.averages);
|
||||||
|
|
||||||
|
export function BattleDistributionAcrossTiers() {
|
||||||
|
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||||
|
(state) => state.playerCountPeriod,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EChart
|
||||||
|
height="25rem"
|
||||||
|
title="Battle distribution across tiers"
|
||||||
|
option={{
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: TIERS.map((tier) => TIER_ROMAN_NUMERALS[tier]),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: TIERS.map((tier) => {
|
||||||
|
return averageDefinitionsArray.reduce(
|
||||||
|
(accumulator, [idString, entry]) => {
|
||||||
|
const id = Number(idString);
|
||||||
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
|
if (tank === undefined || tank.tier !== tier) {
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
accumulator +
|
||||||
|
entry.mu.battles * entry.samples[playerCountPeriod]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
type: 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
TANK_CLASSES,
|
||||||
|
TIERS,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
fetchAverageDefinitions,
|
||||||
|
fetchTankDefinitions,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import strings from '@blitzkit/core/lang/en-US.json';
|
||||||
|
import { TankPerformanceEphemeral } from '../../../../stores/tankPerformanceEphemeral';
|
||||||
|
import { EChart } from '../../../EChart';
|
||||||
|
|
||||||
|
const averageDefinitions = await fetchAverageDefinitions();
|
||||||
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
|
const averageDefinitionsArray = Object.entries(averageDefinitions.averages);
|
||||||
|
|
||||||
|
export function BattleDistributionAcrossTiersByClass() {
|
||||||
|
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||||
|
(state) => state.playerCountPeriod,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EChart
|
||||||
|
height="25rem"
|
||||||
|
title="Battle distribution across tiers by class"
|
||||||
|
option={{
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: TIERS.map((tier) => TIER_ROMAN_NUMERALS[tier]),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: TANK_CLASSES.map((c) => strings.common.tank_class_short[c]),
|
||||||
|
},
|
||||||
|
series: TANK_CLASSES.map((c) => ({
|
||||||
|
name: strings.common.tank_class_short[c],
|
||||||
|
data: TIERS.map((tier) => {
|
||||||
|
return averageDefinitionsArray.reduce(
|
||||||
|
(accumulator, [idString, entry]) => {
|
||||||
|
const id = Number(idString);
|
||||||
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
|
if (
|
||||||
|
tank === undefined ||
|
||||||
|
tank.tier !== tier ||
|
||||||
|
tank.class !== c
|
||||||
|
) {
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
accumulator +
|
||||||
|
entry.mu.battles * entry.samples[playerCountPeriod]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
type: 'bar',
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import { BattleDistributionAcrossTiers } from './components/BattleDistributionAcrossTiers';
|
||||||
|
import { BattleDistributionAcrossTiersByClass } from './components/BattleDistributionAcrossTiersByClass';
|
||||||
|
|
||||||
|
export function Charts() {
|
||||||
|
return (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Flex direction="column" maxWidth="35rem" flexGrow="1" py="6" gap="9">
|
||||||
|
<BattleDistributionAcrossTiers />
|
||||||
|
<BattleDistributionAcrossTiersByClass />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ export function StickyTableRoot({ style, ...props }: RootProps) {
|
|||||||
const table = useRef<HTMLTableElement>(null);
|
const table = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// table.current
|
table.current
|
||||||
// ?.querySelector('table')
|
?.querySelector('table')
|
||||||
// ?.style.setProperty('overflow', 'auto');
|
?.style.setProperty('overflow', 'auto');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,23 @@ type Name =
|
|||||||
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||||
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
||||||
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`
|
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`
|
||||||
| `${RadixColor}-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`;
|
| `${RadixColor | 'accent' | 'gray'}-${
|
||||||
|
| 1
|
||||||
|
| 2
|
||||||
|
| 3
|
||||||
|
| 4
|
||||||
|
| 5
|
||||||
|
| 6
|
||||||
|
| 7
|
||||||
|
| 8
|
||||||
|
| 9
|
||||||
|
| 10
|
||||||
|
| 11
|
||||||
|
| 12
|
||||||
|
| 'surface'
|
||||||
|
| 'indicator'
|
||||||
|
| 'track'
|
||||||
|
| 'contrast'}`;
|
||||||
|
|
||||||
export function Var(name: Name) {
|
export function Var(name: Name) {
|
||||||
return `var(--${name})`;
|
return `var(--${name})`;
|
||||||
|
|||||||
@@ -31,14 +31,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
|
|
||||||
<PerformanceInfo skeleton={skeleton} />
|
<PerformanceInfo skeleton={skeleton} />
|
||||||
<FilterControl />
|
<FilterControl />
|
||||||
|
<PlayerCountControl />
|
||||||
|
|
||||||
{mode === TankPerformanceMode.Table && (
|
{mode === TankPerformanceMode.Table && <TankTable skeleton={skeleton} />}
|
||||||
<>
|
|
||||||
<PlayerCountControl />
|
|
||||||
<TankTable skeleton={skeleton} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === TankPerformanceMode.Charts && <Charts />}
|
{mode === TankPerformanceMode.Charts && <Charts />}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ export enum PlayerCountPeriod {
|
|||||||
export const TankPerformanceEphemeral = createContextualStore(() =>
|
export const TankPerformanceEphemeral = createContextualStore(() =>
|
||||||
create<TankPerformanceEphemeral>()(() => ({
|
create<TankPerformanceEphemeral>()(() => ({
|
||||||
playerCountPeriod: PlayerCountPeriod.ThisMonth,
|
playerCountPeriod: PlayerCountPeriod.ThisMonth,
|
||||||
mode: TankPerformanceMode.Table,
|
mode: TankPerformanceMode.Charts,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
Reference in new issue
Block a user