@forwardimpact/pathway 0.25.12 → 0.25.20

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/src/lib/radar.js CHANGED
@@ -2,19 +2,18 @@
2
2
  * Radar chart visualization using SVG
3
3
  */
4
4
 
5
- /**
6
- * Escape HTML special characters to prevent XSS
7
- * @param {string} text
8
- * @returns {string}
9
- */
10
- function escapeHtml(text) {
11
- return text
12
- .replace(/&/g, "&")
13
- .replace(/</g, "&lt;")
14
- .replace(/>/g, "&gt;")
15
- .replace(/"/g, "&quot;")
16
- .replace(/'/g, "&#039;");
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
- // Create SVG
73
- this.svg = this.createSvg();
68
+ this.svg = createRadarSvg(this.options.size);
74
69
 
75
- // Draw concentric rings
76
- this.drawLevels();
70
+ const sharedParams = {
71
+ radius: this.radius,
72
+ center: this.center,
73
+ angleSlice: this.angleSlice,
74
+ dataLength: this.data.length,
75
+ };
77
76
 
78
- // Draw axes
79
- this.drawAxes();
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
- // Add labels
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.createTooltip();
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 = this.getTextAnchor(angle);
173
+ text.style.textAnchor = getTextAnchor(angle);
290
174
  text.style.dominantBaseline = "middle";
291
175
 
292
- // Split long labels into multiple lines
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
- }