diff --git a/package.json b/package.json index 791fa4e..5223df0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@gh-top-languages/lib", - "version": "1.1.3", - "description": "Library for github-top-languages — chart generation, SVG output, and parameter parsing", + "version": "1.2.0", + "description": "Library for gh-top-languages — chart generation, SVG output, and parameter parsing", "keywords": [ "svg", "charts", diff --git a/src/charts/format.ts b/src/charts/format.ts deleted file mode 100644 index 105fbe1..0000000 --- a/src/charts/format.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GapType, Language } from "./types.js"; - -export function displayPct(pct: number, totalPct: number, gapType: GapType): number { - return gapType === "adapt" && totalPct > 0 ? pct * (100 / totalPct) : pct; -} - -export function formatLegendEntry(lang: Language, totalPct: number, gapType: GapType): string { - return `${lang.lang} ${displayPct(lang.pct, totalPct, gapType).toFixed(1)}%`; -} diff --git a/src/charts/geometry.ts b/src/charts/geometry.ts index 1ae9fe3..336e299 100644 --- a/src/charts/geometry.ts +++ b/src/charts/geometry.ts @@ -1,5 +1,6 @@ import { FULL_CIRCLE_ANGLE } from "../constants/geometry.js"; import type { Point, Language, Geometry, GapType } from "./types.js" +import { resolveColour } from "./helpers.js"; export const polarToCartesian = ( cx: number, @@ -83,11 +84,11 @@ export const createDonutSegments = ( ); currentAngle += angle; - const fillColour = colours[i % colours.length]; + const fill = resolveColour(colours, i); const strokeAttr = stroke ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"` - : ` stroke="${fillColour}" stroke-width="0.2"`; - return ``; + : ` stroke="${fill}" stroke-width="0.2"`; + return ``; }).join(''); if (gapType === "gap" && totalPct > 0 && totalPct < 100) { diff --git a/src/charts/helpers.ts b/src/charts/helpers.ts new file mode 100644 index 0000000..a5c6fd1 --- /dev/null +++ b/src/charts/helpers.ts @@ -0,0 +1,38 @@ +import type { GapType, Language } from "./types.js"; + +export function displayPct(pct: number, totalPct: number, gapType: GapType): number { + return gapType === "adapt" && totalPct > 0 ? pct * (100 / totalPct) : pct; +} + +export function formatLegendEntry(lang: Language, totalPct: number, gapType: GapType): string { + return `${lang.lang} ${displayPct(lang.pct, totalPct, gapType).toFixed(1)}%`; +} + +function hexToHsl(hex: string): { s: number; l: number } | null { + const match = /^#?([0-9a-f]{6})$/i.exec(hex); + if (!match) return null; + + const r = parseInt(match[1]!.slice(0, 2), 16) / 255; + const g = parseInt(match[1]!.slice(2, 4), 16) / 255; + const b = parseInt(match[1]!.slice(4, 6), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + const d = max - min; + const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)); + + return { s: s * 100, l: l * 100 }; +} + +export function resolveColour(colours: readonly string[], index: number): string { + const base = colours[index]; + if (base) return base; + + const parsed = colours.map(hexToHsl).filter((c): c is { s: number; l: number } => c !== null); + const avg = (key: "s" | "l") => + parsed.length > 0 ? parsed.reduce((sum, c) => sum + c[key], 0) / parsed.length : 65; + + const hue = (index * 137.508) % 360; + return `hsl(${hue.toFixed(1)}, ${avg("s").toFixed(1)}%, ${avg("l").toFixed(1)}%)`; +} diff --git a/src/charts/layout.ts b/src/charts/layout.ts index 22643bb..956cba4 100644 --- a/src/charts/layout.ts +++ b/src/charts/layout.ts @@ -6,7 +6,7 @@ import { LEGEND_SHIFT_THRESHOLD } from "../constants/styles.js"; import type { GapType, Geometry, Language } from "./types.js"; -import { formatLegendEntry } from "./format.js"; +import { formatLegendEntry } from "./helpers.js"; const measureText = (text: string, fontSize: number): number => [...text] .reduce((sum, ch) => sum + (ARIAL_CHAR_WIDTHS[ch] ?? DEFAULT_CHAR_WIDTH), 0) * fontSize / 1000; diff --git a/src/charts/legend.ts b/src/charts/legend.ts index 610a77d..426e112 100644 --- a/src/charts/legend.ts +++ b/src/charts/legend.ts @@ -1,6 +1,6 @@ import { LEGEND_SHIFT_THRESHOLD, LEGEND_STYLES } from "../constants/styles.js"; import type { Theme, Language, GapType } from "./types.js"; -import { formatLegendEntry } from "./format.js"; +import { formatLegendEntry, resolveColour } from "./helpers.js"; export function createLegend( languages: Language[], @@ -28,7 +28,7 @@ export function createLegend( y = LEGEND_STYLES.START_Y + row * LEGEND_STYLES.ROW_HEIGHT; } - const fill = selectedTheme.colours[i]; + const fill = resolveColour(selectedTheme.colours, i); const strokeAttr = stroke ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"` : ``;