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/LICENSE +21 -0
- package/README.md +77 -2
- package/client.js +915 -0
- package/index.js +4687 -0
- package/package.json +42 -4
- package/styles.css +441 -0
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
|
+
"&": "&",
|
|
232
|
+
"<": "<",
|
|
233
|
+
">": ">",
|
|
234
|
+
'"': """,
|
|
235
|
+
"'": "'"
|
|
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
|
+
})();
|