feat(ui): show repo count in contribution graph summaries

Add "in N repositories" to both the year and all-time graph summary
lines. Year graph counts repos with overlapping activity; all-time
graph uses total project count. OG image includes repo count too.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 17:50:15 +03:00
parent 13db392273
commit 03c816d2d3
2 changed files with 38 additions and 3 deletions

View File

@@ -180,7 +180,10 @@ async fn og_contributions(
.await .await
.map_err(internal)?; .map_err(internal)?;
let png = render_contributions_png(&counts, earliest, today).map_err(|e| ApiError { let projects = state.store.list_projects().await.map_err(internal)?;
let repo_count = projects.len();
let png = render_contributions_png(&counts, earliest, today, repo_count).map_err(|e| ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR, status: StatusCode::INTERNAL_SERVER_ERROR,
message: e, message: e,
})?; })?;
@@ -199,6 +202,7 @@ fn render_contributions_png(
counts: &[DailyCount], counts: &[DailyCount],
from: NaiveDate, from: NaiveDate,
to: NaiveDate, to: NaiveDate,
repo_count: usize,
) -> Result<Vec<u8>, String> { ) -> Result<Vec<u8>, String> {
use std::collections::HashMap; use std::collections::HashMap;
@@ -280,8 +284,13 @@ fn render_contributions_png(
let mut svg = format!( let mut svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{svg_w}" height="{svg_h}" viewBox="0 0 {svg_w} {svg_h}"><rect width="100%" height="100%" fill="{bg}"/>"#, r#"<svg xmlns="http://www.w3.org/2000/svg" width="{svg_w}" height="{svg_h}" viewBox="0 0 {svg_w} {svg_h}"><rect width="100%" height="100%" fill="{bg}"/>"#,
); );
let repo_text = if repo_count > 0 {
format!(" in {repo_count} repositories")
} else {
String::new()
};
svg.push_str(&format!( svg.push_str(&format!(
r##"<text x="{x}" y="18" fill="#ecf0f1" font-size="12" opacity="0.6">{total} contributions since {from}</text>"##, r##"<text x="{x}" y="18" fill="#ecf0f1" font-size="12" opacity="0.6">{total} contributions since {from}{repo_text}</text>"##,
x = year_label_w, x = year_label_w,
)); ));

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { fetchDailyCounts, fetchSources } from '../api/client'; import { fetchDailyCounts, fetchProjects, fetchSources } from '../api/client';
const CELL_SIZE = 12; const CELL_SIZE = 12;
const GAP = 3; const GAP = 3;
@@ -37,6 +37,23 @@ export function ContributionGraph() {
staleTime: 5 * 60_000, staleTime: 5 * 60_000,
}); });
const projectsQ = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
staleTime: 60_000,
});
const repoCount = useMemo(() => {
if (!projectsQ.data) return 0;
const fromMs = from.getTime();
const toMs = to.getTime();
return projectsQ.data.filter((p) => {
const first = p.first_activity ? new Date(p.first_activity).getTime() : Infinity;
const last = p.last_activity ? new Date(p.last_activity).getTime() : 0;
return last >= fromMs && first <= toMs;
}).length;
}, [projectsQ.data]);
const navigate = useNavigate(); const navigate = useNavigate();
const { weeks, monthMarkers, thresholds, totalCount } = useMemo(() => { const { weeks, monthMarkers, thresholds, totalCount } = useMemo(() => {
@@ -89,6 +106,7 @@ export function ContributionGraph() {
<div className="contribution-graph mb-3"> <div className="contribution-graph mb-3">
<p style={{ fontSize: '0.8rem', opacity: 0.6 }}> <p style={{ fontSize: '0.8rem', opacity: 0.6 }}>
{totalCount} contributions in the last year {totalCount} contributions in the last year
{repoCount > 0 && ` in ${repoCount} repositories`}
</p> </p>
<div> <div>
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block"> <svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block">
@@ -155,6 +173,13 @@ export function AllTimeGraph() {
return dates.length > 0 ? new Date(Math.min(...dates.map((d) => d.getTime()))) : null; return dates.length > 0 ? new Date(Math.min(...dates.map((d) => d.getTime()))) : null;
}, [sourcesQ.data]); }, [sourcesQ.data]);
const projectsQ = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
staleTime: 60_000,
});
const repoCount = projectsQ.data?.length ?? 0;
const to = new Date(); const to = new Date();
const from = earliest ?? new Date(to.getFullYear() - 5, 0, 1); const from = earliest ?? new Date(to.getFullYear() - 5, 0, 1);
const fromStr = fmt(from); const fromStr = fmt(from);
@@ -227,6 +252,7 @@ export function AllTimeGraph() {
<div className="contribution-graph mb-4"> <div className="contribution-graph mb-4">
<p style={{ fontSize: '0.8rem', opacity: 0.6 }}> <p style={{ fontSize: '0.8rem', opacity: 0.6 }}>
{totalCount} contributions since {fmt(from)} {totalCount} contributions since {fmt(from)}
{repoCount > 0 && ` in ${repoCount} repositories`}
</p> </p>
<div> <div>
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block"> <svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block">