forked from hypertrons/hypertrons-crx
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Fiveneves/fiveneves
feat: Add Community OpenRank Detail Network
- Loading branch information
Showing
8 changed files
with
447 additions
and
2 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
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
258 changes: 258 additions & 0 deletions
258
src/pages/ContentScripts/features/community-openrank-network/Network.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,258 @@ | ||
import React, { CSSProperties, forwardRef, useEffect, useRef, ForwardedRef, useImperativeHandle } from 'react'; | ||
import * as echarts from 'echarts'; | ||
|
||
import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage'; | ||
|
||
import { debounce } from 'lodash-es'; | ||
import getGithubTheme from '../../../../helpers/get-github-theme'; | ||
import dayjs from 'dayjs'; | ||
import { getOpenRank } from '../../../../api/community'; | ||
|
||
export interface DateControllers { | ||
update: (newDate: string) => void; | ||
} | ||
|
||
interface NetworkProps { | ||
/** | ||
* data | ||
*/ | ||
readonly data: any; | ||
/** | ||
* `style` for graph container | ||
*/ | ||
readonly style?: CSSProperties; | ||
|
||
readonly focusedNodeID: string; | ||
|
||
date?: string; | ||
} | ||
|
||
const typeMap = new Map([ | ||
['r', 'repo'], | ||
['i', 'issue'], | ||
['p', 'pull'], | ||
['u', 'user'], | ||
]); | ||
|
||
const genName = (node: { c: string; n: { toString: () => any } }) => | ||
node.c == 'i' || node.c == 'p' ? `#${node.n.toString()}` : node.n.toString(); | ||
|
||
const categories = Array.from(typeMap.values()); | ||
|
||
const theme = getGithubTheme(); | ||
const DARK_TEXT_COLOR = 'rgba(230, 237, 243, 0.9)'; | ||
|
||
const generateEchartsData = (data: any, focusedNodeID: string | undefined): any => { | ||
const generateNodes = (nodes: any[]): any => { | ||
return nodes.map((n: any) => { | ||
return { | ||
id: n.id, | ||
name: genName(n), | ||
value: n.v, | ||
symbolSize: Math.log(n.v + 1) * 6, | ||
category: typeMap.get(n.c), | ||
}; | ||
}); | ||
}; | ||
const generateEdges = (edges: any[]): any => { | ||
if (edges.length === 0) { | ||
return []; | ||
} | ||
return edges.map((e: any) => { | ||
return { | ||
source: e.s, | ||
target: e.t, | ||
value: e.w, | ||
}; | ||
}); | ||
}; | ||
return { | ||
nodes: generateNodes(data.nodes), | ||
edges: generateEdges(data.links), | ||
}; | ||
}; | ||
|
||
const getOption = (data: any, date: string | undefined) => { | ||
return { | ||
tooltip: { | ||
trigger: 'item', | ||
}, | ||
animation: true, | ||
animationDuration: 2000, | ||
|
||
legend: [ | ||
{ | ||
data: categories, | ||
}, | ||
], | ||
series: [ | ||
{ | ||
name: 'Collaborative graph', | ||
type: 'graph', | ||
layout: 'force', | ||
nodes: data.nodes, | ||
edges: data.edges, | ||
categories: categories.map((c) => { | ||
return { name: c }; | ||
}), | ||
// Enable mouse zooming and translating | ||
roam: true, | ||
label: { | ||
position: 'right', | ||
show: true, | ||
}, | ||
force: { | ||
// initLayout: 'circular', | ||
// gravity: 0.1, | ||
repulsion: 300, | ||
// edgeLength: [50, 100], | ||
// Disable the iteration animation of layout | ||
layoutAnimation: false, | ||
}, | ||
lineStyle: { | ||
curveness: 0.3, | ||
opacity: 0.2, | ||
}, | ||
emphasis: { | ||
focus: 'adjacency', | ||
label: { | ||
position: 'right', | ||
show: true, | ||
}, | ||
}, | ||
}, | ||
], | ||
graphic: { | ||
elements: [ | ||
{ | ||
type: 'text', | ||
right: 60, | ||
bottom: 60, | ||
style: { | ||
text: date, | ||
font: 'bolder 60px monospace', | ||
fill: theme === 'light' ? 'rgba(100, 100, 100, 0.3)' : DARK_TEXT_COLOR, | ||
}, | ||
z: 100, | ||
}, | ||
], | ||
}, | ||
}; | ||
}; | ||
|
||
const Network = forwardRef( | ||
( | ||
{ data, style = {}, focusedNodeID, date }: NetworkProps, | ||
forwardedRef: ForwardedRef<DateControllers> | ||
): JSX.Element => { | ||
const divEL = useRef(null); | ||
let graphData = generateEchartsData(data, focusedNodeID); | ||
let option = getOption(graphData, date); | ||
|
||
const clearDiv = (id: string) => { | ||
var div = document.getElementById(id); | ||
if (div && div.hasChildNodes()) { | ||
var children = div.childNodes; | ||
for (var child of children) { | ||
div.removeChild(child); | ||
} | ||
} | ||
}; | ||
|
||
const addRow = (table: HTMLElement | null, texts: any[]) => { | ||
// @ts-ignore | ||
var tr = table.insertRow(); | ||
for (var t of texts) { | ||
var td = tr.insertCell(); | ||
td.appendChild(document.createTextNode(t)); | ||
} | ||
}; | ||
|
||
const update = (newDate: string) => { | ||
getOpenRank(focusedNodeID, newDate).then((openRank) => { | ||
let chartDOM = divEL.current; | ||
const instance = echarts.getInstanceByDom(chartDOM as any); | ||
if (instance) { | ||
if (openRank == null) { | ||
instance.setOption( | ||
{ | ||
title: { | ||
text: `OpenRank for ${focusedNodeID} in ${newDate} is has not been generated`, | ||
top: 'middle', | ||
left: 'center', | ||
}, | ||
}, | ||
{ notMerge: true } | ||
); | ||
} else { | ||
graphData = generateEchartsData(openRank, focusedNodeID); | ||
option = getOption(graphData, newDate); | ||
instance.setOption(option, { notMerge: true }); | ||
} | ||
} | ||
}); | ||
}; | ||
|
||
const setDetails = (graph: { links: any[]; nodes: any[] }, node: { r: number; i: number; id: any }) => { | ||
clearDiv('details_table'); | ||
var table = document.getElementById('details_table'); | ||
addRow(table, ['From', 'Ratio', 'Value', 'OpenRank']); | ||
addRow(table, ['Self', node.r, node.i, (node.r * node.i).toFixed(3)]); | ||
var other = graph.links | ||
.filter((l) => l.t == node.id) | ||
.map((l) => { | ||
var source = graph.nodes.find((n) => n.id == l.s); | ||
return [ | ||
genName(source), | ||
parseFloat(((1 - node.r) * l.w).toFixed(3)), | ||
source.v, | ||
parseFloat(((1 - node.r) * l.w * source.v).toFixed(3)), | ||
]; | ||
}) | ||
.sort((a, b) => b[3] - a[3]); | ||
for (var r of other) { | ||
addRow(table, r); | ||
} | ||
}; | ||
|
||
useImperativeHandle(forwardedRef, () => ({ | ||
update, | ||
})); | ||
|
||
useEffect(() => { | ||
let chartDOM = divEL.current; | ||
const instance = echarts.init(chartDOM as any); | ||
|
||
return () => { | ||
instance.dispose(); | ||
}; | ||
}, []); | ||
|
||
useEffect(() => { | ||
let chartDOM = divEL.current; | ||
const instance = echarts.getInstanceByDom(chartDOM as any); | ||
if (instance) { | ||
instance.setOption(option); | ||
instance.on('dblclick', function (params) { | ||
setDetails( | ||
data, | ||
// @ts-ignore | ||
data.nodes.find((i: { id: any }) => i.id === params.data.id) | ||
); | ||
}); | ||
const debouncedResize = debounce(() => { | ||
instance.resize(); | ||
}, 1000); | ||
window.addEventListener('resize', debouncedResize); | ||
} | ||
}, []); | ||
|
||
return ( | ||
<div className="hypertrons-crx-border"> | ||
<div ref={divEL} style={style}></div> | ||
</div> | ||
); | ||
} | ||
); | ||
|
||
export default Network; |
31 changes: 31 additions & 0 deletions
31
src/pages/ContentScripts/features/community-openrank-network/index.scss
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,31 @@ | ||
#details_table { | ||
width: 95%; | ||
margin: 10px; | ||
tr:nth-child(even) { | ||
background-color: #d6eeee; | ||
} | ||
th, | ||
td { | ||
border: 1px solid black; | ||
text-align: center; /* 水平居中 */ | ||
vertical-align: middle; /* 垂直居中 */ | ||
} | ||
} | ||
|
||
#details_title { | ||
text-align: center; | ||
font-size: 12px; | ||
} | ||
|
||
.bordered { | ||
border: 2px solid grey; | ||
} | ||
|
||
#details_div { | ||
height: 250px; | ||
} | ||
|
||
.scrollit { | ||
overflow-x: hidden; | ||
overflow-y: auto; | ||
} |
64 changes: 64 additions & 0 deletions
64
src/pages/ContentScripts/features/community-openrank-network/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,64 @@ | ||
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, isPublicRepoWithMeta, isRepoRoot } from '../../../../helpers/get-repo-info'; | ||
import { getOpenRank } from '../../../../api/community'; | ||
import { RepoMeta, metaStore } from '../../../../api/common'; | ||
import View from './view'; | ||
import './index.scss'; | ||
import DataNotFound from '../repo-networks/DataNotFound'; | ||
|
||
const featureId = features.getFeatureID(import.meta.url); | ||
let repoName: string; | ||
let openRank: any; | ||
let meta: RepoMeta; | ||
|
||
const getData = async () => { | ||
meta = (await metaStore.get(repoName)) as RepoMeta; | ||
// const lastDataAvailableMonth = meta.updatedAt ? new Date(meta.updatedAt) : new Date(); | ||
// lastDataAvailableMonth.setDate(0); | ||
// | ||
// const newestMonth = | ||
// lastDataAvailableMonth.getFullYear() + '-' + (lastDataAvailableMonth.getMonth() + 1).toString().padStart(2, '0'); | ||
openRank = await getOpenRank(repoName, '2023-09'); | ||
}; | ||
|
||
const renderTo = (container: Container) => { | ||
if (!openRank) { | ||
render(<DataNotFound />, container); | ||
return; | ||
} | ||
render(<View repoName={repoName} openrank={openRank} meta={meta} />, container); | ||
}; | ||
|
||
const init = async (): Promise<void> => { | ||
repoName = getRepoName(); | ||
await getData(); | ||
// create container | ||
const container = document.createElement('div'); | ||
container.id = featureId; | ||
|
||
$('#hypercrx-perceptor-slot-community-openrank-network').append(container); | ||
renderTo(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(); | ||
await getData(); | ||
} | ||
// rerender the chart or it will be empty | ||
renderTo($(`#${featureId}`)[0]); | ||
}; | ||
|
||
features.add(featureId, { | ||
asLongAs: [isPerceptor, isPublicRepoWithMeta], | ||
awaitDomReady: true, | ||
init, | ||
restore, | ||
}); |
Oops, something went wrong.