init sigma.js

This commit is contained in:
pa
2026-02-02 00:13:14 +09:00
committed by Natsumi
parent 84502fc4af
commit d4eacff506
6 changed files with 306 additions and 654 deletions
@@ -1,267 +0,0 @@
import { i18n } from '../../../plugin/i18n';
const COLORS_PALETTE = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc'
];
const MAX_LABEL_NAME_LENGTH = 22;
function truncateLabelText(text) {
if (!text) {
return 'Unknown';
}
return text.length > MAX_LABEL_NAME_LENGTH
? `${text.slice(0, MAX_LABEL_NAME_LENGTH)}`
: text;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
export function computeForceOptions(nodes, links) {
const nodeCount = nodes.length || 1;
const degreeSum = nodes.reduce((sum, node) => sum + (node.degree || 0), 0);
const maxSymbol = nodes.reduce(
(max, node) => Math.max(max, node.symbolSize || 0),
0
);
const avgDegree = degreeSum / nodeCount || 0;
const density = links.length ? links.length / nodeCount : 0;
const repulsionBase = 140 + maxSymbol * 4 + avgDegree * 6;
const repulsion = clamp(repulsionBase, 180, 720);
const minEdge = clamp(48 + avgDegree * 1.2, 48, 90);
const maxEdge = clamp(
minEdge + 60 + Math.max(0, 140 - density * 18),
90,
200
);
return {
repulsion,
edgeLength: [minEdge, maxEdge],
gravity: 0.3,
layoutAnimation: nodes.length < 1000
};
}
export function applyForceOverrides(force, forceOverrides) {
if (!forceOverrides) {
return force;
}
const merged = { ...force };
if (typeof forceOverrides.repulsion === 'number') {
merged.repulsion = Math.max(0, forceOverrides.repulsion);
}
if (Array.isArray(forceOverrides.edgeLength)) {
const [
minRaw = merged.edgeLength?.[0],
maxRaw = merged.edgeLength?.[1]
] = forceOverrides.edgeLength;
const min =
typeof minRaw === 'number' ? minRaw : merged.edgeLength?.[0];
const max =
typeof maxRaw === 'number' ? maxRaw : merged.edgeLength?.[1];
const hasBoth = typeof min === 'number' && typeof max === 'number';
if (hasBoth) {
const normalizedMin = Math.max(0, min);
merged.edgeLength = [normalizedMin, Math.max(normalizedMin, max)];
}
}
if (typeof forceOverrides.gravity === 'number') {
merged.gravity = clamp(forceOverrides.gravity, 0, 1);
}
if (typeof forceOverrides.layoutAnimation === 'boolean') {
merged.layoutAnimation = forceOverrides.layoutAnimation;
}
return merged;
}
const t = i18n.global.t;
export function useMutualGraphChart({ cachedUsers, graphPayload }) {
function buildGraph(mutualMap, updateChart) {
const nodes = new Map();
const links = [];
const linkKeys = new Set();
function ensureNode(id, name) {
if (!id) {
return null;
}
const existing = nodes.get(id);
if (existing) {
return existing;
}
const node = {
id,
name: name || id
};
nodes.set(id, node);
return node;
}
function incrementDegree(nodeId) {
const node = nodes.get(nodeId);
if (!node) {
return;
}
node.degree = (node.degree || 0) + 1;
}
function addLink(source, target) {
if (!source || !target || source === target) {
return;
}
const key = [source, target].sort().join('__');
if (linkKeys.has(key)) {
return;
}
linkKeys.add(key);
links.push({ source, target });
incrementDegree(source);
incrementDegree(target);
}
for (const [friendId, { friend, mutuals }] of mutualMap.entries()) {
const friendRef = friend?.ref || cachedUsers.get(friendId);
const friendName = friendRef?.displayName;
ensureNode(friendId, friendName);
for (const mutual of mutuals) {
if (!mutual?.id) {
continue;
}
const cached = cachedUsers.get(mutual.id);
const label =
cached?.displayName || mutual.displayName || mutual.id;
ensureNode(mutual.id, label);
addLink(friendId, mutual.id);
}
}
const nodeList = Array.from(nodes.values());
const maxDegree = nodeList.reduce(
(acc, node) => Math.max(acc, node.degree || 0),
0
);
nodeList.forEach((node, index) => {
const normalized = maxDegree ? (node.degree || 0) / maxDegree : 0;
const size = Math.round(26 + normalized * 52);
const color = COLORS_PALETTE[index % COLORS_PALETTE.length];
const displayName = truncateLabelText(node.name || node.id);
node.symbolSize = size;
node.label = {
show: true,
formatter: displayName
};
node.itemStyle = {
...(node.itemStyle || {}),
color
};
});
graphPayload.value = {
nodes: nodeList,
links
};
updateChart?.(graphPayload.value);
}
function createChartOption(payload, force) {
const nodes = payload?.nodes ?? [];
const links = payload?.links ?? [];
const labelMap = Object.create(null);
nodes.forEach((node) => {
if (node?.id) {
labelMap[node.id] = node.name || node.id;
}
});
const resolvedForce = {
...(force || {}),
layoutAnimation: false
};
return {
color: COLORS_PALETTE,
backgroundColor: 'transparent',
animation: false,
animationDuration: 0,
animationDurationUpdate: 0,
tooltip: {
trigger: 'item',
formatter: (params) => {
if (params.dataType === 'node') {
const name =
params.data?.name || params.data?.id || 'Unknown';
const mutualCount = Number.isFinite(params.data?.degree)
? params.data.degree
: 0;
const mutualLabel = t(
'view.charts.mutual_friend.tooltip.mutual_friends_count',
{
count: mutualCount
}
);
return `${name}\n${mutualLabel}`;
}
}
},
series: [
{
type: 'graph',
layout: 'force',
legendHoverLink: false,
roam: true,
roamTrigger: 'global',
animation: false,
animationDuration: 0,
animationDurationUpdate: 0,
data: nodes,
links,
label: {
position: 'right'
},
symbol: 'circle',
emphasis: {
focus: 'adjacency',
itemStyle: {
borderWidth: 3,
opacity: 1
}
},
force: resolvedForce,
itemStyle: {
borderColor: '#ffffff',
borderWidth: 1,
shadowBlur: 16,
shadowColor: 'rgba(0,0,0,0.35)'
},
lineStyle: {
curveness: 0.18,
width: 0.5,
opacity: 0.4
}
}
]
};
}
return {
buildGraph,
createChartOption
};
}