mirror of https://github.com/go-gitea/gitea.git
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:
parent
6f6120dfa8
commit
875f5ea6d8
|
@ -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]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue