@gh-top-languages/lib 1.0.3 → 1.0.4

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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -33
  3. package/dist/charts/donut.d.ts +1 -1
  4. package/dist/charts/donut.d.ts.map +1 -1
  5. package/dist/charts/donut.js +5 -6
  6. package/dist/charts/donut.js.map +1 -1
  7. package/dist/charts/geometry.js +11 -11
  8. package/dist/charts/layout.d.ts +13 -2
  9. package/dist/charts/layout.d.ts.map +1 -1
  10. package/dist/charts/layout.js +27 -8
  11. package/dist/charts/layout.js.map +1 -1
  12. package/dist/charts/legend.d.ts +1 -1
  13. package/dist/charts/legend.d.ts.map +1 -1
  14. package/dist/charts/legend.js +20 -20
  15. package/dist/charts/legend.js.map +1 -1
  16. package/dist/charts/pie.d.ts +1 -1
  17. package/dist/charts/pie.d.ts.map +1 -1
  18. package/dist/charts/pie.js +5 -6
  19. package/dist/charts/pie.js.map +1 -1
  20. package/dist/constants/styles.d.ts +4 -2
  21. package/dist/constants/styles.d.ts.map +1 -1
  22. package/dist/constants/styles.js +18 -2
  23. package/dist/constants/styles.js.map +1 -1
  24. package/dist/render/chart.d.ts +1 -1
  25. package/dist/render/chart.d.ts.map +1 -1
  26. package/dist/render/chart.js +2 -2
  27. package/dist/render/chart.js.map +1 -1
  28. package/dist/render/error.js +7 -7
  29. package/dist/render/svg.d.ts +2 -1
  30. package/dist/render/svg.d.ts.map +1 -1
  31. package/dist/render/svg.js +21 -18
  32. package/dist/render/svg.js.map +1 -1
  33. package/dist/types.d.ts +2 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +48 -48
  36. package/src/charts/donut.ts +38 -22
  37. package/src/charts/geometry.ts +88 -88
  38. package/src/charts/layout.ts +50 -19
  39. package/src/charts/legend.ts +54 -53
  40. package/src/charts/pie.ts +41 -22
  41. package/src/constants/config.ts +11 -11
  42. package/src/constants/geometry.ts +9 -9
  43. package/src/constants/styles.ts +40 -23
  44. package/src/constants/themes.ts +68 -68
  45. package/src/constants/types.ts +4 -4
  46. package/src/render/chart.ts +22 -24
  47. package/src/render/error.ts +25 -25
  48. package/src/render/svg.ts +30 -31
  49. package/src/types.ts +30 -28
  50. package/src/utils/params.ts +47 -47
  51. package/src/utils/sanitize.ts +14 -14
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "@gh-top-languages/lib",
3
- "description": "Library for github-top-languages — chart generation, SVG output, and parameter parsing",
4
- "author": "Mason L'Etoile",
5
- "version": "1.0.3",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/gh-top-languages/lib.git"
10
- },
11
- "homepage": "https://github.com/gh-top-languages/lib#readme",
12
- "bugs": {
13
- "url": "https://github.com/gh-top-languages/lib/issues"
14
- },
15
- "keywords": ["svg", "charts", "github-profile", "github-top-languages"],
16
- "type": "module",
17
- "scripts": {
18
- "test": "vitest",
19
- "test:coverage": "vitest --coverage",
20
- "typecheck": "tsc --noEmit",
21
- "build": "tsc -p tsconfig.build.json",
22
- "prepare": "tsc -p tsconfig.build.json"
23
- },
24
- "devDependencies": {
25
- "typescript": "~6.0.2",
26
- "vitest": "^4.0.17",
27
- "@vitest/coverage-v8": "^4.0.17"
28
- },
29
- "files": ["dist", "src"],
30
- "exports": {
31
- "./types.js": { "types": "./dist/types.d.ts", "import": "./dist/types.js" },
32
- "./charts/donut.js": { "types": "./dist/charts/donut.d.ts", "import": "./dist/charts/donut.js" },
33
- "./charts/pie.js": { "types": "./dist/charts/pie.d.ts", "import": "./dist/charts/pie.js" },
34
- "./charts/geometry.js": { "types": "./dist/charts/geometry.d.ts", "import": "./dist/charts/geometry.js" },
35
- "./charts/legend.js": { "types": "./dist/charts/legend.d.ts", "import": "./dist/charts/legend.js" },
36
- "./charts/layout.js": { "types": "./dist/charts/layout.d.ts", "import": "./dist/charts/layout.js" },
37
- "./render/chart.js": { "types": "./dist/render/chart.d.ts", "import": "./dist/render/chart.js" },
38
- "./render/svg.js": { "types": "./dist/render/svg.d.ts", "import": "./dist/render/svg.js" },
39
- "./render/error.js": { "types": "./dist/render/error.d.ts", "import": "./dist/render/error.js" },
40
- "./utils/params.js": { "types": "./dist/utils/params.d.ts", "import": "./dist/utils/params.js" },
41
- "./utils/sanitize.js": { "types": "./dist/utils/sanitize.d.ts", "import": "./dist/utils/sanitize.js" },
42
- "./constants/config.js": { "types": "./dist/constants/config.d.ts", "import": "./dist/constants/config.js" },
43
- "./constants/geometry.js": { "types": "./dist/constants/geometry.d.ts", "import": "./dist/constants/geometry.js" },
44
- "./constants/styles.js": { "types": "./dist/constants/styles.d.ts", "import": "./dist/constants/styles.js" },
45
- "./constants/themes.js": { "types": "./dist/constants/themes.d.ts", "import": "./dist/constants/themes.js" },
46
- "./constants/types.js": { "types": "./dist/constants/types.d.ts", "import": "./dist/constants/types.js" }
47
- }
48
- }
1
+ {
2
+ "name": "@gh-top-languages/lib",
3
+ "description": "Library for github-top-languages — chart generation, SVG output, and parameter parsing",
4
+ "author": "Mason L'Etoile",
5
+ "version": "1.0.4",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/gh-top-languages/lib.git"
10
+ },
11
+ "homepage": "https://github.com/gh-top-languages/lib#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/gh-top-languages/lib/issues"
14
+ },
15
+ "keywords": ["svg", "charts", "github-profile", "github-top-languages"],
16
+ "type": "module",
17
+ "scripts": {
18
+ "test": "vitest",
19
+ "test:coverage": "vitest --coverage",
20
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "prepare": "tsc -p tsconfig.build.json"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "~6.0.2",
26
+ "vitest": "^4.0.17",
27
+ "@vitest/coverage-v8": "^4.0.17"
28
+ },
29
+ "files": ["dist", "src"],
30
+ "exports": {
31
+ "./types.js": { "types": "./dist/types.d.ts", "import": "./dist/types.js" },
32
+ "./charts/donut.js": { "types": "./dist/charts/donut.d.ts", "import": "./dist/charts/donut.js" },
33
+ "./charts/pie.js": { "types": "./dist/charts/pie.d.ts", "import": "./dist/charts/pie.js" },
34
+ "./charts/geometry.js": { "types": "./dist/charts/geometry.d.ts", "import": "./dist/charts/geometry.js" },
35
+ "./charts/legend.js": { "types": "./dist/charts/legend.d.ts", "import": "./dist/charts/legend.js" },
36
+ "./charts/layout.js": { "types": "./dist/charts/layout.d.ts", "import": "./dist/charts/layout.js" },
37
+ "./render/chart.js": { "types": "./dist/render/chart.d.ts", "import": "./dist/render/chart.js" },
38
+ "./render/svg.js": { "types": "./dist/render/svg.d.ts", "import": "./dist/render/svg.js" },
39
+ "./render/error.js": { "types": "./dist/render/error.d.ts", "import": "./dist/render/error.js" },
40
+ "./utils/params.js": { "types": "./dist/utils/params.d.ts", "import": "./dist/utils/params.js" },
41
+ "./utils/sanitize.js": { "types": "./dist/utils/sanitize.d.ts", "import": "./dist/utils/sanitize.js" },
42
+ "./constants/config.js": { "types": "./dist/constants/config.d.ts", "import": "./dist/constants/config.js" },
43
+ "./constants/geometry.js": { "types": "./dist/constants/geometry.d.ts", "import": "./dist/constants/geometry.js" },
44
+ "./constants/styles.js": { "types": "./dist/constants/styles.d.ts", "import": "./dist/constants/styles.js" },
45
+ "./constants/themes.js": { "types": "./dist/constants/themes.d.ts", "import": "./dist/constants/themes.js" },
46
+ "./constants/types.js": { "types": "./dist/constants/types.d.ts", "import": "./dist/constants/types.js" }
47
+ }
48
+ }
@@ -1,22 +1,38 @@
1
- import type { Language, Theme, ChartResult } from "../types.js";
2
- import { resolveLayout, calculateChartCenter, calculateLegendStartX } from "./layout.js";
3
- import { DONUT_GEOMETRY } from "../constants/geometry.js";
4
- import { createDonutSegments } from "./geometry.js";
5
- import { createLegend } from "./legend.js";
6
-
7
- export function generateDonutChart(
8
- normalizedLanguages: Language[],
9
- selectedTheme: Theme,
10
- width: number,
11
- stroke: boolean
12
- ): ChartResult {
13
- const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke);
14
-
15
- const chartX = calculateChartCenter(width, isShifted);
16
- const legendStartX = calculateLegendStartX(chartX, DONUT_GEOMETRY.OUTER_RADIUS);
17
-
18
- const segments = createDonutSegments(normalizedLanguages, chartX, DONUT_GEOMETRY, [...selectedTheme.colours], useStroke);
19
- const legend = createLegend(normalizedLanguages, isShifted, selectedTheme, legendStartX, useStroke);
20
-
21
- return { segments, legend };
22
- }
1
+ import type { Language, Theme, ChartResult } from "../types.js";
2
+ import { DONUT_GEOMETRY } from "../constants/geometry.js";
3
+ import { createDonutSegments } from "./geometry.js";
4
+ import { createLegend } from "./legend.js";
5
+ import { resolveLayout, computeLayout } from "./layout.js";
6
+
7
+ export function generateDonutChart(
8
+ normalizedLanguages: Language[],
9
+ selectedTheme: Theme,
10
+ stroke: boolean
11
+ ): ChartResult {
12
+ const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke);
13
+ const {
14
+ chartX,
15
+ legendStartX,
16
+ columnWidth,
17
+ contentWidth,
18
+ contentHeight
19
+ } = computeLayout(normalizedLanguages, isShifted, DONUT_GEOMETRY);
20
+
21
+ const segments = createDonutSegments(
22
+ normalizedLanguages,
23
+ chartX,
24
+ DONUT_GEOMETRY,
25
+ [...selectedTheme.colours],
26
+ useStroke
27
+ );
28
+ const legend = createLegend(
29
+ normalizedLanguages,
30
+ isShifted,
31
+ selectedTheme,
32
+ legendStartX,
33
+ useStroke,
34
+ columnWidth
35
+ );
36
+
37
+ return { segments, legend, contentWidth, contentHeight };
38
+ }
@@ -1,88 +1,88 @@
1
- import type { Point, Language, Geometry } from "../types.js"
2
- import { FULL_CIRCLE_ANGLE } from "../constants/geometry.js";
3
-
4
- export const polarToCartesian = (
5
- cx: number,
6
- cy: number,
7
- r: number,
8
- angleDeg: number
9
- ): Point => {
10
- const angleRad = (angleDeg - 90) * Math.PI / 180;
11
- return {
12
- x: cx + (r * Math.cos(angleRad)),
13
- y: cy + (r * Math.sin(angleRad))
14
- };
15
- };
16
-
17
- export const describeSegment = (
18
- cx: number,
19
- cy: number,
20
- innerR: number,
21
- outerR: number,
22
- startAngle: number,
23
- endAngle: number
24
- ): string => {
25
- const angleDiff = endAngle - startAngle
26
-
27
- if (angleDiff >= 360 || angleDiff <= -360) {
28
- const midAngle = startAngle + 180;
29
- const firstHalf = describeSegment(cx, cy, innerR, outerR, startAngle, midAngle);
30
- const secondHalf = describeSegment(cx, cy, innerR, outerR, midAngle, endAngle);
31
- return firstHalf + ' ' + secondHalf;
32
- }
33
-
34
- const startOuter = polarToCartesian(cx, cy, outerR, endAngle);
35
- const endOuter = polarToCartesian(cx, cy, outerR, startAngle);
36
- const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
37
-
38
- if (innerR === 0) {
39
- return `
40
- M ${cx} ${cy}
41
- L ${startOuter.x} ${startOuter.y}
42
- A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y}
43
- Z
44
- `.trim();
45
- }
46
-
47
- const startInner = polarToCartesian(cx, cy, innerR, startAngle);
48
- const endInner = polarToCartesian(cx, cy, innerR, endAngle);
49
-
50
- return `
51
- M ${startOuter.x} ${startOuter.y}
52
- A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y}
53
- L ${startInner.x} ${startInner.y}
54
- A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${endInner.x} ${endInner.y}
55
- Z
56
- `.trim();
57
- };
58
-
59
- export const createDonutSegments = (
60
- languages: Language[],
61
- cx: number,
62
- geometry: Geometry,
63
- colours: string[],
64
- stroke: boolean
65
- ): string => {
66
- let currentAngle = -0.1;
67
-
68
- return languages.map((lang, i) => {
69
- let angle = (lang.pct / 100) * 360;
70
-
71
- const segmentAngle = Math.min(currentAngle + angle + 0.1, FULL_CIRCLE_ANGLE);
72
- const path = describeSegment(
73
- cx,
74
- geometry.CENTER_Y,
75
- geometry.INNER_RADIUS,
76
- geometry.OUTER_RADIUS,
77
- currentAngle,
78
- segmentAngle
79
- );
80
-
81
- currentAngle += angle;
82
- const fillColour = colours[i % colours.length];
83
- const strokeAttr = stroke
84
- ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"`
85
- : ` stroke="${fillColour}" stroke-width="0.2"`;
86
- return `<path d="${path}" fill="${fillColour}"${strokeAttr} shape-rendering="geometricPrecision"/>`;
87
- }).join('');
88
- }
1
+ import type { Point, Language, Geometry } from "../types.js"
2
+ import { FULL_CIRCLE_ANGLE } from "../constants/geometry.js";
3
+
4
+ export const polarToCartesian = (
5
+ cx: number,
6
+ cy: number,
7
+ r: number,
8
+ angleDeg: number
9
+ ): Point => {
10
+ const angleRad = (angleDeg - 90) * Math.PI / 180;
11
+ return {
12
+ x: cx + (r * Math.cos(angleRad)),
13
+ y: cy + (r * Math.sin(angleRad))
14
+ };
15
+ };
16
+
17
+ export const describeSegment = (
18
+ cx: number,
19
+ cy: number,
20
+ innerR: number,
21
+ outerR: number,
22
+ startAngle: number,
23
+ endAngle: number
24
+ ): string => {
25
+ const angleDiff = endAngle - startAngle
26
+
27
+ if (angleDiff >= 360 || angleDiff <= -360) {
28
+ const midAngle = startAngle + 180;
29
+ const firstHalf = describeSegment(cx, cy, innerR, outerR, startAngle, midAngle);
30
+ const secondHalf = describeSegment(cx, cy, innerR, outerR, midAngle, endAngle);
31
+ return firstHalf + ' ' + secondHalf;
32
+ }
33
+
34
+ const startOuter = polarToCartesian(cx, cy, outerR, endAngle);
35
+ const endOuter = polarToCartesian(cx, cy, outerR, startAngle);
36
+ const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
37
+
38
+ if (innerR === 0) {
39
+ return `
40
+ M ${cx} ${cy}
41
+ L ${startOuter.x} ${startOuter.y}
42
+ A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y}
43
+ Z
44
+ `.trim();
45
+ }
46
+
47
+ const startInner = polarToCartesian(cx, cy, innerR, startAngle);
48
+ const endInner = polarToCartesian(cx, cy, innerR, endAngle);
49
+
50
+ return `
51
+ M ${startOuter.x} ${startOuter.y}
52
+ A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y}
53
+ L ${startInner.x} ${startInner.y}
54
+ A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${endInner.x} ${endInner.y}
55
+ Z
56
+ `.trim();
57
+ };
58
+
59
+ export const createDonutSegments = (
60
+ languages: Language[],
61
+ cx: number,
62
+ geometry: Geometry,
63
+ colours: string[],
64
+ stroke: boolean
65
+ ): string => {
66
+ let currentAngle = -0.1;
67
+
68
+ return languages.map((lang, i) => {
69
+ let angle = (lang.pct / 100) * 360;
70
+
71
+ const segmentAngle = Math.min(currentAngle + angle + 0.1, FULL_CIRCLE_ANGLE);
72
+ const path = describeSegment(
73
+ cx,
74
+ geometry.CENTER_Y,
75
+ geometry.INNER_RADIUS,
76
+ geometry.OUTER_RADIUS,
77
+ currentAngle,
78
+ segmentAngle
79
+ );
80
+
81
+ currentAngle += angle;
82
+ const fillColour = colours[i % colours.length];
83
+ const strokeAttr = stroke
84
+ ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"`
85
+ : ` stroke="${fillColour}" stroke-width="0.2"`;
86
+ return `<path d="${path}" fill="${fillColour}"${strokeAttr} shape-rendering="geometricPrecision"/>`;
87
+ }).join('');
88
+ }
@@ -1,19 +1,50 @@
1
- import { LEGEND_SHIFT_THRESHOLD, LEGEND_STYLES, CHART_MARGIN_RIGHT } from "../constants/styles.js";
2
-
3
- export function resolveLayout(count: number, stroke: boolean) {
4
- return {
5
- isShifted: count > LEGEND_SHIFT_THRESHOLD,
6
- useStroke: count > 1 ? stroke : false
7
- };
8
- }
9
-
10
- export function calculateChartCenter(width: number, isShifted: boolean): number {
11
- const legendWidth = isShifted
12
- ? LEGEND_STYLES.COLUMN_WIDTH * 2
13
- : LEGEND_STYLES.WIDTH;
14
- return (width - legendWidth - CHART_MARGIN_RIGHT) / 2;
15
- }
16
-
17
- export function calculateLegendStartX(chartCenterX: number, radius: number): number {
18
- return chartCenterX + radius + CHART_MARGIN_RIGHT;
19
- }
1
+ import { LEGEND_SHIFT_THRESHOLD, LEGEND_STYLES, CHART_MARGIN_RIGHT, ARIAL_CHAR_WIDTHS, DEFAULT_CHAR_WIDTH } from "../constants/styles.js";
2
+ import type { Geometry, Language } from "../types.js";
3
+
4
+ export function resolveLayout(count: number, stroke: boolean) {
5
+ return {
6
+ isShifted: count > LEGEND_SHIFT_THRESHOLD,
7
+ useStroke: count > 1 ? stroke : false
8
+ };
9
+ }
10
+
11
+ const measureText = (text: string, fontSize: number): number => [...text]
12
+ .reduce((sum, ch) => sum + (ARIAL_CHAR_WIDTHS[ch] ?? DEFAULT_CHAR_WIDTH), 0) * fontSize / 1000;
13
+
14
+ const estimateEntryWidth = (label: string): number =>
15
+ LEGEND_STYLES.SQUARE_SIZE + LEGEND_STYLES.TEXT_GAP + measureText(label, LEGEND_STYLES.FONT_SIZE);
16
+
17
+ export function measureLegend(languages: Language[], isShifted: boolean) {
18
+ const entryWidth = (lang: Language) => estimateEntryWidth(`${lang.lang} ${lang.pct.toFixed(1)}%`);
19
+
20
+ if (!isShifted) {
21
+ const width = languages.reduce((max, l) => Math.max(max, entryWidth(l)), 0);
22
+ return { columnWidth: width, legendWidth: width };
23
+ }
24
+
25
+ const half = Math.ceil(languages.length / 2);
26
+ const col0Width = languages.slice(0, half).reduce((max, l) => Math.max(max, entryWidth(l)), 0);
27
+ const col1Width = languages.slice(half).reduce((max, l) => Math.max(max, entryWidth(l)), 0);
28
+
29
+ const columnWidth = col0Width + LEGEND_STYLES.COLUMN_GAP;
30
+ const legendWidth = columnWidth + col1Width;
31
+
32
+ return { columnWidth, legendWidth };
33
+ }
34
+
35
+ export const CONTENT_PAD = 20;
36
+
37
+ export function computeLayout(languages: Language[], isShifted: boolean, geometry: Geometry) {
38
+ const { columnWidth, legendWidth } = measureLegend(languages, isShifted);
39
+
40
+ const chartX = CONTENT_PAD + geometry.OUTER_RADIUS;
41
+ const legendStartX = chartX + geometry.OUTER_RADIUS + CHART_MARGIN_RIGHT;
42
+ const contentWidth = legendStartX + legendWidth + CONTENT_PAD;
43
+
44
+ const rows = isShifted ? Math.ceil(languages.length / 2) : languages.length;
45
+ const legendBottom = LEGEND_STYLES.START_Y + (rows - 1) * LEGEND_STYLES.ROW_HEIGHT + LEGEND_STYLES.SQUARE_SIZE;
46
+ const chartBottom = geometry.CENTER_Y + geometry.OUTER_RADIUS;
47
+ const contentHeight = Math.max(chartBottom, legendBottom) + CONTENT_PAD;
48
+
49
+ return { chartX, legendStartX, columnWidth, contentWidth, contentHeight };
50
+ }
@@ -1,53 +1,54 @@
1
- import { LEGEND_STYLES } from "../constants/styles.js";
2
- import type { Language, Theme } from "../types.js";
3
-
4
- export function createLegend(
5
- languages: Language[],
6
- isShifted: boolean,
7
- selectedTheme: Theme,
8
- legendStartX: number,
9
- stroke: boolean
10
- ): string {
11
- const numLangs = languages.length;
12
-
13
- return languages.map((lang, i) => {
14
- let x: number, y: number;
15
-
16
- if (!isShifted) {
17
- x = legendStartX;
18
- y = LEGEND_STYLES.START_Y + i * LEGEND_STYLES.ROW_HEIGHT;
19
- } else {
20
- const half = Math.ceil(numLangs / 2);
21
- const col = Math.floor(i / half);
22
- const row = i % half;
23
-
24
- x = legendStartX + col * LEGEND_STYLES.COLUMN_WIDTH;
25
- y = LEGEND_STYLES.START_Y + row * LEGEND_STYLES.ROW_HEIGHT;
26
- }
27
-
28
- const fill = selectedTheme.colours[i];
29
- const strokeAttr = stroke
30
- ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"`
31
- : ``;
32
-
33
- return `
34
- <rect
35
- x="${x}"
36
- y="${y - LEGEND_STYLES.SQUARE_SIZE + 3}"
37
- width="${LEGEND_STYLES.SQUARE_SIZE}"
38
- height="${LEGEND_STYLES.SQUARE_SIZE}"
39
- fill="${fill}"
40
- rx="${LEGEND_STYLES.SQUARE_RADIUS}"${strokeAttr}
41
- />
42
- <text
43
- x="${x + LEGEND_STYLES.SQUARE_SIZE + 5}"
44
- y="${y}"
45
- fill="${selectedTheme.text}"
46
- font-size="${LEGEND_STYLES.FONT_SIZE}"
47
- font-family="Arial"
48
- >
49
- ${lang.lang} ${lang.pct.toFixed(1)}%
50
- </text>
51
- `;
52
- }).join('');
53
- }
1
+ import { LEGEND_STYLES } from "../constants/styles.js";
2
+ import type { Language, Theme } from "../types.js";
3
+
4
+ export function createLegend(
5
+ languages: Language[],
6
+ isShifted: boolean,
7
+ selectedTheme: Theme,
8
+ legendStartX: number,
9
+ stroke: boolean,
10
+ columnWidth: number
11
+ ): string {
12
+ const numLangs = languages.length;
13
+
14
+ return languages.map((lang, i) => {
15
+ let x: number, y: number;
16
+
17
+ if (!isShifted) {
18
+ x = legendStartX;
19
+ y = LEGEND_STYLES.START_Y + i * LEGEND_STYLES.ROW_HEIGHT;
20
+ } else {
21
+ const half = Math.ceil(numLangs / 2);
22
+ const col = Math.floor(i / half);
23
+ const row = i % half;
24
+
25
+ x = legendStartX + col * columnWidth;
26
+ y = LEGEND_STYLES.START_Y + row * LEGEND_STYLES.ROW_HEIGHT;
27
+ }
28
+
29
+ const fill = selectedTheme.colours[i];
30
+ const strokeAttr = stroke
31
+ ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"`
32
+ : ``;
33
+
34
+ return `
35
+ <rect
36
+ x="${x}"
37
+ y="${y - LEGEND_STYLES.SQUARE_SIZE + 3}"
38
+ width="${LEGEND_STYLES.SQUARE_SIZE}"
39
+ height="${LEGEND_STYLES.SQUARE_SIZE}"
40
+ fill="${fill}"
41
+ rx="${LEGEND_STYLES.SQUARE_RADIUS}"${strokeAttr}
42
+ />
43
+ <text
44
+ x="${x + LEGEND_STYLES.SQUARE_SIZE + LEGEND_STYLES.TEXT_GAP}"
45
+ y="${y}"
46
+ fill="${selectedTheme.text}"
47
+ font-size="${LEGEND_STYLES.FONT_SIZE}"
48
+ font-family="Arial"
49
+ >
50
+ ${lang.lang} ${lang.pct.toFixed(1)}%
51
+ </text>
52
+ `;
53
+ }).join('');
54
+ }
package/src/charts/pie.ts CHANGED
@@ -1,22 +1,41 @@
1
- import type { Language, Theme, ChartResult } from "../types.js";
2
- import { resolveLayout, calculateChartCenter, calculateLegendStartX } from "./layout.js";
3
- import { PIE_GEOMETRY } from "../constants/geometry.js";
4
- import { createDonutSegments } from "./geometry.js";
5
- import { createLegend } from "./legend.js";
6
-
7
- export function generatePieChart(
8
- normalizedLanguages: Language[],
9
- selectedTheme: Theme,
10
- width: number,
11
- stroke: boolean
12
- ): ChartResult {
13
- const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke);
14
-
15
- const chartX = calculateChartCenter(width, isShifted);
16
- const legendStartX = calculateLegendStartX(chartX, PIE_GEOMETRY.OUTER_RADIUS);
17
-
18
- const segments = createDonutSegments(normalizedLanguages, chartX, PIE_GEOMETRY, [...selectedTheme.colours], useStroke);
19
- const legend = createLegend(normalizedLanguages, isShifted, selectedTheme, legendStartX, useStroke);
20
-
21
- return { segments, legend };
22
- }
1
+ import type { Language, Theme, ChartResult } from "../types.js";
2
+ import { PIE_GEOMETRY } from "../constants/geometry.js";
3
+ import { createDonutSegments } from "./geometry.js";
4
+ import { createLegend } from "./legend.js";
5
+ import {
6
+ resolveLayout,
7
+ computeLayout
8
+ } from "./layout.js";
9
+
10
+ export function generatePieChart(
11
+ normalizedLanguages: Language[],
12
+ selectedTheme: Theme,
13
+ stroke: boolean
14
+ ): ChartResult {
15
+ const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke);
16
+ const {
17
+ chartX,
18
+ legendStartX,
19
+ columnWidth,
20
+ contentWidth,
21
+ contentHeight
22
+ } = computeLayout(normalizedLanguages, isShifted, PIE_GEOMETRY);
23
+
24
+ const segments = createDonutSegments(
25
+ normalizedLanguages,
26
+ chartX,
27
+ PIE_GEOMETRY,
28
+ [...selectedTheme.colours],
29
+ useStroke
30
+ );
31
+ const legend = createLegend(
32
+ normalizedLanguages,
33
+ isShifted,
34
+ selectedTheme,
35
+ legendStartX,
36
+ useStroke,
37
+ columnWidth
38
+ );
39
+
40
+ return { segments, legend, contentWidth, contentHeight };
41
+ }
@@ -1,11 +1,11 @@
1
- export const DEFAULT_CONFIG = {
2
- TITLE: "Top Languages",
3
- WIDTH: 400,
4
- MIN_WIDTH: 400,
5
- HEIGHT: 300,
6
- MIN_HEIGHT: 265,
7
- COUNT: 8,
8
- MAX_COUNT: 16
9
- } as const;
10
-
11
- export const REFRESH_INTERVAL = 1000 * 60 * 60;
1
+ export const DEFAULT_CONFIG = {
2
+ TITLE: "Top Languages",
3
+ WIDTH: 400,
4
+ MIN_WIDTH: 400,
5
+ HEIGHT: 300,
6
+ MIN_HEIGHT: 265,
7
+ COUNT: 8,
8
+ MAX_COUNT: 16
9
+ } as const;
10
+
11
+ export const REFRESH_INTERVAL = 1000 * 60 * 60;
@@ -1,9 +1,9 @@
1
- export const FULL_CIRCLE_ANGLE = 359.9999;
2
-
3
- const BASE_GEOMETRY = {
4
- CENTER_Y: 170,
5
- OUTER_RADIUS: 80,
6
- } as const;
7
-
8
- export const DONUT_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 50 } as const;
9
- export const PIE_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 0 } as const;
1
+ export const FULL_CIRCLE_ANGLE = 359.9999;
2
+
3
+ const BASE_GEOMETRY = {
4
+ CENTER_Y: 170,
5
+ OUTER_RADIUS: 80,
6
+ } as const;
7
+
8
+ export const DONUT_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 50 } as const;
9
+ export const PIE_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 0 } as const;