1<script>
2import {SvgIcon} from '../svg.js';
3import {
4 Chart,
5 Legend,
6 LinearScale,
7 TimeScale,
8 PointElement,
9 LineElement,
10 Filler,
11} from 'chart.js';
12import {GET} from '../modules/fetch.js';
13import {Line as ChartLine} from 'vue-chartjs';
14import {
15 startDaysBetween,
16 firstStartDateAfterDate,
17 fillEmptyStartDaysWithZeroes,
18} from '../utils/time.js';
19import {chartJsColors} from '../utils/color.js';
20import {sleep} from '../utils.js';
21import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
22
23const {pageData} = window.config;
24
25Chart.defaults.color = chartJsColors.text;
26Chart.defaults.borderColor = chartJsColors.border;
27
28Chart.register(
29 TimeScale,
30 LinearScale,
31 Legend,
32 PointElement,
33 LineElement,
34 Filler,
35);
36
37export default {
38 components: {ChartLine, SvgIcon},
39 props: {
40 locale: {
41 type: Object,
42 required: true,
43 },
44 },
45 data: () => ({
46 isLoading: false,
47 errorText: '',
48 repoLink: pageData.repoLink || [],
49 data: [],
50 }),
51 mounted() {
52 this.fetchGraphData();
53 },
54 methods: {
55 async fetchGraphData() {
56 this.isLoading = true;
57 try {
58 let response;
59 do {
60 response = await GET(`${this.repoLink}/activity/code-frequency/data`);
61 if (response.status === 202) {
62 await sleep(1000); // wait for 1 second before retrying
63 }
64 } while (response.status === 202);
65 if (response.ok) {
66 this.data = await response.json();
67 const weekValues = Object.values(this.data);
68 const start = weekValues[0].week;
69 const end = firstStartDateAfterDate(new Date());
70 const startDays = startDaysBetween(start, end);
71 this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
72 this.errorText = '';
73 } else {
74 this.errorText = response.statusText;
75 }
76 } catch (err) {
77 this.errorText = err.message;
78 } finally {
79 this.isLoading = false;
80 }
81 },
82
83 toGraphData(data) {
84 return {
85 datasets: [
86 {
87 data: data.map((i) => ({x: i.week, y: i.additions})),
88 pointRadius: 0,
89 pointHitRadius: 0,
90 fill: true,
91 label: 'Additions',
92 backgroundColor: chartJsColors['additions'],
93 borderWidth: 0,
94 tension: 0.3,
95 },
96 {
97 data: data.map((i) => ({x: i.week, y: -i.deletions})),
98 pointRadius: 0,
99 pointHitRadius: 0,
100 fill: true,
101 label: 'Deletions',
102 backgroundColor: chartJsColors['deletions'],
103 borderWidth: 0,
104 tension: 0.3,
105 },
106 ],
107 };
108 },
109
110 getOptions() {
111 return {
112 responsive: true,
113 maintainAspectRatio: false,
114 animation: true,
115 plugins: {
116 legend: {
117 display: true,
118 },
119 },
120 scales: {
121 x: {
122 type: 'time',
123 grid: {
124 display: false,
125 },
126 time: {
127 minUnit: 'month',
128 },
129 ticks: {
130 maxRotation: 0,
131 maxTicksLimit: 12,
132 },
133 },
134 y: {
135 ticks: {
136 maxTicksLimit: 6,
137 },
138 },
139 },
140 };
141 },
142 },
143};
144</script>
145<template>
146 <div>
147 <div class="ui header tw-flex tw-items-center tw-justify-between">
148 {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
149 </div>
150 <div class="tw-flex ui segment main-graph">
151 <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
152 <div v-if="isLoading">
153 <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
154 {{ locale.loadingInfo }}
155 </div>
156 <div v-else class="text red">
157 <SvgIcon name="octicon-x-circle-fill"/>
158 {{ errorText }}
159 </div>
160 </div>
161 <ChartLine
162 v-memo="data" v-if="data.length !== 0"
163 :data="toGraphData(data)" :options="getOptions()"
164 />
165 </div>
166 </div>
167</template>
168<style scoped>
169.main-graph {
170 height: 440px;
171}
172</style>