From a0c54b50fcac10b954d22ebbbc64429b0f29a3f6 Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Sat, 10 Oct 2026 11:45:07 +1300 Subject: [PATCH 1/9] 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 }) => ( +
+ ))} +
+ ); + }, +); From b65229dc80f1928faa7050c1f218f61dd43ddd59 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:03:37 -0600 Subject: [PATCH 2/9] remove granuality --- .../components/Characteristics/components/StatScale.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 32f867b01..d124a2c81 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -1,6 +1,5 @@ import { memo, useMemo } from "react"; -const RESOLUTION = 1000; const HEIGHT = 9; const DENSE_TICKS = 100; @@ -20,9 +19,9 @@ export const StatScale = memo( for (const other of values) { const raw = range === 0 ? 0.5 : (other - min) / range; - const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + const x = lowerIsBetter ? 1 - raw : raw; - if (x === 0 || x === RESOLUTION) continue; + if (x === 0 || x === 1) continue; counts.set(x, (counts.get(x) ?? 0) + 1); } @@ -58,7 +57,7 @@ export const StatScale = memo( style={{ position: "absolute", top: 0, - left: `${(x / RESOLUTION) * 100}%`, + left: `${x * 100}%`, width: 1, height: "100%", backgroundColor: color, From ff507ed4e61ada022fcbe1e576823568a609cb09 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:05:23 -0600 Subject: [PATCH 3/9] use canvas --- .../Characteristics/components/StatScale.tsx | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) 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 index d124a2c81..0be1f2904 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from "react"; +import { memo, useEffect, useMemo, useRef } from "react"; const HEIGHT = 9; const DENSE_TICKS = 100; @@ -11,6 +11,7 @@ interface StatScaleProps { export const StatScale = memo( ({ values, value, lowerIsBetter }) => { + const canvas = useRef(null); const ticks = useMemo(() => { const min = Math.min(value, ...values); const max = Math.max(value, ...values); @@ -29,16 +30,42 @@ export const StatScale = memo( return Array.from(counts, ([x, count]) => ({ x, color: - count === 1 - ? "var(--gray-a5)" - : count <= 3 - ? "var(--gray-a6)" - : "var(--gray-a7)", + count === 1 ? "--gray-a5" : count <= 3 ? "--gray-a6" : "--gray-a7", })); }, [values, value, lowerIsBetter]); + useEffect(() => { + const element = canvas.current; + if (!element) return; + + const draw = () => { + const { width } = element.getBoundingClientRect(); + const pixelRatio = window.devicePixelRatio || 1; + const context = element.getContext("2d"); + if (!context) return; + + element.width = Math.round(width * pixelRatio); + element.height = Math.round(HEIGHT * pixelRatio); + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + + const styles = getComputedStyle(element); + for (const { x, color } of ticks) { + context.fillStyle = styles.getPropertyValue(color); + context.fillRect(x * width, 0, 1, HEIGHT); + } + }; + + const observer = new ResizeObserver(draw); + observer.observe(element); + draw(); + + return () => observer.disconnect(); + }, [ticks]); + return ( -