diff --git a/.vscode/settings.json b/.vscode/settings.json index a170310c1..be8de5a69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,6 +63,7 @@ "DVPLCRC", "dvpm", "dxgi", + "echarts", "Everytime", "FASTNAME", "FOURCC", diff --git a/bun.lockb b/bun.lockb index fd099fde3..d70c1121f 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/core/src/blitzkit/tankDefinitions/constants.ts b/packages/core/src/blitzkit/tankDefinitions/constants.ts index ccb1b0f3c..36178c3cb 100644 --- a/packages/core/src/blitzkit/tankDefinitions/constants.ts +++ b/packages/core/src/blitzkit/tankDefinitions/constants.ts @@ -55,7 +55,18 @@ export const TANK_ICONS_COLLECTOR: Record = { [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.I]: 'I', [Tier.II]: 'II', diff --git a/packages/website/package.json b/packages/website/package.json index 070b424d0..dcecbd642 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -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", diff --git a/packages/website/src/components/EChart/index.tsx b/packages/website/src/components/EChart/index.tsx new file mode 100644 index 000000000..0387b2078 --- /dev/null +++ b/packages/website/src/components/EChart/index.tsx @@ -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(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 ( + + {title && {title}} + + + ); +} diff --git a/packages/website/src/components/EChart/theme.ts b/packages/website/src/components/EChart/theme.ts new file mode 100644 index 000000000..d4dff35d1 --- /dev/null +++ b/packages/website/src/components/EChart/theme.ts @@ -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', +}; diff --git a/packages/website/src/components/Performance/Charts.tsx b/packages/website/src/components/Performance/Charts.tsx deleted file mode 100644 index 9d2091681..000000000 --- a/packages/website/src/components/Performance/Charts.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Charts() { - return 'amogos'; -} diff --git a/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiers.tsx b/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiers.tsx new file mode 100644 index 000000000..2f5a53876 --- /dev/null +++ b/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiers.tsx @@ -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 ( + 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', + }, + ], + }} + /> + ); +} diff --git a/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiersByClass.tsx b/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiersByClass.tsx new file mode 100644 index 000000000..fea1bca49 --- /dev/null +++ b/packages/website/src/components/Performance/Charts/components/BattleDistributionAcrossTiersByClass.tsx @@ -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 ( + 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', + })), + }} + /> + ); +} diff --git a/packages/website/src/components/Performance/Charts/index.tsx b/packages/website/src/components/Performance/Charts/index.tsx new file mode 100644 index 000000000..27190efd0 --- /dev/null +++ b/packages/website/src/components/Performance/Charts/index.tsx @@ -0,0 +1,14 @@ +import { Flex } from '@radix-ui/themes'; +import { BattleDistributionAcrossTiers } from './components/BattleDistributionAcrossTiers'; +import { BattleDistributionAcrossTiersByClass } from './components/BattleDistributionAcrossTiersByClass'; + +export function Charts() { + return ( + + + + + + + ); +} diff --git a/packages/website/src/components/StickyTableRoot.tsx b/packages/website/src/components/StickyTableRoot.tsx index 04fc7546d..d0b8e2cdc 100644 --- a/packages/website/src/components/StickyTableRoot.tsx +++ b/packages/website/src/components/StickyTableRoot.tsx @@ -6,9 +6,9 @@ export function StickyTableRoot({ style, ...props }: RootProps) { const table = useRef(null); useEffect(() => { - // table.current - // ?.querySelector('table') - // ?.style.setProperty('overflow', 'auto'); + table.current + ?.querySelector('table') + ?.style.setProperty('overflow', 'auto'); }, []); return ( diff --git a/packages/website/src/core/radix/var.ts b/packages/website/src/core/radix/var.ts index 57ac36f64..4c397dbba 100644 --- a/packages/website/src/core/radix/var.ts +++ b/packages/website/src/core/radix/var.ts @@ -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})`; diff --git a/packages/website/src/pages/tools/performance/_index.tsx b/packages/website/src/pages/tools/performance/_index.tsx index a8555f2c8..d877ddc37 100644 --- a/packages/website/src/pages/tools/performance/_index.tsx +++ b/packages/website/src/pages/tools/performance/_index.tsx @@ -31,14 +31,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) { + - {mode === TankPerformanceMode.Table && ( - <> - - - - )} - + {mode === TankPerformanceMode.Table && } {mode === TankPerformanceMode.Charts && } ); diff --git a/packages/website/src/stores/tankPerformanceEphemeral.ts b/packages/website/src/stores/tankPerformanceEphemeral.ts index 5b54031d6..b4188367f 100644 --- a/packages/website/src/stores/tankPerformanceEphemeral.ts +++ b/packages/website/src/stores/tankPerformanceEphemeral.ts @@ -23,6 +23,6 @@ export enum PlayerCountPeriod { export const TankPerformanceEphemeral = createContextualStore(() => create()(() => ({ playerCountPeriod: PlayerCountPeriod.ThisMonth, - mode: TankPerformanceMode.Table, + mode: TankPerformanceMode.Charts, })), );