mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Implement code frequency graph (#29191)
### Overview This is the implementation of Code Frequency page. This feature was mentioned on these issues: #18262, #7392. It adds another tab to Activity page called Code Frequency. Code Frequency tab shows additions and deletions over time since the repository existed. Before: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/2603504f-aee7-4929-a8c4-fb3412a7a0f6"> After: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/58c03721-729f-4536-a663-9f337f240963"> --- #### Features - See additions deletions over time since repository existed - Click on "Additions" or "Deletions" legend to show only one type of contribution - Use the same cache from Contributors page so that the loading of data will be fast once it is cached by visiting either one of the pages --------- Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		
							
								
								
									
										172
									
								
								web_src/js/components/RepoCodeFrequency.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								web_src/js/components/RepoCodeFrequency.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| <script> | ||||
| import {SvgIcon} from '../svg.js'; | ||||
| import { | ||||
|   Chart, | ||||
|   Legend, | ||||
|   LinearScale, | ||||
|   TimeScale, | ||||
|   PointElement, | ||||
|   LineElement, | ||||
|   Filler, | ||||
| } from 'chart.js'; | ||||
| import {GET} from '../modules/fetch.js'; | ||||
| import {Line as ChartLine} from 'vue-chartjs'; | ||||
| import { | ||||
|   startDaysBetween, | ||||
|   firstStartDateAfterDate, | ||||
|   fillEmptyStartDaysWithZeroes, | ||||
| } from '../utils/time.js'; | ||||
| import {chartJsColors} from '../utils/color.js'; | ||||
| import {sleep} from '../utils.js'; | ||||
| import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; | ||||
|  | ||||
| const {pageData} = window.config; | ||||
|  | ||||
| Chart.defaults.color = chartJsColors.text; | ||||
| Chart.defaults.borderColor = chartJsColors.border; | ||||
|  | ||||
| Chart.register( | ||||
|   TimeScale, | ||||
|   LinearScale, | ||||
|   Legend, | ||||
|   PointElement, | ||||
|   LineElement, | ||||
|   Filler, | ||||
| ); | ||||
|  | ||||
| export default { | ||||
|   components: {ChartLine, SvgIcon}, | ||||
|   props: { | ||||
|     locale: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     isLoading: false, | ||||
|     errorText: '', | ||||
|     repoLink: pageData.repoLink || [], | ||||
|     data: [], | ||||
|   }), | ||||
|   mounted() { | ||||
|     this.fetchGraphData(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async fetchGraphData() { | ||||
|       this.isLoading = true; | ||||
|       try { | ||||
|         let response; | ||||
|         do { | ||||
|           response = await GET(`${this.repoLink}/activity/code-frequency/data`); | ||||
|           if (response.status === 202) { | ||||
|             await sleep(1000); // wait for 1 second before retrying | ||||
|           } | ||||
|         } while (response.status === 202); | ||||
|         if (response.ok) { | ||||
|           this.data = await response.json(); | ||||
|           const weekValues = Object.values(this.data); | ||||
|           const start = weekValues[0].week; | ||||
|           const end = firstStartDateAfterDate(new Date()); | ||||
|           const startDays = startDaysBetween(new Date(start), new Date(end)); | ||||
|           this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); | ||||
|           this.errorText = ''; | ||||
|         } else { | ||||
|           this.errorText = response.statusText; | ||||
|         } | ||||
|       } catch (err) { | ||||
|         this.errorText = err.message; | ||||
|       } finally { | ||||
|         this.isLoading = false; | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     toGraphData(data) { | ||||
|       return { | ||||
|         datasets: [ | ||||
|           { | ||||
|             data: data.map((i) => ({x: i.week, y: i.additions})), | ||||
|             pointRadius: 0, | ||||
|             pointHitRadius: 0, | ||||
|             fill: true, | ||||
|             label: 'Additions', | ||||
|             backgroundColor: chartJsColors['additions'], | ||||
|             borderWidth: 0, | ||||
|             tension: 0.3, | ||||
|           }, | ||||
|           { | ||||
|             data: data.map((i) => ({x: i.week, y: -i.deletions})), | ||||
|             pointRadius: 0, | ||||
|             pointHitRadius: 0, | ||||
|             fill: true, | ||||
|             label: 'Deletions', | ||||
|             backgroundColor: chartJsColors['deletions'], | ||||
|             borderWidth: 0, | ||||
|             tension: 0.3, | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     getOptions() { | ||||
|       return { | ||||
|         responsive: true, | ||||
|         maintainAspectRatio: false, | ||||
|         animation: true, | ||||
|         plugins: { | ||||
|           legend: { | ||||
|             display: true, | ||||
|           }, | ||||
|         }, | ||||
|         scales: { | ||||
|           x: { | ||||
|             type: 'time', | ||||
|             grid: { | ||||
|               display: false, | ||||
|             }, | ||||
|             time: { | ||||
|               minUnit: 'month', | ||||
|             }, | ||||
|             ticks: { | ||||
|               maxRotation: 0, | ||||
|               maxTicksLimit: 12 | ||||
|             }, | ||||
|           }, | ||||
|           y: { | ||||
|             ticks: { | ||||
|               maxTicksLimit: 6 | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="ui header gt-df gt-ac gt-sb"> | ||||
|       {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }} | ||||
|     </div> | ||||
|     <div class="gt-df ui segment main-graph"> | ||||
|       <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto"> | ||||
|         <div v-if="isLoading"> | ||||
|           <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/> | ||||
|           {{ locale.loadingInfo }} | ||||
|         </div> | ||||
|         <div v-else class="text red"> | ||||
|           <SvgIcon name="octicon-x-circle-fill"/> | ||||
|           {{ errorText }} | ||||
|         </div> | ||||
|       </div> | ||||
|       <ChartLine | ||||
|         v-memo="data" v-if="data.length !== 0" | ||||
|         :data="toGraphData(data)" :options="getOptions()" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <style scoped> | ||||
| .main-graph { | ||||
|   height: 440px; | ||||
| } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user