From a0c54b50fcac10b954d22ebbbc64429b0f29a3f6 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sat, 10 Oct 2026 11:45:07 +1300 Subject: [PATCH] Feature: tank distribution scale on Tankopedia stats --- .../components/InfoWithDelta.tsx | 34 ++++++--- .../Characteristics/components/StatScale.tsx | 71 +++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx index 7d910738d..352317ddf 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx @@ -1,5 +1,5 @@ import { createDefaultSkills } from "@blitzkit/core"; -import { Flex, Progress, Text } from "@radix-ui/themes"; +import { Box, Flex, Progress, Text } from "@radix-ui/themes"; import { clamp } from "lodash-es"; import { memo, useMemo, type ComponentProps, type ReactNode } from "react"; import { api } from "../../../../../../core/blitzkit/api"; @@ -16,6 +16,7 @@ import { TankopediaRelativeAgainst, } from "../../../../../../stores/tankopedia"; import { Info, type InfoProps } from "./Info"; +import { StatScale } from "./StatScale"; type InfoWithDeltaProps = Omit & { stats: TankCharacteristics; @@ -122,6 +123,15 @@ export const InfoWithDelta = memo( return othersValue !== undefined; }) as TankCharacteristics[]; }, [relativeAgainst, shellIndex, equipmentMatrix, equalize]); + const values = useMemo( + () => + others.map((tank) => + typeof props.value === "function" + ? props.value(tank)! + : (tank[props.value] as number), + ), + [others], + ); const betterTanks = others.filter((tank) => { const othersValue = typeof props.value === "function" @@ -161,13 +171,21 @@ export const InfoWithDelta = memo( {!noRanking && ( - + + + + + {clamp(betterTanks.length + 1, 1, others.length)} /{" "} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx new file mode 100644 index 000000000..32f867b01 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -0,0 +1,71 @@ +import { memo, useMemo } from "react"; + +const RESOLUTION = 1000; +const HEIGHT = 9; +const DENSE_TICKS = 100; + +interface StatScaleProps { + values: number[]; + value: number; + lowerIsBetter?: boolean; +} + +export const StatScale = memo( + ({ values, value, lowerIsBetter }) => { + const ticks = useMemo(() => { + const min = Math.min(value, ...values); + const max = Math.max(value, ...values); + const range = max - min; + const counts = new Map(); + + for (const other of values) { + const raw = range === 0 ? 0.5 : (other - min) / range; + const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + + if (x === 0 || x === RESOLUTION) continue; + + counts.set(x, (counts.get(x) ?? 0) + 1); + } + + return Array.from(counts, ([x, count]) => ({ + x, + color: + count === 1 + ? "var(--gray-a5)" + : count <= 3 + ? "var(--gray-a6)" + : "var(--gray-a7)", + })); + }, [values, value, lowerIsBetter]); + + return ( +
+ {ticks.map(({ x, color }) => ( +
+ ))} +
+ ); + }, +);