@forwardimpact/pathway 0.1.0

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 (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Radar chart visualization using SVG
3
+ */
4
+
5
+ /**
6
+ * @typedef {Object} RadarDataPoint
7
+ * @property {string} label - Label for this axis
8
+ * @property {number} value - Current value
9
+ * @property {number} maxValue - Maximum possible value
10
+ * @property {string} [description] - Optional description for tooltip
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} RadarOptions
15
+ * @property {number} [levels=5] - Number of concentric rings
16
+ * @property {string} [color='#3b82f6'] - Fill color
17
+ * @property {string} [strokeColor='#2563eb'] - Stroke color
18
+ * @property {boolean} [showLabels=true] - Show axis labels
19
+ * @property {boolean} [showTooltips=true] - Enable tooltips
20
+ * @property {number} [size=400] - Chart size in pixels
21
+ * @property {number} [labelOffset=25] - Distance of labels from edge
22
+ */
23
+
24
+ export class RadarChart {
25
+ /**
26
+ * @param {Object} config
27
+ * @param {HTMLElement} config.container
28
+ * @param {RadarDataPoint[]} config.data
29
+ * @param {RadarOptions} [config.options]
30
+ */
31
+ constructor({ container, data, options = {} }) {
32
+ this.container = container;
33
+ this.data = data;
34
+ this.options = {
35
+ levels: options.levels || 5,
36
+ color: options.color || "#3b82f6",
37
+ strokeColor: options.strokeColor || "#2563eb",
38
+ showLabels: options.showLabels !== false,
39
+ showTooltips: options.showTooltips !== false,
40
+ size: options.size || 400,
41
+ labelOffset: options.labelOffset || 50,
42
+ };
43
+
44
+ this.center = this.options.size / 2;
45
+ this.radius = this.options.size / 2 - this.options.labelOffset - 20;
46
+ this.angleSlice = (Math.PI * 2) / this.data.length;
47
+
48
+ this.svg = null;
49
+ this.tooltip = null;
50
+ }
51
+
52
+ /**
53
+ * Render the radar chart
54
+ */
55
+ render() {
56
+ this.container.innerHTML = "";
57
+
58
+ // Create SVG
59
+ this.svg = this.createSvg();
60
+
61
+ // Draw concentric rings
62
+ this.drawLevels();
63
+
64
+ // Draw axes
65
+ this.drawAxes();
66
+
67
+ // Draw data polygon
68
+ this.drawDataPolygon();
69
+
70
+ // Draw data points
71
+ this.drawDataPoints();
72
+
73
+ // Add labels
74
+ if (this.options.showLabels) {
75
+ this.drawLabels();
76
+ }
77
+
78
+ // Add tooltip container
79
+ if (this.options.showTooltips) {
80
+ this.createTooltip();
81
+ }
82
+
83
+ this.container.appendChild(this.svg);
84
+ }
85
+
86
+ /**
87
+ * Create the SVG element
88
+ * @returns {SVGElement}
89
+ */
90
+ createSvg() {
91
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
92
+ // Add padding around the chart to prevent label cutoff
93
+ const padding = 40;
94
+ const totalSize = this.options.size + padding * 2;
95
+ svg.setAttribute("width", totalSize);
96
+ svg.setAttribute("height", totalSize);
97
+ svg.setAttribute(
98
+ "viewBox",
99
+ `${-padding} ${-padding} ${totalSize} ${totalSize}`,
100
+ );
101
+ svg.classList.add("radar-chart");
102
+ return svg;
103
+ }
104
+
105
+ /**
106
+ * Draw concentric level rings
107
+ */
108
+ drawLevels() {
109
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
110
+ group.classList.add("radar-levels");
111
+
112
+ for (let level = 1; level <= this.options.levels; level++) {
113
+ const levelRadius = (this.radius * level) / this.options.levels;
114
+ const points = this.data.map((_, i) => {
115
+ const angle = this.angleSlice * i - Math.PI / 2;
116
+ return {
117
+ x: this.center + levelRadius * Math.cos(angle),
118
+ y: this.center + levelRadius * Math.sin(angle),
119
+ };
120
+ });
121
+
122
+ const polygon = document.createElementNS(
123
+ "http://www.w3.org/2000/svg",
124
+ "polygon",
125
+ );
126
+ polygon.setAttribute(
127
+ "points",
128
+ points.map((p) => `${p.x},${p.y}`).join(" "),
129
+ );
130
+ polygon.classList.add("radar-level");
131
+ polygon.style.fill = "none";
132
+ polygon.style.stroke = "#e2e8f0";
133
+ polygon.style.strokeWidth = "1";
134
+ group.appendChild(polygon);
135
+
136
+ // Add level label
137
+ const labelX = this.center + 5;
138
+ const labelY = this.center - levelRadius + 4;
139
+ const label = document.createElementNS(
140
+ "http://www.w3.org/2000/svg",
141
+ "text",
142
+ );
143
+ label.setAttribute("x", labelX);
144
+ label.setAttribute("y", labelY);
145
+ label.textContent = level;
146
+ label.classList.add("radar-level-label");
147
+ label.style.fontSize = "10px";
148
+ label.style.fill = "#94a3b8";
149
+ group.appendChild(label);
150
+ }
151
+
152
+ this.svg.appendChild(group);
153
+ }
154
+
155
+ /**
156
+ * Draw axis lines
157
+ */
158
+ drawAxes() {
159
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
160
+ group.classList.add("radar-axes");
161
+
162
+ this.data.forEach((_, i) => {
163
+ const angle = this.angleSlice * i - Math.PI / 2;
164
+ const x = this.center + this.radius * Math.cos(angle);
165
+ const y = this.center + this.radius * Math.sin(angle);
166
+
167
+ const line = document.createElementNS(
168
+ "http://www.w3.org/2000/svg",
169
+ "line",
170
+ );
171
+ line.setAttribute("x1", this.center);
172
+ line.setAttribute("y1", this.center);
173
+ line.setAttribute("x2", x);
174
+ line.setAttribute("y2", y);
175
+ line.classList.add("radar-axis");
176
+ line.style.stroke = "#cbd5e1";
177
+ line.style.strokeWidth = "1";
178
+ group.appendChild(line);
179
+ });
180
+
181
+ this.svg.appendChild(group);
182
+ }
183
+
184
+ /**
185
+ * Draw the data polygon
186
+ */
187
+ drawDataPolygon() {
188
+ const points = this.data.map((d, i) => {
189
+ const angle = this.angleSlice * i - Math.PI / 2;
190
+ const value = d.value / d.maxValue;
191
+ const r = this.radius * value;
192
+ return {
193
+ x: this.center + r * Math.cos(angle),
194
+ y: this.center + r * Math.sin(angle),
195
+ };
196
+ });
197
+
198
+ const polygon = document.createElementNS(
199
+ "http://www.w3.org/2000/svg",
200
+ "polygon",
201
+ );
202
+ polygon.setAttribute(
203
+ "points",
204
+ points.map((p) => `${p.x},${p.y}`).join(" "),
205
+ );
206
+ polygon.classList.add("radar-data");
207
+ polygon.style.fill = this.options.color;
208
+ polygon.style.fillOpacity = "0.3";
209
+ polygon.style.stroke = this.options.strokeColor;
210
+ polygon.style.strokeWidth = "2";
211
+
212
+ this.svg.appendChild(polygon);
213
+ }
214
+
215
+ /**
216
+ * Draw data points
217
+ */
218
+ drawDataPoints() {
219
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
220
+ group.classList.add("radar-points");
221
+
222
+ this.data.forEach((d, i) => {
223
+ const angle = this.angleSlice * i - Math.PI / 2;
224
+ const value = d.value / d.maxValue;
225
+ const r = this.radius * value;
226
+ const x = this.center + r * Math.cos(angle);
227
+ const y = this.center + r * Math.sin(angle);
228
+
229
+ const circle = document.createElementNS(
230
+ "http://www.w3.org/2000/svg",
231
+ "circle",
232
+ );
233
+ circle.setAttribute("cx", x);
234
+ circle.setAttribute("cy", y);
235
+ circle.setAttribute("r", 5);
236
+ circle.classList.add("radar-point");
237
+ circle.style.fill = this.options.strokeColor;
238
+ circle.style.stroke = "#fff";
239
+ circle.style.strokeWidth = "2";
240
+ circle.style.cursor = "pointer";
241
+
242
+ if (this.options.showTooltips) {
243
+ circle.addEventListener("mouseenter", (e) => this.showTooltip(e, d));
244
+ circle.addEventListener("mouseleave", () => this.hideTooltip());
245
+ }
246
+
247
+ group.appendChild(circle);
248
+ });
249
+
250
+ this.svg.appendChild(group);
251
+ }
252
+
253
+ /**
254
+ * Draw axis labels
255
+ */
256
+ drawLabels() {
257
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
258
+ group.classList.add("radar-labels");
259
+
260
+ this.data.forEach((d, i) => {
261
+ const angle = this.angleSlice * i - Math.PI / 2;
262
+ const labelRadius = this.radius + this.options.labelOffset;
263
+ const x = this.center + labelRadius * Math.cos(angle);
264
+ const y = this.center + labelRadius * Math.sin(angle);
265
+
266
+ const text = document.createElementNS(
267
+ "http://www.w3.org/2000/svg",
268
+ "text",
269
+ );
270
+ text.setAttribute("x", x);
271
+ text.setAttribute("y", y);
272
+ text.classList.add("radar-label");
273
+ text.style.fontSize = "11px";
274
+ text.style.fill = "#475569";
275
+ text.style.textAnchor = this.getTextAnchor(angle);
276
+ text.style.dominantBaseline = "middle";
277
+
278
+ // Split long labels into multiple lines
279
+ const lines = this.wrapLabel(d.label, 12);
280
+ const lineHeight = 13;
281
+ const offsetY = -((lines.length - 1) * lineHeight) / 2;
282
+
283
+ lines.forEach((line, lineIndex) => {
284
+ const tspan = document.createElementNS(
285
+ "http://www.w3.org/2000/svg",
286
+ "tspan",
287
+ );
288
+ tspan.setAttribute("x", x);
289
+ tspan.setAttribute("dy", lineIndex === 0 ? offsetY : lineHeight);
290
+ tspan.textContent = line;
291
+ text.appendChild(tspan);
292
+ });
293
+
294
+ if (this.options.showTooltips) {
295
+ text.style.cursor = "pointer";
296
+ text.addEventListener("mouseenter", (e) => this.showTooltip(e, d));
297
+ text.addEventListener("mouseleave", () => this.hideTooltip());
298
+ }
299
+
300
+ group.appendChild(text);
301
+ });
302
+
303
+ this.svg.appendChild(group);
304
+ }
305
+
306
+ /**
307
+ * Wrap label text into multiple lines
308
+ * @param {string} text
309
+ * @param {number} maxCharsPerLine
310
+ * @returns {string[]}
311
+ */
312
+ wrapLabel(text, maxCharsPerLine) {
313
+ if (text.length <= maxCharsPerLine) return [text];
314
+
315
+ const words = text.split(/\s+/);
316
+ const lines = [];
317
+ let currentLine = "";
318
+
319
+ for (const word of words) {
320
+ if (currentLine.length === 0) {
321
+ currentLine = word;
322
+ } else if (currentLine.length + 1 + word.length <= maxCharsPerLine) {
323
+ currentLine += " " + word;
324
+ } else {
325
+ lines.push(currentLine);
326
+ currentLine = word;
327
+ }
328
+ }
329
+
330
+ if (currentLine.length > 0) {
331
+ lines.push(currentLine);
332
+ }
333
+
334
+ return lines.length > 0 ? lines : [text];
335
+ }
336
+
337
+ /**
338
+ * Get text anchor based on angle
339
+ * @param {number} angle
340
+ * @returns {string}
341
+ */
342
+ getTextAnchor(angle) {
343
+ const degrees = (angle * 180) / Math.PI + 90;
344
+ if (degrees > 45 && degrees < 135) return "start";
345
+ if (degrees > 225 && degrees < 315) return "end";
346
+ return "middle";
347
+ }
348
+
349
+ /**
350
+ * Truncate label text
351
+ * @param {string} text
352
+ * @param {number} maxLength
353
+ * @returns {string}
354
+ */
355
+ truncateLabel(text, maxLength) {
356
+ if (text.length <= maxLength) return text;
357
+ return text.slice(0, maxLength - 1) + "…";
358
+ }
359
+
360
+ /**
361
+ * Create tooltip element
362
+ */
363
+ createTooltip() {
364
+ this.tooltip = document.createElement("div");
365
+ this.tooltip.className = "radar-tooltip";
366
+ this.tooltip.style.cssText = `
367
+ position: absolute;
368
+ background: #1e293b;
369
+ color: white;
370
+ padding: 8px 12px;
371
+ border-radius: 6px;
372
+ font-size: 12px;
373
+ pointer-events: none;
374
+ opacity: 0;
375
+ transition: opacity 0.2s;
376
+ z-index: 100;
377
+ max-width: 200px;
378
+ `;
379
+ this.container.style.position = "relative";
380
+ this.container.appendChild(this.tooltip);
381
+ }
382
+
383
+ /**
384
+ * Show tooltip
385
+ * @param {MouseEvent} event
386
+ * @param {RadarDataPoint} data
387
+ */
388
+ showTooltip(event, data) {
389
+ if (!this.tooltip) return;
390
+
391
+ const rect = this.container.getBoundingClientRect();
392
+ const x = event.clientX - rect.left;
393
+ const y = event.clientY - rect.top;
394
+
395
+ this.tooltip.innerHTML = `
396
+ <strong>${data.label}</strong><br>
397
+ Value: ${data.value}/${data.maxValue}
398
+ ${data.description ? `<br><small>${data.description}</small>` : ""}
399
+ `;
400
+
401
+ this.tooltip.style.left = `${x + 10}px`;
402
+ this.tooltip.style.top = `${y - 10}px`;
403
+ this.tooltip.style.opacity = "1";
404
+ }
405
+
406
+ /**
407
+ * Hide tooltip
408
+ */
409
+ hideTooltip() {
410
+ if (this.tooltip) {
411
+ this.tooltip.style.opacity = "0";
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Update data and re-render
417
+ * @param {RadarDataPoint[]} newData
418
+ */
419
+ update(newData) {
420
+ this.data = newData;
421
+ this.angleSlice = (Math.PI * 2) / this.data.length;
422
+ this.render();
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Comparison Radar Chart - displays two overlaid radar charts
428
+ */
429
+ export class ComparisonRadarChart {
430
+ /**
431
+ * @param {Object} config
432
+ * @param {HTMLElement} config.container
433
+ * @param {RadarDataPoint[]} config.currentData
434
+ * @param {RadarDataPoint[]} config.targetData
435
+ * @param {Object} [config.options]
436
+ */
437
+ constructor({ container, currentData, targetData, options = {} }) {
438
+ this.container = container;
439
+ this.currentData = currentData;
440
+ this.targetData = targetData;
441
+ this.options = {
442
+ levels: options.levels || 5,
443
+ currentColor: options.currentColor || "#3b82f6",
444
+ targetColor: options.targetColor || "#10b981",
445
+ showLabels: options.showLabels !== false,
446
+ showTooltips: options.showTooltips !== false,
447
+ size: options.size || 400,
448
+ labelOffset: options.labelOffset || 50,
449
+ };
450
+
451
+ this.center = this.options.size / 2;
452
+ this.radius = this.options.size / 2 - this.options.labelOffset - 20;
453
+ this.angleSlice = (Math.PI * 2) / this.currentData.length;
454
+
455
+ this.svg = null;
456
+ this.tooltip = null;
457
+ }
458
+
459
+ /**
460
+ * Render the comparison radar chart
461
+ */
462
+ render() {
463
+ this.container.innerHTML = "";
464
+
465
+ // Create SVG
466
+ this.svg = this.createSvg();
467
+
468
+ // Draw concentric rings
469
+ this.drawLevels();
470
+
471
+ // Draw axes
472
+ this.drawAxes();
473
+
474
+ // Draw target polygon (behind)
475
+ this.drawDataPolygon(this.targetData, this.options.targetColor, 0.2);
476
+
477
+ // Draw current polygon (in front)
478
+ this.drawDataPolygon(this.currentData, this.options.currentColor, 0.3);
479
+
480
+ // Draw target points
481
+ this.drawDataPoints(this.targetData, this.options.targetColor, "target");
482
+
483
+ // Draw current points
484
+ this.drawDataPoints(this.currentData, this.options.currentColor, "current");
485
+
486
+ // Add labels
487
+ if (this.options.showLabels) {
488
+ this.drawLabels();
489
+ }
490
+
491
+ // Add tooltip container
492
+ if (this.options.showTooltips) {
493
+ this.createTooltip();
494
+ }
495
+
496
+ this.container.appendChild(this.svg);
497
+ }
498
+
499
+ /**
500
+ * Create the SVG element
501
+ * @returns {SVGElement}
502
+ */
503
+ createSvg() {
504
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
505
+ const padding = 40;
506
+ const totalSize = this.options.size + padding * 2;
507
+ svg.setAttribute("width", totalSize);
508
+ svg.setAttribute("height", totalSize);
509
+ svg.setAttribute(
510
+ "viewBox",
511
+ `${-padding} ${-padding} ${totalSize} ${totalSize}`,
512
+ );
513
+ svg.classList.add("radar-chart", "comparison-radar-chart");
514
+ return svg;
515
+ }
516
+
517
+ /**
518
+ * Draw concentric level rings
519
+ */
520
+ drawLevels() {
521
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
522
+ group.classList.add("radar-levels");
523
+
524
+ for (let level = 1; level <= this.options.levels; level++) {
525
+ const levelRadius = (this.radius * level) / this.options.levels;
526
+ const points = this.currentData.map((_, i) => {
527
+ const angle = this.angleSlice * i - Math.PI / 2;
528
+ return {
529
+ x: this.center + levelRadius * Math.cos(angle),
530
+ y: this.center + levelRadius * Math.sin(angle),
531
+ };
532
+ });
533
+
534
+ const polygon = document.createElementNS(
535
+ "http://www.w3.org/2000/svg",
536
+ "polygon",
537
+ );
538
+ polygon.setAttribute(
539
+ "points",
540
+ points.map((p) => `${p.x},${p.y}`).join(" "),
541
+ );
542
+ polygon.classList.add("radar-level");
543
+ polygon.style.fill = "none";
544
+ polygon.style.stroke = "#e2e8f0";
545
+ polygon.style.strokeWidth = "1";
546
+ group.appendChild(polygon);
547
+
548
+ // Add level label
549
+ const labelX = this.center + 5;
550
+ const labelY = this.center - levelRadius + 4;
551
+ const label = document.createElementNS(
552
+ "http://www.w3.org/2000/svg",
553
+ "text",
554
+ );
555
+ label.setAttribute("x", labelX);
556
+ label.setAttribute("y", labelY);
557
+ label.textContent = level;
558
+ label.classList.add("radar-level-label");
559
+ label.style.fontSize = "10px";
560
+ label.style.fill = "#94a3b8";
561
+ group.appendChild(label);
562
+ }
563
+
564
+ this.svg.appendChild(group);
565
+ }
566
+
567
+ /**
568
+ * Draw axis lines
569
+ */
570
+ drawAxes() {
571
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
572
+ group.classList.add("radar-axes");
573
+
574
+ this.currentData.forEach((_, i) => {
575
+ const angle = this.angleSlice * i - Math.PI / 2;
576
+ const x = this.center + this.radius * Math.cos(angle);
577
+ const y = this.center + this.radius * Math.sin(angle);
578
+
579
+ const line = document.createElementNS(
580
+ "http://www.w3.org/2000/svg",
581
+ "line",
582
+ );
583
+ line.setAttribute("x1", this.center);
584
+ line.setAttribute("y1", this.center);
585
+ line.setAttribute("x2", x);
586
+ line.setAttribute("y2", y);
587
+ line.classList.add("radar-axis");
588
+ line.style.stroke = "#cbd5e1";
589
+ line.style.strokeWidth = "1";
590
+ group.appendChild(line);
591
+ });
592
+
593
+ this.svg.appendChild(group);
594
+ }
595
+
596
+ /**
597
+ * Draw a data polygon
598
+ * @param {RadarDataPoint[]} data
599
+ * @param {string} color
600
+ * @param {number} opacity
601
+ */
602
+ drawDataPolygon(data, color, opacity) {
603
+ const points = data.map((d, i) => {
604
+ const angle = this.angleSlice * i - Math.PI / 2;
605
+ const value = d.value / d.maxValue;
606
+ const r = this.radius * value;
607
+ return {
608
+ x: this.center + r * Math.cos(angle),
609
+ y: this.center + r * Math.sin(angle),
610
+ };
611
+ });
612
+
613
+ const polygon = document.createElementNS(
614
+ "http://www.w3.org/2000/svg",
615
+ "polygon",
616
+ );
617
+ polygon.setAttribute(
618
+ "points",
619
+ points.map((p) => `${p.x},${p.y}`).join(" "),
620
+ );
621
+ polygon.classList.add("radar-data");
622
+ polygon.style.fill = color;
623
+ polygon.style.fillOpacity = String(opacity);
624
+ polygon.style.stroke = color;
625
+ polygon.style.strokeWidth = "2";
626
+
627
+ this.svg.appendChild(polygon);
628
+ }
629
+
630
+ /**
631
+ * Draw data points
632
+ * @param {RadarDataPoint[]} data
633
+ * @param {string} color
634
+ * @param {string} type
635
+ */
636
+ drawDataPoints(data, color, type) {
637
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
638
+ group.classList.add("radar-points", `radar-points-${type}`);
639
+
640
+ data.forEach((d, i) => {
641
+ const angle = this.angleSlice * i - Math.PI / 2;
642
+ const value = d.value / d.maxValue;
643
+ const r = this.radius * value;
644
+ const x = this.center + r * Math.cos(angle);
645
+ const y = this.center + r * Math.sin(angle);
646
+
647
+ const circle = document.createElementNS(
648
+ "http://www.w3.org/2000/svg",
649
+ "circle",
650
+ );
651
+ circle.setAttribute("cx", x);
652
+ circle.setAttribute("cy", y);
653
+ circle.setAttribute("r", 4);
654
+ circle.classList.add("radar-point");
655
+ circle.style.fill = color;
656
+ circle.style.stroke = "#fff";
657
+ circle.style.strokeWidth = "2";
658
+ circle.style.cursor = "pointer";
659
+
660
+ if (this.options.showTooltips) {
661
+ circle.addEventListener("mouseenter", (e) =>
662
+ this.showTooltip(e, d, type),
663
+ );
664
+ circle.addEventListener("mouseleave", () => this.hideTooltip());
665
+ }
666
+
667
+ group.appendChild(circle);
668
+ });
669
+
670
+ this.svg.appendChild(group);
671
+ }
672
+
673
+ /**
674
+ * Draw axis labels
675
+ */
676
+ drawLabels() {
677
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
678
+ group.classList.add("radar-labels");
679
+
680
+ this.currentData.forEach((d, i) => {
681
+ const targetD = this.targetData[i];
682
+ const angle = this.angleSlice * i - Math.PI / 2;
683
+ const labelRadius = this.radius + this.options.labelOffset;
684
+ const x = this.center + labelRadius * Math.cos(angle);
685
+ const y = this.center + labelRadius * Math.sin(angle);
686
+
687
+ // Check if there's a difference
688
+ const diff = targetD.value - d.value;
689
+ const hasDiff = diff !== 0;
690
+
691
+ const text = document.createElementNS(
692
+ "http://www.w3.org/2000/svg",
693
+ "text",
694
+ );
695
+ text.setAttribute("x", x);
696
+ text.setAttribute("y", y);
697
+ text.classList.add("radar-label");
698
+ if (hasDiff) text.classList.add("has-change");
699
+ text.style.fontSize = "11px";
700
+ text.style.fill = hasDiff
701
+ ? diff > 0
702
+ ? "#059669"
703
+ : "#dc2626"
704
+ : "#475569";
705
+ text.style.fontWeight = hasDiff ? "600" : "400";
706
+ text.style.textAnchor = this.getTextAnchor(angle);
707
+ text.style.dominantBaseline = "middle";
708
+
709
+ // Create label with change indicator
710
+ let labelText = d.label;
711
+ if (hasDiff) {
712
+ labelText += ` (${diff > 0 ? "+" : ""}${diff})`;
713
+ }
714
+
715
+ const lines = this.wrapLabel(labelText, 15);
716
+ const lineHeight = 13;
717
+ const offsetY = -((lines.length - 1) * lineHeight) / 2;
718
+
719
+ lines.forEach((line, lineIndex) => {
720
+ const tspan = document.createElementNS(
721
+ "http://www.w3.org/2000/svg",
722
+ "tspan",
723
+ );
724
+ tspan.setAttribute("x", x);
725
+ tspan.setAttribute("dy", lineIndex === 0 ? offsetY : lineHeight);
726
+ tspan.textContent = line;
727
+ text.appendChild(tspan);
728
+ });
729
+
730
+ if (this.options.showTooltips) {
731
+ text.style.cursor = "pointer";
732
+ text.addEventListener("mouseenter", (e) =>
733
+ this.showComparisonTooltip(e, d, targetD),
734
+ );
735
+ text.addEventListener("mouseleave", () => this.hideTooltip());
736
+ }
737
+
738
+ group.appendChild(text);
739
+ });
740
+
741
+ this.svg.appendChild(group);
742
+ }
743
+
744
+ /**
745
+ * Wrap label text into multiple lines
746
+ */
747
+ wrapLabel(text, maxCharsPerLine) {
748
+ if (text.length <= maxCharsPerLine) return [text];
749
+
750
+ const words = text.split(/\s+/);
751
+ const lines = [];
752
+ let currentLine = "";
753
+
754
+ for (const word of words) {
755
+ if (currentLine.length === 0) {
756
+ currentLine = word;
757
+ } else if (currentLine.length + 1 + word.length <= maxCharsPerLine) {
758
+ currentLine += " " + word;
759
+ } else {
760
+ lines.push(currentLine);
761
+ currentLine = word;
762
+ }
763
+ }
764
+
765
+ if (currentLine.length > 0) {
766
+ lines.push(currentLine);
767
+ }
768
+
769
+ return lines.length > 0 ? lines : [text];
770
+ }
771
+
772
+ /**
773
+ * Get text anchor based on angle
774
+ */
775
+ getTextAnchor(angle) {
776
+ const degrees = (angle * 180) / Math.PI + 90;
777
+ if (degrees > 45 && degrees < 135) return "start";
778
+ if (degrees > 225 && degrees < 315) return "end";
779
+ return "middle";
780
+ }
781
+
782
+ /**
783
+ * Create tooltip element
784
+ */
785
+ createTooltip() {
786
+ this.tooltip = document.createElement("div");
787
+ this.tooltip.className = "radar-tooltip";
788
+ this.tooltip.style.cssText = `
789
+ position: absolute;
790
+ background: #1e293b;
791
+ color: white;
792
+ padding: 8px 12px;
793
+ border-radius: 6px;
794
+ font-size: 12px;
795
+ pointer-events: none;
796
+ opacity: 0;
797
+ transition: opacity 0.2s;
798
+ z-index: 100;
799
+ max-width: 250px;
800
+ `;
801
+ this.container.style.position = "relative";
802
+ this.container.appendChild(this.tooltip);
803
+ }
804
+
805
+ /**
806
+ * Show tooltip for a single data point
807
+ */
808
+ showTooltip(event, data, type) {
809
+ if (!this.tooltip) return;
810
+
811
+ const rect = this.container.getBoundingClientRect();
812
+ const x = event.clientX - rect.left;
813
+ const y = event.clientY - rect.top;
814
+
815
+ const typeLabel = type === "current" ? "Current" : "Target";
816
+
817
+ this.tooltip.innerHTML = `
818
+ <strong>${data.label}</strong><br>
819
+ ${typeLabel}: ${data.value}/${data.maxValue}
820
+ ${data.description ? `<br><small>${data.description}</small>` : ""}
821
+ `;
822
+
823
+ this.tooltip.style.left = `${x + 10}px`;
824
+ this.tooltip.style.top = `${y - 10}px`;
825
+ this.tooltip.style.opacity = "1";
826
+ }
827
+
828
+ /**
829
+ * Show comparison tooltip
830
+ */
831
+ showComparisonTooltip(event, currentData, targetData) {
832
+ if (!this.tooltip) return;
833
+
834
+ const rect = this.container.getBoundingClientRect();
835
+ const x = event.clientX - rect.left;
836
+ const y = event.clientY - rect.top;
837
+
838
+ const diff = targetData.value - currentData.value;
839
+ const diffText =
840
+ diff > 0
841
+ ? `<span style="color: #10b981">↑ ${diff} level${diff > 1 ? "s" : ""}</span>`
842
+ : diff < 0
843
+ ? `<span style="color: #ef4444">↓ ${Math.abs(diff)} level${Math.abs(diff) > 1 ? "s" : ""}</span>`
844
+ : "<span style='color: #94a3b8'>No change</span>";
845
+
846
+ this.tooltip.innerHTML = `
847
+ <strong>${currentData.label}</strong><br>
848
+ Current: ${currentData.value}/${currentData.maxValue}<br>
849
+ Target: ${targetData.value}/${targetData.maxValue}<br>
850
+ ${diffText}
851
+ `;
852
+
853
+ this.tooltip.style.left = `${x + 10}px`;
854
+ this.tooltip.style.top = `${y - 10}px`;
855
+ this.tooltip.style.opacity = "1";
856
+ }
857
+
858
+ /**
859
+ * Hide tooltip
860
+ */
861
+ hideTooltip() {
862
+ if (this.tooltip) {
863
+ this.tooltip.style.opacity = "0";
864
+ }
865
+ }
866
+ }