Cricket Player Profile
Bouncer player profile with batting and bowling ratings across formats
Show code
t20_batting = fetchParquet(base_url + "cricket/t20-batting.parquet")
t20_bowling = fetchParquet(base_url + "cricket/t20-bowling.parquet")
odi_batting = fetchParquet(base_url + "cricket/odi-batting.parquet")
odi_bowling = fetchParquet(base_url + "cricket/odi-bowling.parquet")
test_batting = fetchParquet(base_url + "cricket/test-batting.parquet")
test_bowling = fetchParquet(base_url + "cricket/test-bowling.parquet")Show code
playerName = {
const raw = window._getHashParam("name")
return raw ? raw.replace(/\+/g, " ") : null
}
// Find player across all formats (search full_name first, fall back to player)
playerData = {
if (!playerName) return null
const find = (data) => data ? (data.find(d => d.full_name === playerName) || data.find(d => d.player === playerName)) : null
return {
t20_bat: find(t20_batting),
t20_bowl: find(t20_bowling),
odi_bat: find(odi_batting),
odi_bowl: find(odi_bowling),
test_bat: find(test_batting),
test_bowl: find(test_bowling)
}
}
// Get first available record for bio info
playerBio = {
if (!playerData) return null
const first = playerData.t20_bat || playerData.t20_bowl || playerData.odi_bat || playerData.odi_bowl || playerData.test_bat || playerData.test_bowl
if (!first) return null
let age = null
if (first.dob) {
const ms = Date.now() - new Date(first.dob).getTime()
age = +(ms / 31557600000).toFixed(1)
}
return {
name: first.player,
country: first.country || "",
full_name: first.full_name || "",
batting_style: first.batting_style || "",
bowling_style: first.bowling_style || "",
age
}
}Show code
Show code
// Breadcrumb + player header
{
if (!playerName) {
return html`<div class="breadcrumb"><a href="index.html">Cricket</a> > <a href="player-ratings.html">Player Ratings</a></div>
<p class="text-muted">No player selected. Go to <a href="player-ratings.html">Player Ratings</a> and click a player name.</p>`
}
const allNull = [t20_batting, t20_bowling, odi_batting, odi_bowling, test_batting, test_bowling].every(d => d === null)
if (allNull) {
return html`<div class="breadcrumb"><a href="index.html">Cricket</a> > <a href="player-ratings.html">Player Ratings</a></div>
<p class="text-muted">Player data could not be loaded. Try refreshing the page.</p>`
}
if (!playerBio) {
return html`<div class="breadcrumb"><a href="index.html">Cricket</a> > <a href="player-ratings.html">Player Ratings</a></div>
<p class="text-muted">Player "${statsEsc(playerName)}" not found in any format. <a href="player-ratings.html">Browse all players</a>.</p>`
}
const name = playerBio.full_name || playerBio.name || playerName
const initials = name.split(" ").map(w => w[0]).join("").substring(0, 2).toUpperCase()
const teamColor = "#f59e0b" // cricket amber accent
// Style badges as inline spans
function makeBadge(val, colorMap) {
const p = colorMap[val] || { a: String(val || "").substring(0, 3), c: "#9ca3af" }
return html`<span class="pos-badge" style="background:${p.c}18;color:${p.c};border:1px solid ${p.c}35">${p.a}</span>`
}
const badges = []
const normBat = window.cricketStyleMaps.normalizeBatStyle
const normBowl = window.cricketStyleMaps.normalizeBowlStyle
if (playerBio.batting_style) badges.push(makeBadge(normBat(playerBio.batting_style), batStyleColors))
if (playerBio.bowling_style) badges.push(makeBadge(normBowl(playerBio.bowling_style), bowlStyleColors))
// Info line
const parts = []
if (playerBio.country) parts.push(playerBio.country)
if (playerBio.age) parts.push(`Age ${playerBio.age}`)
const infoLine = parts.join(" · ")
return html`<div class="breadcrumb"><a href="index.html">Cricket</a> > <a href="player-ratings.html">Player Ratings</a> > ${name}</div>
<div class="player-header">
<div class="player-avatar" style="background:${teamColor}18;color:${teamColor};border:2px solid ${teamColor}">${initials}</div>
<div class="player-info">
<div class="player-name">${name}</div>
<div class="player-meta">${badges}</div>
${infoLine ? html`<div class="player-bio">${infoLine}</div>` : html``}
</div>
</div>`
}Show code
radarAxes = ["Bat Score", "Bat Survival", "Bowl Economy", "Bowl Strike Rate"]
// Percentile helper: % of values <= val
pctile = (vals, val) => {
if (!vals || vals.length === 0) return null
const sorted = vals.filter(v => v != null).sort((a, b) => a - b)
if (sorted.length === 0) return null
const rank = sorted.filter(v => v <= val).length
return Math.round((rank / sorted.length) * 100)
}
// For bowling metrics, lower is better — invert the percentile
pctileInv = (vals, val) => {
const p = pctile(vals, val)
return p != null ? 100 - p : null
}
// Build radar data per format
radarFormats = {
if (!playerData || !playerBio) return []
const formatDefs = [
{ key: "t20", label: "T20", color: "#22d3ee", bat: t20_batting, bowl: t20_bowling, pBat: playerData.t20_bat, pBowl: playerData.t20_bowl },
{ key: "odi", label: "ODI", color: "#a78bfa", bat: odi_batting, bowl: odi_bowling, pBat: playerData.odi_bat, pBowl: playerData.odi_bowl },
{ key: "test", label: "Test", color: "#fb923c", bat: test_batting, bowl: test_bowling, pBat: playerData.test_bat, pBowl: playerData.test_bowl }
]
return formatDefs.filter(f => f.pBat || f.pBowl).map(f => {
const batScoreP = f.pBat ? pctile(f.bat?.map(d => d.scoring_index), f.pBat.scoring_index) : null
const batSurvP = f.pBat ? pctile(f.bat?.map(d => d.survival_rate), f.pBat.survival_rate) : null
const bowlEconP = f.pBowl ? pctileInv(f.bowl?.map(d => d.economy_index), f.pBowl.economy_index) : null
const bowlSRP = f.pBowl ? pctileInv(f.bowl?.map(d => d.strike_rate), f.pBowl.strike_rate) : null
return {
label: f.label,
color: f.color,
values: [batScoreP, batSurvP, bowlEconP, bowlSRP]
}
})
}Show code
// ── Radar Chart — format comparison ─────────────────────────
{
if (!playerData || !playerBio || radarFormats.length === 0) return html``
const n = radarAxes.length
const cx = 250, cy = 200, R = 130
const ns = "http://www.w3.org/2000/svg"
const svg = document.createElementNS(ns, "svg")
svg.setAttribute("viewBox", "0 0 500 400")
svg.setAttribute("class", "skill-radar")
function angle(i) { return (Math.PI * 2 * i / n) - Math.PI / 2 }
function pt(i, r) {
const a = angle(i)
return [cx + r * Math.cos(a), cy + r * Math.sin(a)]
}
// Concentric rings
for (const pct of [0.25, 0.5, 0.75, 1.0]) {
const r = R * pct
const points = Array.from({ length: n }, (_, i) => pt(i, r).join(",")).join(" ")
const ring = document.createElementNS(ns, "polygon")
ring.setAttribute("points", points)
ring.setAttribute("class", "radar-ring")
svg.appendChild(ring)
}
// Axis lines
for (let i = 0; i < n; i++) {
const [x, y] = pt(i, R)
const line = document.createElementNS(ns, "line")
line.setAttribute("x1", cx)
line.setAttribute("y1", cy)
line.setAttribute("x2", x)
line.setAttribute("y2", y)
line.setAttribute("class", "radar-axis")
svg.appendChild(line)
}
// Draw one polygon per format
for (const fmt of radarFormats) {
const validPts = []
for (let i = 0; i < n; i++) {
const v = fmt.values[i]
if (v != null) validPts.push(pt(i, R * v / 100))
else validPts.push(pt(i, 0))
}
const polyPoints = validPts.map(p => p.join(",")).join(" ")
const fill = document.createElementNS(ns, "polygon")
fill.setAttribute("points", polyPoints)
fill.setAttribute("class", "radar-fill")
fill.setAttribute("fill", fmt.color + "25")
fill.setAttribute("stroke", fmt.color)
fill.setAttribute("stroke-width", "2")
svg.appendChild(fill)
// Dots
for (let i = 0; i < n; i++) {
if (fmt.values[i] == null) continue
const [x, y] = validPts[i]
const dot = document.createElementNS(ns, "circle")
dot.setAttribute("cx", x)
dot.setAttribute("cy", y)
dot.setAttribute("r", 4)
dot.setAttribute("fill", fmt.color)
dot.setAttribute("class", "radar-dot")
svg.appendChild(dot)
}
}
// Labels — adjust text-anchor for left/right labels
const labelPad = 22
for (let i = 0; i < n; i++) {
const [x, y] = pt(i, R + labelPad)
const lbl = document.createElementNS(ns, "text")
lbl.setAttribute("x", x)
lbl.setAttribute("y", y)
lbl.setAttribute("class", "radar-label")
// Right-side labels: anchor start; left-side: anchor end; top/bottom: middle
// Use style (not attribute) to override CSS .radar-label { text-anchor: middle }
if (x > cx + 10) lbl.style.textAnchor = "start"
else if (x < cx - 10) lbl.style.textAnchor = "end"
lbl.textContent = radarAxes[i]
svg.appendChild(lbl)
}
// Legend
const legend = document.createElement("div")
legend.className = "radar-legend"
for (const fmt of radarFormats) {
const item = document.createElement("span")
item.className = "radar-legend-item"
const swatch = document.createElement("span")
swatch.className = "radar-legend-swatch"
swatch.style.background = fmt.color
item.appendChild(swatch)
item.appendChild(document.createTextNode(fmt.label))
legend.appendChild(item)
}
const wrap = document.createElement("div")
wrap.className = "skill-radar-wrap"
wrap.appendChild(svg)
wrap.appendChild(legend)
const heading = document.createElement("h2")
heading.textContent = "Skill Profile"
const outer = document.createElement("div")
outer.appendChild(heading)
outer.appendChild(wrap)
return outer
}Show code
// ── Format rating cards ──────────────────────────────────────
{
if (!playerData || !playerBio) return html``
const formats = [
{ label: "T20", bat: playerData.t20_bat, bowl: playerData.t20_bowl },
{ label: "ODI", bat: playerData.odi_bat, bowl: playerData.odi_bowl },
{ label: "Test", bat: playerData.test_bat, bowl: playerData.test_bowl }
].filter(f => f.bat || f.bowl)
if (formats.length === 0) return html`<p class="text-muted">No rating data available for this player.</p>`
function fmtVal(v, decimals) {
if (v == null) return "—"
return typeof v === "number" ? v.toFixed(decimals) : String(v)
}
function fmtInt(v) {
if (v == null) return "—"
return typeof v === "number" ? v.toLocaleString() : String(v)
}
const sections = formats.map(f => {
const cards = []
if (f.bat) {
cards.push(html`<div style="margin-bottom:0.5rem">
<span style="font-size:0.75rem;color:#8b929e;text-transform:uppercase;letter-spacing:0.05em">Batting</span>
</div>
<div class="season-summary">
<div class="stat-card">
<div class="stat-label">Scoring</div>
<div class="stat-value">${fmtVal(f.bat.scoring_index, 4)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Survival</div>
<div class="stat-value">${fmtVal(f.bat.survival_rate, 4)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Balls Faced</div>
<div class="stat-value">${fmtInt(f.bat.balls_faced)}</div>
</div>
</div>`)
}
if (f.bowl) {
cards.push(html`<div style="margin-bottom:0.5rem;${f.bat ? 'margin-top:1rem' : ''}">
<span style="font-size:0.75rem;color:#8b929e;text-transform:uppercase;letter-spacing:0.05em">Bowling</span>
</div>
<div class="season-summary">
<div class="stat-card">
<div class="stat-label">Economy</div>
<div class="stat-value">${fmtVal(f.bowl.economy_index, 4)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Strike Rate</div>
<div class="stat-value">${fmtVal(f.bowl.strike_rate, 4)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Balls Bowled</div>
<div class="stat-value">${fmtInt(f.bowl.balls_bowled)}</div>
</div>
</div>`)
}
return html`<h2 style="margin-top:1.5rem">${f.label}</h2>
${cards}`
})
return html`${sections}`
}