@gh-top-languages/lib 1.0.3
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 +33 -0
- package/dist/charts/donut.d.ts +3 -0
- package/dist/charts/donut.d.ts.map +1 -0
- package/dist/charts/donut.js +13 -0
- package/dist/charts/donut.js.map +1 -0
- package/dist/charts/geometry.d.ts +5 -0
- package/dist/charts/geometry.d.ts.map +1 -0
- package/dist/charts/geometry.js +52 -0
- package/dist/charts/geometry.js.map +1 -0
- package/dist/charts/layout.d.ts +7 -0
- package/dist/charts/layout.d.ts.map +1 -0
- package/dist/charts/layout.js +17 -0
- package/dist/charts/layout.js.map +1 -0
- package/dist/charts/legend.d.ts +3 -0
- package/dist/charts/legend.d.ts.map +1 -0
- package/dist/charts/legend.js +42 -0
- package/dist/charts/legend.js.map +1 -0
- package/dist/charts/pie.d.ts +3 -0
- package/dist/charts/pie.d.ts.map +1 -0
- package/dist/charts/pie.js +13 -0
- package/dist/charts/pie.js.map +1 -0
- package/dist/constants/config.d.ts +11 -0
- package/dist/constants/config.d.ts.map +1 -0
- package/dist/constants/config.js +11 -0
- package/dist/constants/config.js.map +1 -0
- package/dist/constants/geometry.d.ts +12 -0
- package/dist/constants/geometry.d.ts.map +1 -0
- package/dist/constants/geometry.js +8 -0
- package/dist/constants/geometry.js.map +1 -0
- package/dist/constants/styles.d.ts +21 -0
- package/dist/constants/styles.d.ts.map +1 -0
- package/dist/constants/styles.js +21 -0
- package/dist/constants/styles.js.map +1 -0
- package/dist/constants/themes.d.ts +18 -0
- package/dist/constants/themes.d.ts.map +1 -0
- package/dist/constants/themes.js +69 -0
- package/dist/constants/themes.js.map +1 -0
- package/dist/constants/types.d.ts +2 -0
- package/dist/constants/types.d.ts.map +1 -0
- package/dist/constants/types.js +5 -0
- package/dist/constants/types.js.map +1 -0
- package/dist/render/chart.d.ts +3 -0
- package/dist/render/chart.d.ts.map +1 -0
- package/dist/render/chart.js +11 -0
- package/dist/render/chart.js.map +1 -0
- package/dist/render/error.d.ts +3 -0
- package/dist/render/error.d.ts.map +1 -0
- package/dist/render/error.js +19 -0
- package/dist/render/error.js.map +1 -0
- package/dist/render/svg.d.ts +2 -0
- package/dist/render/svg.d.ts.map +1 -0
- package/dist/render/svg.js +22 -0
- package/dist/render/svg.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/params.d.ts +18 -0
- package/dist/utils/params.d.ts.map +1 -0
- package/dist/utils/params.js +37 -0
- package/dist/utils/params.js.map +1 -0
- package/dist/utils/sanitize.d.ts +2 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +13 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/package.json +48 -0
- package/src/charts/donut.ts +22 -0
- package/src/charts/geometry.ts +88 -0
- package/src/charts/layout.ts +19 -0
- package/src/charts/legend.ts +53 -0
- package/src/charts/pie.ts +22 -0
- package/src/constants/config.ts +11 -0
- package/src/constants/geometry.ts +9 -0
- package/src/constants/styles.ts +23 -0
- package/src/constants/themes.ts +68 -0
- package/src/constants/types.ts +4 -0
- package/src/render/chart.ts +24 -0
- package/src/render/error.ts +25 -0
- package/src/render/svg.ts +31 -0
- package/src/types.ts +28 -0
- package/src/utils/params.ts +47 -0
- package/src/utils/sanitize.ts +14 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Point = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
export type Language = {
|
|
6
|
+
lang: string;
|
|
7
|
+
pct: number;
|
|
8
|
+
};
|
|
9
|
+
export type Geometry = {
|
|
10
|
+
CENTER_Y: number;
|
|
11
|
+
INNER_RADIUS: number;
|
|
12
|
+
OUTER_RADIUS: number;
|
|
13
|
+
};
|
|
14
|
+
export type ChartType = "donut" | "pie";
|
|
15
|
+
export type ChartResult = {
|
|
16
|
+
segments: string;
|
|
17
|
+
legend: string;
|
|
18
|
+
};
|
|
19
|
+
export type Theme = {
|
|
20
|
+
readonly colours: readonly string[];
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly bg: string;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG;IAClB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAG,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAM,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAExC,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAI,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAK,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAO,MAAM,CAAC;CAC1B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChartType } from "../types.js";
|
|
2
|
+
export type QueryParams = Record<string, string | undefined>;
|
|
3
|
+
export declare function parseQueryParams(query: QueryParams): {
|
|
4
|
+
chartType: ChartType;
|
|
5
|
+
chartTitle: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
count: number;
|
|
9
|
+
selectedTheme: {
|
|
10
|
+
bg: "#0d1117" | "#ffffff" | "#1a1a1a";
|
|
11
|
+
text: string;
|
|
12
|
+
colours: string[];
|
|
13
|
+
};
|
|
14
|
+
stroke: boolean;
|
|
15
|
+
useTestData: boolean;
|
|
16
|
+
errorTest: string;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=params.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/utils/params.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAY7D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW;;;;;;;;;;;;;;EA4BlD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { sanitize } from "./sanitize.js";
|
|
2
|
+
import { VALID_TYPES } from "../constants/types.js";
|
|
3
|
+
import { DEFAULT_CONFIG } from "../constants/config.js";
|
|
4
|
+
import { THEMES } from "../constants/themes.js";
|
|
5
|
+
const parseIntSafe = (val, fallback) => {
|
|
6
|
+
const parsed = Number.parseInt(val ?? '', 10);
|
|
7
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
8
|
+
};
|
|
9
|
+
const normalizeHex = (val) => `#${val.replace(/^#/, '')}`;
|
|
10
|
+
export function parseQueryParams(query) {
|
|
11
|
+
const baseTheme = THEMES[query["theme"]] ?? THEMES.default;
|
|
12
|
+
const count = parseIntSafe(query["count"], DEFAULT_CONFIG.COUNT);
|
|
13
|
+
const customColours = [...baseTheme.colours];
|
|
14
|
+
for (let i = 1; i <= DEFAULT_CONFIG.MAX_COUNT; i++) {
|
|
15
|
+
const colourVal = query[`c${i}`];
|
|
16
|
+
if (colourVal)
|
|
17
|
+
customColours[i - 1] = normalizeHex(colourVal);
|
|
18
|
+
}
|
|
19
|
+
const typeParam = query["type"];
|
|
20
|
+
const chartType = VALID_TYPES.some(t => t === typeParam) ? typeParam : "donut";
|
|
21
|
+
return {
|
|
22
|
+
chartType,
|
|
23
|
+
chartTitle: query["hide_title"] === "true" ? '' : sanitize(query["title"] ?? DEFAULT_CONFIG.TITLE),
|
|
24
|
+
width: Math.max(parseIntSafe(query["width"], DEFAULT_CONFIG.WIDTH), DEFAULT_CONFIG.MIN_WIDTH),
|
|
25
|
+
height: Math.max(parseIntSafe(query["height"], DEFAULT_CONFIG.HEIGHT), DEFAULT_CONFIG.MIN_HEIGHT),
|
|
26
|
+
count: Math.min(Math.max(count, 1), DEFAULT_CONFIG.MAX_COUNT),
|
|
27
|
+
selectedTheme: {
|
|
28
|
+
bg: THEMES[query["bg"]]?.bg ?? (query["bg"] ? normalizeHex(query["bg"]) : baseTheme.bg),
|
|
29
|
+
text: query["text"] ? normalizeHex(query["text"]) : baseTheme.text,
|
|
30
|
+
colours: customColours
|
|
31
|
+
},
|
|
32
|
+
stroke: query["stroke"] === "true",
|
|
33
|
+
useTestData: query["test"] === "true",
|
|
34
|
+
errorTest: sanitize(query["error"] ?? '')
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=params.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"params.js","sourceRoot":"","sources":["../../src/utils/params.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,QAAQ,GAAI,KAAK,OAAO,KAAG,MAKvC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ESCAPE_MAP = {
|
|
2
|
+
'<': '<',
|
|
3
|
+
'>': '>',
|
|
4
|
+
'&': '&',
|
|
5
|
+
'"': '"',
|
|
6
|
+
"'": ''',
|
|
7
|
+
};
|
|
8
|
+
export const sanitize = (str) => {
|
|
9
|
+
if (typeof str !== "string")
|
|
10
|
+
return '';
|
|
11
|
+
return str.replace(/[<>&"']/g, (m) => ESCAPE_MAP[m]);
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG;IACjB,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,OAAO;CACJ,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAY,EAAU,EAAE;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACvC,OAAO,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAS,EAAU,EAAE,CACnD,UAAU,CAAC,CAA4B,CAAC,CACzC,CAAC;AACJ,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,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
|
+
): 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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +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;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const TITLE_STYLES = {
|
|
2
|
+
TEXT_Y: 30,
|
|
3
|
+
FONT_SIZE: 24
|
|
4
|
+
} as const;
|
|
5
|
+
|
|
6
|
+
export const LEGEND_STYLES = {
|
|
7
|
+
START_Y: 80,
|
|
8
|
+
ROW_HEIGHT: 25,
|
|
9
|
+
SQUARE_SIZE: 12,
|
|
10
|
+
SQUARE_RADIUS: 2,
|
|
11
|
+
FONT_SIZE: 11,
|
|
12
|
+
WIDTH: 130,
|
|
13
|
+
COLUMN_WIDTH: 105
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export const ERROR_STYLES = {
|
|
17
|
+
TEXT_Y: 100,
|
|
18
|
+
FONT_SIZE: 18,
|
|
19
|
+
COLOUR: "#ff6b6b"
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const LEGEND_SHIFT_THRESHOLD = 8;
|
|
23
|
+
export const CHART_MARGIN_RIGHT = 20;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const THEMES = {
|
|
2
|
+
default: {
|
|
3
|
+
bg: "#0d1117",
|
|
4
|
+
text: "#ffffff",
|
|
5
|
+
colours: [
|
|
6
|
+
"#A8D5Ba",
|
|
7
|
+
"#FFD6A5",
|
|
8
|
+
"#FFAAA6",
|
|
9
|
+
"#D0CFCF",
|
|
10
|
+
"#CBAACB",
|
|
11
|
+
"#FFE156",
|
|
12
|
+
"#96D5E9",
|
|
13
|
+
"#F3B0C3",
|
|
14
|
+
"#B4A7D6",
|
|
15
|
+
"#FFB6B9",
|
|
16
|
+
"#A3E4D7",
|
|
17
|
+
"#F8B88B",
|
|
18
|
+
"#C9E4CA",
|
|
19
|
+
"#FAD7A0",
|
|
20
|
+
"#AED6F1",
|
|
21
|
+
"#D7BDE2"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
light: {
|
|
25
|
+
bg: "#ffffff",
|
|
26
|
+
text: "#2f2f2f",
|
|
27
|
+
colours: [
|
|
28
|
+
"#2ecc71",
|
|
29
|
+
"#3498db",
|
|
30
|
+
"#e74c3c",
|
|
31
|
+
"#f39c12",
|
|
32
|
+
"#9b59b6",
|
|
33
|
+
"#1abc9c",
|
|
34
|
+
"#e67e22",
|
|
35
|
+
"#34495e",
|
|
36
|
+
"#16a085",
|
|
37
|
+
"#c0392b",
|
|
38
|
+
"#8e44ad",
|
|
39
|
+
"#27ae60",
|
|
40
|
+
"#d35400",
|
|
41
|
+
"#2980b9",
|
|
42
|
+
"#7f8c8d",
|
|
43
|
+
"#f1c40f"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
dark: {
|
|
47
|
+
bg: "#1a1a1a",
|
|
48
|
+
text: "#ccd6f6",
|
|
49
|
+
colours: [
|
|
50
|
+
"#ff6b6b",
|
|
51
|
+
"#4ecdc3",
|
|
52
|
+
"#45b7d1",
|
|
53
|
+
"#ffa07a",
|
|
54
|
+
"#98d8c8",
|
|
55
|
+
"#f7dc6f",
|
|
56
|
+
"#bb8fce",
|
|
57
|
+
"#64ffda",
|
|
58
|
+
"#85c1e2",
|
|
59
|
+
"#ff8a80",
|
|
60
|
+
"#a7ffeb",
|
|
61
|
+
"#ffd54f",
|
|
62
|
+
"#ea80fc",
|
|
63
|
+
"#80d8ff",
|
|
64
|
+
"#ffab91",
|
|
65
|
+
"#b9f6ca"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
} as const;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Language, Theme, ChartType, ChartResult } from "../types.js";
|
|
2
|
+
import { generateDonutChart } from "../charts/donut.js";
|
|
3
|
+
import { generatePieChart } from "../charts/pie.js";
|
|
4
|
+
|
|
5
|
+
const CHART_GENERATORS: Record<ChartType, (
|
|
6
|
+
data: Language[],
|
|
7
|
+
theme: Theme,
|
|
8
|
+
width: number,
|
|
9
|
+
stroke: boolean
|
|
10
|
+
) => ChartResult> = {
|
|
11
|
+
donut: generateDonutChart,
|
|
12
|
+
pie: generatePieChart,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generateChartData(
|
|
16
|
+
data: Language[],
|
|
17
|
+
theme: Theme,
|
|
18
|
+
chartType: ChartType,
|
|
19
|
+
width: number,
|
|
20
|
+
stroke: boolean
|
|
21
|
+
): ChartResult {
|
|
22
|
+
const generator = CHART_GENERATORS[chartType] || CHART_GENERATORS.donut;
|
|
23
|
+
return generator(data, theme, width, stroke);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Theme } from "../types.js";
|
|
2
|
+
import { THEMES } from "../constants/themes.js";
|
|
3
|
+
import { ERROR_STYLES } from "../constants/styles.js"
|
|
4
|
+
import { sanitize } from "../utils/sanitize.js";
|
|
5
|
+
|
|
6
|
+
export function renderError(
|
|
7
|
+
message: string,
|
|
8
|
+
width: number,
|
|
9
|
+
height: number,
|
|
10
|
+
selectedTheme?: Theme
|
|
11
|
+
): string {
|
|
12
|
+
const background = selectedTheme?.bg || THEMES.default.bg;
|
|
13
|
+
const maxLen = 40;
|
|
14
|
+
const truncated = message.length > maxLen
|
|
15
|
+
? sanitize(message.slice(0, maxLen)) + "..."
|
|
16
|
+
: sanitize(message);
|
|
17
|
+
return `
|
|
18
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
19
|
+
<rect width="${width}" height="${height}" fill="${background}" rx="10"/>
|
|
20
|
+
<text x="${width/2}" y="${ERROR_STYLES.TEXT_Y}" text-anchor="middle" fill="${ERROR_STYLES.COLOUR}" font-family="Arial" font-size="${ERROR_STYLES.FONT_SIZE}">
|
|
21
|
+
Error: ${truncated}
|
|
22
|
+
</text>
|
|
23
|
+
</svg>
|
|
24
|
+
`.trim();
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TITLE_STYLES } from "../constants/styles.js"
|
|
2
|
+
|
|
3
|
+
export function renderSvg(
|
|
4
|
+
width: number,
|
|
5
|
+
height: number,
|
|
6
|
+
background: string,
|
|
7
|
+
segments: string,
|
|
8
|
+
legend: string,
|
|
9
|
+
title: string | null,
|
|
10
|
+
textColour: string
|
|
11
|
+
): string {
|
|
12
|
+
const titleElement = title ? `
|
|
13
|
+
<text
|
|
14
|
+
x="${width/2}"
|
|
15
|
+
y="${TITLE_STYLES.TEXT_Y}"
|
|
16
|
+
text-anchor="middle" fill="${textColour}"
|
|
17
|
+
font-family="Arial" font-size="${TITLE_STYLES.FONT_SIZE}"
|
|
18
|
+
>
|
|
19
|
+
${title}
|
|
20
|
+
</text>
|
|
21
|
+
` : '';
|
|
22
|
+
|
|
23
|
+
return `
|
|
24
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
25
|
+
<rect width="${width}" height="${height}" fill="${background}" rx="10"/>
|
|
26
|
+
${titleElement}
|
|
27
|
+
${segments}
|
|
28
|
+
${legend}
|
|
29
|
+
</svg>
|
|
30
|
+
`.trim();
|
|
31
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type Point = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Language = {
|
|
7
|
+
lang: string;
|
|
8
|
+
pct: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Geometry = {
|
|
12
|
+
CENTER_Y: number;
|
|
13
|
+
INNER_RADIUS: number;
|
|
14
|
+
OUTER_RADIUS: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ChartType = "donut" | "pie";
|
|
18
|
+
|
|
19
|
+
export type ChartResult = {
|
|
20
|
+
segments: string;
|
|
21
|
+
legend: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Theme = {
|
|
25
|
+
readonly colours: readonly string[];
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly bg: string;
|
|
28
|
+
};
|