diff --git a/config.toml b/config.toml index 5a709f8..85d1b9f 100644 --- a/config.toml +++ b/config.toml @@ -2,6 +2,7 @@ title = "The Development Seed Contributor Network" author = "Pete Gadomski" description = "An interactive visualization of contributors to Development Seed code and their connections to other repositories" organization_name = "Development Seed" +organization_nickname = "DevSeed" repositories = [ "developmentseed/titiler", "developmentseed/lonboard", diff --git a/public/data/config.json b/public/data/config.json index aeaf1d9..99f1f4d 100644 --- a/public/data/config.json +++ b/public/data/config.json @@ -3,6 +3,7 @@ "author": "Pete Gadomski", "description": "An interactive visualization of contributors to Development Seed code and their connections to other repositories", "organization_name": "Development Seed", + "organization_nickname": "DevSeed", "contributor_padding": 20, "contributors": { "AMSCamacho": "Angela Camacho", diff --git a/python/contributor_network/cli.py b/python/contributor_network/cli.py index 54c951b..3548652 100644 --- a/python/contributor_network/cli.py +++ b/python/contributor_network/cli.py @@ -149,6 +149,7 @@ def build( "author": config.author, "description": config.description, "organization_name": config.organization_name, + "organization_nickname": config.organization_nickname, "contributor_padding": config.contributor_padding, "contributors": config.all_contributors, } diff --git a/python/contributor_network/config.py b/python/contributor_network/config.py index 2d5b7e6..d8070b6 100644 --- a/python/contributor_network/config.py +++ b/python/contributor_network/config.py @@ -21,6 +21,7 @@ class Config(BaseModel): author: Author attribution description: Description shown on the page organization_name: Name of the organization (e.g., "Development Seed") + organization_nickname: Short name used in tooltips (e.g., "DevSeed") repositories: List of GitHub repos to track (format: "owner/repo") contributors: Nested dict of contributor categories, each mapping GitHub username to display name @@ -31,6 +32,7 @@ class Config(BaseModel): author: str description: str organization_name: str + organization_nickname: str = "" repositories: list[str] contributors: dict[ str, dict[str, str] diff --git a/src/chart.ts b/src/chart.ts index 7700013..e430ec3 100644 --- a/src/chart.ts +++ b/src/chart.ts @@ -108,7 +108,8 @@ export const createContributorNetworkVisual = ( container: HTMLElement, contributor_padding: number, masterContributorsList: Record, - displayNameMap: Record + displayNameMap: Record, + orgNickname: string = 'DevSeed', ): ChartFunction => { const PI = Math.PI; const TAU = PI * 2; @@ -887,6 +888,7 @@ export const createContributorNetworkVisual = ( COLOR_REPO, COLOR_OWNER, min, + orgNickname, }; drawTooltipModule( ctx, diff --git a/src/data/prepare.ts b/src/data/prepare.ts index f26c4ce..1f6ec32 100644 --- a/src/data/prepare.ts +++ b/src/data/prepare.ts @@ -187,7 +187,7 @@ export function prepareData( d.archived = d.repo_archived === "true" || d.repo_archived === true; d.totalContributors = +(d.repo_total_contributors ?? 0); - d.devseedContributors = +(d.repo_devseed_contributors ?? 0); + d.orgContributors = +(d.repo_devseed_contributors ?? 0); d.externalContributors = +(d.repo_external_contributors ?? 0); d.communityRatio = +(d.repo_community_ratio ?? 0); @@ -325,6 +325,8 @@ export function prepareData( contributors.find((r) => r.contributor_name === l.contributor_name), ) .filter((c): c is ContributorData => c !== undefined); + d.totalCommits = d.repo_total_commits ? +d.repo_total_commits : undefined; + d.orgCommits = d3.sum(d.links_original, (l) => l.commit_count); }); let owners: OwnerData[] = []; diff --git a/src/main.ts b/src/main.ts index f174521..9a1dfd5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { createContributorNetworkVisual } from "./chart"; interface Config { organization_name?: string; + organization_nickname?: string; contributor_padding?: number; contributors?: Record; title?: string; @@ -18,6 +19,7 @@ if (!configResponse.ok) { const config: Config = await configResponse.json(); const organizationName = config.organization_name || "Development Seed"; +const orgNickname = config.organization_nickname || organizationName; const contributor_padding = config.contributor_padding || 20; const masterContributors: Record = config.contributors || {}; @@ -51,7 +53,8 @@ const contributorNetworkVisual = createContributorNetworkVisual( container, contributor_padding, masterContributors, - displayNameToUsername + displayNameToUsername, + orgNickname, ); contributorNetworkVisual.width(size).height(size); diff --git a/src/render/repoCard.ts b/src/render/repoCard.ts index 00ca810..f804b3c 100644 --- a/src/render/repoCard.ts +++ b/src/render/repoCard.ts @@ -28,9 +28,11 @@ export interface RepoCardData { watchers?: number; languages?: string[]; totalContributors?: number; - devseedContributors?: number; + orgContributors?: number; externalContributors?: number; communityRatio?: number; + totalCommits?: number; + orgCommits?: number; license?: string | null; archived?: boolean; [key: string]: unknown; @@ -279,8 +281,10 @@ export function renderCommunityMetrics( x: number, y: number, SF: number, + orgNickname?: string, ): number { const config = REPO_CARD_CONFIG; + const org = orgNickname ?? 'DevSeed'; if (!data.totalContributors || data.totalContributors === 0) { return y; @@ -297,24 +301,36 @@ export function renderCommunityMetrics( setFont(context, config.valueFontSize * SF, 400, 'normal'); const total = data.totalContributors; - const devseed = data.devseedContributors || 0; + const orgContributors = data.orgContributors || 0; const external = data.externalContributors || 0; renderText( context, - `${total} contributors (${devseed} DevSeed, ${external} community)`, + `${total} contributors (${orgContributors} ${org}, ${external} community)`, x * SF, y * SF, 1.25 * SF, ); - if (devseed === 1 && total > 0) { + if (data.totalCommits && data.totalCommits > 0) { + y += config.valueFontSize * config.lineHeight; + const orgPct = Math.round((data.orgCommits || 0) / data.totalCommits * 100); + renderText( + context, + `${data.totalCommits.toLocaleString()} total commits (${orgPct}% from ${org})`, + x * SF, + y * SF, + 1.25 * SF, + ); + } + + if (orgContributors === 1 && total > 0) { y += config.valueFontSize * config.lineHeight; context.globalAlpha = config.warningOpacity; setFont(context, config.valueFontSize * SF, 400, 'italic'); renderText( context, - '⚠ Single DevSeed maintainer', + `⚠ Single ${org} maintainer`, x * SF, y * SF, 1.25 * SF, diff --git a/src/render/tooltip.ts b/src/render/tooltip.ts index c7ba761..e022472 100644 --- a/src/render/tooltip.ts +++ b/src/render/tooltip.ts @@ -27,6 +27,7 @@ interface TooltipConfig { COLOR_REPO: string; COLOR_OWNER: string; min: (...values: number[]) => number; + orgNickname?: string; } /** @@ -81,7 +82,10 @@ function calculateRepoTooltipHeight( if (data.totalContributors && data.totalContributors > 0) { y += config.sectionSpacing; y += config.valueFontSize * config.lineHeight + 4; - if (data.devseedContributors === 1 && data.totalContributors > 0) { + if (data.totalCommits && data.totalCommits > 0) { + y += config.valueFontSize * config.lineHeight; + } + if (data.orgContributors === 1 && data.totalContributors > 0) { y += config.valueFontSize * config.lineHeight; y += config.valueFontSize * config.lineHeight; } else { @@ -131,7 +135,9 @@ function calculateRepoTooltipWidth( formatDate: (value: Date) => string, formatDateExact: (value: Date) => string, formatDigit: (value: number) => string, + orgNickname?: string, ): number { + const org = orgNickname ?? 'DevSeed'; const config = REPO_CARD_CONFIG; let maxWidth = 0; @@ -191,17 +197,26 @@ function calculateRepoTooltipWidth( if (data.totalContributors && data.totalContributors > 0) { setFont(context, config.valueFontSize * SF, 400, 'normal'); const total = data.totalContributors; - const devseed = data.devseedContributors || 0; + const orgContributors = data.orgContributors || 0; const external = data.externalContributors || 0; width = context.measureText( - `${total} contributors (${devseed} DevSeed, ${external} community)`, + `${total} contributors (${orgContributors} ${org}, ${external} community)`, ).width * 1.25; if (width > maxWidth) maxWidth = width; - if (devseed === 1 && total > 0) { + if (data.totalCommits && data.totalCommits > 0) { + const orgPct = Math.round((data.orgCommits || 0) / data.totalCommits * 100); + width = + context.measureText( + `${data.totalCommits.toLocaleString()} total commits (${orgPct}% from ${org})`, + ).width * 1.25; + if (width > maxWidth) maxWidth = width; + } + + if (orgContributors === 1 && total > 0) { width = - context.measureText('⚠ Single DevSeed maintainer').width * 1.25; + context.measureText(`⚠ Single ${org} maintainer`).width * 1.25; if (width > maxWidth) maxWidth = width; } } @@ -282,7 +297,7 @@ export function drawTooltip( formatDateExact: (value: Date) => string, formatDigit: (value: number) => string, ): void { - const { SF, COLOR_BACKGROUND, COLOR_TEXT, COLOR_CONTRIBUTOR, COLOR_REPO, COLOR_OWNER } = + const { SF, COLOR_BACKGROUND, COLOR_TEXT, COLOR_CONTRIBUTOR, COLOR_REPO, COLOR_OWNER, orgNickname } = config; let line_height = 1.2; @@ -298,7 +313,7 @@ export function drawTooltip( let H: number, W: number; if (d.type === 'contributor') { - H = 100; + H = 125; W = 320; } else if (d.type === 'owner') { H = 155; @@ -313,6 +328,7 @@ export function drawTooltip( formatDate, formatDateExact, formatDigit, + orgNickname, ); } else { H = 116; @@ -365,6 +381,12 @@ export function drawTooltip( text = nodeData.contributor_name ?? nodeData.author_name; let tW = context.measureText(text).width * 1.25; if (tW + 40 * SF > W * SF) W = tW / SF + 40; + const repoCount = (nodeData.links_original as LinkData[] | undefined)?.length ?? 0; + const totalCommits = (nodeData.total_commits as number) || 0; + setFont(context, 14 * SF, 400, 'normal'); + const statsText = `${repoCount} ${repoCount === 1 ? 'repo' : 'repos'} · ${totalCommits.toLocaleString()} commits`; + tW = context.measureText(statsText).width * 1.25; + if (tW + 40 * SF > W * SF) W = tW / SF + 40; } let H_OFFSET = d.y < 0 ? 20 : -H - 20; @@ -415,6 +437,16 @@ export function drawTooltip( setFont(context, font_size * SF, 700, 'normal'); text = nodeData.contributor_name ?? nodeData.author_name; renderText(context, text, xLeft * SF, y * SF, 1.25 * SF); + + y += 26; + const contribRepoCount = (nodeData.links_original as LinkData[] | undefined)?.length ?? 0; + const contribTotalCommits = (nodeData.total_commits as number) || 0; + font_size = 14; + context.globalAlpha = 0.6; + setFont(context, font_size * SF, 400, 'normal'); + text = `${contribRepoCount} ${contribRepoCount === 1 ? 'repo' : 'repos'} · ${contribTotalCommits.toLocaleString()} commits`; + renderText(context, text, xLeft * SF, y * SF, 1.25 * SF); + context.globalAlpha = 1; } else if (d.type === 'owner') { font_size = 22; setFont(context, font_size * SF, 700, 'normal'); @@ -509,7 +541,7 @@ export function drawTooltip( if (nodeData.totalContributors && nodeData.totalContributors > 0) { if (y > statsY) drawSectionDivider(context, y + 4, W, x, SF); } - y = renderCommunityMetrics(context, repoData, xLeft, y, SF); + y = renderCommunityMetrics(context, repoData, xLeft, y, SF, orgNickname); if (nodeData.license) { if (y > statsY) drawSectionDivider(context, y + 4, W, x, SF); diff --git a/src/types.ts b/src/types.ts index 3734590..2bd1487 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,9 +26,11 @@ export interface RepoData { hasDiscussions: boolean; archived: boolean; totalContributors: number; - devseedContributors: number; + orgContributors: number; externalContributors: number; communityRatio: number; + totalCommits?: number; + orgCommits?: number; createdAt: Date; updatedAt: Date; languages: string[]; @@ -46,6 +48,7 @@ export interface RepoData { repo_has_discussions?: string | boolean; repo_archived?: string | boolean; repo_total_contributors?: string; + repo_total_commits?: string; repo_devseed_contributors?: string; repo_external_contributors?: string; repo_community_ratio?: string;