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