From 7aedbc3934aad46a2fb1389f16c1ebdc6111a04f Mon Sep 17 00:00:00 2001 From: andyhuang18 <763230412@qq.com> Date: Wed, 14 Jun 2023 23:30:35 +0800 Subject: [PATCH] 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 --- src/api/repo.ts | 5 + src/locales/en/messages.json | 6 + src/locales/zh_CN/messages.json | 6 + .../repo-activity-racing-bar/RacingBar.tsx | 171 ++++++++++++++++++ .../repo-activity-racing-bar/index.tsx | 59 ++++++ .../repo-activity-racing-bar/view.tsx | 65 +++++++ src/pages/ContentScripts/index.scss | 17 ++ src/pages/ContentScripts/index.ts | 1 + 8 files changed, 330 insertions(+) create mode 100644 src/pages/ContentScripts/features/repo-activity-racing-bar/RacingBar.tsx create mode 100644 src/pages/ContentScripts/features/repo-activity-racing-bar/index.tsx create mode 100644 src/pages/ContentScripts/features/repo-activity-racing-bar/view.tsx diff --git a/src/api/repo.ts b/src/api/repo.ts index 5be9f536..63858546 100644 --- a/src/api/repo.ts +++ b/src/api/repo.ts @@ -18,6 +18,7 @@ const metricNameMap = new Map([ ['merged_code_sum', 'code_change_lines_sum'], ['developer_network', 'developer_network'], ['repo_network', 'repo_network'], + ['activity_details', 'activity_details'], ]); export const getActivity = async (repo: string) => { @@ -83,3 +84,7 @@ export const getDeveloperNetwork = async (repo: string) => { export const getRepoNetwork = async (repo: string) => { return getMetricByName(repo, metricNameMap, 'repo_network'); }; + +export const getActivityDetails = async (repo: string) => { + return getMetricByName(repo, metricNameMap, 'activity_details'); +}; diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 77ef9ea2..bc591c44 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -149,9 +149,15 @@ "component_developerActORTrend_yName2": { "message": "OpenRank" }, + "component_projectRacingBar_title": { + "message": "Project User Activity Racing Bar" + }, "component_projectCorrelationNetwork_title": { "message": "Project Correlation Network" }, + "component_projectRacingBar_description": { + "message": "Project User Activity Racing Bar Shows the change of user activity, which is used to discover the trend of user participation in projects." + }, "component_projectCorrelationNetwork_description": { "message": "Project Correlation Network shows the correlation between projects for a given time period. From this graph you can find the projects that are related to the given project." }, diff --git a/src/locales/zh_CN/messages.json b/src/locales/zh_CN/messages.json index 44d8076e..927407bb 100644 --- a/src/locales/zh_CN/messages.json +++ b/src/locales/zh_CN/messages.json @@ -149,9 +149,15 @@ "component_developerActORTrend_yName2": { "message": "OpenRank" }, + "component_projectRacingBar_title": { + "message": "项目用户活跃度对比图" + }, "component_projectCorrelationNetwork_title": { "message": "项目关系网络图" }, + "component_projectRacingBar_description": { + "message": "项目用户活跃度对比图展示了用户活跃度的变化,用于发现用户参与项目变化趋势。" + }, "component_projectCorrelationNetwork_description": { "message": "项目关系网络图展示了在给定的时间段内,项目与项目之间的联结关系,用于项目间关系的追踪与挖掘。从该网络图中,可以找出与该项目有联结关系的其他项目。" }, diff --git a/src/pages/ContentScripts/features/repo-activity-racing-bar/RacingBar.tsx b/src/pages/ContentScripts/features/repo-activity-racing-bar/RacingBar.tsx new file mode 100644 index 00000000..8629a61d --- /dev/null +++ b/src/pages/ContentScripts/features/repo-activity-racing-bar/RacingBar.tsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect, useRef } from 'react'; +import * as echarts from 'echarts'; + +interface RacingBarProps { + //theme: 'light' | 'dark'; + width: number; + height: number; + repoName: string; + data: any; +} + +const RacingBar = (props: RacingBarProps): JSX.Element => { + const [playing, setPlaying] = useState(0); + const { width, height, data } = props; + const divEL = useRef(null); + const updateFrequency = 3000; + const colorMap = new Map(); + const option = { + grid: { + top: 10, + bottom: 30, + left: 150, + right: 80, + }, + 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('-', '')}|}`; + }, + rich: null, + }, + animationDuration: 300, + animationDurationUpdate: 300, + }, + series: [ + { + realtimeSort: true, + seriesLayoutBy: 'column', + type: 'bar', + itemStyle: { + color: function (params: { value: 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: null, + 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: null, + font: 'bolder 60px monospace', + fill: 'rgba(100, 100, 100, 0.25)', + }, + z: 100, + }, + ], + }, + }; + + useEffect(() => { + // @ts-ignore + let chartDOM = divEL.current; + const instance = echarts.init(chartDOM as any); + // 组件卸载时销毁图表 + return () => { + instance.dispose(); + }; + }, [playing]); + + useEffect(() => { + let chartDOM = divEL.current; + const instance = echarts.getInstanceByDom(chartDOM as any); + const months = Object.keys(data); + // 在数据变化时调用图表更新函数 + // 根据传入的新数据进行图表的更新操作 + let startIndex = 0; + + for (let i = startIndex; i < months.length - 1; ++i) { + (function (i) { + setTimeout(function () { + updateMonth(months[i + 1]); + if (i + 1 === months.length - 1) { + } + }, (i - startIndex) * updateFrequency); + })(i); + } + + // @ts-ignore + function updateMonth(month: string | null) { + const rich = {}; + // @ts-ignore + data[month].forEach((item: any[]) => { + // rich name cannot contain special characters such as '-' + // @ts-ignore + 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; + // @ts-ignore + instance.setOption(option); + } + }, [playing]); + + const handleReplayClick = () => { + let chartDOM = divEL.current; + const instance = echarts.getInstanceByDom(chartDOM as any); + // @ts-ignore + instance.setOption(option); + setPlaying(playing + 1); + }; + + return ( +
+
+
+ +
+
+ ); +}; + +export default RacingBar; diff --git a/src/pages/ContentScripts/features/repo-activity-racing-bar/index.tsx b/src/pages/ContentScripts/features/repo-activity-racing-bar/index.tsx new file mode 100644 index 00000000..76bf23f5 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-activity-racing-bar/index.tsx @@ -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(, container); + return; + } + render( + , + container + ); +}; + +const init = async (): Promise => { + 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, +}); diff --git a/src/pages/ContentScripts/features/repo-activity-racing-bar/view.tsx b/src/pages/ContentScripts/features/repo-activity-racing-bar/view.tsx new file mode 100644 index 00000000..7ee1364f --- /dev/null +++ b/src/pages/ContentScripts/features/repo-activity-racing-bar/view.tsx @@ -0,0 +1,65 @@ +import React, { useState, useEffect } from 'react'; + +import getMessageByLocale from '../../../../helpers/get-message-by-locale'; +import optionsStorage, { + HypercrxOptions, + defaults, +} from '../../../../options-storage'; +import RacingBar from './RacingBar'; + +interface Props { + currentRepo: string; + repoActivityDetails: any; +} + +const View = ({ currentRepo, repoActivityDetails }: Props): JSX.Element => { + const [options, setOptions] = useState(defaults); + + useEffect(() => { + (async function () { + setOptions(await optionsStorage.getAll()); + })(); + }, []); + + return ( +
+
+
+ + {getMessageByLocale( + 'component_projectRacingBar_title', + options.locale + )} + +
+
+
+
+ +
+
+
+
+

+ {getMessageByLocale( + 'component_projectRacingBar_description', + options.locale + )} +

+
+
+
+
+
+ ); +}; + +export default View; diff --git a/src/pages/ContentScripts/index.scss b/src/pages/ContentScripts/index.scss index 3862433e..5a0e05e1 100644 --- a/src/pages/ContentScripts/index.scss +++ b/src/pages/ContentScripts/index.scss @@ -109,3 +109,20 @@ -ms-user-select: none; /* IE10+/Edge */ user-select: none; /* Standard */ } + +.replay-button { + display: inline-block; + padding: 5px 10px; + font-size: 8px; + font-weight: bold; + text-align: center; + text-decoration: none; + border-radius: 4px; + cursor: pointer; + background-color: #4caf50; + color: white; + border: none; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s, transform 0.2s, box-shadow 0.3s; + margin-bottom: 2px; +} diff --git a/src/pages/ContentScripts/index.ts b/src/pages/ContentScripts/index.ts index 346f7331..bab274a3 100644 --- a/src/pages/ContentScripts/index.ts +++ b/src/pages/ContentScripts/index.ts @@ -12,3 +12,4 @@ import './features/perceptor-layout'; import './features/repo-networks'; import './features/developer-networks'; import './features/oss-gpt'; +import './features/repo-activity-racing-bar';