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<string>,
enabledKinds: Iterable<string>,
): DecoratedPoint[] {
const languages = new Set(enabledLanguages)
const kinds = new Set(enabledKinds)
const running = new Map<string, number>()
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 }
}