mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: graphs
This commit is contained in:
1 parent
973d24a615
commit
dee0dc6a14
14 files changed
+377
-17
No files matched your search
@@ -55,7 +55,18 @@ export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
|
||||
[TankClass.MEDIUM]: 'https://i.imgur.com/u8YDMBh.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> = {
|
||||
[Tier.I]: 'I',
|
||||
[Tier.II]: 'II',
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"@blitzkit/core": "workspace:*",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nivo/core": "^0.87.0",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/themes": "^3.1.3",
|
||||
@@ -22,6 +21,7 @@
|
||||
"@vanilla-extract/css": "^1.15.5",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
"compare-versions": "^6.1.1",
|
||||
"echarts": "^5.5.1",
|
||||
"immer": "^10.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"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);
|
||||
|
||||
useEffect(() => {
|
||||
// table.current
|
||||
// ?.querySelector('table')
|
||||
// ?.style.setProperty('overflow', 'auto');
|
||||
table.current
|
||||
?.querySelector('table')
|
||||
?.style.setProperty('overflow', 'auto');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,23 @@ type Name =
|
||||
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
||||
| `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) {
|
||||
return `var(--${name})`;
|
||||
|
||||
@@ -31,14 +31,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
<PerformanceInfo skeleton={skeleton} />
|
||||
<FilterControl />
|
||||
<PlayerCountControl />
|
||||
|
||||
{mode === TankPerformanceMode.Table && (
|
||||
<>
|
||||
<PlayerCountControl />
|
||||
<TankTable skeleton={skeleton} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === TankPerformanceMode.Table && <TankTable skeleton={skeleton} />}
|
||||
{mode === TankPerformanceMode.Charts && <Charts />}
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,6 @@ export enum PlayerCountPeriod {
|
||||
export const TankPerformanceEphemeral = createContextualStore(() =>
|
||||
create<TankPerformanceEphemeral>()(() => ({
|
||||
playerCountPeriod: PlayerCountPeriod.ThisMonth,
|
||||
mode: TankPerformanceMode.Table,
|
||||
mode: TankPerformanceMode.Charts,
|
||||
})),
|
||||
);
|
||||
Reference in new issue
Block a user