┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ import { calculateBarWidth, sortItems, type SortBy } from "../sorting"; │
│ import { generateLoadoutDropdown } from "../loadout"; │
│ import { formatRace as utilFormatRace, toTitleCase } from "../../../lib/utils"; │
│ import { getSpecIcon } from "../../../lib/client-utils"; │
│ │
│ type Item = { │
│ label: string; │
│ value: number; │
│ min: number; │
│ max: number; │
│ stdev: number; │
│ percent?: number; │
│ color?: string; │
│ loadout?: any; │
│ className?: string; │
│ specName?: string; │
│ }; │
│ │
│ export function renderRankingsChartHTML( │
│ data: any, │
│ sortBy: SortBy, │
│ classColors: Record<string, string>, │
│ ): string { │
│ const items: Item[] = []; │
│ for (const [className, classData] of Object.entries<any>( │
│ data.results || {}, │
│ )) { │
│ const colorKey = String(className).toLowerCase().replace(/\s+/g, "_"); │
│ const barColor = classColors[colorKey] || "#666"; │
│ for (const [specName, specData] of Object.entries<any>(classData)) { │
│ items.push({ │
│ label: String(specName) │
│ .replace(/_/g, " ") │
│ .replace(/\b\w/g, (s) => s.toUpperCase()), │
│ value: specData.dps, │
│ min: specData.min, │
│ max: specData.max, │
│ stdev: specData.stdev, │
│ color: barColor, │
│ loadout: specData.loadout || null, │
│ className: String(className), │
│ specName: String(specName), │
│ }); │
│ } │
│ } │
│ sortItems(items, sortBy); │
│ const metric = (it: Item) => │
│ String(sortBy) === "max" │
│ ? it.max │
│ : String(sortBy) === "min" │
│ ? it.min │
│ : String(sortBy) === "stdev" │
│ ? it.stdev │
│ : it.value; │
│ const max = Math.max(...items.map(metric), 0); │
│ const min = │
│ String(sortBy) === "dps" || │
│ String(sortBy) === "max" || │
│ String(sortBy) === "min" │
│ ? 0 │
│ : Math.min(...items.map(metric), max); │
│ │
│ const bars = items │
│ .map((it, idx) => { │
│ const mval = metric(it); │
│ const width = calculateBarWidth(mval, max, min, sortBy).toFixed(1); │
│ const displayRaw = String(sortBy) === "stdev" ? it.stdev : mval; │
│ const display = Math.round(displayRaw).toLocaleString(); │
│ const tooltip = `${it.label}: ${display}`; │
│ │
│ let specIcon = ""; │
│ │
│ if (it.className && it.specName) { │
│ const properClassName = toTitleCase(it.className); │
│ const properSpecName = toTitleCase(it.specName); │
│ │
│ const iconUrl = getSpecIcon(properClassName, properSpecName); │
│ if (iconUrl) { │
│ specIcon = `<img src="${iconUrl}" alt="${properSpecName}" class="spec-ic │
│ on" style="width: 24px; height: 24px; border-radius: 2px;" />`; │
│ } │
│ } │
│ │
│ const dropdownHtml = generateLoadoutDropdown(it.loadout); │
│ const hasDropdown = dropdownHtml && dropdownHtml.length > 0; │
│ return ` │
│ <div class="chart-item-wrapper" data-index="${idx}"> │
│ <div class="chart-item-header" title="${tooltip}"> │
│ <div class="chart-item"> │
│ <div class="chart-labels"> │
│ ${specIcon} │
│ </div> │
│ <div class="chart-bar-container"> │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style="width: ${width}%; background: ${it.c │
│ olor || "#6aa84f"};"></div> │
│ </div> │
│ <div class="chart-value">${display}</div> │
│ </div> │
│ </div> │
│ </div> │
│ ${hasDropdown ? `<div class="chart-dropdown">${dropdownHtml}</div>` : ""} │
│ </div>`; │
│ }) │
│ .join(""); │
│ const titles: Record<string, string> = { │
│ dps: "DPS Rankings (Average)", │
│ max: "DPS Rankings (Maximum)", │
│ min: "DPS Rankings (Minimum)", │
│ stdev: "DPS Consistency Rankings (Low StdDev = More Consistent)", │
│ }; │
│ const title = titles[String(sortBy)] || "DPS Rankings"; │
│ return `<h2 class="card-title-large">${title}</h2><div class="chart-bars">${bars │
│ }</div>`; │
│ } │
│ │
│ function trinketIconDataFromLoadout(loadout: any) { │
│ try { │
│ const items = loadout?.equipment?.items; │
│ const t1 = items?.[12]; │
│ const t2 = items?.[13]; │
│ const tr = t1 && t1.id ? t1 : t2 && t2.id ? t2 : null; │
│ if (!tr) return null; │
│ const ilvl = tr.stats?.ilvl ?? ""; │
│ return { icon: tr.icon, ilvl, name: tr.name }; │
│ } catch { │
│ return null; │
│ } │
│ } │
│ │
│ export function renderComparisonChartHTML( │
│ data: any, │
│ sortBy: SortBy, │
│ comparisonType: "race" | "trinket" = "trinket", │
│ barColor: string = "#666", │
│ ): string { │
│ const baseline = data?.results?.baseline?.dps || 0; │
│ const baselineMax = data?.results?.baseline?.max || 0; │
│ const baselineMin = data?.results?.baseline?.min || 0; │
│ const items: Item[] = []; │
│ for (const [key, v] of Object.entries<any>(data.results || {})) { │
│ if (key === "baseline") continue; │
│ │
│ let label = key.replace(/_/g, " "); │
│ let iconData: any = null; │
│ let percent: number | undefined; │
│ let dpsIncrease: number | undefined; │
│ │
│ if (comparisonType === "race") { │
│ label = utilFormatRace(key); │
│ } │
│ if (comparisonType === "trinket") { │
│ iconData = trinketIconDataFromLoadout(v.loadout); │
│ if (iconData) label = iconData.ilvl ? `${iconData.ilvl}` : iconData.name; │
│ } │
│ percent = baseline > 0 ? ((v.dps - baseline) / baseline) * 100 : undefined; │
│ dpsIncrease = baseline > 0 ? v.dps - baseline : undefined; │
│ │
│ (items as any).push({ │
│ label, │
│ value: v.dps, │
│ min: v.min, │
│ max: v.max, │
│ stdev: v.stdev, │
│ percent, │
│ dpsIncrease, │
│ maxIncrease: baselineMax > 0 ? v.max - baselineMax : undefined, │
│ minIncrease: baselineMin > 0 ? v.min - baselineMin : undefined, │
│ loadout: v.loadout, │
│ iconData, │
│ }); │
│ } │
│ │
│ const effectiveSort: SortBy = │
│ comparisonType === "race" && String(sortBy) === "percent" ? "dps" : sortBy; │
│ sortItems(items, effectiveSort); │
│ const metric = (it: Item) => │
│ String(effectiveSort) === "percent" │
│ ? (it.percent ?? 0) │
│ : String(effectiveSort) === "max" │
│ ? ((it as any).maxIncrease ?? 0) │
│ : String(effectiveSort) === "min" │
│ ? ((it as any).minIncrease ?? 0) │
│ : String(effectiveSort) === "stdev" │
│ ? it.stdev │
│ : ((it as any).dpsIncrease ?? 0); │
│ const max = Math.max(...items.map(metric), 0); │
│ const min = │
│ String(effectiveSort) === "percent" || │
│ String(effectiveSort) === "dps" || │
│ String(effectiveSort) === "max" || │
│ String(effectiveSort) === "min" │
│ ? 0 │
│ : Math.min(...items.map(metric), max); │
│ │
│ const bars = items │
│ .map((it, idx) => { │
│ const mval = metric(it); │
│ const width = calculateBarWidth(mval, max, min, effectiveSort).toFixed(1); │
│ const display = Math.round(mval).toLocaleString(); │
│ const percentStr = │
│ it.percent != null ? ` (+${it.percent.toFixed(1)}%)` : ""; │
│ const dropdownHtml = generateLoadoutDropdown(it.loadout); │
│ const hasDropdown = dropdownHtml && dropdownHtml.length > 0; │
│ const labelContent = │
│ comparisonType === "trinket" && (it as any).iconData │
│ ? `<img src="https://wow.zamimg.com/images/wow/icons/small/${(it as any) │
│ .iconData.icon}.jpg" alt="${(it as any).iconData.name}" class="trinket-icon" title │
│ ="${(it as any).iconData.name}" /><span class="trinket-ilvl">${it.label}</span>` │
│ : `<span class="chart-label">${it.label}</span>`; │
│ let chartDisplay = display; │
│ let tooltip = display; │
│ if (comparisonType === "trinket" && it.percent != null) { │
│ const percentDisplay = `+${it.percent.toFixed(1)}%`; │
│ const dpsIncreaseDisplay = `+${Math.round((it as any).dpsIncrease || 0).to │
│ LocaleString()}`; │
│ if (String(effectiveSort) === "percent") { │
│ chartDisplay = percentDisplay; │
│ tooltip = `${percentDisplay} (${display} DPS)`; │
│ } else if (String(effectiveSort) === "stdev") { │
│ chartDisplay = Math.round(it.stdev).toLocaleString(); │
│ tooltip = `${chartDisplay} StdDev (${display} DPS avg, lower is more con │
│ sistent)`; │
│ } else { │
│ chartDisplay = dpsIncreaseDisplay; │
│ tooltip = │
│ String(effectiveSort) === "dps" │
│ ? `${display} DPS (${dpsIncreaseDisplay} vs baseline)` │
│ : `${display} (Avg: ${display} DPS, ${dpsIncreaseDisplay} vs baselin │
│ e)`; │
│ } │
│ } │
│ return ` │
│ <div class="chart-item-wrapper" data-index="${idx}"> │
│ <div class="chart-item-header"> │
│ <div class="chart-item chart-item-comparison"> │
│ <div class="chart-labels"> │
│ <span class="chart-rank">#${idx + 1}</span> │
│ ${labelContent} │
│ </div> │
│ <div class="chart-bar-container"> │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style="width: ${width}%; background: ${barC │
│ olor};"></div> │
│ </div> │
│ <div class="chart-value" title="${tooltip}">${comparisonType === "tr │
│ inket" && String(effectiveSort) === "percent" ? (it.percent === 0 ? "Baseline" : ` │
│ +${(it.percent || 0).toFixed(1)}%`) : chartDisplay}</div> │
│ </div> │
│ </div> │
│ </div> │
│ ${hasDropdown ? `<div class="chart-dropdown">${dropdownHtml}</div>` : ""} │
│ </div>`; │
│ }) │
│ .join(""); │
│ const titles: Record<string, string> = { │
│ dps: `${comparisonType === "race" ? "Race" : "Trinket"} DPS Rankings (Average) │
│ `, │
│ max: `${comparisonType === "race" ? "Race" : "Trinket"} DPS Rankings (Maximum) │
│ `, │
│ min: `${comparisonType === "race" ? "Race" : "Trinket"} DPS Rankings (Minimum) │
│ `, │
│ stdev: `${comparisonType === "race" ? "Race" : "Trinket"} DPS Consistency Rank │
│ ings (Low StdDev = More Consistent)`, │
│ percent: `${comparisonType === "race" ? "Race" : "Trinket"} Performance Rankin │
│ gs (% Increase)`, │
│ }; │
│ const title = │
│ titles[String(effectiveSort)] || │
│ `${comparisonType === "race" ? "Race" : "Trinket"} DPS Rankings`; │
│ return `<h2 class="card-title-large">${title}</h2><div class="chart-bars">${bars │
│ }</div>`; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ import { calculateBarWidth, sortItems, type │
│ SortBy } from "../sorting"; │
│ import { generateLoadoutDropdown } from "../ │
│ loadout"; │
│ import { formatRace as utilFormatRace, toTit │
│ leCase } from "../../../lib/utils"; │
│ import { getSpecIcon } from "../../../lib/cl │
│ ient-utils"; │
│ │
│ type Item = { │
│ label: string; │
│ value: number; │
│ min: number; │
│ max: number; │
│ stdev: number; │
│ percent?: number; │
│ color?: string; │
│ loadout?: any; │
│ className?: string; │
│ specName?: string; │
│ }; │
│ │
│ export function renderRankingsChartHTML( │
│ data: any, │
│ sortBy: SortBy, │
│ classColors: Record<string, string>, │
│ ): string { │
│ const items: Item[] = []; │
│ for (const [className, classData] of Objec │
│ t.entries<any>( │
│ data.results || {}, │
│ )) { │
│ const colorKey = String(className).toLow │
│ erCase().replace(/\s+/g, "_"); │
│ const barColor = classColors[colorKey] | │
│ | "#666"; │
│ for (const [specName, specData] of Objec │
│ t.entries<any>(classData)) { │
│ items.push({ │
│ label: String(specName) │
│ .replace(/_/g, " ") │
│ .replace(/\b\w/g, (s) => s.toUpper │
│ Case()), │
│ value: specData.dps, │
│ min: specData.min, │
│ max: specData.max, │
│ stdev: specData.stdev, │
│ color: barColor, │
│ loadout: specData.loadout || null, │
│ className: String(className), │
│ specName: String(specName), │
│ }); │
│ } │
│ } │
│ sortItems(items, sortBy); │
│ const metric = (it: Item) => │
│ String(sortBy) === "max" │
│ ? it.max │
│ : String(sortBy) === "min" │
│ ? it.min │
│ : String(sortBy) === "stdev" │
│ ? it.stdev │
│ : it.value; │
│ const max = Math.max(...items.map(metric), │
│ 0); │
│ const min = │
│ String(sortBy) === "dps" || │
│ String(sortBy) === "max" || │
│ String(sortBy) === "min" │
│ ? 0 │
│ : Math.min(...items.map(metric), max); │
│ │
│ const bars = items │
│ .map((it, idx) => { │
│ const mval = metric(it); │
│ const width = calculateBarWidth(mval, │
│ max, min, sortBy).toFixed(1); │
│ const displayRaw = String(sortBy) === │
│ "stdev" ? it.stdev : mval; │
│ const display = Math.round(displayRaw) │
│ .toLocaleString(); │
│ const tooltip = `${it.label}: ${displa │
│ y}`; │
│ │
│ let specIcon = ""; │
│ │
│ if (it.className && it.specName) { │
│ const properClassName = toTitleCase( │
│ it.className); │
│ const properSpecName = toTitleCase(i │
│ t.specName); │
│ │
│ const iconUrl = getSpecIcon(properCl │
│ assName, properSpecName); │
│ if (iconUrl) { │
│ specIcon = `<img src="${iconUrl}" │
│ alt="${properSpecName}" class="spec-icon" st │
│ yle="width: 24px; height: 24px; border-radiu │
│ s: 2px;" />`; │
│ } │
│ } │
│ │
│ const dropdownHtml = generateLoadoutDr │
│ opdown(it.loadout); │
│ const hasDropdown = dropdownHtml && dr │
│ opdownHtml.length > 0; │
│ return ` │
│ <div class="chart-item-wrapper" data-i │
│ ndex="${idx}"> │
│ <div class="chart-item-header" title │
│ ="${tooltip}"> │
│ <div class="chart-item"> │
│ <div class="chart-labels"> │
│ ${specIcon} │
│ </div> │
│ <div class="chart-bar-container" │
│ > │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style │
│ ="width: ${width}%; background: ${it.color | │
│ | "#6aa84f"};"></div> │
│ </div> │
│ <div class="chart-value">${dis │
│ play}</div> │
│ </div> │
│ </div> │
│ </div> │
│ ${hasDropdown ? `<div class="chart-d │
│ ropdown">${dropdownHtml}</div>` : ""} │
│ </div>`; │
│ }) │
│ .join(""); │
│ const titles: Record<string, string> = { │
│ dps: "DPS Rankings (Average)", │
│ max: "DPS Rankings (Maximum)", │
│ min: "DPS Rankings (Minimum)", │
│ stdev: "DPS Consistency Rankings (Low St │
│ dDev = More Consistent)", │
│ }; │
│ const title = titles[String(sortBy)] || "D │
│ PS Rankings"; │
│ return `<h2 class="card-title-large">${tit │
│ le}</h2><div class="chart-bars">${bars}</div │
│ >`; │
│ } │
│ │
│ function trinketIconDataFromLoadout(loadout: │
│ any) { │
│ try { │
│ const items = loadout?.equipment?.items; │
│ const t1 = items?.[12]; │
│ const t2 = items?.[13]; │
│ const tr = t1 && t1.id ? t1 : t2 && t2.i │
│ d ? t2 : null; │
│ if (!tr) return null; │
│ const ilvl = tr.stats?.ilvl ?? ""; │
│ return { icon: tr.icon, ilvl, name: tr.n │
│ ame }; │
│ } catch { │
│ return null; │
│ } │
│ } │
│ │
│ export function renderComparisonChartHTML( │
│ data: any, │
│ sortBy: SortBy, │
│ comparisonType: "race" | "trinket" = "trin │
│ ket", │
│ barColor: string = "#666", │
│ ): string { │
│ const baseline = data?.results?.baseline?. │
│ dps || 0; │
│ const baselineMax = data?.results?.baselin │
│ e?.max || 0; │
│ const baselineMin = data?.results?.baselin │
│ e?.min || 0; │
│ const items: Item[] = []; │
│ for (const [key, v] of Object.entries<any> │
│ (data.results || {})) { │
│ if (key === "baseline") continue; │
│ │
│ let label = key.replace(/_/g, " "); │
│ let iconData: any = null; │
│ let percent: number | undefined; │
│ let dpsIncrease: number | undefined; │
│ │
│ if (comparisonType === "race") { │
│ label = utilFormatRace(key); │
│ } │
│ if (comparisonType === "trinket") { │
│ iconData = trinketIconDataFromLoadout( │
│ v.loadout); │
│ if (iconData) label = iconData.ilvl ? │
│ `${iconData.ilvl}` : iconData.name; │
│ } │
│ percent = baseline > 0 ? ((v.dps - basel │
│ ine) / baseline) * 100 : undefined; │
│ dpsIncrease = baseline > 0 ? v.dps - bas │
│ eline : undefined; │
│ │
│ (items as any).push({ │
│ label, │
│ value: v.dps, │
│ min: v.min, │
│ max: v.max, │
│ stdev: v.stdev, │
│ percent, │
│ dpsIncrease, │
│ maxIncrease: baselineMax > 0 ? v.max - │
│ baselineMax : undefined, │
│ minIncrease: baselineMin > 0 ? v.min - │
│ baselineMin : undefined, │
│ loadout: v.loadout, │
│ iconData, │
│ }); │
│ } │
│ │
│ const effectiveSort: SortBy = │
│ comparisonType === "race" && String(sort │
│ By) === "percent" ? "dps" : sortBy; │
│ sortItems(items, effectiveSort); │
│ const metric = (it: Item) => │
│ String(effectiveSort) === "percent" │
│ ? (it.percent ?? 0) │
│ : String(effectiveSort) === "max" │
│ ? ((it as any).maxIncrease ?? 0) │
│ : String(effectiveSort) === "min" │
│ ? ((it as any).minIncrease ?? 0) │
│ : String(effectiveSort) === "stdev │
│ " │
│ ? it.stdev │
│ : ((it as any).dpsIncrease ?? 0) │
│ ; │
│ const max = Math.max(...items.map(metric), │
│ 0); │
│ const min = │
│ String(effectiveSort) === "percent" || │
│ String(effectiveSort) === "dps" || │
│ String(effectiveSort) === "max" || │
│ String(effectiveSort) === "min" │
│ ? 0 │
│ : Math.min(...items.map(metric), max); │
│ │
│ const bars = items │
│ .map((it, idx) => { │
│ const mval = metric(it); │
│ const width = calculateBarWidth(mval, │
│ max, min, effectiveSort).toFixed(1); │
│ const display = Math.round(mval).toLoc │
│ aleString(); │
│ const percentStr = │
│ it.percent != null ? ` (+${it.percen │
│ t.toFixed(1)}%)` : ""; │
│ const dropdownHtml = generateLoadoutDr │
│ opdown(it.loadout); │
│ const hasDropdown = dropdownHtml && dr │
│ opdownHtml.length > 0; │
│ const labelContent = │
│ comparisonType === "trinket" && (it │
│ as any).iconData │
│ ? `<img src="https://wow.zamimg.co │
│ m/images/wow/icons/small/${(it as any).iconD │
│ ata.icon}.jpg" alt="${(it as any).iconData.n │
│ ame}" class="trinket-icon" title="${(it as a │
│ ny).iconData.name}" /><span class="trinket-i │
│ lvl">${it.label}</span>` │
│ : `<span class="chart-label">${it. │
│ label}</span>`; │
│ let chartDisplay = display; │
│ let tooltip = display; │
│ if (comparisonType === "trinket" && it │
│ .percent != null) { │
│ const percentDisplay = `+${it.percen │
│ t.toFixed(1)}%`; │
│ const dpsIncreaseDisplay = `+${Math. │
│ round((it as any).dpsIncrease || 0).toLocale │
│ String()}`; │
│ if (String(effectiveSort) === "perce │
│ nt") { │
│ chartDisplay = percentDisplay; │
│ tooltip = `${percentDisplay} (${di │
│ splay} DPS)`; │
│ } else if (String(effectiveSort) === │
│ "stdev") { │
│ chartDisplay = Math.round(it.stdev │
│ ).toLocaleString(); │
│ tooltip = `${chartDisplay} StdDev │
│ (${display} DPS avg, lower is more consisten │
│ t)`; │
│ } else { │
│ chartDisplay = dpsIncreaseDisplay; │
│ tooltip = │
│ String(effectiveSort) === "dps" │
│ ? `${display} DPS (${dpsIncrea │
│ seDisplay} vs baseline)` │
│ : `${display} (Avg: ${display} │
│ DPS, ${dpsIncreaseDisplay} vs baseline)`; │
│ } │
│ } │
│ return ` │
│ <div class="chart-item-wrapper" data-i │
│ ndex="${idx}"> │
│ <div class="chart-item-header"> │
│ <div class="chart-item chart-item- │
│ comparison"> │
│ <div class="chart-labels"> │
│ <span class="chart-rank">#${id │
│ x + 1}</span> │
│ ${labelContent} │
│ </div> │
│ <div class="chart-bar-container" │
│ > │
│ <div class="chart-bar-track"> │
│ <div class="chart-bar" style │
│ ="width: ${width}%; background: ${barColor}; │
│ "></div> │
│ </div> │
│ <div class="chart-value" title │
│ ="${tooltip}">${comparisonType === "trinket" │
│ && String(effectiveSort) === "percent" ? (i │
│ t.percent === 0 ? "Baseline" : `+${(it.perce │
│ nt || 0).toFixed(1)}%`) : chartDisplay}</div │
│ > │
│ </div> │
│ </div> │
│ </div> │
│ ${hasDropdown ? `<div class="chart-d │
│ ropdown">${dropdownHtml}</div>` : ""} │
│ </div>`; │
│ }) │
│ .join(""); │
│ const titles: Record<string, string> = { │
│ dps: `${comparisonType === "race" ? "Rac │
│ e" : "Trinket"} DPS Rankings (Average)`, │
│ max: `${comparisonType === "race" ? "Rac │
│ e" : "Trinket"} DPS Rankings (Maximum)`, │
│ min: `${comparisonType === "race" ? "Rac │
│ e" : "Trinket"} DPS Rankings (Minimum)`, │
│ stdev: `${comparisonType === "race" ? "R │
│ ace" : "Trinket"} DPS Consistency Rankings ( │
│ Low StdDev = More Consistent)`, │
│ percent: `${comparisonType === "race" ? │
│ "Race" : "Trinket"} Performance Rankings (% │
│ Increase)`, │
│ }; │
│ const title = │
│ titles[String(effectiveSort)] || │
│ `${comparisonType === "race" ? "Race" : │
│ "Trinket"} DPS Rankings`; │
│ return `<h2 class="card-title-large">${tit │
│ le}</h2><div class="chart-bars">${bars}</div │
│ >`; │
│ } │
└──────────────────────────────────────────────┘