[UI] Replace vue-bar-graph with chart.js

- Backport of #4571
- The usage of the `vue-bar-graph` is complicated, because of the `GSAP`
dependency they pull in, the dependency uses a non-free license.
- The code is rewritten to use the `chart.js` library, which is already
used to draw other charts in the activity tab. Due to the limitation of
`chart.js`, we have to create a plugin in order to have images as labels
and do click handling for those images.
- The chart isn't the same as the previous one, once again simply due to
how `chart.js` works, the amount of commits isn't drawn anymore in the
bar, you instead have to hover over it or look at the y-axis.
- Resolves #4569

(cherry picked from commit a83002679d)
This commit is contained in:
Gusted 2024-07-18 22:05:02 +02:00
parent 75808d5ba9
commit a3fc16bb03
No known key found for this signature in database
GPG key ID: FD821B732837125F
7 changed files with 121 additions and 94 deletions

View file

@ -2116,6 +2116,7 @@ activity.git_stats_addition_n = %d additions
activity.git_stats_and_deletions = and activity.git_stats_and_deletions = and
activity.git_stats_deletion_1 = %d deletion activity.git_stats_deletion_1 = %d deletion
activity.git_stats_deletion_n = %d deletions activity.git_stats_deletion_n = %d deletions
activity.commit = Commit activity
contributors.contribution_type.filter_label = Contribution type: contributors.contribution_type.filter_label = Contribution type:
contributors.contribution_type.commits = Commits contributors.contribution_type.commits = Commits

17
package-lock.json generated
View file

@ -55,7 +55,6 @@
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.4.31", "vue": "3.4.31",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5", "vue3-calendar-heatmap": "2.0.5",
@ -7301,12 +7300,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/gsap": {
"version": "3.12.5",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/hammerjs": { "node_modules/hammerjs": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
@ -14074,16 +14067,6 @@
} }
} }
}, },
"node_modules/vue-bar-graph": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-bar-graph/-/vue-bar-graph-2.0.0.tgz",
"integrity": "sha512-IoYP+r5Ggjys6QdUNYFPh7qD41wi/uDOJj9nMawvDgvV6niOz3Dw8O2/98ZnUgjTpcgcGFDaaAaK6qa9x1jgpw==",
"license": "MIT",
"dependencies": {
"gsap": "^3.10.4",
"vue": "^3.2.37"
}
},
"node_modules/vue-chartjs": { "node_modules/vue-chartjs": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz",

View file

@ -54,7 +54,6 @@
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.4.31", "vue": "3.4.31",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5", "vue3-calendar-heatmap": "2.0.5",

View file

@ -105,7 +105,7 @@
<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>. <strong class="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>
<div class="ui attached segment"> <div class="ui attached segment">
<div id="repo-activity-top-authors-chart"></div> <div id="repo-activity-top-authors-chart" data-locale-commit-activity="{{ctx.Locale.Tr "repo.activity.commit"}}"></div>
</div> </div>
</div> </div>
{{end}} {{end}}

View file

@ -1138,10 +1138,6 @@ overflow-menu .ui.label {
color: var(--color-primary-contrast); color: var(--color-primary-contrast);
} }
.activity-bar-graph-alt {
color: var(--color-primary-contrast);
}
.archived-icon { .archived-icon {
color: var(--color-secondary-dark-2) !important; color: var(--color-secondary-dark-2) !important;
} }

View file

@ -2995,3 +2995,7 @@ tbody.commit-list {
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
} }
#repo-activity-top-authors-chart {
height: 150px; /* Pre-allocate the height that will be taken up by the chart, to avoid the container 'jumping'. */
}

View file

@ -1,14 +1,36 @@
<script> <script>
import VueBarGraph from 'vue-bar-graph'; import {Bar} from 'vue-chartjs';
import {
Chart,
Tooltip,
BarElement,
CategoryScale,
LinearScale,
} from 'chart.js';
import {chartJsColors} from '../utils/color.js';
import {createApp} from 'vue'; import {createApp} from 'vue';
Chart.defaults.color = chartJsColors.text;
Chart.defaults.borderColor = chartJsColors.border;
Chart.register(
CategoryScale,
LinearScale,
BarElement,
Tooltip,
);
const sfc = { const sfc = {
components: {VueBarGraph}, components: {Bar},
props: {
locale: {
type: Object,
required: true,
},
},
data: () => ({ data: () => ({
colors: { colors: {
barColor: 'green', barColor: 'green',
textColor: 'black',
textAltColor: 'white',
}, },
// possible keys: // possible keys:
@ -18,42 +40,108 @@ const sfc = {
// * login: (...) // * login: (...)
// * name: (...) // * name: (...)
activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [],
i18nCommitActivity: this,
}), }),
computed: { methods: {
graphPoints() { graphPoints() {
return this.activityTopAuthors.map((item) => { return {
return { datasets: [{
value: item.commits, label: this.locale.commitActivity,
label: item.name, data: this.activityTopAuthors.map((item) => item.commits),
}; backgroundColor: this.colors.barColor,
}); barThickness: 40,
borderWidth: 0,
tension: 0.3,
}],
labels: this.activityTopAuthors.map((item) => item.name),
};
}, },
graphAuthors() { getOptions() {
return this.activityTopAuthors.map((item, idx) => { return {
return { responsive: true,
position: idx + 1, maintainAspectRatio: false,
...item, animation: true,
}; scales: {
}); x: {
}, type: 'category',
graphWidth() { grid: {
return this.activityTopAuthors.length * 40; display: false,
},
ticks: {
color: 'transparent', // Disable drawing of labels on the x-axis.
},
},
y: {
ticks: {
stepSize: 1,
},
},
},
};
}, },
}, },
mounted() { mounted() {
const refStyle = window.getComputedStyle(this.$refs.style); const refStyle = window.getComputedStyle(this.$refs.style);
const refAltStyle = window.getComputedStyle(this.$refs.altStyle);
this.colors.barColor = refStyle.backgroundColor; this.colors.barColor = refStyle.backgroundColor;
this.colors.textColor = refStyle.color;
this.colors.textAltColor = refAltStyle.color; for (const item of this.activityTopAuthors) {
const img = new Image();
img.src = item.avatar_link;
item.avatar_img = img;
}
Chart.register({
id: 'image_label',
afterDraw: (chart) => {
const xAxis = chart.boxes[0];
const yAxis = chart.boxes[1];
for (const [index] of xAxis.ticks.entries()) {
const x = xAxis.getPixelForTick(index);
const img = this.activityTopAuthors[index].avatar_img;
chart.ctx.save();
chart.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, x - 10, yAxis.bottom + 10, 20, 20);
chart.ctx.restore();
}
},
beforeEvent: (chart, args) => {
const event = args.event;
if (event.type !== 'mousemove' && event.type !== 'click') return;
const yAxis = chart.boxes[1];
if (event.y < yAxis.bottom + 10 || event.y > yAxis.bottom + 30) {
chart.canvas.style.cursor = '';
return;
}
const xAxis = chart.boxes[0];
const pointIdx = xAxis.ticks.findIndex((_, index) => {
const x = xAxis.getPixelForTick(index);
return event.x >= x - 10 && event.x <= x + 10;
});
if (pointIdx === -1) {
chart.canvas.style.cursor = '';
return;
}
chart.canvas.style.cursor = 'pointer';
if (event.type === 'click' && this.activityTopAuthors[pointIdx].home_link) {
window.location.href = this.activityTopAuthors[pointIdx].home_link;
}
},
});
}, },
}; };
export function initRepoActivityTopAuthorsChart() { export function initRepoActivityTopAuthorsChart() {
const el = document.getElementById('repo-activity-top-authors-chart'); const el = document.getElementById('repo-activity-top-authors-chart');
if (el) { if (el) {
createApp(sfc).mount(el); createApp(sfc, {
locale: {
commitActivity: el.getAttribute('data-locale-commit-activity'),
},
}).mount(el);
} }
} }
@ -62,50 +150,6 @@ export default sfc; // activate the IDE's Vue plugin
<template> <template>
<div> <div>
<div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/> <div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/>
<div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/> <Bar height="150px" :data="graphPoints()" :options="getOptions()"/>
<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>
</div> </div>
</template> </template>