Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/lineage

src/ui/chart-model.ts

160 lines · 4.0 KB · TypeScript
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 }
}