-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Repo Activity Racing Bar #696
Changes from all commits
7aedbc3
278957f
99a681d
ee48dd9
c5e1c87
55d740c
967d3ec
36ec44f
35dd841
2228868
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
.env.production.local | ||
.history | ||
.vscode | ||
.idea | ||
package-lock.json | ||
|
||
# secrets | ||
|
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, | ||
}); | ||
}; | ||
Comment on lines
+115
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
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(); | ||
Comment on lines
+129
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another way to play |
||
}; | ||
|
||
/** | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed there are many directive in this part, I think it would be better if we could remove some of these. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can have Andy fixing this later as a patch. I will merge this feature anyway but you can still improve your work. @andyhuang18 What do you say? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
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); | ||
} | ||
Comment on lines
+187
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Timer should be cleared. |
||
}; | ||
}, []); | ||
|
||
return ( | ||
<div className="hypertrons-crx-border"> | ||
<div ref={divEL} style={{ width: '100%', height }}></div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default RacingBar; |
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, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// @ts-ignore
is likexxx: any
, we should fix them all one day :D