@stats-forge/github-stats-forge-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/build/api/api-result.d.ts +44 -0
- package/build/api/api-result.d.ts.map +1 -0
- package/build/api/api-result.js +33 -0
- package/build/api/gist.d.ts +29 -0
- package/build/api/gist.d.ts.map +1 -0
- package/build/api/gist.js +52 -0
- package/build/api/index.d.ts +13 -0
- package/build/api/index.d.ts.map +1 -0
- package/build/api/index.js +8 -0
- package/build/api/params.d.ts +93 -0
- package/build/api/params.d.ts.map +1 -0
- package/build/api/params.js +145 -0
- package/build/api/pin.d.ts +47 -0
- package/build/api/pin.d.ts.map +1 -0
- package/build/api/pin.js +87 -0
- package/build/api/stats.d.ts +42 -0
- package/build/api/stats.d.ts.map +1 -0
- package/build/api/stats.js +132 -0
- package/build/api/top-langs.d.ts +37 -0
- package/build/api/top-langs.d.ts.map +1 -0
- package/build/api/top-langs.js +101 -0
- package/build/api/wakatime.d.ts +34 -0
- package/build/api/wakatime.d.ts.map +1 -0
- package/build/api/wakatime.js +91 -0
- package/build/calculateRank.d.ts +29 -0
- package/build/calculateRank.d.ts.map +1 -0
- package/build/calculateRank.js +58 -0
- package/build/cards/gist.d.ts +16 -0
- package/build/cards/gist.d.ts.map +1 -0
- package/build/cards/gist.js +119 -0
- package/build/cards/index.d.ts +11 -0
- package/build/cards/index.d.ts.map +1 -0
- package/build/cards/index.js +7 -0
- package/build/cards/options.d.ts +19 -0
- package/build/cards/options.d.ts.map +1 -0
- package/build/cards/options.js +1 -0
- package/build/cards/repo.d.ts +25 -0
- package/build/cards/repo.d.ts.map +1 -0
- package/build/cards/repo.js +276 -0
- package/build/cards/stats.d.ts +37 -0
- package/build/cards/stats.d.ts.map +1 -0
- package/build/cards/stats.js +457 -0
- package/build/cards/top-languages.d.ts +153 -0
- package/build/cards/top-languages.d.ts.map +1 -0
- package/build/cards/top-languages.js +805 -0
- package/build/cards/wakatime.d.ts +31 -0
- package/build/cards/wakatime.d.ts.map +1 -0
- package/build/cards/wakatime.js +376 -0
- package/build/common/Card.d.ts +121 -0
- package/build/common/Card.d.ts.map +1 -0
- package/build/common/Card.js +305 -0
- package/build/common/I18n.d.ts +27 -0
- package/build/common/I18n.d.ts.map +1 -0
- package/build/common/I18n.js +35 -0
- package/build/common/color.d.ts +112 -0
- package/build/common/color.d.ts.map +1 -0
- package/build/common/color.js +188 -0
- package/build/common/config.d.ts +53 -0
- package/build/common/config.d.ts.map +1 -0
- package/build/common/config.js +87 -0
- package/build/common/constants.d.ts +7 -0
- package/build/common/constants.d.ts.map +1 -0
- package/build/common/constants.js +9 -0
- package/build/common/date.d.ts +29 -0
- package/build/common/date.d.ts.map +1 -0
- package/build/common/date.js +23 -0
- package/build/common/error.d.ts +87 -0
- package/build/common/error.d.ts.map +1 -0
- package/build/common/error.js +108 -0
- package/build/common/fmt.d.ts +28 -0
- package/build/common/fmt.d.ts.map +1 -0
- package/build/common/fmt.js +68 -0
- package/build/common/html.d.ts +8 -0
- package/build/common/html.d.ts.map +1 -0
- package/build/common/html.js +14 -0
- package/build/common/http.d.ts +53 -0
- package/build/common/http.d.ts.map +1 -0
- package/build/common/http.js +47 -0
- package/build/common/icons.d.ts +28 -0
- package/build/common/icons.d.ts.map +1 -0
- package/build/common/icons.js +77 -0
- package/build/common/languageColors.d.ts +11 -0
- package/build/common/languageColors.d.ts.map +1 -0
- package/build/common/languageColors.js +15 -0
- package/build/common/languageColors.json +678 -0
- package/build/common/log.d.ts +9 -0
- package/build/common/log.d.ts.map +1 -0
- package/build/common/log.js +8 -0
- package/build/common/ops.d.ts +59 -0
- package/build/common/ops.d.ts.map +1 -0
- package/build/common/ops.js +115 -0
- package/build/common/render.d.ts +196 -0
- package/build/common/render.d.ts.map +1 -0
- package/build/common/render.js +474 -0
- package/build/common/retryer.d.ts +36 -0
- package/build/common/retryer.d.ts.map +1 -0
- package/build/common/retryer.js +67 -0
- package/build/fetchers/gist.d.ts +15 -0
- package/build/fetchers/gist.d.ts.map +1 -0
- package/build/fetchers/gist.js +68 -0
- package/build/fetchers/index.d.ts +13 -0
- package/build/fetchers/index.d.ts.map +1 -0
- package/build/fetchers/index.js +10 -0
- package/build/fetchers/repo.d.ts +27 -0
- package/build/fetchers/repo.d.ts.map +1 -0
- package/build/fetchers/repo.js +100 -0
- package/build/fetchers/stats.d.ts +74 -0
- package/build/fetchers/stats.d.ts.map +1 -0
- package/build/fetchers/stats.js +498 -0
- package/build/fetchers/top-languages.d.ts +23 -0
- package/build/fetchers/top-languages.d.ts.map +1 -0
- package/build/fetchers/top-languages.js +89 -0
- package/build/fetchers/types.d.ts +130 -0
- package/build/fetchers/types.d.ts.map +1 -0
- package/build/fetchers/types.js +1 -0
- package/build/fetchers/wakatime.d.ts +19 -0
- package/build/fetchers/wakatime.d.ts.map +1 -0
- package/build/fetchers/wakatime.js +36 -0
- package/build/graphql/contributionsDocument.d.ts +18 -0
- package/build/graphql/contributionsDocument.d.ts.map +1 -0
- package/build/graphql/contributionsDocument.js +30 -0
- package/build/graphql/generated/common.d.ts +42 -0
- package/build/graphql/generated/common.d.ts.map +1 -0
- package/build/graphql/generated/common.js +2 -0
- package/build/graphql/generated/gist.d.ts +44 -0
- package/build/graphql/generated/gist.d.ts.map +1 -0
- package/build/graphql/generated/gist.js +27 -0
- package/build/graphql/generated/repo.d.ts +67 -0
- package/build/graphql/generated/repo.d.ts.map +1 -0
- package/build/graphql/generated/repo.js +30 -0
- package/build/graphql/generated/stats.d.ts +150 -0
- package/build/graphql/generated/stats.d.ts.map +1 -0
- package/build/graphql/generated/stats.js +95 -0
- package/build/graphql/generated/top-languages.d.ts +54 -0
- package/build/graphql/generated/top-languages.d.ts.map +1 -0
- package/build/graphql/generated/top-languages.js +27 -0
- package/build/graphql/graphqlDocument.d.ts +22 -0
- package/build/graphql/graphqlDocument.d.ts.map +1 -0
- package/build/graphql/graphqlDocument.js +8 -0
- package/build/graphql/reposContributedToDocument.d.ts +27 -0
- package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
- package/build/graphql/reposContributedToDocument.js +59 -0
- package/build/index.d.ts +15 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +10 -0
- package/build/themes/index.d.ts +505 -0
- package/build/themes/index.d.ts.map +1 -0
- package/build/themes/index.js +505 -0
- package/build/translations.d.ts +1153 -0
- package/build/translations.d.ts.map +1 -0
- package/build/translations.js +1156 -0
- package/package.json +114 -0
- package/src/_emoji-name-map.d.ts +10 -0
- package/src/_github-username-regex.d.ts +4 -0
- package/src/api/api-result.ts +69 -0
- package/src/api/gist.ts +66 -0
- package/src/api/index.ts +16 -0
- package/src/api/params.ts +221 -0
- package/src/api/pin.ts +126 -0
- package/src/api/stats.ts +195 -0
- package/src/api/top-langs.ts +150 -0
- package/src/api/wakatime.ts +130 -0
- package/src/calculateRank.ts +91 -0
- package/src/cards/gist.ts +169 -0
- package/src/cards/index.ts +21 -0
- package/src/cards/options.ts +18 -0
- package/src/cards/repo.ts +376 -0
- package/src/cards/stats.ts +615 -0
- package/src/cards/top-languages.ts +1077 -0
- package/src/cards/wakatime.ts +501 -0
- package/src/common/Card.ts +364 -0
- package/src/common/I18n.ts +51 -0
- package/src/common/color.ts +296 -0
- package/src/common/config.ts +118 -0
- package/src/common/constants.ts +12 -0
- package/src/common/date.ts +34 -0
- package/src/common/error.ts +163 -0
- package/src/common/fmt.ts +86 -0
- package/src/common/html.ts +17 -0
- package/src/common/http.ts +98 -0
- package/src/common/icons.ts +80 -0
- package/src/common/languageColors.json +678 -0
- package/src/common/languageColors.ts +19 -0
- package/src/common/log.ts +12 -0
- package/src/common/ops.ts +145 -0
- package/src/common/render.ts +641 -0
- package/src/common/retryer.ts +111 -0
- package/src/fetchers/gist.ts +82 -0
- package/src/fetchers/index.ts +25 -0
- package/src/fetchers/repo.ts +140 -0
- package/src/fetchers/stats.ts +742 -0
- package/src/fetchers/top-languages.ts +134 -0
- package/src/fetchers/types.ts +136 -0
- package/src/fetchers/wakatime.ts +56 -0
- package/src/graphql/contributionsDocument.ts +44 -0
- package/src/graphql/generated/common.ts +26 -0
- package/src/graphql/generated/gist.ts +56 -0
- package/src/graphql/generated/repo.ts +79 -0
- package/src/graphql/generated/stats.ts +180 -0
- package/src/graphql/generated/top-languages.ts +66 -0
- package/src/graphql/graphqlDocument.ts +25 -0
- package/src/graphql/queries/gist.graphql +25 -0
- package/src/graphql/queries/repo.graphql +28 -0
- package/src/graphql/queries/stats.graphql +113 -0
- package/src/graphql/queries/top-languages.graphql +27 -0
- package/src/graphql/reposContributedToDocument.ts +83 -0
- package/src/index.ts +18 -0
- package/src/themes/index.ts +519 -0
- package/src/translations.ts +1169 -0
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
import { Card } from '../common/Card.js';
|
|
2
|
+
import { getLightDarkColors, isPrefixedHexColor } from '../common/color.js';
|
|
3
|
+
import { formatBytes } from '../common/fmt.js';
|
|
4
|
+
import { encodeHTML } from '../common/html.js';
|
|
5
|
+
import { I18n } from '../common/I18n.js';
|
|
6
|
+
import { DEFAULT_LANG_COLOR } from '../common/languageColors.js';
|
|
7
|
+
import { chunkArray, clampValue, lowercaseTrim } from '../common/ops.js';
|
|
8
|
+
import { createProgressNode, flexLayout, measureText } from '../common/render.js';
|
|
9
|
+
import type { Lang, TopLangData } from '../fetchers/types.js';
|
|
10
|
+
import { langCardLocales } from '../translations.js';
|
|
11
|
+
|
|
12
|
+
import type { CardOptions, CommonCardOptions } from './options.js';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CARD_WIDTH = 300;
|
|
15
|
+
const MIN_CARD_WIDTH = 280;
|
|
16
|
+
const CARD_PADDING = 25;
|
|
17
|
+
const COMPACT_LAYOUT_BASE_HEIGHT = 90;
|
|
18
|
+
const MAXIMUM_LANGS_COUNT = 20;
|
|
19
|
+
|
|
20
|
+
const NORMAL_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
|
21
|
+
const COMPACT_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
22
|
+
const DONUT_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
|
23
|
+
const PIE_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
24
|
+
const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
|
25
|
+
|
|
26
|
+
/** Layouts the card can draw; the api validates `layout` against this. */
|
|
27
|
+
const TOP_LANG_LAYOUTS = ['compact', 'normal', 'donut', 'donut-vertical', 'pie'] as const;
|
|
28
|
+
type TopLangLayout = (typeof TOP_LANG_LAYOUTS)[number];
|
|
29
|
+
|
|
30
|
+
/** How a language's value is shown; the api validates `stats_format` against this. */
|
|
31
|
+
const TOP_LANG_STATS_FORMATS = ['percentages', 'bytes'] as const;
|
|
32
|
+
type TopLangStatsFormat = (typeof TOP_LANG_STATS_FORMATS)[number];
|
|
33
|
+
|
|
34
|
+
interface TopLangOptions extends CommonCardOptions {
|
|
35
|
+
locale: string;
|
|
36
|
+
hide_title: boolean;
|
|
37
|
+
card_width: number;
|
|
38
|
+
hide: Array<string>;
|
|
39
|
+
layout: TopLangLayout;
|
|
40
|
+
custom_title: string;
|
|
41
|
+
langs_count: number;
|
|
42
|
+
disable_animations: boolean;
|
|
43
|
+
hide_progress: boolean;
|
|
44
|
+
hide_values: boolean;
|
|
45
|
+
prog_bar_bg_color: string;
|
|
46
|
+
stats_format: TopLangStatsFormat;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves the programming language whose name is the longest.
|
|
51
|
+
*
|
|
52
|
+
* @param arr Array of programming languages.
|
|
53
|
+
* @returns Longest programming language object.
|
|
54
|
+
*/
|
|
55
|
+
const getLongestLang = (arr: Array<Lang>): Pick<Lang, 'name' | 'size' | 'color'> =>
|
|
56
|
+
arr.reduce<Pick<Lang, 'name' | 'size' | 'color'>>(
|
|
57
|
+
(savedLang, lang) => (lang.name.length > savedLang.name.length ? lang : savedLang),
|
|
58
|
+
{ name: '', size: 0, color: '' },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert degrees to radians.
|
|
63
|
+
*
|
|
64
|
+
* @param angleInDegrees Angle in degrees.
|
|
65
|
+
* @returns Angle in radians.
|
|
66
|
+
*/
|
|
67
|
+
const degreesToRadians = (angleInDegrees: number): number => angleInDegrees * (Math.PI / 180.0);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert radians to degrees.
|
|
71
|
+
*
|
|
72
|
+
* @param angleInRadians Angle in radians.
|
|
73
|
+
* @returns Angle in degrees.
|
|
74
|
+
*/
|
|
75
|
+
const radiansToDegrees = (angleInRadians: number): number => angleInRadians / (Math.PI / 180.0);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert polar coordinates to cartesian coordinates.
|
|
79
|
+
*
|
|
80
|
+
* @param centerX Center x coordinate.
|
|
81
|
+
* @param centerY Center y coordinate.
|
|
82
|
+
* @param radius Radius of the circle.
|
|
83
|
+
* @param angleInDegrees Angle in degrees.
|
|
84
|
+
* @returns Cartesian coordinates.
|
|
85
|
+
*/
|
|
86
|
+
const polarToCartesian = (
|
|
87
|
+
centerX: number,
|
|
88
|
+
centerY: number,
|
|
89
|
+
radius: number,
|
|
90
|
+
angleInDegrees: number,
|
|
91
|
+
): { x: number; y: number } => {
|
|
92
|
+
const rads = degreesToRadians(angleInDegrees);
|
|
93
|
+
return {
|
|
94
|
+
x: centerX + radius * Math.cos(rads),
|
|
95
|
+
y: centerY + radius * Math.sin(rads),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert cartesian coordinates to polar coordinates.
|
|
101
|
+
*
|
|
102
|
+
* @param centerX Center x coordinate.
|
|
103
|
+
* @param centerY Center y coordinate.
|
|
104
|
+
* @param x Point x coordinate.
|
|
105
|
+
* @param y Point y coordinate.
|
|
106
|
+
* @returns Polar coordinates.
|
|
107
|
+
*/
|
|
108
|
+
const cartesianToPolar = (
|
|
109
|
+
centerX: number,
|
|
110
|
+
centerY: number,
|
|
111
|
+
x: number,
|
|
112
|
+
y: number,
|
|
113
|
+
): { radius: number; angleInDegrees: number } => {
|
|
114
|
+
const radius = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
|
115
|
+
let angleInDegrees = radiansToDegrees(Math.atan2(y - centerY, x - centerX));
|
|
116
|
+
if (angleInDegrees < 0) {
|
|
117
|
+
angleInDegrees += 360;
|
|
118
|
+
}
|
|
119
|
+
return { radius, angleInDegrees };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculates length of circle.
|
|
124
|
+
*
|
|
125
|
+
* @param radius Radius of the circle.
|
|
126
|
+
* @returns The length of the circle.
|
|
127
|
+
*/
|
|
128
|
+
const getCircleLength = (radius: number): number => {
|
|
129
|
+
return 2 * Math.PI * radius;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Calculates height for the compact layout.
|
|
134
|
+
*
|
|
135
|
+
* @param totalLangs Total number of languages.
|
|
136
|
+
* @returns Card height.
|
|
137
|
+
*/
|
|
138
|
+
const calculateCompactLayoutHeight = (totalLangs: number): number => {
|
|
139
|
+
return COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculates height for the normal layout.
|
|
144
|
+
*
|
|
145
|
+
* @param totalLangs Total number of languages.
|
|
146
|
+
* @returns Card height.
|
|
147
|
+
*/
|
|
148
|
+
const calculateNormalLayoutHeight = (totalLangs: number): number => {
|
|
149
|
+
return 45 + (totalLangs + 1) * 40;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Calculates height for the donut layout.
|
|
154
|
+
*
|
|
155
|
+
* @param totalLangs Total number of languages.
|
|
156
|
+
* @returns Card height.
|
|
157
|
+
*/
|
|
158
|
+
const calculateDonutLayoutHeight = (totalLangs: number): number => {
|
|
159
|
+
return 215 + Math.max(totalLangs - 5, 0) * 32;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculates height for the donut vertical layout.
|
|
164
|
+
*
|
|
165
|
+
* @param totalLangs Total number of languages.
|
|
166
|
+
* @returns Card height.
|
|
167
|
+
*/
|
|
168
|
+
const calculateDonutVerticalLayoutHeight = (totalLangs: number): number => {
|
|
169
|
+
return 300 + Math.round(totalLangs / 2) * 25;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculates height for the pie layout.
|
|
174
|
+
*
|
|
175
|
+
* @param totalLangs Total number of languages.
|
|
176
|
+
* @returns Card height.
|
|
177
|
+
*/
|
|
178
|
+
const calculatePieLayoutHeight = (totalLangs: number): number => {
|
|
179
|
+
return 300 + Math.round(totalLangs / 2) * 25;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Calculates the center translation needed to keep the donut chart centred.
|
|
184
|
+
*
|
|
185
|
+
* @param totalLangs Total number of languages.
|
|
186
|
+
* @returns Donut center translation.
|
|
187
|
+
*/
|
|
188
|
+
const donutCenterTranslation = (totalLangs: number): number => {
|
|
189
|
+
return -45 + Math.max(totalLangs - 5, 0) * 16;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Trim top languages to lang_count while also hiding certain languages.
|
|
194
|
+
*
|
|
195
|
+
* @param topLangs Top languages.
|
|
196
|
+
* @param langs_count Number of languages to show.
|
|
197
|
+
* @param hide Languages to hide.
|
|
198
|
+
* @returns Trimmed top languages and total size.
|
|
199
|
+
*/
|
|
200
|
+
const trimTopLanguages = (
|
|
201
|
+
topLangs: TopLangData,
|
|
202
|
+
langs_count: number,
|
|
203
|
+
hide?: Array<string>,
|
|
204
|
+
): { langs: Array<Lang>; totalLanguageSize: number } => {
|
|
205
|
+
let langs = Object.values(topLangs);
|
|
206
|
+
const langsToHide: Record<string, boolean> = {};
|
|
207
|
+
const langsCount = clampValue(langs_count, 1, MAXIMUM_LANGS_COUNT);
|
|
208
|
+
|
|
209
|
+
// populate langsToHide map for quick lookup while filtering out
|
|
210
|
+
if (hide) {
|
|
211
|
+
hide.forEach((langName) => {
|
|
212
|
+
langsToHide[lowercaseTrim(langName)] = true;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// filter out languages to be hidden
|
|
217
|
+
langs = langs
|
|
218
|
+
.sort((a, b) => b.size - a.size)
|
|
219
|
+
.filter((lang) => !langsToHide[lowercaseTrim(lang.name)])
|
|
220
|
+
.slice(0, langsCount);
|
|
221
|
+
|
|
222
|
+
const totalLanguageSize = langs.reduce((acc, curr) => acc + curr.size, 0);
|
|
223
|
+
|
|
224
|
+
return { langs, totalLanguageSize };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get display value corresponding to the format.
|
|
229
|
+
*
|
|
230
|
+
* @param size Bytes size.
|
|
231
|
+
* @param percentages Percentage value.
|
|
232
|
+
* @param format Format of the stats.
|
|
233
|
+
* @returns Display value.
|
|
234
|
+
*/
|
|
235
|
+
const getDisplayValue = (size: number, percentages: number, format: string): string => {
|
|
236
|
+
return format === 'bytes' ? formatBytes(size) : `${percentages.toFixed(2)}%`;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolves a language's display color, falling back to the default, and
|
|
241
|
+
* validates it is a prefixed hex color.
|
|
242
|
+
*
|
|
243
|
+
* @param lang Programming language object.
|
|
244
|
+
* @returns Validated language color.
|
|
245
|
+
*/
|
|
246
|
+
const resolveLangColor = (lang: Lang): string => {
|
|
247
|
+
const color = lang.color || DEFAULT_LANG_COLOR;
|
|
248
|
+
if (!isPrefixedHexColor(color)) {
|
|
249
|
+
throw new Error(`Invalid language color: "${color}"`);
|
|
250
|
+
}
|
|
251
|
+
return color;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create progress bar text item for a programming language.
|
|
256
|
+
*
|
|
257
|
+
* @param props Function properties.
|
|
258
|
+
* @param props.width The card width
|
|
259
|
+
* @param props.color Color of the programming language.
|
|
260
|
+
* @param props.name Name of the programming language.
|
|
261
|
+
* @param props.size Size of the programming language.
|
|
262
|
+
* @param props.totalSize Total size of all languages.
|
|
263
|
+
* @param props.statsFormat Stats format.
|
|
264
|
+
* @param props.hideValues Whether to hide stats values.
|
|
265
|
+
* @param props.index Index of the programming language.
|
|
266
|
+
* @returns Programming language SVG node.
|
|
267
|
+
*/
|
|
268
|
+
const createProgressTextNode = ({
|
|
269
|
+
width,
|
|
270
|
+
color,
|
|
271
|
+
name,
|
|
272
|
+
size,
|
|
273
|
+
totalSize,
|
|
274
|
+
statsFormat,
|
|
275
|
+
hideValues,
|
|
276
|
+
index,
|
|
277
|
+
}: {
|
|
278
|
+
width: number;
|
|
279
|
+
color: string;
|
|
280
|
+
name: string;
|
|
281
|
+
size: number;
|
|
282
|
+
totalSize: number;
|
|
283
|
+
statsFormat: string;
|
|
284
|
+
hideValues?: boolean | undefined;
|
|
285
|
+
index: number;
|
|
286
|
+
}): string => {
|
|
287
|
+
const staggerDelay = (index + 3) * 150;
|
|
288
|
+
const paddingRight = hideValues ? CARD_PADDING * 2 : 95;
|
|
289
|
+
const progressTextX = width - paddingRight + 10;
|
|
290
|
+
const progressWidth = width - paddingRight;
|
|
291
|
+
|
|
292
|
+
const progress = (size / totalSize) * 100;
|
|
293
|
+
const displayValue = getDisplayValue(size, progress, statsFormat);
|
|
294
|
+
|
|
295
|
+
return `
|
|
296
|
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
297
|
+
<text data-testid="lang-name" x="2" y="15" class="lang-name">${encodeHTML(name)}</text>
|
|
298
|
+
${hideValues ? '' : `<text x="${progressTextX}" y="34" class="lang-name">${encodeHTML(displayValue)}</text>`}
|
|
299
|
+
${createProgressNode({
|
|
300
|
+
x: 0,
|
|
301
|
+
y: 25,
|
|
302
|
+
color,
|
|
303
|
+
width: progressWidth,
|
|
304
|
+
progress,
|
|
305
|
+
delay: staggerDelay + 300,
|
|
306
|
+
})}
|
|
307
|
+
</g>
|
|
308
|
+
`;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Creates compact text item for a programming language.
|
|
313
|
+
*
|
|
314
|
+
* @param props Function properties.
|
|
315
|
+
* @param props.lang Programming language object.
|
|
316
|
+
* @param props.totalSize Total size of all languages.
|
|
317
|
+
* @param props.hideProgress Whether to hide percentage.
|
|
318
|
+
* @param props.hideValues Whether to hide stats values (percentages/bytes).
|
|
319
|
+
* @param props.statsFormat Stats format
|
|
320
|
+
* @param props.index Index of the programming language.
|
|
321
|
+
* @returns Compact layout programming language SVG node.
|
|
322
|
+
*/
|
|
323
|
+
const createCompactLangNode = ({
|
|
324
|
+
lang,
|
|
325
|
+
totalSize,
|
|
326
|
+
hideProgress,
|
|
327
|
+
hideValues,
|
|
328
|
+
statsFormat = 'percentages',
|
|
329
|
+
index,
|
|
330
|
+
}: {
|
|
331
|
+
lang: Lang;
|
|
332
|
+
totalSize: number;
|
|
333
|
+
hideProgress?: boolean | undefined;
|
|
334
|
+
hideValues?: boolean | undefined;
|
|
335
|
+
statsFormat?: string | undefined;
|
|
336
|
+
index: number;
|
|
337
|
+
}): string => {
|
|
338
|
+
const percentages = (lang.size / totalSize) * 100;
|
|
339
|
+
const displayValue = getDisplayValue(lang.size, percentages, statsFormat);
|
|
340
|
+
|
|
341
|
+
const staggerDelay = (index + 3) * 150;
|
|
342
|
+
const color = resolveLangColor(lang);
|
|
343
|
+
|
|
344
|
+
return `
|
|
345
|
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
346
|
+
<circle cx="5" cy="6" r="5" fill="${color}" />
|
|
347
|
+
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
|
348
|
+
${encodeHTML(lang.name)} ${hideProgress || hideValues ? '' : encodeHTML(displayValue)}
|
|
349
|
+
</text>
|
|
350
|
+
</g>
|
|
351
|
+
`;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Create compact languages text items for all programming languages.
|
|
356
|
+
*
|
|
357
|
+
* @param props Function properties.
|
|
358
|
+
* @param props.langs Array of programming languages.
|
|
359
|
+
* @param props.totalSize Total size of all languages.
|
|
360
|
+
* @param props.hideProgress Whether to hide percentage.
|
|
361
|
+
* @param props.hideValues Whether to hide stats values.
|
|
362
|
+
* @param props.statsFormat Stats format
|
|
363
|
+
* @returns Programming languages SVG node.
|
|
364
|
+
*/
|
|
365
|
+
const createLanguageTextNode = ({
|
|
366
|
+
langs,
|
|
367
|
+
totalSize,
|
|
368
|
+
hideProgress,
|
|
369
|
+
hideValues,
|
|
370
|
+
statsFormat,
|
|
371
|
+
}: {
|
|
372
|
+
langs: Array<Lang>;
|
|
373
|
+
totalSize: number;
|
|
374
|
+
hideProgress?: boolean | undefined;
|
|
375
|
+
hideValues?: boolean | undefined;
|
|
376
|
+
statsFormat?: string | undefined;
|
|
377
|
+
}): string => {
|
|
378
|
+
const longestLang = getLongestLang(langs);
|
|
379
|
+
const chunked = chunkArray(langs, langs.length / 2);
|
|
380
|
+
const layouts = chunked.map((array) => {
|
|
381
|
+
const items = array.map((lang, index) =>
|
|
382
|
+
createCompactLangNode({
|
|
383
|
+
lang,
|
|
384
|
+
totalSize,
|
|
385
|
+
hideProgress,
|
|
386
|
+
hideValues,
|
|
387
|
+
statsFormat,
|
|
388
|
+
index,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
return flexLayout({
|
|
392
|
+
items,
|
|
393
|
+
gap: 25,
|
|
394
|
+
direction: 'column',
|
|
395
|
+
}).join('');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
|
|
399
|
+
const minGap = 150;
|
|
400
|
+
const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
|
|
401
|
+
return flexLayout({
|
|
402
|
+
items: layouts,
|
|
403
|
+
gap: maxGap < minGap ? minGap : maxGap,
|
|
404
|
+
}).join('');
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create donut languages text items for all programming languages.
|
|
409
|
+
*
|
|
410
|
+
* @param props Function properties.
|
|
411
|
+
* @param props.langs Array of programming languages.
|
|
412
|
+
* @param props.totalSize Total size of all languages.
|
|
413
|
+
* @param props.hideValues Whether to hide stats values.
|
|
414
|
+
* @param props.statsFormat Stats format
|
|
415
|
+
* @returns Donut layout programming language SVG node.
|
|
416
|
+
*/
|
|
417
|
+
const createDonutLanguagesNode = ({
|
|
418
|
+
langs,
|
|
419
|
+
totalSize,
|
|
420
|
+
hideValues,
|
|
421
|
+
statsFormat,
|
|
422
|
+
}: {
|
|
423
|
+
langs: Array<Lang>;
|
|
424
|
+
totalSize: number;
|
|
425
|
+
hideValues?: boolean | undefined;
|
|
426
|
+
statsFormat?: string | undefined;
|
|
427
|
+
}): string => {
|
|
428
|
+
return flexLayout({
|
|
429
|
+
items: langs.map((lang, index) => {
|
|
430
|
+
return createCompactLangNode({
|
|
431
|
+
lang,
|
|
432
|
+
totalSize,
|
|
433
|
+
hideProgress: false,
|
|
434
|
+
hideValues,
|
|
435
|
+
statsFormat,
|
|
436
|
+
index,
|
|
437
|
+
});
|
|
438
|
+
}),
|
|
439
|
+
gap: 32,
|
|
440
|
+
direction: 'column',
|
|
441
|
+
}).join('');
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Renders the default language card layout.
|
|
446
|
+
*
|
|
447
|
+
* @param langs Array of programming languages.
|
|
448
|
+
* @param width Card width.
|
|
449
|
+
* @param totalLanguageSize Total size of all languages.
|
|
450
|
+
* @param statsFormat Stats format.
|
|
451
|
+
* @param hideValues Whether to hide stats values.
|
|
452
|
+
* @returns Normal layout card SVG object.
|
|
453
|
+
*/
|
|
454
|
+
const renderNormalLayout = (
|
|
455
|
+
langs: Array<Lang>,
|
|
456
|
+
width: number,
|
|
457
|
+
totalLanguageSize: number,
|
|
458
|
+
statsFormat: string,
|
|
459
|
+
hideValues?: boolean,
|
|
460
|
+
): string => {
|
|
461
|
+
return flexLayout({
|
|
462
|
+
items: langs.map((lang, index) => {
|
|
463
|
+
return createProgressTextNode({
|
|
464
|
+
width,
|
|
465
|
+
name: lang.name,
|
|
466
|
+
color: lang.color || DEFAULT_LANG_COLOR,
|
|
467
|
+
size: lang.size,
|
|
468
|
+
totalSize: totalLanguageSize,
|
|
469
|
+
statsFormat,
|
|
470
|
+
hideValues,
|
|
471
|
+
index,
|
|
472
|
+
});
|
|
473
|
+
}),
|
|
474
|
+
gap: 40,
|
|
475
|
+
direction: 'column',
|
|
476
|
+
}).join('');
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Renders the compact language card layout.
|
|
481
|
+
*
|
|
482
|
+
* @param langs Array of programming languages.
|
|
483
|
+
* @param width Card width.
|
|
484
|
+
* @param totalLanguageSize Total size of all languages.
|
|
485
|
+
* @param hideProgress Whether to hide progress bar.
|
|
486
|
+
* @param statsFormat Stats format.
|
|
487
|
+
* @param hideValues Whether to hide stats values.
|
|
488
|
+
* @returns Compact layout card SVG object.
|
|
489
|
+
*/
|
|
490
|
+
const renderCompactLayout = (
|
|
491
|
+
langs: Array<Lang>,
|
|
492
|
+
width: number,
|
|
493
|
+
totalLanguageSize: number,
|
|
494
|
+
hideProgress?: boolean,
|
|
495
|
+
statsFormat = 'percentages',
|
|
496
|
+
hideValues?: boolean,
|
|
497
|
+
): string => {
|
|
498
|
+
const paddingRight = 50;
|
|
499
|
+
const offsetWidth = width - paddingRight;
|
|
500
|
+
// progressOffset holds the previous language's width and used to offset the next language
|
|
501
|
+
// so that we can stack them one after another, like this: [--][----][---]
|
|
502
|
+
let progressOffset = 0;
|
|
503
|
+
const compactProgressBar = langs
|
|
504
|
+
.map((lang) => {
|
|
505
|
+
const langColor = resolveLangColor(lang);
|
|
506
|
+
|
|
507
|
+
const percentage = parseFloat(((lang.size / totalLanguageSize) * offsetWidth).toFixed(2));
|
|
508
|
+
|
|
509
|
+
const progress = percentage < 10 ? percentage + 10 : percentage;
|
|
510
|
+
|
|
511
|
+
const output = `
|
|
512
|
+
<rect
|
|
513
|
+
mask="url(#rect-mask)"
|
|
514
|
+
data-testid="lang-progress"
|
|
515
|
+
x="${progressOffset}"
|
|
516
|
+
y="0"
|
|
517
|
+
width="${progress}"
|
|
518
|
+
height="8"
|
|
519
|
+
fill="${langColor}"
|
|
520
|
+
/>
|
|
521
|
+
`;
|
|
522
|
+
progressOffset += percentage;
|
|
523
|
+
return output;
|
|
524
|
+
})
|
|
525
|
+
.join('');
|
|
526
|
+
|
|
527
|
+
return `
|
|
528
|
+
${
|
|
529
|
+
hideProgress
|
|
530
|
+
? ''
|
|
531
|
+
: `
|
|
532
|
+
<mask id="rect-mask">
|
|
533
|
+
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
|
534
|
+
</mask>
|
|
535
|
+
${compactProgressBar}
|
|
536
|
+
`
|
|
537
|
+
}
|
|
538
|
+
<g transform="translate(0, ${hideProgress ? '0' : '25'})">
|
|
539
|
+
${createLanguageTextNode({
|
|
540
|
+
langs,
|
|
541
|
+
totalSize: totalLanguageSize,
|
|
542
|
+
hideProgress,
|
|
543
|
+
statsFormat,
|
|
544
|
+
hideValues,
|
|
545
|
+
})}
|
|
546
|
+
</g>
|
|
547
|
+
`;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Renders donut vertical layout to display user's most frequently used programming languages.
|
|
552
|
+
*
|
|
553
|
+
* @param langs Array of programming languages.
|
|
554
|
+
* @param totalLanguageSize Total size of all languages.
|
|
555
|
+
* @param statsFormat Stats format.
|
|
556
|
+
* @param hideValues Whether to hide stats values.
|
|
557
|
+
* @returns Compact layout card SVG object.
|
|
558
|
+
*/
|
|
559
|
+
const renderDonutVerticalLayout = (
|
|
560
|
+
langs: Array<Lang>,
|
|
561
|
+
totalLanguageSize: number,
|
|
562
|
+
statsFormat: string,
|
|
563
|
+
hideValues?: boolean,
|
|
564
|
+
): string => {
|
|
565
|
+
// Donut vertical chart radius and total length
|
|
566
|
+
const radius = 80;
|
|
567
|
+
const totalCircleLength = getCircleLength(radius);
|
|
568
|
+
|
|
569
|
+
// SVG circles
|
|
570
|
+
const circles = [];
|
|
571
|
+
|
|
572
|
+
// Start indent for donut vertical chart parts
|
|
573
|
+
let indent = 0;
|
|
574
|
+
|
|
575
|
+
// Start delay coefficient for donut vertical chart parts
|
|
576
|
+
let startDelayCoefficient = 1;
|
|
577
|
+
|
|
578
|
+
// Generate each donut vertical chart part
|
|
579
|
+
for (const lang of langs) {
|
|
580
|
+
const langColor = resolveLangColor(lang);
|
|
581
|
+
|
|
582
|
+
const percentage = (lang.size / totalLanguageSize) * 100;
|
|
583
|
+
const circleLength = totalCircleLength * (percentage / 100);
|
|
584
|
+
const delay = startDelayCoefficient * 100;
|
|
585
|
+
|
|
586
|
+
circles.push(`
|
|
587
|
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
|
588
|
+
<circle
|
|
589
|
+
cx="150"
|
|
590
|
+
cy="100"
|
|
591
|
+
r="${radius}"
|
|
592
|
+
fill="transparent"
|
|
593
|
+
stroke="${langColor}"
|
|
594
|
+
stroke-width="25"
|
|
595
|
+
stroke-dasharray="${totalCircleLength}"
|
|
596
|
+
stroke-dashoffset="${indent}"
|
|
597
|
+
size="${percentage}"
|
|
598
|
+
data-testid="lang-donut"
|
|
599
|
+
/>
|
|
600
|
+
</g>
|
|
601
|
+
`);
|
|
602
|
+
|
|
603
|
+
// Update the indent for the next part
|
|
604
|
+
indent += circleLength;
|
|
605
|
+
// Update the start delay coefficient for the next part
|
|
606
|
+
startDelayCoefficient += 1;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return `
|
|
610
|
+
<svg data-testid="lang-items">
|
|
611
|
+
<g transform="translate(0, 0)">
|
|
612
|
+
<svg data-testid="donut">
|
|
613
|
+
${circles.join('')}
|
|
614
|
+
</svg>
|
|
615
|
+
</g>
|
|
616
|
+
<g transform="translate(0, 220)">
|
|
617
|
+
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
|
618
|
+
${createLanguageTextNode({
|
|
619
|
+
langs,
|
|
620
|
+
totalSize: totalLanguageSize,
|
|
621
|
+
hideProgress: false,
|
|
622
|
+
statsFormat,
|
|
623
|
+
hideValues,
|
|
624
|
+
})}
|
|
625
|
+
</svg>
|
|
626
|
+
</g>
|
|
627
|
+
</svg>
|
|
628
|
+
`;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Renders pie layout to display user's most frequently used programming languages.
|
|
633
|
+
*
|
|
634
|
+
* @param langs Array of programming languages.
|
|
635
|
+
* @param totalLanguageSize Total size of all languages.
|
|
636
|
+
* @param statsFormat Stats format.
|
|
637
|
+
* @param hideValues Whether to hide stats values.
|
|
638
|
+
* @returns Compact layout card SVG object.
|
|
639
|
+
*/
|
|
640
|
+
const renderPieLayout = (
|
|
641
|
+
langs: Array<Lang>,
|
|
642
|
+
totalLanguageSize: number,
|
|
643
|
+
statsFormat: string,
|
|
644
|
+
hideValues?: boolean,
|
|
645
|
+
): string => {
|
|
646
|
+
// Pie chart radius and center coordinates
|
|
647
|
+
const radius = 90;
|
|
648
|
+
const centerX = 150;
|
|
649
|
+
const centerY = 100;
|
|
650
|
+
|
|
651
|
+
// Start angle for the pie chart parts
|
|
652
|
+
let startAngle = 0;
|
|
653
|
+
|
|
654
|
+
// Start delay coefficient for the pie chart parts
|
|
655
|
+
let startDelayCoefficient = 1;
|
|
656
|
+
|
|
657
|
+
// SVG paths
|
|
658
|
+
const paths = [];
|
|
659
|
+
|
|
660
|
+
// Generate each pie chart part
|
|
661
|
+
for (const lang of langs) {
|
|
662
|
+
const langColor = resolveLangColor(lang);
|
|
663
|
+
|
|
664
|
+
if (langs.length === 1) {
|
|
665
|
+
paths.push(`
|
|
666
|
+
<circle
|
|
667
|
+
cx="${centerX}"
|
|
668
|
+
cy="${centerY}"
|
|
669
|
+
r="${radius}"
|
|
670
|
+
stroke="none"
|
|
671
|
+
fill="${langColor}"
|
|
672
|
+
data-testid="lang-pie"
|
|
673
|
+
size="100"
|
|
674
|
+
/>
|
|
675
|
+
`);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const langSizePart = lang.size / totalLanguageSize;
|
|
680
|
+
const percentage = langSizePart * 100;
|
|
681
|
+
// Calculate the angle for the current part
|
|
682
|
+
const angle = langSizePart * 360;
|
|
683
|
+
|
|
684
|
+
// Calculate the end angle
|
|
685
|
+
const endAngle = startAngle + angle;
|
|
686
|
+
|
|
687
|
+
// Calculate the coordinates of the start and end points of the arc
|
|
688
|
+
const startPoint = polarToCartesian(centerX, centerY, radius, startAngle);
|
|
689
|
+
const endPoint = polarToCartesian(centerX, centerY, radius, endAngle);
|
|
690
|
+
|
|
691
|
+
// Determine the large arc flag based on the angle
|
|
692
|
+
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
693
|
+
|
|
694
|
+
// Calculate delay
|
|
695
|
+
const delay = startDelayCoefficient * 100;
|
|
696
|
+
|
|
697
|
+
// SVG arc markup
|
|
698
|
+
paths.push(`
|
|
699
|
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
|
700
|
+
<path
|
|
701
|
+
data-testid="lang-pie"
|
|
702
|
+
size="${percentage}"
|
|
703
|
+
d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
|
|
704
|
+
fill="${langColor}"
|
|
705
|
+
/>
|
|
706
|
+
</g>
|
|
707
|
+
`);
|
|
708
|
+
|
|
709
|
+
// Update the start angle for the next part
|
|
710
|
+
startAngle = endAngle;
|
|
711
|
+
// Update the start delay coefficient for the next part
|
|
712
|
+
startDelayCoefficient += 1;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return `
|
|
716
|
+
<svg data-testid="lang-items">
|
|
717
|
+
<g transform="translate(0, 0)">
|
|
718
|
+
<svg data-testid="pie">
|
|
719
|
+
${paths.join('')}
|
|
720
|
+
</svg>
|
|
721
|
+
</g>
|
|
722
|
+
<g transform="translate(0, 220)">
|
|
723
|
+
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
|
724
|
+
${createLanguageTextNode({
|
|
725
|
+
langs,
|
|
726
|
+
totalSize: totalLanguageSize,
|
|
727
|
+
hideProgress: false,
|
|
728
|
+
statsFormat,
|
|
729
|
+
hideValues,
|
|
730
|
+
})}
|
|
731
|
+
</svg>
|
|
732
|
+
</g>
|
|
733
|
+
</svg>
|
|
734
|
+
`;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Creates the SVG paths for the language donut chart.
|
|
739
|
+
*
|
|
740
|
+
* @param cx Donut center x-position.
|
|
741
|
+
* @param cy Donut center y-position.
|
|
742
|
+
* @param radius Donut arc Radius.
|
|
743
|
+
* @param percentages Array with donut section percentages.
|
|
744
|
+
* @returns Array of svg path elements
|
|
745
|
+
*/
|
|
746
|
+
const createDonutPaths = (
|
|
747
|
+
cx: number,
|
|
748
|
+
cy: number,
|
|
749
|
+
radius: number,
|
|
750
|
+
percentages: Array<number>,
|
|
751
|
+
): Array<{ d: string; percent: number }> => {
|
|
752
|
+
const paths: Array<{ d: string; percent: number }> = [];
|
|
753
|
+
let startAngle = 0;
|
|
754
|
+
|
|
755
|
+
const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0);
|
|
756
|
+
for (const rawPercent of percentages) {
|
|
757
|
+
const percent = parseFloat(((rawPercent / totalPercent) * 100).toFixed(2));
|
|
758
|
+
|
|
759
|
+
const endAngle = 3.6 * percent + startAngle;
|
|
760
|
+
const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
|
761
|
+
const endPoint = polarToCartesian(cx, cy, radius, startAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
|
762
|
+
const largeArc = endAngle - startAngle <= 180 ? 0 : 1;
|
|
763
|
+
|
|
764
|
+
paths.push({
|
|
765
|
+
percent,
|
|
766
|
+
d: `M ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArc} 0 ${endPoint.x} ${endPoint.y}`,
|
|
767
|
+
});
|
|
768
|
+
startAngle = endAngle;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return paths;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Renders the donut language card layout.
|
|
776
|
+
*
|
|
777
|
+
* @param langs Array of programming languages.
|
|
778
|
+
* @param width Card width.
|
|
779
|
+
* @param totalLanguageSize Total size of all languages.
|
|
780
|
+
* @param statsFormat Stats format.
|
|
781
|
+
* @param hideValues Whether to hide stats values.
|
|
782
|
+
* @returns Donut layout card SVG object.
|
|
783
|
+
*/
|
|
784
|
+
const renderDonutLayout = (
|
|
785
|
+
langs: Array<Lang>,
|
|
786
|
+
width: number,
|
|
787
|
+
totalLanguageSize: number,
|
|
788
|
+
statsFormat: string,
|
|
789
|
+
hideValues?: boolean,
|
|
790
|
+
): string => {
|
|
791
|
+
if (!Number.isFinite(width)) {
|
|
792
|
+
throw new Error(`Invalid width: "${width}"`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const centerX = width / 3;
|
|
796
|
+
const centerY = width / 3;
|
|
797
|
+
const radius = centerX - 60;
|
|
798
|
+
const strokeWidth = 12;
|
|
799
|
+
|
|
800
|
+
const colors = langs.map(resolveLangColor);
|
|
801
|
+
const langsPercents = langs.map((lang) =>
|
|
802
|
+
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
const langPaths = createDonutPaths(centerX, centerY, radius, langsPercents);
|
|
806
|
+
|
|
807
|
+
const donutPaths =
|
|
808
|
+
langs.length === 1
|
|
809
|
+
? `<circle cx="${centerX}" cy="${centerY}" r="${radius}" stroke="${colors[0] ?? DEFAULT_LANG_COLOR}" fill="none" stroke-width="${strokeWidth}" data-testid="lang-donut" size="100"/>`
|
|
810
|
+
: langPaths
|
|
811
|
+
.map((section, index) => {
|
|
812
|
+
const staggerDelay = (index + 3) * 100;
|
|
813
|
+
const delay = staggerDelay + 300;
|
|
814
|
+
|
|
815
|
+
const output = `
|
|
816
|
+
<g class="stagger" style="animation-delay: ${delay}ms">
|
|
817
|
+
<path
|
|
818
|
+
data-testid="lang-donut"
|
|
819
|
+
size="${section.percent}"
|
|
820
|
+
d="${section.d}"
|
|
821
|
+
stroke="${colors[index] ?? DEFAULT_LANG_COLOR}"
|
|
822
|
+
fill="none"
|
|
823
|
+
stroke-width="${strokeWidth}">
|
|
824
|
+
</path>
|
|
825
|
+
</g>
|
|
826
|
+
`;
|
|
827
|
+
|
|
828
|
+
return output;
|
|
829
|
+
})
|
|
830
|
+
.join('');
|
|
831
|
+
|
|
832
|
+
const donut = `<svg width="${width}" height="${width}">${donutPaths}</svg>`;
|
|
833
|
+
|
|
834
|
+
return `
|
|
835
|
+
<g transform="translate(0, 0)">
|
|
836
|
+
<g transform="translate(0, 0)">
|
|
837
|
+
${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, hideValues, statsFormat })}
|
|
838
|
+
</g>
|
|
839
|
+
|
|
840
|
+
<g transform="translate(125, ${donutCenterTranslation(langs.length)})">
|
|
841
|
+
${donut}
|
|
842
|
+
</g>
|
|
843
|
+
</g>
|
|
844
|
+
`;
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Creates the no languages data SVG node.
|
|
849
|
+
*
|
|
850
|
+
* @param props Object with function properties.
|
|
851
|
+
* @param props.text No languages data translated text.
|
|
852
|
+
* @param props.layout Card layout.
|
|
853
|
+
* @returns No languages data SVG node string.
|
|
854
|
+
*/
|
|
855
|
+
const noLanguagesDataNode = ({
|
|
856
|
+
text,
|
|
857
|
+
layout,
|
|
858
|
+
}: {
|
|
859
|
+
text: string;
|
|
860
|
+
layout: TopLangLayout | undefined;
|
|
861
|
+
}): string => {
|
|
862
|
+
return `
|
|
863
|
+
<text x="${
|
|
864
|
+
layout === 'pie' || layout === 'donut-vertical' ? CARD_PADDING : 0
|
|
865
|
+
}" y="11" class="stat bold">${encodeHTML(text)}</text>
|
|
866
|
+
`;
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Get default languages count for provided card layout.
|
|
871
|
+
*
|
|
872
|
+
* @param props Function properties.
|
|
873
|
+
* @param props.layout Input layout string.
|
|
874
|
+
* @param props.hide_progress Input hide_progress parameter value.
|
|
875
|
+
* @returns Default languages count for input layout.
|
|
876
|
+
*/
|
|
877
|
+
const getDefaultLanguagesCountByLayout = ({
|
|
878
|
+
layout,
|
|
879
|
+
hide_progress,
|
|
880
|
+
}: {
|
|
881
|
+
layout?: TopLangLayout | undefined;
|
|
882
|
+
hide_progress?: boolean | undefined;
|
|
883
|
+
}): number => {
|
|
884
|
+
if (layout === 'compact' || hide_progress === true) {
|
|
885
|
+
return COMPACT_LAYOUT_DEFAULT_LANGS_COUNT;
|
|
886
|
+
} else if (layout === 'donut') {
|
|
887
|
+
return DONUT_LAYOUT_DEFAULT_LANGS_COUNT;
|
|
888
|
+
} else if (layout === 'donut-vertical') {
|
|
889
|
+
return DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
|
890
|
+
} else if (layout === 'pie') {
|
|
891
|
+
return PIE_LAYOUT_DEFAULT_LANGS_COUNT;
|
|
892
|
+
} else {
|
|
893
|
+
return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Renders card that display user's most frequently used programming languages.
|
|
899
|
+
*
|
|
900
|
+
* @param topLangs User's most frequently used programming languages.
|
|
901
|
+
* @param options Card options.
|
|
902
|
+
* @returns Language card SVG object.
|
|
903
|
+
*/
|
|
904
|
+
const renderTopLanguages = (
|
|
905
|
+
topLangs: TopLangData,
|
|
906
|
+
options: CardOptions<TopLangOptions> = {},
|
|
907
|
+
): string => {
|
|
908
|
+
const {
|
|
909
|
+
hide_title = false,
|
|
910
|
+
hide_border = false,
|
|
911
|
+
card_width,
|
|
912
|
+
hide,
|
|
913
|
+
hide_progress,
|
|
914
|
+
hide_values,
|
|
915
|
+
layout,
|
|
916
|
+
custom_title,
|
|
917
|
+
locale,
|
|
918
|
+
langs_count = getDefaultLanguagesCountByLayout({ layout, hide_progress }),
|
|
919
|
+
border_radius,
|
|
920
|
+
disable_animations,
|
|
921
|
+
stats_format = 'percentages',
|
|
922
|
+
} = options;
|
|
923
|
+
|
|
924
|
+
const i18n = new I18n({
|
|
925
|
+
locale,
|
|
926
|
+
translations: langCardLocales,
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const { langs, totalLanguageSize } = trimTopLanguages(topLangs, langs_count, hide);
|
|
930
|
+
|
|
931
|
+
let width = card_width
|
|
932
|
+
? isNaN(card_width)
|
|
933
|
+
? DEFAULT_CARD_WIDTH
|
|
934
|
+
: card_width < MIN_CARD_WIDTH
|
|
935
|
+
? MIN_CARD_WIDTH
|
|
936
|
+
: card_width
|
|
937
|
+
: DEFAULT_CARD_WIDTH;
|
|
938
|
+
let height = calculateNormalLayoutHeight(langs.length);
|
|
939
|
+
|
|
940
|
+
const { lightColors, darkColors } = getLightDarkColors(options);
|
|
941
|
+
|
|
942
|
+
let finalLayout: string;
|
|
943
|
+
if (langs.length === 0) {
|
|
944
|
+
height = COMPACT_LAYOUT_BASE_HEIGHT;
|
|
945
|
+
finalLayout = noLanguagesDataNode({
|
|
946
|
+
text: i18n.t('langcard.nodata'),
|
|
947
|
+
layout,
|
|
948
|
+
});
|
|
949
|
+
} else if (layout === 'pie') {
|
|
950
|
+
height = calculatePieLayoutHeight(langs.length);
|
|
951
|
+
finalLayout = renderPieLayout(langs, totalLanguageSize, stats_format, hide_values);
|
|
952
|
+
} else if (layout === 'donut-vertical') {
|
|
953
|
+
height = calculateDonutVerticalLayoutHeight(langs.length);
|
|
954
|
+
finalLayout = renderDonutVerticalLayout(langs, totalLanguageSize, stats_format, hide_values);
|
|
955
|
+
} else if (layout === 'compact' || hide_progress === true) {
|
|
956
|
+
height = calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
|
957
|
+
|
|
958
|
+
finalLayout = renderCompactLayout(
|
|
959
|
+
langs,
|
|
960
|
+
width,
|
|
961
|
+
totalLanguageSize,
|
|
962
|
+
hide_progress,
|
|
963
|
+
stats_format,
|
|
964
|
+
hide_values,
|
|
965
|
+
);
|
|
966
|
+
} else if (layout === 'donut') {
|
|
967
|
+
height = calculateDonutLayoutHeight(langs.length);
|
|
968
|
+
width = width + 50; // padding
|
|
969
|
+
finalLayout = renderDonutLayout(langs, width, totalLanguageSize, stats_format, hide_values);
|
|
970
|
+
} else {
|
|
971
|
+
finalLayout = renderNormalLayout(langs, width, totalLanguageSize, stats_format, hide_values);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const card = new Card({
|
|
975
|
+
customTitle: custom_title,
|
|
976
|
+
defaultTitle: i18n.t('langcard.title'),
|
|
977
|
+
width,
|
|
978
|
+
height,
|
|
979
|
+
border_radius,
|
|
980
|
+
colors: { light: lightColors, dark: darkColors },
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
if (disable_animations) {
|
|
984
|
+
card.disableAnimations();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
card.setHideBorder(hide_border);
|
|
988
|
+
card.setHideTitle(hide_title);
|
|
989
|
+
card.setCSS({
|
|
990
|
+
light: ({ textColor, progBarBgColor }) => `
|
|
991
|
+
@keyframes slideInAnimation {
|
|
992
|
+
from {
|
|
993
|
+
width: 0;
|
|
994
|
+
}
|
|
995
|
+
to {
|
|
996
|
+
width: calc(100%-100px);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
@keyframes growWidthAnimation {
|
|
1000
|
+
from {
|
|
1001
|
+
width: 0;
|
|
1002
|
+
}
|
|
1003
|
+
to {
|
|
1004
|
+
width: 100%;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
.stat {
|
|
1008
|
+
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
|
1009
|
+
}
|
|
1010
|
+
@supports(-moz-appearance: auto) {
|
|
1011
|
+
/* Selector detects Firefox */
|
|
1012
|
+
.stat { font-size:12px; }
|
|
1013
|
+
}
|
|
1014
|
+
.bold { font-weight: 700 }
|
|
1015
|
+
.lang-name {
|
|
1016
|
+
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
|
1017
|
+
fill: ${textColor};
|
|
1018
|
+
}
|
|
1019
|
+
.stagger {
|
|
1020
|
+
opacity: 0;
|
|
1021
|
+
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
|
1022
|
+
}
|
|
1023
|
+
#rect-mask rect{
|
|
1024
|
+
animation: slideInAnimation 1s ease-in-out forwards;
|
|
1025
|
+
}
|
|
1026
|
+
.lang-progress{
|
|
1027
|
+
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
|
1028
|
+
}
|
|
1029
|
+
.progress-background { fill: ${progBarBgColor}; }
|
|
1030
|
+
`,
|
|
1031
|
+
dark: ({ textColor, progBarBgColor }) => `
|
|
1032
|
+
.stat { fill: ${textColor}; }
|
|
1033
|
+
.lang-name { fill: ${textColor}; }
|
|
1034
|
+
.progress-background { fill: ${progBarBgColor}; }
|
|
1035
|
+
`,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// `role="img"` hides the inner text from assistive tech, so everything the card
|
|
1039
|
+
// shows has to be repeated here.
|
|
1040
|
+
card.setAccessibilityLabel({
|
|
1041
|
+
title: card.title,
|
|
1042
|
+
desc: langs
|
|
1043
|
+
.map((lang) => `${lang.name} ${((lang.size / totalLanguageSize) * 100).toFixed(2)}%`)
|
|
1044
|
+
.join(', '),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
if (layout === 'pie' || layout === 'donut-vertical') {
|
|
1048
|
+
return card.render(finalLayout);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return card.render(`
|
|
1052
|
+
<svg data-testid="lang-items" x="${CARD_PADDING}">
|
|
1053
|
+
${finalLayout}
|
|
1054
|
+
</svg>
|
|
1055
|
+
`);
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
export {
|
|
1059
|
+
getLongestLang,
|
|
1060
|
+
degreesToRadians,
|
|
1061
|
+
radiansToDegrees,
|
|
1062
|
+
polarToCartesian,
|
|
1063
|
+
cartesianToPolar,
|
|
1064
|
+
getCircleLength,
|
|
1065
|
+
calculateCompactLayoutHeight,
|
|
1066
|
+
calculateNormalLayoutHeight,
|
|
1067
|
+
calculateDonutLayoutHeight,
|
|
1068
|
+
calculateDonutVerticalLayoutHeight,
|
|
1069
|
+
calculatePieLayoutHeight,
|
|
1070
|
+
donutCenterTranslation,
|
|
1071
|
+
trimTopLanguages,
|
|
1072
|
+
renderTopLanguages,
|
|
1073
|
+
MIN_CARD_WIDTH,
|
|
1074
|
+
getDefaultLanguagesCountByLayout,
|
|
1075
|
+
TOP_LANG_LAYOUTS,
|
|
1076
|
+
TOP_LANG_STATS_FORMATS,
|
|
1077
|
+
};
|