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> (cherry picked from commit 875f5ea6d83c8371f309df99654ca3556623004c)
This commit is contained in:
		
					parent
					
						
							
								fc384e494e
							
						
					
				
			
			
				commit
				
					
						f097799953
					
				
			
		
					 13 changed files with 277 additions and 32 deletions
				
			
		| 
						 | 
				
			
			@ -1962,6 +1962,7 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
 | 
			
		|||
activity = Activity
 | 
			
		||||
activity.navbar.pulse = Pulse
 | 
			
		||||
activity.navbar.contributors = Contributors
 | 
			
		||||
activity.navbar.code_frequency = Code Frequency
 | 
			
		||||
activity.period.filter_label = Period:
 | 
			
		||||
activity.period.daily = 1 day
 | 
			
		||||
activity.period.halfweekly = 3 days
 | 
			
		||||
| 
						 | 
				
			
			@ -2656,6 +2657,7 @@ component_loading = Loading %s...
 | 
			
		|||
component_loading_failed = Could not load %s
 | 
			
		||||
component_loading_info = This might take a bit…
 | 
			
		||||
component_failed_to_load = An unexpected error happened.
 | 
			
		||||
code_frequency.what = code frequency
 | 
			
		||||
contributors.what = contributions
 | 
			
		||||
 | 
			
		||||
[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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1448,6 +1448,10 @@ func registerRoutes(m *web.Route) {
 | 
			
		|||
				m.Get("", repo.Contributors)
 | 
			
		||||
				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))
 | 
			
		||||
 | 
			
		||||
		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 {
 | 
			
		||||
			_ = stdoutWriter.Close()
 | 
			
		||||
			scanner := bufio.NewScanner(stdoutReader)
 | 
			
		||||
			scanner.Split(bufio.ScanLines)
 | 
			
		||||
 | 
			
		||||
			for scanner.Scan() {
 | 
			
		||||
				line := strings.TrimSpace(scanner.Text())
 | 
			
		||||
| 
						 | 
				
			
			@ -180,7 +179,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
				commitStats.Total = commitStats.Additions + commitStats.Deletions
 | 
			
		||||
				scanner.Scan()
 | 
			
		||||
				scanner.Text() // empty line at the end
 | 
			
		||||
 | 
			
		||||
				res := &ExtendedCommitStats{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@
 | 
			
		|||
		<div class="flex-container-main">
 | 
			
		||||
			{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
 | 
			
		||||
			{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
 | 
			
		||||
			{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
 | 
			
		||||
		</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">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
 | 
			
		||||
	</a>
 | 
			
		||||
	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
 | 
			
		||||
		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
 | 
			
		||||
	</a>
 | 
			
		||||
</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 {
 | 
			
		||||
  Chart,
 | 
			
		||||
  Title,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Legend,
 | 
			
		||||
  BarElement,
 | 
			
		||||
  CategoryScale,
 | 
			
		||||
  LinearScale,
 | 
			
		||||
  TimeScale,
 | 
			
		||||
  PointElement,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,27 +18,13 @@ import {
 | 
			
		|||
  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';
 | 
			
		||||
import $ from 'jquery';
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
  id: 'customEventListener',
 | 
			
		||||
  afterEvent: (chart, args, opts) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,17 +37,14 @@ const customEventListener = {
 | 
			
		|||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Chart.defaults.color = colors.text;
 | 
			
		||||
Chart.defaults.borderColor = colors.border;
 | 
			
		||||
Chart.defaults.color = chartJsColors.text;
 | 
			
		||||
Chart.defaults.borderColor = chartJsColors.border;
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
  TimeScale,
 | 
			
		||||
  CategoryScale,
 | 
			
		||||
  LinearScale,
 | 
			
		||||
  BarElement,
 | 
			
		||||
  Title,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Legend,
 | 
			
		||||
  PointElement,
 | 
			
		||||
  LineElement,
 | 
			
		||||
  Filler,
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +102,7 @@ export default {
 | 
			
		|||
        do {
 | 
			
		||||
          response = await GET(`${this.repoLink}/activity/contributors/data`);
 | 
			
		||||
          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);
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,7 +202,7 @@ export default {
 | 
			
		|||
            pointRadius: 0,
 | 
			
		||||
            pointHitRadius: 0,
 | 
			
		||||
            fill: 'start',
 | 
			
		||||
            backgroundColor: colors[this.type],
 | 
			
		||||
            backgroundColor: chartJsColors[this.type],
 | 
			
		||||
            borderWidth: 0,
 | 
			
		||||
            tension: 0.3,
 | 
			
		||||
          },
 | 
			
		||||
| 
						 | 
				
			
			@ -254,7 +234,6 @@ export default {
 | 
			
		|||
          title: {
 | 
			
		||||
            display: type === 'main',
 | 
			
		||||
            text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
 | 
			
		||||
            color: colors.title,
 | 
			
		||||
            position: 'top',
 | 
			
		||||
            align: 'center',
 | 
			
		||||
          },
 | 
			
		||||
| 
						 | 
				
			
			@ -262,9 +241,6 @@ export default {
 | 
			
		|||
            chartType: type,
 | 
			
		||||
            instance: this,
 | 
			
		||||
          },
 | 
			
		||||
          legend: {
 | 
			
		||||
            display: false,
 | 
			
		||||
          },
 | 
			
		||||
          zoom: {
 | 
			
		||||
            pan: {
 | 
			
		||||
              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');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +84,7 @@ import {onDomReady} from './utils/dom.js';
 | 
			
		|||
import {initRepoIssueList} from './features/repo-issue-list.js';
 | 
			
		||||
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
 | 
			
		||||
import {initRepoContributors} from './features/contributors.js';
 | 
			
		||||
import {initRepoCodeFrequency} from './features/code-frequency.js';
 | 
			
		||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
 | 
			
		||||
import {initDirAuto} from './modules/dirauto.js';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +175,7 @@ onDomReady(() => {
 | 
			
		|||
  initRepository();
 | 
			
		||||
  initRepositoryActionView();
 | 
			
		||||
  initRepoContributors();
 | 
			
		||||
  initRepoCodeFrequency();
 | 
			
		||||
 | 
			
		||||
  initCommitStatuses();
 | 
			
		||||
  initCaptcha();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,3 +139,5 @@ export function parseDom(text, contentType) {
 | 
			
		|||
export function serializeXml(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) {
 | 
			
		||||
  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',
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue