From 06550900acf4dd07b8b1c273ba2ff8ff893c6387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=B1=D9=8A=D8=A7=D9=86?= <112495059+ryanab00@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:12:42 -0500 Subject: [PATCH 1/4] Issue 3169, Exposing weekly pub/turbo trends. This update introduces a Dotabuff-style change indicator column to the public and turbo hero tables, tracking 7-day win-rate momentum using absolute percentage points (e.g., $+1.5\%$). Under the hood, the calculation handles OpenDota's trend payloads by slicing off the incomplete trailing partial day to ensure clean 24-hour comparisons. It also neutralizes weekend volume skews by computing daily win rates (wins divided by picks) before averaging, and uses a multi-day midpoint split to smooth out single-day noise and provide a stable view of hero performance trends. --- src/components/Heroes/Heroes.tsx | 2 +- src/components/Heroes/rankColumns_2.ts | 345 +++++++++++++++++++++++++ 2 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/components/Heroes/rankColumns_2.ts diff --git a/src/components/Heroes/Heroes.tsx b/src/components/Heroes/Heroes.tsx index e016276896..4c0512f124 100644 --- a/src/components/Heroes/Heroes.tsx +++ b/src/components/Heroes/Heroes.tsx @@ -8,7 +8,7 @@ import Table from "../Table/Table"; import TabBar from "../TabBar/TabBar"; import Hero from "../Hero/Hero"; import { sum, abbreviateNumber } from "../../utility"; -import { HeroesTab, rankColumns } from "./rankColumns"; +import { HeroesTab, rankColumns } from "./rankColumns_2"; type HeroesProps = { dispatchHeroStats: Function; diff --git a/src/components/Heroes/rankColumns_2.ts b/src/components/Heroes/rankColumns_2.ts new file mode 100644 index 0000000000..2211e7b644 --- /dev/null +++ b/src/components/Heroes/rankColumns_2.ts @@ -0,0 +1,345 @@ +import { heroes } from "dotaconstants"; +import constants from "../constants"; +import { abbreviateNumber, displayHeroId } from "../../utility"; +import React from "react"; + +type Props = { + tabType: HeroesTab; + strings: Strings; +}; + +export enum HeroesTab { + PRO = "pro", + PUBLIC = "public", + TURBO = "turbo", +} + +type ProColumn = { + displayName: string; + field: string; + sortFn: boolean; + percentBarsWithValue: (row: Row) => string; +}; + +type PublicColumn = { + displayName: string; + field: string; + sortFn: boolean | Function; + percentBarsWithValue?: (row: Row) => string | number; + displayIcon?: string; + colColor?: string; + displayFn?: Function; +}; + +type HeroColumn = { + displayName: string; + tooltip: string; + field: string; + displayFn: () => React.ReactNode; + sortFn: (row: Row) => string; +}; + +type PreparedColumn = (ProColumn | PublicColumn | HeroColumn) & ColumnAddition; + +type ColumnAddition = { + paddingLeft: number; + paddingRight: number; + tooltip: string; +}; + +type Row = Record; + +type Column = ProColumn | PublicColumn | HeroColumn; + +export const rankColumns = (props: Props) => { + const columns = { + [HeroesTab.PRO]: generateProTabColumns(props.strings), + [HeroesTab.PUBLIC]: generatePublicTabColumns(props.strings), + [HeroesTab.TURBO]: generateTurboTabColumns(props.strings), + }; + return columns[props.tabType]; +}; + +// Helper function to calculate percentage change safely +const calculateTrendChange = (winsTrend: number[], picksTrend: number[]) => { + if (!winsTrend || !picksTrend || !Array.isArray(winsTrend) || !Array.isArray(picksTrend)) return 0; + + // Slice off the incomplete 7th day if present + const wins = winsTrend.length === 7 ? winsTrend.slice(0, 6) : winsTrend; + const picks = picksTrend.length === 7 ? picksTrend.slice(0, 6) : picksTrend; + + if (wins.length < 2 || wins.length !== picks.length) return 0; + + // Calculate daily win rate (wins / picks) to neutralize volume/weekend skews + const dailyRates = wins.map((w, i) => { + const p = picks[i] || 0; + return p > 0 ? w / p : 0; + }); + + const midPoint = Math.floor(dailyRates.length / 2); + const earlyDays = dailyRates.slice(0, midPoint); + const recentDays = dailyRates.slice(midPoint); + + const earlyAvg = earlyDays.reduce((a, b) => a + b, 0) / earlyDays.length; + const recentAvg = recentDays.reduce((a, b) => a + b, 0) / recentDays.length; + + if (!earlyAvg || earlyAvg === 0) return 0; + + // Calculate absolute percentage point change (e.g., 0.52 - 0.50 = +2.0%) + const change = (recentAvg - earlyAvg) * 100; + + return Number.isFinite(change) ? Number(change.toFixed(2)) : 0; +}; + +const generateTurboTabColumns = (strings: Strings) => { + const heroColumn = generateHeroColumn(strings); + + const combinedColumns = [ + heroColumn, + { + displayName: strings.hero_turbo_pick_rate, + field: "pickRateTurbo", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRateTurbo, row.matchCountTurbo), + }, + { + displayName: strings.hero_turbo_win_rate, + field: "winRateTurbo", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRateTurbo, row.turbo_picks), + }, + { + displayName: `Change (7D)`, + field: "custom_turbo_win_change", + sortFn: (row: Row) => calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend), + displayFn: (row: Row) => { + const change = calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend); + const isPositive = change >= 0; + const sign = isPositive ? "+" : ""; + const arrow = isPositive ? "▲" : "▼"; + const color = isPositive ? "#4ade80" : "#f87171"; + + return React.createElement( + "span", + { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, + React.createElement("span", { style: { fontSize: "10px" } }, arrow), + `${sign}${change.toFixed(2)}%` + ); + }, + }, + { + displayName: strings.hero_turbo_win_rate_diff, + field: "winRateTurboVsPub", + sortFn: (row: Row) => row.winRateTurbo - row.winRatePub, + displayFn: (_row: Row, _col: string, value: any) => { + if (!Number.isFinite(value)) { + return "–"; + } + const diff = value * 100; + return `${diff > 0 ? "+" : ""}${diff.toFixed(1)}`; + }, + }, + ]; + + return combinedColumns; +}; + +const getRankIcon = (number: number) => + `/assets/images/dota2/rank_icons/rank_icon_${number}.png`; + +const generateProTabColumns = (strings: Strings) => { + const heroColumn = generateHeroColumn(strings); + + const combinedColumns = [ + heroColumn, + { + displayName: strings.hero_pick_ban_rate, + field: "pickBanRatePro", + sortFn: true, + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickBanRatePro, row.matchCountPro), + }, + { + displayName: strings.hero_pick_rate, + field: "pickRatePro", + sortFn: true, + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRatePro, row.matchCountPro), + }, + { + displayName: strings.hero_ban_rate, + field: "banRatePro", + sortFn: true, + percentBarsWithValue: (row: Row) => + decimalToCount(row.banRatePro, row.matchCountPro), + }, + { + displayName: strings.hero_win_rate, + field: "winRatePro", + sortFn: true, + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRatePro, row.pro_pick), + }, + ]; + + return combinedColumns; +}; + +const generateHeroColumn = (strings: Strings): HeroColumn => { + return { + displayName: strings.th_hero_id, + tooltip: strings.tooltip_hero_id, + field: "hero_id", + displayFn: displayHeroId, + sortFn: (row: Row) => { + return heroes[row.hero_id as keyof Heroes]?.localized_name; + }, + }; +}; + +const decimalToCount = (decimal: number, matchTotal: number) => { + if (decimal > 0) { + const count = decimal * matchTotal; + const roundedCount = abbreviateNumber(Math.floor(count)); + return roundedCount; + } + return 0; +}; + +const generatePublicTabColumns = (strings: Strings) => { + const columns = [ + { + displayName: `${strings.rank_tier_overall} p%`, + field: "pickRatePub", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRatePub, row.matchCountPub), + }, + { + displayName: `${strings.rank_tier_overall} w%`, + field: "winRatePub", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRatePub, row.pickCountPub), + }, + { + displayName: `Change (7D)`, + field: "custom_win_change", + sortFn: (row: Row) => calculateTrendChange(row.pub_win_trend, row.pub_pick_trend), + displayFn: (row: Row) => { + const change = calculateTrendChange(row.pub_win_trend, row.pub_pick_trend); + const isPositive = change >= 0; + const sign = isPositive ? "+" : ""; + const arrow = isPositive ? "▲" : "▼"; + const color = isPositive ? "#4ade80" : "#f87171"; + + return React.createElement( + "span", + { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, + React.createElement("span", { style: { fontSize: "10px" } }, arrow), + `${sign}${change.toFixed(2)}%` + ); + }, + }, + { + displayName: `${strings.rank_tier_high} p%`, + displayIcon: getRankIcon(8), + field: "pickRateHigh", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRateHigh, row.matchCountHigh), + colColor: constants.colorImmortal, + }, + { + displayName: `${strings.rank_tier_high} w%`, + displayIcon: getRankIcon(8), + field: "winRateHigh", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRateHigh, row.pickCountHigh), + colColor: constants.colorImmortalAlt, + }, + { + displayName: `${strings.rank_tier_mid} p%`, + displayIcon: getRankIcon(5), + field: "pickRateMid", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRateMid, row.matchCountMid), + colColor: constants.colorLegend, + }, + { + displayName: `${strings.rank_tier_mid} w%`, + displayIcon: getRankIcon(5), + field: "winRateMid", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRateMid, row.pickCountMid), + colColor: constants.colorLegendAlt, + }, + { + displayName: `${strings.rank_tier_low} p%`, + displayIcon: getRankIcon(3), + field: "pickRateLow", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.pickRateLow, row.matchCountLow), + colColor: constants.colorCrusader, + }, + { + displayName: `${strings.rank_tier_low} w%`, + displayIcon: getRankIcon(3), + field: "winRateLow", + sortFn: true, + displayFn: (_row: Row, _col: string, field: any) => + (field * 100).toFixed(1), + percentBarsWithValue: (row: Row) => + decimalToCount(row.winRateLow, row.pickCountLow), + colColor: constants.colorCrusaderAlt, + }, + ]; + + const preparedHeroColumn = prepareHeroColumn(strings); + const preparedColumns = prepareColumns(columns, strings); + + return preparedHeroColumn.concat(preparedColumns); +}; + +const prepareColumns = (columns: Column[], strings: Strings) => { + return columns.map((column) => { + const preparedColumn: PreparedColumn = { + ...column, + tooltip: column.displayName, + paddingRight: 1, + paddingLeft: 4, + }; + + return preparedColumn; + }); +}; + +const prepareHeroColumn = (strings: Strings): PreparedColumn[] => { + const columns = [generateHeroColumn(strings)]; + + return prepareColumns(columns, strings); +}; \ No newline at end of file From 89c924d01648451c841361fecc76447500e68207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=B1=D9=8A=D8=A7=D9=86?= <112495059+ryanab00@users.noreply.github.com> Date: Sat, 10 Oct 2026 08:58:16 -0500 Subject: [PATCH 2/4] Removed dupe rankColumns page All changes are now seeable in the original rankColumns.ts file. --- src/components/Heroes/Heroes.tsx | 2 +- src/components/Heroes/rankColumns.ts | 98 ++++++- src/components/Heroes/rankColumns_2.ts | 345 ------------------------- 3 files changed, 85 insertions(+), 360 deletions(-) delete mode 100644 src/components/Heroes/rankColumns_2.ts diff --git a/src/components/Heroes/Heroes.tsx b/src/components/Heroes/Heroes.tsx index 4c0512f124..e016276896 100644 --- a/src/components/Heroes/Heroes.tsx +++ b/src/components/Heroes/Heroes.tsx @@ -8,7 +8,7 @@ import Table from "../Table/Table"; import TabBar from "../TabBar/TabBar"; import Hero from "../Hero/Hero"; import { sum, abbreviateNumber } from "../../utility"; -import { HeroesTab, rankColumns } from "./rankColumns_2"; +import { HeroesTab, rankColumns } from "./rankColumns"; type HeroesProps = { dispatchHeroStats: Function; diff --git a/src/components/Heroes/rankColumns.ts b/src/components/Heroes/rankColumns.ts index 3e3644e057..2211e7b644 100644 --- a/src/components/Heroes/rankColumns.ts +++ b/src/components/Heroes/rankColumns.ts @@ -24,10 +24,11 @@ type ProColumn = { type PublicColumn = { displayName: string; field: string; - sortFn: boolean; - percentBarsWithValue: (row: Row) => string | number; + sortFn: boolean | Function; + percentBarsWithValue?: (row: Row) => string | number; displayIcon?: string; colColor?: string; + displayFn?: Function; }; type HeroColumn = { @@ -59,6 +60,37 @@ export const rankColumns = (props: Props) => { return columns[props.tabType]; }; +// Helper function to calculate percentage change safely +const calculateTrendChange = (winsTrend: number[], picksTrend: number[]) => { + if (!winsTrend || !picksTrend || !Array.isArray(winsTrend) || !Array.isArray(picksTrend)) return 0; + + // Slice off the incomplete 7th day if present + const wins = winsTrend.length === 7 ? winsTrend.slice(0, 6) : winsTrend; + const picks = picksTrend.length === 7 ? picksTrend.slice(0, 6) : picksTrend; + + if (wins.length < 2 || wins.length !== picks.length) return 0; + + // Calculate daily win rate (wins / picks) to neutralize volume/weekend skews + const dailyRates = wins.map((w, i) => { + const p = picks[i] || 0; + return p > 0 ? w / p : 0; + }); + + const midPoint = Math.floor(dailyRates.length / 2); + const earlyDays = dailyRates.slice(0, midPoint); + const recentDays = dailyRates.slice(midPoint); + + const earlyAvg = earlyDays.reduce((a, b) => a + b, 0) / earlyDays.length; + const recentAvg = recentDays.reduce((a, b) => a + b, 0) / recentDays.length; + + if (!earlyAvg || earlyAvg === 0) return 0; + + // Calculate absolute percentage point change (e.g., 0.52 - 0.50 = +2.0%) + const change = (recentAvg - earlyAvg) * 100; + + return Number.isFinite(change) ? Number(change.toFixed(2)) : 0; +}; + const generateTurboTabColumns = (strings: Strings) => { const heroColumn = generateHeroColumn(strings); @@ -82,6 +114,25 @@ const generateTurboTabColumns = (strings: Strings) => { percentBarsWithValue: (row: Row) => decimalToCount(row.winRateTurbo, row.turbo_picks), }, + { + displayName: `Change (7D)`, + field: "custom_turbo_win_change", + sortFn: (row: Row) => calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend), + displayFn: (row: Row) => { + const change = calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend); + const isPositive = change >= 0; + const sign = isPositive ? "+" : ""; + const arrow = isPositive ? "▲" : "▼"; + const color = isPositive ? "#4ade80" : "#f87171"; + + return React.createElement( + "span", + { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, + React.createElement("span", { style: { fontSize: "10px" } }, arrow), + `${sign}${change.toFixed(2)}%` + ); + }, + }, { displayName: strings.hero_turbo_win_rate_diff, field: "winRateTurboVsPub", @@ -99,6 +150,9 @@ const generateTurboTabColumns = (strings: Strings) => { return combinedColumns; }; +const getRankIcon = (number: number) => + `/assets/images/dota2/rank_icons/rank_icon_${number}.png`; + const generateProTabColumns = (strings: Strings) => { const heroColumn = generateHeroColumn(strings); @@ -161,7 +215,7 @@ const decimalToCount = (decimal: number, matchTotal: number) => { const generatePublicTabColumns = (strings: Strings) => { const columns = [ { - displayName: `${strings.rank_tier_overall} ${strings.abbr_pick}%`, + displayName: `${strings.rank_tier_overall} p%`, field: "pickRatePub", sortFn: true, displayFn: (_row: Row, _col: string, field: any) => @@ -170,7 +224,7 @@ const generatePublicTabColumns = (strings: Strings) => { decimalToCount(row.pickRatePub, row.matchCountPub), }, { - displayName: `${strings.rank_tier_overall} ${strings.abbr_win}%`, + displayName: `${strings.rank_tier_overall} w%`, field: "winRatePub", sortFn: true, displayFn: (_row: Row, _col: string, field: any) => @@ -178,8 +232,27 @@ const generatePublicTabColumns = (strings: Strings) => { percentBarsWithValue: (row: Row) => decimalToCount(row.winRatePub, row.pickCountPub), }, + { + displayName: `Change (7D)`, + field: "custom_win_change", + sortFn: (row: Row) => calculateTrendChange(row.pub_win_trend, row.pub_pick_trend), + displayFn: (row: Row) => { + const change = calculateTrendChange(row.pub_win_trend, row.pub_pick_trend); + const isPositive = change >= 0; + const sign = isPositive ? "+" : ""; + const arrow = isPositive ? "▲" : "▼"; + const color = isPositive ? "#4ade80" : "#f87171"; + + return React.createElement( + "span", + { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, + React.createElement("span", { style: { fontSize: "10px" } }, arrow), + `${sign}${change.toFixed(2)}%` + ); + }, + }, { - displayName: `${strings.rank_tier_high} ${strings.abbr_pick}%`, + displayName: `${strings.rank_tier_high} p%`, displayIcon: getRankIcon(8), field: "pickRateHigh", sortFn: true, @@ -190,7 +263,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorImmortal, }, { - displayName: `${strings.rank_tier_high} ${strings.abbr_win}%`, + displayName: `${strings.rank_tier_high} w%`, displayIcon: getRankIcon(8), field: "winRateHigh", sortFn: true, @@ -201,7 +274,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorImmortalAlt, }, { - displayName: `${strings.rank_tier_mid} ${strings.abbr_pick}%`, + displayName: `${strings.rank_tier_mid} p%`, displayIcon: getRankIcon(5), field: "pickRateMid", sortFn: true, @@ -212,7 +285,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorLegend, }, { - displayName: `${strings.rank_tier_mid} ${strings.abbr_win}%`, + displayName: `${strings.rank_tier_mid} w%`, displayIcon: getRankIcon(5), field: "winRateMid", sortFn: true, @@ -223,7 +296,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorLegendAlt, }, { - displayName: `${strings.rank_tier_low} ${strings.abbr_pick}%`, + displayName: `${strings.rank_tier_low} p%`, displayIcon: getRankIcon(3), field: "pickRateLow", sortFn: true, @@ -234,7 +307,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorCrusader, }, { - displayName: `${strings.rank_tier_low} ${strings.abbr_win}%`, + displayName: `${strings.rank_tier_low} w%`, displayIcon: getRankIcon(3), field: "winRateLow", sortFn: true, @@ -252,9 +325,6 @@ const generatePublicTabColumns = (strings: Strings) => { return preparedHeroColumn.concat(preparedColumns); }; -const getRankIcon = (number: number) => - `/assets/images/dota2/rank_icons/rank_icon_${number}.png`; - const prepareColumns = (columns: Column[], strings: Strings) => { return columns.map((column) => { const preparedColumn: PreparedColumn = { @@ -272,4 +342,4 @@ const prepareHeroColumn = (strings: Strings): PreparedColumn[] => { const columns = [generateHeroColumn(strings)]; return prepareColumns(columns, strings); -}; +}; \ No newline at end of file diff --git a/src/components/Heroes/rankColumns_2.ts b/src/components/Heroes/rankColumns_2.ts deleted file mode 100644 index 2211e7b644..0000000000 --- a/src/components/Heroes/rankColumns_2.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { heroes } from "dotaconstants"; -import constants from "../constants"; -import { abbreviateNumber, displayHeroId } from "../../utility"; -import React from "react"; - -type Props = { - tabType: HeroesTab; - strings: Strings; -}; - -export enum HeroesTab { - PRO = "pro", - PUBLIC = "public", - TURBO = "turbo", -} - -type ProColumn = { - displayName: string; - field: string; - sortFn: boolean; - percentBarsWithValue: (row: Row) => string; -}; - -type PublicColumn = { - displayName: string; - field: string; - sortFn: boolean | Function; - percentBarsWithValue?: (row: Row) => string | number; - displayIcon?: string; - colColor?: string; - displayFn?: Function; -}; - -type HeroColumn = { - displayName: string; - tooltip: string; - field: string; - displayFn: () => React.ReactNode; - sortFn: (row: Row) => string; -}; - -type PreparedColumn = (ProColumn | PublicColumn | HeroColumn) & ColumnAddition; - -type ColumnAddition = { - paddingLeft: number; - paddingRight: number; - tooltip: string; -}; - -type Row = Record; - -type Column = ProColumn | PublicColumn | HeroColumn; - -export const rankColumns = (props: Props) => { - const columns = { - [HeroesTab.PRO]: generateProTabColumns(props.strings), - [HeroesTab.PUBLIC]: generatePublicTabColumns(props.strings), - [HeroesTab.TURBO]: generateTurboTabColumns(props.strings), - }; - return columns[props.tabType]; -}; - -// Helper function to calculate percentage change safely -const calculateTrendChange = (winsTrend: number[], picksTrend: number[]) => { - if (!winsTrend || !picksTrend || !Array.isArray(winsTrend) || !Array.isArray(picksTrend)) return 0; - - // Slice off the incomplete 7th day if present - const wins = winsTrend.length === 7 ? winsTrend.slice(0, 6) : winsTrend; - const picks = picksTrend.length === 7 ? picksTrend.slice(0, 6) : picksTrend; - - if (wins.length < 2 || wins.length !== picks.length) return 0; - - // Calculate daily win rate (wins / picks) to neutralize volume/weekend skews - const dailyRates = wins.map((w, i) => { - const p = picks[i] || 0; - return p > 0 ? w / p : 0; - }); - - const midPoint = Math.floor(dailyRates.length / 2); - const earlyDays = dailyRates.slice(0, midPoint); - const recentDays = dailyRates.slice(midPoint); - - const earlyAvg = earlyDays.reduce((a, b) => a + b, 0) / earlyDays.length; - const recentAvg = recentDays.reduce((a, b) => a + b, 0) / recentDays.length; - - if (!earlyAvg || earlyAvg === 0) return 0; - - // Calculate absolute percentage point change (e.g., 0.52 - 0.50 = +2.0%) - const change = (recentAvg - earlyAvg) * 100; - - return Number.isFinite(change) ? Number(change.toFixed(2)) : 0; -}; - -const generateTurboTabColumns = (strings: Strings) => { - const heroColumn = generateHeroColumn(strings); - - const combinedColumns = [ - heroColumn, - { - displayName: strings.hero_turbo_pick_rate, - field: "pickRateTurbo", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRateTurbo, row.matchCountTurbo), - }, - { - displayName: strings.hero_turbo_win_rate, - field: "winRateTurbo", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRateTurbo, row.turbo_picks), - }, - { - displayName: `Change (7D)`, - field: "custom_turbo_win_change", - sortFn: (row: Row) => calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend), - displayFn: (row: Row) => { - const change = calculateTrendChange(row.turbo_wins_trend, row.turbo_picks_trend); - const isPositive = change >= 0; - const sign = isPositive ? "+" : ""; - const arrow = isPositive ? "▲" : "▼"; - const color = isPositive ? "#4ade80" : "#f87171"; - - return React.createElement( - "span", - { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, - React.createElement("span", { style: { fontSize: "10px" } }, arrow), - `${sign}${change.toFixed(2)}%` - ); - }, - }, - { - displayName: strings.hero_turbo_win_rate_diff, - field: "winRateTurboVsPub", - sortFn: (row: Row) => row.winRateTurbo - row.winRatePub, - displayFn: (_row: Row, _col: string, value: any) => { - if (!Number.isFinite(value)) { - return "–"; - } - const diff = value * 100; - return `${diff > 0 ? "+" : ""}${diff.toFixed(1)}`; - }, - }, - ]; - - return combinedColumns; -}; - -const getRankIcon = (number: number) => - `/assets/images/dota2/rank_icons/rank_icon_${number}.png`; - -const generateProTabColumns = (strings: Strings) => { - const heroColumn = generateHeroColumn(strings); - - const combinedColumns = [ - heroColumn, - { - displayName: strings.hero_pick_ban_rate, - field: "pickBanRatePro", - sortFn: true, - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickBanRatePro, row.matchCountPro), - }, - { - displayName: strings.hero_pick_rate, - field: "pickRatePro", - sortFn: true, - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRatePro, row.matchCountPro), - }, - { - displayName: strings.hero_ban_rate, - field: "banRatePro", - sortFn: true, - percentBarsWithValue: (row: Row) => - decimalToCount(row.banRatePro, row.matchCountPro), - }, - { - displayName: strings.hero_win_rate, - field: "winRatePro", - sortFn: true, - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRatePro, row.pro_pick), - }, - ]; - - return combinedColumns; -}; - -const generateHeroColumn = (strings: Strings): HeroColumn => { - return { - displayName: strings.th_hero_id, - tooltip: strings.tooltip_hero_id, - field: "hero_id", - displayFn: displayHeroId, - sortFn: (row: Row) => { - return heroes[row.hero_id as keyof Heroes]?.localized_name; - }, - }; -}; - -const decimalToCount = (decimal: number, matchTotal: number) => { - if (decimal > 0) { - const count = decimal * matchTotal; - const roundedCount = abbreviateNumber(Math.floor(count)); - return roundedCount; - } - return 0; -}; - -const generatePublicTabColumns = (strings: Strings) => { - const columns = [ - { - displayName: `${strings.rank_tier_overall} p%`, - field: "pickRatePub", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRatePub, row.matchCountPub), - }, - { - displayName: `${strings.rank_tier_overall} w%`, - field: "winRatePub", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRatePub, row.pickCountPub), - }, - { - displayName: `Change (7D)`, - field: "custom_win_change", - sortFn: (row: Row) => calculateTrendChange(row.pub_win_trend, row.pub_pick_trend), - displayFn: (row: Row) => { - const change = calculateTrendChange(row.pub_win_trend, row.pub_pick_trend); - const isPositive = change >= 0; - const sign = isPositive ? "+" : ""; - const arrow = isPositive ? "▲" : "▼"; - const color = isPositive ? "#4ade80" : "#f87171"; - - return React.createElement( - "span", - { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, - React.createElement("span", { style: { fontSize: "10px" } }, arrow), - `${sign}${change.toFixed(2)}%` - ); - }, - }, - { - displayName: `${strings.rank_tier_high} p%`, - displayIcon: getRankIcon(8), - field: "pickRateHigh", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRateHigh, row.matchCountHigh), - colColor: constants.colorImmortal, - }, - { - displayName: `${strings.rank_tier_high} w%`, - displayIcon: getRankIcon(8), - field: "winRateHigh", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRateHigh, row.pickCountHigh), - colColor: constants.colorImmortalAlt, - }, - { - displayName: `${strings.rank_tier_mid} p%`, - displayIcon: getRankIcon(5), - field: "pickRateMid", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRateMid, row.matchCountMid), - colColor: constants.colorLegend, - }, - { - displayName: `${strings.rank_tier_mid} w%`, - displayIcon: getRankIcon(5), - field: "winRateMid", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRateMid, row.pickCountMid), - colColor: constants.colorLegendAlt, - }, - { - displayName: `${strings.rank_tier_low} p%`, - displayIcon: getRankIcon(3), - field: "pickRateLow", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.pickRateLow, row.matchCountLow), - colColor: constants.colorCrusader, - }, - { - displayName: `${strings.rank_tier_low} w%`, - displayIcon: getRankIcon(3), - field: "winRateLow", - sortFn: true, - displayFn: (_row: Row, _col: string, field: any) => - (field * 100).toFixed(1), - percentBarsWithValue: (row: Row) => - decimalToCount(row.winRateLow, row.pickCountLow), - colColor: constants.colorCrusaderAlt, - }, - ]; - - const preparedHeroColumn = prepareHeroColumn(strings); - const preparedColumns = prepareColumns(columns, strings); - - return preparedHeroColumn.concat(preparedColumns); -}; - -const prepareColumns = (columns: Column[], strings: Strings) => { - return columns.map((column) => { - const preparedColumn: PreparedColumn = { - ...column, - tooltip: column.displayName, - paddingRight: 1, - paddingLeft: 4, - }; - - return preparedColumn; - }); -}; - -const prepareHeroColumn = (strings: Strings): PreparedColumn[] => { - const columns = [generateHeroColumn(strings)]; - - return prepareColumns(columns, strings); -}; \ No newline at end of file From 84e623f8d2fccf0164944e064a9d6a761a13a6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=B1=D9=8A=D8=A7=D9=86?= <112495059+ryanab00@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:50:29 -0500 Subject: [PATCH 3/4] Converted rankColumns to .tsx, fixed column header naming. --- package-lock.json | 48 ++----------------- .../{rankColumns.ts => rankColumns.tsx} | 16 +++---- 2 files changed, 12 insertions(+), 52 deletions(-) rename src/components/Heroes/{rankColumns.ts => rankColumns.tsx} (94%) diff --git a/package-lock.json b/package-lock.json index 0681d76ac0..a1c8b5494f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@melloware/react-logviewer": "^6.6.0", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.6", - "abcolor": "^0.5.5", "ace-builds": "^1.43.5", "air-datepicker": "^3.6.0", "dota2-emoticons": "^1.0.4", @@ -112,7 +111,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -2175,7 +2173,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2845,7 +2842,6 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -3651,7 +3647,6 @@ "version": "17.0.47", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3755,14 +3750,6 @@ "integrity": "sha512-UMKd/doXE3karDUd5l+2LqFKUU4SmuPWOT/VA22OrPOUGMlb8/qQvSf+DBLQf/sd8UphuBGxOK/jB/1BB5oQRw==", "dev": true }, - "node_modules/abcolor": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/abcolor/-/abcolor-0.5.5.tgz", - "integrity": "sha512-xYDsPOWM2/n6Xvc9mwcJ1BIHdk6ahjWIqp5dwx9P+s0Ou/f6moythvJOdUNTdeRkB18e0yGin4DBZUksoaEQiw==", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/ace-builds": { "version": "1.43.5", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.5.tgz", @@ -4109,7 +4096,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6924,7 +6910,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7028,7 +7013,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -7065,7 +7049,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -7387,7 +7370,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -7897,7 +7879,6 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", "hasInstallScript": true, - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.0.0", @@ -7926,8 +7907,7 @@ "node_modules/stylis": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "peer": true + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" }, "node_modules/stylis-rule-sheet": { "version": "0.0.10", @@ -8715,7 +8695,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -9324,7 +9303,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -9613,7 +9591,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -11035,7 +11012,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "requires": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -11383,7 +11359,6 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", - "peer": true, "requires": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -11862,7 +11837,6 @@ "version": "17.0.47", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.47.tgz", "integrity": "sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==", - "peer": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -11963,11 +11937,6 @@ "integrity": "sha512-UMKd/doXE3karDUd5l+2LqFKUU4SmuPWOT/VA22OrPOUGMlb8/qQvSf+DBLQf/sd8UphuBGxOK/jB/1BB5oQRw==", "dev": true }, - "abcolor": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/abcolor/-/abcolor-0.5.5.tgz", - "integrity": "sha512-xYDsPOWM2/n6Xvc9mwcJ1BIHdk6ahjWIqp5dwx9P+s0Ou/f6moythvJOdUNTdeRkB18e0yGin4DBZUksoaEQiw==" - }, "ace-builds": { "version": "1.43.5", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.5.tgz", @@ -12221,7 +12190,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -14113,7 +14081,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -14184,7 +14151,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -14208,7 +14174,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -14462,7 +14427,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, "requires": { "@babel/runtime": "^7.9.2" } @@ -14834,7 +14798,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", - "peer": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.0.0", @@ -14861,8 +14824,7 @@ "stylis": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "peer": true + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" }, "stylis-rule-sheet": { "version": "0.0.10", @@ -15453,8 +15415,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true } } }, @@ -15804,8 +15765,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "peer": true + "dev": true } } }, diff --git a/src/components/Heroes/rankColumns.ts b/src/components/Heroes/rankColumns.tsx similarity index 94% rename from src/components/Heroes/rankColumns.ts rename to src/components/Heroes/rankColumns.tsx index 2211e7b644..5637249936 100644 --- a/src/components/Heroes/rankColumns.ts +++ b/src/components/Heroes/rankColumns.tsx @@ -215,7 +215,7 @@ const decimalToCount = (decimal: number, matchTotal: number) => { const generatePublicTabColumns = (strings: Strings) => { const columns = [ { - displayName: `${strings.rank_tier_overall} p%`, + displayName: `${strings.rank_tier_overall} ${strings.abbr_pick}%`, field: "pickRatePub", sortFn: true, displayFn: (_row: Row, _col: string, field: any) => @@ -224,7 +224,7 @@ const generatePublicTabColumns = (strings: Strings) => { decimalToCount(row.pickRatePub, row.matchCountPub), }, { - displayName: `${strings.rank_tier_overall} w%`, + displayName: `${strings.rank_tier_overall} ${strings.abbr_win}%`, field: "winRatePub", sortFn: true, displayFn: (_row: Row, _col: string, field: any) => @@ -252,7 +252,7 @@ const generatePublicTabColumns = (strings: Strings) => { }, }, { - displayName: `${strings.rank_tier_high} p%`, + displayName: `${strings.rank_tier_high} ${strings.abbr_pick}%`, displayIcon: getRankIcon(8), field: "pickRateHigh", sortFn: true, @@ -263,7 +263,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorImmortal, }, { - displayName: `${strings.rank_tier_high} w%`, + displayName: `${strings.rank_tier_high} ${strings.abbr_win}%`, displayIcon: getRankIcon(8), field: "winRateHigh", sortFn: true, @@ -274,7 +274,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorImmortalAlt, }, { - displayName: `${strings.rank_tier_mid} p%`, + displayName: `${strings.rank_tier_mid} ${strings.abbr_pick}%`, displayIcon: getRankIcon(5), field: "pickRateMid", sortFn: true, @@ -285,7 +285,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorLegend, }, { - displayName: `${strings.rank_tier_mid} w%`, + displayName: `${strings.rank_tier_mid} ${strings.abbr_win}%`, displayIcon: getRankIcon(5), field: "winRateMid", sortFn: true, @@ -296,7 +296,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorLegendAlt, }, { - displayName: `${strings.rank_tier_low} p%`, + displayName: `${strings.rank_tier_low} ${strings.abbr_pick}%`, displayIcon: getRankIcon(3), field: "pickRateLow", sortFn: true, @@ -307,7 +307,7 @@ const generatePublicTabColumns = (strings: Strings) => { colColor: constants.colorCrusader, }, { - displayName: `${strings.rank_tier_low} w%`, + displayName: `${strings.rank_tier_low} ${strings.abbr_win}%`, displayIcon: getRankIcon(3), field: "winRateLow", sortFn: true, From 363f923280529c2ab0e1754ab31100ad71186bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=B1=D9=8A=D8=A7=D9=86?= <112495059+ryanab00@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:26:37 -0500 Subject: [PATCH 4/4] Use JSX for rank change indicators Replace React.createElement calls with JSX in the turbo and public rank columns. --- src/components/Heroes/rankColumns.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/Heroes/rankColumns.tsx b/src/components/Heroes/rankColumns.tsx index 5637249936..90f7c5ba83 100644 --- a/src/components/Heroes/rankColumns.tsx +++ b/src/components/Heroes/rankColumns.tsx @@ -125,11 +125,11 @@ const generateTurboTabColumns = (strings: Strings) => { const arrow = isPositive ? "▲" : "▼"; const color = isPositive ? "#4ade80" : "#f87171"; - return React.createElement( - "span", - { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, - React.createElement("span", { style: { fontSize: "10px" } }, arrow), - `${sign}${change.toFixed(2)}%` + return ( + + {arrow} + {`${sign}${change.toFixed(2)}%`} + ); }, }, @@ -243,11 +243,11 @@ const generatePublicTabColumns = (strings: Strings) => { const arrow = isPositive ? "▲" : "▼"; const color = isPositive ? "#4ade80" : "#f87171"; - return React.createElement( - "span", - { style: { color, display: "inline-flex", alignItems: "center", gap: "4px", fontWeight: "500" } }, - React.createElement("span", { style: { fontSize: "10px" } }, arrow), - `${sign}${change.toFixed(2)}%` + return ( + + {arrow} + {`${sign}${change.toFixed(2)}%`} + ); }, },