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 73%
rename from src/components/Heroes/rankColumns.ts
rename to src/components/Heroes/rankColumns.tsx
index 3e3644e057..90f7c5ba83 100644
--- a/src/components/Heroes/rankColumns.ts
+++ b/src/components/Heroes/rankColumns.tsx
@@ -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 (
+
+ {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);
@@ -178,6 +232,25 @@ 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 (
+
+ {arrow}
+ {`${sign}${change.toFixed(2)}%`}
+
+ );
+ },
+ },
{
displayName: `${strings.rank_tier_high} ${strings.abbr_pick}%`,
displayIcon: getRankIcon(8),
@@ -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