SocialNetworks_duan/src/views/GroupEvolution/component/groupGraph.vue

379 lines
11 KiB
Vue
Raw Normal View History

<template>
2025-08-05 14:23:32 +08:00
<div class="groupGraph-component">
<img src="@/assets/images/groupEvolution/graph-title.png" class="titleImage" />
<div class="container" id="container"></div>
<div class="timeList">
<TimeAxis
v-if="timeList.length"
:time-list="timeList"
2025-08-08 09:47:19 +08:00
:is-auto-play="false"
2025-08-05 14:23:32 +08:00
:start-time="new Date(timeList[0])"
:end-time="new Date(timeList[timeList.length - 1])"
@click:pointerDown="handlePointerDown"
@slide:pointerUp="handlePointerDown"
></TimeAxis>
</div>
</div>
</template>
2025-08-05 14:23:32 +08:00
<script setup>
2025-08-07 14:54:33 +08:00
import { defineProps, defineEmits, onUnmounted, ref, toRaw, watch } from "vue"
2025-08-05 14:23:32 +08:00
import { storeToRefs } from "pinia"
import { convertToUtcIsoString } from "@/utils/transform"
2025-08-07 10:00:14 +08:00
import { paintNodeFunction, paintLineFunction } from "@/utils/customePaint"
import TimeAxis from "@/components/timeAxis.vue"
import GraphVis from "@/assets/package/graphvis.esm.min.js"
2025-08-08 17:41:30 +08:00
2025-08-05 14:23:32 +08:00
const props = defineProps({
store: {
required: true
}
})
const emit = defineEmits(["click:pointerDownAndSlide"])
2025-08-06 15:01:12 +08:00
const { timeList, graph } = storeToRefs(props.store)
2025-08-07 20:13:33 +08:00
let isPlay = ref(false)
2025-08-07 10:00:14 +08:00
let graphVis = null
let forceSimulator = null
let currentSelectNode = ref(null)
2025-08-08 15:08:10 +08:00
const storeId = props.store.$id
2025-08-07 10:00:14 +08:00
const defaultConfig = {
node: {
label: {
show: true,
font: "normal 14px KaiTi",
color: "250,250,250",
2025-08-08 09:47:19 +08:00
textPosition: "Middle_Center", //Middle_Center
2025-08-07 10:00:14 +08:00
textOffsetY: 10
},
shape: "circle",
2025-08-08 09:47:19 +08:00
size: 60,
2025-08-07 10:00:14 +08:00
borderColor: "200,50,50",
borderWidth: 0,
selected: {
borderWidth: 2,
borderColor: "100,250,100",
showShadow: true, // 是否展示阴影
2025-08-08 09:47:19 +08:00
shadowBlur: 5, //阴影范围大小
2025-08-07 10:00:14 +08:00
shadowColor: "50,250,30" // 选中是的阴影颜色
}
},
link: {
label: {
// 连线标签
show: false, // 是否显示
color: "245,245,245", // 字体颜色
font: "normal 11px KaiTi", // 字体大小及类型
background: "255,255,255" //文字背景色(设置后文字居中,一般与画布背景色一致)
},
2025-08-07 14:54:33 +08:00
lineType: "straight", // curver
2025-08-08 09:47:19 +08:00
showArrow: true,
2025-08-07 10:00:14 +08:00
lineWidth: 2,
colorType: "both",
color: "240,240,240",
selected: {
color: "100,250,100"
}
},
highLightNeiber: true // 相邻节点高亮开关
}
const registCustomePaintFunc = () => {
graphVis.definedNodePaintFunc(paintNodeFunction) //自定义节点绘图方法
graphVis.definedLinkPaintFunc(paintLineFunction) //自定义关系绘图方法
}
2025-08-05 14:23:32 +08:00
// 处理时间轴点击事件和拉动
const handlePointerDown = (time) => {
const utcTime = convertToUtcIsoString(time)
emit("click:pointerDownAndSlide", utcTime)
}
2025-08-06 15:01:12 +08:00
2025-08-07 10:00:14 +08:00
const registEvents = () => {
//全局记录包裹层元素
const containerDom = document.getElementById("container")
graphVis.registEventListener("node", "mouseOver", function (event, node) {
containerDom.style.cursor = "pointer"
})
graphVis.registEventListener("node", "mouseOut", function (event, node) {
containerDom.style.cursor = ""
})
//节点开始拖动
graphVis.registEventListener("node", "mousedrag", function (event, node) {
currentSelectNode.value = node
//开始拖动,必须要设置的属性
currentSelectNode.value.fx = node.x
currentSelectNode.value.fy = node.y
forceSimulator.alphaTarget(0.3).restart()
})
graphVis.registEventListener("node", "dblClick", function (event, node) {
node.fx = null
node.fy = null
forceSimulator.alphaTarget(0.3).restart()
})
//拖动中
graphVis.registEventListener("scene", "mouseDraging", function (event, client) {
if (currentSelectNode.value != null) {
currentSelectNode.value.fx = currentSelectNode.value.x
currentSelectNode.value.fy = currentSelectNode.value.y
}
})
graphVis.registEventListener("scene", "mouseDragEnd", function (event, client) {
if (currentSelectNode.value != null) {
forceSimulator.alphaTarget(0)
//如果拖动结束需要固定拖拽的节点,则注释下面两行,保留最后拖动的位置即可
//that.currentSelectNode.fx = null;
//that.currentSelectNode.fy = null;
currentSelectNode.value = null
}
})
}
2025-08-08 15:08:10 +08:00
const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
function handleLayoutSuccess() {
//处理四个关系图的差异函数
const handleGroupDiscoveryDiff = () => {
console.log(storeId)
}
const handleGroupStructureDiff = () => {
console.log(storeId)
}
const handleGroupMemberDiff = () => {
console.log(storeId)
}
2025-08-08 09:47:19 +08:00
2025-08-08 15:08:10 +08:00
const handleAnomalousGroup = () => {
console.log(storeId)
}
2025-08-08 15:08:10 +08:00
new Map([
["groupDiscovery", () => handleGroupDiscoveryDiff()],
["groupStructure", () => handleGroupStructureDiff()],
["groupMember", () => handleGroupMemberDiff()],
["anomalousGroup", () => handleAnomalousGroup()]
]).get(storeId)?.()
graphVis.zoomFit() //场景视图大小自适应缩放
// graphVis.hideIsolatedNodes() //隐藏孤立节点
}
graphVis.excuteWorkerLayout(graphVis.getGraphData(), layoutType, layoutConfig, isAsync, () =>
handleLayoutSuccess()
)
}
// 根据节点的cluster属性进行分组
const clusterAnalyze = () => {
graphVis.removeAllGroup() // 清除原有分组
// 创建cluster到nodes的映射
const clusterNodesMap = new Map()
graphVis.nodes.forEach((node) => {
const cluster = parseInt(node.type)
if (!clusterNodesMap.has(cluster)) {
clusterNodesMap.set(cluster, [])
2025-08-07 10:20:33 +08:00
}
clusterNodesMap.get(cluster).push(node)
2025-08-07 10:00:14 +08:00
})
2025-08-08 17:41:30 +08:00
let colorMap = {
0: "50,141,120", // 绿色
1: "133,129,48", // 黄色
6: "12,112,144" // 蓝色
}
clusterNodesMap.forEach((nodes, cluster) => {
const color = colorMap[cluster]
nodes.forEach((node) => {
node.fillColor = color
node.color = color
})
2025-08-08 15:08:10 +08:00
graphVis.addNodesInGroup(nodes, {
2025-08-11 09:21:20 +08:00
shape: "circle", //circle|rect|polygon|bubbleset
color: color,
2025-08-08 15:08:10 +08:00
alpha: 0.2
})
2025-08-07 10:00:14 +08:00
})
graphVis.autoGroupLayout(graphVis.nodes)
function handleGroupDiscoveryDiff() {
console.log(storeId)
}
function handleGroupStructureDiff() {
console.log(storeId)
}
function handleGroupMemberDiff() {
console.log(storeId)
}
function handleAnomalousGroup() {
console.log(storeId)
2025-08-08 17:41:30 +08:00
// 当前时间从 store 取;没有就用 T0
const now = (props.store.currentUtc);
const TA = "2024-06-19T08:57:55Z"; // A
const TB = "2024-06-19T10:58:03Z"; // B
const TC = "2024-06-19T12:58:04Z"; // C
// 组色(与你现有 colorMap 一致)
const GROUP_COLOR = { 0: "12,112,144", 1: "180,150,20", 6: "50,141,120" };
const GROUP_ALPHA = 0.30;
const RED = "220,50,60";
// 覆盖画三大组大圆(蓝/黄/绿透明)
/* const buckets = new Map();
graphVis.nodes.forEach(n => {
const t = parseInt(n.type); // "0"|"1"|"6" → 0/1/6
if (!buckets.has(t)) buckets.set(t, []);
buckets.get(t).push(n);
});
buckets.forEach((nodes, t) => {
const color = GROUP_COLOR[t] || "120,120,120";
const g = graphVis.addNodesInGroup(nodes, { shape: "circle", color, alpha: GROUP_ALPHA });
g.smoothPath = false;
}); */
2025-08-11 09:21:20 +08:00
// 时间门控:达到阈值就把各组异常节点染红
2025-08-08 17:41:30 +08:00
const shouldA = now >= TA;
const shouldB = now >= TB;
const shouldC = now >= TC;
if(shouldA) {
graphVis.nodes = graphVis.nodes.map(n => {
if(props.store.graphAbnormalData.groupA.includes(n.id)) {
n.fillColor = RED;
}
return n;
})
2025-08-11 09:21:20 +08:00
console.log(graphVis.nodes)
graphVis.addNodesInGroup(graphVis.nodes.filter(n => props.store.graphAbnormalData.groupA.includes(n.id)), {
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
})
2025-08-08 17:41:30 +08:00
}
if(shouldB) {
graphVis.nodes = graphVis.nodes.map(n => {
if(props.store.graphAbnormalData.groupB.includes(n.id)) {
n.fillColor = RED;
}
return n;
})
2025-08-11 09:21:20 +08:00
graphVis.addNodesInGroup(graphVis.nodes.filter(n => props.store.graphAbnormalData.groupB.includes(n.id)), {
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
})
2025-08-08 17:41:30 +08:00
}
if(shouldC) {
graphVis.nodes = graphVis.nodes.map(n => {
if(props.store.graphAbnormalData.groupC.includes(n.id)) {
n.fillColor = RED;
}
return n;
})
2025-08-11 09:21:20 +08:00
graphVis.addNodesInGroup(graphVis.nodes.filter(n => props.store.graphAbnormalData.groupC.includes(n.id)), {
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
})
2025-08-08 17:41:30 +08:00
}
graphVis.autoGroupLayout(graphVis.nodes);
graphVis.zoomFit(); // 没有 refresh这两个就够
}
new Map([
["groupDiscovery", () => handleGroupDiscoveryDiff()],
["groupStructure", () => handleGroupStructureDiff()],
["groupMember", () => handleGroupMemberDiff()],
["anomalousGroup", () => handleAnomalousGroup()]
]).get(storeId)?.()
2025-08-08 09:47:19 +08:00
// graphVis.selectedEdge(graphVis.links[0])
}
2025-08-08 17:41:30 +08:00
// 仅对“异常群体模块”生效:时间变化时强制重绘一次
2025-08-11 09:20:57 +08:00
if (storeId === "anomalousGroup") {
2025-08-08 17:41:30 +08:00
watch(
() => props.store.currentUtc,
() => {
// 不改 graph 数据结构,直接用当前 graph 强制走一遍addGraph → clusterAnalyze → runForceLayout
if (graphVis) {
2025-08-11 09:20:57 +08:00
updateChart(toRaw(graph.value))
2025-08-08 17:41:30 +08:00
}
}
2025-08-11 09:20:57 +08:00
)
2025-08-08 17:41:30 +08:00
}
// 实例化 GraphVis、注册自定义绘制/事件
const createGraph = () => {
if (!graphVis) {
graphVis = new GraphVis({
container: document.getElementById("container"),
licenseKey: "hbsy",
config: defaultConfig
})
}
graphVis.setDragHideLine(false) //拖拽时隐藏连线
graphVis.setShowDetailScale(0.1) //展示细节的比例
graphVis.setZoomRange(0.1, 5) //缩放区间
registCustomePaintFunc() //注册自定义绘图方法
registEvents()
2025-08-07 10:00:14 +08:00
}
const initChart = () => {
createGraph()
graphVis.addGraph({ ...toRaw(graph.value) })
clusterAnalyze()
2025-08-08 15:08:10 +08:00
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
}
2025-08-07 10:35:34 +08:00
// 添加更新图表的函数
const updateChart = (newGraphData) => {
if (!graphVis) {
2025-08-07 14:54:33 +08:00
initChart()
return
2025-08-07 10:35:34 +08:00
}
2025-08-07 14:54:33 +08:00
graphVis.clearAll()
graphVis.addGraph({ ...toRaw(newGraphData) })
2025-08-07 10:35:34 +08:00
// 重新运行力导向布局
clusterAnalyze()
2025-08-08 15:08:10 +08:00
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
2025-08-06 15:01:12 +08:00
}
2025-08-07 14:54:33 +08:00
let lastLength = 0 //记录上一次的长度
2025-08-07 10:35:34 +08:00
watch(
graph,
(newValue) => {
2025-08-08 15:08:10 +08:00
if (newValue) {
2025-08-07 14:54:33 +08:00
updateChart(newValue)
2025-08-07 10:35:34 +08:00
}
},
2025-08-07 14:54:33 +08:00
{ deep: true }
)
2025-08-05 14:23:32 +08:00
</script>
<style scoped lang="less">
.groupGraph-component {
width: 100%;
height: 100%;
2025-08-05 14:23:32 +08:00
position: relative;
.titleImage {
margin: 0 auto;
}
.container {
width: 100%;
height: 503px;
}
.timeList {
width: 95%;
height: 42px;
position: absolute;
left: 20px;
bottom: 20px;
z-index: 1;
}
}
</style>