mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-25 04:54:35 +00:00
refactor: replace vue-bar-graph dependency with inlined SVG chart (#38292)
Inlines the small SVG bar graph into `RepoActivityTopAuthors.vue` (its only consumer) and drops the `vue-bar-graph` npm dependency. - Bars render at static height (dropped the grow animation). - Theme-aware axis color instead of a hardcoded `#555555`. - Removed the dangling `role="img"`/`aria-labelledby` on the `<svg>`. - Reserve the chart height so the page does not shift when the component mounts. <img width="416" height="110" alt="Screenshot 2026-07-01 at 11 15 25" src="https://github.com/user-attachments/assets/b2db4d0c-20f1-4345-9951-32a908abfaba" /> <img width="419" height="110" alt="Screenshot 2026-07-01 at 11 15 35" src="https://github.com/user-attachments/assets/853305a5-575f-4a26-ba3b-12fc51081324" /> fyi @lafriks --------- Signed-off-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import {VueBarGraph} from 'vue-bar-graph';
|
||||
import {computed, onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
||||
import {onMounted, shallowRef, useTemplateRef, type ShallowRef} from 'vue';
|
||||
|
||||
const barSlotWidth = 40; // horizontal space allotted per author
|
||||
const chartHeight = 100; // keep in sync with reserved height in template
|
||||
const innerChartHeight = chartHeight - 28; // 28 = avatar/x-axis label row (20) + 8px padding
|
||||
const barMidPoint = barSlotWidth / 2;
|
||||
const barWidth = barSlotWidth - 2; // 2px gap between bars
|
||||
const avatarSize = 20;
|
||||
const labelInsideThreshold = 22; // bars at least this tall carry the commit count inside them
|
||||
|
||||
const colors = shallowRef({
|
||||
barColor: 'green',
|
||||
@@ -18,26 +25,19 @@ type ActivityAuthorData = {
|
||||
|
||||
const activityTopAuthors: Array<ActivityAuthorData> = window.config.pageData.repoActivityTopAuthors || [];
|
||||
|
||||
const graphPoints = computed(() => {
|
||||
return activityTopAuthors.map((item) => {
|
||||
return {
|
||||
value: item.commits,
|
||||
label: item.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
const graphWidth = activityTopAuthors.length * barSlotWidth;
|
||||
const maxCommits = Math.max(...activityTopAuthors.map((author) => author.commits));
|
||||
|
||||
const graphAuthors = computed(() => {
|
||||
return activityTopAuthors.map((item, idx: number) => {
|
||||
return {
|
||||
position: idx + 1,
|
||||
...item,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const graphWidth = computed(() => {
|
||||
return activityTopAuthors.length * 40;
|
||||
const bars = activityTopAuthors.map((author, index) => {
|
||||
const height = author.commits / maxCommits * innerChartHeight;
|
||||
return {
|
||||
author,
|
||||
index,
|
||||
x: index * barSlotWidth,
|
||||
height,
|
||||
yOffset: innerChartHeight - height,
|
||||
labelInside: height >= labelInsideThreshold,
|
||||
};
|
||||
});
|
||||
|
||||
const styleElement = useTemplateRef('styleElement') as Readonly<ShallowRef<HTMLDivElement>>;
|
||||
@@ -59,49 +59,34 @@ onMounted(() => {
|
||||
<div>
|
||||
<div class="activity-bar-graph tw-w-0 tw-h-0" ref="styleElement"/>
|
||||
<div class="activity-bar-graph-alt tw-w-0 tw-h-0" ref="altStyleElement"/>
|
||||
<vue-bar-graph
|
||||
:points="graphPoints"
|
||||
:show-x-axis="true"
|
||||
:show-y-axis="false"
|
||||
:show-values="true"
|
||||
:width="graphWidth"
|
||||
:bar-color="colors.barColor"
|
||||
:text-color="colors.textColor"
|
||||
:text-alt-color="colors.textAltColor"
|
||||
:height="100"
|
||||
:label-height="20"
|
||||
>
|
||||
<template #label="opt">
|
||||
<g v-for="(author, idx) in graphAuthors" :key="author.position">
|
||||
<a
|
||||
v-if="opt.bar.index === idx && author.home_link"
|
||||
:href="author.home_link"
|
||||
>
|
||||
<image
|
||||
:x="`${opt.bar.midPoint - 10}px`"
|
||||
:y="`${opt.bar.yLabel}px`"
|
||||
height="20"
|
||||
width="20"
|
||||
:href="author.avatar_link"
|
||||
/>
|
||||
</a>
|
||||
<image
|
||||
v-else-if="opt.bar.index === idx"
|
||||
:x="`${opt.bar.midPoint - 10}px`"
|
||||
:y="`${opt.bar.yLabel}px`"
|
||||
height="20"
|
||||
width="20"
|
||||
:href="author.avatar_link"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
<template #title="opt">
|
||||
<tspan v-for="(author, idx) in graphAuthors" :key="author.position">
|
||||
<tspan v-if="opt.bar.index === idx">
|
||||
{{ author.name }}
|
||||
</tspan>
|
||||
</tspan>
|
||||
</template>
|
||||
</vue-bar-graph>
|
||||
<svg :width="graphWidth" :height="chartHeight">
|
||||
<g v-for="bar in bars" :key="bar.index" :transform="`translate(${bar.x},0)`">
|
||||
<title>{{ bar.author.name }}</title>
|
||||
<rect :width="barWidth" :height="bar.height" :x="2" :y="bar.yOffset" :style="{fill: colors.barColor}"/>
|
||||
<text
|
||||
:x="barMidPoint"
|
||||
:y="bar.yOffset"
|
||||
:dy="bar.labelInside ? '15px' : '-5px'"
|
||||
text-anchor="middle"
|
||||
:style="{fill: bar.labelInside ? colors.textAltColor : colors.textColor, font: '10px sans-serif'}"
|
||||
>{{ bar.author.commits }}</text>
|
||||
<a v-if="bar.author.home_link" :href="bar.author.home_link">
|
||||
<image :x="barMidPoint - avatarSize / 2" :y="innerChartHeight + 4" :height="avatarSize" :width="avatarSize" :href="bar.author.avatar_link"/>
|
||||
</a>
|
||||
<image v-else :x="barMidPoint - avatarSize / 2" :y="innerChartHeight + 4" :height="avatarSize" :width="avatarSize" :href="bar.author.avatar_link"/>
|
||||
<line class="axis-line" :x1="barMidPoint" :x2="barMidPoint" :y1="innerChartHeight + 3" :y2="innerChartHeight"/>
|
||||
</g>
|
||||
<line class="axis-line" :x1="2" :x2="graphWidth" :y1="innerChartHeight" :y2="innerChartHeight"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: block; /* avoid the inline-baseline gap so the reserved container height matches exactly */
|
||||
}
|
||||
.axis-line {
|
||||
stroke: var(--color-secondary-alpha-60);
|
||||
stroke-width: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user