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