@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.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/build/api/api-result.d.ts +44 -0
  4. package/build/api/api-result.d.ts.map +1 -0
  5. package/build/api/api-result.js +33 -0
  6. package/build/api/gist.d.ts +29 -0
  7. package/build/api/gist.d.ts.map +1 -0
  8. package/build/api/gist.js +52 -0
  9. package/build/api/index.d.ts +13 -0
  10. package/build/api/index.d.ts.map +1 -0
  11. package/build/api/index.js +8 -0
  12. package/build/api/params.d.ts +93 -0
  13. package/build/api/params.d.ts.map +1 -0
  14. package/build/api/params.js +145 -0
  15. package/build/api/pin.d.ts +47 -0
  16. package/build/api/pin.d.ts.map +1 -0
  17. package/build/api/pin.js +87 -0
  18. package/build/api/stats.d.ts +42 -0
  19. package/build/api/stats.d.ts.map +1 -0
  20. package/build/api/stats.js +132 -0
  21. package/build/api/top-langs.d.ts +37 -0
  22. package/build/api/top-langs.d.ts.map +1 -0
  23. package/build/api/top-langs.js +101 -0
  24. package/build/api/wakatime.d.ts +34 -0
  25. package/build/api/wakatime.d.ts.map +1 -0
  26. package/build/api/wakatime.js +91 -0
  27. package/build/calculateRank.d.ts +29 -0
  28. package/build/calculateRank.d.ts.map +1 -0
  29. package/build/calculateRank.js +58 -0
  30. package/build/cards/gist.d.ts +16 -0
  31. package/build/cards/gist.d.ts.map +1 -0
  32. package/build/cards/gist.js +119 -0
  33. package/build/cards/index.d.ts +11 -0
  34. package/build/cards/index.d.ts.map +1 -0
  35. package/build/cards/index.js +7 -0
  36. package/build/cards/options.d.ts +19 -0
  37. package/build/cards/options.d.ts.map +1 -0
  38. package/build/cards/options.js +1 -0
  39. package/build/cards/repo.d.ts +25 -0
  40. package/build/cards/repo.d.ts.map +1 -0
  41. package/build/cards/repo.js +276 -0
  42. package/build/cards/stats.d.ts +37 -0
  43. package/build/cards/stats.d.ts.map +1 -0
  44. package/build/cards/stats.js +457 -0
  45. package/build/cards/top-languages.d.ts +153 -0
  46. package/build/cards/top-languages.d.ts.map +1 -0
  47. package/build/cards/top-languages.js +805 -0
  48. package/build/cards/wakatime.d.ts +31 -0
  49. package/build/cards/wakatime.d.ts.map +1 -0
  50. package/build/cards/wakatime.js +376 -0
  51. package/build/common/Card.d.ts +121 -0
  52. package/build/common/Card.d.ts.map +1 -0
  53. package/build/common/Card.js +305 -0
  54. package/build/common/I18n.d.ts +27 -0
  55. package/build/common/I18n.d.ts.map +1 -0
  56. package/build/common/I18n.js +35 -0
  57. package/build/common/color.d.ts +112 -0
  58. package/build/common/color.d.ts.map +1 -0
  59. package/build/common/color.js +188 -0
  60. package/build/common/config.d.ts +53 -0
  61. package/build/common/config.d.ts.map +1 -0
  62. package/build/common/config.js +87 -0
  63. package/build/common/constants.d.ts +7 -0
  64. package/build/common/constants.d.ts.map +1 -0
  65. package/build/common/constants.js +9 -0
  66. package/build/common/date.d.ts +29 -0
  67. package/build/common/date.d.ts.map +1 -0
  68. package/build/common/date.js +23 -0
  69. package/build/common/error.d.ts +87 -0
  70. package/build/common/error.d.ts.map +1 -0
  71. package/build/common/error.js +108 -0
  72. package/build/common/fmt.d.ts +28 -0
  73. package/build/common/fmt.d.ts.map +1 -0
  74. package/build/common/fmt.js +68 -0
  75. package/build/common/html.d.ts +8 -0
  76. package/build/common/html.d.ts.map +1 -0
  77. package/build/common/html.js +14 -0
  78. package/build/common/http.d.ts +53 -0
  79. package/build/common/http.d.ts.map +1 -0
  80. package/build/common/http.js +47 -0
  81. package/build/common/icons.d.ts +28 -0
  82. package/build/common/icons.d.ts.map +1 -0
  83. package/build/common/icons.js +77 -0
  84. package/build/common/languageColors.d.ts +11 -0
  85. package/build/common/languageColors.d.ts.map +1 -0
  86. package/build/common/languageColors.js +15 -0
  87. package/build/common/languageColors.json +678 -0
  88. package/build/common/log.d.ts +9 -0
  89. package/build/common/log.d.ts.map +1 -0
  90. package/build/common/log.js +8 -0
  91. package/build/common/ops.d.ts +59 -0
  92. package/build/common/ops.d.ts.map +1 -0
  93. package/build/common/ops.js +115 -0
  94. package/build/common/render.d.ts +196 -0
  95. package/build/common/render.d.ts.map +1 -0
  96. package/build/common/render.js +474 -0
  97. package/build/common/retryer.d.ts +36 -0
  98. package/build/common/retryer.d.ts.map +1 -0
  99. package/build/common/retryer.js +67 -0
  100. package/build/fetchers/gist.d.ts +15 -0
  101. package/build/fetchers/gist.d.ts.map +1 -0
  102. package/build/fetchers/gist.js +68 -0
  103. package/build/fetchers/index.d.ts +13 -0
  104. package/build/fetchers/index.d.ts.map +1 -0
  105. package/build/fetchers/index.js +10 -0
  106. package/build/fetchers/repo.d.ts +27 -0
  107. package/build/fetchers/repo.d.ts.map +1 -0
  108. package/build/fetchers/repo.js +100 -0
  109. package/build/fetchers/stats.d.ts +74 -0
  110. package/build/fetchers/stats.d.ts.map +1 -0
  111. package/build/fetchers/stats.js +498 -0
  112. package/build/fetchers/top-languages.d.ts +23 -0
  113. package/build/fetchers/top-languages.d.ts.map +1 -0
  114. package/build/fetchers/top-languages.js +89 -0
  115. package/build/fetchers/types.d.ts +130 -0
  116. package/build/fetchers/types.d.ts.map +1 -0
  117. package/build/fetchers/types.js +1 -0
  118. package/build/fetchers/wakatime.d.ts +19 -0
  119. package/build/fetchers/wakatime.d.ts.map +1 -0
  120. package/build/fetchers/wakatime.js +36 -0
  121. package/build/graphql/contributionsDocument.d.ts +18 -0
  122. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  123. package/build/graphql/contributionsDocument.js +30 -0
  124. package/build/graphql/generated/common.d.ts +42 -0
  125. package/build/graphql/generated/common.d.ts.map +1 -0
  126. package/build/graphql/generated/common.js +2 -0
  127. package/build/graphql/generated/gist.d.ts +44 -0
  128. package/build/graphql/generated/gist.d.ts.map +1 -0
  129. package/build/graphql/generated/gist.js +27 -0
  130. package/build/graphql/generated/repo.d.ts +67 -0
  131. package/build/graphql/generated/repo.d.ts.map +1 -0
  132. package/build/graphql/generated/repo.js +30 -0
  133. package/build/graphql/generated/stats.d.ts +150 -0
  134. package/build/graphql/generated/stats.d.ts.map +1 -0
  135. package/build/graphql/generated/stats.js +95 -0
  136. package/build/graphql/generated/top-languages.d.ts +54 -0
  137. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  138. package/build/graphql/generated/top-languages.js +27 -0
  139. package/build/graphql/graphqlDocument.d.ts +22 -0
  140. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  141. package/build/graphql/graphqlDocument.js +8 -0
  142. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  143. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  144. package/build/graphql/reposContributedToDocument.js +59 -0
  145. package/build/index.d.ts +15 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +10 -0
  148. package/build/themes/index.d.ts +505 -0
  149. package/build/themes/index.d.ts.map +1 -0
  150. package/build/themes/index.js +505 -0
  151. package/build/translations.d.ts +1153 -0
  152. package/build/translations.d.ts.map +1 -0
  153. package/build/translations.js +1156 -0
  154. package/package.json +114 -0
  155. package/src/_emoji-name-map.d.ts +10 -0
  156. package/src/_github-username-regex.d.ts +4 -0
  157. package/src/api/api-result.ts +69 -0
  158. package/src/api/gist.ts +66 -0
  159. package/src/api/index.ts +16 -0
  160. package/src/api/params.ts +221 -0
  161. package/src/api/pin.ts +126 -0
  162. package/src/api/stats.ts +195 -0
  163. package/src/api/top-langs.ts +150 -0
  164. package/src/api/wakatime.ts +130 -0
  165. package/src/calculateRank.ts +91 -0
  166. package/src/cards/gist.ts +169 -0
  167. package/src/cards/index.ts +21 -0
  168. package/src/cards/options.ts +18 -0
  169. package/src/cards/repo.ts +376 -0
  170. package/src/cards/stats.ts +615 -0
  171. package/src/cards/top-languages.ts +1077 -0
  172. package/src/cards/wakatime.ts +501 -0
  173. package/src/common/Card.ts +364 -0
  174. package/src/common/I18n.ts +51 -0
  175. package/src/common/color.ts +296 -0
  176. package/src/common/config.ts +118 -0
  177. package/src/common/constants.ts +12 -0
  178. package/src/common/date.ts +34 -0
  179. package/src/common/error.ts +163 -0
  180. package/src/common/fmt.ts +86 -0
  181. package/src/common/html.ts +17 -0
  182. package/src/common/http.ts +98 -0
  183. package/src/common/icons.ts +80 -0
  184. package/src/common/languageColors.json +678 -0
  185. package/src/common/languageColors.ts +19 -0
  186. package/src/common/log.ts +12 -0
  187. package/src/common/ops.ts +145 -0
  188. package/src/common/render.ts +641 -0
  189. package/src/common/retryer.ts +111 -0
  190. package/src/fetchers/gist.ts +82 -0
  191. package/src/fetchers/index.ts +25 -0
  192. package/src/fetchers/repo.ts +140 -0
  193. package/src/fetchers/stats.ts +742 -0
  194. package/src/fetchers/top-languages.ts +134 -0
  195. package/src/fetchers/types.ts +136 -0
  196. package/src/fetchers/wakatime.ts +56 -0
  197. package/src/graphql/contributionsDocument.ts +44 -0
  198. package/src/graphql/generated/common.ts +26 -0
  199. package/src/graphql/generated/gist.ts +56 -0
  200. package/src/graphql/generated/repo.ts +79 -0
  201. package/src/graphql/generated/stats.ts +180 -0
  202. package/src/graphql/generated/top-languages.ts +66 -0
  203. package/src/graphql/graphqlDocument.ts +25 -0
  204. package/src/graphql/queries/gist.graphql +25 -0
  205. package/src/graphql/queries/repo.graphql +28 -0
  206. package/src/graphql/queries/stats.graphql +113 -0
  207. package/src/graphql/queries/top-languages.graphql +27 -0
  208. package/src/graphql/reposContributedToDocument.ts +83 -0
  209. package/src/index.ts +18 -0
  210. package/src/themes/index.ts +519 -0
  211. 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
+ };