kyd-shared-badge 0.3.100 → 0.3.101
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/package.json
CHANGED
|
@@ -81,7 +81,8 @@ export default function RoleOverviewCard({
|
|
|
81
81
|
<div className="relative" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 360 }}>
|
|
82
82
|
<GaugeComponent
|
|
83
83
|
type="semicircle"
|
|
84
|
-
style={{ width: '100%', height: '100%' }}
|
|
84
|
+
style={{ width: 'calc(100% - 16px)', height: '100%', marginLeft: 8, marginRight: 8 }}
|
|
85
|
+
marginInPercent={{ top: 0.08, bottom: 0.0, left: 0.1, right: 0.1 }}
|
|
85
86
|
value={pct}
|
|
86
87
|
minValue={0}
|
|
87
88
|
maxValue={100}
|
|
@@ -13,6 +13,8 @@ type SkillsRadarPoint = {
|
|
|
13
13
|
self_reported?: number;
|
|
14
14
|
certified?: number;
|
|
15
15
|
experience?: number; // 0-100 saturation driver
|
|
16
|
+
// Total evidence count (backend computed) per category
|
|
17
|
+
evidence_count_total?: number;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type HoverTooltipState = {
|
|
@@ -50,7 +52,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[] }>; headless?: boolean }) {
|
|
55
|
+
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[]; evidenceCount?: number }>; headless?: boolean }) {
|
|
54
56
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
55
57
|
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
56
58
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -85,7 +87,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
85
87
|
};
|
|
86
88
|
const hideLegendTooltip = () => setLegendTooltip(null);
|
|
87
89
|
|
|
88
|
-
// ratio drives size
|
|
90
|
+
// ratio drives size by default; prefer backend evidence_count_total when present
|
|
89
91
|
const bubbles = useMemo(() => {
|
|
90
92
|
const seriesAvg = (d: SkillsRadarPoint): number => {
|
|
91
93
|
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
@@ -94,10 +96,19 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
94
96
|
return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
|
|
95
97
|
};
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
// Prefer evidence_count_total across categories if available; fallback to ratio
|
|
100
|
+
const evidenceTotals = (skillsCategoryRadar || []).map((d) => Number((d as any).evidence_count_total || 0));
|
|
101
|
+
const evidenceAvailable = evidenceTotals.some((v) => v > 0);
|
|
102
|
+
|
|
103
|
+
const maxValue = evidenceAvailable
|
|
104
|
+
? Math.max(1, ...evidenceTotals)
|
|
105
|
+
: Math.max(1, ...skillsRadarLimited.map(seriesAvg));
|
|
98
106
|
|
|
99
107
|
return skillsRadarLimited.map((d) => {
|
|
100
|
-
const
|
|
108
|
+
const ratio = seriesAvg(d);
|
|
109
|
+
const evidenceCountTotal = Number((d as any).evidence_count_total || 0);
|
|
110
|
+
const valueRaw = evidenceAvailable ? evidenceCountTotal : ratio;
|
|
111
|
+
const value = Math.max(0, Number(valueRaw));
|
|
101
112
|
const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
|
|
102
113
|
const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
|
|
103
114
|
const color = 'var(--content-card-background)';
|
|
@@ -105,11 +116,13 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
105
116
|
label: d.axis,
|
|
106
117
|
value: size,
|
|
107
118
|
color,
|
|
108
|
-
tooltip:
|
|
109
|
-
|
|
119
|
+
tooltip: evidenceAvailable
|
|
120
|
+
? `${d.axis}\nEvidence: ${value} sources\nExperience: ${experience}`
|
|
121
|
+
: `${d.axis}\nRatio: ${ratio}\nExperience: ${experience}`,
|
|
122
|
+
data: { ratio, experience, evidence: value }
|
|
110
123
|
};
|
|
111
124
|
});
|
|
112
|
-
}, [skillsRadarLimited]);
|
|
125
|
+
}, [skillsRadarLimited, skillsCategoryRadar]);
|
|
113
126
|
|
|
114
127
|
const bubbleData = useMemo(() => {
|
|
115
128
|
return bubbles.map((b) => ({
|
|
@@ -245,39 +258,70 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
245
258
|
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: entry ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
|
|
246
259
|
<span className="shrink-0 opacity-70 ">{idx + (isLeft ? 1 : 6)}.</span>
|
|
247
260
|
{entry && typeof entry !== 'string' ? (
|
|
248
|
-
<span className="truncate" title={entry.label}>
|
|
261
|
+
<span className="truncate" title={entry.label}>
|
|
262
|
+
{entry.label}
|
|
263
|
+
{/* Evidence count bullet */}
|
|
264
|
+
{(() => {
|
|
265
|
+
const meta = skillsMeta?.[entry.label];
|
|
266
|
+
const count = Number((meta as any)?.evidenceCount || 0);
|
|
267
|
+
return count > 0 ? (
|
|
268
|
+
<>
|
|
269
|
+
{' '}<span className="opacity-60">•</span>{' '}
|
|
270
|
+
<span
|
|
271
|
+
className="underline decoration-dotted underline-offset-2 cursor-help opacity-80"
|
|
272
|
+
onMouseEnter={(e) =>
|
|
273
|
+
showLegendTooltipAt(
|
|
274
|
+
e.currentTarget,
|
|
275
|
+
'Evidence count',
|
|
276
|
+
'Total number of sources across observed, certified, and self-reported evidence for this skill.'
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
onMouseLeave={hideLegendTooltip}
|
|
280
|
+
>
|
|
281
|
+
{count}
|
|
282
|
+
</span>
|
|
283
|
+
</>
|
|
284
|
+
) : null;
|
|
285
|
+
})()}
|
|
286
|
+
</span>
|
|
249
287
|
) : (
|
|
250
288
|
<span className="truncate">{typeof entry === 'string' ? entry : '\u00A0'}</span>
|
|
251
289
|
)}
|
|
252
290
|
</div>
|
|
253
291
|
<span className="text-xs text-[var(--text-secondary)] flex flex-wrap items-center gap-1">
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
292
|
+
{entry && typeof entry !== 'string' ? (
|
|
293
|
+
<>
|
|
294
|
+
<span
|
|
295
|
+
className="underline decoration-dotted underline-offset-2 cursor-help"
|
|
296
|
+
onMouseEnter={(e) => showLegendTooltipAt(e.currentTarget, 'Sources', 'The source where we observed this skill.')}
|
|
297
|
+
onMouseLeave={hideLegendTooltip}
|
|
298
|
+
>
|
|
299
|
+
Sources
|
|
300
|
+
</span>:
|
|
301
|
+
{Array.isArray((entry as any).sources) && (entry as any).sources.length > 0 ? (
|
|
302
|
+
(() => {
|
|
303
|
+
const sourceProviders: string[] = ((entry as any).sources as string[]).map((src: string) => {
|
|
304
|
+
const str = String(src);
|
|
305
|
+
let provider = str.split(':')[0] || '';
|
|
306
|
+
if (!provider || provider === str) {
|
|
307
|
+
// If split(':')[0] didn't find a delimiter or provider (i.e., no ':'), try split('.')
|
|
308
|
+
provider = str.split('.')[0] || '';
|
|
309
|
+
}
|
|
310
|
+
return provider.toLowerCase();
|
|
311
|
+
});
|
|
312
|
+
const uniqueProviders = Array.from(new Set<string>(sourceProviders));
|
|
313
|
+
const filteredProviders = uniqueProviders.filter((provider) =>
|
|
314
|
+
providers.includes(provider.toLowerCase())
|
|
315
|
+
);
|
|
316
|
+
return filteredProviders.map((provider) => (
|
|
317
|
+
<ProviderIcon key={provider} name={provider} />
|
|
318
|
+
));
|
|
319
|
+
})()
|
|
320
|
+
) : null}
|
|
321
|
+
</>
|
|
322
|
+
) : (
|
|
323
|
+
<span className="opacity-0 whitespace-nowrap">'\u00A0'</span>
|
|
324
|
+
)}
|
|
281
325
|
</span>
|
|
282
326
|
</div>
|
|
283
327
|
{entry && typeof entry !== 'string' ? (
|
|
@@ -348,7 +392,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
348
392
|
>
|
|
349
393
|
<div className="flex items-center gap-2">
|
|
350
394
|
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
|
|
351
|
-
<span>Size =
|
|
395
|
+
<span>Size = evidence count per category</span>
|
|
352
396
|
</div>
|
|
353
397
|
<div className="flex items-center gap-2 mt-1">
|
|
354
398
|
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
|
package/src/types.ts
CHANGED
|
@@ -397,11 +397,21 @@ export interface GraphInsightsPayload {
|
|
|
397
397
|
certified?: number; // 0-100
|
|
398
398
|
// New: experience metric (0-100) for color saturation
|
|
399
399
|
experience?: number;
|
|
400
|
+
// New: total evidence count across all skills in this category
|
|
401
|
+
evidence_count_total?: number;
|
|
400
402
|
}>;
|
|
401
403
|
// New: mapping of category -> list of skills contributing to that category
|
|
402
404
|
skillsByCategory?: Record<string, string[]>;
|
|
403
405
|
// New: per-skill metadata used by UI (e.g., presence label, experience years)
|
|
404
|
-
skillsMeta?: Record<string, {
|
|
406
|
+
skillsMeta?: Record<string, {
|
|
407
|
+
presence?: 'certified' | 'observed' | 'self-reported';
|
|
408
|
+
// New: list of presence types for multi-dot rendering
|
|
409
|
+
presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>;
|
|
410
|
+
years?: number;
|
|
411
|
+
sources?: string[];
|
|
412
|
+
// New: total number of evidence sources across observed/self-reported/certified
|
|
413
|
+
evidenceCount?: number;
|
|
414
|
+
}>;
|
|
405
415
|
// New: Flattened list of business rule selections (for appendix)
|
|
406
416
|
business_rules_all?: Array<{
|
|
407
417
|
provider: string;
|