diff --git a/src/api/repo.ts b/src/api/repo.ts index 63858546..7fce3104 100644 --- a/src/api/repo.ts +++ b/src/api/repo.ts @@ -19,6 +19,7 @@ const metricNameMap = new Map([ ['developer_network', 'developer_network'], ['repo_network', 'repo_network'], ['activity_details', 'activity_details'], + ['issue_response_time', 'issue_response_time'], ]); export const getActivity = async (repo: string) => { @@ -88,3 +89,7 @@ export const getRepoNetwork = async (repo: string) => { export const getActivityDetails = async (repo: string) => { return getMetricByName(repo, metricNameMap, 'activity_details'); }; + +export const getIssueResponseTime = async (repo: string) => { + return getMetricByName(repo, metricNameMap, 'issue_response_time'); +}; diff --git a/src/helpers/get-newest-month.ts b/src/helpers/get-newest-month.ts new file mode 100644 index 00000000..4f9b2160 --- /dev/null +++ b/src/helpers/get-newest-month.ts @@ -0,0 +1,14 @@ +const getNewestMonth = () => { + const now = new Date(); + if (now.getDate() === 1) { + // data for last month is not ready in the first day of the month (#595) + now.setDate(0); // a way to let month - 1 + } + now.setDate(0); // see issue #632 + + return ( + now.getFullYear() + '-' + (now.getMonth() + 1).toString().padStart(2, '0') + ); +}; + +export default getNewestMonth; diff --git a/src/pages/ContentScripts/features/charts-design/index.tsx b/src/pages/ContentScripts/features/charts-design/index.tsx deleted file mode 100644 index 244fc9e7..00000000 --- a/src/pages/ContentScripts/features/charts-design/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useState } from 'react'; -import { render, Container } from 'react-dom'; -import $ from 'jquery'; -import View from './view'; - -import features from '../../../../feature-manager'; -import { isPublicRepo } from '../../../../helpers/get-repo-info'; - -const featureId = features.getFeatureID(import.meta.url); - -const renderTo = (container: Container) => { - render(, container); -}; - -const init = async (): Promise => { - const container = document.createElement('div'); - container.id = featureId; - renderTo(container); - $('body').append(container); -}; - -features.add(featureId, { - asLongAs: [isPublicRepo], - awaitDomReady: false, - init, -}); diff --git a/src/pages/ContentScripts/features/charts-design/view.tsx b/src/pages/ContentScripts/features/charts-design/view.tsx deleted file mode 100644 index c8ffc8f5..00000000 --- a/src/pages/ContentScripts/features/charts-design/view.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import getGithubTheme from '../../../../helpers/get-github-theme'; -import optionsStorage, { - HypercrxOptions, - defaults, -} from '../../../../options-storage'; -import { Modal, Row, Col } from 'antd'; -import Bars from '../../../../components/Bars'; - -const githubTheme = getGithubTheme(); - -const LIGHT_THEME = { - FG_COLOR: '#24292f', - BG_COLOR: '#ffffff', - PALLET: ['#5470c6', '#91cc75'], -}; - -const DARK_THEME = { - FG_COLOR: '#c9d1d9', - BG_COLOR: '#0d1118', - PALLET: ['#58a6ff', '#3fb950'], -}; - -interface Props {} -const data1: any = [ - ['2022-01', 5], - ['2022-02', 10], - ['2022-03', 15], - ['2022-05', 25], - ['2022-06', 30], - ['2022-08', 40], - ['2022-09', 45], -]; -const data2: any = [ - ['2022-01', 12], - ['2022-02', 18], - ['2022-03', 27], - ['2022-04', 8], - ['2022-05', 36], - ['2022-06', 42], - ['2022-07', 20], - ['2022-08', 50], - ['2022-09', 15], -]; - -const mockData = { - bar: { - legend1: 'legend1', - legend2: 'legend2', - yName1: 'yName1', - yName2: 'yName2', - data1: data1, - data2: data2, - }, -}; - -const View = ({}: Props): JSX.Element | null => { - const [options, setOptions] = useState(defaults); - const [isModalOpen, setIsModalOpen] = useState(false); - - const showModal = () => { - setIsModalOpen(true); - }; - - const handleOk = () => { - setIsModalOpen(false); - }; - - useEffect(() => { - (async function () { - setOptions(await optionsStorage.getAll()); - })(); - }, []); - - // receive message from popup - chrome.runtime.onMessage.addListener(function ( - request, - sender, - sendResponse - ) { - if (request.greeting === 'demo') { - showModal(); - focus(); // change the focus to the browser content - } - }); - - return ( -
- - - -

Bar Chart Light Theme

-
- -
- - -

Bar Chart Dark Theme

-
- -
- -
-
-
- ); -}; - -export default View; diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.css b/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.css new file mode 100644 index 00000000..3ed0d540 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.css @@ -0,0 +1,6 @@ +/* ChartCard.css */ + +.custom-card { + background-color: #fafafa; + /* 其他样式属性 */ +} diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.tsx b/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.tsx new file mode 100644 index 00000000..13856c10 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/CollectionContent/ChartCard.tsx @@ -0,0 +1,23 @@ +import React, { ReactNode } from 'react'; +import { Card } from 'antd'; +import './ChartCard.css'; // 导入自定义样式 + +interface ChartCardProps { + title: ReactNode; + children: React.ReactNode; +} + +function ChartCard({ title, children }: ChartCardProps) { + return ( + + {children} + + ); +} + +export default ChartCard; diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionContent/CollectionDashboard.tsx b/src/pages/ContentScripts/features/repo-collection/CollectionContent/CollectionDashboard.tsx new file mode 100644 index 00000000..1ea7b3ce --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/CollectionContent/CollectionDashboard.tsx @@ -0,0 +1,170 @@ +import LineChart from '../charts/LineChart'; +import BarChart from '../charts/BarChart'; +import SankeyChart from '../charts/SankeyChart'; +import PieChart from '../charts/PieChart'; +import StackedBarChart from '../charts/StackedBarChart'; +import CodeStackedBarChart from '../charts/CodeStackedBarChart'; +import BoxplotChart from '../charts/BoxplotChart'; +import ChartCard from './ChartCard'; +import NumericPanel from '../charts/NumericPanel'; + +import React from 'react'; +import { Row, Col } from 'antd'; + +interface CollectionDashboardProps { + repoNames: string[]; + currentRepo?: string; +} + +const CollectionDashboard: React.FC = ({ + repoNames, + currentRepo, +}) => { + return ( +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ ); +}; + +export default CollectionDashboard; diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionContent/index.tsx b/src/pages/ContentScripts/features/repo-collection/CollectionContent/index.tsx new file mode 100644 index 00000000..62e74824 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/CollectionContent/index.tsx @@ -0,0 +1,56 @@ +// Index.tsx +import React, { useState, useEffect } from 'react'; +import { Layout, Menu, theme } from 'antd'; + +import CollectionDashboard from './CollectionDashboard'; + +const { Content, Sider } = Layout; +interface Index { + repoNames: string[]; + + currentRepo?: string; +} + +const LIGHT_THEME = { + BG_COLOR: '#ffffff', +}; + +const CollectionContent: React.FC = ({ repoNames, currentRepo }) => { + const menuItems = repoNames.map((repo, index) => ({ + key: index, + label: repo, + })); + const { + token: { colorBgContainer }, + } = theme.useToken(); + + // 添加一个状态来跟踪选中的仓库名 + const [selectedRepo, setSelectedRepo] = useState( + undefined + ); + useEffect(() => { + setSelectedRepo(currentRepo); + }, [currentRepo]); + const handleMenuClick = (key: string) => { + setSelectedRepo(key); + }; + + return ( + + + handleMenuClick(key)} //点击切换选中的repo + /> + + + + + + ); +}; + +export default CollectionContent; diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionModal/CollectionEditor.tsx b/src/pages/ContentScripts/features/repo-collection/CollectionModal/CollectionEditor.tsx index 734207cf..e89235dd 100644 --- a/src/pages/ContentScripts/features/repo-collection/CollectionModal/CollectionEditor.tsx +++ b/src/pages/ContentScripts/features/repo-collection/CollectionModal/CollectionEditor.tsx @@ -32,7 +32,7 @@ interface RepositoryInfo { interface CollectionEditorProps { open: boolean; - onCreate: (values: Values, newRepoData: string[] | undefined) => void; + onCreate: (values: Values, newRepoData: string[]) => void; onCancel: () => void; isEdit: boolean | undefined; collectionName: string; diff --git a/src/pages/ContentScripts/features/repo-collection/CollectionModal/index.tsx b/src/pages/ContentScripts/features/repo-collection/CollectionModal/index.tsx index b5281f80..72e22132 100644 --- a/src/pages/ContentScripts/features/repo-collection/CollectionModal/index.tsx +++ b/src/pages/ContentScripts/features/repo-collection/CollectionModal/index.tsx @@ -1,5 +1,6 @@ import { useRepoCollectionContext } from '../context'; import CollectionEditor from './CollectionEditor'; +import CollectionContent from '../CollectionContent'; import React, { useState, useEffect } from 'react'; import { Modal, Tabs, List, Col, Row, Button } from 'antd'; @@ -8,7 +9,7 @@ type TargetKey = React.MouseEvent | React.KeyboardEvent | string; type CollectionTabType = { label: string; - children: string; + children: React.ReactNode; key: string; }; @@ -25,7 +26,7 @@ export const CollectionManageModal = () => { const [activeKey, setActiveKey] = useState(); const [items, setItems] = useState([]); - const [listData, setListData] = useState( + const [listData, setListData] = useState( allRelations .filter((relation) => relation.collectionId === allCollections[0].id) .map((relation) => relation.repositoryId) @@ -58,11 +59,16 @@ export const CollectionManageModal = () => { ); useEffect(() => { - const initialItems = allCollections.map((collection, index) => ({ - label: collection.name, - children: `Content of ${collection.name}`, - key: collection.id, - })); + const initialItems = allCollections.map((collection) => { + const repoList = allRelations + .filter((relation) => relation.collectionId === collection.name) + .map((relation) => relation.repositoryId); + return { + label: collection.name, + children: , + key: collection.id, + }; + }); const initialListData = allRelations .filter((relation) => relation.collectionId === selectedCollection @@ -77,7 +83,7 @@ export const CollectionManageModal = () => { useEffect(() => {}, []); - const onCreate = async (values: any, newRepoData: string[] | undefined) => { + const onCreate = async (values: any, newRepoData: string[]) => { if (isEdit) { const updatedItems = items.map((item) => { if (item.key === activeKey?.toString()) { @@ -90,7 +96,7 @@ export const CollectionManageModal = () => { const newPanes = [...items]; newPanes.push({ label: values.collectionName, - children: `Content of ${values.collectionName}`, + children: , key: values.collectionName, }); setItems(newPanes); @@ -199,41 +205,17 @@ export const CollectionManageModal = () => { height: '100vh', maxWidth: 'unset', }} - bodyStyle={{ height: 'calc(100vh - 40px)' }} // 40px is the sum of top and bottom padding + bodyStyle={{ height: 'calc(100vh - 40px)', overflow: 'auto' }} // 40px is the sum of top and bottom padding > - - -
- - {selectedCollection - ? selectedCollection - : 'Select tab first'} -
- } - bordered - dataSource={allRelations - .filter( - (relation) => relation.collectionId === selectedCollection - ) - .map((relation) => relation.repositoryId)} - renderItem={(item) => {item}} - /> - - - - - -
+ {isClick && ( { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'axis', + }, + // legend: { + // type: 'scroll', + // }, + grid: { + left: '5%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'time', + splitLine: { + show: false, + }, + axisLabel: { + color: TH.FG_COLOR, + formatter: { + year: '{yearStyle|{yy}}', + month: '{MMM}', + }, + rich: { + yearStyle: { + fontWeight: 'bold', + }, + }, + }, + }, + yAxis: { + type: 'value', + }, + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 3600 * 24 * 1000 * 180, + }, + ], + series: BarChartSeries(data), // / Utilize the transformed series data + }; + console.log('bar', BarChartSeries(data)); + + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getStars(repo); + // Update Data/ + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: Number(currentRepo), + // dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; +const BarChartSeries = (data: { + [repo: string]: RawRepoData; +}): echarts.SeriesOption[] => + Object.entries(data).map(([repoName, repoData]) => ({ + name: repoName, + type: 'bar', + symbol: 'none', + data: getLastSixMonth(generateDataByMonth(repoData)), + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + triggerLineEvent: true, + })); + +const getLastSixMonth = (data: any[]) => + data.length > 6 ? data.slice(-6) : data; + +export default BarChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/BoxplotChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/BoxplotChart.tsx new file mode 100644 index 00000000..65d9d45c --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/BoxplotChart.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import { getIssueResponseTime } from '../../../../../api/repo'; +import getNewestMonth from '../../../../../helpers/get-newest-month'; + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface BarChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const BoxplotChart = (props: BarChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{}>({}); + + console.log('Boxplot_data,', lastMonthRepoData(data)); + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'item', + axisPointer: { + type: 'shadow', + }, + }, + legend: { + type: 'scroll', + }, + grid: { + left: '5%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + show: false, + type: 'category', + boundaryGap: true, + nameGap: 30, + splitArea: { + show: false, + }, + // data: Object.keys(data), + splitLine: { + show: false, + }, + // data: lastMonthRepoData(data).map((repo) => repo.name), + }, + yAxis: { + type: 'value', + name: 'value', + splitArea: { + show: true, + }, + }, + // dataZoom: [ + // { + // type: 'inside', + // start: 0, + // end: 100, + // minValueSpan: 3600 * 24 * 1000 * 180, + // }, + // ], + series: lastMonthRepoData(data).map((repoData) => { + return { + type: 'boxplot', + name: repoData.name, + data: [repoData], + }; + }), + // { + // type: 'boxplot', + // data: lastMonthRepoData(data), + // }, + }; + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getIssueResponseTime(repo); + // console.log('starsDatastarsData', starsData); + // Update Data/ + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + // console.log('data', data); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + // console.log('data', data); + // console.log('lastMonthRepoData', lastMonthRepoData(data)); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + seriesIndex: Number(currentRepo), + dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +//原始数据 =>各仓库最近一个月的数据[] +function lastMonthRepoData(repo_data: any) { + const resultArray = []; + const lastMonth = getNewestMonth(); + for (const repoName in repo_data) { + if (repo_data.hasOwnProperty(repoName)) { + const lastMonthData = { + name: repoName, + value: + repo_data[repoName][`avg`][lastMonth] !== undefined + ? Array.from( + { length: 5 }, + (_, q) => repo_data[repoName][`quantile_${q}`][lastMonth] + ) + : [null, null, null, null, null], + }; + + resultArray.push(lastMonthData); + // 将转换后的数据存储为对象,并添加到结果数组中 + // console.log('repoName', repoName); + // console.log('lastM', repo_data[repoName][`avg`][lastMonth]); + } + } + return resultArray; +} + +export default BoxplotChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/CodeStackedBarChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/CodeStackedBarChart.tsx new file mode 100644 index 00000000..430a369c --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/CodeStackedBarChart.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import generateDataByMonth from '../../../../../helpers/generate-data-by-month'; +import { + getMergedCodeAddition, + getMergedCodeDeletion, +} from '../../../../../api/repo'; + +interface RawRepoData { + [date: string]: number; +} + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface StackedBarChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const CodeStackedBarChart = (props: StackedBarChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', // 设置 axisPointer 的类型为 cross,即十字准星 + }, + // formatter: (params: any) => { + // console.log('params',params); + + // return result; + // }, + }, + legend: { + type: 'scroll', + }, + grid: { + left: '5%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'time', + splitLine: { + show: false, + }, + axisLabel: { + color: TH.FG_COLOR, + formatter: { + year: '{yearStyle|{yy}}', + month: '{MMM}', + }, + rich: { + yearStyle: { + fontWeight: 'bold', + }, + }, + }, + }, + yAxis: { + type: 'value', + }, + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 3600 * 24 * 1000 * 180, + }, + ], + series: MCDeletionSeries(data).concat(MCAdditionSeries(data)), // Series Data: Code Addition + Code CodeDeletion + }; + console.log( + 'BarChartSeries', + MCDeletionSeries(data).concat(MCAdditionSeries(data)) + ); + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + const MCAdditionData = await getMergedCodeAddition(repo); + const MCDeletionData = await getMergedCodeDeletion(repo); + const MergedCodeData = { + MCAdditionData: MCAdditionData, + MCDeletionData: MCDeletionData, + }; + setData((prevData) => ({ ...prevData, [repo]: MergedCodeData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + console.log('data', data); + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: Number(currentRepo), + // dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +//Series:各仓库代码增加行数 +const MCAdditionSeries = (data: { + [repo: string]: RawRepoData; +}): echarts.SeriesOption[] => + Object.entries(data).map(([repoName, repoData]) => ({ + name: repoName, + type: 'line', + areaStyle: {}, + smooth: true, + symbol: 'none', + stack: repoName, + data: generateDataByMonth(repoData.MCAdditionData), + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + triggerLineEvent: true, + })); + +//Series:各仓库代码删减行数 +const MCDeletionSeries = (data: { + [repo: string]: RawRepoData; +}): echarts.SeriesOption[] => + Object.entries(data).map(([repoName, repoData]) => ({ + name: repoName, + type: 'line', + areaStyle: {}, + symbol: 'none', + smooth: true, + stack: repoName, + data: generateDataByMonth(repoData.MCDeletionData).map((item) => [ + item[0], + -item[1], + ]), + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + triggerLineEvent: true, + })); + +//const getLastSixMonth = (data: any[]) => (data.length > 6 ? data.slice(-6) : data); +export default CodeStackedBarChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/LineChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/LineChart.tsx new file mode 100644 index 00000000..508da19e --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/LineChart.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import generateDataByMonth from '../../../../../helpers/generate-data-by-month'; +import { getStars } from '../../../../../api/repo'; + +interface RawRepoData { + [date: string]: number; +} + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface LineChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const LineChart = (props: LineChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'axis', + }, + legend: { + type: 'scroll', + }, + grid: { + left: '5%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'time', + splitLine: { + show: false, + }, + axisLabel: { + color: TH.FG_COLOR, + formatter: { + year: '{yearStyle|{yy}}', + month: '{MMM}', + }, + rich: { + yearStyle: { + fontWeight: 'bold', + }, + }, + }, + }, + yAxis: { + type: 'value', + }, + dataZoom: [ + { + type: 'slider', + }, + { + type: 'inside', + // start: 0, + // end: 100, + minValueSpan: 3600 * 24 * 1000 * 180, + }, + ], + series: LineChartSeries(data), + }; + + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getStars(repo); + // Update Data + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: Number(currentRepo), + // dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; +const LineChartSeries = (data: { + [repo: string]: RawRepoData; +}): echarts.SeriesOption[] => + Object.entries(data).map(([repoName, repoData]) => ({ + name: repoName, + type: 'line', + symbol: 'none', + smooth: true, + data: generateDataByMonth(repoData), + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + triggerLineEvent: true, + })); +export default LineChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/NumericPanel.tsx b/src/pages/ContentScripts/features/repo-collection/charts/NumericPanel.tsx new file mode 100644 index 00000000..e60735bf --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/NumericPanel.tsx @@ -0,0 +1,101 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import generateDataByMonth from '../../../../../helpers/generate-data-by-month'; +import { getStars } from '../../../../../api/repo'; +import getNewestMonth from '../../../../../helpers/get-newest-month'; + +interface RawRepoData { + [date: string]: number; +} + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface BarChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + currentRepo?: string; +} + +const NumericPanel = (props: BarChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + graphic: [ + { + type: 'text', + left: 'center', + top: 'center', + style: { + fill: '#333', + text: valueSum(data).toString(), + font: 'bold 48px Arial', + }, + }, + ], + }; + + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getStars(repo); + // Update Data/ + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: Number(currentRepo), + // dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +function valueSum(data: Record): number { + return Object.values(data).reduce((sum, repoData) => { + const lastData = generateDataByMonth(repoData).at(-1); + const value = + lastData !== undefined && lastData[0] === getNewestMonth() + ? lastData[1] + : 0; + return sum + value; + }, 0); +} + +export default NumericPanel; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/PieChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/PieChart.tsx new file mode 100644 index 00000000..f8af46e1 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/PieChart.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import generateDataByMonth from '../../../../../helpers/generate-data-by-month'; +import { getParticipant } from '../../../../../api/repo'; +import getNewestMonth from '../../../../../helpers/get-newest-month'; + +interface RawRepoData { + [date: string]: number; +} + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface PieChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const PieChart = (props: PieChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'item', + }, + legend: { + type: 'scroll', + }, + + series: [ + { + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 20, + fontWeight: 'bold', + }, + }, + data: PieChartData(data), + }, + ], + }; + + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getParticipant(repo); + // Update Data + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + console.log('pieChart,currentRepo', currentRepo); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: 0, + dataIndex: Number(currentRepo), + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +// Retrieve data for the current month +const PieChartData = (data: { [repo: string]: RawRepoData }) => + Object.entries(data).map(([repoName, repoData]) => { + const lastData = generateDataByMonth(repoData).at(-1); + return { + name: repoName, + value: + lastData !== undefined && lastData[0] === getNewestMonth() + ? lastData[1] + : 0, + }; + }); + +export default PieChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/SankeyChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/SankeyChart.tsx new file mode 100644 index 00000000..02c15c1f --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/SankeyChart.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import getNewestMonth from '../../../../../helpers/get-newest-month'; +import { getActivityDetails } from '../../../../../api/repo'; + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface SankeyChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const SankeyChart = (props: SankeyChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const [data, setData] = useState<{ [repoName: string]: RepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'item', + triggerOn: 'mousemove', + }, + // legend: { + // type: 'scroll', + // }, + animation: false, + grid: { + left: '2%', + right: '10%', + bottom: '3%', + // containLabel: true, + }, + series: [ + { + type: 'sankey', + // bottom: '10%', + emphasis: { + focus: 'adjacency', + }, + data: lastMonthData(data).nodes, + links: lastMonthData(data).links, + // orient: "vertical", + // label: { + // position: "top" + // }, + lineStyle: { + color: 'source', + curveness: 0.5, + }, + }, + ], + }; + + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + //getStars() to fetch repository data + const starsData = await getActivityDetails(repo); + // Update Data/ + setData((prevData) => ({ ...prevData, [repo]: starsData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + // If the retrieval fails, set the data to an empty object + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + // console.log('sankeydata', data); + // console.log('lastMonthData', lastMonthData(data)); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + // seriesIndex: Number(currentRepo), + // dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +interface RepoData { + [month: string]: [string, number][]; +} + +interface DataNode { + name: string; +} + +interface DataLink { + source: string; + target: string; + value: number; +} + +interface LastMonthData { + nodes: DataNode[]; + links: DataLink[]; +} + +function lastMonthData(repo_data: { + [repoName: string]: RepoData; +}): LastMonthData { + const data: LastMonthData = { + nodes: [], + links: [], + }; + const userSet = new Set(); + const newestMonth = getNewestMonth(); + for (const [repoName, repoData] of Object.entries(repo_data)) { + const monthData = repoData[newestMonth]; + if (monthData) { + monthData.forEach(([userName, value]) => { + const link: DataLink = { + source: repoName, + target: userName, + value: value, + }; + userSet.add(userName); + data.links.push(link); + }); + } + } + data.nodes = [ + ...Object.keys(repo_data).map((repoName) => ({ name: repoName })), + ...Array.from(userSet).map((userName) => ({ name: userName })), + ]; + // console.log(data.nodes); + // console.log(data.links); + return data; +} + +export default SankeyChart; diff --git a/src/pages/ContentScripts/features/repo-collection/charts/StackedBarChart.tsx b/src/pages/ContentScripts/features/repo-collection/charts/StackedBarChart.tsx new file mode 100644 index 00000000..1ca420dd --- /dev/null +++ b/src/pages/ContentScripts/features/repo-collection/charts/StackedBarChart.tsx @@ -0,0 +1,135 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as echarts from 'echarts'; +import generateDataByMonth from '../../../../../helpers/generate-data-by-month'; +import { getStars } from '../../../../../api/repo'; + +interface RawRepoData { + [date: string]: number; +} + +const LIGHT_THEME = { + FG_COLOR: '#24292f', + BG_COLOR: '#ffffff', + PALLET: ['#5470c6', '#91cc75'], +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + PALLET: ['#58a6ff', '#3fb950'], +}; + +interface StackedBarChartProps { + theme: 'light' | 'dark'; + height: number; + repoNames: string[]; + + currentRepo?: string; +} + +const StackedBarChart = (props: StackedBarChartProps): JSX.Element => { + const { theme, height, repoNames, currentRepo } = props; + const divEL = useRef(null); + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + const [data, setData] = useState<{ [repo: string]: RawRepoData }>({}); + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'axis', + }, + legend: { + type: 'scroll', + }, + grid: { + left: '5%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'time', + splitLine: { + show: false, + }, + axisLabel: { + color: TH.FG_COLOR, + formatter: { + year: '{yearStyle|{yy}}', + month: '{MMM}', + }, + rich: { + yearStyle: { + fontWeight: 'bold', + }, + }, + }, + }, + yAxis: { + type: 'value', + }, + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 3600 * 24 * 1000 * 180, + }, + ], + series: StarSeries(data), + }; + console.log('BarChartSeries??', StarSeries(data)); + useEffect(() => { + const fetchData = async () => { + for (const repo of repoNames) { + try { + const StarData = await getStars(repo); + setData((prevData) => ({ ...prevData, [repo]: StarData })); + } catch (error) { + console.error(`Error fetching stars data for ${repo}:`, error); + + setData((prevData) => ({ ...prevData, [repo]: {} })); + } + } + }; + fetchData(); + }, []); + // console.log('datatest', data); + useEffect(() => { + let chartDOM = divEL.current; + const TH = 'light' ? LIGHT_THEME : DARK_THEME; + + const instance = echarts.init(chartDOM as any); + instance.setOption(option); + instance.dispatchAction({ + type: 'highlight', + seriesIndex: Number(currentRepo), + dataIndex: Number(currentRepo), + name: repoNames[Number(currentRepo)], + seriesName: repoNames[Number(currentRepo)], + }); + return () => { + instance.dispose(); + }; + }, [data, currentRepo]); + + return
; +}; + +//Series:各仓库代码增加行数 +const StarSeries = (data: { + [repo: string]: RawRepoData; +}): echarts.SeriesOption[] => + Object.entries(data).map(([repoName, repoData]) => ({ + name: repoName, + type: 'bar', + stack: 'total', + // emphasis: emphasisStyle, + data: generateDataByMonth(repoData), + emphasis: { + focus: 'series', + }, + // yAxisIndex: 0, + triggerLineEvent: true, + })); + +export default StackedBarChart;