@anxin233/gitviz 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli.js → cli.cjs} +179 -166
- package/package.json +2 -2
- package/scripts/build-esbuild.js +14 -8
- package/dist/cli/index.js +0 -67
- package/dist/core/analyzer.js +0 -99
- package/dist/core/git-parser.js +0 -53
- package/dist/core/types.js +0 -1
- package/dist/visualizers/html-generator.js +0 -292
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anxin233/gitviz",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "🎨 Beautiful, interactive Git repository visualizations - Transform your Git history into stunning visual stories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"gitviz": "./dist/cli.
|
|
7
|
+
"gitviz": "./dist/cli.cjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "node --loader ts-node/esm src/cli/index.ts",
|
package/scripts/build-esbuild.js
CHANGED
|
@@ -13,13 +13,9 @@ build({
|
|
|
13
13
|
bundle: true,
|
|
14
14
|
platform: 'node',
|
|
15
15
|
target: 'node18',
|
|
16
|
-
format: '
|
|
17
|
-
outfile: join(rootDir, 'dist', 'cli.
|
|
18
|
-
banner: {
|
|
19
|
-
js: '#!/usr/bin/env node'
|
|
20
|
-
},
|
|
16
|
+
format: 'cjs',
|
|
17
|
+
outfile: join(rootDir, 'dist', 'cli.cjs'),
|
|
21
18
|
external: [
|
|
22
|
-
// 不打包这些 node 内置模块
|
|
23
19
|
'fs',
|
|
24
20
|
'path',
|
|
25
21
|
'url',
|
|
@@ -30,12 +26,22 @@ build({
|
|
|
30
26
|
'os',
|
|
31
27
|
'crypto'
|
|
32
28
|
],
|
|
33
|
-
minify: false,
|
|
29
|
+
minify: false,
|
|
34
30
|
sourcemap: false,
|
|
35
31
|
logLevel: 'info'
|
|
36
32
|
}).then(() => {
|
|
33
|
+
// 读取生成的文件
|
|
34
|
+
const outputPath = join(rootDir, 'dist', 'cli.cjs');
|
|
35
|
+
let content = readFileSync(outputPath, 'utf-8');
|
|
36
|
+
|
|
37
|
+
// 检查是否已有 shebang,如果没有才添加
|
|
38
|
+
if (!content.startsWith('#!/usr/bin/env node')) {
|
|
39
|
+
content = '#!/usr/bin/env node\n' + content;
|
|
40
|
+
writeFileSync(outputPath, content, 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
console.log('✅ Build successful!');
|
|
38
|
-
console.log('📁 Output: dist/cli.
|
|
44
|
+
console.log('📁 Output: dist/cli.cjs');
|
|
39
45
|
}).catch((error) => {
|
|
40
46
|
console.error('❌ Build failed:', error);
|
|
41
47
|
process.exit(1);
|
package/dist/cli/index.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { writeFileSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { GitParser } from '../core/git-parser.js';
|
|
8
|
-
import { Analyzer } from '../core/analyzer.js';
|
|
9
|
-
import { generateHTML } from '../visualizers/html-generator.js';
|
|
10
|
-
const program = new Command();
|
|
11
|
-
program
|
|
12
|
-
.name('gitviz')
|
|
13
|
-
.description('🎨 Beautiful, interactive Git repository visualizations')
|
|
14
|
-
.version('1.0.0');
|
|
15
|
-
program
|
|
16
|
-
.command('analyze')
|
|
17
|
-
.description('Analyze a Git repository and generate visualizations')
|
|
18
|
-
.option('-p, --path <path>', 'Path to Git repository', '.')
|
|
19
|
-
.option('-o, --output <file>', 'Output HTML file', 'gitviz-report.html')
|
|
20
|
-
.option('-l, --limit <number>', 'Limit number of commits to analyze', '1000')
|
|
21
|
-
.action(async (options) => {
|
|
22
|
-
const spinner = ora('Initializing GitViz...').start();
|
|
23
|
-
try {
|
|
24
|
-
const parser = new GitParser(options.path);
|
|
25
|
-
spinner.text = 'Checking if directory is a Git repository...';
|
|
26
|
-
const isRepo = await parser.isGitRepository();
|
|
27
|
-
if (!isRepo) {
|
|
28
|
-
spinner.fail(chalk.red('Error: Not a Git repository'));
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
spinner.text = 'Parsing Git commits...';
|
|
32
|
-
const commits = await parser.parseCommits(parseInt(options.limit));
|
|
33
|
-
if (commits.length === 0) {
|
|
34
|
-
spinner.fail(chalk.red('Error: No commits found'));
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
spinner.text = `Analyzing ${commits.length} commits...`;
|
|
38
|
-
const analyzer = new Analyzer();
|
|
39
|
-
const analysis = analyzer.analyze(commits);
|
|
40
|
-
spinner.text = 'Generating visualization data...';
|
|
41
|
-
const vizData = analyzer.generateVisualizationData(analysis);
|
|
42
|
-
spinner.text = 'Creating HTML report...';
|
|
43
|
-
const repoName = options.path === '.' ? 'Current Repository' : options.path;
|
|
44
|
-
const html = generateHTML(vizData, repoName);
|
|
45
|
-
const outputPath = join(process.cwd(), options.output);
|
|
46
|
-
writeFileSync(outputPath, html, 'utf-8');
|
|
47
|
-
spinner.succeed(chalk.green('✨ Visualization generated successfully!'));
|
|
48
|
-
console.log('\n' + chalk.bold('📊 Repository Statistics:'));
|
|
49
|
-
console.log(chalk.cyan(` Total Commits: ${analysis.totalCommits}`));
|
|
50
|
-
console.log(chalk.cyan(` Contributors: ${analysis.totalContributors}`));
|
|
51
|
-
console.log(chalk.cyan(` Files Changed: ${analysis.files.size}`));
|
|
52
|
-
console.log(chalk.cyan(` Date Range: ${analysis.dateRange.start.toLocaleDateString()} - ${analysis.dateRange.end.toLocaleDateString()}`));
|
|
53
|
-
console.log('\n' + chalk.bold(`📁 Output: ${outputPath}`));
|
|
54
|
-
console.log(chalk.gray(`\nOpen the file in your browser to view the interactive visualization.\n`));
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
spinner.fail(chalk.red('Error: ' + error.message));
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
program
|
|
62
|
-
.command('quick')
|
|
63
|
-
.description('Quick analysis of current directory')
|
|
64
|
-
.action(async () => {
|
|
65
|
-
program.parse(['node', 'gitviz', 'analyze']);
|
|
66
|
-
});
|
|
67
|
-
program.parse();
|
package/dist/core/analyzer.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
export class Analyzer {
|
|
2
|
-
analyze(commits) {
|
|
3
|
-
const contributors = new Map();
|
|
4
|
-
const files = new Map();
|
|
5
|
-
for (const commit of commits) {
|
|
6
|
-
const key = commit.email || commit.author;
|
|
7
|
-
if (!contributors.has(key)) {
|
|
8
|
-
contributors.set(key, {
|
|
9
|
-
name: commit.author,
|
|
10
|
-
email: commit.email,
|
|
11
|
-
commits: 0,
|
|
12
|
-
insertions: 0,
|
|
13
|
-
deletions: 0,
|
|
14
|
-
firstCommit: commit.date,
|
|
15
|
-
lastCommit: commit.date
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
const contributor = contributors.get(key);
|
|
19
|
-
contributor.commits++;
|
|
20
|
-
contributor.insertions += commit.insertions;
|
|
21
|
-
contributor.deletions += commit.deletions;
|
|
22
|
-
contributor.lastCommit = commit.date;
|
|
23
|
-
for (const file of commit.files) {
|
|
24
|
-
if (!files.has(file)) {
|
|
25
|
-
files.set(file, {
|
|
26
|
-
path: file,
|
|
27
|
-
changes: 0,
|
|
28
|
-
lastModified: commit.date,
|
|
29
|
-
contributors: new Set()
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
const fileChange = files.get(file);
|
|
33
|
-
fileChange.changes++;
|
|
34
|
-
fileChange.lastModified = commit.date;
|
|
35
|
-
fileChange.contributors.add(commit.author);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const dates = commits.map(c => c.date);
|
|
39
|
-
return {
|
|
40
|
-
commits,
|
|
41
|
-
contributors,
|
|
42
|
-
files,
|
|
43
|
-
totalCommits: commits.length,
|
|
44
|
-
totalContributors: contributors.size,
|
|
45
|
-
dateRange: {
|
|
46
|
-
start: new Date(Math.min(...dates.map(d => d.getTime()))),
|
|
47
|
-
end: new Date(Math.max(...dates.map(d => d.getTime())))
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
generateVisualizationData(analysis) {
|
|
52
|
-
return {
|
|
53
|
-
timeline: this.generateTimelineData(analysis.commits),
|
|
54
|
-
contributors: this.generateContributorData(analysis.contributors),
|
|
55
|
-
heatmap: this.generateHeatmapData(analysis.files)
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
generateTimelineData(commits) {
|
|
59
|
-
const dailyStats = new Map();
|
|
60
|
-
for (const commit of commits) {
|
|
61
|
-
const dateKey = commit.date.toISOString().split('T')[0];
|
|
62
|
-
if (!dailyStats.has(dateKey)) {
|
|
63
|
-
dailyStats.set(dateKey, { commits: 0, insertions: 0, deletions: 0 });
|
|
64
|
-
}
|
|
65
|
-
const stats = dailyStats.get(dateKey);
|
|
66
|
-
stats.commits++;
|
|
67
|
-
stats.insertions += commit.insertions;
|
|
68
|
-
stats.deletions += commit.deletions;
|
|
69
|
-
}
|
|
70
|
-
return Array.from(dailyStats.entries())
|
|
71
|
-
.map(([date, stats]) => ({
|
|
72
|
-
date,
|
|
73
|
-
...stats
|
|
74
|
-
}))
|
|
75
|
-
.sort((a, b) => a.date.localeCompare(b.date));
|
|
76
|
-
}
|
|
77
|
-
generateContributorData(contributors) {
|
|
78
|
-
return Array.from(contributors.values())
|
|
79
|
-
.map(c => ({
|
|
80
|
-
name: c.name,
|
|
81
|
-
commits: c.commits,
|
|
82
|
-
lines: c.insertions + c.deletions
|
|
83
|
-
}))
|
|
84
|
-
.sort((a, b) => b.commits - a.commits)
|
|
85
|
-
.slice(0, 20);
|
|
86
|
-
}
|
|
87
|
-
generateHeatmapData(files) {
|
|
88
|
-
const fileArray = Array.from(files.values());
|
|
89
|
-
const maxChanges = Math.max(...fileArray.map(f => f.changes));
|
|
90
|
-
return fileArray
|
|
91
|
-
.map(f => ({
|
|
92
|
-
file: f.path,
|
|
93
|
-
changes: f.changes,
|
|
94
|
-
heat: f.changes / maxChanges
|
|
95
|
-
}))
|
|
96
|
-
.sort((a, b) => b.changes - a.changes)
|
|
97
|
-
.slice(0, 50);
|
|
98
|
-
}
|
|
99
|
-
}
|
package/dist/core/git-parser.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import simpleGit from 'simple-git';
|
|
2
|
-
export class GitParser {
|
|
3
|
-
git;
|
|
4
|
-
repoPath;
|
|
5
|
-
constructor(repoPath = '.') {
|
|
6
|
-
this.repoPath = repoPath;
|
|
7
|
-
this.git = simpleGit(repoPath);
|
|
8
|
-
}
|
|
9
|
-
async isGitRepository() {
|
|
10
|
-
try {
|
|
11
|
-
await this.git.status();
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
async parseCommits(limit = 1000) {
|
|
19
|
-
const log = await this.git.log({
|
|
20
|
-
maxCount: limit,
|
|
21
|
-
'--numstat': null,
|
|
22
|
-
'--pretty': 'format:%H|%an|%ae|%ai|%s'
|
|
23
|
-
});
|
|
24
|
-
const commits = [];
|
|
25
|
-
for (const commit of log.all) {
|
|
26
|
-
const diffSummary = await this.git.diffSummary([`${commit.hash}^`, commit.hash]);
|
|
27
|
-
commits.push({
|
|
28
|
-
hash: commit.hash,
|
|
29
|
-
author: commit.author_name || 'Unknown',
|
|
30
|
-
email: commit.author_email || '',
|
|
31
|
-
date: new Date(commit.date),
|
|
32
|
-
message: commit.message,
|
|
33
|
-
files: diffSummary.files.map(f => f.file),
|
|
34
|
-
insertions: diffSummary.insertions,
|
|
35
|
-
deletions: diffSummary.deletions
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return commits;
|
|
39
|
-
}
|
|
40
|
-
async getBranchName() {
|
|
41
|
-
const branch = await this.git.branch();
|
|
42
|
-
return branch.current;
|
|
43
|
-
}
|
|
44
|
-
async getRemoteUrl() {
|
|
45
|
-
try {
|
|
46
|
-
const remotes = await this.git.getRemotes(true);
|
|
47
|
-
return remotes[0]?.refs?.fetch || null;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
package/dist/core/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
export function generateHTML(data, repoName) {
|
|
2
|
-
return `<!DOCTYPE html>
|
|
3
|
-
<html lang="en">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="UTF-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
-
<title>GitViz - ${repoName}</title>
|
|
8
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
9
|
-
<style>
|
|
10
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
-
body {
|
|
12
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
13
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
14
|
-
color: #333;
|
|
15
|
-
padding: 2rem;
|
|
16
|
-
}
|
|
17
|
-
.container {
|
|
18
|
-
max-width: 1400px;
|
|
19
|
-
margin: 0 auto;
|
|
20
|
-
background: white;
|
|
21
|
-
border-radius: 20px;
|
|
22
|
-
padding: 3rem;
|
|
23
|
-
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
24
|
-
}
|
|
25
|
-
h1 {
|
|
26
|
-
font-size: 3rem;
|
|
27
|
-
margin-bottom: 0.5rem;
|
|
28
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
29
|
-
-webkit-background-clip: text;
|
|
30
|
-
-webkit-text-fill-color: transparent;
|
|
31
|
-
background-clip: text;
|
|
32
|
-
}
|
|
33
|
-
.subtitle {
|
|
34
|
-
color: #666;
|
|
35
|
-
font-size: 1.2rem;
|
|
36
|
-
margin-bottom: 3rem;
|
|
37
|
-
}
|
|
38
|
-
.section {
|
|
39
|
-
margin-bottom: 4rem;
|
|
40
|
-
}
|
|
41
|
-
.section h2 {
|
|
42
|
-
font-size: 1.8rem;
|
|
43
|
-
margin-bottom: 1.5rem;
|
|
44
|
-
color: #333;
|
|
45
|
-
}
|
|
46
|
-
.chart {
|
|
47
|
-
background: #f8f9fa;
|
|
48
|
-
border-radius: 12px;
|
|
49
|
-
padding: 2rem;
|
|
50
|
-
}
|
|
51
|
-
.bar { fill: #667eea; transition: fill 0.3s; }
|
|
52
|
-
.bar:hover { fill: #764ba2; }
|
|
53
|
-
.axis { font-size: 12px; }
|
|
54
|
-
.axis path, .axis line { stroke: #ddd; }
|
|
55
|
-
.tooltip {
|
|
56
|
-
position: absolute;
|
|
57
|
-
background: rgba(0,0,0,0.8);
|
|
58
|
-
color: white;
|
|
59
|
-
padding: 8px 12px;
|
|
60
|
-
border-radius: 6px;
|
|
61
|
-
font-size: 14px;
|
|
62
|
-
pointer-events: none;
|
|
63
|
-
opacity: 0;
|
|
64
|
-
transition: opacity 0.3s;
|
|
65
|
-
}
|
|
66
|
-
.stats {
|
|
67
|
-
display: grid;
|
|
68
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
69
|
-
gap: 1.5rem;
|
|
70
|
-
margin-bottom: 3rem;
|
|
71
|
-
}
|
|
72
|
-
.stat-card {
|
|
73
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
74
|
-
color: white;
|
|
75
|
-
padding: 1.5rem;
|
|
76
|
-
border-radius: 12px;
|
|
77
|
-
text-align: center;
|
|
78
|
-
}
|
|
79
|
-
.stat-value {
|
|
80
|
-
font-size: 2.5rem;
|
|
81
|
-
font-weight: bold;
|
|
82
|
-
margin-bottom: 0.5rem;
|
|
83
|
-
}
|
|
84
|
-
.stat-label {
|
|
85
|
-
font-size: 1rem;
|
|
86
|
-
opacity: 0.9;
|
|
87
|
-
}
|
|
88
|
-
</style>
|
|
89
|
-
</head>
|
|
90
|
-
<body>
|
|
91
|
-
<div class="container">
|
|
92
|
-
<h1>📊 GitViz</h1>
|
|
93
|
-
<p class="subtitle">Repository: ${repoName}</p>
|
|
94
|
-
|
|
95
|
-
<div class="stats">
|
|
96
|
-
<div class="stat-card">
|
|
97
|
-
<div class="stat-value">${data.timeline.reduce((sum, d) => sum + d.commits, 0)}</div>
|
|
98
|
-
<div class="stat-label">Total Commits</div>
|
|
99
|
-
</div>
|
|
100
|
-
<div class="stat-card">
|
|
101
|
-
<div class="stat-value">${data.contributors.length}</div>
|
|
102
|
-
<div class="stat-label">Contributors</div>
|
|
103
|
-
</div>
|
|
104
|
-
<div class="stat-card">
|
|
105
|
-
<div class="stat-value">${data.heatmap.length}</div>
|
|
106
|
-
<div class="stat-label">Files Changed</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div class="section">
|
|
111
|
-
<h2>📈 Commit Timeline</h2>
|
|
112
|
-
<div class="chart" id="timeline"></div>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div class="section">
|
|
116
|
-
<h2>👥 Top Contributors</h2>
|
|
117
|
-
<div class="chart" id="contributors"></div>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div class="section">
|
|
121
|
-
<h2>🔥 File Change Heatmap</h2>
|
|
122
|
-
<div class="chart" id="heatmap"></div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="tooltip" id="tooltip"></div>
|
|
127
|
-
|
|
128
|
-
<script>
|
|
129
|
-
const data = ${JSON.stringify(data)};
|
|
130
|
-
|
|
131
|
-
// Timeline Chart
|
|
132
|
-
{
|
|
133
|
-
const margin = {top: 20, right: 30, bottom: 40, left: 50};
|
|
134
|
-
const width = 1200 - margin.left - margin.right;
|
|
135
|
-
const height = 300 - margin.top - margin.bottom;
|
|
136
|
-
|
|
137
|
-
const svg = d3.select("#timeline")
|
|
138
|
-
.append("svg")
|
|
139
|
-
.attr("width", width + margin.left + margin.right)
|
|
140
|
-
.attr("height", height + margin.top + margin.bottom)
|
|
141
|
-
.append("g")
|
|
142
|
-
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
143
|
-
|
|
144
|
-
const x = d3.scaleBand()
|
|
145
|
-
.domain(data.timeline.map(d => d.date))
|
|
146
|
-
.range([0, width])
|
|
147
|
-
.padding(0.1);
|
|
148
|
-
|
|
149
|
-
const y = d3.scaleLinear()
|
|
150
|
-
.domain([0, d3.max(data.timeline, d => d.commits)])
|
|
151
|
-
.range([height, 0]);
|
|
152
|
-
|
|
153
|
-
svg.append("g")
|
|
154
|
-
.attr("class", "axis")
|
|
155
|
-
.attr("transform", \`translate(0,\${height})\`)
|
|
156
|
-
.call(d3.axisBottom(x).tickValues(x.domain().filter((d, i) => i % Math.ceil(data.timeline.length / 10) === 0)));
|
|
157
|
-
|
|
158
|
-
svg.append("g")
|
|
159
|
-
.attr("class", "axis")
|
|
160
|
-
.call(d3.axisLeft(y));
|
|
161
|
-
|
|
162
|
-
svg.selectAll(".bar")
|
|
163
|
-
.data(data.timeline)
|
|
164
|
-
.enter()
|
|
165
|
-
.append("rect")
|
|
166
|
-
.attr("class", "bar")
|
|
167
|
-
.attr("x", d => x(d.date))
|
|
168
|
-
.attr("y", d => y(d.commits))
|
|
169
|
-
.attr("width", x.bandwidth())
|
|
170
|
-
.attr("height", d => height - y(d.commits))
|
|
171
|
-
.on("mouseover", function(event, d) {
|
|
172
|
-
d3.select("#tooltip")
|
|
173
|
-
.style("opacity", 1)
|
|
174
|
-
.html(\`<strong>\${d.date}</strong><br/>Commits: \${d.commits}<br/>+\${d.insertions} -\${d.deletions}\`)
|
|
175
|
-
.style("left", (event.pageX + 10) + "px")
|
|
176
|
-
.style("top", (event.pageY - 10) + "px");
|
|
177
|
-
})
|
|
178
|
-
.on("mouseout", function() {
|
|
179
|
-
d3.select("#tooltip").style("opacity", 0);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Contributors Chart
|
|
184
|
-
{
|
|
185
|
-
const margin = {top: 20, right: 30, bottom: 100, left: 50};
|
|
186
|
-
const width = 1200 - margin.left - margin.right;
|
|
187
|
-
const height = 400 - margin.top - margin.bottom;
|
|
188
|
-
|
|
189
|
-
const svg = d3.select("#contributors")
|
|
190
|
-
.append("svg")
|
|
191
|
-
.attr("width", width + margin.left + margin.right)
|
|
192
|
-
.attr("height", height + margin.top + margin.bottom)
|
|
193
|
-
.append("g")
|
|
194
|
-
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
195
|
-
|
|
196
|
-
const x = d3.scaleBand()
|
|
197
|
-
.domain(data.contributors.map(d => d.name))
|
|
198
|
-
.range([0, width])
|
|
199
|
-
.padding(0.2);
|
|
200
|
-
|
|
201
|
-
const y = d3.scaleLinear()
|
|
202
|
-
.domain([0, d3.max(data.contributors, d => d.commits)])
|
|
203
|
-
.range([height, 0]);
|
|
204
|
-
|
|
205
|
-
svg.append("g")
|
|
206
|
-
.attr("class", "axis")
|
|
207
|
-
.attr("transform", \`translate(0,\${height})\`)
|
|
208
|
-
.call(d3.axisBottom(x))
|
|
209
|
-
.selectAll("text")
|
|
210
|
-
.attr("transform", "rotate(-45)")
|
|
211
|
-
.style("text-anchor", "end");
|
|
212
|
-
|
|
213
|
-
svg.append("g")
|
|
214
|
-
.attr("class", "axis")
|
|
215
|
-
.call(d3.axisLeft(y));
|
|
216
|
-
|
|
217
|
-
svg.selectAll(".bar")
|
|
218
|
-
.data(data.contributors)
|
|
219
|
-
.enter()
|
|
220
|
-
.append("rect")
|
|
221
|
-
.attr("class", "bar")
|
|
222
|
-
.attr("x", d => x(d.name))
|
|
223
|
-
.attr("y", d => y(d.commits))
|
|
224
|
-
.attr("width", x.bandwidth())
|
|
225
|
-
.attr("height", d => height - y(d.commits))
|
|
226
|
-
.on("mouseover", function(event, d) {
|
|
227
|
-
d3.select("#tooltip")
|
|
228
|
-
.style("opacity", 1)
|
|
229
|
-
.html(\`<strong>\${d.name}</strong><br/>Commits: \${d.commits}<br/>Lines: \${d.lines}\`)
|
|
230
|
-
.style("left", (event.pageX + 10) + "px")
|
|
231
|
-
.style("top", (event.pageY - 10) + "px");
|
|
232
|
-
})
|
|
233
|
-
.on("mouseout", function() {
|
|
234
|
-
d3.select("#tooltip").style("opacity", 0);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Heatmap
|
|
239
|
-
{
|
|
240
|
-
const margin = {top: 20, right: 30, bottom: 20, left: 300};
|
|
241
|
-
const width = 1200 - margin.left - margin.right;
|
|
242
|
-
const height = Math.min(800, data.heatmap.length * 20);
|
|
243
|
-
|
|
244
|
-
const svg = d3.select("#heatmap")
|
|
245
|
-
.append("svg")
|
|
246
|
-
.attr("width", width + margin.left + margin.right)
|
|
247
|
-
.attr("height", height + margin.top + margin.bottom)
|
|
248
|
-
.append("g")
|
|
249
|
-
.attr("transform", \`translate(\${margin.left},\${margin.top})\`);
|
|
250
|
-
|
|
251
|
-
const y = d3.scaleBand()
|
|
252
|
-
.domain(data.heatmap.map(d => d.file))
|
|
253
|
-
.range([0, height])
|
|
254
|
-
.padding(0.1);
|
|
255
|
-
|
|
256
|
-
const colorScale = d3.scaleSequential(d3.interpolateReds)
|
|
257
|
-
.domain([0, 1]);
|
|
258
|
-
|
|
259
|
-
svg.selectAll("rect")
|
|
260
|
-
.data(data.heatmap)
|
|
261
|
-
.enter()
|
|
262
|
-
.append("rect")
|
|
263
|
-
.attr("y", d => y(d.file))
|
|
264
|
-
.attr("width", width)
|
|
265
|
-
.attr("height", y.bandwidth())
|
|
266
|
-
.attr("fill", d => colorScale(d.heat))
|
|
267
|
-
.on("mouseover", function(event, d) {
|
|
268
|
-
d3.select("#tooltip")
|
|
269
|
-
.style("opacity", 1)
|
|
270
|
-
.html(\`<strong>\${d.file}</strong><br/>Changes: \${d.changes}\`)
|
|
271
|
-
.style("left", (event.pageX + 10) + "px")
|
|
272
|
-
.style("top", (event.pageY - 10) + "px");
|
|
273
|
-
})
|
|
274
|
-
.on("mouseout", function() {
|
|
275
|
-
d3.select("#tooltip").style("opacity", 0);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
svg.selectAll("text")
|
|
279
|
-
.data(data.heatmap)
|
|
280
|
-
.enter()
|
|
281
|
-
.append("text")
|
|
282
|
-
.attr("x", -10)
|
|
283
|
-
.attr("y", d => y(d.file) + y.bandwidth() / 2)
|
|
284
|
-
.attr("dy", "0.35em")
|
|
285
|
-
.attr("text-anchor", "end")
|
|
286
|
-
.attr("font-size", "12px")
|
|
287
|
-
.text(d => d.file);
|
|
288
|
-
}
|
|
289
|
-
</script>
|
|
290
|
-
</body>
|
|
291
|
-
</html>`;
|
|
292
|
-
}
|