', 'use origin git repository')
455 | .parse(process.argv)
456 |
457 | // 程序启动
458 | async function run() {
459 | let gitCloneTmpFolder
460 | let repoPath
461 |
462 | if (program.git) {
463 | try {
464 | gitCloneTmpFolder = (await gitClone(program.git)) + '/'
465 | // 临时文件夹中,只有一个文件夹,是克隆下来的 git 仓库
466 | REPO_NAME = fs.readdirSync(gitCloneTmpFolder)[0]
467 | repoPath = gitCloneTmpFolder + REPO_NAME + '/'
468 | CD_COMMAND = `cd ${repoPath} && `
469 | } catch (err) {
470 | console.log('git clone error', err)
471 | return
472 | }
473 | } else {
474 | repoPath = CURRENT_PATH + '/'
475 | }
476 |
477 | console.time('count')
478 | console.log('analyzing...May be it take some times')
479 |
480 | Promise.all([
481 | countProject(
482 | repoPath,
483 | REPO_NAME,
484 | ['*']
485 | ),
486 | countGitCommit(),
487 | collectAuthorCommitMsg(repoPath),
488 | collectTime(repoPath),
489 | ]).then(data => {
490 | // 词云与commit数量 只保留前20个用户
491 | const showAuthor = data[1].slice(0, 20).map(d => d.author)
492 | const wordCloudData = {}
493 |
494 | showAuthor.concat(['all']).forEach(author => {
495 | wordCloudData[author] = data[2][author]
496 | })
497 | let summary = {
498 | codeData: data[0],
499 | commitData: data[1].slice(0, 20),
500 | wordCloudData,
501 | commitTime: data[3]
502 | }
503 |
504 | let json = 'window._source = ' + JSON.stringify(summary, null, 2)
505 | let file = '_source.js'
506 | let targetPath = CURRENT_PATH + '/commit-analyze/'
507 |
508 | // 判断是否存在,存在则删除
509 | if (fs.existsSync(targetPath)) {
510 | rmdir(targetPath)
511 | }
512 |
513 | // 复制 html 等文件
514 | copyFolder(path.resolve(__dirname + '/../build'), targetPath)
515 |
516 | // 删除克隆仓库的文件夹
517 | if (gitCloneTmpFolder) {
518 | rmdir(gitCloneTmpFolder)
519 | }
520 |
521 | // 写入统计文件
522 | fs.writeFile(targetPath + file, json, 'utf-8', (err, data) => {
523 | if (err) {
524 | throw new Error('write file error', err)
525 | }
526 |
527 | console.timeEnd('count')
528 | console.log('succesful!')
529 | // 自动打开
530 | opn(targetPath + 'index.html')
531 | })
532 | }).catch(error => {
533 | console.error('analyze error', error.toString())
534 | })
535 | }
536 |
537 | run()
538 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "visualize-commit",
3 | "version": "1.1.0",
4 | "description": "",
5 | "main": "index.js",
6 | "bin": {
7 | "vsz-commit": "./bin/index.js"
8 | },
9 | "author": "mojingzhi",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/jingzhiMo/visualize-commit.git"
13 | },
14 | "keywords": [
15 | "git",
16 | "data visualize",
17 | "echarts"
18 | ],
19 | "license": "MIT",
20 | "dependencies": {
21 | "commander": "^6.0.0",
22 | "fs-extra": "^8.1.0",
23 | "lodash": "^4.17.20",
24 | "nodejieba": "^2.4.1",
25 | "open": "^7.0.3"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": "react-app"
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | },
48 | "homepage": "./",
49 | "devDependencies": {
50 | "antd": "^3.20.7",
51 | "classnames": "^2.2.6",
52 | "cz-conventional-changelog": "3.0.2",
53 | "echarts": "^4.2.1",
54 | "echarts-wordcloud": "^1.1.3",
55 | "react": "^16.13.1",
56 | "react-dom": "^16.13.1",
57 | "react-scripts": "^3.0.1"
58 | },
59 | "config": {
60 | "commitizen": {
61 | "path": "./node_modules/cz-conventional-changelog"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/public/_source.js:
--------------------------------------------------------------------------------
1 | // TODO 该文件需要通过命令生成数据
2 | window._source = {}
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jingzhiMo/visualize-commit/f32d0ff94dbb7c563a1c9719cfaf94db68647ccf/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 | visualize-commit
11 |
12 |
13 |
14 |
15 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useMemo } from 'react'
2 | import logo from './logo.svg'
3 | import './App.css'
4 | import Tree from './component/tree.jsx'
5 | import Echarts from './component/echarts.jsx'
6 | import AuthorFile from './component/author-file.jsx'
7 | import AuthorWordCloud from './component/author-wordcloud.jsx'
8 | import CommitPane from './component/commit-pane.jsx'
9 | import CommitTimePane from './component/commit-time'
10 | import { pie } from './service/echarts-pie'
11 | import treeContext from './context/tree-context'
12 |
13 | const { codeData, commitData, wordCloudData, commitTime } = window._source
14 |
15 | /**
16 | * @desc 深度优先查找文件
17 | * @param {Number} id 文件对应的id
18 | * @param {String} type 文件/文件夹
19 | * @param {Object} treeData 需要查找的树状数据
20 | */
21 | function depthFindFile (id, type, treeData) {
22 | // 找到当前的节点数据
23 | if (id === treeData.id) return treeData
24 |
25 | // 选中节点的数据
26 | let nodeData = null
27 |
28 | // 查找文件夹
29 | if (type === 'folder') {
30 | for (let i = 0; i < treeData.children.length; i++) {
31 | nodeData = depthFindFile(id, type, treeData.children[i])
32 |
33 | if (nodeData) break
34 | }
35 |
36 | return nodeData
37 | }
38 |
39 | // 查找文件
40 | // 文件所在当前文件夹内
41 | if (treeData.file.length && id >= treeData.file[0].id) {
42 | return treeData.file.find(item => item.id === id)
43 | }
44 |
45 | // 文件不在当前文件夹
46 | for (let i = 0; i < treeData.children.length; i++) {
47 | nodeData = depthFindFile(id, type, treeData.children[i])
48 |
49 | if (nodeData) break
50 | }
51 |
52 | return nodeData
53 | }
54 |
55 | /**
56 | * @desc 提取当前文件夹的简要数据
57 | * @param {Object} detailData 当前文件夹的详细数据
58 | */
59 | function extractSummary (detailData) {
60 | let key = ['code', 'contribution', 'file', 'id', 'line', 'name', 'path', 'type', 'children']
61 | let summary = {}
62 |
63 | key.forEach(k => {
64 | summary[k] = detailData[k]
65 | })
66 |
67 | return summary
68 | }
69 |
70 | /**
71 | * @desc 提取代码贡献数据
72 | * @param {Object} sourceData 树状图的源数据
73 | */
74 | function extractContribution (sourceData) {
75 | let contribution = sourceData.contribution.slice(0).sort((a, b) => b.line - a.line)
76 | return {
77 | data: contribution.map(item => {
78 | return {
79 | value: item.line,
80 | name: item.author
81 | }
82 | }).sort((a, b) => b.value - a.value).slice(0, 20),
83 | legendData: contribution.map(item => item.author)
84 | }
85 | }
86 |
87 | /**
88 | * @desc 提取代码文件类型数据
89 | * @param {Object} sourceData 树状图的源数据
90 | */
91 | function extractFileType (sourceData) {
92 | let code = sourceData.code
93 | let data = []
94 |
95 | // 文件节点没有对应的代码
96 | if (!code) {
97 | return {
98 | data: [{
99 | name: sourceData.type,
100 | value: sourceData.line
101 | }],
102 | legendData: [sourceData.type]
103 | }
104 | }
105 |
106 | for (let key in code) {
107 | data.push({
108 | name: key,
109 | value: code[key]
110 | })
111 | }
112 |
113 | data.sort((a, b) => b.value - a.value)
114 |
115 | return {
116 | data,
117 | legendData: data.map(item => item.name)
118 | }
119 | }
120 |
121 | function App() {
122 | const [selectNodeId, setSelectNodeId] = useState(0)
123 | const [treeData, setTreeData] = useState(extractSummary(depthFindFile(0, 'folder', codeData)))
124 |
125 | // 修改节点数据
126 | const selectNode = useCallback((id, type) => {
127 | setSelectNodeId(id)
128 | setTreeData(extractSummary(depthFindFile(id, type, codeData)))
129 | }, [])
130 | const contextValue = {
131 | selectNodeId,
132 | selectNode
133 | }
134 |
135 | const codeContribution = useMemo(() => {
136 | return pie(extractContribution(treeData), {
137 | title: '代码贡献占比'
138 | })
139 | }, [treeData])
140 | const fileContribution = useMemo(() => {
141 | return pie(extractFileType(treeData), {
142 | title: '文件数量占比'
143 | })
144 | }, [treeData])
145 |
146 | return
147 |
156 |
157 |
158 |
159 | 代码统计概览
160 |
161 |
162 |
163 |
文件路径:{treeData.path}
164 |
该文件/文件夹代码行数为:{treeData.line}
165 |
166 |
167 |
168 |
172 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | }
184 |
185 | export default App;
186 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/asset/iconfont-2019-07-04.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jingzhiMo/visualize-commit/f32d0ff94dbb7c563a1c9719cfaf94db68647ccf/src/asset/iconfont-2019-07-04.woff
--------------------------------------------------------------------------------
/src/component/author-file.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Select from 'antd/es/select'
3 | import 'antd/dist/antd.css'
4 | import Echarts from './echarts.jsx'
5 | import { pie } from '../service/echarts-pie'
6 |
7 | const { Option } = Select
8 |
9 | /**
10 | * @desc 合并两个用户贡献代码到到第一个用户
11 | * @param {Object} target 合并目标用户 { author: { json: xx, js: xx } }
12 | * @param {Object} source 合并源用户
13 | */
14 | function mergeAuthor (target, source) {
15 | for (let name in source) {
16 | let targetAuthor = target[name]
17 | let sourceAuthor = source[name]
18 |
19 | if (targetAuthor) {
20 | for (let fileType in sourceAuthor) {
21 | targetAuthor[fileType] = (targetAuthor[fileType] || 0) + sourceAuthor[fileType]
22 | }
23 | } else {
24 | target[name] = source[name]
25 | }
26 | }
27 |
28 | return target
29 | }
30 |
31 | /**
32 | * @desc 该节点下,每个作者贡献的不同类型
33 | */
34 | function extractAuthorFile (sourceData) {
35 | let author = {} // e.g { authorName: { vue: 100, json: 50 }}
36 |
37 | // 当前数据是只针对一个文件
38 | if (!sourceData.file && sourceData.contribution) {
39 | let fileType = sourceData.type
40 |
41 | return sourceData.contribution
42 | .reduce((base, item) => {
43 | base[item.author] = {}
44 | base[item.author][fileType] = item.line
45 |
46 | return base
47 | }, author)
48 | }
49 | // 遍历当前节点的文件类型
50 | for (let file of (sourceData.file || [])) {
51 | let fileType = file.type
52 |
53 | for (let contribution of file.contribution) {
54 | let fileLine = {}
55 | const { line, author: name } = contribution
56 |
57 | fileLine[fileType] = line // e.g { vue: 100 }
58 |
59 | if (author[name]) {
60 | author[name][fileType] = (author[name][fileType] || 0) + line
61 | } else {
62 | author[name] = fileLine
63 | }
64 | }
65 | }
66 |
67 |
68 | if (!sourceData.children || !sourceData.children.length) return author
69 |
70 | // 有对应的子节点
71 | return sourceData.children.reduce((base, child) => {
72 | const childAuthor = extractAuthorFile(child)
73 |
74 | // 合并子节点的数据
75 | return mergeAuthor(base, childAuthor)
76 | }, author)
77 | }
78 |
79 | function genAuthor (allAuthorData, authorName) {
80 | let authorData
81 |
82 | // 没有指定用户名,则取第一个
83 | if (!authorName) {
84 | authorName = Object.keys(allAuthorData)[0]
85 | }
86 |
87 | authorData = allAuthorData[authorName]
88 |
89 | let fileList = authorData ? Object.keys(authorData) : []
90 | let data = fileList.map(type => {
91 | return {
92 | name: type,
93 | value: authorData[type]
94 | }
95 | }).sort((a, b) => b.value - a.value)
96 | return {
97 | title: `${authorName || ''} 贡献文件类型`,
98 | chartData: {
99 | data,
100 | legendData: data.map(item => item.name)
101 | }
102 | }
103 | }
104 |
105 | function AuthorFile (props) {
106 | const showAuthor = props.data.contribution
107 | .sort((a, b) => b.line - a.line)
108 | .slice(0, 20)
109 | .map(item => item.author)
110 |
111 | const allAuthorData = extractAuthorFile(props.data)
112 | const [authorData, setAuthorData] = useState(genAuthor(allAuthorData, showAuthor[0]))
113 | const [selectAuthor, setSelectAuthor] = useState(showAuthor[0])
114 |
115 | function updateSelect (value) {
116 | setSelectAuthor(value)
117 | setAuthorData(genAuthor(allAuthorData, value))
118 | }
119 |
120 | return
121 |
122 | 不同用户对应贡献的代码详情
123 |
124 |
135 |
138 |
139 | }
140 |
141 | export default AuthorFile
142 |
--------------------------------------------------------------------------------
/src/component/author-wordcloud.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Select from 'antd/es/select'
3 | import Echarts from './echarts.jsx'
4 | import { wordcloud } from '../service/echarts-wordcloud'
5 | import { authorMap } from '../const'
6 |
7 | const { Option } = Select
8 |
9 | function AuthorWordCloud ({ data }) {
10 | const author = [
11 | 'all',
12 | ...Object.keys(data).filter(name => name !== 'all')
13 | ]
14 | const [selectAuthor, setSelectAuthor] = useState(author[0]) // 默认选中所有人
15 |
16 | const updateSelect = value => {
17 | setSelectAuthor(value)
18 | }
19 | return
20 |
21 | 不同用户 commit 信息文案分析
22 |
23 |
34 |
35 |
36 | }
37 |
38 | export default AuthorWordCloud
39 |
--------------------------------------------------------------------------------
/src/component/commit-pane.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { bar } from '../service/echarts-bar.js'
3 | import Echarts from './echarts.jsx'
4 |
5 | function commitPane ({commit: commitData, line: lineData}) {
6 | let chartData
7 |
8 | // 对贡献 commit 进行排序
9 | commitData = commitData.slice(0).sort((a, b) => b.commit - a.commit).map(item => {
10 | let author = lineData.filter(ld => ld.author === item.author)[0]
11 |
12 | if (!author) return null
13 |
14 | return {
15 | ...item,
16 | average: parseInt(author.line / item.commit).toFixed(2)
17 | }
18 | }).filter(item => !!item)
19 |
20 | chartData = bar(commitData)
21 |
22 | return
23 |
24 | 不同用户对应贡献的 commit 详情
25 |
26 |
30 |
31 | }
32 |
33 | export default commitPane
34 |
--------------------------------------------------------------------------------
/src/component/commit-time.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Select from 'antd/es/select'
3 | import { commitTimeBar } from '../service/echarts-bar.js'
4 | import Echarts from './echarts.jsx'
5 | import { authorMap } from '../const'
6 |
7 | const { Option } = Select
8 |
9 | export default function CommitTime(props) {
10 | const { data } = props
11 | const [selectAuthor, setSelectAuthor] = useState('all')
12 |
13 | const selectData = data.filter(item => item.author === selectAuthor)
14 | const chartData = commitTimeBar(selectData.length ? selectData[0].time : [])
15 |
16 | return (
17 |
18 |
19 | 一周每天代码详情
20 |
21 |
32 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/component/echarts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import echart from 'echarts/lib/echarts'
4 | import 'echarts/lib/component/tooltip'
5 | import 'echarts/lib/component/title'
6 | import 'echarts/lib/component/legendScroll'
7 | import 'echarts/lib/component/markLine'
8 | import 'echarts/lib/chart/pie'
9 | import 'echarts/lib/chart/bar'
10 |
11 | // 需要监听 onresize 的方法
12 | let instanceList = []
13 |
14 | function addInstance (instance) {
15 | instanceList.push(instance)
16 | }
17 |
18 | function removeInstance (instance) {
19 | instanceList = instanceList.filter(item => item.id !== instance.id)
20 | }
21 |
22 | let timer
23 | let handler = () => {
24 | clearTimeout(timer)
25 | timer = setTimeout(() => {
26 | instanceList.forEach(instance => (instance.resize()))
27 | }, 300)
28 | }
29 | window.addEventListener('resize', handler)
30 |
31 | function Echarts (props) {
32 | let instance
33 |
34 | useEffect(() => {
35 | return () => {
36 | if (instance) {
37 | removeInstance(instance)
38 | }
39 | }
40 | })
41 |
42 | const initRef = (element) => {
43 | if (!element) return
44 |
45 | instance = echart.init(element)
46 | instance.setOption(props.chartData)
47 | addInstance(instance)
48 | }
49 |
50 | return
51 |
echart container
52 |
53 | }
54 |
55 | export default Echarts
56 |
--------------------------------------------------------------------------------
/src/component/tree.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react'
2 | import cx from 'classnames'
3 | import treeContext from '../context/tree-context'
4 |
5 | function Tree(props) {
6 | const { treeData, isFolder } = props
7 | const currentNodeId = treeData.id
8 | const { selectNodeId, selectNode } = useContext(treeContext)
9 | const [collapsed, setCollapsed] = useState(
10 | // 根目录默认打开
11 | currentNodeId === 0
12 | ? false
13 | : (props.collapsed || true)
14 | )
15 |
16 | const toggleCollapse = ev => {
17 | setCollapsed(!collapsed)
18 | ev.stopPropagation()
19 | }
20 |
21 | let children // 当前文件夹的子文件夹
22 | let leaf // 当前文件夹的文件,叶子结点
23 |
24 | // 当前文件夹折叠
25 | if (collapsed) {
26 | children = []
27 | leaf = []
28 | } else {
29 | // 当前文件夹展开
30 | children = (treeData.children || []).map((item, idx) => {
31 | return
36 | })
37 | leaf = (treeData.file || []).map((item, idx) => {
38 | let data = {
39 | ...item
40 | }
41 |
42 | return
47 | })
48 | }
49 |
50 | return
51 |
selectNode(currentNodeId, isFolder ? 'folder' : 'file')}
54 | >
55 | {
56 | isFolder &&
57 |
64 | }
65 |
74 | {treeData.name}
75 |
76 |
77 | {children}
78 | {leaf}
79 |
80 | }
81 |
82 |
83 | export default Tree
84 |
--------------------------------------------------------------------------------
/src/const/index.js:
--------------------------------------------------------------------------------
1 | // 不需要统计的作者
2 | exports.BLACK_LIST = [
3 | 'dependabot[bot]'
4 | ]
5 | // 作者名称的中文映射
6 | exports.authorMap = {
7 | all: '所有人'
8 | }
9 |
--------------------------------------------------------------------------------
/src/context/tree-context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const treeContext = createContext({
4 | selectNodeId: 0,
5 | selectNode: () => {}
6 | })
7 | treeContext.displayName = 'treeContext'
8 |
9 | export default treeContext
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'iconfont';
3 | src: url('./asset/iconfont-2019-07-04.woff') format('woff');
4 | }
5 |
6 |
7 | .t-icon {
8 | display: inline-block;
9 | font-family: "iconfont" !important;
10 | font-style: normal;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | line-height: 1;
14 | }
15 | .t-icon:before {
16 | display: inline-block;
17 | }
18 | .t-icon-right-arrow:before {
19 | content: "\e643";
20 | }
21 | body {
22 | margin: 0;
23 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
24 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
25 | sans-serif;
26 | -webkit-font-smoothing: antialiased;
27 | -moz-osx-font-smoothing: grayscale;
28 | }
29 |
30 | code {
31 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
32 | monospace;
33 | }
34 |
35 | html, body, #root {
36 | height: 100%;
37 | }
38 | body {
39 | flex: 1;
40 | }
41 | .tree {
42 | display: flex;
43 | height: 100%;
44 | overflow: hidden;
45 | }
46 | .tree-aside {
47 | flex: 0 0 300px;
48 | padding-bottom: 100px;
49 | background-color: #21252b;
50 | overflow-y: auto;
51 | }
52 | .tree-content {
53 | flex: 1 0 auto;
54 | padding: 0 24px;
55 | overflow-y: auto;
56 | }
57 | @keyframes round {
58 | from {
59 | transform: rotate(0);
60 | }
61 | to {
62 | transform: rotate(180deg);
63 | }
64 | }
65 | .t-logo {
66 | width: 100px;
67 | height: 100px;
68 | }
69 | .t-logo.loading {
70 | animation: 1.5s round linear infinite;
71 | }
72 | .t-line {
73 | display: flex;
74 | }
75 |
76 | /*树状节点*/
77 | .t-node {
78 | display: flex;
79 | flex-direction: column;
80 | }
81 | .t-node > .t-node {
82 | padding-left: 16px;
83 | }
84 | .t-arrow {
85 | display: inline-block;
86 | padding: 4px 2px 4px;
87 | cursor: pointer;
88 | color: #9da5b4;
89 | font-size: 14px;
90 | transition: .15s ease transform;
91 | transform: rotate(0);
92 | }
93 | .t-arrow.rotate {
94 | transform: rotate(90deg);
95 | }
96 | .t-name {
97 | padding-left: 2px;
98 | flex: 1 0 auto;
99 | font-size: 14px;
100 | color: #9da5b4;
101 | }
102 | .t-name.file {
103 | padding-left: 22px;
104 | }
105 | .t-name.active {
106 | background-color: #464a50;
107 | }
108 | .t-name:hover {
109 | color: #fff;
110 | cursor: pointer;
111 | }
112 | .t-text {
113 | display: flex;
114 | flex: 1 0 auto;
115 | align-items: center;
116 | /*padding-left: 4px;*/
117 | line-height: 22px;
118 | }
119 | .t-children {
120 | padding-left: 16px;
121 | }
122 | /*代码行数的显示*/
123 | .t-code-line {
124 | color: #606266;
125 | }
126 |
127 | /* 图表相关 */
128 | .echarts-container {
129 | height: 400px;
130 | padding-bottom: 40px;
131 | }
132 |
133 | .vsz-code-summary {
134 | display: flex;
135 | }
136 | .vsz-code-summary__echart {
137 | flex: 1 0 50%;
138 | }
139 |
140 |
141 | /* 标题相关*/
142 | .vsz-title {
143 | padding: 26px 0 20px;
144 | font-size: 15px;
145 | font-weight: 700;
146 | color: #5097e9;
147 | text-align: left;
148 | }
149 | .vsz-title span {
150 | display: inline-block;
151 | border-left: 4px solid #5097e9;
152 | padding-left: 4px;
153 | line-height: 1;
154 | vertical-align: middle;
155 | }
156 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App.jsx';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/service/color.js:
--------------------------------------------------------------------------------
1 | export default [
2 | '#5097e9',
3 | '#00bcd4',
4 | '#ffa726',
5 | '#e57373',
6 | '#8d6e63',
7 | '#9575cd',
8 | '#ff8a65',
9 | '#81c784',
10 | '#ce93d8',
11 | '#90a4ae',
12 | '#9dc6f5',
13 | '#80deea',
14 | '#ffcc80',
15 | '#ef9a9a',
16 | '#bcaaa4',
17 | '#d1c4e9',
18 | '#ffccbc',
19 | '#c8e6c9',
20 | '#e1bee7',
21 | '#cfd8dc'
22 | ]
23 |
--------------------------------------------------------------------------------
/src/service/echarts-bar.js:
--------------------------------------------------------------------------------
1 | import { groupBy } from 'lodash'
2 | import color from './color'
3 |
4 | export const bar = (data) => {
5 | let legendData = []
6 | let commitData = []
7 | let averageData = []
8 |
9 | data.forEach(item => {
10 | legendData.push(item.author)
11 | commitData.push(item.commit)
12 | averageData.push(item.average)
13 | })
14 |
15 | console.log('commit data', commitData)
16 | return {
17 | title: {
18 | text: 'commit 贡献',
19 | x: 'center',
20 | top: 0
21 | },
22 | color,
23 | legend: {
24 | type: 'scroll',
25 | data: ['commit 贡献数量', 'commit 平均贡献行数'],
26 | width: '80%',
27 | bottom: 0
28 | },
29 | tooltip : {
30 | trigger: 'item',
31 | showDelay: 20,
32 | formatter (param) {
33 | let tip
34 |
35 | // 平均贡献行数
36 | if (param.seriesIndex === 1) {
37 | tip = ' commit 平均贡献行数'
38 | } else {
39 | tip = '贡献 commit 数量'
40 | }
41 | return `${param.name}
${tip}:${param.data}`
42 | }
43 | },
44 | xAxis: {
45 | type: 'category',
46 | data: legendData,
47 | axisTick: {
48 | show: false
49 | },
50 | axisLine: {
51 | show: false,
52 | lineStyle: {
53 | color: '#d0d3da'
54 | }
55 | },
56 | axisLabel: {
57 | color: '#909399'
58 | },
59 | splitLine: {
60 | lineStyle: {
61 | color: '#909939',
62 | type: 'solid'
63 | }
64 | }
65 | },
66 | yAxis: [
67 | {
68 | name: 'commit 贡献数量',
69 | type: 'value',
70 | axisLabel: {
71 | color: '#909399'
72 | },
73 | axisLine: {
74 | show: false,
75 | lineStyle: {
76 | color: '#d0d3da'
77 | }
78 | },
79 | axisTick: {
80 | show: false
81 | },
82 | splitLine: {
83 | lineStyle: {
84 | color: '#ebeef5'
85 | }
86 | }
87 | },
88 | {
89 | name: 'commit平均贡献行数',
90 | type: 'value',
91 | axisLabel: {
92 | color: '#909399'
93 | },
94 | axisLine: {
95 | show: false,
96 | lineStyle: {
97 | color: '#d0d3da'
98 | }
99 | },
100 | axisTick: {
101 | show: false
102 | },
103 | splitLine: {
104 | show: false,
105 | lineStyle: {
106 | color: '#ebeef5'
107 | }
108 | }
109 | }
110 | ],
111 | series : [
112 | {
113 | name: 'commit 贡献数量',
114 | data: commitData,
115 | type: 'bar',
116 | barMaxWidth: '48px',
117 | barMinHeight: 4
118 | },
119 | {
120 | name: 'commit 平均贡献行数',
121 | data: averageData,
122 | type: 'bar',
123 | barMaxWidth: '48px',
124 | barMinHeight: 4,
125 | yAxisIndex: 1
126 | }
127 | ]
128 | }
129 | }
130 |
131 | export const commitTimeBar = data => {
132 | if (!data.length) return
133 |
134 | const week = groupBy(
135 | // 先转为毫秒数
136 | data.map(time => Number(time + '000')),
137 | time => new Date(time).getDay(),
138 | )
139 | let weekData = [0, 0, 0, 0, 0, 0, 0].map((item, index) => week[index] ? week[index].length : item)
140 |
141 | weekData = [...weekData.slice(1), weekData[0]]
142 | return {
143 | title: {
144 | text: '一周每天贡献',
145 | x: 'center',
146 | top: 0
147 | },
148 | color,
149 | legend: {
150 | type: 'scroll',
151 | data: ['commit 贡献数量'],
152 | width: '80%',
153 | bottom: 0
154 | },
155 | tooltip: {
156 | trigger: 'item',
157 | showDelay: 20,
158 | },
159 | xAxis: {
160 | type: 'category',
161 | data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
162 | axisTick: {
163 | show: false
164 | },
165 | axisLine: {
166 | show: false,
167 | lineStyle: {
168 | color: '#d0d3da'
169 | }
170 | },
171 | axisLabel: {
172 | color: '#909399'
173 | },
174 | splitLine: {
175 | lineStyle: {
176 | color: '#909939',
177 | type: 'solid'
178 | }
179 | }
180 | },
181 | yAxis: {
182 | name: 'commit 贡献数量',
183 | type: 'value',
184 | axisLabel: {
185 | color: '#909399'
186 | },
187 | axisLine: {
188 | show: false,
189 | lineStyle: {
190 | color: '#d0d3da'
191 | }
192 | },
193 | axisTick: {
194 | show: false
195 | },
196 | splitLine: {
197 | lineStyle: {
198 | color: '#ebeef5'
199 | }
200 | }
201 | },
202 | series: [
203 | {
204 | name: 'commit 贡献数量',
205 | data: weekData,
206 | type: 'bar',
207 | barMaxWidth: '48px',
208 | barMinHeight: 4
209 | },
210 | ]
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/service/echarts-commit-time.js:
--------------------------------------------------------------------------------
1 | import color from './color'
2 | export const bar = (data) => {
3 | let legendData = []
4 | let commitData = []
5 | let averageData = []
6 |
7 | data.forEach(item => {
8 | legendData.push(item.author)
9 | commitData.push(item.commit)
10 | averageData.push(item.average)
11 | })
12 |
13 | return {
14 | title: {
15 | text: '一周每天贡献',
16 | x: 'center',
17 | top: 0
18 | },
19 | color,
20 | legend: {
21 | type: 'scroll',
22 | data: ['commit 贡献数量'],
23 | width: '80%',
24 | bottom: 0
25 | },
26 | tooltip: {
27 | trigger: 'item',
28 | showDelay: 20,
29 | formatter(param) {
30 | let tip
31 |
32 | // 平均贡献行数
33 | if (param.seriesIndex === 1) {
34 | tip = ' commit 平均贡献行数'
35 | } else {
36 | tip = '贡献 commit 数量'
37 | }
38 | return `${param.name}
${tip}:${param.data}`
39 | }
40 | },
41 | xAxis: {
42 | type: 'category',
43 | data: legendData,
44 | axisTick: {
45 | show: false
46 | },
47 | axisLine: {
48 | show: false,
49 | lineStyle: {
50 | color: '#d0d3da'
51 | }
52 | },
53 | axisLabel: {
54 | color: '#909399'
55 | },
56 | splitLine: {
57 | lineStyle: {
58 | color: '#909939',
59 | type: 'solid'
60 | }
61 | }
62 | },
63 | yAxis: [
64 | {
65 | name: 'commit 贡献数量',
66 | type: 'value',
67 | axisLabel: {
68 | color: '#909399'
69 | },
70 | axisLine: {
71 | show: false,
72 | lineStyle: {
73 | color: '#d0d3da'
74 | }
75 | },
76 | axisTick: {
77 | show: false
78 | },
79 | splitLine: {
80 | lineStyle: {
81 | color: '#ebeef5'
82 | }
83 | }
84 | },
85 | {
86 | name: 'commit平均贡献行数',
87 | type: 'value',
88 | axisLabel: {
89 | color: '#909399'
90 | },
91 | axisLine: {
92 | show: false,
93 | lineStyle: {
94 | color: '#d0d3da'
95 | }
96 | },
97 | axisTick: {
98 | show: false
99 | },
100 | splitLine: {
101 | show: false,
102 | lineStyle: {
103 | color: '#ebeef5'
104 | }
105 | }
106 | }
107 | ],
108 | series: [
109 | {
110 | name: 'commit 贡献数量',
111 | data: commitData,
112 | type: 'bar',
113 | barMaxWidth: '48px',
114 | barMinHeight: 4
115 | },
116 | {
117 | name: 'commit 平均贡献行数',
118 | data: averageData,
119 | type: 'bar',
120 | barMaxWidth: '48px',
121 | barMinHeight: 4,
122 | yAxisIndex: 1
123 | }
124 | ]
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/service/echarts-pie.js:
--------------------------------------------------------------------------------
1 | export const pie = (data, option = {}) => {
2 | return {
3 | title: {
4 | text: option.title,
5 | x:'center',
6 | top: 0
7 | },
8 | color: [
9 | '#5097e9',
10 | '#00bcd4',
11 | '#ffa726',
12 | '#e57373',
13 | '#8d6e63',
14 | '#9575cd',
15 | '#ff8a65',
16 | '#81c784',
17 | '#ce93d8',
18 | '#90a4ae',
19 | '#9dc6f5',
20 | '#80deea',
21 | '#ffcc80',
22 | '#ef9a9a',
23 | '#bcaaa4',
24 | '#d1c4e9',
25 | '#ffccbc',
26 | '#c8e6c9',
27 | '#e1bee7',
28 | '#cfd8dc'
29 | ],
30 | tooltip : {
31 | trigger: 'item',
32 | formatter: "{a}
{b} : {c} ({d}%)"
33 | },
34 | legend: {
35 | type: 'scroll',
36 | data: data.legendData,
37 | width: '80%',
38 | bottom: 0
39 | },
40 | series : [
41 | {
42 | name: option.title,
43 | type: 'pie',
44 | radius : '80%',
45 | center: ['50%', '50%'],
46 | label: {
47 | normal: {
48 | show: false
49 | },
50 | emphasis: {
51 | show: false
52 | }
53 | },
54 | labelLine: {
55 | normal: {
56 | show: false
57 | }
58 | },
59 | data: data.data
60 | }
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/service/echarts-wordcloud.js:
--------------------------------------------------------------------------------
1 | import colorSet from './color'
2 | import 'echarts-wordcloud'
3 |
4 | export const wordcloud = (data, chartOption) => {
5 | let option = {
6 | title: {
7 | text: '',
8 | left: 'center'
9 | },
10 | tooltip: {
11 | padding: 10,
12 | trigger: 'item',
13 | textStyle: {
14 | color: '#fff'
15 | },
16 | axisPointer: {
17 | type: 'line',
18 | lineStyle: {
19 | color: 'rgba(33, 35, 41, 0.1)'
20 | }
21 | },
22 | formatter: '{b} 出现次数:{c} 次'
23 | },
24 | visualMap: {
25 | show: false,
26 | type: 'continuous'
27 | },
28 | series: [{
29 | type: 'wordCloud',
30 | shape: 'circle',
31 | left: 'center',
32 | top: 'center',
33 | width: '100%',
34 | height: '80%',
35 | right: null,
36 | bottom: null,
37 | sizeRange: [20, 80],
38 | rotationRange: [0, 0],
39 | rotationStep: 45,
40 | gridSize: 14,
41 | drawOutOfBound: false,
42 |
43 | textStyle: {
44 | normal: {
45 | fontWeight: 'normal',
46 | color: function (params) {
47 | return colorSet[params.dataIndex % colorSet.length]
48 | }
49 | }
50 | },
51 | data: data
52 | }]
53 | }
54 |
55 | return option
56 | }
57 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------