@takagaki/cortex-decisions-viewer 0.4.5 → 0.4.7
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/dist/build.js +43 -26
- package/dist/render.js +69 -10
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -472,7 +472,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
472
472
|
for (const sec of sections)
|
|
473
473
|
for (const r of sec.records)
|
|
474
474
|
fileNameToId.set(r.fileName, r.id);
|
|
475
|
-
// 参照先ID
|
|
475
|
+
// 参照先IDから層を推定してノード化する。
|
|
476
|
+
// 外部リンク(http(s))はノード化せず null を返す(外部URLの点はグラフ肥大化=描画負荷の主因。
|
|
477
|
+
// リンク自体は各レコードの詳細・参照欄に残るため情報は失われない)。
|
|
476
478
|
const classifyTarget = (target) => {
|
|
477
479
|
if (nodes.has(target))
|
|
478
480
|
return target;
|
|
@@ -496,9 +498,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
496
498
|
return addNode({ id: target, label, group, url });
|
|
497
499
|
}
|
|
498
500
|
else if (/^https?:\/\//.test(target)) {
|
|
499
|
-
|
|
500
|
-
label = target.replace(/^https?:\/\//, "").slice(0, 60);
|
|
501
|
-
return addNode({ id: target, label, group, url: target });
|
|
501
|
+
return null; // 外部リンクはノード化しない
|
|
502
502
|
}
|
|
503
503
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
504
504
|
group = "silver"; // リポジトリ内ファイルへのパス参照 → GitHubのコピーへ飛べるようにする
|
|
@@ -508,36 +508,45 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
508
508
|
}
|
|
509
509
|
return addNode({ id: target, label, group });
|
|
510
510
|
};
|
|
511
|
+
// 解決先が null(外部リンク等で非ノード化)の辺は張らない
|
|
512
|
+
const pushEdge = (fromId, target, rel) => {
|
|
513
|
+
if (target)
|
|
514
|
+
edges.push({ from: fromId, to: target, rel });
|
|
515
|
+
};
|
|
516
|
+
// 外部URLノードを作る(source/references の出典用フォールバック)。
|
|
517
|
+
const externalNode = (url, label) => addNode({
|
|
518
|
+
id: url,
|
|
519
|
+
label: (label ?? url.replace(/^https?:\/\//, "")).slice(0, 60),
|
|
520
|
+
group: "external",
|
|
521
|
+
url,
|
|
522
|
+
});
|
|
523
|
+
// 出典(source/reference)の解決: Backlog課題URLは実ノード(bronze)へ、議事録ID等は該当ノードへ。
|
|
524
|
+
// 解決できない外部URL(Wiki・GitHub等)は外部ノードとして必ず繋ぐ(出典は少数で意味があるため残す。
|
|
525
|
+
// 大量の「本文リンク」の騒音は addLinkEdges 側でドロップしたまま)。
|
|
526
|
+
const provenanceTarget = (raw, label) => {
|
|
527
|
+
const v = raw.trim();
|
|
528
|
+
return classifyTarget(backlogViewKey(v) ?? v) ?? externalNode(v, label);
|
|
529
|
+
};
|
|
511
530
|
const addRelationEdges = (fromId, relations) => {
|
|
512
|
-
for (const r of relations)
|
|
513
|
-
|
|
514
|
-
}
|
|
531
|
+
for (const r of relations)
|
|
532
|
+
pushEdge(fromId, classifyTarget(r.target), r.rel);
|
|
515
533
|
};
|
|
516
534
|
const addSourceEdge = (fromId, source) => {
|
|
517
535
|
if (source && source.trim() !== "")
|
|
518
|
-
edges.push({ from: fromId, to:
|
|
536
|
+
edges.push({ from: fromId, to: provenanceTarget(source), rel: "source" });
|
|
519
537
|
};
|
|
520
538
|
const addReferenceEdges = (fromId, references) => {
|
|
521
539
|
for (const ref of references) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
id: ref.url,
|
|
525
|
-
label: ref.label.slice(0, 60),
|
|
526
|
-
group: "external",
|
|
527
|
-
url: ref.url,
|
|
528
|
-
});
|
|
529
|
-
edges.push({ from: fromId, to: id, rel: "reference" });
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
edges.push({ from: fromId, to: classifyTarget(ref.label), rel: "reference" });
|
|
533
|
-
}
|
|
540
|
+
const to = ref.url ? provenanceTarget(ref.url, ref.label) : classifyTarget(ref.label);
|
|
541
|
+
pushEdge(fromId, to, "reference");
|
|
534
542
|
}
|
|
535
543
|
};
|
|
544
|
+
// Gold層(Decision/用語/レポート/Home)の本文リンクは必ず繋ぐ(外部URLでも外部ノードとして接続)。
|
|
545
|
+
// =frontmatterを持つレコードは孤立させない。大量の騒音は「コンテキスト側の本文リンク」(content edges)
|
|
546
|
+
// のみドロップする(下の content ループは classifyTarget のまま)。
|
|
536
547
|
const addLinkEdges = (fromId, links) => {
|
|
537
|
-
for (const link of links)
|
|
538
|
-
|
|
539
|
-
edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
|
|
540
|
-
}
|
|
548
|
+
for (const link of links)
|
|
549
|
+
edges.push({ from: fromId, to: provenanceTarget(link), rel: "link" });
|
|
541
550
|
};
|
|
542
551
|
// Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
|
|
543
552
|
for (const c of content) {
|
|
@@ -573,7 +582,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
573
582
|
}
|
|
574
583
|
for (const c of content) {
|
|
575
584
|
for (const e of c.edges) {
|
|
576
|
-
|
|
585
|
+
pushEdge(c.id, classifyTarget(e.target), e.rel);
|
|
577
586
|
}
|
|
578
587
|
}
|
|
579
588
|
// 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
|
|
@@ -587,7 +596,15 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
587
596
|
seen.add(key);
|
|
588
597
|
return true;
|
|
589
598
|
});
|
|
590
|
-
|
|
599
|
+
// 描画対象は「レコード(Gold) or 辺に接続のあるノード」だけ。外部リンク除去で孤立した
|
|
600
|
+
// 非レコードノードは描画もされないため、ペイロードからも落としておく(初期ロードを軽くする)。
|
|
601
|
+
const usedIds = new Set();
|
|
602
|
+
for (const e of deduped) {
|
|
603
|
+
usedIds.add(e.from);
|
|
604
|
+
usedIds.add(e.to);
|
|
605
|
+
}
|
|
606
|
+
const prunedNodes = [...nodes.values()].filter((n) => n.recordId || usedIds.has(n.id));
|
|
607
|
+
return { nodes: prunedNodes, edges: deduped };
|
|
591
608
|
}
|
|
592
609
|
/**
|
|
593
610
|
* 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
|
package/dist/render.js
CHANGED
|
@@ -979,8 +979,8 @@ const CLIENT_JS = `
|
|
|
979
979
|
}
|
|
980
980
|
resize();
|
|
981
981
|
|
|
982
|
-
|
|
983
|
-
|
|
982
|
+
// 反発(O(n²)・全ペア総当たり)。点が少ないうちはこれが最速。
|
|
983
|
+
function repulseNaive() {
|
|
984
984
|
for (var i = 0; i < nodes.length; i++) {
|
|
985
985
|
for (var j = i + 1; j < nodes.length; j++) {
|
|
986
986
|
var a = nodes[i], b = nodes[j];
|
|
@@ -992,6 +992,60 @@ const CLIENT_JS = `
|
|
|
992
992
|
a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy;
|
|
993
993
|
}
|
|
994
994
|
}
|
|
995
|
+
}
|
|
996
|
+
// 反発(Barnes-Hut・O(n log n))。四分木で遠いノード群を重心1点に近似する。
|
|
997
|
+
// 点が多い案件(フリート拡大時)でも tick が重くならない=点を削らずに固まりを防ぐための土台。
|
|
998
|
+
function repulseBH() {
|
|
999
|
+
var nn = nodes.length, i;
|
|
1000
|
+
if (nn < 2) return;
|
|
1001
|
+
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1002
|
+
for (i = 0; i < nn; i++) { var p0 = nodes[i]; if (p0.x < minX) minX = p0.x; if (p0.y < minY) minY = p0.y; if (p0.x > maxX) maxX = p0.x; if (p0.y > maxY) maxY = p0.y; }
|
|
1003
|
+
var size = Math.max(maxX - minX, maxY - minY, 1) + 1;
|
|
1004
|
+
// 配列ベースの四分木。BODY: -1=空 / >=0=葉(そのノードindex) / -2=内部ノード
|
|
1005
|
+
var QX = [minX], QY = [minY], QS = [size], MASS = [0], CX = [0], CY = [0];
|
|
1006
|
+
var C0 = [-1], C1 = [-1], C2 = [-1], C3 = [-1], BODY = [-1];
|
|
1007
|
+
function nc(x, y, s) { QX.push(x); QY.push(y); QS.push(s); MASS.push(0); CX.push(0); CY.push(0); C0.push(-1); C1.push(-1); C2.push(-1); C3.push(-1); BODY.push(-1); return QX.length - 1; }
|
|
1008
|
+
function childOf(c, q) { return q === 0 ? C0[c] : q === 1 ? C1[c] : q === 2 ? C2[c] : C3[c]; }
|
|
1009
|
+
function setChild(c, q, v) { if (q === 0) C0[c] = v; else if (q === 1) C1[c] = v; else if (q === 2) C2[c] = v; else C3[c] = v; }
|
|
1010
|
+
function pushDown(c, bi, depth) {
|
|
1011
|
+
var hx = QX[c] + QS[c] / 2, hy = QY[c] + QS[c] / 2;
|
|
1012
|
+
var q = (nodes[bi].x >= hx ? 1 : 0) + (nodes[bi].y >= hy ? 2 : 0);
|
|
1013
|
+
var ch = childOf(c, q);
|
|
1014
|
+
if (ch === -1) { var s = QS[c] / 2; ch = nc(QX[c] + ((q & 1) ? s : 0), QY[c] + ((q & 2) ? s : 0), s); setChild(c, q, ch); }
|
|
1015
|
+
insert(ch, bi, depth + 1);
|
|
1016
|
+
}
|
|
1017
|
+
function insert(c, bi, depth) {
|
|
1018
|
+
var x = nodes[bi].x, y = nodes[bi].y;
|
|
1019
|
+
if (MASS[c] === 0) { CX[c] = x; CY[c] = y; } else { CX[c] = (CX[c] * MASS[c] + x) / (MASS[c] + 1); CY[c] = (CY[c] * MASS[c] + y) / (MASS[c] + 1); }
|
|
1020
|
+
if (MASS[c] === 0 && BODY[c] === -1) { BODY[c] = bi; MASS[c] = 1; return; }
|
|
1021
|
+
MASS[c]++;
|
|
1022
|
+
if (depth >= 24) return; // 深さ上限(ほぼ同一座標): これ以上分割せず集約
|
|
1023
|
+
if (BODY[c] !== -2) { var old = BODY[c]; BODY[c] = -2; pushDown(c, old, depth); }
|
|
1024
|
+
pushDown(c, bi, depth);
|
|
1025
|
+
}
|
|
1026
|
+
for (i = 0; i < nn; i++) insert(0, i, 0);
|
|
1027
|
+
var theta2 = 1.0; // 受容角θ=1.0 の二乗(セルサイズ/距離 < θ なら重心1点で近似)
|
|
1028
|
+
for (i = 0; i < nn; i++) {
|
|
1029
|
+
var p = nodes[i]; var stack = [0];
|
|
1030
|
+
while (stack.length) {
|
|
1031
|
+
var c = stack.pop();
|
|
1032
|
+
if (MASS[c] === 0) continue;
|
|
1033
|
+
if (BODY[c] === i && MASS[c] === 1) continue; // 自分自身の葉
|
|
1034
|
+
var dx = CX[c] - p.x, dy = CY[c] - p.y; var d2 = dx * dx + dy * dy + 0.01;
|
|
1035
|
+
if (BODY[c] !== -2 || QS[c] * QS[c] < theta2 * d2) {
|
|
1036
|
+
var f = 1800 * MASS[c] / d2; var d = Math.sqrt(d2);
|
|
1037
|
+
p.vx -= dx / d * f; p.vy -= dy / d * f;
|
|
1038
|
+
} else {
|
|
1039
|
+
if (C0[c] !== -1) stack.push(C0[c]); if (C1[c] !== -1) stack.push(C1[c]);
|
|
1040
|
+
if (C2[c] !== -1) stack.push(C2[c]); if (C3[c] !== -1) stack.push(C3[c]);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// 約900点を境に切替: 小規模=naive(オーバーヘッド無しで最速) / 大規模=Barnes-Hut(固まらない)。
|
|
1046
|
+
var REPULSE_NAIVE_MAX = 900;
|
|
1047
|
+
function tick() {
|
|
1048
|
+
if (nodes.length < REPULSE_NAIVE_MAX) repulseNaive(); else repulseBH();
|
|
995
1049
|
// ばね(辺)
|
|
996
1050
|
edges.forEach(function (e) {
|
|
997
1051
|
var dx = e.b.x - e.a.x, dy = e.b.y - e.a.y;
|
|
@@ -1000,20 +1054,22 @@ const CLIENT_JS = `
|
|
|
1000
1054
|
var fx = (dx / d) * f, fy = (dy / d) * f;
|
|
1001
1055
|
e.a.vx += fx; e.a.vy += fy; e.b.vx -= fx; e.b.vy -= fy;
|
|
1002
1056
|
});
|
|
1003
|
-
// Gold
|
|
1057
|
+
// 全ノードを中心へ向ける等方的な引力(Goldは強め=中央に、コンテキストは弱め=その周り)。
|
|
1058
|
+
// さらに半径 RING の円で外周を押し戻して封じ込める。これで対角の伸び・飛び出したスポークが
|
|
1059
|
+
// 抑えられ、全体が円形のディスクに収まる(force-directed本来の「丸める力の不在」を補う)。
|
|
1004
1060
|
nodes.forEach(function (n) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
var rf = (r - RING) * 0.
|
|
1061
|
+
var g = n.recordId ? 0.012 : 0.005;
|
|
1062
|
+
n.vx -= n.x * g; n.vy -= n.y * g;
|
|
1063
|
+
var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
|
|
1064
|
+
if (r > RING) {
|
|
1065
|
+
var rf = (r - RING) * 0.05; // 円の外に出た分だけ内側へ強めに戻す(円形の境界)
|
|
1010
1066
|
n.vx -= (n.x / r) * rf; n.vy -= (n.y / r) * rf;
|
|
1011
1067
|
}
|
|
1012
1068
|
if (n === draggingNode) { n.vx = 0; n.vy = 0; return; }
|
|
1013
1069
|
n.vx *= 0.85; n.vy *= 0.85;
|
|
1014
1070
|
n.x += n.vx * alpha; n.y += n.vy * alpha;
|
|
1015
1071
|
});
|
|
1016
|
-
if (alpha > 0.
|
|
1072
|
+
if (alpha > 0.0015) alpha *= 0.97; // 収束まで減衰しきる(下げ止まりさせず、整定後はループ側で物理計算を止める)
|
|
1017
1073
|
}
|
|
1018
1074
|
|
|
1019
1075
|
function nodeRadius(n) {
|
|
@@ -1257,6 +1313,7 @@ const CLIENT_JS = `
|
|
|
1257
1313
|
else if (n.url) window.open(n.url, "_blank", "noopener");
|
|
1258
1314
|
}
|
|
1259
1315
|
}
|
|
1316
|
+
if (draggingNode) alpha = Math.max(alpha, 0.2); // 動かした点の周りを少し整え直してから止まる
|
|
1260
1317
|
draggingNode = null;
|
|
1261
1318
|
panning = null;
|
|
1262
1319
|
canvas.classList.remove("dragging");
|
|
@@ -1282,7 +1339,9 @@ const CLIENT_JS = `
|
|
|
1282
1339
|
var running = true;
|
|
1283
1340
|
function loop() {
|
|
1284
1341
|
if (!running || !document.body.contains(canvas)) { running = false; return; }
|
|
1285
|
-
|
|
1342
|
+
// 収束後(alpha減衰しきり・無操作)は O(n²) の物理計算 tick を止める。これが「固まり続ける」主因。
|
|
1343
|
+
// ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
|
|
1344
|
+
if (alpha > 0.004 || draggingNode || panning) tick();
|
|
1286
1345
|
draw();
|
|
1287
1346
|
requestAnimationFrame(loop);
|
|
1288
1347
|
}
|