@countermeasure-platform/web-components 1.2.2-dev.17.1 → 1.2.2-dev.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/funnel/index.d.ts.map +1 -1
- package/dist/charts/funnel/index.js +158 -13
- package/dist/charts/funnel/index.js.map +1 -1
- package/dist/charts/gauge/index.d.ts +17 -0
- package/dist/charts/gauge/index.d.ts.map +1 -1
- package/dist/charts/gauge/index.js +104 -27
- package/dist/charts/gauge/index.js.map +1 -1
- package/dist/charts/gauge/types.d.ts +10 -5
- package/dist/charts/gauge/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/charts/funnel/index.ts +206 -8
- package/src/charts/gauge/index.ts +304 -74
- package/src/charts/gauge/types.ts +10 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/charts/funnel/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/charts/funnel/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAqN/D,qBAAa,WAAY,SAAQ,SAAS;IACxC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,GAAG,CAAQ;IAEnB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,eAAe,CAAY;gBAEvB,MAAM,EAAE,iBAAiB;IA4BrC,yCAAyC;IACzC,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI;IAMvC,MAAM,IAAI,IAAI;IAYL,OAAO,IAAI,IAAI;IASxB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,cAAc;IAsDtB,OAAO,CAAC,gBAAgB;IAuDxB,OAAO,CAAC,eAAe;IA6BvB,OAAO,CAAC,eAAe;IA2DvB,OAAO,CAAC,gBAAgB;CASzB"}
|
|
@@ -2,7 +2,151 @@ import { BaseChart as e } from "../base.js";
|
|
|
2
2
|
import { ChartTooltip as t } from "../tooltip.js";
|
|
3
3
|
import { defaultColors as n, formatNumber as r, hexToRgba as i } from "../utils.js";
|
|
4
4
|
//#region src/charts/funnel/index.ts
|
|
5
|
-
var a = "
|
|
5
|
+
var a = "600 12px ui-sans-serif, system-ui, -apple-system, sans-serif", o = "500 11px ui-sans-serif, system-ui, -apple-system, sans-serif", s = .12, c = new Map([
|
|
6
|
+
["black", [
|
|
7
|
+
0,
|
|
8
|
+
0,
|
|
9
|
+
0
|
|
10
|
+
]],
|
|
11
|
+
["white", [
|
|
12
|
+
255,
|
|
13
|
+
255,
|
|
14
|
+
255
|
|
15
|
+
]],
|
|
16
|
+
["navy", [
|
|
17
|
+
0,
|
|
18
|
+
0,
|
|
19
|
+
128
|
|
20
|
+
]],
|
|
21
|
+
["rebeccapurple", [
|
|
22
|
+
102,
|
|
23
|
+
51,
|
|
24
|
+
153
|
|
25
|
+
]],
|
|
26
|
+
["transparent", [
|
|
27
|
+
0,
|
|
28
|
+
0,
|
|
29
|
+
0
|
|
30
|
+
]]
|
|
31
|
+
]);
|
|
32
|
+
function l(e) {
|
|
33
|
+
let t = u(e);
|
|
34
|
+
if (t === null) return {
|
|
35
|
+
text: "#0f172a",
|
|
36
|
+
shadow: "rgba(255, 255, 255, 0.45)"
|
|
37
|
+
};
|
|
38
|
+
let [n, r, i] = t;
|
|
39
|
+
return (.299 * n + .587 * r + .114 * i) / 255 > .6 ? {
|
|
40
|
+
text: "#0f172a",
|
|
41
|
+
shadow: "rgba(255, 255, 255, 0.45)"
|
|
42
|
+
} : {
|
|
43
|
+
text: "#ffffff",
|
|
44
|
+
shadow: "rgba(0, 0, 0, 0.45)"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function u(e) {
|
|
48
|
+
let t = e.trim(), n = d(t);
|
|
49
|
+
if (n !== null) return n;
|
|
50
|
+
let r = c.get(t.toLowerCase());
|
|
51
|
+
if (r !== void 0) return r;
|
|
52
|
+
let i = m(t);
|
|
53
|
+
return i === null ? null : d(i);
|
|
54
|
+
}
|
|
55
|
+
function d(e) {
|
|
56
|
+
if (e.startsWith("#")) {
|
|
57
|
+
let t = e.slice(1);
|
|
58
|
+
return t.length === 3 ? [
|
|
59
|
+
parseInt(t[0] + t[0], 16),
|
|
60
|
+
parseInt(t[1] + t[1], 16),
|
|
61
|
+
parseInt(t[2] + t[2], 16)
|
|
62
|
+
] : t.length === 6 ? [
|
|
63
|
+
parseInt(t.slice(0, 2), 16),
|
|
64
|
+
parseInt(t.slice(2, 4), 16),
|
|
65
|
+
parseInt(t.slice(4, 6), 16)
|
|
66
|
+
] : null;
|
|
67
|
+
}
|
|
68
|
+
let t = e.match(/^rgba?\(([^)]+)\)$/i);
|
|
69
|
+
if (t) {
|
|
70
|
+
let e = h(t[1]), n = g(e[0]), r = g(e[1]), i = g(e[2]);
|
|
71
|
+
if (Number.isFinite(n) && Number.isFinite(r) && Number.isFinite(i)) return [
|
|
72
|
+
n,
|
|
73
|
+
r,
|
|
74
|
+
i
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
let n = e.match(/^hsla?\(([^)]+)\)$/i);
|
|
78
|
+
if (n) {
|
|
79
|
+
let e = h(n[1]), t = _(e[0]), r = v(e[1]), i = v(e[2]);
|
|
80
|
+
if (Number.isFinite(t) && Number.isFinite(r) && Number.isFinite(i)) return y(t, r, i);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
var f;
|
|
85
|
+
function p() {
|
|
86
|
+
if (f !== void 0) return f;
|
|
87
|
+
if (typeof document > "u") return f = null, null;
|
|
88
|
+
let e = document.createElement("canvas");
|
|
89
|
+
return e.width = 1, e.height = 1, f = e.getContext("2d"), f;
|
|
90
|
+
}
|
|
91
|
+
function m(e) {
|
|
92
|
+
let t = p();
|
|
93
|
+
if (t === null) return null;
|
|
94
|
+
t.fillStyle = "#010203";
|
|
95
|
+
let n = t.fillStyle;
|
|
96
|
+
t.fillStyle = e;
|
|
97
|
+
let r = t.fillStyle;
|
|
98
|
+
return r === n && e.trim().toLowerCase() !== "#010203" ? null : typeof r == "string" ? r : null;
|
|
99
|
+
}
|
|
100
|
+
function h(e) {
|
|
101
|
+
let t = e.split("/")[0].trim();
|
|
102
|
+
return t.includes(",") ? t.split(",").map((e) => e.trim()) : t.split(/\s+/).map((e) => e.trim());
|
|
103
|
+
}
|
|
104
|
+
function g(e) {
|
|
105
|
+
if (e === void 0) return NaN;
|
|
106
|
+
if (e.endsWith("%")) {
|
|
107
|
+
let t = Number(e.slice(0, -1));
|
|
108
|
+
return Number.isFinite(t) ? Math.round(t / 100 * 255) : NaN;
|
|
109
|
+
}
|
|
110
|
+
return Number(e);
|
|
111
|
+
}
|
|
112
|
+
function _(e) {
|
|
113
|
+
if (e === void 0) return NaN;
|
|
114
|
+
if (e.endsWith("turn")) {
|
|
115
|
+
let t = Number(e.slice(0, -4));
|
|
116
|
+
return Number.isFinite(t) ? t % 1 : NaN;
|
|
117
|
+
}
|
|
118
|
+
if (e.endsWith("rad")) {
|
|
119
|
+
let t = Number(e.slice(0, -3));
|
|
120
|
+
return Number.isFinite(t) ? t / (Math.PI * 2) % 1 : NaN;
|
|
121
|
+
}
|
|
122
|
+
let t = e.endsWith("deg") ? e.slice(0, -3) : e, n = Number(t);
|
|
123
|
+
return Number.isFinite(n) ? (n % 360 + 360) % 360 / 360 : NaN;
|
|
124
|
+
}
|
|
125
|
+
function v(e) {
|
|
126
|
+
if (e === void 0 || !e.endsWith("%")) return NaN;
|
|
127
|
+
let t = Number(e.slice(0, -1));
|
|
128
|
+
return Number.isFinite(t) ? t / 100 : NaN;
|
|
129
|
+
}
|
|
130
|
+
function y(e, t, n) {
|
|
131
|
+
if (t === 0) {
|
|
132
|
+
let e = Math.round(n * 255);
|
|
133
|
+
return [
|
|
134
|
+
e,
|
|
135
|
+
e,
|
|
136
|
+
e
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
let r = (e, t, n) => {
|
|
140
|
+
let r = n;
|
|
141
|
+
return r < 0 && (r += 1), r > 1 && --r, r < 1 / 6 ? e + (t - e) * 6 * r : r < 1 / 2 ? t : r < 2 / 3 ? e + (t - e) * (2 / 3 - r) * 6 : e;
|
|
142
|
+
}, i = n < .5 ? n * (1 + t) : n + t - n * t, a = 2 * n - i;
|
|
143
|
+
return [
|
|
144
|
+
Math.round(r(a, i, e + 1 / 3) * 255),
|
|
145
|
+
Math.round(r(a, i, e) * 255),
|
|
146
|
+
Math.round(r(a, i, e - 1 / 3) * 255)
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
var b = class extends e {
|
|
6
150
|
segments;
|
|
7
151
|
resolved = [];
|
|
8
152
|
colors;
|
|
@@ -59,12 +203,12 @@ var a = "#334155", o = "#64748b", s = "12px system-ui, sans-serif", c = "11px sy
|
|
|
59
203
|
for (let n = 0; n < t; n++) {
|
|
60
204
|
let t = this.resolved[n];
|
|
61
205
|
if (t === void 0) continue;
|
|
62
|
-
let a = this.resolved[n + 1], o = t.widthRatio * e.width,
|
|
206
|
+
let a = this.resolved[n + 1], o = t.widthRatio * e.width, c = a === void 0 ? o * .7 : a.widthRatio * e.width, l = e.y + n * (r + this.gap), u = e.x + e.width / 2, d = u - o / 2, f = u + o / 2, p = u - c / 2, m = u + c / 2, h = n === this.hoveredIndex ? i(t.color, 1 - s) : t.color;
|
|
63
207
|
if (this.ctx.save(), this.ctx.beginPath(), this.curved) {
|
|
64
|
-
let e =
|
|
65
|
-
this.ctx.moveTo(d,
|
|
66
|
-
} else this.ctx.moveTo(d,
|
|
67
|
-
this.ctx.closePath(), this.ctx.fillStyle = h, this.ctx.fill(), this.ctx.restore(), this.drawSegmentText(u,
|
|
208
|
+
let e = l + r * .6;
|
|
209
|
+
this.ctx.moveTo(d, l), this.ctx.lineTo(f, l), this.ctx.quadraticCurveTo(f, e, m, l + r), this.ctx.lineTo(p, l + r), this.ctx.quadraticCurveTo(d, e, d, l);
|
|
210
|
+
} else this.ctx.moveTo(d, l), this.ctx.lineTo(f, l), this.ctx.lineTo(m, l + r), this.ctx.lineTo(p, l + r);
|
|
211
|
+
this.ctx.closePath(), this.ctx.fillStyle = h, this.ctx.fill(), this.ctx.restore(), this.drawSegmentText(u, l + r / 2, t);
|
|
68
212
|
}
|
|
69
213
|
}
|
|
70
214
|
renderHorizontal() {
|
|
@@ -74,16 +218,17 @@ var a = "#334155", o = "#64748b", s = "12px system-ui, sans-serif", c = "11px sy
|
|
|
74
218
|
for (let n = 0; n < t; n++) {
|
|
75
219
|
let t = this.resolved[n];
|
|
76
220
|
if (t === void 0) continue;
|
|
77
|
-
let a = this.resolved[n + 1], o = t.widthRatio * e.height,
|
|
221
|
+
let a = this.resolved[n + 1], o = t.widthRatio * e.height, c = a === void 0 ? o * .7 : a.widthRatio * e.height, l = e.x + n * (r + this.gap), u = e.y + e.height / 2, d = u - o / 2, f = u + o / 2, p = u - c / 2, m = u + c / 2, h = n === this.hoveredIndex ? i(t.color, 1 - s) : t.color;
|
|
78
222
|
if (this.ctx.save(), this.ctx.beginPath(), this.curved) {
|
|
79
|
-
let e =
|
|
80
|
-
this.ctx.moveTo(
|
|
81
|
-
} else this.ctx.moveTo(
|
|
82
|
-
this.ctx.closePath(), this.ctx.fillStyle = h, this.ctx.fill(), this.ctx.restore(), this.drawSegmentText(
|
|
223
|
+
let e = l + r * .6;
|
|
224
|
+
this.ctx.moveTo(l, d), this.ctx.lineTo(l, f), this.ctx.quadraticCurveTo(e, f, l + r, m), this.ctx.lineTo(l + r, p), this.ctx.quadraticCurveTo(e, d, l, d);
|
|
225
|
+
} else this.ctx.moveTo(l, d), this.ctx.lineTo(l, f), this.ctx.lineTo(l + r, m), this.ctx.lineTo(l + r, p);
|
|
226
|
+
this.ctx.closePath(), this.ctx.fillStyle = h, this.ctx.fill(), this.ctx.restore(), this.drawSegmentText(l + r / 2, u, t);
|
|
83
227
|
}
|
|
84
228
|
}
|
|
85
229
|
drawSegmentText(e, t, n) {
|
|
86
|
-
|
|
230
|
+
let { text: i, shadow: s } = l(n.color);
|
|
231
|
+
this.ctx.save(), this.ctx.textAlign = "center", this.ctx.textBaseline = "middle", this.ctx.shadowColor = s, this.ctx.shadowBlur = 4, this.ctx.shadowOffsetY = 1, this.ctx.fillStyle = i, this.showLabels && this.showValues ? (this.ctx.font = a, this.ctx.fillText(n.label, e, t - 8), this.ctx.font = o, this.ctx.globalAlpha = .9, this.ctx.fillText(`${r(n.value)} (${n.percentage.toFixed(1)}%)`, e, t + 8)) : this.showLabels ? (this.ctx.font = a, this.ctx.fillText(n.label, e, t)) : this.showValues && (this.ctx.font = o, this.ctx.fillText(r(n.value), e, t)), this.ctx.restore();
|
|
87
232
|
}
|
|
88
233
|
handleMouseMove(e) {
|
|
89
234
|
let t = this.canvas.getBoundingClientRect(), n = e.clientX - t.left, i = e.clientY - t.top, a = this.plotArea, o = this.resolved.length;
|
|
@@ -121,6 +266,6 @@ var a = "#334155", o = "#64748b", s = "12px system-ui, sans-serif", c = "11px sy
|
|
|
121
266
|
}
|
|
122
267
|
};
|
|
123
268
|
//#endregion
|
|
124
|
-
export {
|
|
269
|
+
export { b as FunnelChart };
|
|
125
270
|
|
|
126
271
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/charts/funnel/index.ts"],"sourcesContent":["/**\n * Funnel chart using Canvas 2D\n * @countermeasure/web-components/charts/funnel\n */\n\nimport { BaseChart } from '../base'\nimport { defaultColors, hexToRgba, formatNumber } from '../utils'\nimport { ChartTooltip } from '../tooltip'\nimport type { FunnelChartConfig, FunnelSegment } from './types'\n\nconst LABEL_COLOR = '#334155'\nconst VALUE_COLOR = '#64748b'\nconst LABEL_FONT = '12px system-ui, sans-serif'\nconst VALUE_FONT = '11px system-ui, sans-serif'\nconst HOVER_ALPHA_DELTA = 0.12\n\ninterface ResolvedSegment {\n label: string\n value: number\n color: string\n /** Width ratio relative to max value (0-1) */\n widthRatio: number\n /** Percentage of first segment value */\n percentage: number\n}\n\nexport class FunnelChart extends BaseChart {\n private segments: FunnelSegment[]\n private resolved: ResolvedSegment[] = []\n private colors: string[]\n private showLabels: boolean\n private showValues: boolean\n private direction: 'vertical' | 'horizontal'\n private curved: boolean\n private gap: number\n\n private tooltip: ChartTooltip | null = null\n private hoveredIndex = -1\n private boundMouseMove: (e: MouseEvent) => void\n private boundMouseLeave: () => void\n\n constructor(config: FunnelChartConfig) {\n super(config)\n this.segments = config.segments\n this.colors = defaultColors()\n this.showLabels = config.showLabels ?? true\n this.showValues = config.showValues ?? true\n this.direction = config.direction ?? 'vertical'\n this.curved = config.curved ?? false\n this.gap = config.gap ?? 2\n\n if (config.tooltip?.enabled !== false) {\n this.tooltip = new ChartTooltip(this.container)\n }\n\n this.boundMouseMove = (e: MouseEvent) => {\n this.handleMouseMove(e)\n }\n this.boundMouseLeave = () => {\n this.handleMouseLeave()\n }\n\n this.canvas.addEventListener('mousemove', this.boundMouseMove)\n this.canvas.addEventListener('mouseleave', this.boundMouseLeave)\n\n this.resolveSegments()\n this.render()\n }\n\n /** Replace the segments and re-render */\n update(segments: FunnelSegment[]): void {\n this.segments = segments\n this.resolveSegments()\n this.render()\n }\n\n render(): void {\n this.clear()\n\n if (this.resolved.length === 0) return\n\n if (this.direction === 'horizontal') {\n this.renderHorizontal()\n } else {\n this.renderVertical()\n }\n }\n\n override destroy(): void {\n this.canvas.removeEventListener('mousemove', this.boundMouseMove)\n this.canvas.removeEventListener('mouseleave', this.boundMouseLeave)\n if (this.tooltip !== null) {\n this.tooltip.destroy()\n }\n super.destroy()\n }\n\n private resolveSegments(): void {\n const maxVal = this.findMaxValue()\n const firstVal = this.segments[0]?.value ?? 1\n\n this.resolved = []\n\n for (let i = 0; i < this.segments.length; i++) {\n const seg = this.segments[i]\n if (seg === undefined) continue\n\n const color = seg.color ?? this.colors[i % this.colors.length] ?? '#3b82f6'\n const widthRatio = maxVal > 0 ? seg.value / maxVal : 0\n const percentage = firstVal > 0 ? (seg.value / firstVal) * 100 : 0\n\n this.resolved.push({\n label: seg.label,\n value: seg.value,\n color,\n widthRatio,\n percentage,\n })\n }\n }\n\n private findMaxValue(): number {\n let max = 0\n for (const seg of this.segments) {\n if (seg.value > max) {\n max = seg.value\n }\n }\n return max\n }\n\n private renderVertical(): void {\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentHeight = (plot.height - totalGap) / count\n\n for (let i = 0; i < count; i++) {\n const seg = this.resolved[i]\n if (seg === undefined) continue\n\n const nextSeg = this.resolved[i + 1]\n const currentWidth = seg.widthRatio * plot.width\n const nextWidth = nextSeg !== undefined ? nextSeg.widthRatio * plot.width : currentWidth * 0.7\n\n const y = plot.y + i * (segmentHeight + this.gap)\n const cx = plot.x + plot.width / 2\n\n const topLeft = cx - currentWidth / 2\n const topRight = cx + currentWidth / 2\n const bottomLeft = cx - nextWidth / 2\n const bottomRight = cx + nextWidth / 2\n\n const isHovered = i === this.hoveredIndex\n const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color\n\n this.ctx.save()\n this.ctx.beginPath()\n\n if (this.curved) {\n const cpY = y + segmentHeight * 0.6\n this.ctx.moveTo(topLeft, y)\n this.ctx.lineTo(topRight, y)\n this.ctx.quadraticCurveTo(topRight, cpY, bottomRight, y + segmentHeight)\n this.ctx.lineTo(bottomLeft, y + segmentHeight)\n this.ctx.quadraticCurveTo(topLeft, cpY, topLeft, y)\n } else {\n this.ctx.moveTo(topLeft, y)\n this.ctx.lineTo(topRight, y)\n this.ctx.lineTo(bottomRight, y + segmentHeight)\n this.ctx.lineTo(bottomLeft, y + segmentHeight)\n }\n\n this.ctx.closePath()\n this.ctx.fillStyle = fillColor\n this.ctx.fill()\n this.ctx.restore()\n\n // Draw text\n this.drawSegmentText(cx, y + segmentHeight / 2, seg)\n }\n }\n\n private renderHorizontal(): void {\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentWidth = (plot.width - totalGap) / count\n\n for (let i = 0; i < count; i++) {\n const seg = this.resolved[i]\n if (seg === undefined) continue\n\n const nextSeg = this.resolved[i + 1]\n const currentHeight = seg.widthRatio * plot.height\n const nextHeight =\n nextSeg !== undefined ? nextSeg.widthRatio * plot.height : currentHeight * 0.7\n\n const x = plot.x + i * (segmentWidth + this.gap)\n const cy = plot.y + plot.height / 2\n\n const leftTop = cy - currentHeight / 2\n const leftBottom = cy + currentHeight / 2\n const rightTop = cy - nextHeight / 2\n const rightBottom = cy + nextHeight / 2\n\n const isHovered = i === this.hoveredIndex\n const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color\n\n this.ctx.save()\n this.ctx.beginPath()\n\n if (this.curved) {\n const cpX = x + segmentWidth * 0.6\n this.ctx.moveTo(x, leftTop)\n this.ctx.lineTo(x, leftBottom)\n this.ctx.quadraticCurveTo(cpX, leftBottom, x + segmentWidth, rightBottom)\n this.ctx.lineTo(x + segmentWidth, rightTop)\n this.ctx.quadraticCurveTo(cpX, leftTop, x, leftTop)\n } else {\n this.ctx.moveTo(x, leftTop)\n this.ctx.lineTo(x, leftBottom)\n this.ctx.lineTo(x + segmentWidth, rightBottom)\n this.ctx.lineTo(x + segmentWidth, rightTop)\n }\n\n this.ctx.closePath()\n this.ctx.fillStyle = fillColor\n this.ctx.fill()\n this.ctx.restore()\n\n // Draw text\n this.drawSegmentText(x + segmentWidth / 2, cy, seg)\n }\n }\n\n private drawSegmentText(cx: number, cy: number, seg: ResolvedSegment): void {\n this.ctx.save()\n this.ctx.textAlign = 'center'\n this.ctx.textBaseline = 'middle'\n\n if (this.showLabels && this.showValues) {\n this.ctx.fillStyle = LABEL_COLOR\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(seg.label, cx, cy - 8)\n\n this.ctx.fillStyle = VALUE_COLOR\n this.ctx.font = VALUE_FONT\n this.ctx.fillText(`${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`, cx, cy + 8)\n } else if (this.showLabels) {\n this.ctx.fillStyle = LABEL_COLOR\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(seg.label, cx, cy)\n } else if (this.showValues) {\n this.ctx.fillStyle = VALUE_COLOR\n this.ctx.font = VALUE_FONT\n this.ctx.fillText(formatNumber(seg.value), cx, cy)\n }\n\n this.ctx.restore()\n }\n\n private handleMouseMove(e: MouseEvent): void {\n const rect = this.canvas.getBoundingClientRect()\n const mx = e.clientX - rect.left\n const my = e.clientY - rect.top\n\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n let found = -1\n\n if (this.direction === 'horizontal') {\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentWidth = (plot.width - totalGap) / count\n const relX = mx - plot.x\n\n if (relX >= 0 && relX <= plot.width && my >= plot.y && my <= plot.y + plot.height) {\n for (let i = 0; i < count; i++) {\n const segStart = i * (segmentWidth + this.gap)\n const segEnd = segStart + segmentWidth\n if (relX >= segStart && relX < segEnd) {\n found = i\n break\n }\n }\n }\n } else {\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentHeight = (plot.height - totalGap) / count\n const relY = my - plot.y\n\n if (relY >= 0 && relY <= plot.height && mx >= plot.x && mx <= plot.x + plot.width) {\n for (let i = 0; i < count; i++) {\n const segStart = i * (segmentHeight + this.gap)\n const segEnd = segStart + segmentHeight\n if (relY >= segStart && relY < segEnd) {\n found = i\n break\n }\n }\n }\n }\n\n if (found !== this.hoveredIndex) {\n this.hoveredIndex = found\n this.render()\n }\n\n if (found !== -1 && this.tooltip !== null) {\n const seg = this.resolved[found]\n if (seg !== undefined) {\n const content = `<strong>${seg.label}</strong><br/>${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`\n this.tooltip.show(mx, my, content)\n }\n } else if (this.tooltip !== null) {\n this.tooltip.hide()\n }\n }\n\n private handleMouseLeave(): void {\n if (this.hoveredIndex !== -1) {\n this.hoveredIndex = -1\n if (this.tooltip !== null) {\n this.tooltip.hide()\n }\n this.render()\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/charts/funnel/index.ts"],"sourcesContent":["/**\n * Funnel chart using Canvas 2D\n * @countermeasure/web-components/charts/funnel\n */\n\nimport { BaseChart } from '../base'\nimport { defaultColors, hexToRgba, formatNumber } from '../utils'\nimport { ChartTooltip } from '../tooltip'\nimport type { FunnelChartConfig, FunnelSegment } from './types'\n\nconst LABEL_FONT = '600 12px ui-sans-serif, system-ui, -apple-system, sans-serif'\nconst VALUE_FONT = '500 11px ui-sans-serif, system-ui, -apple-system, sans-serif'\nconst HOVER_ALPHA_DELTA = 0.12\nconst NAMED_COLOR_RGB = new Map<string, [number, number, number]>([\n ['black', [0, 0, 0]],\n ['white', [255, 255, 255]],\n ['navy', [0, 0, 128]],\n ['rebeccapurple', [102, 51, 153]],\n ['transparent', [0, 0, 0]],\n])\n\ninterface ResolvedSegment {\n label: string\n value: number\n color: string\n /** Width ratio relative to max value (0-1) */\n widthRatio: number\n /** Percentage of first segment value */\n percentage: number\n}\n\n/**\n * Pick a readable text foreground/shadow pair for a given segment fill color.\n *\n * Funnel labels sit on top of saturated brand colors that vary segment-to-\n * segment, so we can't just use a single theme-foreground value — light text\n * vanishes on yellow/orange, dark text vanishes on navy/teal. Compute the\n * fill's perceived luminance (sRGB → relative-luminance approximation) and\n * pick white-on-shadow or black-on-shadow accordingly.\n */\nfunction pickReadableText(color: string): { text: string; shadow: string } {\n const rgb = parseColorToRgb(color)\n if (rgb === null) {\n return { text: '#0f172a', shadow: 'rgba(255, 255, 255, 0.45)' }\n }\n const [r, g, b] = rgb\n const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n if (luminance > 0.6) {\n return { text: '#0f172a', shadow: 'rgba(255, 255, 255, 0.45)' }\n }\n return { text: '#ffffff', shadow: 'rgba(0, 0, 0, 0.45)' }\n}\n\nfunction parseColorToRgb(color: string): [number, number, number] | null {\n const trimmed = color.trim()\n const parsedLiteral = parseColorLiteralToRgb(trimmed)\n if (parsedLiteral !== null) {\n return parsedLiteral\n }\n\n const named = NAMED_COLOR_RGB.get(trimmed.toLowerCase())\n if (named !== undefined) {\n return named\n }\n\n // For anything else — modern CSS syntaxes (oklch, color-mix, lab, color())\n // and the long tail of CSS named colors — round-trip through canvas to get\n // a canonical rgb()/rgba() string we can then parse with our literal parser.\n const normalized = normalizeColorViaCanvas(trimmed)\n if (normalized === null) {\n return null\n }\n return parseColorLiteralToRgb(normalized)\n}\n\nfunction parseColorLiteralToRgb(trimmed: string): [number, number, number] | null {\n if (trimmed.startsWith('#')) {\n const hex = trimmed.slice(1)\n if (hex.length === 3) {\n const r = parseInt(hex[0]! + hex[0], 16)\n const g = parseInt(hex[1]! + hex[1], 16)\n const b = parseInt(hex[2]! + hex[2], 16)\n return [r, g, b]\n }\n if (hex.length === 6) {\n const r = parseInt(hex.slice(0, 2), 16)\n const g = parseInt(hex.slice(2, 4), 16)\n const b = parseInt(hex.slice(4, 6), 16)\n return [r, g, b]\n }\n return null\n }\n const rgbMatch = trimmed.match(/^rgba?\\(([^)]+)\\)$/i)\n if (rgbMatch) {\n const parts = parseColorFunctionArgs(rgbMatch[1]!)\n const r = parseRgbChannel(parts[0])\n const g = parseRgbChannel(parts[1])\n const b = parseRgbChannel(parts[2])\n if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {\n return [r, g, b]\n }\n }\n const hslMatch = trimmed.match(/^hsla?\\(([^)]+)\\)$/i)\n if (hslMatch) {\n const parts = parseColorFunctionArgs(hslMatch[1]!)\n const h = parseHue(parts[0])\n const s = parsePercentage(parts[1])\n const l = parsePercentage(parts[2])\n if (Number.isFinite(h) && Number.isFinite(s) && Number.isFinite(l)) {\n return hslToRgb(h, s, l)\n }\n }\n return null\n}\n\n// Lazily-allocated 1×1 canvas reused across color normalization calls. The\n// canvas itself is stateless w.r.t. fillStyle (each call writes both sentinel\n// and the input), so it's safe to share — and re-creating one per call would\n// produce a constant stream of small DOM nodes for funnels with many segments\n// that re-render on hover.\nlet normalizeCanvasCtx: CanvasRenderingContext2D | null | undefined\n\nfunction getNormalizeCanvasCtx(): CanvasRenderingContext2D | null {\n if (normalizeCanvasCtx !== undefined) return normalizeCanvasCtx\n if (typeof document === 'undefined') {\n normalizeCanvasCtx = null\n return null\n }\n const canvas = document.createElement('canvas')\n canvas.width = 1\n canvas.height = 1\n normalizeCanvasCtx = canvas.getContext('2d')\n return normalizeCanvasCtx\n}\n\n/**\n * Round-trip an arbitrary CSS color string through a canvas 2D context's\n * `fillStyle` so the browser canonicalizes it to `rgb(...)` or `rgba(...)`.\n * Returns `null` if the color is invalid (canvas keeps the previous value\n * when assignment is rejected — we detect this by comparing against a known\n * sentinel written immediately before the actual color).\n */\nfunction normalizeColorViaCanvas(color: string): string | null {\n const ctx = getNormalizeCanvasCtx()\n if (ctx === null) return null\n\n // Write a known sentinel and read it back to get its canonical form, then\n // write the candidate color. If canvas rejected `color` as invalid, the\n // post-assignment `fillStyle` will still equal the canonicalized sentinel.\n // Reading between writes also prevents linters from flagging the first\n // write as dead.\n ctx.fillStyle = '#010203'\n const baseline = ctx.fillStyle\n ctx.fillStyle = color\n const normalized = ctx.fillStyle\n if (normalized === baseline && color.trim().toLowerCase() !== '#010203') {\n return null\n }\n return typeof normalized === 'string' ? normalized : null\n}\n\nfunction parseColorFunctionArgs(input: string): string[] {\n const channels = input.split('/')[0]!.trim()\n return channels.includes(',')\n ? channels.split(',').map(part => part.trim())\n : channels.split(/\\s+/).map(part => part.trim())\n}\n\nfunction parseRgbChannel(part: string | undefined): number {\n if (part === undefined) return Number.NaN\n if (part.endsWith('%')) {\n const value = Number(part.slice(0, -1))\n return Number.isFinite(value) ? Math.round((value / 100) * 255) : Number.NaN\n }\n return Number(part)\n}\n\nfunction parseHue(part: string | undefined): number {\n if (part === undefined) return Number.NaN\n if (part.endsWith('turn')) {\n const value = Number(part.slice(0, -4))\n return Number.isFinite(value) ? value % 1 : Number.NaN\n }\n if (part.endsWith('rad')) {\n const value = Number(part.slice(0, -3))\n return Number.isFinite(value) ? (value / (Math.PI * 2)) % 1 : Number.NaN\n }\n const raw = part.endsWith('deg') ? part.slice(0, -3) : part\n const value = Number(raw)\n return Number.isFinite(value) ? (((value % 360) + 360) % 360) / 360 : Number.NaN\n}\n\nfunction parsePercentage(part: string | undefined): number {\n if (part === undefined || !part.endsWith('%')) return Number.NaN\n const value = Number(part.slice(0, -1))\n return Number.isFinite(value) ? value / 100 : Number.NaN\n}\n\nfunction hslToRgb(h: number, s: number, l: number): [number, number, number] {\n if (s === 0) {\n const value = Math.round(l * 255)\n return [value, value, value]\n }\n const hue2rgb = (p: number, q: number, t: number): number => {\n let hue = t\n if (hue < 0) hue += 1\n if (hue > 1) hue -= 1\n if (hue < 1 / 6) return p + (q - p) * 6 * hue\n if (hue < 1 / 2) return q\n if (hue < 2 / 3) return p + (q - p) * (2 / 3 - hue) * 6\n return p\n }\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s\n const p = 2 * l - q\n return [\n Math.round(hue2rgb(p, q, h + 1 / 3) * 255),\n Math.round(hue2rgb(p, q, h) * 255),\n Math.round(hue2rgb(p, q, h - 1 / 3) * 255),\n ]\n}\n\nexport class FunnelChart extends BaseChart {\n private segments: FunnelSegment[]\n private resolved: ResolvedSegment[] = []\n private colors: string[]\n private showLabels: boolean\n private showValues: boolean\n private direction: 'vertical' | 'horizontal'\n private curved: boolean\n private gap: number\n\n private tooltip: ChartTooltip | null = null\n private hoveredIndex = -1\n private boundMouseMove: (e: MouseEvent) => void\n private boundMouseLeave: () => void\n\n constructor(config: FunnelChartConfig) {\n super(config)\n this.segments = config.segments\n this.colors = defaultColors()\n this.showLabels = config.showLabels ?? true\n this.showValues = config.showValues ?? true\n this.direction = config.direction ?? 'vertical'\n this.curved = config.curved ?? false\n this.gap = config.gap ?? 2\n\n if (config.tooltip?.enabled !== false) {\n this.tooltip = new ChartTooltip(this.container)\n }\n\n this.boundMouseMove = (e: MouseEvent) => {\n this.handleMouseMove(e)\n }\n this.boundMouseLeave = () => {\n this.handleMouseLeave()\n }\n\n this.canvas.addEventListener('mousemove', this.boundMouseMove)\n this.canvas.addEventListener('mouseleave', this.boundMouseLeave)\n\n this.resolveSegments()\n this.render()\n }\n\n /** Replace the segments and re-render */\n update(segments: FunnelSegment[]): void {\n this.segments = segments\n this.resolveSegments()\n this.render()\n }\n\n render(): void {\n this.clear()\n\n if (this.resolved.length === 0) return\n\n if (this.direction === 'horizontal') {\n this.renderHorizontal()\n } else {\n this.renderVertical()\n }\n }\n\n override destroy(): void {\n this.canvas.removeEventListener('mousemove', this.boundMouseMove)\n this.canvas.removeEventListener('mouseleave', this.boundMouseLeave)\n if (this.tooltip !== null) {\n this.tooltip.destroy()\n }\n super.destroy()\n }\n\n private resolveSegments(): void {\n const maxVal = this.findMaxValue()\n const firstVal = this.segments[0]?.value ?? 1\n\n this.resolved = []\n\n for (let i = 0; i < this.segments.length; i++) {\n const seg = this.segments[i]\n if (seg === undefined) continue\n\n const color = seg.color ?? this.colors[i % this.colors.length] ?? '#3b82f6'\n const widthRatio = maxVal > 0 ? seg.value / maxVal : 0\n const percentage = firstVal > 0 ? (seg.value / firstVal) * 100 : 0\n\n this.resolved.push({\n label: seg.label,\n value: seg.value,\n color,\n widthRatio,\n percentage,\n })\n }\n }\n\n private findMaxValue(): number {\n let max = 0\n for (const seg of this.segments) {\n if (seg.value > max) {\n max = seg.value\n }\n }\n return max\n }\n\n private renderVertical(): void {\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentHeight = (plot.height - totalGap) / count\n\n for (let i = 0; i < count; i++) {\n const seg = this.resolved[i]\n if (seg === undefined) continue\n\n const nextSeg = this.resolved[i + 1]\n const currentWidth = seg.widthRatio * plot.width\n const nextWidth = nextSeg !== undefined ? nextSeg.widthRatio * plot.width : currentWidth * 0.7\n\n const y = plot.y + i * (segmentHeight + this.gap)\n const cx = plot.x + plot.width / 2\n\n const topLeft = cx - currentWidth / 2\n const topRight = cx + currentWidth / 2\n const bottomLeft = cx - nextWidth / 2\n const bottomRight = cx + nextWidth / 2\n\n const isHovered = i === this.hoveredIndex\n const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color\n\n this.ctx.save()\n this.ctx.beginPath()\n\n if (this.curved) {\n const cpY = y + segmentHeight * 0.6\n this.ctx.moveTo(topLeft, y)\n this.ctx.lineTo(topRight, y)\n this.ctx.quadraticCurveTo(topRight, cpY, bottomRight, y + segmentHeight)\n this.ctx.lineTo(bottomLeft, y + segmentHeight)\n this.ctx.quadraticCurveTo(topLeft, cpY, topLeft, y)\n } else {\n this.ctx.moveTo(topLeft, y)\n this.ctx.lineTo(topRight, y)\n this.ctx.lineTo(bottomRight, y + segmentHeight)\n this.ctx.lineTo(bottomLeft, y + segmentHeight)\n }\n\n this.ctx.closePath()\n this.ctx.fillStyle = fillColor\n this.ctx.fill()\n this.ctx.restore()\n\n // Draw text\n this.drawSegmentText(cx, y + segmentHeight / 2, seg)\n }\n }\n\n private renderHorizontal(): void {\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentWidth = (plot.width - totalGap) / count\n\n for (let i = 0; i < count; i++) {\n const seg = this.resolved[i]\n if (seg === undefined) continue\n\n const nextSeg = this.resolved[i + 1]\n const currentHeight = seg.widthRatio * plot.height\n const nextHeight =\n nextSeg !== undefined ? nextSeg.widthRatio * plot.height : currentHeight * 0.7\n\n const x = plot.x + i * (segmentWidth + this.gap)\n const cy = plot.y + plot.height / 2\n\n const leftTop = cy - currentHeight / 2\n const leftBottom = cy + currentHeight / 2\n const rightTop = cy - nextHeight / 2\n const rightBottom = cy + nextHeight / 2\n\n const isHovered = i === this.hoveredIndex\n const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color\n\n this.ctx.save()\n this.ctx.beginPath()\n\n if (this.curved) {\n const cpX = x + segmentWidth * 0.6\n this.ctx.moveTo(x, leftTop)\n this.ctx.lineTo(x, leftBottom)\n this.ctx.quadraticCurveTo(cpX, leftBottom, x + segmentWidth, rightBottom)\n this.ctx.lineTo(x + segmentWidth, rightTop)\n this.ctx.quadraticCurveTo(cpX, leftTop, x, leftTop)\n } else {\n this.ctx.moveTo(x, leftTop)\n this.ctx.lineTo(x, leftBottom)\n this.ctx.lineTo(x + segmentWidth, rightBottom)\n this.ctx.lineTo(x + segmentWidth, rightTop)\n }\n\n this.ctx.closePath()\n this.ctx.fillStyle = fillColor\n this.ctx.fill()\n this.ctx.restore()\n\n // Draw text\n this.drawSegmentText(x + segmentWidth / 2, cy, seg)\n }\n }\n\n private drawSegmentText(cx: number, cy: number, seg: ResolvedSegment): void {\n const { text, shadow } = pickReadableText(seg.color)\n\n this.ctx.save()\n this.ctx.textAlign = 'center'\n this.ctx.textBaseline = 'middle'\n this.ctx.shadowColor = shadow\n this.ctx.shadowBlur = 4\n this.ctx.shadowOffsetY = 1\n this.ctx.fillStyle = text\n\n if (this.showLabels && this.showValues) {\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(seg.label, cx, cy - 8)\n\n this.ctx.font = VALUE_FONT\n this.ctx.globalAlpha = 0.9\n this.ctx.fillText(`${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`, cx, cy + 8)\n } else if (this.showLabels) {\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(seg.label, cx, cy)\n } else if (this.showValues) {\n this.ctx.font = VALUE_FONT\n this.ctx.fillText(formatNumber(seg.value), cx, cy)\n }\n\n this.ctx.restore()\n }\n\n private handleMouseMove(e: MouseEvent): void {\n const rect = this.canvas.getBoundingClientRect()\n const mx = e.clientX - rect.left\n const my = e.clientY - rect.top\n\n const plot = this.plotArea\n const count = this.resolved.length\n if (count === 0) return\n\n let found = -1\n\n if (this.direction === 'horizontal') {\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentWidth = (plot.width - totalGap) / count\n const relX = mx - plot.x\n\n if (relX >= 0 && relX <= plot.width && my >= plot.y && my <= plot.y + plot.height) {\n for (let i = 0; i < count; i++) {\n const segStart = i * (segmentWidth + this.gap)\n const segEnd = segStart + segmentWidth\n if (relX >= segStart && relX < segEnd) {\n found = i\n break\n }\n }\n }\n } else {\n const totalGap = this.gap * Math.max(0, count - 1)\n const segmentHeight = (plot.height - totalGap) / count\n const relY = my - plot.y\n\n if (relY >= 0 && relY <= plot.height && mx >= plot.x && mx <= plot.x + plot.width) {\n for (let i = 0; i < count; i++) {\n const segStart = i * (segmentHeight + this.gap)\n const segEnd = segStart + segmentHeight\n if (relY >= segStart && relY < segEnd) {\n found = i\n break\n }\n }\n }\n }\n\n if (found !== this.hoveredIndex) {\n this.hoveredIndex = found\n this.render()\n }\n\n if (found !== -1 && this.tooltip !== null) {\n const seg = this.resolved[found]\n if (seg !== undefined) {\n const content = `<strong>${seg.label}</strong><br/>${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`\n this.tooltip.show(mx, my, content)\n }\n } else if (this.tooltip !== null) {\n this.tooltip.hide()\n }\n }\n\n private handleMouseLeave(): void {\n if (this.hoveredIndex !== -1) {\n this.hoveredIndex = -1\n if (this.tooltip !== null) {\n this.tooltip.hide()\n }\n this.render()\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -16,11 +16,28 @@ export declare class GaugeChart extends BaseChart {
|
|
|
16
16
|
updateValue(value: number): void;
|
|
17
17
|
/** Update the full configuration and re-render */
|
|
18
18
|
updateConfig(config: Partial<GaugeChartConfig>): void;
|
|
19
|
+
/**
|
|
20
|
+
* Advance `endAngle` by whole 360° turns until it exceeds `startAngle`, so
|
|
21
|
+
* Canvas always draws a positive sweep regardless of how the consumer
|
|
22
|
+
* supplied the angles. Capped at `MAX_ANGLE_NORMALIZE_TURNS` iterations to
|
|
23
|
+
* guarantee termination on pathological inputs (NaN, ±Infinity, very large
|
|
24
|
+
* negative deltas).
|
|
25
|
+
*/
|
|
26
|
+
private normalizeAngles;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a tonal palette from the container's computed styles so the chart
|
|
29
|
+
* follows the active theme (light / dark / monokai / glass). Falls back to
|
|
30
|
+
* neutral light-mode values when the chart isn't attached to the document
|
|
31
|
+
* or when the variable is unset.
|
|
32
|
+
*/
|
|
33
|
+
private resolvePalette;
|
|
19
34
|
render(): void;
|
|
20
35
|
destroy(): void;
|
|
21
36
|
private drawThresholdArcs;
|
|
37
|
+
private drawCapArc;
|
|
22
38
|
private drawFillArc;
|
|
23
39
|
private drawNeedle;
|
|
24
40
|
private drawCenterText;
|
|
41
|
+
private drawMinMaxLabels;
|
|
25
42
|
}
|
|
26
43
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/charts/gauge/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/charts/gauge/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,SAAS,CAAA;AAkC/D,qBAAa,UAAW,SAAQ,SAAS;IACvC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,GAAG,CAAQ;IACnB,OAAO,CAAC,GAAG,CAAQ;IACnB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,EAAE,gBAAgB;IAqBpC,kDAAkD;IAClD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKhC,kDAAkD;IAClD,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAwCrD;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAMvB;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IA4EtB,MAAM,IAAI,IAAI;IAkDL,OAAO,IAAI,IAAI;IAIxB,OAAO,CAAC,iBAAiB;IAgGzB,OAAO,CAAC,UAAU;IAsBlB,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,UAAU;IAoDlB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,gBAAgB;CAyBzB"}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { BaseChart as e } from "../base.js";
|
|
2
2
|
import { clamp as t, defaultColors as n, formatNumber as r } from "../utils.js";
|
|
3
3
|
//#region src/charts/gauge/index.ts
|
|
4
|
-
var i = Math.PI / 180, a = "
|
|
4
|
+
var i = Math.PI / 180, a = "700 32px ui-sans-serif, system-ui, -apple-system, sans-serif", o = "500 13px ui-sans-serif, system-ui, -apple-system, sans-serif", s = "500 11px ui-sans-serif, system-ui, -apple-system, sans-serif", c = .18, l = .08, u = .3, d = .25, f = {
|
|
5
|
+
track: `rgba(148, 163, 184, ${String(c)})`,
|
|
6
|
+
trackInner: `rgba(148, 163, 184, ${String(l)})`,
|
|
7
|
+
foreground: "#0f172a",
|
|
8
|
+
muted: "#64748b",
|
|
9
|
+
needle: "#0f172a",
|
|
10
|
+
needleSoft: `rgba(15, 23, 42, ${String(u)})`,
|
|
11
|
+
fill: "#3b82f6",
|
|
12
|
+
shadow: `rgba(15, 23, 42, ${String(d)})`
|
|
13
|
+
}, p = n()[0] ?? f.fill, m = 32, h = class extends e {
|
|
5
14
|
value;
|
|
6
15
|
min;
|
|
7
16
|
max;
|
|
@@ -13,58 +22,126 @@ var i = Math.PI / 180, a = "#e2e8f0", o = "#1e293b", s = "bold 28px system-ui, s
|
|
|
13
22
|
endAngle;
|
|
14
23
|
showValue;
|
|
15
24
|
constructor(e) {
|
|
16
|
-
super(e), this.value = e.value, this.min = e.min ?? 0, this.max = e.max ?? 100, this.label = e.label, this.unit = e.unit, this.thresholds = e.thresholds ?? [], this.arcWidth = e.arcWidth ?? 20, this.startAngle = e.startAngle ?? 135, this.endAngle = e.endAngle ?? 405, this.showValue = e.showValue ?? !0, this.render();
|
|
25
|
+
super(e), this.value = e.value, this.min = e.min ?? 0, this.max = e.max ?? 100, this.label = e.label, this.unit = e.unit, this.thresholds = e.thresholds ?? [], this.arcWidth = e.arcWidth ?? 20, this.startAngle = e.startAngle ?? 135, this.endAngle = e.endAngle ?? 405, this.normalizeAngles(), this.showValue = e.showValue ?? !0, this.render();
|
|
17
26
|
}
|
|
18
27
|
updateValue(e) {
|
|
19
28
|
this.value = e, this.render();
|
|
20
29
|
}
|
|
21
30
|
updateConfig(e) {
|
|
22
|
-
e.value !== void 0 && (this.value = e.value), e.min !== void 0 && (this.min = e.min), e.max !== void 0 && (this.max = e.max), e.label !== void 0 && (this.label = e.label), e.unit !== void 0 && (this.unit = e.unit), e.thresholds !== void 0 && (this.thresholds = e.thresholds), e.arcWidth !== void 0 && (this.arcWidth = e.arcWidth), e.startAngle !== void 0 && (this.startAngle = e.startAngle), e.endAngle !== void 0 && (this.endAngle = e.endAngle), e.showValue !== void 0 && (this.showValue = e.showValue), this.render();
|
|
31
|
+
e.value !== void 0 && (this.value = e.value), e.min !== void 0 && (this.min = e.min), e.max !== void 0 && (this.max = e.max), e.label !== void 0 && (this.label = e.label), e.unit !== void 0 && (this.unit = e.unit), e.thresholds !== void 0 && (this.thresholds = e.thresholds), e.arcWidth !== void 0 && (this.arcWidth = e.arcWidth), e.startAngle !== void 0 && (this.startAngle = e.startAngle), e.endAngle !== void 0 && (this.endAngle = e.endAngle), (e.startAngle !== void 0 || e.endAngle !== void 0) && this.normalizeAngles(), e.showValue !== void 0 && (this.showValue = e.showValue), this.render();
|
|
32
|
+
}
|
|
33
|
+
normalizeAngles() {
|
|
34
|
+
for (let e = 0; e < m && this.endAngle <= this.startAngle; e++) this.endAngle += 360;
|
|
35
|
+
}
|
|
36
|
+
resolvePalette() {
|
|
37
|
+
let e = this.container;
|
|
38
|
+
if (!(e instanceof HTMLElement) || !e.isConnected) return f;
|
|
39
|
+
let t = e.ownerDocument.defaultView?.getComputedStyle(e);
|
|
40
|
+
if (t === void 0) return f;
|
|
41
|
+
let n = /^var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)\s*$/, r = (e, i = 0) => {
|
|
42
|
+
if (i >= 5) return "";
|
|
43
|
+
let a = t.getPropertyValue(e).trim();
|
|
44
|
+
if (a === "") return "";
|
|
45
|
+
let o = n.exec(a);
|
|
46
|
+
if (o !== null) {
|
|
47
|
+
let e = o[1], t = o[2]?.trim() ?? "";
|
|
48
|
+
if (e !== void 0) {
|
|
49
|
+
let t = r(e, i + 1);
|
|
50
|
+
if (t !== "") return t;
|
|
51
|
+
}
|
|
52
|
+
return t;
|
|
53
|
+
}
|
|
54
|
+
return a;
|
|
55
|
+
}, i = (e, t, n = 1) => {
|
|
56
|
+
let i = r(e);
|
|
57
|
+
return i === "" ? t : i.startsWith("#") || i.startsWith("rgb(") || i.startsWith("rgba(") || i.startsWith("hsl(") || i.startsWith("hsla(") || i.startsWith("oklch(") || i.startsWith("oklab(") || i.startsWith("color(") ? i : n === 1 ? `hsl(${i})` : `hsl(${i} / ${n.toString()})`;
|
|
58
|
+
}, a = i("--foreground", f.foreground);
|
|
59
|
+
return {
|
|
60
|
+
track: i("--muted-foreground", f.track, c),
|
|
61
|
+
trackInner: i("--muted-foreground", f.trackInner, l),
|
|
62
|
+
foreground: a,
|
|
63
|
+
muted: i("--muted-foreground", f.muted),
|
|
64
|
+
needle: a,
|
|
65
|
+
needleSoft: i("--foreground", f.needleSoft, u),
|
|
66
|
+
fill: p,
|
|
67
|
+
shadow: i("--foreground", f.shadow, d)
|
|
68
|
+
};
|
|
23
69
|
}
|
|
24
70
|
render() {
|
|
25
71
|
this.clear();
|
|
26
|
-
let e = this.
|
|
27
|
-
this.ctx.save(), this.ctx.beginPath(), this.ctx.arc(
|
|
72
|
+
let e = this.resolvePalette(), t = this.plotArea, n = t.x + t.width / 2, r = t.y + t.height * .58, a = Math.min(t.width / 2, t.height * .55) - this.arcWidth / 2, o = this.startAngle * i, s = this.endAngle * i, c = s - o;
|
|
73
|
+
this.ctx.save(), this.ctx.beginPath(), this.ctx.arc(n, r, a, o, s), this.ctx.strokeStyle = e.trackInner, this.ctx.lineWidth = this.arcWidth + 4, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.restore(), this.ctx.save(), this.ctx.beginPath(), this.ctx.arc(n, r, a, o, s), this.ctx.strokeStyle = e.track, this.ctx.lineWidth = this.arcWidth, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.restore(), this.thresholds.length > 0 ? this.drawThresholdArcs(n, r, a, o, c, e) : this.drawFillArc(n, r, a, o, c, e), this.drawNeedle(n, r, a, o, c, e), this.showValue && this.drawCenterText(n, r, e), this.drawMinMaxLabels(n, r, a, o, s, e);
|
|
28
74
|
}
|
|
29
75
|
destroy() {
|
|
30
76
|
super.destroy();
|
|
31
77
|
}
|
|
32
|
-
drawThresholdArcs(e, r, i, a, o) {
|
|
78
|
+
drawThresholdArcs(e, n, r, i, a, o) {
|
|
33
79
|
let s = [...this.thresholds].sort((e, t) => e.value - t.value), c = this.max - this.min;
|
|
34
80
|
if (c <= 0) return;
|
|
35
|
-
let l = (t(this.value, this.min, this.max) - this.min) / c, u = 0;
|
|
36
|
-
for (let
|
|
37
|
-
let
|
|
38
|
-
|
|
81
|
+
let l = (t(this.value, this.min, this.max) - this.min) / c, u = i + l * a, d = [], f = 0;
|
|
82
|
+
for (let e of s) {
|
|
83
|
+
let n = t((e.value - this.min) / c, 0, 1), r = i + f * a, o = i + n * a, s = Math.min(o, u);
|
|
84
|
+
s > r && d.push({
|
|
85
|
+
start: r,
|
|
86
|
+
end: s,
|
|
87
|
+
color: e.color
|
|
88
|
+
}), f = n;
|
|
89
|
+
}
|
|
90
|
+
if (f < l) {
|
|
91
|
+
let e = s[s.length - 1]?.color ?? o.fill, t = i + f * a;
|
|
92
|
+
d.push({
|
|
93
|
+
start: t,
|
|
94
|
+
end: u,
|
|
95
|
+
color: e
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (d.length === 0) return;
|
|
99
|
+
let p = d.length - 1;
|
|
100
|
+
if (p === 0) {
|
|
101
|
+
let t = d[0];
|
|
102
|
+
t !== void 0 && this.drawCapArc(e, n, r, t.start, t.end, t.color, o);
|
|
103
|
+
return;
|
|
39
104
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
105
|
+
for (let t of d) this.ctx.save(), this.ctx.shadowColor = o.shadow, this.ctx.shadowBlur = 6, this.ctx.shadowOffsetY = 1, this.ctx.beginPath(), this.ctx.arc(e, n, r, t.start, t.end), this.ctx.strokeStyle = t.color, this.ctx.lineWidth = this.arcWidth, this.ctx.lineCap = "butt", this.ctx.stroke(), this.ctx.restore();
|
|
106
|
+
let m = d[0], h = d[p];
|
|
107
|
+
if (m !== void 0) {
|
|
108
|
+
let t = Math.min(.001, m.end - m.start);
|
|
109
|
+
t > 0 && this.drawCapArc(e, n, r, m.start, m.start + t, m.color, o);
|
|
43
110
|
}
|
|
111
|
+
if (h !== void 0) {
|
|
112
|
+
let t = Math.min(.001, h.end - h.start);
|
|
113
|
+
t > 0 && this.drawCapArc(e, n, r, h.end - t, h.end, h.color, o);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
drawCapArc(e, t, n, r, i, a, o) {
|
|
117
|
+
this.ctx.save(), this.ctx.shadowColor = o.shadow, this.ctx.shadowBlur = 6, this.ctx.shadowOffsetY = 1, this.ctx.beginPath(), this.ctx.arc(e, t, n, r, i), this.ctx.strokeStyle = a, this.ctx.lineWidth = this.arcWidth, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.restore();
|
|
44
118
|
}
|
|
45
|
-
drawFillArc(e, r, i, a, o) {
|
|
119
|
+
drawFillArc(e, n, r, i, a, o) {
|
|
46
120
|
let s = this.max - this.min;
|
|
47
121
|
if (s <= 0) return;
|
|
48
|
-
let c =
|
|
49
|
-
this.ctx.save(), this.ctx.beginPath(), this.ctx.arc(e, r, i,
|
|
122
|
+
let c = i + (t(this.value, this.min, this.max) - this.min) / s * a;
|
|
123
|
+
this.ctx.save(), this.ctx.shadowColor = o.shadow, this.ctx.shadowBlur = 8, this.ctx.shadowOffsetY = 2, this.ctx.beginPath(), this.ctx.arc(e, n, r, i, c), this.ctx.strokeStyle = o.fill, this.ctx.lineWidth = this.arcWidth, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.restore();
|
|
50
124
|
}
|
|
51
|
-
drawNeedle(e, n, r, i, a) {
|
|
125
|
+
drawNeedle(e, n, r, i, a, o) {
|
|
52
126
|
let s = this.max - this.min;
|
|
53
127
|
if (s <= 0) return;
|
|
54
|
-
let c = i + (t(this.value, this.min, this.max) - this.min) / s * a, l = r - this.arcWidth
|
|
55
|
-
this.ctx.save(), this.ctx.beginPath(), this.ctx.moveTo(e, n), this.ctx.lineTo(
|
|
128
|
+
let c = i + (t(this.value, this.min, this.max) - this.min) / s * a, l = Math.max(4, r - this.arcWidth - 2), u = e + Math.cos(c) * l, d = n + Math.sin(c) * l;
|
|
129
|
+
this.ctx.save(), this.ctx.beginPath(), this.ctx.moveTo(e, n), this.ctx.lineTo(u, d), this.ctx.strokeStyle = o.needleSoft, this.ctx.lineWidth = 3, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.beginPath(), this.ctx.moveTo(e, n), this.ctx.lineTo(u, d), this.ctx.strokeStyle = o.needle, this.ctx.lineWidth = 1.5, this.ctx.lineCap = "round", this.ctx.stroke(), this.ctx.beginPath(), this.ctx.arc(e, n, 6, 0, Math.PI * 2), this.ctx.fillStyle = o.needle, this.ctx.fill(), this.ctx.beginPath(), this.ctx.arc(e, n, 2.5, 0, Math.PI * 2), this.ctx.fillStyle = o.fill, this.ctx.fill(), this.ctx.restore();
|
|
56
130
|
}
|
|
57
|
-
drawCenterText(e, t) {
|
|
131
|
+
drawCenterText(e, t, n) {
|
|
58
132
|
this.ctx.save(), this.ctx.textAlign = "center", this.ctx.textBaseline = "middle";
|
|
59
|
-
let
|
|
60
|
-
this.ctx.fillStyle =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
133
|
+
let i = this.label === void 0 ? t - 4 : t - 14, s = t + 16, c = this.unit === void 0 ? r(this.value) : `${r(this.value)}${this.unit}`;
|
|
134
|
+
this.ctx.fillStyle = n.foreground, this.ctx.font = a, this.ctx.fillText(c, e, i), this.label !== void 0 && (this.ctx.fillStyle = n.muted, this.ctx.font = o, this.ctx.fillText(this.label, e, s)), this.ctx.restore();
|
|
135
|
+
}
|
|
136
|
+
drawMinMaxLabels(e, t, n, i, a, o) {
|
|
137
|
+
this.ctx.save(), this.ctx.fillStyle = o.muted, this.ctx.font = s, this.ctx.textAlign = "center", this.ctx.textBaseline = "middle";
|
|
138
|
+
let c = n + this.arcWidth / 2 + 14, l = e + Math.cos(i) * c, u = t + Math.sin(i) * c;
|
|
139
|
+
this.ctx.fillText(r(this.min), l, u);
|
|
140
|
+
let d = e + Math.cos(a) * c, f = t + Math.sin(a) * c;
|
|
141
|
+
this.ctx.fillText(r(this.max), d, f), this.ctx.restore();
|
|
65
142
|
}
|
|
66
143
|
};
|
|
67
144
|
//#endregion
|
|
68
|
-
export {
|
|
145
|
+
export { h as GaugeChart };
|
|
69
146
|
|
|
70
147
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/charts/gauge/index.ts"],"sourcesContent":["/**\n * Gauge chart using Canvas 2D\n * @countermeasure/web-components/charts/gauge\n */\n\nimport { BaseChart } from '../base'\nimport { defaultColors, clamp, formatNumber } from '../utils'\nimport type { GaugeChartConfig, GaugeThreshold } from './types'\n\nconst DEG_TO_RAD = Math.PI / 180\nconst BG_ARC_COLOR = '#e2e8f0'\nconst NEEDLE_COLOR = '#1e293b'\nconst VALUE_FONT = 'bold 28px system-ui, sans-serif'\nconst UNIT_FONT = '14px system-ui, sans-serif'\nconst LABEL_FONT = '13px system-ui, sans-serif'\nconst LABEL_COLOR = '#64748b'\n\nexport class GaugeChart extends BaseChart {\n private value: number\n private min: number\n private max: number\n private label: string | undefined\n private unit: string | undefined\n private thresholds: GaugeThreshold[]\n private arcWidth: number\n private startAngle: number\n private endAngle: number\n private showValue: boolean\n\n constructor(config: GaugeChartConfig) {\n super(config)\n this.value = config.value\n this.min = config.min ?? 0\n this.max = config.max ?? 100\n this.label = config.label\n this.unit = config.unit\n this.thresholds = config.thresholds ?? []\n this.arcWidth = config.arcWidth ?? 20\n this.startAngle = config.startAngle ?? 135\n this.endAngle = config.endAngle ?? 405\n this.showValue = config.showValue ?? true\n\n this.render()\n }\n\n /** Update just the current value and re-render */\n updateValue(value: number): void {\n this.value = value\n this.render()\n }\n\n /** Update the full configuration and re-render */\n updateConfig(config: Partial<GaugeChartConfig>): void {\n if (config.value !== undefined) {\n this.value = config.value\n }\n if (config.min !== undefined) {\n this.min = config.min\n }\n if (config.max !== undefined) {\n this.max = config.max\n }\n if (config.label !== undefined) {\n this.label = config.label\n }\n if (config.unit !== undefined) {\n this.unit = config.unit\n }\n if (config.thresholds !== undefined) {\n this.thresholds = config.thresholds\n }\n if (config.arcWidth !== undefined) {\n this.arcWidth = config.arcWidth\n }\n if (config.startAngle !== undefined) {\n this.startAngle = config.startAngle\n }\n if (config.endAngle !== undefined) {\n this.endAngle = config.endAngle\n }\n if (config.showValue !== undefined) {\n this.showValue = config.showValue\n }\n this.render()\n }\n\n render(): void {\n this.clear()\n\n const plot = this.plotArea\n const cx = plot.x + plot.width / 2\n const cy = plot.y + plot.height * 0.6\n const radius = Math.min(plot.width / 2, plot.height * 0.55) - this.arcWidth / 2\n\n const startRad = this.startAngle * DEG_TO_RAD\n const endRad = this.endAngle * DEG_TO_RAD\n const totalSweep = endRad - startRad\n\n // Draw background arc\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, startRad, endRad)\n this.ctx.strokeStyle = BG_ARC_COLOR\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n\n // Draw threshold segments or single fill arc\n if (this.thresholds.length > 0) {\n this.drawThresholdArcs(cx, cy, radius, startRad, totalSweep)\n } else {\n this.drawFillArc(cx, cy, radius, startRad, totalSweep)\n }\n\n // Draw needle\n this.drawNeedle(cx, cy, radius, startRad, totalSweep)\n\n // Draw center value\n if (this.showValue) {\n this.drawCenterText(cx, cy)\n }\n }\n\n override destroy(): void {\n super.destroy()\n }\n\n private drawThresholdArcs(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number\n ): void {\n // Sort thresholds by value\n const sorted = [...this.thresholds].sort((a, b) => a.value - b.value)\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const valueRatio = (clampedValue - this.min) / range\n\n let prevRatio = 0\n\n for (const threshold of sorted) {\n const thresholdRatio = clamp((threshold.value - this.min) / range, 0, 1)\n const segStart = startRad + prevRatio * totalSweep\n const segEnd = startRad + thresholdRatio * totalSweep\n\n // Only draw segments up to the current value\n const fillEnd = startRad + valueRatio * totalSweep\n const drawEnd = Math.min(segEnd, fillEnd)\n\n if (drawEnd > segStart) {\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, segStart, drawEnd)\n this.ctx.strokeStyle = threshold.color\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'butt'\n this.ctx.stroke()\n this.ctx.restore()\n }\n\n prevRatio = thresholdRatio\n }\n\n // Fill remaining from last threshold to max (if value extends beyond last threshold)\n if (prevRatio < valueRatio) {\n const lastColor = sorted[sorted.length - 1]?.color ?? defaultColors()[0] ?? '#3b82f6'\n const segStart = startRad + prevRatio * totalSweep\n const fillEnd = startRad + valueRatio * totalSweep\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, segStart, fillEnd)\n this.ctx.strokeStyle = lastColor\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'butt'\n this.ctx.stroke()\n this.ctx.restore()\n }\n }\n\n private drawFillArc(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number\n ): void {\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const ratio = (clampedValue - this.min) / range\n const fillEnd = startRad + ratio * totalSweep\n const color = defaultColors()[0] ?? '#3b82f6'\n\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, startRad, fillEnd)\n this.ctx.strokeStyle = color\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n }\n\n private drawNeedle(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number\n ): void {\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const ratio = (clampedValue - this.min) / range\n const needleAngle = startRad + ratio * totalSweep\n const needleLength = radius - this.arcWidth / 2 - 4\n\n this.ctx.save()\n\n // Needle line\n this.ctx.beginPath()\n this.ctx.moveTo(cx, cy)\n this.ctx.lineTo(\n cx + Math.cos(needleAngle) * needleLength,\n cy + Math.sin(needleAngle) * needleLength\n )\n this.ctx.strokeStyle = NEEDLE_COLOR\n this.ctx.lineWidth = 2.5\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n\n // Center dot\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, 5, 0, Math.PI * 2)\n this.ctx.fillStyle = NEEDLE_COLOR\n this.ctx.fill()\n\n this.ctx.restore()\n }\n\n private drawCenterText(cx: number, cy: number): void {\n this.ctx.save()\n this.ctx.textAlign = 'center'\n this.ctx.textBaseline = 'middle'\n\n // Value text\n const valueText =\n this.unit !== undefined ? `${formatNumber(this.value)}${this.unit}` : formatNumber(this.value)\n\n this.ctx.fillStyle = NEEDLE_COLOR\n this.ctx.font = VALUE_FONT\n this.ctx.fillText(valueText, cx, cy + 30)\n\n // Optional label below value\n if (this.label !== undefined) {\n this.ctx.fillStyle = LABEL_COLOR\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(this.label, cx, cy + 52)\n }\n\n // Min and max labels\n this.ctx.fillStyle = LABEL_COLOR\n this.ctx.font = UNIT_FONT\n\n const plot = this.plotArea\n const gaugeRadius = Math.min(plot.width / 2, plot.height * 0.55) - this.arcWidth / 2\n\n const startRad = this.startAngle * DEG_TO_RAD\n const endRad = this.endAngle * DEG_TO_RAD\n\n const minX = cx + Math.cos(startRad) * (gaugeRadius + this.arcWidth)\n const minY = cy + Math.sin(startRad) * (gaugeRadius + this.arcWidth)\n this.ctx.textAlign = 'center'\n this.ctx.fillText(formatNumber(this.min), minX, minY + 14)\n\n const maxX = cx + Math.cos(endRad) * (gaugeRadius + this.arcWidth)\n const maxY = cy + Math.sin(endRad) * (gaugeRadius + this.arcWidth)\n this.ctx.fillText(formatNumber(this.max), maxX, maxY + 14)\n\n this.ctx.restore()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/charts/gauge/index.ts"],"sourcesContent":["/**\n * Gauge chart using Canvas 2D\n * @countermeasure/web-components/charts/gauge\n */\n\nimport { BaseChart } from '../base'\nimport { defaultColors, clamp, formatNumber } from '../utils'\nimport type { GaugeChartConfig, GaugeThreshold } from './types'\n\nconst DEG_TO_RAD = Math.PI / 180\n\nconst VALUE_FONT_DEFAULT = '700 32px ui-sans-serif, system-ui, -apple-system, sans-serif'\nconst LABEL_FONT = '500 13px ui-sans-serif, system-ui, -apple-system, sans-serif'\nconst MINMAX_FONT = '500 11px ui-sans-serif, system-ui, -apple-system, sans-serif'\n\n// Shared alpha constants — used both for the resolved-token hsl(...) path and\n// the literal FALLBACK colors below so the rendered translucency matches\n// regardless of whether theme tokens are present.\nconst TRACK_ALPHA = 0.18\nconst TRACK_INNER_ALPHA = 0.08\nconst NEEDLE_SOFT_ALPHA = 0.3\nconst SHADOW_ALPHA = 0.25\n\nconst FALLBACK = {\n track: `rgba(148, 163, 184, ${String(TRACK_ALPHA)})`,\n trackInner: `rgba(148, 163, 184, ${String(TRACK_INNER_ALPHA)})`,\n foreground: '#0f172a',\n muted: '#64748b',\n needle: '#0f172a',\n needleSoft: `rgba(15, 23, 42, ${String(NEEDLE_SOFT_ALPHA)})`,\n fill: '#3b82f6',\n shadow: `rgba(15, 23, 42, ${String(SHADOW_ALPHA)})`,\n}\nconst DEFAULT_FILL = defaultColors()[0] ?? FALLBACK.fill\n\n// Hard cap on full-turn additions so a pathological caller (e.g. start=NaN or\n// `Number.MAX_SAFE_INTEGER`) can never spin the normalization loop forever. In\n// practice angles are within a turn or two; 32 turns is far more than needed\n// while still being a tight finite bound.\nconst MAX_ANGLE_NORMALIZE_TURNS = 32\n\nexport class GaugeChart extends BaseChart {\n private value: number\n private min: number\n private max: number\n private label: string | undefined\n private unit: string | undefined\n private thresholds: GaugeThreshold[]\n private arcWidth: number\n private startAngle: number\n private endAngle: number\n private showValue: boolean\n\n constructor(config: GaugeChartConfig) {\n super(config)\n this.value = config.value\n this.min = config.min ?? 0\n this.max = config.max ?? 100\n this.label = config.label\n this.unit = config.unit\n this.thresholds = config.thresholds ?? []\n this.arcWidth = config.arcWidth ?? 20\n this.startAngle = config.startAngle ?? 135\n this.endAngle = config.endAngle ?? 405\n // Normalize: `endAngle` must produce a positive sweep. If a consumer passes\n // an `endAngle <= startAngle`, advance it by full turns so Canvas draws\n // the intended arc rather than wrapping the wrong way around — even when\n // the consumer-supplied values are more than one turn apart.\n this.normalizeAngles()\n this.showValue = config.showValue ?? true\n\n this.render()\n }\n\n /** Update just the current value and re-render */\n updateValue(value: number): void {\n this.value = value\n this.render()\n }\n\n /** Update the full configuration and re-render */\n updateConfig(config: Partial<GaugeChartConfig>): void {\n if (config.value !== undefined) {\n this.value = config.value\n }\n if (config.min !== undefined) {\n this.min = config.min\n }\n if (config.max !== undefined) {\n this.max = config.max\n }\n if (config.label !== undefined) {\n this.label = config.label\n }\n if (config.unit !== undefined) {\n this.unit = config.unit\n }\n if (config.thresholds !== undefined) {\n this.thresholds = config.thresholds\n }\n if (config.arcWidth !== undefined) {\n this.arcWidth = config.arcWidth\n }\n if (config.startAngle !== undefined) {\n this.startAngle = config.startAngle\n }\n if (config.endAngle !== undefined) {\n this.endAngle = config.endAngle\n }\n // Re-normalize after either angle changes so consumers updating one without\n // the other can't accidentally invert the sweep — and so that inputs more\n // than one full turn apart still resolve to a positive sweep.\n if (config.startAngle !== undefined || config.endAngle !== undefined) {\n this.normalizeAngles()\n }\n if (config.showValue !== undefined) {\n this.showValue = config.showValue\n }\n this.render()\n }\n\n /**\n * Advance `endAngle` by whole 360° turns until it exceeds `startAngle`, so\n * Canvas always draws a positive sweep regardless of how the consumer\n * supplied the angles. Capped at `MAX_ANGLE_NORMALIZE_TURNS` iterations to\n * guarantee termination on pathological inputs (NaN, ±Infinity, very large\n * negative deltas).\n */\n private normalizeAngles(): void {\n for (let i = 0; i < MAX_ANGLE_NORMALIZE_TURNS && this.endAngle <= this.startAngle; i++) {\n this.endAngle += 360\n }\n }\n\n /**\n * Resolve a tonal palette from the container's computed styles so the chart\n * follows the active theme (light / dark / monokai / glass). Falls back to\n * neutral light-mode values when the chart isn't attached to the document\n * or when the variable is unset.\n */\n private resolvePalette(): typeof FALLBACK {\n const target = this.container\n // `ownerDocument` is non-nullable on HTMLElement — detached elements still\n // carry their original document. `isConnected` is the right signal for\n // \"actually in a rendered tree where getComputedStyle is meaningful\".\n if (!(target instanceof HTMLElement) || !target.isConnected) {\n return FALLBACK\n }\n const cs = target.ownerDocument.defaultView?.getComputedStyle(target)\n if (cs === undefined) {\n return FALLBACK\n }\n\n // CSS tokens in this codebase frequently chain through `var(...)` aliases\n // (e.g. `--muted-foreground: var(--foreground-secondary)`), so we have to\n // follow the chain until we reach a literal value before composing\n // `hsl(...)`. Computed-style on a single property will return the literal\n // text — including the `var(...)` reference — when the chain isn't resolved\n // at the matched rule.\n const MAX_DEPTH = 5\n const VAR_RE = /^var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^)]+))?\\)\\s*$/\n const resolveVar = (name: string, depth = 0): string => {\n if (depth >= MAX_DEPTH) return ''\n const raw = cs.getPropertyValue(name).trim()\n if (raw === '') return ''\n const match = VAR_RE.exec(raw)\n if (match !== null) {\n const ref = match[1]\n const fallback = match[2]?.trim() ?? ''\n if (ref !== undefined) {\n const next = resolveVar(ref, depth + 1)\n if (next !== '') return next\n }\n return fallback\n }\n return raw\n }\n\n const hsl = (name: string, fallback: string, alpha = 1): string => {\n const raw = resolveVar(name)\n if (raw === '') return fallback\n // If the resolved token is already a full color (hex, rgb(), hsl(),\n // named color, etc.) pass it through unchanged so themes can override\n // tokens with literal colors without producing invalid `hsl(#abcdef)`.\n if (\n raw.startsWith('#') ||\n raw.startsWith('rgb(') ||\n raw.startsWith('rgba(') ||\n raw.startsWith('hsl(') ||\n raw.startsWith('hsla(') ||\n raw.startsWith('oklch(') ||\n raw.startsWith('oklab(') ||\n raw.startsWith('color(')\n ) {\n // We can't reliably re-apply alpha to an arbitrary color string from\n // Canvas without parsing it — for those themes we trade the alpha\n // (track translucency) for correctness. The fallback alpha values are\n // applied only when the token resolves to space-separated HSL parts.\n return raw\n }\n return alpha === 1 ? `hsl(${raw})` : `hsl(${raw} / ${alpha.toString()})`\n }\n\n const fg = hsl('--foreground', FALLBACK.foreground)\n return {\n track: hsl('--muted-foreground', FALLBACK.track, TRACK_ALPHA),\n trackInner: hsl('--muted-foreground', FALLBACK.trackInner, TRACK_INNER_ALPHA),\n foreground: fg,\n muted: hsl('--muted-foreground', FALLBACK.muted),\n needle: fg,\n needleSoft: hsl('--foreground', FALLBACK.needleSoft, NEEDLE_SOFT_ALPHA),\n fill: DEFAULT_FILL,\n shadow: hsl('--foreground', FALLBACK.shadow, SHADOW_ALPHA),\n }\n }\n\n render(): void {\n this.clear()\n\n const palette = this.resolvePalette()\n const plot = this.plotArea\n const cx = plot.x + plot.width / 2\n const cy = plot.y + plot.height * 0.58\n const radius = Math.min(plot.width / 2, plot.height * 0.55) - this.arcWidth / 2\n\n const startRad = this.startAngle * DEG_TO_RAD\n const endRad = this.endAngle * DEG_TO_RAD\n const totalSweep = endRad - startRad\n\n // Track — a thin inner ring then the main track on top for a subtle inset\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, startRad, endRad)\n this.ctx.strokeStyle = palette.trackInner\n this.ctx.lineWidth = this.arcWidth + 4\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n\n this.ctx.save()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, startRad, endRad)\n this.ctx.strokeStyle = palette.track\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n\n // Active fill — drawn over the track. Thresholds layer below, fill arc on top.\n if (this.thresholds.length > 0) {\n this.drawThresholdArcs(cx, cy, radius, startRad, totalSweep, palette)\n } else {\n this.drawFillArc(cx, cy, radius, startRad, totalSweep, palette)\n }\n\n // Needle floats above the dial — keep it subtle so it doesn't obscure the value\n this.drawNeedle(cx, cy, radius, startRad, totalSweep, palette)\n\n // Centered value + label sit in the dial's open core\n if (this.showValue) {\n this.drawCenterText(cx, cy, palette)\n }\n\n this.drawMinMaxLabels(cx, cy, radius, startRad, endRad, palette)\n }\n\n override destroy(): void {\n super.destroy()\n }\n\n private drawThresholdArcs(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number,\n palette: typeof FALLBACK\n ): void {\n const sorted = [...this.thresholds].sort((a, b) => a.value - b.value)\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const valueRatio = (clampedValue - this.min) / range\n const fillEnd = startRad + valueRatio * totalSweep\n\n // First pass: build the list of visible segments. We need to know how many\n // segments will actually be drawn before we render so that only the first\n // gets a rounded start-cap and only the last (the active head) gets a\n // rounded end-cap; everything in between is `butt` to prevent overlapping\n // semicircular caps from bleeding into adjacent threshold colors.\n interface Segment {\n start: number\n end: number\n color: string\n }\n const segments: Segment[] = []\n let prevRatio = 0\n\n for (const threshold of sorted) {\n const thresholdRatio = clamp((threshold.value - this.min) / range, 0, 1)\n const segStart = startRad + prevRatio * totalSweep\n const segEnd = startRad + thresholdRatio * totalSweep\n const drawEnd = Math.min(segEnd, fillEnd)\n\n if (drawEnd > segStart) {\n segments.push({ start: segStart, end: drawEnd, color: threshold.color })\n }\n prevRatio = thresholdRatio\n }\n\n // Overflow tail: value extends past the highest threshold — extend the\n // last threshold's color out to the value ratio.\n if (prevRatio < valueRatio) {\n const lastColor = sorted[sorted.length - 1]?.color ?? palette.fill\n const segStart = startRad + prevRatio * totalSweep\n segments.push({ start: segStart, end: fillEnd, color: lastColor })\n }\n\n if (segments.length === 0) return\n\n const lastIdx = segments.length - 1\n // Single visible segment: keep the simple rounded sweep — no junctions to\n // worry about so both caps get the soft rounded edge.\n if (lastIdx === 0) {\n const only = segments[0]\n if (only !== undefined) {\n this.drawCapArc(cx, cy, radius, only.start, only.end, only.color, palette)\n }\n return\n }\n\n // Multi-segment: render every segment with `butt` caps so adjacent\n // threshold colors don't tint each other through overlapping rounded\n // semicircles at the junctions, then add tiny `round`-capped sub-arcs at\n // the outer head and tail so the active sweep's outermost edges stay soft.\n for (const seg of segments) {\n this.ctx.save()\n this.ctx.shadowColor = palette.shadow\n this.ctx.shadowBlur = 6\n this.ctx.shadowOffsetY = 1\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, seg.start, seg.end)\n this.ctx.strokeStyle = seg.color\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'butt'\n this.ctx.stroke()\n this.ctx.restore()\n }\n\n const head = segments[0]\n const tail = segments[lastIdx]\n if (head !== undefined) {\n const capLen = Math.min(0.001, head.end - head.start)\n if (capLen > 0) {\n this.drawCapArc(cx, cy, radius, head.start, head.start + capLen, head.color, palette)\n }\n }\n if (tail !== undefined) {\n const capLen = Math.min(0.001, tail.end - tail.start)\n if (capLen > 0) {\n this.drawCapArc(cx, cy, radius, tail.end - capLen, tail.end, tail.color, palette)\n }\n }\n }\n\n private drawCapArc(\n cx: number,\n cy: number,\n radius: number,\n start: number,\n end: number,\n color: string,\n palette: typeof FALLBACK\n ): void {\n this.ctx.save()\n this.ctx.shadowColor = palette.shadow\n this.ctx.shadowBlur = 6\n this.ctx.shadowOffsetY = 1\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, start, end)\n this.ctx.strokeStyle = color\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n }\n\n private drawFillArc(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number,\n palette: typeof FALLBACK\n ): void {\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const ratio = (clampedValue - this.min) / range\n const fillEnd = startRad + ratio * totalSweep\n\n this.ctx.save()\n this.ctx.shadowColor = palette.shadow\n this.ctx.shadowBlur = 8\n this.ctx.shadowOffsetY = 2\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, radius, startRad, fillEnd)\n this.ctx.strokeStyle = palette.fill\n this.ctx.lineWidth = this.arcWidth\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n this.ctx.restore()\n }\n\n private drawNeedle(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n totalSweep: number,\n palette: typeof FALLBACK\n ): void {\n const range = this.max - this.min\n if (range <= 0) return\n\n const clampedValue = clamp(this.value, this.min, this.max)\n const ratio = (clampedValue - this.min) / range\n const needleAngle = startRad + ratio * totalSweep\n const needleLength = Math.max(4, radius - this.arcWidth - 2)\n\n const tipX = cx + Math.cos(needleAngle) * needleLength\n const tipY = cy + Math.sin(needleAngle) * needleLength\n\n this.ctx.save()\n\n // Faint trail so the needle reads at a glance\n this.ctx.beginPath()\n this.ctx.moveTo(cx, cy)\n this.ctx.lineTo(tipX, tipY)\n this.ctx.strokeStyle = palette.needleSoft\n this.ctx.lineWidth = 3\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n\n // Crisp tip\n this.ctx.beginPath()\n this.ctx.moveTo(cx, cy)\n this.ctx.lineTo(tipX, tipY)\n this.ctx.strokeStyle = palette.needle\n this.ctx.lineWidth = 1.5\n this.ctx.lineCap = 'round'\n this.ctx.stroke()\n\n // Pivot — concentric dots so the centerpiece reads in any theme\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, 6, 0, Math.PI * 2)\n this.ctx.fillStyle = palette.needle\n this.ctx.fill()\n this.ctx.beginPath()\n this.ctx.arc(cx, cy, 2.5, 0, Math.PI * 2)\n this.ctx.fillStyle = palette.fill\n this.ctx.fill()\n\n this.ctx.restore()\n }\n\n private drawCenterText(cx: number, cy: number, palette: typeof FALLBACK): void {\n this.ctx.save()\n this.ctx.textAlign = 'center'\n this.ctx.textBaseline = 'middle'\n\n const hasLabel = this.label !== undefined\n const valueY = hasLabel ? cy - 14 : cy - 4\n const labelY = cy + 16\n\n const valueText =\n this.unit !== undefined ? `${formatNumber(this.value)}${this.unit}` : formatNumber(this.value)\n\n this.ctx.fillStyle = palette.foreground\n this.ctx.font = VALUE_FONT_DEFAULT\n this.ctx.fillText(valueText, cx, valueY)\n\n if (this.label !== undefined) {\n this.ctx.fillStyle = palette.muted\n this.ctx.font = LABEL_FONT\n this.ctx.fillText(this.label, cx, labelY)\n }\n\n this.ctx.restore()\n }\n\n private drawMinMaxLabels(\n cx: number,\n cy: number,\n radius: number,\n startRad: number,\n endRad: number,\n palette: typeof FALLBACK\n ): void {\n this.ctx.save()\n this.ctx.fillStyle = palette.muted\n this.ctx.font = MINMAX_FONT\n this.ctx.textAlign = 'center'\n this.ctx.textBaseline = 'middle'\n\n const labelDistance = radius + this.arcWidth / 2 + 14\n const minX = cx + Math.cos(startRad) * labelDistance\n const minY = cy + Math.sin(startRad) * labelDistance\n this.ctx.fillText(formatNumber(this.min), minX, minY)\n\n const maxX = cx + Math.cos(endRad) * labelDistance\n const maxY = cy + Math.sin(endRad) * labelDistance\n this.ctx.fillText(formatNumber(this.max), maxX, maxY)\n\n this.ctx.restore()\n }\n}\n"],"mappings":"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"}
|