@takagaki/cortex-decisions-viewer 0.4.6 → 0.4.8
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 +23 -26
- package/dist/render.js +63 -4
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -472,9 +472,7 @@ 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
|
|
476
|
-
// 外部リンク(http(s))はノード化せず null を返す(外部URLの点はグラフ肥大化=描画負荷の主因。
|
|
477
|
-
// リンク自体は各レコードの詳細・参照欄に残るため情報は失われない)。
|
|
475
|
+
// 参照先IDから層を推定してノード化する
|
|
478
476
|
const classifyTarget = (target) => {
|
|
479
477
|
if (nodes.has(target))
|
|
480
478
|
return target;
|
|
@@ -498,7 +496,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
498
496
|
return addNode({ id: target, label, group, url });
|
|
499
497
|
}
|
|
500
498
|
else if (/^https?:\/\//.test(target)) {
|
|
501
|
-
|
|
499
|
+
group = "external";
|
|
500
|
+
label = target.replace(/^https?:\/\//, "").slice(0, 60);
|
|
501
|
+
return addNode({ id: target, label, group, url: target });
|
|
502
502
|
}
|
|
503
503
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
504
504
|
group = "silver"; // リポジトリ内ファイルへのパス参照 → GitHubのコピーへ飛べるようにする
|
|
@@ -508,30 +508,35 @@ 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
511
|
const addRelationEdges = (fromId, relations) => {
|
|
517
|
-
for (const r of relations)
|
|
518
|
-
|
|
512
|
+
for (const r of relations) {
|
|
513
|
+
edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
|
|
514
|
+
}
|
|
519
515
|
};
|
|
520
516
|
const addSourceEdge = (fromId, source) => {
|
|
521
517
|
if (source && source.trim() !== "")
|
|
522
|
-
|
|
518
|
+
edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
|
|
523
519
|
};
|
|
524
520
|
const addReferenceEdges = (fromId, references) => {
|
|
525
|
-
// ref.url(http(s))は classifyTarget が null を返す=外部はノード化しない。
|
|
526
|
-
// URLが無い参照(課題キー・議事録ID等)は従来どおり該当ノードへ繋ぐ。
|
|
527
521
|
for (const ref of references) {
|
|
528
|
-
|
|
522
|
+
if (ref.url) {
|
|
523
|
+
const id = addNode({
|
|
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
|
+
}
|
|
529
534
|
}
|
|
530
535
|
};
|
|
531
536
|
const addLinkEdges = (fromId, links) => {
|
|
532
537
|
for (const link of links) {
|
|
533
538
|
const key = backlogViewKey(link);
|
|
534
|
-
|
|
539
|
+
edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
|
|
535
540
|
}
|
|
536
541
|
};
|
|
537
542
|
// Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
|
|
@@ -568,7 +573,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
568
573
|
}
|
|
569
574
|
for (const c of content) {
|
|
570
575
|
for (const e of c.edges) {
|
|
571
|
-
|
|
576
|
+
edges.push({ from: c.id, to: classifyTarget(e.target), rel: e.rel });
|
|
572
577
|
}
|
|
573
578
|
}
|
|
574
579
|
// 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
|
|
@@ -582,15 +587,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
582
587
|
seen.add(key);
|
|
583
588
|
return true;
|
|
584
589
|
});
|
|
585
|
-
|
|
586
|
-
// 非レコードノードは描画もされないため、ペイロードからも落としておく(初期ロードを軽くする)。
|
|
587
|
-
const usedIds = new Set();
|
|
588
|
-
for (const e of deduped) {
|
|
589
|
-
usedIds.add(e.from);
|
|
590
|
-
usedIds.add(e.to);
|
|
591
|
-
}
|
|
592
|
-
const prunedNodes = [...nodes.values()].filter((n) => n.recordId || usedIds.has(n.id));
|
|
593
|
-
return { nodes: prunedNodes, edges: deduped };
|
|
590
|
+
return { nodes: [...nodes.values()], edges: deduped };
|
|
594
591
|
}
|
|
595
592
|
/**
|
|
596
593
|
* 各レコードの 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,7 +1054,9 @@ 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
|
+
// 外周リングを多数のグレー(外部リンク等の枝先)が均等に埋めることで全体が円形になり、
|
|
1059
|
+
// Goldが中心に締まる。この見た目は「点を全部残す」ことで成立する(重さはBarnes-Hut+収束後停止で別途解消)。
|
|
1004
1060
|
nodes.forEach(function (n) {
|
|
1005
1061
|
if (n.recordId) {
|
|
1006
1062
|
n.vx -= n.x * 0.012; n.vy -= n.y * 0.012;
|
|
@@ -1013,7 +1069,10 @@ const CLIENT_JS = `
|
|
|
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
|
-
|
|
1072
|
+
// 元のゆっくり減衰(*0.995)に戻す: ウォームアップ320回が高エネルギーで回り、Goldが中心に締まる
|
|
1073
|
+
// 深い平衡(元の見た目)まで到達する。floorは0.05ではなく0.003まで下げ、整定後はループ側で停止できる
|
|
1074
|
+
// ようにする(②の固まり対策と両立)。*0.97の速い減衰だとウォームアップが早期終了し浅い配置で固まっていた。
|
|
1075
|
+
if (alpha > 0.003) alpha *= 0.995;
|
|
1017
1076
|
}
|
|
1018
1077
|
|
|
1019
1078
|
function nodeRadius(n) {
|