From 37a3f737350a78edad04cab2dcd44570d7f64966 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 18 Dec 2025 13:12:02 -0800 Subject: [PATCH] add chart data utilities and compact score formatting --- src/lib/utils/gw.ts | 175 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/lib/utils/gw.ts b/src/lib/utils/gw.ts index 3fbb0534..af4e1df2 100644 --- a/src/lib/utils/gw.ts +++ b/src/lib/utils/gw.ts @@ -2,6 +2,44 @@ * Guild War (Unite and Fight) utility functions and constants */ +import { + GW_ROUND_LABELS, + type GwRound, + type GwIndividualScore, + type GwCrewScore, + type GwChartDataPoint, + type GwEvent, + type EventScoreSummary +} from '$lib/types/api/gw' + +// ============================================================================ +// Chart Data Types +// ============================================================================ + +/** + * Score data for a single round in a player chart + */ +export interface PlayerRoundScore { + round: GwRound + roundLabel: string + score: number + cumulative: number // Running total +} + +/** + * Data point for crew history chart + */ +export interface HistoryDataPoint { + eventNumber: number + eventLabel: string // "GW #72" + totalScore: number + date: string // For tooltip +} + +// ============================================================================ +// Formatting Utilities +// ============================================================================ + /** * Format a score number with commas for display */ @@ -9,6 +47,29 @@ export function formatScore(score: number): string { return score.toLocaleString() } +/** + * Format a score number in compact form (e.g., 1.5b, 250m, 50k) + * Used for chart axis labels where space is limited + */ +export function formatScoreCompact(score: number): string { + const abs = Math.abs(score) + const sign = score < 0 ? '-' : '' + + if (abs >= 1_000_000_000) { + const value = abs / 1_000_000_000 + return sign + (value % 1 === 0 ? value.toFixed(0) : value.toFixed(1)) + 'b' + } + if (abs >= 1_000_000) { + const value = abs / 1_000_000 + return sign + (value % 1 === 0 ? value.toFixed(0) : value.toFixed(1)) + 'm' + } + if (abs >= 1_000) { + const value = abs / 1_000 + return sign + (value % 1 === 0 ? value.toFixed(0) : value.toFixed(1)) + 'k' + } + return sign + abs.toString() +} + /** * Parse a score string (with commas) back to a number */ @@ -62,3 +123,117 @@ export function getElementColor(elementId: number): string { if (!className) return '#888' return ELEMENT_HEX_COLORS[className] ?? '#888' } + +// ============================================================================ +// Chart Data Transformation Utilities +// ============================================================================ + +/** + * Transform individual scores into chart-ready format + */ +export function toPlayerChartData(scores: GwIndividualScore[]): PlayerRoundScore[] { + const byRound = new Map() + + for (const score of scores) { + byRound.set(score.round, (byRound.get(score.round) ?? 0) + score.score) + } + + let cumulative = 0 + const rounds: GwRound[] = [0, 1, 2, 3, 4, 5] + + return rounds.map((round) => { + const score = byRound.get(round) ?? 0 + cumulative += score + return { + round, + roundLabel: GW_ROUND_LABELS[round], + score, + cumulative + } + }) +} + +/** + * Transform crew scores into chart-ready format for crew battle chart + */ +export function toCrewBattleChartData(crewScores: GwCrewScore[]): GwChartDataPoint[] { + return crewScores + .filter((s) => s.round >= 2) // Only Finals rounds + .sort((a, b) => a.round - b.round) + .map((s) => ({ + round: s.round, + roundLabel: GW_ROUND_LABELS[s.round], + crewScore: s.crewScore, + opponentScore: s.opponentScore, + memberContributions: [] + })) +} + +/** + * Build multi-player chart data from all individual scores + */ +export function toMultiPlayerChartData( + allScores: GwIndividualScore[] +): Map { + const byPlayer = new Map() + + for (const score of allScores) { + const playerId = score.member?.id ?? score.phantom?.id ?? score.playerName + const existing = byPlayer.get(playerId) + + if (existing) { + existing.rawScores.push(score) + } else { + byPlayer.set(playerId, { + name: score.playerName, + rawScores: [score] + }) + } + } + + const result = new Map() + for (const [playerId, { name, rawScores }] of byPlayer) { + result.set(playerId, { + name, + scores: toPlayerChartData(rawScores) + }) + } + + return result +} + +/** + * Transform GW events into history chart data + */ +export function toCrewHistoryChartData( + events: GwEvent[], + formatDate: (date: string) => string +): HistoryDataPoint[] { + return events + .filter((e) => e.crewTotalScore !== undefined && e.crewTotalScore > 0) + .sort((a, b) => a.eventNumber - b.eventNumber) + .map((e) => ({ + eventNumber: e.eventNumber, + eventLabel: `GW #${e.eventNumber}`, + totalScore: e.crewTotalScore ?? 0, + date: formatDate(e.startDate) + })) +} + +/** + * Transform player event scores into history chart data + */ +export function toPlayerHistoryChartData( + eventScores: EventScoreSummary[], + formatDate: (date: string) => string +): HistoryDataPoint[] { + return eventScores + .filter((e) => e.totalScore > 0) + .sort((a, b) => a.gwEvent.eventNumber - b.gwEvent.eventNumber) + .map((e) => ({ + eventNumber: e.gwEvent.eventNumber, + eventLabel: `GW #${e.gwEvent.eventNumber}`, + totalScore: e.totalScore, + date: formatDate(e.gwEvent.startDate) + })) +}