Skip to content

Commit

Permalink
Merge pull request #6 from Fiveneves/fiveneves
Browse files Browse the repository at this point in the history
feat: Add Community OpenRank Detail Network
  • Loading branch information
Fiveneves authored Jul 24, 2024
2 parents b81fc98 + 9678532 commit 93cb3d6
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 2 deletions.
6 changes: 4 additions & 2 deletions src/api/community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import request from '../helpers/request';
import { ErrorCode, OSS_XLAB_ENDPOINT } from '../constant';

export const getMetricByDate = async (repoName: string, date: string) => {
let response;
try {
return await request(
response = await request(
`${OSS_XLAB_ENDPOINT}/open_digger/github/${repoName}/project_openrank_detail/${date}.json`
);
} catch (error) {
Expand All @@ -15,8 +16,9 @@ export const getMetricByDate = async (repoName: string, date: string) => {
throw error;
}
}
return response;
};

export const getOpenrank = async (repo: string, date: string) => {
export const getOpenRank = async (repo: string, date: string) => {
return getMetricByDate(repo, date);
};
2 changes: 2 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"global_day_one": "day",
"global_day_other": "days",
"global_clickToshow": "Click to show",
"component_communityOpenRankNetwork_title": "Community OpenRank Detail Network",
"component_communityOpenRankNetwork_description": "Community OpenRank Detail Network shows the OpenRank about the project by month. Double-click a node in this network to view the details of OpenRank for that node in the table on the right. ",
"component_developerCollaborationNetwork_title": "Developer Collaboration Network",
"component_developerCollaborationNetwork_description": "Developer Collaboration Network shows the collaboration between developers for a given time period. From this graph you can find other developers who are closet to a given developer.",
"component_developerCollaborationNetwork_description_node": "Node: Developer, node size and shades of color indicate developer activity.",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"global_period": "周期",
"global_day": "",
"global_clickToshow": "点击查看",
"component_communityOpenRankNetwork_title": "社区OpenRank网络图",
"component_communityOpenRankNetwork_description": "社区OpenRank网络图按月显示有关该项目的OpenRank。双击此网络中的一个节点,可以在右侧表格中查看该节点OpenRank详细信息。 ",
"component_developerCollaborationNetwork_title": "开发者协作网络图",
"component_developerCollaborationNetwork_description": "开发者协作网络图展示了在给定的时间段内,开发者与开发者之间的协作关系, 用于开发者关系的追踪与挖掘。从该网络图中,可以找出与该开发者联系较为紧密的其他开发者。",
"component_developerCollaborationNetwork_description_node": "节点:一个节点表示开发者,节点大小与颜色的深浅表示开发者活跃度的大小。",
Expand Down
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;
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;
}
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,
});
Loading

0 comments on commit 93cb3d6

Please sign in to comment.