From a83002679df5c1d72b41f2ca015c5cb3e0c6d10d Mon Sep 17 00:00:00 2001 From: Gusted <postmaster@gusted.xyz> Date: Thu, 18 Jul 2024 22:05:02 +0200 Subject: [PATCH] [UI] Replace `vue-bar-graph` with `chart.js` - 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 --- options/locale/locale_en-US.ini | 1 + package-lock.json | 17 -- package.json | 1 - templates/repo/pulse.tmpl | 2 +- web_src/css/base.css | 4 - web_src/css/repo.css | 4 + .../js/components/RepoActivityTopAuthors.vue | 186 +++++++++++------- 7 files changed, 121 insertions(+), 94 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6c21589c7f..e528c62dcd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2117,6 +2117,7 @@ activity.git_stats_addition_n = %d additions activity.git_stats_and_deletions = and activity.git_stats_deletion_1 = %d deletion activity.git_stats_deletion_n = %d deletions +activity.commit = Commit activity contributors.contribution_type.filter_label = Contribution type: contributors.contribution_type.commits = Commits diff --git a/package-lock.json b/package-lock.json index 6e907943e2..f8c33d9b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,6 @@ "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.4.32", - "vue-bar-graph": "2.0.0", "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", @@ -7335,12 +7334,6 @@ "dev": true, "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": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -13707,16 +13700,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": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", diff --git a/package.json b/package.json index 38fda76e0d..7f67dbdc8d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.4.32", - "vue-bar-graph": "2.0.0", "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl index bc25563d48..4790208541 100644 --- a/templates/repo/pulse.tmpl +++ b/templates/repo/pulse.tmpl @@ -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>. </div> <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> {{end}} diff --git a/web_src/css/base.css b/web_src/css/base.css index 8f75a63cd0..4c1317ba79 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1138,10 +1138,6 @@ overflow-menu .ui.label { color: var(--color-primary-contrast); } -.activity-bar-graph-alt { - color: var(--color-primary-contrast); -} - .archived-icon { color: var(--color-secondary-dark-2) !important; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 1a901e07a2..bd2da5fa04 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2995,3 +2995,7 @@ tbody.commit-list { font-size: 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'. */ +} diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index a41fb61d78..52986c0493 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,14 +1,36 @@ <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'; +Chart.defaults.color = chartJsColors.text; +Chart.defaults.borderColor = chartJsColors.border; + +Chart.register( + CategoryScale, + LinearScale, + BarElement, + Tooltip, +); + const sfc = { - components: {VueBarGraph}, + components: {Bar}, + props: { + locale: { + type: Object, + required: true, + }, + }, data: () => ({ colors: { barColor: 'green', - textColor: 'black', - textAltColor: 'white', }, // possible keys: @@ -18,42 +40,108 @@ const sfc = { // * login: (...) // * name: (...) activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], + i18nCommitActivity: this, }), - computed: { + methods: { graphPoints() { - return this.activityTopAuthors.map((item) => { - return { - value: item.commits, - label: item.name, - }; - }); + return { + datasets: [{ + 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() { - return this.activityTopAuthors.map((item, idx) => { - return { - position: idx + 1, - ...item, - }; - }); - }, - graphWidth() { - return this.activityTopAuthors.length * 40; + getOptions() { + return { + responsive: true, + maintainAspectRatio: false, + animation: true, + scales: { + x: { + type: 'category', + grid: { + display: false, + }, + ticks: { + color: 'transparent', // Disable drawing of labels on the x-axis. + }, + }, + y: { + ticks: { + stepSize: 1, + }, + }, + }, + }; }, }, mounted() { const refStyle = window.getComputedStyle(this.$refs.style); - const refAltStyle = window.getComputedStyle(this.$refs.altStyle); - 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() { const el = document.getElementById('repo-activity-top-authors-chart'); 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> <div> <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;"/> - <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> + <Bar height="150px" :data="graphPoints()" :options="getOptions()"/> </div> </template>