codarch 0.0.0-stage → 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.
package/client.js ADDED
@@ -0,0 +1,915 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ var data = JSON.parse(document.getElementById("codarch-data").textContent);
5
+ var F = data.files;
6
+ var N = F.length;
7
+ var METRIC_OFFSET = 8;
8
+
9
+ function pathOf(index) {
10
+ var row = F[index];
11
+ return row[0] < 0 ? row[1] : data.dirs[row[0]] + "/" + row[1];
12
+ }
13
+
14
+ var RISK_STOPS = [
15
+ [0, "#ebe7df"],
16
+ [0.35, "#e9cd98"],
17
+ [0.65, "#d68a54"],
18
+ [1, "#a42e2c"]
19
+ ];
20
+ var METRIC_STOPS = [
21
+ [0, "#ebe7df"],
22
+ [0.5, "#b9c9cf"],
23
+ [1, "#355b6b"]
24
+ ];
25
+ var RISK_GRADIENT =
26
+ "linear-gradient(90deg, #ebe7df 0%, #e9cd98 35%, #d68a54 65%, #a42e2c 100%)";
27
+ var METRIC_GRADIENT =
28
+ "linear-gradient(90deg, #ebe7df 0%, #b9c9cf 50%, #355b6b 100%)";
29
+
30
+ var metricIndex = new Map();
31
+ for (var mi = 0; mi < data.metrics.length; mi++) {
32
+ metricIndex.set(data.metrics[mi].key, mi);
33
+ }
34
+
35
+ var lens = data.lens || "risk";
36
+ var metricKey =
37
+ data.metric && metricIndex.has(data.metric)
38
+ ? data.metric
39
+ : data.metrics.length > 0
40
+ ? data.metrics[0].key
41
+ : null;
42
+ var selected = null;
43
+ var query = "";
44
+ var firstHit = -1;
45
+
46
+ var svg = document.getElementById("map");
47
+ var edgesEl = document.getElementById("edges");
48
+ var tooltip = document.getElementById("tooltip");
49
+ var inspector = document.getElementById("inspector");
50
+ var crumbs = document.getElementById("crumbs");
51
+ var legendEl = document.getElementById("legend");
52
+ var searchInput = document.getElementById("q");
53
+ var metricPicker = document.getElementById("metric-pick");
54
+ var metricSelect = document.getElementById("metric-select");
55
+
56
+ var cellByIndex = new Array(N);
57
+ var rects = new Array(N);
58
+ var texts = new Array(N);
59
+ for (var ti = 0; ti < N; ti++) texts[ti] = [];
60
+ var cells = Array.prototype.slice.call(
61
+ document.querySelectorAll("#map rect[data-i]")
62
+ );
63
+ for (var ci = 0; ci < cells.length; ci++) {
64
+ var cell = cells[ci];
65
+ var index = Number(cell.dataset.i);
66
+ cellByIndex[index] = cell;
67
+ rects[index] = cell;
68
+ var sibling = cell.nextElementSibling;
69
+ while (
70
+ sibling &&
71
+ sibling.tagName === "text" &&
72
+ (sibling.classList.contains("a") || sibling.classList.contains("b"))
73
+ ) {
74
+ texts[index].push(sibling);
75
+ sibling = sibling.nextElementSibling;
76
+ }
77
+ }
78
+ var folderEls = Array.prototype.slice.call(document.querySelectorAll(".folder"));
79
+ var folderPathCache = new Map();
80
+
81
+ function folderPath(el) {
82
+ var known = folderPathCache.get(el);
83
+ if (known !== undefined) return known;
84
+ var parts = [];
85
+ var cursor = el;
86
+ while (cursor) {
87
+ if (cursor.classList && cursor.classList.contains("folder")) {
88
+ parts.unshift(cursor.dataset.nm || "");
89
+ }
90
+ cursor = cursor.parentElement;
91
+ }
92
+ var path = parts.filter(Boolean).join("/");
93
+ folderPathCache.set(el, path);
94
+ return path;
95
+ }
96
+
97
+ /* ------------------------------------------------------------------ *
98
+ * Dependencias
99
+ * ------------------------------------------------------------------ */
100
+ var out = new Array(N);
101
+ var inc = new Array(N);
102
+ for (var ai = 0; ai < N; ai++) {
103
+ out[ai] = [];
104
+ inc[ai] = [];
105
+ }
106
+ var edgeSet = new Set();
107
+ for (var ei = 0; ei < data.edges.length; ei += 2) {
108
+ var from = data.edges[ei];
109
+ var to = data.edges[ei + 1];
110
+ out[from].push(to);
111
+ inc[to].push(from);
112
+ edgeSet.add(from * N + to);
113
+ }
114
+ var cycleSet = new Set();
115
+ for (var cci = 0; cci < data.cycles.length; cci += 2) {
116
+ cycleSet.add(data.cycles[cci] * N + data.cycles[cci + 1]);
117
+ }
118
+
119
+ function isCycle(a, b) {
120
+ return cycleSet.has(a * N + b) || edgeSet.has(b * N + a);
121
+ }
122
+
123
+ /* ------------------------------------------------------------------ *
124
+ * Color
125
+ * ------------------------------------------------------------------ */
126
+ function clamp01(value) {
127
+ if (!isFinite(value)) return 0;
128
+ return Math.max(0, Math.min(1, value));
129
+ }
130
+
131
+ function hexToRgb(hex) {
132
+ var value = hex.replace("#", "");
133
+ return [
134
+ parseInt(value.slice(0, 2), 16),
135
+ parseInt(value.slice(2, 4), 16),
136
+ parseInt(value.slice(4, 6), 16)
137
+ ];
138
+ }
139
+
140
+ function rgbToHex(rgb) {
141
+ return (
142
+ "#" +
143
+ rgb
144
+ .map(function (channel) {
145
+ return channel.toString(16).padStart(2, "0");
146
+ })
147
+ .join("")
148
+ );
149
+ }
150
+
151
+ function mix(a, b, t) {
152
+ var from = hexToRgb(a);
153
+ var to = hexToRgb(b);
154
+ return rgbToHex([
155
+ Math.round(from[0] + (to[0] - from[0]) * t),
156
+ Math.round(from[1] + (to[1] - from[1]) * t),
157
+ Math.round(from[2] + (to[2] - from[2]) * t)
158
+ ]);
159
+ }
160
+
161
+ function ramp(stops, t) {
162
+ var value = clamp01(t);
163
+ for (var i = 0; i < stops.length - 1; i++) {
164
+ var start = stops[i];
165
+ var end = stops[i + 1];
166
+ if (value <= end[0]) {
167
+ var span = end[0] - start[0] || 1;
168
+ return mix(start[1], end[1], (value - start[0]) / span);
169
+ }
170
+ }
171
+ return stops[stops.length - 1][1];
172
+ }
173
+
174
+ function riskColor(risk) {
175
+ return ramp(RISK_STOPS, risk);
176
+ }
177
+
178
+ function metricColor(value, metric) {
179
+ if (value === null || value === undefined) return METRIC_STOPS[0][1];
180
+ var t;
181
+ if (metric.mode === "log") {
182
+ var span = Math.max(1e-9, metric.max - metric.min);
183
+ t = Math.log1p(Math.max(0, value - metric.min)) / Math.log1p(span);
184
+ } else {
185
+ var linear = metric.max - metric.min;
186
+ t = linear > 0 ? (value - metric.min) / linear : 0;
187
+ }
188
+ return ramp(METRIC_STOPS, t);
189
+ }
190
+
191
+ function inkFor(color) {
192
+ var rgb = hexToRgb(color);
193
+ var luminance = (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
194
+ return luminance > 0.52 ? "#1a1a1a" : "#f7f5f1";
195
+ }
196
+
197
+ function metricFor(key) {
198
+ var index = metricIndex.get(key);
199
+ return index === undefined ? undefined : data.metrics[index];
200
+ }
201
+
202
+ function metricValue(index, key) {
203
+ var position = metricIndex.get(key);
204
+ if (position === undefined) return null;
205
+ return F[index][METRIC_OFFSET + position];
206
+ }
207
+
208
+ function sectionFor(index) {
209
+ return (
210
+ data.sections[F[index][2]] || {
211
+ key: "",
212
+ color: "#ddd8cf",
213
+ files: 0,
214
+ area: 0
215
+ }
216
+ );
217
+ }
218
+
219
+ function colorFor(index) {
220
+ if (lens === "structure") return sectionFor(index).color;
221
+ if (lens === "risk") return riskColor(F[index][5]);
222
+ var metric = metricFor(metricKey);
223
+ if (!metric) return riskColor(F[index][5]);
224
+ return metricColor(metricValue(index, metricKey), metric);
225
+ }
226
+
227
+ /* ------------------------------------------------------------------ *
228
+ * Formato y escape
229
+ * ------------------------------------------------------------------ */
230
+ var HTML_ESCAPES = {
231
+ "&": "&amp;",
232
+ "<": "&lt;",
233
+ ">": "&gt;",
234
+ '"': "&quot;",
235
+ "'": "&#39;"
236
+ };
237
+
238
+ function esc(value) {
239
+ return String(value).replace(/[&<>"']/g, function (character) {
240
+ return HTML_ESCAPES[character];
241
+ });
242
+ }
243
+
244
+ function fmt(value) {
245
+ if (value === null || value === undefined || !isFinite(value)) return "—";
246
+ var rounded = Math.round(value * 100) / 100;
247
+ var parts = String(rounded).split(".");
248
+ var integer = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ".");
249
+ return parts.length > 1 && parts[1] ? integer + "," + parts[1] : integer;
250
+ }
251
+
252
+ function pct(risk) {
253
+ return Math.round(risk * 100);
254
+ }
255
+
256
+ function nameOf(path) {
257
+ var slash = path.lastIndexOf("/");
258
+ return slash >= 0 ? path.slice(slash + 1) : path;
259
+ }
260
+
261
+ function inFolder(path, folder) {
262
+ return path === folder || path.indexOf(folder + "/") === 0;
263
+ }
264
+
265
+ /* ------------------------------------------------------------------ *
266
+ * Lentes, leyenda y selector de métrica
267
+ * ------------------------------------------------------------------ */
268
+ function applyLens() {
269
+ for (var i = 0; i < N; i++) {
270
+ var rect = rects[i];
271
+ if (!rect) continue;
272
+ var color = colorFor(i);
273
+ rect.setAttribute("fill", color);
274
+ var ink = inkFor(color);
275
+ var labels = texts[i];
276
+ for (var t = 0; t < labels.length; t++) {
277
+ labels[t].setAttribute("fill", ink);
278
+ }
279
+ }
280
+ renderLegend();
281
+ updateMetricPicker();
282
+ if (selected) renderInspector();
283
+ }
284
+
285
+ function setLens(next) {
286
+ if (next === "metric" && !metricKey) return;
287
+ lens = next;
288
+ var buttons = document.querySelectorAll(".lens");
289
+ for (var i = 0; i < buttons.length; i++) {
290
+ buttons[i].setAttribute("aria-selected", String(buttons[i].dataset.lens === lens));
291
+ }
292
+ applyLens();
293
+ }
294
+
295
+ function updateMetricPicker() {
296
+ if (!metricPicker) return;
297
+ metricPicker.hidden = lens !== "metric";
298
+ if (metricSelect && metricKey) metricSelect.value = metricKey;
299
+ }
300
+
301
+ function renderLegend() {
302
+ if (lens === "structure") {
303
+ var swatches = data.sections
304
+ .slice(0, 6)
305
+ .map(function (section) {
306
+ return (
307
+ '<span><i style="background:' +
308
+ section.color +
309
+ '"></i>' +
310
+ esc(section.key) +
311
+ "</span>"
312
+ );
313
+ })
314
+ .join("");
315
+ legendEl.innerHTML =
316
+ '<span>Carpeta</span><span class="sws">' + swatches + "</span>";
317
+ return;
318
+ }
319
+
320
+ if (lens === "metric") {
321
+ var metric = metricFor(metricKey);
322
+ if (metric) {
323
+ var direction =
324
+ metric.dir === -1
325
+ ? " · menos es mejor"
326
+ : metric.dir === 1
327
+ ? " · más es mejor"
328
+ : "";
329
+ legendEl.innerHTML =
330
+ "<span>" +
331
+ esc(metric.title) +
332
+ direction +
333
+ '</span><span class="mono">' +
334
+ esc(fmt(metric.min)) +
335
+ '</span><span class="bar" style="background:' +
336
+ METRIC_GRADIENT +
337
+ '"></span><span class="mono">' +
338
+ esc(fmt(metric.max)) +
339
+ "</span>";
340
+ return;
341
+ }
342
+ }
343
+
344
+ legendEl.innerHTML =
345
+ '<span>Riesgo (0–100)</span><span class="mono">0</span><span class="bar" style="background:' +
346
+ RISK_GRADIENT +
347
+ '"></span><span class="mono">100</span>';
348
+ }
349
+
350
+ /* ------------------------------------------------------------------ *
351
+ * Búsqueda
352
+ * ------------------------------------------------------------------ */
353
+ function syncText(index) {
354
+ var cell = cellByIndex[index];
355
+ if (!cell) return;
356
+ var dim = cell.classList.contains("dim") || cell.classList.contains("outside");
357
+ var labels = texts[index];
358
+ for (var i = 0; i < labels.length; i++) {
359
+ labels[i].style.opacity = dim ? "0.12" : "";
360
+ }
361
+ }
362
+
363
+ function applySearch() {
364
+ var needle = query.trim().toLowerCase();
365
+ var hits = 0;
366
+ firstHit = -1;
367
+ var bestArea = -Infinity;
368
+
369
+ for (var i = 0; i < N; i++) {
370
+ var cell = cellByIndex[i];
371
+ if (!cell) continue;
372
+ var hit = Boolean(needle) && pathOf(i).toLowerCase().indexOf(needle) !== -1;
373
+ if (hit) {
374
+ hits++;
375
+ if (F[i][4] > bestArea) {
376
+ bestArea = F[i][4];
377
+ firstHit = i;
378
+ }
379
+ }
380
+ cell.classList.toggle("hit", hit);
381
+ cell.classList.toggle("dim", Boolean(needle) && !hit);
382
+ syncText(i);
383
+ }
384
+
385
+ for (var f = 0; f < folderEls.length; f++) {
386
+ var folder = folderEls[f];
387
+ var folderMatch =
388
+ Boolean(needle) &&
389
+ folderPath(folder).toLowerCase().indexOf(needle) !== -1;
390
+ var hasHit =
391
+ Boolean(needle) && folder.querySelector("rect[data-i].hit") !== null;
392
+ folder.classList.toggle("dim", Boolean(needle) && !folderMatch && !hasHit);
393
+ folder.classList.toggle("hit-folder", folderMatch);
394
+ }
395
+
396
+ var status = document.getElementById("search-status");
397
+ if (status) {
398
+ status.textContent = needle
399
+ ? hits + (hits === 1 ? " coincidencia" : " coincidencias")
400
+ : "";
401
+ }
402
+ }
403
+
404
+ /* ------------------------------------------------------------------ *
405
+ * Selección, aristas, inspector
406
+ * ------------------------------------------------------------------ */
407
+ function selectFile(index) {
408
+ selected = { kind: "file", index: index };
409
+ afterSelectionChange();
410
+ }
411
+
412
+ function selectFolder(path) {
413
+ selected = { kind: "folder", path: path };
414
+ afterSelectionChange();
415
+ }
416
+
417
+ function clearSelection() {
418
+ selected = null;
419
+ afterSelectionChange();
420
+ }
421
+
422
+ function afterSelectionChange() {
423
+ applySelection();
424
+ renderInspector();
425
+ drawEdges();
426
+ updateCrumbs();
427
+ inspector.scrollTop = 0;
428
+ }
429
+
430
+ function applySelection() {
431
+ var active = selected && selected.kind === "file" ? selected.index : -1;
432
+ var related = new Set();
433
+ if (active >= 0) {
434
+ related.add(active);
435
+ for (var oi = 0; oi < Math.min(8, out[active].length); oi++) {
436
+ related.add(out[active][oi]);
437
+ }
438
+ for (var ii = 0; ii < Math.min(8, inc[active].length); ii++) {
439
+ related.add(inc[active][ii]);
440
+ }
441
+ }
442
+
443
+ for (var i = 0; i < N; i++) {
444
+ var cell = cellByIndex[i];
445
+ if (!cell) continue;
446
+ cell.classList.toggle("selected", i === active);
447
+ cell.classList.toggle("related", active >= 0 && i !== active && related.has(i));
448
+ cell.classList.toggle(
449
+ "outside",
450
+ Boolean(selected && selected.kind === "folder" && !inFolder(pathOf(i), selected.path))
451
+ );
452
+ syncText(i);
453
+ }
454
+
455
+ for (var f = 0; f < folderEls.length; f++) {
456
+ var folder = folderEls[f];
457
+ var path = folderPath(folder);
458
+ var focused =
459
+ Boolean(selected && selected.kind === "folder" && path === selected.path);
460
+ var inside =
461
+ Boolean(selected && selected.kind === "folder") &&
462
+ (path === selected.path || path.indexOf(selected.path + "/") === 0);
463
+ folder.classList.toggle("focused", focused);
464
+ folder.classList.toggle(
465
+ "outside",
466
+ Boolean(selected && selected.kind === "folder") && !inside
467
+ );
468
+ }
469
+ }
470
+
471
+ function drawEdges() {
472
+ while (edgesEl.firstChild) edgesEl.removeChild(edgesEl.firstChild);
473
+ if (!selected || selected.kind !== "file") return;
474
+
475
+ var source = rects[selected.index];
476
+ if (!source) return;
477
+ var sx = Number(source.getAttribute("x")) + Number(source.getAttribute("width")) / 2;
478
+ var sy = Number(source.getAttribute("y")) + Number(source.getAttribute("height")) / 2;
479
+ var namespace = "http://www.w3.org/2000/svg";
480
+
481
+ function curve(other, incoming) {
482
+ var target = rects[other];
483
+ if (!target) return;
484
+ var tx =
485
+ Number(target.getAttribute("x")) + Number(target.getAttribute("width")) / 2;
486
+ var ty =
487
+ Number(target.getAttribute("y")) + Number(target.getAttribute("height")) / 2;
488
+ var startX = incoming ? tx : sx;
489
+ var startY = incoming ? ty : sy;
490
+ var endX = incoming ? sx : tx;
491
+ var endY = incoming ? sy : ty;
492
+ var middleX = (startX + endX) / 2;
493
+ var middleY =
494
+ (startY + endY) / 2 - Math.min(70, Math.abs(endX - startX) * 0.18 + 22);
495
+ var bad = incoming ? isCycle(other, selected.index) : isCycle(selected.index, other);
496
+
497
+ var path = document.createElementNS(namespace, "path");
498
+ path.setAttribute(
499
+ "d",
500
+ "M " +
501
+ startX +
502
+ " " +
503
+ startY +
504
+ " Q " +
505
+ middleX +
506
+ " " +
507
+ middleY +
508
+ " " +
509
+ endX +
510
+ " " +
511
+ endY
512
+ );
513
+ path.setAttribute("fill", "none");
514
+ path.setAttribute("stroke", bad ? "#a42e2c" : "#1a1a1a");
515
+ path.setAttribute("stroke-width", bad ? "1.8" : "1.1");
516
+ path.setAttribute("stroke-opacity", bad ? "0.95" : "0.32");
517
+ path.setAttribute("vector-effect", "non-scaling-stroke");
518
+ if (incoming) path.setAttribute("stroke-dasharray", "3 3");
519
+ edgesEl.appendChild(path);
520
+
521
+ var circle = document.createElementNS(namespace, "circle");
522
+ circle.setAttribute("cx", endX);
523
+ circle.setAttribute("cy", endY);
524
+ circle.setAttribute("r", bad ? "3" : "2.2");
525
+ circle.setAttribute("fill", bad ? "#a42e2c" : "#1a1a1a");
526
+ circle.setAttribute("fill-opacity", bad ? "1" : "0.45");
527
+ edgesEl.appendChild(circle);
528
+ }
529
+
530
+ for (var i = 0; i < Math.min(8, out[selected.index].length); i++) {
531
+ curve(out[selected.index][i], false);
532
+ }
533
+ for (var j = 0; j < Math.min(8, inc[selected.index].length); j++) {
534
+ curve(inc[selected.index][j], true);
535
+ }
536
+ }
537
+
538
+ function dependencyList(indices, emptyText) {
539
+ if (!indices.length) return '<p class="empty">' + emptyText + "</p>";
540
+ return (
541
+ '<ul class="deps">' +
542
+ indices
543
+ .slice(0, 8)
544
+ .map(function (index) {
545
+ return (
546
+ '<li data-file="' +
547
+ index +
548
+ '"><i class="sw" style="background:' +
549
+ colorFor(index) +
550
+ '"></i><span>' +
551
+ esc(pathOf(index)) +
552
+ "</span><em>" +
553
+ pct(F[index][5]) +
554
+ "</em></li>"
555
+ );
556
+ })
557
+ .join("") +
558
+ "</ul>"
559
+ );
560
+ }
561
+
562
+ function cycleWarning(index) {
563
+ var partners = new Set();
564
+ for (var i = 0; i < out[index].length; i++) {
565
+ if (isCycle(index, out[index][i])) partners.add(out[index][i]);
566
+ }
567
+ for (var j = 0; j < inc[index].length; j++) {
568
+ if (isCycle(inc[index][j], index)) partners.add(inc[index][j]);
569
+ }
570
+ if (partners.size === 0) return "";
571
+ var first = partners.values().next().value;
572
+ return (
573
+ '<div class="violation"><b>Ciclo de dependencias</b>Este fichero participa en un ciclo con ' +
574
+ esc(nameOf(pathOf(first))) +
575
+ ".</div>"
576
+ );
577
+ }
578
+
579
+ function renderInspector() {
580
+ if (!selected) {
581
+ var top = [];
582
+ for (var i = 0; i < N; i++) top.push(i);
583
+ top.sort(function (a, b) {
584
+ return F[b][5] - F[a][5] || pathOf(a).localeCompare(pathOf(b));
585
+ });
586
+ top = top.slice(0, 5);
587
+ inspector.innerHTML =
588
+ '<div class="summary">' +
589
+ '<div class="sub">Proyecto</div>' +
590
+ '<div class="big">' +
591
+ esc(fmt(data.fileCount)) +
592
+ "</div>" +
593
+ '<div class="sub">ficheros · ' +
594
+ esc(fmt(data.folderCount)) +
595
+ " carpetas</div>" +
596
+ "<p>" +
597
+ esc(fmt(data.totalArea)) +
598
+ " de " +
599
+ esc(data.area.title.toLowerCase()) +
600
+ " · " +
601
+ esc(fmt(data.metrics.length)) +
602
+ " métricas · " +
603
+ esc(fmt(data.hot)) +
604
+ " hotspots.</p>" +
605
+ "<h4>Mayor riesgo</h4>" +
606
+ '<ul class="deps">' +
607
+ top
608
+ .map(function (index) {
609
+ return (
610
+ '<li data-file="' +
611
+ index +
612
+ '"><i class="sw" style="background:' +
613
+ riskColor(F[index][5]) +
614
+ '"></i><span>' +
615
+ esc(pathOf(index)) +
616
+ "</span><em>" +
617
+ pct(F[index][5]) +
618
+ "</em></li>"
619
+ );
620
+ })
621
+ .join("") +
622
+ "</ul>" +
623
+ "<p>Selecciona un bloque para ver sus métricas y sus dependencias. Clic en la cabecera de una carpeta para enfocarla.</p>" +
624
+ "</div>";
625
+ return;
626
+ }
627
+
628
+ if (selected.kind === "folder") {
629
+ var folderPath = selected.path;
630
+ var indexes = [];
631
+ var total = 0;
632
+ for (var fi = 0; fi < N; fi++) {
633
+ if (inFolder(pathOf(fi), folderPath)) {
634
+ indexes.push(fi);
635
+ total += F[fi][4];
636
+ }
637
+ }
638
+ indexes.sort(function (a, b) {
639
+ return F[b][5] - F[a][5] || pathOf(a).localeCompare(pathOf(b));
640
+ });
641
+ inspector.innerHTML =
642
+ '<div class="insp">' +
643
+ '<div class="insp-path">' +
644
+ esc(folderPath) +
645
+ "</div>" +
646
+ '<div class="tags"><span class="tag">carpeta</span><span class="tag">' +
647
+ esc(fmt(indexes.length)) +
648
+ " ficheros</span></div>" +
649
+ '<dl class="metrics">' +
650
+ '<div class="metric"><dt>' +
651
+ esc(data.area.title) +
652
+ "</dt><dd>" +
653
+ esc(fmt(total)) +
654
+ "</dd></div>" +
655
+ '<div class="metric"><dt>Hotspots</dt><dd>' +
656
+ esc(fmt(indexes.filter(function (index) { return F[index][5] >= 0.66; }).length)) +
657
+ "</dd></div>" +
658
+ "</dl>" +
659
+ "<h4>Mayor riesgo en la carpeta</h4>" +
660
+ dependencyList(indexes.slice(0, 8), "carpeta vacía") +
661
+ "</div>";
662
+ return;
663
+ }
664
+
665
+ var index = selected.index;
666
+ var file = F[index];
667
+ var section = sectionFor(index);
668
+ var name = file[1];
669
+ var dot = name.lastIndexOf(".");
670
+ var extension = dot > 0 ? name.slice(dot + 1) : "—";
671
+ var functions = metricValue(index, "number_of_functions");
672
+ var hotspot = file[5] >= 0.66;
673
+
674
+ inspector.innerHTML =
675
+ '<div class="insp">' +
676
+ '<div class="insp-path">' +
677
+ esc(pathOf(index)) +
678
+ "</div>" +
679
+ '<div class="tags"><span class="tag">' +
680
+ esc(extension) +
681
+ '</span><span class="tag">' +
682
+ esc(section.key) +
683
+ "</span>" +
684
+ (hotspot ? '<span class="tag bad">hotspot</span>' : "") +
685
+ "</div>" +
686
+ '<dl class="metrics">' +
687
+ '<div class="metric"><dt>' +
688
+ esc(data.area.title) +
689
+ "</dt><dd>" +
690
+ esc(fmt(file[4])) +
691
+ "</dd></div>" +
692
+ '<div class="metric"><dt>Complejidad</dt><dd>' +
693
+ esc(fmt(file[6])) +
694
+ "</dd></div>" +
695
+ '<div class="metric"><dt>Churn</dt><dd>' +
696
+ esc(fmt(file[7])) +
697
+ " <small>commits</small></dd></div>" +
698
+ '<div class="metric"><dt>Fan-in / out</dt><dd>' +
699
+ esc(fmt(inc[index].length)) +
700
+ " / " +
701
+ esc(fmt(out[index].length)) +
702
+ "</dd></div>" +
703
+ (functions === null
704
+ ? ""
705
+ : '<div class="metric"><dt>Funciones</dt><dd>' +
706
+ esc(fmt(functions)) +
707
+ "</dd></div>") +
708
+ "</dl>" +
709
+ '<div class="risk-row"><span>Riesgo</span><b>' +
710
+ pct(file[5]) +
711
+ "/100</b></div>" +
712
+ '<div class="risk-track"><div class="risk-fill" style="width:' +
713
+ pct(file[5]) +
714
+ '%"></div></div>' +
715
+ "<h4>Depende de</h4>" +
716
+ dependencyList(out[index], "sin dependencias conocidas") +
717
+ "<h4>Usado por</h4>" +
718
+ dependencyList(inc[index], "nadie lo importa") +
719
+ cycleWarning(index) +
720
+ "</div>";
721
+ }
722
+
723
+ function updateCrumbs() {
724
+ var root = data.meta.root
725
+ ? esc(data.meta.project) + ' <span class="sep">/</span> ' + esc(data.meta.root)
726
+ : esc(data.meta.project);
727
+
728
+ if (!selected) {
729
+ crumbs.innerHTML = root;
730
+ return;
731
+ }
732
+
733
+ var path = selected.kind === "file" ? pathOf(selected.index) : selected.path;
734
+ var parts = path.split("/");
735
+ crumbs.innerHTML = [esc(data.meta.project)]
736
+ .concat(
737
+ parts.map(function (part, index) {
738
+ return index === parts.length - 1 ? "<b>" + esc(part) + "</b>" : esc(part);
739
+ })
740
+ )
741
+ .join(' <span class="sep">/</span> ');
742
+ }
743
+
744
+ /* ------------------------------------------------------------------ *
745
+ * Tooltip
746
+ * ------------------------------------------------------------------ */
747
+ function showCellTooltip(index) {
748
+ var file = F[index];
749
+ tooltip.innerHTML =
750
+ "<b>" +
751
+ esc(file[1]) +
752
+ "</b><br><span class=\"t-metrics\">" +
753
+ esc(pathOf(index)) +
754
+ "<br>" +
755
+ esc(data.area.title) +
756
+ " " +
757
+ esc(fmt(file[4])) +
758
+ " · riesgo " +
759
+ pct(file[5]) +
760
+ " · cx " +
761
+ esc(fmt(file[6])) +
762
+ " · churn " +
763
+ esc(fmt(file[7])) +
764
+ "</span>";
765
+ tooltip.style.display = "block";
766
+ }
767
+
768
+ function showFolderTooltip(folder) {
769
+ tooltip.innerHTML =
770
+ "<b>" +
771
+ esc(folderPath(folder)) +
772
+ "</b><br><span class=\"t-metrics\">" +
773
+ esc(fmt(Number(folder.dataset.n))) +
774
+ " ficheros · " +
775
+ esc(fmt(Number(folder.dataset.a))) +
776
+ " " +
777
+ esc(data.area.title.toLowerCase()) +
778
+ "</span>";
779
+ tooltip.style.display = "block";
780
+ }
781
+
782
+ function moveTooltip(event) {
783
+ if (tooltip.style.display !== "block") return;
784
+ var pad = 14;
785
+ var x = event.clientX + pad;
786
+ var y = event.clientY + pad;
787
+ var rect = tooltip.getBoundingClientRect();
788
+ if (x + rect.width > window.innerWidth - 8) x = event.clientX - rect.width - pad;
789
+ if (y + rect.height > window.innerHeight - 8) y = event.clientY - rect.height - pad;
790
+ tooltip.style.left = x + "px";
791
+ tooltip.style.top = y + "px";
792
+ }
793
+
794
+ function hideTooltip() {
795
+ tooltip.style.display = "none";
796
+ }
797
+
798
+ /* ------------------------------------------------------------------ *
799
+ * Eventos
800
+ * ------------------------------------------------------------------ */
801
+ svg.addEventListener("click", function (event) {
802
+ var cell = event.target.closest("rect[data-i]");
803
+ if (cell) {
804
+ selectFile(Number(cell.dataset.i));
805
+ return;
806
+ }
807
+ var folder = event.target.closest(".folder");
808
+ if (folder) {
809
+ selectFolder(folderPath(folder));
810
+ return;
811
+ }
812
+ clearSelection();
813
+ });
814
+
815
+ svg.addEventListener("mouseover", function (event) {
816
+ var cell = event.target.closest("rect[data-i]");
817
+ if (cell) {
818
+ showCellTooltip(Number(cell.dataset.i));
819
+ return;
820
+ }
821
+ var folder = event.target.closest(".folder");
822
+ if (folder) {
823
+ showFolderTooltip(folder);
824
+ return;
825
+ }
826
+ hideTooltip();
827
+ });
828
+
829
+ svg.addEventListener("mousemove", moveTooltip);
830
+ svg.addEventListener("mouseleave", hideTooltip);
831
+
832
+ inspector.addEventListener("click", function (event) {
833
+ var fileItem = event.target.closest("[data-file]");
834
+ if (fileItem) {
835
+ selectFile(Number(fileItem.dataset.file));
836
+ return;
837
+ }
838
+ var folderItem = event.target.closest("[data-folder]");
839
+ if (folderItem) selectFolder(folderItem.dataset.folder);
840
+ });
841
+
842
+ var lensButtons = document.querySelectorAll(".lens");
843
+ for (var li = 0; li < lensButtons.length; li++) {
844
+ lensButtons[li].addEventListener("click", function (event) {
845
+ setLens(event.currentTarget.dataset.lens);
846
+ });
847
+ }
848
+
849
+ if (metricSelect) {
850
+ metricSelect.addEventListener("change", function () {
851
+ metricKey = metricSelect.value;
852
+ setLens("metric");
853
+ });
854
+ }
855
+
856
+ var focusButtons = document.querySelectorAll(".finding[data-focus]");
857
+ for (var fbi = 0; fbi < focusButtons.length; fbi++) {
858
+ focusButtons[fbi].addEventListener("click", function (event) {
859
+ var focus = event.currentTarget.dataset.focus;
860
+ if (focus === "cycle" && data.cycles.length >= 2) {
861
+ selectFile(data.cycles[0]);
862
+ return;
863
+ }
864
+ var best = -1;
865
+ for (var i = 0; i < N; i++) {
866
+ if (best === -1 || F[i][5] > F[best][5]) best = i;
867
+ }
868
+ if (best !== -1) selectFile(best);
869
+ });
870
+ }
871
+
872
+ searchInput.addEventListener("input", function () {
873
+ query = searchInput.value;
874
+ applySearch();
875
+ });
876
+
877
+ searchInput.addEventListener("keydown", function (event) {
878
+ if (event.key === "Enter" && firstHit !== -1) {
879
+ selectFile(firstHit);
880
+ searchInput.blur();
881
+ }
882
+ });
883
+
884
+ document.addEventListener("keydown", function (event) {
885
+ if (event.key === "Escape") {
886
+ query = "";
887
+ searchInput.value = "";
888
+ applySearch();
889
+ clearSelection();
890
+ searchInput.blur();
891
+ hideTooltip();
892
+ return;
893
+ }
894
+ if (
895
+ event.key === "/" &&
896
+ document.activeElement !== searchInput &&
897
+ !/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName)
898
+ ) {
899
+ event.preventDefault();
900
+ searchInput.focus();
901
+ }
902
+ });
903
+
904
+ /* ------------------------------------------------------------------ *
905
+ * Arranque
906
+ * ------------------------------------------------------------------ */
907
+ applySearch();
908
+ applyLens();
909
+ if (data.default !== null && data.default !== undefined && cellByIndex[data.default]) {
910
+ selectFile(data.default);
911
+ } else {
912
+ renderInspector();
913
+ updateCrumbs();
914
+ }
915
+ })();