finance-dashboard/lib/graph.js

116 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ═══════════════════════════════════════════════════════════
// graph.js — 從知識庫筆記內 [[wikilink]] 建築節點與邊(給圖譜視圖)
// ═══════════════════════════════════════════════════════════
const WIKI_RE = /\[\[([^\]]+)\]\]/g;
const KIND_META = {
overview: { color: '#0071e3', shape: 'box' },
principleMap: { color: '#5856d6', shape: 'box' },
quiz: { color: '#ff2d55', shape: 'box' },
category: { color: '#0071e3', shape: 'box' },
case: { color: '#34c759', shape: 'dot' },
principle: { color: '#af52de', shape: 'dot' },
term: { color: '#ff9500', shape: 'dot' },
company: { color: '#5ac8fa', shape: 'dot' },
episode: { color: '#8e8e93', shape: 'dot' },
};
function nodeKey(hit) { return `${hit.kind}:${hit.id}`; }
function resolveLink(linkMap, target) {
const t = (target || '').trim();
return linkMap[t] || linkMap[t.split('#').pop()] || linkMap[t.split('/').pop()] || null;
}
function shortLabel(title, max = 28) {
const s = String(title || '');
return s.length > max ? s.slice(0, max - 1) + '…' : s;
}
export function buildGraph(knowledge, opts = {}) {
const linkMap = knowledge.linkMap || {};
const includeIndex = opts.includeIndex === '1' || opts.includeIndex === true;
const kinds = opts.kinds ? new Set(String(opts.kinds).split(',')) : null;
const limit = Math.min(Number(opts.limit) || 450, 800);
const center = opts.center ? String(opts.center) : null;
const depth = Math.min(Math.max(Number(opts.depth) || 2, 1), 4);
const nodes = new Map();
const edgeSet = new Set();
const edges = [];
const addNode = (hit) => {
if (!hit) return null;
if (kinds && !kinds.has(hit.kind)) return null;
const id = nodeKey(hit);
if (nodes.size >= limit && !nodes.has(id)) return null;
if (!nodes.has(id)) {
const meta = KIND_META[hit.kind] || { color: '#8e8e93', shape: 'dot' };
nodes.set(id, {
id, label: shortLabel(hit.title || hit.id),
title: hit.title || hit.id,
kind: hit.kind,
color: meta.color,
shape: meta.shape,
});
}
return id;
};
const addEdge = (from, to) => {
if (!from || !to || from === to) return;
const k = `${from}|${to}`;
if (edgeSet.has(k)) return;
edgeSet.add(k);
edges.push({ from, to });
};
const parseBody = (body, fromId) => {
if (!body || !fromId) return;
let m;
WIKI_RE.lastIndex = 0;
while ((m = WIKI_RE.exec(body)) !== null) {
const hit = resolveLink(linkMap, m[1]);
const toId = addNode(hit);
if (toId) addEdge(fromId, toId);
}
};
const seed = (hit, body) => {
const id = addNode(hit);
if (id && body) parseBody(body, id);
return id;
};
if (knowledge.overview) seed({ kind: 'overview', id: knowledge.overview.id, title: knowledge.overview.title }, knowledge.overview.body);
if (knowledge.principleMap) seed({ kind: 'principleMap', id: knowledge.principleMap.id, title: knowledge.principleMap.title }, knowledge.principleMap.body);
if (knowledge.quiz) seed({ kind: 'quiz', id: knowledge.quiz.id, title: knowledge.quiz.title }, knowledge.quiz.body);
for (const c of (knowledge.categories || [])) seed({ kind: 'category', id: c.id, title: c.title }, c.body);
for (const c of (knowledge.cases || [])) seed({ kind: 'case', id: c.id, title: c.title }, c.body);
for (const p of (knowledge.principles || [])) seed({ kind: 'principle', id: p.id, title: p.title }, p.body);
if (includeIndex) {
for (const x of (knowledge.index || [])) addNode({ kind: x.kind, id: x.id, title: x.title });
}
// 以某節點為中心BFS 保留 depth 層內的節點與邊
if (center && nodes.has(center)) {
const keep = new Set([center]);
let frontier = [center];
for (let d = 0; d < depth; d++) {
const next = [];
for (const e of edges) {
if (frontier.includes(e.from) && nodes.has(e.to)) { keep.add(e.to); next.push(e.to); }
if (frontier.includes(e.to) && nodes.has(e.from)) { keep.add(e.from); next.push(e.from); }
}
frontier = [...new Set(next)];
}
const filtEdges = edges.filter(e => keep.has(e.from) && keep.has(e.to));
const filtNodes = [...nodes.values()].filter(n => keep.has(n.id));
return { nodes: filtNodes, edges: filtEdges, center, depth, total: filtNodes.length };
}
return { nodes: [...nodes.values()], edges, total: nodes.size };
}