-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Repo Activity Racing Bar (#696)
* feat: add activity racing bar feat: initial implementation of activity racing bar(need to be modified) feat: initial implementation of activity racing bar(need to be modified v1) feat: initial implementation of activity racing bar(need to be modified v2) feat: implementation of activity racing bar feat: change the style of chart and button * feat: change chart width and put button to corner * feat: support for i18n * feat: update .gitignore for .idea files * style: better style makes it look better * chore: let and const has their own scope * chore: better way to play and replay the racing bar * chore: update messages * feat: variable bar number according to contributors number * fix: fix performance issue --------- Co-authored-by: Lam Tang <[email protected]>
- Loading branch information
1 parent
746d58b
commit 2e5abf4
Showing
9 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
.env.production.local | ||
.history | ||
.vscode | ||
.idea | ||
package-lock.json | ||
|
||
# secrets | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
src/pages/ContentScripts/features/repo-activity-racing-bar/RacingBar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import React, { useEffect, useRef } from 'react'; | ||
import * as echarts from 'echarts'; | ||
import type { EChartsOption, EChartsType } from 'echarts'; | ||
|
||
interface RacingBarProps { | ||
repoName: string; | ||
data: any; | ||
} | ||
|
||
// TODO generate color from user avatar | ||
const colorMap = new Map(); | ||
|
||
const updateFrequency = 3000; | ||
|
||
const option: EChartsOption = { | ||
grid: { | ||
top: 10, | ||
bottom: 30, | ||
left: 150, | ||
right: 50, | ||
}, | ||
xAxis: { | ||
max: 'dataMax', | ||
}, | ||
yAxis: { | ||
type: 'category', | ||
inverse: true, | ||
max: 10, | ||
axisLabel: { | ||
show: true, | ||
fontSize: 14, | ||
formatter: function (value: string) { | ||
if (!value || value.endsWith('[bot]')) return value; | ||
return `${value} {avatar${value.replaceAll('-', '')}|}`; | ||
}, | ||
}, | ||
axisTick: { | ||
show: false, | ||
}, | ||
animationDuration: 0, | ||
animationDurationUpdate: 200, | ||
}, | ||
series: [ | ||
{ | ||
realtimeSort: true, | ||
seriesLayoutBy: 'column', | ||
type: 'bar', | ||
itemStyle: { | ||
color: function (params: any) { | ||
const githubId = params.value[0]; | ||
if (colorMap.has(githubId)) { | ||
return colorMap.get(githubId); | ||
} else { | ||
const randomColor = | ||
'#' + Math.floor(Math.random() * 16777215).toString(16); | ||
colorMap.set(githubId, randomColor); | ||
return randomColor; | ||
} | ||
}, | ||
}, | ||
data: undefined, | ||
encode: { | ||
x: 1, | ||
y: 0, | ||
}, | ||
label: { | ||
show: true, | ||
precision: 1, | ||
position: 'right', | ||
valueAnimation: true, | ||
fontFamily: 'monospace', | ||
}, | ||
}, | ||
], | ||
// Disable init animation. | ||
animationDuration: 0, | ||
animationDurationUpdate: updateFrequency, | ||
animationEasing: 'linear', | ||
animationEasingUpdate: 'linear', | ||
graphic: { | ||
elements: [ | ||
{ | ||
type: 'text', | ||
right: 60, | ||
bottom: 60, | ||
style: { | ||
text: undefined, | ||
font: 'bolder 60px monospace', | ||
fill: 'rgba(100, 100, 100, 0.25)', | ||
}, | ||
z: 100, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
const updateMonth = (instance: EChartsType, data: any, month: string) => { | ||
const rich: any = {}; | ||
data[month].forEach((item: any[]) => { | ||
// rich name cannot contain special characters such as '-' | ||
rich[`avatar${item[0].replaceAll('-', '')}`] = { | ||
backgroundColor: { | ||
image: `https://avatars.githubusercontent.com/${item[0]}?s=48&v=4`, | ||
}, | ||
height: 20, | ||
}; | ||
}); | ||
// @ts-ignore | ||
option.yAxis.axisLabel.rich = rich; | ||
// @ts-ignore | ||
option.series[0].data = data[month]; | ||
// @ts-ignore | ||
option.graphic.elements[0].style.text = month; | ||
|
||
// it seems that hidden bars are also rendered, so when each setOption merge more data into the chart, | ||
// the fps goes down. So we use notMerge to avoid merging data. But this disables the xAxis animation. | ||
// Hope we can find a better solution. | ||
instance.setOption(option, { | ||
notMerge: true, | ||
}); | ||
}; | ||
|
||
let timer: NodeJS.Timeout; | ||
|
||
const play = (instance: EChartsType, data: any) => { | ||
const months = Object.keys(data); | ||
let i = 0; | ||
|
||
const playNext = () => { | ||
updateMonth(instance, data, months[i]); | ||
i++; | ||
if (i < months.length) { | ||
timer = setTimeout(playNext, updateFrequency); | ||
} | ||
}; | ||
|
||
playNext(); | ||
}; | ||
|
||
/** | ||
* Count the number of unique contributors in the data | ||
*/ | ||
const countLongTermContributors = (data: any) => { | ||
const contributors = new Map<string, number>(); | ||
Object.keys(data).forEach((month) => { | ||
data[month].forEach((item: any[]) => { | ||
if (contributors.has(item[0])) { | ||
contributors.set(item[0], contributors.get(item[0])! + 1); | ||
} else { | ||
contributors.set(item[0], 0); | ||
} | ||
}); | ||
}); | ||
let count = 0; | ||
contributors.forEach((value) => { | ||
// only count contributors who have contributed more than 3 months | ||
if (value >= 3) { | ||
count++; | ||
} | ||
}); | ||
return count; | ||
}; | ||
|
||
const RacingBar = ({ data }: RacingBarProps): JSX.Element => { | ||
const divEL = useRef<HTMLDivElement>(null); | ||
|
||
let height = 300; | ||
const longTermContributorsCount = countLongTermContributors(data); | ||
if (longTermContributorsCount >= 20) { | ||
// @ts-ignore | ||
option.yAxis.max = 20; | ||
height = 600; | ||
} | ||
|
||
useEffect(() => { | ||
if (!divEL.current) return; | ||
|
||
const chartDOM = divEL.current; | ||
const instance = echarts.init(chartDOM); | ||
|
||
play(instance, data); | ||
|
||
return () => { | ||
if (!instance.isDisposed()) { | ||
instance.dispose(); | ||
} | ||
// clear timer if user replay the chart before it finishes | ||
if (timer) { | ||
clearTimeout(timer); | ||
} | ||
}; | ||
}, []); | ||
|
||
return ( | ||
<div className="hypertrons-crx-border"> | ||
<div ref={divEL} style={{ width: '100%', height }}></div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default RacingBar; |
59 changes: 59 additions & 0 deletions
59
src/pages/ContentScripts/features/repo-activity-racing-bar/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React from 'react'; | ||
import { render, Container } from 'react-dom'; | ||
import $ from 'jquery'; | ||
|
||
import features from '../../../../feature-manager'; | ||
import isPerceptor from '../../../../helpers/is-perceptor'; | ||
import { getRepoName } from '../../../../helpers/get-repo-info'; | ||
import { getActivityDetails } from '../../../../api/repo'; | ||
import View from './view'; | ||
import DataNotFound from '../repo-networks/DataNotFound'; | ||
import * as pageDetect from 'github-url-detection'; | ||
|
||
const featureId = features.getFeatureID(import.meta.url); | ||
let repoName: string; | ||
let repoActivityDetails: any; | ||
|
||
const getData = async () => { | ||
repoActivityDetails = await getActivityDetails(repoName); | ||
}; | ||
|
||
const renderTo = (container: Container) => { | ||
if (!repoActivityDetails) { | ||
render(<DataNotFound />, container); | ||
return; | ||
} | ||
render( | ||
<View currentRepo={repoName} repoActivityDetails={repoActivityDetails} />, | ||
container | ||
); | ||
}; | ||
|
||
const init = async (): Promise<void> => { | ||
repoName = getRepoName(); | ||
await getData(); | ||
const container = document.createElement('div'); | ||
container.id = featureId; | ||
renderTo(container); | ||
const parentElement = document.getElementById('hypercrx-perceptor-layout'); | ||
if (parentElement) { | ||
parentElement.append(container); | ||
} | ||
}; | ||
|
||
const restore = async () => { | ||
// Clicking another repo link in one repo will trigger a turbo:visit, | ||
// so in a restoration visit we should be careful of the current repo. | ||
if (repoName !== getRepoName()) { | ||
repoName = getRepoName(); | ||
} | ||
// rerender the chart or it will be empty | ||
renderTo($(`#${featureId}`)[0]); | ||
}; | ||
|
||
features.add(featureId, { | ||
asLongAs: [isPerceptor], | ||
awaitDomReady: false, | ||
init, | ||
restore, | ||
}); |
Oops, something went wrong.