mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-27 09:11:53 -05:00
[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:
parent
ddc7d62afc
commit
f45928e608
7 changed files with 121 additions and 92 deletions
|
@ -2089,6 +2089,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
|
||||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -55,7 +55,6 @@
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
"uint8-to-base64": "0.2.0",
|
"uint8-to-base64": "0.2.0",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.21",
|
||||||
"vue-bar-graph": "2.0.0",
|
|
||||||
"vue-chartjs": "5.3.0",
|
"vue-chartjs": "5.3.0",
|
||||||
"vue-loader": "17.4.2",
|
"vue-loader": "17.4.2",
|
||||||
"vue3-calendar-heatmap": "2.0.5",
|
"vue3-calendar-heatmap": "2.0.5",
|
||||||
|
@ -6583,11 +6582,6 @@
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"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",
|
||||||
|
@ -12228,15 +12222,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==",
|
|
||||||
"dependencies": {
|
|
||||||
"gsap": "^3.10.4",
|
|
||||||
"vue": "^3.2.37"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-chartjs": {
|
"node_modules/vue-chartjs": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
|
||||||
|
|
|
@ -54,7 +54,6 @@
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
"uint8-to-base64": "0.2.0",
|
"uint8-to-base64": "0.2.0",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.21",
|
||||||
"vue-bar-graph": "2.0.0",
|
|
||||||
"vue-chartjs": "5.3.0",
|
"vue-chartjs": "5.3.0",
|
||||||
"vue-loader": "17.4.2",
|
"vue-loader": "17.4.2",
|
||||||
"vue3-calendar-heatmap": "2.0.5",
|
"vue3-calendar-heatmap": "2.0.5",
|
||||||
|
|
|
@ -101,7 +101,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}}
|
||||||
|
|
|
@ -1214,10 +1214,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3105,3 +3105,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'. */
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
value: item.commits,
|
datasets: [{
|
||||||
label: item.name,
|
label: this.locale.commitActivity,
|
||||||
|
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 {
|
||||||
position: idx + 1,
|
responsive: true,
|
||||||
...item,
|
maintainAspectRatio: false,
|
||||||
};
|
animation: true,
|
||||||
});
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'category',
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
},
|
},
|
||||||
graphWidth() {
|
ticks: {
|
||||||
return this.activityTopAuthors.length * 40;
|
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>
|
||||||
|
|
Loading…
Reference in a new issue