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:
silverwind
2026-07-01 13:17:23 +02:00
committed by GitHub
parent 67a6bd7fc0
commit e8654c7e06
5 changed files with 51 additions and 97 deletions

View File

@@ -70,7 +70,6 @@
"vite": "8.1.0",
"vite-string-plugin": "2.0.4",
"vue": "3.5.38",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3"
},
"devDependencies": {

12
pnpm-lock.yaml generated
View File

@@ -200,9 +200,6 @@ importers:
vue:
specifier: 3.5.38
version: 3.5.38(typescript@6.0.3)
vue-bar-graph:
specifier: 2.2.0
version: 2.2.0(typescript@6.0.3)
vue-chartjs:
specifier: 5.3.3
version: 5.3.3(chart.js@4.5.1)(vue@3.5.38(typescript@6.0.3))
@@ -4306,9 +4303,6 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-bar-graph@2.2.0:
resolution: {integrity: sha512-1xFPho2nM6nFDziExLu48vKO+Q90gjxz1NyHfc+MhgfYDSxR9BMyhOIXUO5EmwKIVEX5dBoP2n3Ius8SjKRD4g==}
vue-chartjs@5.3.3:
resolution: {integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==}
peerDependencies:
@@ -8979,12 +8973,6 @@ snapshots:
vscode-uri@3.1.0: {}
vue-bar-graph@2.2.0(typescript@6.0.3):
dependencies:
vue: 3.5.38(typescript@6.0.3)
transitivePeerDependencies:
- typescript
vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.38(typescript@6.0.3)):
dependencies:
chart.js: 4.5.1

View File

@@ -105,7 +105,7 @@
<strong class="tw-text-red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
</div>
<div class="ui attached segment">
<div id="repo-activity-top-authors-chart"></div>
<div id="repo-activity-top-authors-chart" class="tw-h-[100px]"></div>
</div>
</div>
{{end}}

18
types.d.ts vendored
View File

@@ -47,21 +47,3 @@ declare module '@citation-js/core' {
declare module '@citation-js/plugin-software-formats' {}
declare module '@citation-js/plugin-bibtex' {}
declare module '@citation-js/plugin-csl' {}
declare module 'vue-bar-graph' {
import type {DefineComponent} from 'vue';
interface BarGraphPoint {
value: number;
label: string;
}
export const VueBarGraph: DefineComponent<{
points?: Array<BarGraphPoint>;
barColor?: string;
textColor?: string;
textAltColor?: string;
height?: number;
labelHeight?: number;
}>;
}

View File

@@ -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>