mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +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:
		| @@ -1919,6 +1919,7 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend | |||||||
| activity = Activity | activity = Activity | ||||||
| activity.navbar.pulse = Pulse | activity.navbar.pulse = Pulse | ||||||
| activity.navbar.contributors = Contributors | activity.navbar.contributors = Contributors | ||||||
|  | activity.navbar.code_frequency = Code Frequency | ||||||
| activity.period.filter_label = Period: | activity.period.filter_label = Period: | ||||||
| activity.period.daily = 1 day | activity.period.daily = 1 day | ||||||
| activity.period.halfweekly = 3 days | activity.period.halfweekly = 3 days | ||||||
| @@ -2597,6 +2598,7 @@ component_loading = Loading %s... | |||||||
| component_loading_failed = Could not load %s | component_loading_failed = Could not load %s | ||||||
| component_loading_info = This might take a bit… | component_loading_info = This might take a bit… | ||||||
| component_failed_to_load = An unexpected error happened. | component_failed_to_load = An unexpected error happened. | ||||||
|  | code_frequency.what = code frequency | ||||||
| contributors.what = contributions | contributors.what = contributions | ||||||
|  |  | ||||||
| [org] | [org] | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								routers/web/repo/code_frequency.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								routers/web/repo/code_frequency.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	contributors_service "code.gitea.io/gitea/services/repository" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	tplCodeFrequency base.TplName = "repo/activity" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // CodeFrequency renders the page to show repository code frequency | ||||||
|  | func CodeFrequency(ctx *context.Context) { | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency") | ||||||
|  |  | ||||||
|  | 	ctx.Data["PageIsActivity"] = true | ||||||
|  | 	ctx.Data["PageIsCodeFrequency"] = true | ||||||
|  | 	ctx.PageData["repoLink"] = ctx.Repo.RepoLink | ||||||
|  |  | ||||||
|  | 	ctx.HTML(http.StatusOK, tplCodeFrequency) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CodeFrequencyData returns JSON of code frequency data | ||||||
|  | func CodeFrequencyData(ctx *context.Context) { | ||||||
|  | 	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { | ||||||
|  | 		if errors.Is(err, contributors_service.ErrAwaitGeneration) { | ||||||
|  | 			ctx.Status(http.StatusAccepted) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.ServerError("GetCodeFrequencyData", err) | ||||||
|  | 	} else { | ||||||
|  | 		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1403,6 +1403,10 @@ func registerRoutes(m *web.Route) { | |||||||
| 				m.Get("", repo.Contributors) | 				m.Get("", repo.Contributors) | ||||||
| 				m.Get("/data", repo.ContributorsData) | 				m.Get("/data", repo.ContributorsData) | ||||||
| 			}) | 			}) | ||||||
|  | 			m.Group("/code-frequency", func() { | ||||||
|  | 				m.Get("", repo.CodeFrequency) | ||||||
|  | 				m.Get("/data", repo.CodeFrequencyData) | ||||||
|  | 			}) | ||||||
| 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) | 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) | ||||||
|  |  | ||||||
| 		m.Group("/activity_author_data", func() { | 		m.Group("/activity_author_data", func() { | ||||||
|   | |||||||
| @@ -143,7 +143,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int | |||||||
| 		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { | 		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { | ||||||
| 			_ = stdoutWriter.Close() | 			_ = stdoutWriter.Close() | ||||||
| 			scanner := bufio.NewScanner(stdoutReader) | 			scanner := bufio.NewScanner(stdoutReader) | ||||||
| 			scanner.Split(bufio.ScanLines) |  | ||||||
|  |  | ||||||
| 			for scanner.Scan() { | 			for scanner.Scan() { | ||||||
| 				line := strings.TrimSpace(scanner.Text()) | 				line := strings.TrimSpace(scanner.Text()) | ||||||
| @@ -180,7 +179,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 				commitStats.Total = commitStats.Additions + commitStats.Deletions | 				commitStats.Total = commitStats.Additions + commitStats.Deletions | ||||||
| 				scanner.Scan() |  | ||||||
| 				scanner.Text() // empty line at the end | 				scanner.Text() // empty line at the end | ||||||
|  |  | ||||||
| 				res := &ExtendedCommitStats{ | 				res := &ExtendedCommitStats{ | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| 		<div class="flex-container-main"> | 		<div class="flex-container-main"> | ||||||
| 			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} | 			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} | ||||||
| 			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} | 			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} | ||||||
|  | 			{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}} | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								templates/repo/code_frequency.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/repo/code_frequency.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | {{if .Permission.CanRead $.UnitTypeCode}} | ||||||
|  | 	<div id="repo-code-frequency-chart" | ||||||
|  | 		data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}" | ||||||
|  | 		data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.what")}}" | ||||||
|  | 		data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}" | ||||||
|  | 		data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}" | ||||||
|  | 	> | ||||||
|  | 	</div> | ||||||
|  | {{end}} | ||||||
| @@ -5,4 +5,7 @@ | |||||||
| 	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> | 	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> | ||||||
| 		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}} | 		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}} | ||||||
| 	</a> | 	</a> | ||||||
|  | 	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> | ||||||
|  | 		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} | ||||||
|  | 	</a> | ||||||
| </div> | </div> | ||||||
|   | |||||||
							
								
								
									
										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> | ||||||
| @@ -3,10 +3,7 @@ import {SvgIcon} from '../svg.js'; | |||||||
| import { | import { | ||||||
|   Chart, |   Chart, | ||||||
|   Title, |   Title, | ||||||
|   Tooltip, |  | ||||||
|   Legend, |  | ||||||
|   BarElement, |   BarElement, | ||||||
|   CategoryScale, |  | ||||||
|   LinearScale, |   LinearScale, | ||||||
|   TimeScale, |   TimeScale, | ||||||
|   PointElement, |   PointElement, | ||||||
| @@ -21,27 +18,13 @@ import { | |||||||
|   firstStartDateAfterDate, |   firstStartDateAfterDate, | ||||||
|   fillEmptyStartDaysWithZeroes, |   fillEmptyStartDaysWithZeroes, | ||||||
| } from '../utils/time.js'; | } 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'; | import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; | ||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
|  |  | ||||||
| const {pageData} = window.config; | const {pageData} = window.config; | ||||||
|  |  | ||||||
| const colors = { |  | ||||||
|   text: '--color-text', |  | ||||||
|   border: '--color-secondary-alpha-60', |  | ||||||
|   commits: '--color-primary-alpha-60', |  | ||||||
|   additions: '--color-green', |  | ||||||
|   deletions: '--color-red', |  | ||||||
|   title: '--color-secondary-dark-4', |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const styles = window.getComputedStyle(document.documentElement); |  | ||||||
| const getColor = (name) => styles.getPropertyValue(name).trim(); |  | ||||||
|  |  | ||||||
| for (const [key, value] of Object.entries(colors)) { |  | ||||||
|   colors[key] = getColor(value); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const customEventListener = { | const customEventListener = { | ||||||
|   id: 'customEventListener', |   id: 'customEventListener', | ||||||
|   afterEvent: (chart, args, opts) => { |   afterEvent: (chart, args, opts) => { | ||||||
| @@ -54,17 +37,14 @@ const customEventListener = { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| Chart.defaults.color = colors.text; | Chart.defaults.color = chartJsColors.text; | ||||||
| Chart.defaults.borderColor = colors.border; | Chart.defaults.borderColor = chartJsColors.border; | ||||||
|  |  | ||||||
| Chart.register( | Chart.register( | ||||||
|   TimeScale, |   TimeScale, | ||||||
|   CategoryScale, |  | ||||||
|   LinearScale, |   LinearScale, | ||||||
|   BarElement, |   BarElement, | ||||||
|   Title, |   Title, | ||||||
|   Tooltip, |  | ||||||
|   Legend, |  | ||||||
|   PointElement, |   PointElement, | ||||||
|   LineElement, |   LineElement, | ||||||
|   Filler, |   Filler, | ||||||
| @@ -122,7 +102,7 @@ export default { | |||||||
|         do { |         do { | ||||||
|           response = await GET(`${this.repoLink}/activity/contributors/data`); |           response = await GET(`${this.repoLink}/activity/contributors/data`); | ||||||
|           if (response.status === 202) { |           if (response.status === 202) { | ||||||
|             await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying |             await sleep(1000); // wait for 1 second before retrying | ||||||
|           } |           } | ||||||
|         } while (response.status === 202); |         } while (response.status === 202); | ||||||
|         if (response.ok) { |         if (response.ok) { | ||||||
| @@ -222,7 +202,7 @@ export default { | |||||||
|             pointRadius: 0, |             pointRadius: 0, | ||||||
|             pointHitRadius: 0, |             pointHitRadius: 0, | ||||||
|             fill: 'start', |             fill: 'start', | ||||||
|             backgroundColor: colors[this.type], |             backgroundColor: chartJsColors[this.type], | ||||||
|             borderWidth: 0, |             borderWidth: 0, | ||||||
|             tension: 0.3, |             tension: 0.3, | ||||||
|           }, |           }, | ||||||
| @@ -254,7 +234,6 @@ export default { | |||||||
|           title: { |           title: { | ||||||
|             display: type === 'main', |             display: type === 'main', | ||||||
|             text: 'drag: zoom, shift+drag: pan, double click: reset zoom', |             text: 'drag: zoom, shift+drag: pan, double click: reset zoom', | ||||||
|             color: colors.title, |  | ||||||
|             position: 'top', |             position: 'top', | ||||||
|             align: 'center', |             align: 'center', | ||||||
|           }, |           }, | ||||||
| @@ -262,9 +241,6 @@ export default { | |||||||
|             chartType: type, |             chartType: type, | ||||||
|             instance: this, |             instance: this, | ||||||
|           }, |           }, | ||||||
|           legend: { |  | ||||||
|             display: false, |  | ||||||
|           }, |  | ||||||
|           zoom: { |           zoom: { | ||||||
|             pan: { |             pan: { | ||||||
|               enabled: true, |               enabled: true, | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								web_src/js/features/code-frequency.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web_src/js/features/code-frequency.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import {createApp} from 'vue'; | ||||||
|  |  | ||||||
|  | export async function initRepoCodeFrequency() { | ||||||
|  |   const el = document.getElementById('repo-code-frequency-chart'); | ||||||
|  |   if (!el) return; | ||||||
|  |  | ||||||
|  |   const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue'); | ||||||
|  |   try { | ||||||
|  |     const View = createApp(RepoCodeFrequency, { | ||||||
|  |       locale: { | ||||||
|  |         loadingTitle: el.getAttribute('data-locale-loading-title'), | ||||||
|  |         loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'), | ||||||
|  |         loadingInfo: el.getAttribute('data-locale-loading-info'), | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     View.mount(el); | ||||||
|  |   } catch (err) { | ||||||
|  |     console.error('RepoCodeFrequency failed to load', err); | ||||||
|  |     el.textContent = el.getAttribute('data-locale-component-failed-to-load'); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -87,6 +87,7 @@ import {onDomReady} from './utils/dom.js'; | |||||||
| import {initRepoIssueList} from './features/repo-issue-list.js'; | import {initRepoIssueList} from './features/repo-issue-list.js'; | ||||||
| import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; | import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; | ||||||
| import {initRepoContributors} from './features/contributors.js'; | import {initRepoContributors} from './features/contributors.js'; | ||||||
|  | import {initRepoCodeFrequency} from './features/code-frequency.js'; | ||||||
| import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; | import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; | ||||||
| import {initDirAuto} from './modules/dirauto.js'; | import {initDirAuto} from './modules/dirauto.js'; | ||||||
|  |  | ||||||
| @@ -177,6 +178,7 @@ onDomReady(() => { | |||||||
|   initRepository(); |   initRepository(); | ||||||
|   initRepositoryActionView(); |   initRepositoryActionView(); | ||||||
|   initRepoContributors(); |   initRepoContributors(); | ||||||
|  |   initRepoCodeFrequency(); | ||||||
|  |  | ||||||
|   initCommitStatuses(); |   initCommitStatuses(); | ||||||
|   initCaptcha(); |   initCaptcha(); | ||||||
|   | |||||||
| @@ -139,3 +139,5 @@ export function parseDom(text, contentType) { | |||||||
| export function serializeXml(node) { | export function serializeXml(node) { | ||||||
|   return xmlSerializer.serializeToString(node); |   return xmlSerializer.serializeToString(node); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||||||
|   | |||||||
| @@ -19,3 +19,17 @@ function getLuminance(r, g, b) { | |||||||
| export function useLightTextOnBackground(r, g, b) { | export function useLightTextOnBackground(r, g, b) { | ||||||
|   return getLuminance(r, g, b) < 0.453; |   return getLuminance(r, g, b) < 0.453; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function resolveColors(obj) { | ||||||
|  |   const styles = window.getComputedStyle(document.documentElement); | ||||||
|  |   const getColor = (name) => styles.getPropertyValue(name).trim(); | ||||||
|  |   return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const chartJsColors = resolveColors({ | ||||||
|  |   text: '--color-text', | ||||||
|  |   border: '--color-secondary-alpha-60', | ||||||
|  |   commits: '--color-primary-alpha-60', | ||||||
|  |   additions: '--color-green', | ||||||
|  |   deletions: '--color-red', | ||||||
|  | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Şahin Akkaya
					Şahin Akkaya