@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.
- package/bin/fit-pathway.js +62 -54
- package/package.json +1 -3
- package/src/commands/agent-io.js +120 -0
- package/src/commands/agent.js +266 -349
- package/src/commands/init.js +2 -2
- package/src/commands/job.js +237 -183
- package/src/components/comparison-radar.js +118 -103
- package/src/components/progression-table.js +244 -208
- package/src/formatters/index.js +0 -19
- package/src/formatters/interview/markdown.js +100 -88
- package/src/formatters/job/description.js +76 -75
- package/src/formatters/job/dom.js +113 -97
- package/src/formatters/level/dom.js +87 -102
- package/src/formatters/questions/markdown.js +37 -33
- package/src/formatters/questions/shared.js +142 -75
- package/src/formatters/skill/dom.js +102 -93
- package/src/lib/comparison-radar-chart.js +256 -0
- package/src/lib/radar-utils.js +199 -0
- package/src/lib/radar.js +25 -662
- package/src/pages/agent-builder-download.js +170 -0
- package/src/pages/agent-builder-preview.js +344 -0
- package/src/pages/agent-builder.js +6 -550
- package/src/pages/progress-comparison.js +110 -0
- package/src/pages/progress.js +11 -111
- package/src/pages/self-assessment-steps.js +494 -0
- package/src/pages/self-assessment.js +54 -504
- package/src/formatters/behaviour/microdata.js +0 -106
- package/src/formatters/discipline/microdata.js +0 -117
- package/src/formatters/driver/microdata.js +0 -91
- package/src/formatters/level/microdata.js +0 -141
- package/src/formatters/microdata-shared.js +0 -184
- package/src/formatters/skill/microdata.js +0 -151
- package/src/formatters/stage/microdata.js +0 -116
- package/src/formatters/track/microdata.js +0 -111
package/src/lib/radar.js
CHANGED
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
* Radar chart visualization using SVG
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
escapeHtml,
|
|
7
|
+
wrapLabel,
|
|
8
|
+
getTextAnchor,
|
|
9
|
+
createRadarSvg,
|
|
10
|
+
drawLevelRings,
|
|
11
|
+
drawAxisLines,
|
|
12
|
+
createRadarTooltip,
|
|
13
|
+
} from "./radar-utils.js";
|
|
14
|
+
|
|
15
|
+
// Re-export ComparisonRadarChart so existing imports keep working
|
|
16
|
+
export { ComparisonRadarChart } from "./comparison-radar-chart.js";
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* @typedef {Object} RadarDataPoint
|
|
@@ -63,141 +62,32 @@ export class RadarChart {
|
|
|
63
62
|
this.tooltip = null;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
/**
|
|
67
|
-
* Render the radar chart
|
|
68
|
-
*/
|
|
69
65
|
render() {
|
|
70
66
|
this.container.innerHTML = "";
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
this.svg = this.createSvg();
|
|
68
|
+
this.svg = createRadarSvg(this.options.size);
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
const sharedParams = {
|
|
71
|
+
radius: this.radius,
|
|
72
|
+
center: this.center,
|
|
73
|
+
angleSlice: this.angleSlice,
|
|
74
|
+
dataLength: this.data.length,
|
|
75
|
+
};
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
this.
|
|
77
|
+
drawLevelRings(this.svg, { ...sharedParams, levels: this.options.levels });
|
|
78
|
+
drawAxisLines(this.svg, sharedParams);
|
|
80
79
|
|
|
81
|
-
// Draw data polygon
|
|
82
80
|
this.drawDataPolygon();
|
|
83
|
-
|
|
84
|
-
// Draw data points
|
|
85
81
|
this.drawDataPoints();
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
if (this.options.showLabels) {
|
|
89
|
-
this.drawLabels();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Add tooltip container
|
|
83
|
+
if (this.options.showLabels) this.drawLabels();
|
|
93
84
|
if (this.options.showTooltips) {
|
|
94
|
-
this.
|
|
85
|
+
this.tooltip = createRadarTooltip(this.container);
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
this.container.appendChild(this.svg);
|
|
98
89
|
}
|
|
99
90
|
|
|
100
|
-
/**
|
|
101
|
-
* Create the SVG element
|
|
102
|
-
* @returns {SVGElement}
|
|
103
|
-
*/
|
|
104
|
-
createSvg() {
|
|
105
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
106
|
-
// Add padding around the chart to prevent label cutoff
|
|
107
|
-
const padding = 40;
|
|
108
|
-
const totalSize = this.options.size + padding * 2;
|
|
109
|
-
svg.setAttribute("width", totalSize);
|
|
110
|
-
svg.setAttribute("height", totalSize);
|
|
111
|
-
svg.setAttribute(
|
|
112
|
-
"viewBox",
|
|
113
|
-
`${-padding} ${-padding} ${totalSize} ${totalSize}`,
|
|
114
|
-
);
|
|
115
|
-
svg.classList.add("radar-chart");
|
|
116
|
-
return svg;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Draw concentric level rings
|
|
121
|
-
*/
|
|
122
|
-
drawLevels() {
|
|
123
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
124
|
-
group.classList.add("radar-levels");
|
|
125
|
-
|
|
126
|
-
for (let level = 1; level <= this.options.levels; level++) {
|
|
127
|
-
const levelRadius = (this.radius * level) / this.options.levels;
|
|
128
|
-
const points = this.data.map((_, i) => {
|
|
129
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
130
|
-
return {
|
|
131
|
-
x: this.center + levelRadius * Math.cos(angle),
|
|
132
|
-
y: this.center + levelRadius * Math.sin(angle),
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const polygon = document.createElementNS(
|
|
137
|
-
"http://www.w3.org/2000/svg",
|
|
138
|
-
"polygon",
|
|
139
|
-
);
|
|
140
|
-
polygon.setAttribute(
|
|
141
|
-
"points",
|
|
142
|
-
points.map((p) => `${p.x},${p.y}`).join(" "),
|
|
143
|
-
);
|
|
144
|
-
polygon.classList.add("radar-level");
|
|
145
|
-
polygon.style.fill = "none";
|
|
146
|
-
polygon.style.stroke = "#e2e8f0";
|
|
147
|
-
polygon.style.strokeWidth = "1";
|
|
148
|
-
group.appendChild(polygon);
|
|
149
|
-
|
|
150
|
-
// Add level label
|
|
151
|
-
const labelX = this.center + 5;
|
|
152
|
-
const labelY = this.center - levelRadius + 4;
|
|
153
|
-
const label = document.createElementNS(
|
|
154
|
-
"http://www.w3.org/2000/svg",
|
|
155
|
-
"text",
|
|
156
|
-
);
|
|
157
|
-
label.setAttribute("x", labelX);
|
|
158
|
-
label.setAttribute("y", labelY);
|
|
159
|
-
label.textContent = level;
|
|
160
|
-
label.classList.add("radar-level-label");
|
|
161
|
-
label.style.fontSize = "10px";
|
|
162
|
-
label.style.fill = "#94a3b8";
|
|
163
|
-
group.appendChild(label);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
this.svg.appendChild(group);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Draw axis lines
|
|
171
|
-
*/
|
|
172
|
-
drawAxes() {
|
|
173
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
174
|
-
group.classList.add("radar-axes");
|
|
175
|
-
|
|
176
|
-
this.data.forEach((_, i) => {
|
|
177
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
178
|
-
const x = this.center + this.radius * Math.cos(angle);
|
|
179
|
-
const y = this.center + this.radius * Math.sin(angle);
|
|
180
|
-
|
|
181
|
-
const line = document.createElementNS(
|
|
182
|
-
"http://www.w3.org/2000/svg",
|
|
183
|
-
"line",
|
|
184
|
-
);
|
|
185
|
-
line.setAttribute("x1", this.center);
|
|
186
|
-
line.setAttribute("y1", this.center);
|
|
187
|
-
line.setAttribute("x2", x);
|
|
188
|
-
line.setAttribute("y2", y);
|
|
189
|
-
line.classList.add("radar-axis");
|
|
190
|
-
line.style.stroke = "#cbd5e1";
|
|
191
|
-
line.style.strokeWidth = "1";
|
|
192
|
-
group.appendChild(line);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
this.svg.appendChild(group);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Draw the data polygon
|
|
200
|
-
*/
|
|
201
91
|
drawDataPolygon() {
|
|
202
92
|
const points = this.data.map((d, i) => {
|
|
203
93
|
const angle = this.angleSlice * i - Math.PI / 2;
|
|
@@ -226,9 +116,6 @@ export class RadarChart {
|
|
|
226
116
|
this.svg.appendChild(polygon);
|
|
227
117
|
}
|
|
228
118
|
|
|
229
|
-
/**
|
|
230
|
-
* Draw data points
|
|
231
|
-
*/
|
|
232
119
|
drawDataPoints() {
|
|
233
120
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
234
121
|
group.classList.add("radar-points");
|
|
@@ -264,9 +151,6 @@ export class RadarChart {
|
|
|
264
151
|
this.svg.appendChild(group);
|
|
265
152
|
}
|
|
266
153
|
|
|
267
|
-
/**
|
|
268
|
-
* Draw axis labels
|
|
269
|
-
*/
|
|
270
154
|
drawLabels() {
|
|
271
155
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
272
156
|
group.classList.add("radar-labels");
|
|
@@ -286,11 +170,10 @@ export class RadarChart {
|
|
|
286
170
|
text.classList.add("radar-label");
|
|
287
171
|
text.style.fontSize = "11px";
|
|
288
172
|
text.style.fill = "#475569";
|
|
289
|
-
text.style.textAnchor =
|
|
173
|
+
text.style.textAnchor = getTextAnchor(angle);
|
|
290
174
|
text.style.dominantBaseline = "middle";
|
|
291
175
|
|
|
292
|
-
|
|
293
|
-
const lines = this.wrapLabel(d.label, 12);
|
|
176
|
+
const lines = wrapLabel(d.label, 12);
|
|
294
177
|
const lineHeight = 13;
|
|
295
178
|
const offsetY = -((lines.length - 1) * lineHeight) / 2;
|
|
296
179
|
|
|
@@ -317,49 +200,6 @@ export class RadarChart {
|
|
|
317
200
|
this.svg.appendChild(group);
|
|
318
201
|
}
|
|
319
202
|
|
|
320
|
-
/**
|
|
321
|
-
* Wrap label text into multiple lines
|
|
322
|
-
* @param {string} text
|
|
323
|
-
* @param {number} maxCharsPerLine
|
|
324
|
-
* @returns {string[]}
|
|
325
|
-
*/
|
|
326
|
-
wrapLabel(text, maxCharsPerLine) {
|
|
327
|
-
if (text.length <= maxCharsPerLine) return [text];
|
|
328
|
-
|
|
329
|
-
const words = text.split(/\s+/);
|
|
330
|
-
const lines = [];
|
|
331
|
-
let currentLine = "";
|
|
332
|
-
|
|
333
|
-
for (const word of words) {
|
|
334
|
-
if (currentLine.length === 0) {
|
|
335
|
-
currentLine = word;
|
|
336
|
-
} else if (currentLine.length + 1 + word.length <= maxCharsPerLine) {
|
|
337
|
-
currentLine += " " + word;
|
|
338
|
-
} else {
|
|
339
|
-
lines.push(currentLine);
|
|
340
|
-
currentLine = word;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (currentLine.length > 0) {
|
|
345
|
-
lines.push(currentLine);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return lines.length > 0 ? lines : [text];
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Get text anchor based on angle
|
|
353
|
-
* @param {number} angle
|
|
354
|
-
* @returns {string}
|
|
355
|
-
*/
|
|
356
|
-
getTextAnchor(angle) {
|
|
357
|
-
const degrees = (angle * 180) / Math.PI + 90;
|
|
358
|
-
if (degrees > 45 && degrees < 135) return "start";
|
|
359
|
-
if (degrees > 225 && degrees < 315) return "end";
|
|
360
|
-
return "middle";
|
|
361
|
-
}
|
|
362
|
-
|
|
363
203
|
/**
|
|
364
204
|
* Truncate label text
|
|
365
205
|
* @param {string} text
|
|
@@ -371,34 +211,6 @@ export class RadarChart {
|
|
|
371
211
|
return text.slice(0, maxLength - 1) + "…";
|
|
372
212
|
}
|
|
373
213
|
|
|
374
|
-
/**
|
|
375
|
-
* Create tooltip element
|
|
376
|
-
*/
|
|
377
|
-
createTooltip() {
|
|
378
|
-
this.tooltip = document.createElement("div");
|
|
379
|
-
this.tooltip.className = "radar-tooltip";
|
|
380
|
-
this.tooltip.style.cssText = `
|
|
381
|
-
position: absolute;
|
|
382
|
-
background: #1e293b;
|
|
383
|
-
color: white;
|
|
384
|
-
padding: 8px 12px;
|
|
385
|
-
border-radius: 6px;
|
|
386
|
-
font-size: 12px;
|
|
387
|
-
pointer-events: none;
|
|
388
|
-
opacity: 0;
|
|
389
|
-
transition: opacity 0.2s;
|
|
390
|
-
z-index: 100;
|
|
391
|
-
max-width: 200px;
|
|
392
|
-
`;
|
|
393
|
-
this.container.style.position = "relative";
|
|
394
|
-
this.container.appendChild(this.tooltip);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Show tooltip
|
|
399
|
-
* @param {MouseEvent} event
|
|
400
|
-
* @param {RadarDataPoint} data
|
|
401
|
-
*/
|
|
402
214
|
showTooltip(event, data) {
|
|
403
215
|
if (!this.tooltip) return;
|
|
404
216
|
|
|
@@ -417,464 +229,15 @@ export class RadarChart {
|
|
|
417
229
|
this.tooltip.style.opacity = "1";
|
|
418
230
|
}
|
|
419
231
|
|
|
420
|
-
/**
|
|
421
|
-
* Hide tooltip
|
|
422
|
-
*/
|
|
423
232
|
hideTooltip() {
|
|
424
233
|
if (this.tooltip) {
|
|
425
234
|
this.tooltip.style.opacity = "0";
|
|
426
235
|
}
|
|
427
236
|
}
|
|
428
237
|
|
|
429
|
-
/**
|
|
430
|
-
* Update data and re-render
|
|
431
|
-
* @param {RadarDataPoint[]} newData
|
|
432
|
-
*/
|
|
433
238
|
update(newData) {
|
|
434
239
|
this.data = newData;
|
|
435
240
|
this.angleSlice = (Math.PI * 2) / this.data.length;
|
|
436
241
|
this.render();
|
|
437
242
|
}
|
|
438
243
|
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Comparison Radar Chart - displays two overlaid radar charts
|
|
442
|
-
*/
|
|
443
|
-
export class ComparisonRadarChart {
|
|
444
|
-
/**
|
|
445
|
-
* @param {Object} config
|
|
446
|
-
* @param {HTMLElement} config.container
|
|
447
|
-
* @param {RadarDataPoint[]} config.currentData
|
|
448
|
-
* @param {RadarDataPoint[]} config.targetData
|
|
449
|
-
* @param {Object} [config.options]
|
|
450
|
-
*/
|
|
451
|
-
constructor({ container, currentData, targetData, options = {} }) {
|
|
452
|
-
this.container = container;
|
|
453
|
-
this.currentData = currentData;
|
|
454
|
-
this.targetData = targetData;
|
|
455
|
-
this.options = {
|
|
456
|
-
levels: options.levels || 5,
|
|
457
|
-
currentColor: options.currentColor || "#3b82f6",
|
|
458
|
-
targetColor: options.targetColor || "#10b981",
|
|
459
|
-
showLabels: options.showLabels !== false,
|
|
460
|
-
showTooltips: options.showTooltips !== false,
|
|
461
|
-
size: options.size || 400,
|
|
462
|
-
labelOffset: options.labelOffset || 50,
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
this.center = this.options.size / 2;
|
|
466
|
-
this.radius = this.options.size / 2 - this.options.labelOffset - 20;
|
|
467
|
-
this.angleSlice = (Math.PI * 2) / this.currentData.length;
|
|
468
|
-
|
|
469
|
-
this.svg = null;
|
|
470
|
-
this.tooltip = null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Render the comparison radar chart
|
|
475
|
-
*/
|
|
476
|
-
render() {
|
|
477
|
-
this.container.innerHTML = "";
|
|
478
|
-
|
|
479
|
-
// Create SVG
|
|
480
|
-
this.svg = this.createSvg();
|
|
481
|
-
|
|
482
|
-
// Draw concentric rings
|
|
483
|
-
this.drawLevels();
|
|
484
|
-
|
|
485
|
-
// Draw axes
|
|
486
|
-
this.drawAxes();
|
|
487
|
-
|
|
488
|
-
// Draw target polygon (behind)
|
|
489
|
-
this.drawDataPolygon(this.targetData, this.options.targetColor, 0.2);
|
|
490
|
-
|
|
491
|
-
// Draw current polygon (in front)
|
|
492
|
-
this.drawDataPolygon(this.currentData, this.options.currentColor, 0.3);
|
|
493
|
-
|
|
494
|
-
// Draw target points
|
|
495
|
-
this.drawDataPoints(this.targetData, this.options.targetColor, "target");
|
|
496
|
-
|
|
497
|
-
// Draw current points
|
|
498
|
-
this.drawDataPoints(this.currentData, this.options.currentColor, "current");
|
|
499
|
-
|
|
500
|
-
// Add labels
|
|
501
|
-
if (this.options.showLabels) {
|
|
502
|
-
this.drawLabels();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Add tooltip container
|
|
506
|
-
if (this.options.showTooltips) {
|
|
507
|
-
this.createTooltip();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
this.container.appendChild(this.svg);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Create the SVG element
|
|
515
|
-
* @returns {SVGElement}
|
|
516
|
-
*/
|
|
517
|
-
createSvg() {
|
|
518
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
519
|
-
const padding = 40;
|
|
520
|
-
const totalSize = this.options.size + padding * 2;
|
|
521
|
-
svg.setAttribute("width", totalSize);
|
|
522
|
-
svg.setAttribute("height", totalSize);
|
|
523
|
-
svg.setAttribute(
|
|
524
|
-
"viewBox",
|
|
525
|
-
`${-padding} ${-padding} ${totalSize} ${totalSize}`,
|
|
526
|
-
);
|
|
527
|
-
svg.classList.add("radar-chart", "comparison-radar-chart");
|
|
528
|
-
return svg;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Draw concentric level rings
|
|
533
|
-
*/
|
|
534
|
-
drawLevels() {
|
|
535
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
536
|
-
group.classList.add("radar-levels");
|
|
537
|
-
|
|
538
|
-
for (let level = 1; level <= this.options.levels; level++) {
|
|
539
|
-
const levelRadius = (this.radius * level) / this.options.levels;
|
|
540
|
-
const points = this.currentData.map((_, i) => {
|
|
541
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
542
|
-
return {
|
|
543
|
-
x: this.center + levelRadius * Math.cos(angle),
|
|
544
|
-
y: this.center + levelRadius * Math.sin(angle),
|
|
545
|
-
};
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
const polygon = document.createElementNS(
|
|
549
|
-
"http://www.w3.org/2000/svg",
|
|
550
|
-
"polygon",
|
|
551
|
-
);
|
|
552
|
-
polygon.setAttribute(
|
|
553
|
-
"points",
|
|
554
|
-
points.map((p) => `${p.x},${p.y}`).join(" "),
|
|
555
|
-
);
|
|
556
|
-
polygon.classList.add("radar-level");
|
|
557
|
-
polygon.style.fill = "none";
|
|
558
|
-
polygon.style.stroke = "#e2e8f0";
|
|
559
|
-
polygon.style.strokeWidth = "1";
|
|
560
|
-
group.appendChild(polygon);
|
|
561
|
-
|
|
562
|
-
// Add level label
|
|
563
|
-
const labelX = this.center + 5;
|
|
564
|
-
const labelY = this.center - levelRadius + 4;
|
|
565
|
-
const label = document.createElementNS(
|
|
566
|
-
"http://www.w3.org/2000/svg",
|
|
567
|
-
"text",
|
|
568
|
-
);
|
|
569
|
-
label.setAttribute("x", labelX);
|
|
570
|
-
label.setAttribute("y", labelY);
|
|
571
|
-
label.textContent = level;
|
|
572
|
-
label.classList.add("radar-level-label");
|
|
573
|
-
label.style.fontSize = "10px";
|
|
574
|
-
label.style.fill = "#94a3b8";
|
|
575
|
-
group.appendChild(label);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
this.svg.appendChild(group);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Draw axis lines
|
|
583
|
-
*/
|
|
584
|
-
drawAxes() {
|
|
585
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
586
|
-
group.classList.add("radar-axes");
|
|
587
|
-
|
|
588
|
-
this.currentData.forEach((_, i) => {
|
|
589
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
590
|
-
const x = this.center + this.radius * Math.cos(angle);
|
|
591
|
-
const y = this.center + this.radius * Math.sin(angle);
|
|
592
|
-
|
|
593
|
-
const line = document.createElementNS(
|
|
594
|
-
"http://www.w3.org/2000/svg",
|
|
595
|
-
"line",
|
|
596
|
-
);
|
|
597
|
-
line.setAttribute("x1", this.center);
|
|
598
|
-
line.setAttribute("y1", this.center);
|
|
599
|
-
line.setAttribute("x2", x);
|
|
600
|
-
line.setAttribute("y2", y);
|
|
601
|
-
line.classList.add("radar-axis");
|
|
602
|
-
line.style.stroke = "#cbd5e1";
|
|
603
|
-
line.style.strokeWidth = "1";
|
|
604
|
-
group.appendChild(line);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
this.svg.appendChild(group);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Draw a data polygon
|
|
612
|
-
* @param {RadarDataPoint[]} data
|
|
613
|
-
* @param {string} color
|
|
614
|
-
* @param {number} opacity
|
|
615
|
-
*/
|
|
616
|
-
drawDataPolygon(data, color, opacity) {
|
|
617
|
-
const points = data.map((d, i) => {
|
|
618
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
619
|
-
const value = d.value / d.maxValue;
|
|
620
|
-
const r = this.radius * value;
|
|
621
|
-
return {
|
|
622
|
-
x: this.center + r * Math.cos(angle),
|
|
623
|
-
y: this.center + r * Math.sin(angle),
|
|
624
|
-
};
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
const polygon = document.createElementNS(
|
|
628
|
-
"http://www.w3.org/2000/svg",
|
|
629
|
-
"polygon",
|
|
630
|
-
);
|
|
631
|
-
polygon.setAttribute(
|
|
632
|
-
"points",
|
|
633
|
-
points.map((p) => `${p.x},${p.y}`).join(" "),
|
|
634
|
-
);
|
|
635
|
-
polygon.classList.add("radar-data");
|
|
636
|
-
polygon.style.fill = color;
|
|
637
|
-
polygon.style.fillOpacity = String(opacity);
|
|
638
|
-
polygon.style.stroke = color;
|
|
639
|
-
polygon.style.strokeWidth = "2";
|
|
640
|
-
|
|
641
|
-
this.svg.appendChild(polygon);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Draw data points
|
|
646
|
-
* @param {RadarDataPoint[]} data
|
|
647
|
-
* @param {string} color
|
|
648
|
-
* @param {string} type
|
|
649
|
-
*/
|
|
650
|
-
drawDataPoints(data, color, type) {
|
|
651
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
652
|
-
group.classList.add("radar-points", `radar-points-${type}`);
|
|
653
|
-
|
|
654
|
-
data.forEach((d, i) => {
|
|
655
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
656
|
-
const value = d.value / d.maxValue;
|
|
657
|
-
const r = this.radius * value;
|
|
658
|
-
const x = this.center + r * Math.cos(angle);
|
|
659
|
-
const y = this.center + r * Math.sin(angle);
|
|
660
|
-
|
|
661
|
-
const circle = document.createElementNS(
|
|
662
|
-
"http://www.w3.org/2000/svg",
|
|
663
|
-
"circle",
|
|
664
|
-
);
|
|
665
|
-
circle.setAttribute("cx", x);
|
|
666
|
-
circle.setAttribute("cy", y);
|
|
667
|
-
circle.setAttribute("r", 4);
|
|
668
|
-
circle.classList.add("radar-point");
|
|
669
|
-
circle.style.fill = color;
|
|
670
|
-
circle.style.stroke = "#fff";
|
|
671
|
-
circle.style.strokeWidth = "2";
|
|
672
|
-
circle.style.cursor = "pointer";
|
|
673
|
-
|
|
674
|
-
if (this.options.showTooltips) {
|
|
675
|
-
circle.addEventListener("mouseenter", (e) =>
|
|
676
|
-
this.showTooltip(e, d, type),
|
|
677
|
-
);
|
|
678
|
-
circle.addEventListener("mouseleave", () => this.hideTooltip());
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
group.appendChild(circle);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
this.svg.appendChild(group);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Draw axis labels
|
|
689
|
-
*/
|
|
690
|
-
drawLabels() {
|
|
691
|
-
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
692
|
-
group.classList.add("radar-labels");
|
|
693
|
-
|
|
694
|
-
this.currentData.forEach((d, i) => {
|
|
695
|
-
const targetD = this.targetData[i];
|
|
696
|
-
const angle = this.angleSlice * i - Math.PI / 2;
|
|
697
|
-
const labelRadius = this.radius + this.options.labelOffset;
|
|
698
|
-
const x = this.center + labelRadius * Math.cos(angle);
|
|
699
|
-
const y = this.center + labelRadius * Math.sin(angle);
|
|
700
|
-
|
|
701
|
-
// Check if there's a difference
|
|
702
|
-
const diff = targetD.value - d.value;
|
|
703
|
-
const hasDiff = diff !== 0;
|
|
704
|
-
|
|
705
|
-
const text = document.createElementNS(
|
|
706
|
-
"http://www.w3.org/2000/svg",
|
|
707
|
-
"text",
|
|
708
|
-
);
|
|
709
|
-
text.setAttribute("x", x);
|
|
710
|
-
text.setAttribute("y", y);
|
|
711
|
-
text.classList.add("radar-label");
|
|
712
|
-
if (hasDiff) text.classList.add("has-change");
|
|
713
|
-
text.style.fontSize = "11px";
|
|
714
|
-
text.style.fill = hasDiff
|
|
715
|
-
? diff > 0
|
|
716
|
-
? "#059669"
|
|
717
|
-
: "#dc2626"
|
|
718
|
-
: "#475569";
|
|
719
|
-
text.style.fontWeight = hasDiff ? "600" : "400";
|
|
720
|
-
text.style.textAnchor = this.getTextAnchor(angle);
|
|
721
|
-
text.style.dominantBaseline = "middle";
|
|
722
|
-
|
|
723
|
-
// Create label with change indicator
|
|
724
|
-
let labelText = d.label;
|
|
725
|
-
if (hasDiff) {
|
|
726
|
-
labelText += ` (${diff > 0 ? "+" : ""}${diff})`;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const lines = this.wrapLabel(labelText, 15);
|
|
730
|
-
const lineHeight = 13;
|
|
731
|
-
const offsetY = -((lines.length - 1) * lineHeight) / 2;
|
|
732
|
-
|
|
733
|
-
lines.forEach((line, lineIndex) => {
|
|
734
|
-
const tspan = document.createElementNS(
|
|
735
|
-
"http://www.w3.org/2000/svg",
|
|
736
|
-
"tspan",
|
|
737
|
-
);
|
|
738
|
-
tspan.setAttribute("x", x);
|
|
739
|
-
tspan.setAttribute("dy", lineIndex === 0 ? offsetY : lineHeight);
|
|
740
|
-
tspan.textContent = line;
|
|
741
|
-
text.appendChild(tspan);
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
if (this.options.showTooltips) {
|
|
745
|
-
text.style.cursor = "pointer";
|
|
746
|
-
text.addEventListener("mouseenter", (e) =>
|
|
747
|
-
this.showComparisonTooltip(e, d, targetD),
|
|
748
|
-
);
|
|
749
|
-
text.addEventListener("mouseleave", () => this.hideTooltip());
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
group.appendChild(text);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
this.svg.appendChild(group);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Wrap label text into multiple lines
|
|
760
|
-
*/
|
|
761
|
-
wrapLabel(text, maxCharsPerLine) {
|
|
762
|
-
if (text.length <= maxCharsPerLine) return [text];
|
|
763
|
-
|
|
764
|
-
const words = text.split(/\s+/);
|
|
765
|
-
const lines = [];
|
|
766
|
-
let currentLine = "";
|
|
767
|
-
|
|
768
|
-
for (const word of words) {
|
|
769
|
-
if (currentLine.length === 0) {
|
|
770
|
-
currentLine = word;
|
|
771
|
-
} else if (currentLine.length + 1 + word.length <= maxCharsPerLine) {
|
|
772
|
-
currentLine += " " + word;
|
|
773
|
-
} else {
|
|
774
|
-
lines.push(currentLine);
|
|
775
|
-
currentLine = word;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (currentLine.length > 0) {
|
|
780
|
-
lines.push(currentLine);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
return lines.length > 0 ? lines : [text];
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
* Get text anchor based on angle
|
|
788
|
-
*/
|
|
789
|
-
getTextAnchor(angle) {
|
|
790
|
-
const degrees = (angle * 180) / Math.PI + 90;
|
|
791
|
-
if (degrees > 45 && degrees < 135) return "start";
|
|
792
|
-
if (degrees > 225 && degrees < 315) return "end";
|
|
793
|
-
return "middle";
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Create tooltip element
|
|
798
|
-
*/
|
|
799
|
-
createTooltip() {
|
|
800
|
-
this.tooltip = document.createElement("div");
|
|
801
|
-
this.tooltip.className = "radar-tooltip";
|
|
802
|
-
this.tooltip.style.cssText = `
|
|
803
|
-
position: absolute;
|
|
804
|
-
background: #1e293b;
|
|
805
|
-
color: white;
|
|
806
|
-
padding: 8px 12px;
|
|
807
|
-
border-radius: 6px;
|
|
808
|
-
font-size: 12px;
|
|
809
|
-
pointer-events: none;
|
|
810
|
-
opacity: 0;
|
|
811
|
-
transition: opacity 0.2s;
|
|
812
|
-
z-index: 100;
|
|
813
|
-
max-width: 250px;
|
|
814
|
-
`;
|
|
815
|
-
this.container.style.position = "relative";
|
|
816
|
-
this.container.appendChild(this.tooltip);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Show tooltip for a single data point
|
|
821
|
-
*/
|
|
822
|
-
showTooltip(event, data, type) {
|
|
823
|
-
if (!this.tooltip) return;
|
|
824
|
-
|
|
825
|
-
const rect = this.container.getBoundingClientRect();
|
|
826
|
-
const x = event.clientX - rect.left;
|
|
827
|
-
const y = event.clientY - rect.top;
|
|
828
|
-
|
|
829
|
-
const typeLabel = type === "current" ? "Current" : "Target";
|
|
830
|
-
|
|
831
|
-
this.tooltip.innerHTML = `
|
|
832
|
-
<strong>${escapeHtml(data.label)}</strong><br>
|
|
833
|
-
${typeLabel}: ${data.value}/${data.maxValue}
|
|
834
|
-
${data.description ? `<br><small>${escapeHtml(data.description)}</small>` : ""}
|
|
835
|
-
`;
|
|
836
|
-
|
|
837
|
-
this.tooltip.style.left = `${x + 10}px`;
|
|
838
|
-
this.tooltip.style.top = `${y - 10}px`;
|
|
839
|
-
this.tooltip.style.opacity = "1";
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Show comparison tooltip
|
|
844
|
-
*/
|
|
845
|
-
showComparisonTooltip(event, currentData, targetData) {
|
|
846
|
-
if (!this.tooltip) return;
|
|
847
|
-
|
|
848
|
-
const rect = this.container.getBoundingClientRect();
|
|
849
|
-
const x = event.clientX - rect.left;
|
|
850
|
-
const y = event.clientY - rect.top;
|
|
851
|
-
|
|
852
|
-
const diff = targetData.value - currentData.value;
|
|
853
|
-
const diffText =
|
|
854
|
-
diff > 0
|
|
855
|
-
? `<span style="color: #10b981">↑ ${diff} level${diff > 1 ? "s" : ""}</span>`
|
|
856
|
-
: diff < 0
|
|
857
|
-
? `<span style="color: #ef4444">↓ ${Math.abs(diff)} level${Math.abs(diff) > 1 ? "s" : ""}</span>`
|
|
858
|
-
: "<span style='color: #94a3b8'>No change</span>";
|
|
859
|
-
|
|
860
|
-
this.tooltip.innerHTML = `
|
|
861
|
-
<strong>${escapeHtml(currentData.label)}</strong><br>
|
|
862
|
-
Current: ${currentData.value}/${currentData.maxValue}<br>
|
|
863
|
-
Target: ${targetData.value}/${targetData.maxValue}<br>
|
|
864
|
-
${diffText}
|
|
865
|
-
`;
|
|
866
|
-
|
|
867
|
-
this.tooltip.style.left = `${x + 10}px`;
|
|
868
|
-
this.tooltip.style.top = `${y - 10}px`;
|
|
869
|
-
this.tooltip.style.opacity = "1";
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Hide tooltip
|
|
874
|
-
*/
|
|
875
|
-
hideTooltip() {
|
|
876
|
-
if (this.tooltip) {
|
|
877
|
-
this.tooltip.style.opacity = "0";
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|