@forwardimpact/pathway 0.25.15 → 0.25.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/fit-pathway.js +62 -54
  2. package/package.json +1 -3
  3. package/src/commands/agent-io.js +120 -0
  4. package/src/commands/agent.js +266 -349
  5. package/src/commands/init.js +2 -2
  6. package/src/commands/job.js +237 -183
  7. package/src/components/comparison-radar.js +118 -103
  8. package/src/components/progression-table.js +244 -208
  9. package/src/formatters/index.js +0 -19
  10. package/src/formatters/interview/markdown.js +100 -88
  11. package/src/formatters/job/description.js +76 -75
  12. package/src/formatters/job/dom.js +113 -97
  13. package/src/formatters/level/dom.js +87 -102
  14. package/src/formatters/questions/markdown.js +37 -33
  15. package/src/formatters/questions/shared.js +142 -75
  16. package/src/formatters/skill/dom.js +102 -93
  17. package/src/lib/comparison-radar-chart.js +256 -0
  18. package/src/lib/radar-utils.js +199 -0
  19. package/src/lib/radar.js +25 -662
  20. package/src/pages/agent-builder-download.js +170 -0
  21. package/src/pages/agent-builder-preview.js +344 -0
  22. package/src/pages/agent-builder.js +6 -550
  23. package/src/pages/progress-comparison.js +110 -0
  24. package/src/pages/progress.js +11 -111
  25. package/src/pages/self-assessment-steps.js +494 -0
  26. package/src/pages/self-assessment.js +54 -504
  27. package/src/formatters/behaviour/microdata.js +0 -106
  28. package/src/formatters/discipline/microdata.js +0 -117
  29. package/src/formatters/driver/microdata.js +0 -91
  30. package/src/formatters/level/microdata.js +0 -141
  31. package/src/formatters/microdata-shared.js +0 -184
  32. package/src/formatters/skill/microdata.js +0 -151
  33. package/src/formatters/stage/microdata.js +0 -116
  34. package/src/formatters/track/microdata.js +0 -111
@@ -37,6 +37,106 @@ import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
37
37
  * @param {string} [context.agentSkillContent] - Pre-generated SKILL.md content for agent file viewer
38
38
  * @returns {HTMLElement}
39
39
  */
40
+ /**
41
+ * Create the required tools section
42
+ * @param {Object} view
43
+ * @param {boolean} showBackLink
44
+ * @returns {HTMLElement}
45
+ */
46
+ function createToolsSection(view, showBackLink) {
47
+ return div(
48
+ { className: "detail-section" },
49
+ heading2({ className: "section-title" }, "Required Tools"),
50
+ table(
51
+ { className: "tools-table" },
52
+ thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
53
+ tbody(
54
+ {},
55
+ ...view.toolReferences.map((tool) =>
56
+ tr(
57
+ {},
58
+ td(
59
+ { className: "tool-name-cell" },
60
+ tool.simpleIcon
61
+ ? createToolIcon(tool.simpleIcon, tool.name)
62
+ : null,
63
+ tool.url
64
+ ? a(
65
+ {
66
+ href: tool.url,
67
+ target: "_blank",
68
+ rel: "noopener noreferrer",
69
+ },
70
+ tool.name,
71
+ span({ className: "external-icon" }, " ↗"),
72
+ )
73
+ : tool.name,
74
+ ),
75
+ td({}, tool.useWhen),
76
+ ),
77
+ ),
78
+ ),
79
+ ),
80
+ showBackLink
81
+ ? p(
82
+ { className: "see-all-link" },
83
+ a({ href: "#/tool" }, "See all tools →"),
84
+ )
85
+ : null,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Create the related disciplines and drivers section
91
+ * @param {Object} view
92
+ * @param {boolean} showBackLink
93
+ * @returns {HTMLElement|null}
94
+ */
95
+ function createRelationsSection(view, showBackLink) {
96
+ if (
97
+ view.relatedDisciplines.length === 0 &&
98
+ view.relatedDrivers.length === 0
99
+ ) {
100
+ return null;
101
+ }
102
+ return div(
103
+ { className: "detail-section" },
104
+ div(
105
+ { className: "content-columns" },
106
+ view.relatedDisciplines.length > 0
107
+ ? div(
108
+ { className: "column" },
109
+ heading2({ className: "section-title" }, "Used in Disciplines"),
110
+ ...view.relatedDisciplines.map((d) =>
111
+ div(
112
+ { className: "list-item" },
113
+ showBackLink
114
+ ? a({ href: `#/discipline/${d.id}` }, d.name)
115
+ : span({}, d.name),
116
+ " ",
117
+ span({ className: `badge badge-${d.skillType}` }, d.skillType),
118
+ ),
119
+ ),
120
+ )
121
+ : null,
122
+ view.relatedDrivers.length > 0
123
+ ? div(
124
+ { className: "column" },
125
+ heading2({ className: "section-title" }, "Linked to Drivers"),
126
+ ...view.relatedDrivers.map((d) =>
127
+ div(
128
+ { className: "list-item" },
129
+ showBackLink
130
+ ? a({ href: `#/driver/${d.id}` }, d.name)
131
+ : span({}, d.name),
132
+ ),
133
+ ),
134
+ )
135
+ : null,
136
+ ),
137
+ );
138
+ }
139
+
40
140
  export function skillToDOM(
41
141
  skill,
42
142
  {
@@ -57,9 +157,7 @@ export function skillToDOM(
57
157
  });
58
158
  return div(
59
159
  { className: "detail-page skill-detail" },
60
- // JSON-LD structured data
61
160
  createJsonLdScript(skillToJsonLd(skill, { capabilities })),
62
- // Header
63
161
  div(
64
162
  { className: "page-header" },
65
163
  showBackLink ? createBackLink("/skill", "← Back to Skills") : null,
@@ -88,7 +186,6 @@ export function skillToDOM(
88
186
  p({ className: "page-description" }, view.description),
89
187
  ),
90
188
 
91
- // Level descriptions
92
189
  div(
93
190
  { className: "detail-section" },
94
191
  heading2({ className: "section-title" }, "Level Descriptions"),
@@ -109,55 +206,8 @@ export function skillToDOM(
109
206
  ),
110
207
  ),
111
208
 
112
- // Used in Disciplines and Linked to Drivers in two columns
113
- view.relatedDisciplines.length > 0 || view.relatedDrivers.length > 0
114
- ? div(
115
- { className: "detail-section" },
116
- div(
117
- { className: "content-columns" },
118
- // Used in Disciplines column
119
- view.relatedDisciplines.length > 0
120
- ? div(
121
- { className: "column" },
122
- heading2(
123
- { className: "section-title" },
124
- "Used in Disciplines",
125
- ),
126
- ...view.relatedDisciplines.map((d) =>
127
- div(
128
- { className: "list-item" },
129
- showBackLink
130
- ? a({ href: `#/discipline/${d.id}` }, d.name)
131
- : span({}, d.name),
132
- " ",
133
- span(
134
- { className: `badge badge-${d.skillType}` },
135
- d.skillType,
136
- ),
137
- ),
138
- ),
139
- )
140
- : null,
141
- // Linked to Drivers column
142
- view.relatedDrivers.length > 0
143
- ? div(
144
- { className: "column" },
145
- heading2({ className: "section-title" }, "Linked to Drivers"),
146
- ...view.relatedDrivers.map((d) =>
147
- div(
148
- { className: "list-item" },
149
- showBackLink
150
- ? a({ href: `#/driver/${d.id}` }, d.name)
151
- : span({}, d.name),
152
- ),
153
- ),
154
- )
155
- : null,
156
- ),
157
- )
158
- : null,
209
+ createRelationsSection(view, showBackLink),
159
210
 
160
- // Related tracks
161
211
  view.relatedTracks.length > 0
162
212
  ? div(
163
213
  { className: "detail-section" },
@@ -180,51 +230,10 @@ export function skillToDOM(
180
230
  )
181
231
  : null,
182
232
 
183
- // Required Tools
184
233
  showToolsAndPatterns && view.toolReferences.length > 0
185
- ? div(
186
- { className: "detail-section" },
187
- heading2({ className: "section-title" }, "Required Tools"),
188
- table(
189
- { className: "tools-table" },
190
- thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
191
- tbody(
192
- {},
193
- ...view.toolReferences.map((tool) =>
194
- tr(
195
- {},
196
- td(
197
- { className: "tool-name-cell" },
198
- tool.simpleIcon
199
- ? createToolIcon(tool.simpleIcon, tool.name)
200
- : null,
201
- tool.url
202
- ? a(
203
- {
204
- href: tool.url,
205
- target: "_blank",
206
- rel: "noopener noreferrer",
207
- },
208
- tool.name,
209
- span({ className: "external-icon" }, " ↗"),
210
- )
211
- : tool.name,
212
- ),
213
- td({}, tool.useWhen),
214
- ),
215
- ),
216
- ),
217
- ),
218
- showBackLink
219
- ? p(
220
- { className: "see-all-link" },
221
- a({ href: "#/tool" }, "See all tools →"),
222
- )
223
- : null,
224
- )
234
+ ? createToolsSection(view, showBackLink)
225
235
  : null,
226
236
 
227
- // Agent Skill Files
228
237
  showToolsAndPatterns &&
229
238
  (agentSkillContent || view.implementationReference || view.installScript)
230
239
  ? div(
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Comparison Radar Chart - displays two overlaid radar charts
3
+ */
4
+
5
+ import {
6
+ escapeHtml,
7
+ wrapLabel,
8
+ getTextAnchor,
9
+ createRadarSvg,
10
+ drawLevelRings,
11
+ drawAxisLines,
12
+ createRadarTooltip,
13
+ } from "./radar-utils.js";
14
+
15
+ export class ComparisonRadarChart {
16
+ /**
17
+ * @param {Object} config
18
+ * @param {HTMLElement} config.container
19
+ * @param {import('./radar.js').RadarDataPoint[]} config.currentData
20
+ * @param {import('./radar.js').RadarDataPoint[]} config.targetData
21
+ * @param {Object} [config.options]
22
+ */
23
+ constructor({ container, currentData, targetData, options = {} }) {
24
+ this.container = container;
25
+ this.currentData = currentData;
26
+ this.targetData = targetData;
27
+ this.options = {
28
+ levels: options.levels || 5,
29
+ currentColor: options.currentColor || "#3b82f6",
30
+ targetColor: options.targetColor || "#10b981",
31
+ showLabels: options.showLabels !== false,
32
+ showTooltips: options.showTooltips !== false,
33
+ size: options.size || 400,
34
+ labelOffset: options.labelOffset || 50,
35
+ };
36
+
37
+ this.center = this.options.size / 2;
38
+ this.radius = this.options.size / 2 - this.options.labelOffset - 20;
39
+ this.angleSlice = (Math.PI * 2) / this.currentData.length;
40
+
41
+ this.svg = null;
42
+ this.tooltip = null;
43
+ }
44
+
45
+ render() {
46
+ this.container.innerHTML = "";
47
+
48
+ this.svg = createRadarSvg(this.options.size, ["comparison-radar-chart"]);
49
+
50
+ const sharedParams = {
51
+ radius: this.radius,
52
+ center: this.center,
53
+ angleSlice: this.angleSlice,
54
+ dataLength: this.currentData.length,
55
+ };
56
+
57
+ drawLevelRings(this.svg, { ...sharedParams, levels: this.options.levels });
58
+ drawAxisLines(this.svg, sharedParams);
59
+
60
+ this.drawDataPolygon(this.targetData, this.options.targetColor, 0.2);
61
+ this.drawDataPolygon(this.currentData, this.options.currentColor, 0.3);
62
+ this.drawDataPoints(this.targetData, this.options.targetColor, "target");
63
+ this.drawDataPoints(this.currentData, this.options.currentColor, "current");
64
+
65
+ if (this.options.showLabels) this.drawLabels();
66
+ if (this.options.showTooltips) {
67
+ this.tooltip = createRadarTooltip(this.container, 250);
68
+ }
69
+
70
+ this.container.appendChild(this.svg);
71
+ }
72
+
73
+ drawDataPolygon(data, color, opacity) {
74
+ const points = data.map((d, i) => {
75
+ const angle = this.angleSlice * i - Math.PI / 2;
76
+ const value = d.value / d.maxValue;
77
+ const r = this.radius * value;
78
+ return {
79
+ x: this.center + r * Math.cos(angle),
80
+ y: this.center + r * Math.sin(angle),
81
+ };
82
+ });
83
+
84
+ const polygon = document.createElementNS(
85
+ "http://www.w3.org/2000/svg",
86
+ "polygon",
87
+ );
88
+ polygon.setAttribute(
89
+ "points",
90
+ points.map((p) => `${p.x},${p.y}`).join(" "),
91
+ );
92
+ polygon.classList.add("radar-data");
93
+ polygon.style.fill = color;
94
+ polygon.style.fillOpacity = String(opacity);
95
+ polygon.style.stroke = color;
96
+ polygon.style.strokeWidth = "2";
97
+
98
+ this.svg.appendChild(polygon);
99
+ }
100
+
101
+ drawDataPoints(data, color, type) {
102
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
103
+ group.classList.add("radar-points", `radar-points-${type}`);
104
+
105
+ data.forEach((d, i) => {
106
+ const angle = this.angleSlice * i - Math.PI / 2;
107
+ const value = d.value / d.maxValue;
108
+ const r = this.radius * value;
109
+ const x = this.center + r * Math.cos(angle);
110
+ const y = this.center + r * Math.sin(angle);
111
+
112
+ const circle = document.createElementNS(
113
+ "http://www.w3.org/2000/svg",
114
+ "circle",
115
+ );
116
+ circle.setAttribute("cx", x);
117
+ circle.setAttribute("cy", y);
118
+ circle.setAttribute("r", 4);
119
+ circle.classList.add("radar-point");
120
+ circle.style.fill = color;
121
+ circle.style.stroke = "#fff";
122
+ circle.style.strokeWidth = "2";
123
+ circle.style.cursor = "pointer";
124
+
125
+ if (this.options.showTooltips) {
126
+ circle.addEventListener("mouseenter", (e) =>
127
+ this.showTooltip(e, d, type),
128
+ );
129
+ circle.addEventListener("mouseleave", () => this.hideTooltip());
130
+ }
131
+
132
+ group.appendChild(circle);
133
+ });
134
+
135
+ this.svg.appendChild(group);
136
+ }
137
+
138
+ drawLabels() {
139
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
140
+ group.classList.add("radar-labels");
141
+
142
+ this.currentData.forEach((d, i) => {
143
+ const targetD = this.targetData[i];
144
+ const angle = this.angleSlice * i - Math.PI / 2;
145
+ const labelRadius = this.radius + this.options.labelOffset;
146
+ const x = this.center + labelRadius * Math.cos(angle);
147
+ const y = this.center + labelRadius * Math.sin(angle);
148
+
149
+ const diff = targetD.value - d.value;
150
+ const hasDiff = diff !== 0;
151
+
152
+ const text = document.createElementNS(
153
+ "http://www.w3.org/2000/svg",
154
+ "text",
155
+ );
156
+ text.setAttribute("x", x);
157
+ text.setAttribute("y", y);
158
+ text.classList.add("radar-label");
159
+ if (hasDiff) text.classList.add("has-change");
160
+ text.style.fontSize = "11px";
161
+ text.style.fill = hasDiff
162
+ ? diff > 0
163
+ ? "#059669"
164
+ : "#dc2626"
165
+ : "#475569";
166
+ text.style.fontWeight = hasDiff ? "600" : "400";
167
+ text.style.textAnchor = getTextAnchor(angle);
168
+ text.style.dominantBaseline = "middle";
169
+
170
+ let labelText = d.label;
171
+ if (hasDiff) {
172
+ labelText += ` (${diff > 0 ? "+" : ""}${diff})`;
173
+ }
174
+
175
+ const lines = wrapLabel(labelText, 15);
176
+ const lineHeight = 13;
177
+ const offsetY = -((lines.length - 1) * lineHeight) / 2;
178
+
179
+ lines.forEach((line, lineIndex) => {
180
+ const tspan = document.createElementNS(
181
+ "http://www.w3.org/2000/svg",
182
+ "tspan",
183
+ );
184
+ tspan.setAttribute("x", x);
185
+ tspan.setAttribute("dy", lineIndex === 0 ? offsetY : lineHeight);
186
+ tspan.textContent = line;
187
+ text.appendChild(tspan);
188
+ });
189
+
190
+ if (this.options.showTooltips) {
191
+ text.style.cursor = "pointer";
192
+ text.addEventListener("mouseenter", (e) =>
193
+ this.showComparisonTooltip(e, d, targetD),
194
+ );
195
+ text.addEventListener("mouseleave", () => this.hideTooltip());
196
+ }
197
+
198
+ group.appendChild(text);
199
+ });
200
+
201
+ this.svg.appendChild(group);
202
+ }
203
+
204
+ showTooltip(event, data, type) {
205
+ if (!this.tooltip) return;
206
+
207
+ const rect = this.container.getBoundingClientRect();
208
+ const x = event.clientX - rect.left;
209
+ const y = event.clientY - rect.top;
210
+
211
+ const typeLabel = type === "current" ? "Current" : "Target";
212
+
213
+ this.tooltip.innerHTML = `
214
+ <strong>${escapeHtml(data.label)}</strong><br>
215
+ ${typeLabel}: ${data.value}/${data.maxValue}
216
+ ${data.description ? `<br><small>${escapeHtml(data.description)}</small>` : ""}
217
+ `;
218
+
219
+ this.tooltip.style.left = `${x + 10}px`;
220
+ this.tooltip.style.top = `${y - 10}px`;
221
+ this.tooltip.style.opacity = "1";
222
+ }
223
+
224
+ showComparisonTooltip(event, currentData, targetData) {
225
+ if (!this.tooltip) return;
226
+
227
+ const rect = this.container.getBoundingClientRect();
228
+ const x = event.clientX - rect.left;
229
+ const y = event.clientY - rect.top;
230
+
231
+ const diff = targetData.value - currentData.value;
232
+ const diffText =
233
+ diff > 0
234
+ ? `<span style="color: #10b981">↑ ${diff} level${diff > 1 ? "s" : ""}</span>`
235
+ : diff < 0
236
+ ? `<span style="color: #ef4444">↓ ${Math.abs(diff)} level${Math.abs(diff) > 1 ? "s" : ""}</span>`
237
+ : "<span style='color: #94a3b8'>No change</span>";
238
+
239
+ this.tooltip.innerHTML = `
240
+ <strong>${escapeHtml(currentData.label)}</strong><br>
241
+ Current: ${currentData.value}/${currentData.maxValue}<br>
242
+ Target: ${targetData.value}/${targetData.maxValue}<br>
243
+ ${diffText}
244
+ `;
245
+
246
+ this.tooltip.style.left = `${x + 10}px`;
247
+ this.tooltip.style.top = `${y - 10}px`;
248
+ this.tooltip.style.opacity = "1";
249
+ }
250
+
251
+ hideTooltip() {
252
+ if (this.tooltip) {
253
+ this.tooltip.style.opacity = "0";
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Shared utilities for radar chart visualizations
3
+ */
4
+
5
+ /**
6
+ * Escape HTML special characters to prevent XSS
7
+ * @param {string} text
8
+ * @returns {string}
9
+ */
10
+ export function escapeHtml(text) {
11
+ return text
12
+ .replace(/&/g, "&amp;")
13
+ .replace(/</g, "&lt;")
14
+ .replace(/>/g, "&gt;")
15
+ .replace(/"/g, "&quot;")
16
+ .replace(/'/g, "&#039;");
17
+ }
18
+
19
+ /**
20
+ * Wrap label text into multiple lines
21
+ * @param {string} text
22
+ * @param {number} maxCharsPerLine
23
+ * @returns {string[]}
24
+ */
25
+ export function wrapLabel(text, maxCharsPerLine) {
26
+ if (text.length <= maxCharsPerLine) return [text];
27
+
28
+ const words = text.split(/\s+/);
29
+ const lines = [];
30
+ let currentLine = "";
31
+
32
+ for (const word of words) {
33
+ if (currentLine.length === 0) {
34
+ currentLine = word;
35
+ } else if (currentLine.length + 1 + word.length <= maxCharsPerLine) {
36
+ currentLine += " " + word;
37
+ } else {
38
+ lines.push(currentLine);
39
+ currentLine = word;
40
+ }
41
+ }
42
+
43
+ if (currentLine.length > 0) {
44
+ lines.push(currentLine);
45
+ }
46
+
47
+ return lines.length > 0 ? lines : [text];
48
+ }
49
+
50
+ /**
51
+ * Get text anchor based on angle
52
+ * @param {number} angle
53
+ * @returns {string}
54
+ */
55
+ export function getTextAnchor(angle) {
56
+ const degrees = (angle * 180) / Math.PI + 90;
57
+ if (degrees > 45 && degrees < 135) return "start";
58
+ if (degrees > 225 && degrees < 315) return "end";
59
+ return "middle";
60
+ }
61
+
62
+ /**
63
+ * Create an SVG element with standard radar chart dimensions
64
+ * @param {number} size
65
+ * @param {string[]} [extraClasses]
66
+ * @returns {SVGElement}
67
+ */
68
+ export function createRadarSvg(size, extraClasses = []) {
69
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
70
+ const padding = 40;
71
+ const totalSize = size + padding * 2;
72
+ svg.setAttribute("width", totalSize);
73
+ svg.setAttribute("height", totalSize);
74
+ svg.setAttribute(
75
+ "viewBox",
76
+ `${-padding} ${-padding} ${totalSize} ${totalSize}`,
77
+ );
78
+ svg.classList.add("radar-chart", ...extraClasses);
79
+ return svg;
80
+ }
81
+
82
+ /**
83
+ * Draw concentric level rings on a radar chart
84
+ * @param {SVGElement} svg
85
+ * @param {Object} params
86
+ * @param {number} params.levels
87
+ * @param {number} params.radius
88
+ * @param {number} params.center
89
+ * @param {number} params.angleSlice
90
+ * @param {number} params.dataLength
91
+ */
92
+ export function drawLevelRings(
93
+ svg,
94
+ { levels, radius, center, angleSlice, dataLength },
95
+ ) {
96
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
97
+ group.classList.add("radar-levels");
98
+
99
+ for (let level = 1; level <= levels; level++) {
100
+ const levelRadius = (radius * level) / levels;
101
+ const points = [];
102
+ for (let i = 0; i < dataLength; i++) {
103
+ const angle = angleSlice * i - Math.PI / 2;
104
+ points.push({
105
+ x: center + levelRadius * Math.cos(angle),
106
+ y: center + levelRadius * Math.sin(angle),
107
+ });
108
+ }
109
+
110
+ const polygon = document.createElementNS(
111
+ "http://www.w3.org/2000/svg",
112
+ "polygon",
113
+ );
114
+ polygon.setAttribute(
115
+ "points",
116
+ points.map((p) => `${p.x},${p.y}`).join(" "),
117
+ );
118
+ polygon.classList.add("radar-level");
119
+ polygon.style.fill = "none";
120
+ polygon.style.stroke = "#e2e8f0";
121
+ polygon.style.strokeWidth = "1";
122
+ group.appendChild(polygon);
123
+
124
+ const labelX = center + 5;
125
+ const labelY = center - levelRadius + 4;
126
+ const label = document.createElementNS(
127
+ "http://www.w3.org/2000/svg",
128
+ "text",
129
+ );
130
+ label.setAttribute("x", labelX);
131
+ label.setAttribute("y", labelY);
132
+ label.textContent = level;
133
+ label.classList.add("radar-level-label");
134
+ label.style.fontSize = "10px";
135
+ label.style.fill = "#94a3b8";
136
+ group.appendChild(label);
137
+ }
138
+
139
+ svg.appendChild(group);
140
+ }
141
+
142
+ /**
143
+ * Draw axis lines on a radar chart
144
+ * @param {SVGElement} svg
145
+ * @param {Object} params
146
+ * @param {number} params.radius
147
+ * @param {number} params.center
148
+ * @param {number} params.angleSlice
149
+ * @param {number} params.dataLength
150
+ */
151
+ export function drawAxisLines(svg, { radius, center, angleSlice, dataLength }) {
152
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
153
+ group.classList.add("radar-axes");
154
+
155
+ for (let i = 0; i < dataLength; i++) {
156
+ const angle = angleSlice * i - Math.PI / 2;
157
+ const x = center + radius * Math.cos(angle);
158
+ const y = center + radius * Math.sin(angle);
159
+
160
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
161
+ line.setAttribute("x1", center);
162
+ line.setAttribute("y1", center);
163
+ line.setAttribute("x2", x);
164
+ line.setAttribute("y2", y);
165
+ line.classList.add("radar-axis");
166
+ line.style.stroke = "#cbd5e1";
167
+ line.style.strokeWidth = "1";
168
+ group.appendChild(line);
169
+ }
170
+
171
+ svg.appendChild(group);
172
+ }
173
+
174
+ /**
175
+ * Create a tooltip element for radar charts
176
+ * @param {HTMLElement} container
177
+ * @param {number} [maxWidth=200]
178
+ * @returns {HTMLDivElement}
179
+ */
180
+ export function createRadarTooltip(container, maxWidth = 200) {
181
+ const tooltip = document.createElement("div");
182
+ tooltip.className = "radar-tooltip";
183
+ tooltip.style.cssText = `
184
+ position: absolute;
185
+ background: #1e293b;
186
+ color: white;
187
+ padding: 8px 12px;
188
+ border-radius: 6px;
189
+ font-size: 12px;
190
+ pointer-events: none;
191
+ opacity: 0;
192
+ transition: opacity 0.2s;
193
+ z-index: 100;
194
+ max-width: ${maxWidth}px;
195
+ `;
196
+ container.style.position = "relative";
197
+ container.appendChild(tooltip);
198
+ return tooltip;
199
+ }