Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152export const ACTIVITY_METRICS = ["pulls", "commits", "repos", "issues"] as const;
export type ActivityMetric = (typeof ACTIVITY_METRICS)[number];
export interface ActivityChartColumn { left: number; width: number; divider: boolean;}
export interface ActivityChartPoint { x: number; y: number; value: number;}
export interface ActivityChartSegment { d: string; rising: boolean;}
export interface ActivityChartLayout { width: number; height: number; columns: ActivityChartColumn[]; points: Record<ActivityMetric, ActivityChartPoint[]>; segments: Record<ActivityMetric, ActivityChartSegment[]>;}
const MIN_WIDTH = 988;const HEIGHT = 256;const PLOT_TOP = 20;const PLOT_BOTTOM = 196;const POINT_GAP = 30;const POINT_INSET = 12;const COLUMN_WEIGHTS = [146, 146, 146, 146, 146, 258];
const normalizedY = (values: number[]): number[] => { const min = Math.min(...values); const max = Math.max(...values); if (min === max) return values.map(() => (PLOT_TOP + PLOT_BOTTOM) / 2); return values.map( (value) => PLOT_BOTTOM - ((value - min) / (max - min)) * (PLOT_BOTTOM - PLOT_TOP) );};
const resolvePointCollisions = ( points: { metric: ActivityMetric; y: number }[]): Map<ActivityMetric, number> => { const sorted = points.toSorted((a, b) => a.y - b.y); const clusters = sorted.map((point) => ({ metrics: [point.metric], sum: point.y })); const topOf = (cluster: { metrics: ActivityMetric[]; sum: number }) => { const span = (cluster.metrics.length - 1) * POINT_GAP; const centered = cluster.sum / cluster.metrics.length; return Math.min(Math.max(centered, PLOT_TOP), PLOT_BOTTOM - span); };
let index = 1; while (index < clusters.length) { const previous = clusters[index - 1]; const current = clusters[index]; if (topOf(previous) + previous.metrics.length * POINT_GAP <= topOf(current)) { index += 1; continue; } clusters.splice(index - 1, 2, { metrics: [...previous.metrics, ...current.metrics], sum: previous.sum + current.sum - previous.metrics.length * current.metrics.length * POINT_GAP }); index = Math.max(index - 1, 1); }
const resolved = new Map<ActivityMetric, number>(); for (const cluster of clusters) { const top = topOf(cluster); for (const [offset, metric] of cluster.metrics.entries()) { resolved.set(metric, top + offset * POINT_GAP); } } return resolved;};
export const layoutActivityChart = ( values: Record<ActivityMetric, number[]>, availableWidth: number): ActivityChartLayout => { const width = Math.max(MIN_WIDTH, availableWidth); const scale = width / MIN_WIDTH; const columns: ActivityChartColumn[] = []; let left = 0; for (const [index, weight] of COLUMN_WEIGHTS.entries()) { const columnWidth = index === COLUMN_WEIGHTS.length - 1 ? width - left : weight * scale; columns.push({ left, width: columnWidth, divider: index > 0 }); left += columnWidth; }
const rawY = Object.fromEntries( ACTIVITY_METRICS.map((metric) => [metric, normalizedY(values[metric])]) ) as Record<ActivityMetric, number[]>; const points = Object.fromEntries( ACTIVITY_METRICS.map((metric) => [metric, [] as ActivityChartPoint[]]) ) as Record<ActivityMetric, ActivityChartPoint[]>;
for (const [monthIndex, column] of columns.entries()) { const resolved = resolvePointCollisions( ACTIVITY_METRICS.map((metric) => ({ metric, y: rawY[metric][monthIndex] })) ); for (const metric of ACTIVITY_METRICS) { points[metric].push({ x: column.left + POINT_INSET, y: resolved.get(metric)!, value: values[metric][monthIndex] }); } }
const segments = Object.fromEntries( ACTIVITY_METRICS.map((metric) => { const metricPoints = points[metric]; const connectors = metricPoints.slice(0, -1).map((point, index) => { const next = metricPoints[index + 1]; const control = (next.x - point.x) / 2; return { d: `M ${point.x} ${point.y} C ${point.x + control} ${point.y}, ${next.x - control} ${next.y}, ${next.x} ${next.y}`, rising: next.value >= point.value }; }); const first = metricPoints[0]; const last = metricPoints.at(-1)!; return [ metric, [ { d: `M 0 ${first.y} L ${first.x} ${first.y}`, rising: connectors[0]?.rising ?? true }, ...connectors, { d: `M ${last.x} ${last.y} L ${width} ${last.y}`, rising: connectors.at(-1)?.rising ?? true } ] ]; }) ) as Record<ActivityMetric, ActivityChartSegment[]>;
return { width, height: HEIGHT, columns, points, segments };};