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 - }; -}