From 4b0130584fe6b945cb1c59e32125b4ca520fc2de Mon Sep 17 00:00:00 2001 From: Beowulf Date: Mon, 29 Dec 2025 20:04:47 +0100 Subject: [PATCH] fix: don't stretch activity top author image (#10556) This is a followup to !10524. In addition I changed that the tooltip triggers for the whole height, instead for only the bar height, because otherwise it is esp for small bars nearly impossible to get the tooltip to open. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10556 Reviewed-by: Gusted Co-authored-by: Beowulf Co-committed-by: Beowulf --- .../components/RepoActivityTopAuthors.test.js | 28 ++++ .../js/components/RepoActivityTopAuthors.vue | 125 +++++++++++------- 2 files changed, 103 insertions(+), 50 deletions(-) create mode 100644 web_src/js/components/RepoActivityTopAuthors.test.js diff --git a/web_src/js/components/RepoActivityTopAuthors.test.js b/web_src/js/components/RepoActivityTopAuthors.test.js new file mode 100644 index 0000000000..410329441a --- /dev/null +++ b/web_src/js/components/RepoActivityTopAuthors.test.js @@ -0,0 +1,28 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +import {flushPromises, mount} from '@vue/test-utils'; +import RepoActivityTopAuthors from './RepoActivityTopAuthors.vue'; +import {expect, test, vi} from 'vitest'; + +test('calc image size and shift', async () => { + vi.spyOn(RepoActivityTopAuthors.methods, 'init').mockResolvedValue({}); + + const repoActivityTopAuthors = mount(RepoActivityTopAuthors, { + props: { + locale: { + commitActivity: '', + }, + }, + }); + await flushPromises(); + + const square = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 50, naturalHeight: 50}); + expect(square).toEqual([20, 20, 0, 0]); + + const portrait = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 5, naturalHeight: 50}); + expect(portrait).toEqual([2, 20, 9, 0]); + + const landscape = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 500, naturalHeight: 5}); + expect(landscape).toEqual([20, 0.2, 0, 9.9]); +}); diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 22c0b9bdb3..eeaf0c4f73 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -42,56 +42,7 @@ export default { i18nCommitActivity: this, }), mounted() { - const refStyle = window.getComputedStyle(this.$refs.style); - this.colors.barColor = refStyle.backgroundColor; - - 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; - } - }, - }); + this.init(); }, methods: { graphPoints() { @@ -137,8 +88,82 @@ export default { }, }, }, + plugins: { + tooltip: { + intersect: false, + }, + }, }; }, + init() { + const refStyle = window.getComputedStyle(this.$refs.style); + this.colors.barColor = refStyle.backgroundColor; + + 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(); + const [width, height, dx, dy] = this.calcImageSizeAndShift(img); + chart.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, x - 10 + dx, yAxis.bottom + 10 + dy, width, height); + 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; + } + }, + }); + }, + calcImageSizeAndShift(img) { + const targetSize = 20; + const [imgWidth, imgHeight] = [img.naturalWidth, img.naturalHeight]; + + // The image should be contained in a square, + // so the scale depends on the longer dimension. + const scale = targetSize / (Math.max(imgWidth, imgHeight)); + const calcScale = (size) => size * scale; + const [width, height] = [calcScale(imgWidth), calcScale(imgHeight)]; + + // The image should be centered in the 20x20 square. + const calcShift = (size) => (targetSize - size) / 2; + const [dx, dy] = [calcShift(width), calcShift(height)]; + + return [width, height, dx, dy]; + }, }, };