diff --git a/package-lock.json b/package-lock.json
index 3cb7213c..b2a49877 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,8 @@
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0",
"globals": "^17.2.0",
+ "graphology": "^0.26.0",
+ "graphology-layout-forceatlas2": "^0.10.1",
"jest": "^30.2.0",
"lightningcss": "^1.31.1",
"lucide-vue-next": "^0.562.0",
@@ -57,6 +59,7 @@
"reka-ui": "^2.8.0",
"remixicon": "^4.9.1",
"sass-embedded": "^1.97.3",
+ "sigma": "^3.0.2",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
@@ -9321,6 +9324,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/exec-sh": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz",
@@ -10202,6 +10215,50 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/graphology": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz",
+ "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.24.0"
+ }
+ },
+ "node_modules/graphology-layout-forceatlas2": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
+ "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.1.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-types": {
+ "version": "0.24.8",
+ "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
+ "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/graphology-utils": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
+ "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "graphology-types": ">=0.23.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -16427,6 +16484,17 @@
"node": "^12.20.0 || >=14"
}
},
+ "node_modules/sigma": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz",
+ "integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0",
+ "graphology-utils": "^2.5.2"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
diff --git a/package.json b/package.json
index bda0bbfa..8cb59a00 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,8 @@
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0",
"globals": "^17.2.0",
+ "graphology": "^0.26.0",
+ "graphology-layout-forceatlas2": "^0.10.1",
"jest": "^30.2.0",
"lightningcss": "^1.31.1",
"lucide-vue-next": "^0.562.0",
@@ -78,6 +80,7 @@
"reka-ui": "^2.8.0",
"remixicon": "^4.9.1",
"sass-embedded": "^1.97.3",
+ "sigma": "^3.0.2",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
@@ -181,4 +184,4 @@
"hazardous": "^0.3.0",
"node-api-dotnet": "^0.9.19"
}
-}
\ No newline at end of file
+}
diff --git a/src/stores/charts.js b/src/stores/charts.js
index fb823e53..7fbe7f51 100644
--- a/src/stores/charts.js
+++ b/src/stores/charts.js
@@ -11,20 +11,12 @@ function createDefaultFetchState() {
};
}
-function createDefaultPayload() {
- return {
- nodes: [],
- links: []
- };
-}
-
export const useChartsStore = defineStore('Charts', () => {
const friendStore = useFriendStore();
const { t } = useI18n();
const activeTab = ref('instance');
- const mutualGraphPayload = ref(createDefaultPayload());
const mutualGraphFetchState = reactive(createDefaultFetchState());
const mutualGraphStatus = reactive({
isFetching: false,
@@ -92,7 +84,6 @@ export const useChartsStore = defineStore('Charts', () => {
});
function resetMutualGraphState() {
- mutualGraphPayload.value = createDefaultPayload();
Object.assign(mutualGraphFetchState, createDefaultFetchState());
mutualGraphStatus.isFetching = false;
mutualGraphStatus.hasFetched = false;
@@ -104,7 +95,6 @@ export const useChartsStore = defineStore('Charts', () => {
return {
activeTab,
- mutualGraphPayload,
mutualGraphFetchState,
mutualGraphStatus,
resetMutualGraphState
diff --git a/src/views/Charts/components/InstanceActivityDetail.vue b/src/views/Charts/components/InstanceActivityDetail.vue
index 37941e8e..90787fbd 100644
--- a/src/views/Charts/components/InstanceActivityDetail.vue
+++ b/src/views/Charts/components/InstanceActivityDetail.vue
@@ -18,7 +18,7 @@
diff --git a/src/views/Charts/composables/useMutualGraphChart.js b/src/views/Charts/composables/useMutualGraphChart.js
deleted file mode 100644
index 9c72e0c7..00000000
--- a/src/views/Charts/composables/useMutualGraphChart.js
+++ /dev/null
@@ -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
- };
-}