export const LANGUAGE_PALETTE = [ '#c8102e', '#f5c400', '#2f6fed', '#0f8a5f', '#9b4dca', '#e86b1f', '#128f8b', '#d23f78', '#4b5d1e', '#6d4c41', '#1d4ed8', '#b45309', ] export type ModelKind = 'source' | 'tests' | 'docs' export type ModelBucket = { language: string kind: string additions: number deletions: number net?: number cumulative?: number } export type ModelPoint = { sha: string at: string message: string htmlUrl?: string additions: number deletions: number net: number cumulative: number buckets: ModelBucket[] } export type DecoratedPoint = ModelPoint & { byLanguage: Map< string, { additions: number; deletions: number; net: number; cumulative: number } > } /** * Recompute running totals from the full series so a commit that does not * touch TypeScript still carries TypeScript's countable lines forward. */ export function decoratePoints( points: ModelPoint[], enabledLanguages: Iterable, enabledKinds: Iterable, ): DecoratedPoint[] { const languages = new Set(enabledLanguages) const kinds = new Set(enabledKinds) const running = new Map() for (const language of languages) running.set(language, 0) return points.map((point) => { const byLanguage = new Map< string, { additions: number; deletions: number; net: number; cumulative: number } >() for (const language of languages) { byLanguage.set(language, { additions: 0, deletions: 0, net: 0, cumulative: running.get(language) ?? 0, }) } let additions = 0 let deletions = 0 for (const bucket of point.buckets) { if (!languages.has(bucket.language) || !kinds.has(bucket.kind)) continue const next = (running.get(bucket.language) ?? 0) + (bucket.additions - bucket.deletions) running.set(bucket.language, next) const current = byLanguage.get(bucket.language) ?? { additions: 0, deletions: 0, net: 0, cumulative: 0, } current.additions += bucket.additions current.deletions += bucket.deletions current.net += bucket.additions - bucket.deletions current.cumulative = next byLanguage.set(bucket.language, current) additions += bucket.additions deletions += bucket.deletions } let cumulative = 0 for (const language of languages) cumulative += running.get(language) ?? 0 return { ...point, additions, deletions, net: additions - deletions, cumulative, byLanguage, } }) } export function filterPointsByRange( points: DecoratedPoint[], from: string, to: string, ): DecoratedPoint[] { return points.filter((point) => { if (from && point.at < from) return false if (to && point.at > to) return false return true }) } export function languageStats( points: DecoratedPoint[], languages: string[], ): Array<{ language: string; lines: number }> { const latest = points[points.length - 1] return [...languages] .map((language) => ({ language, lines: latest?.byLanguage.get(language)?.cumulative ?? 0, })) .sort((left, right) => right.lines - left.lines || left.language.localeCompare(right.language)) } export function colorForLanguage(language: string, languages: string[]): string { const ordered = [...languages].sort((left, right) => left.localeCompare(right)) const index = Math.max(0, ordered.indexOf(language)) return LANGUAGE_PALETTE[index % LANGUAGE_PALETTE.length] ?? LANGUAGE_PALETTE[0] } export function closestIndex(points: Array<{ at: string }>, iso: string): number { if (points.length === 0) return 0 if (!iso) return 0 let best = 0 let bestDelta = Math.abs(Date.parse(points[0]?.at ?? iso) - Date.parse(iso)) for (let index = 1; index < points.length; index += 1) { const delta = Math.abs(Date.parse(points[index]?.at ?? iso) - Date.parse(iso)) if (delta < bestDelta) { best = index bestDelta = delta } } return best } export function clampRange(fromIndex: number, toIndex: number, maxIndex: number) { const last = Math.max(0, maxIndex) let from = Math.min(Math.max(0, fromIndex), last) let to = Math.min(Math.max(0, toIndex), last) if (from > to) [from, to] = [to, from] return { from, to } }