[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
This commit is contained in:
		
					parent
					
						
							
								3e8f975345
							
						
					
				
			
			
				commit
				
					
						a83002679d
					
				
			
		
					 7 changed files with 121 additions and 94 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'. */
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue