Skip to content

Commit 9c24c01

Browse files
authored
Merge pull request #878 from hyemimi/newviz
[feat]: 기여자 - 기여자, 파일 - 파일, 기여자-파일 관계 시각화를 위한 유틸리티
2 parents 6efa7eb + 81f992f commit 9c24c01

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import * as d3 from "d3";
2+
3+
import type { ClusterNode } from "types";
4+
5+
export interface NetworkNode extends d3.SimulationNodeDatum {
6+
id: string;
7+
type: "contributor" | "file";
8+
radius: number;
9+
weight: number;
10+
connections: number;
11+
}
12+
13+
export interface NetworkLink extends d3.SimulationLinkDatum<NetworkNode> {
14+
source: NetworkNode;
15+
target: NetworkNode;
16+
weight: number;
17+
sourceType: "contributor" | "file";
18+
targetType: "contributor" | "file";
19+
}
20+
21+
export interface NetworkGraphData {
22+
nodes: NetworkNode[];
23+
links: NetworkLink[];
24+
colorScale: d3.ScaleOrdinal<string, string>;
25+
}
26+
27+
export function processNetworkGraphData(
28+
data: ClusterNode[],
29+
nodeType: "contributor" | "file" | "both"
30+
): NetworkGraphData | null {
31+
if (!data || data.length === 0) return null;
32+
33+
const contributorStats = new Map<
34+
string,
35+
{
36+
commitCount: number;
37+
totalChangeCount: number;
38+
files: Set<string>;
39+
lastActivityDate: Date;
40+
}
41+
>();
42+
43+
const fileStats = new Map<
44+
string,
45+
{
46+
commitCount: number;
47+
totalChangeCount: number;
48+
contributors: Set<string>;
49+
lastModifiedDate: Date;
50+
}
51+
>();
52+
53+
const contributorFileRelations = new Map<string, Map<string, number>>();
54+
55+
data.forEach((cluster) => {
56+
cluster.commitNodeList.forEach((commitNode) => {
57+
const { commit } = commitNode;
58+
const author = commit.author.names[0] || "Unknown";
59+
const commitDate = new Date(commit.authorDate);
60+
61+
if (!contributorStats.has(author)) {
62+
contributorStats.set(author, {
63+
commitCount: 0,
64+
totalChangeCount: 0,
65+
files: new Set(),
66+
lastActivityDate: commitDate,
67+
});
68+
}
69+
70+
const contributorStat = contributorStats.get(author)!;
71+
contributorStat.commitCount += 1;
72+
contributorStat.lastActivityDate =
73+
commitDate > contributorStat.lastActivityDate ? commitDate : contributorStat.lastActivityDate;
74+
75+
if (!contributorFileRelations.has(author)) {
76+
contributorFileRelations.set(author, new Map());
77+
}
78+
79+
const files = commit.diffStatistics.files || {};
80+
Object.entries(files).forEach(([fileName, stats]) => {
81+
if (!fileStats.has(fileName)) {
82+
fileStats.set(fileName, {
83+
commitCount: 0,
84+
totalChangeCount: 0,
85+
contributors: new Set(),
86+
lastModifiedDate: commitDate,
87+
});
88+
}
89+
90+
const fileStat = fileStats.get(fileName)!;
91+
fileStat.commitCount += 1;
92+
fileStat.totalChangeCount += stats.insertions + stats.deletions;
93+
fileStat.contributors.add(author);
94+
fileStat.lastModifiedDate = commitDate > fileStat.lastModifiedDate ? commitDate : fileStat.lastModifiedDate;
95+
96+
contributorStat.totalChangeCount += stats.insertions + stats.deletions;
97+
contributorStat.files.add(fileName);
98+
99+
const authorFileMap = contributorFileRelations.get(author)!;
100+
authorFileMap.set(fileName, (authorFileMap.get(fileName) || 0) + 1);
101+
});
102+
});
103+
});
104+
105+
const nodes: NetworkNode[] = [];
106+
const nodeMap = new Map<string, NetworkNode>();
107+
108+
if (nodeType === "contributor" || nodeType === "both") {
109+
const topContributors = Array.from(contributorStats.entries())
110+
.sort(([, a], [, b]) => b.commitCount - a.commitCount)
111+
.slice(0, 20);
112+
113+
topContributors.forEach(([author, stats]) => {
114+
const weight = stats.commitCount / Math.max(...Array.from(contributorStats.values()).map((s) => s.commitCount));
115+
const radius = Math.max(8, Math.min(25, weight * 20 + 8));
116+
const connections = stats.files.size;
117+
118+
const node: NetworkNode = {
119+
id: author,
120+
type: "contributor",
121+
radius,
122+
weight,
123+
connections,
124+
};
125+
126+
nodes.push(node);
127+
nodeMap.set(author, node);
128+
});
129+
}
130+
131+
if (nodeType === "file" || nodeType === "both") {
132+
const topFiles = Array.from(fileStats.entries())
133+
.sort(([, a], [, b]) => b.commitCount - a.commitCount)
134+
.slice(0, 30);
135+
136+
topFiles.forEach(([fileName, stats]) => {
137+
const shortFileName = fileName.split("/").pop() || fileName;
138+
const weight = stats.commitCount / Math.max(...Array.from(fileStats.values()).map((s) => s.commitCount));
139+
const radius = Math.max(6, Math.min(20, weight * 15 + 6));
140+
const connections = stats.contributors.size;
141+
142+
const node: NetworkNode = {
143+
id: shortFileName,
144+
type: "file",
145+
radius,
146+
weight,
147+
connections,
148+
};
149+
150+
nodes.push(node);
151+
nodeMap.set(shortFileName, node);
152+
});
153+
}
154+
155+
const links: NetworkLink[] = [];
156+
157+
if (nodeType === "both") {
158+
contributorFileRelations.forEach((fileMap, author) => {
159+
if (!nodeMap.has(author)) return;
160+
161+
fileMap.forEach((strength, fileName) => {
162+
const shortFileName = fileName.split("/").pop() || fileName;
163+
if (!nodeMap.has(shortFileName)) return;
164+
165+
const maxStrength = Math.max(...Array.from(fileMap.values()));
166+
const normalizedWeight = strength / maxStrength;
167+
168+
const sourceNode = nodeMap.get(author)!;
169+
const targetNode = nodeMap.get(shortFileName)!;
170+
links.push({
171+
source: sourceNode,
172+
target: targetNode,
173+
weight: normalizedWeight,
174+
sourceType: "contributor",
175+
targetType: "file",
176+
});
177+
});
178+
});
179+
} else if (nodeType === "contributor") {
180+
const contributors = Array.from(contributorStats.keys());
181+
for (let i = 0; i < contributors.length; i += 1) {
182+
for (let j = i + 1; j < contributors.length; j += 1) {
183+
const author1 = contributors[i];
184+
const author2 = contributors[j];
185+
186+
if (nodeMap.has(author1) && nodeMap.has(author2)) {
187+
const files1 = contributorStats.get(author1)!.files;
188+
const files2 = contributorStats.get(author2)!.files;
189+
190+
const commonFiles = new Set(Array.from(files1).filter((file) => files2.has(file)));
191+
192+
if (commonFiles.size > 0) {
193+
const maxCommonFiles = Math.max(
194+
...contributors.map((c) => {
195+
const otherFiles = contributorStats.get(c)!.files;
196+
return new Set(Array.from(files1).filter((file) => otherFiles.has(file))).size;
197+
})
198+
);
199+
200+
const weight = commonFiles.size / maxCommonFiles;
201+
202+
if (weight > 0.1) {
203+
const sourceNode = nodeMap.get(author1)!;
204+
const targetNode = nodeMap.get(author2)!;
205+
links.push({
206+
source: sourceNode,
207+
target: targetNode,
208+
weight,
209+
sourceType: "contributor",
210+
targetType: "contributor",
211+
});
212+
}
213+
}
214+
}
215+
}
216+
}
217+
} else if (nodeType === "file") {
218+
const files = Array.from(fileStats.keys());
219+
for (let i = 0; i < files.length; i += 1) {
220+
for (let j = i + 1; j < files.length; j += 1) {
221+
const file1 = files[i];
222+
const file2 = files[j];
223+
224+
const shortFileName1 = file1.split("/").pop() || file1;
225+
const shortFileName2 = file2.split("/").pop() || file2;
226+
227+
if (nodeMap.has(shortFileName1) && nodeMap.has(shortFileName2)) {
228+
const contributors1 = fileStats.get(file1)!.contributors;
229+
const contributors2 = fileStats.get(file2)!.contributors;
230+
231+
const commonContributors = new Set(Array.from(contributors1).filter((c) => contributors2.has(c)));
232+
233+
if (commonContributors.size > 0) {
234+
const maxCommonContributors = Math.max(
235+
...files.map((f) => {
236+
const otherContributors = fileStats.get(f)!.contributors;
237+
return new Set(Array.from(contributors1).filter((c) => otherContributors.has(c))).size;
238+
})
239+
);
240+
241+
const weight = commonContributors.size / maxCommonContributors;
242+
243+
if (weight > 0.1) {
244+
const sourceNode = nodeMap.get(shortFileName1)!;
245+
const targetNode = nodeMap.get(shortFileName2)!;
246+
links.push({
247+
source: sourceNode,
248+
target: targetNode,
249+
weight,
250+
sourceType: "file",
251+
targetType: "file",
252+
});
253+
}
254+
}
255+
}
256+
}
257+
}
258+
}
259+
const colorScale = d3.scaleOrdinal<string>().domain(["contributor", "file"]).range(["#4a9eff", "#39d353"]);
260+
261+
return {
262+
nodes,
263+
links,
264+
colorScale,
265+
};
266+
}

0 commit comments

Comments
 (0)