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

267 lines
8.0 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-07 10:00:14 +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 10:00:14 +08:00
import {
defineProps,
defineEmits,
onMounted,
onUnmounted,
nextTick,
ref,
toRaw,
watchEffect,
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 { demoData } from "@/assets/customeGraph/data"
import TimeAxis from "@/components/timeAxis.vue"
import GraphVis from "@/assets/package/graphvis.esm.min.js"
2025-08-05 14:23:32 +08:00
const props = defineProps({
store: {
required: true
}
})
const emit = defineEmits(["click:pointerDownAndSlide"])
// 解构 store 中的 state
2025-08-06 15:01:12 +08:00
const { timeList, graph } = storeToRefs(props.store)
2025-08-07 10:00:14 +08:00
let graphVis = null
let forceSimulator = null
let currentSelectNode = ref(null)
let isGraphVisReady = false // 添加一个标志位
const defaultConfig = {
node: {
label: {
show: true,
font: "normal 14px KaiTi",
color: "250,250,250",
textPosition: "Bottom_Center", //Middle_Center
textOffsetY: 10
},
shape: "circle",
size: 30,
color: "120,120,240",
borderColor: "200,50,50",
borderWidth: 0,
selected: {
borderWidth: 2,
borderColor: "100,250,100",
showShadow: true, // 是否展示阴影
shadowBlur: 30, //阴影范围大小
shadowColor: "50,250,30" // 选中是的阴影颜色
}
},
link: {
label: {
// 连线标签
show: false, // 是否显示
color: "245,245,245", // 字体颜色
font: "normal 11px KaiTi", // 字体大小及类型
background: "255,255,255" //文字背景色(设置后文字居中,一般与画布背景色一致)
},
lineType: "curver", //straight
showArrow: false,
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
}
})
}
const runForceLayout = () => {
let viewCenter = graphVis.getViewCenter()
let simulation = graphVis.getSimulationLayout()
let curForceSimulator = simulation.forceSimulation()
curForceSimulator
.nodes(graphVis.nodes)
.force("center", simulation.forceCenter(viewCenter.x, viewCenter.y))
.force("charge", simulation.forceManyBody().strength(-550).theta(0.85)) // manyBodyReuse|forceManyBody .distanceMin(300).distanceMax(400).theta(0.9)
.force("link", simulation.forceLink(graphVis.links).distance(120).strength(0.35)) //.distance(120).strength(0.15)
.force(
"collide",
simulation.forceCollide().radius((d) => d.radius + 5)
) //.iterations(5)
.force("x", simulation.forceX())
.force("y", simulation.forceY())
.alphaDecay(0.02) //设置 alpha 衰减率.迭代150默认0.0228
//.alphaMin(0.005) //须要在 [0, 1] 之间。若是没有指定 min 则返回当前的最小 alpha 值,默认为 0.001. 在仿真内部,会不断的减少 alpha 值直到 alpha 值小于 最小 alpha
.velocityDecay(0.15) //默认为 0.4,较低的衰减系数可使得迭代次数更多,其布局结果也会更理性,可是可能会引发数值不稳定从而致使震荡。
curForceSimulator.alpha(1).restart()
curForceSimulator.on("tick", () => {
2025-08-07 10:20:33 +08:00
if (graphVis) {
graphVis.refreshView() // 刷新视图
}
2025-08-07 10:00:14 +08:00
})
curForceSimulator.on("end", () => {
curForceSimulator.alpha(0)
curForceSimulator.stop()
})
forceSimulator = curForceSimulator
}
2025-08-06 15:01:12 +08:00
const initChart = () => {
2025-08-07 10:00:14 +08:00
const getGroupCenters = (groupCount, width, height, radius = 200) => {
// 三组分布在三角形顶点
const angleStep = (2 * Math.PI) / groupCount
const centerX = width / 2
const centerY = height / 2
return Array.from({ length: groupCount }).map((_, i) => ({
x: centerX + Math.cos(i * angleStep) * radius,
y: centerY + Math.sin(i * angleStep) * radius
}))
2025-08-06 15:01:12 +08:00
}
2025-08-07 10:00:14 +08:00
const assignNodePositions = (nodes, groupCenters) => {
const groupMap = {}
nodes.forEach((node) => {
const groupIdx = node.groupId || 0
if (!groupMap[groupIdx]) groupMap[groupIdx] = []
groupMap[groupIdx].push(node)
})
Object.entries(groupMap).forEach(([groupIdx, groupNodes]) => {
const center = groupCenters[groupIdx]
groupNodes.forEach((node, i) => {
// 每组节点在各自中心附近随机分布
node.x = center.x + Math.random() * 80 - 40
node.y = center.y + Math.random() * 80 - 40
})
})
2025-08-06 15:01:12 +08:00
}
2025-08-07 10:00:14 +08:00
const createGraph = () => {
if (!graphVis) {
graphVis = new GraphVis({
container: document.getElementById("container"),
licenseKey: "hbsy",
config: defaultConfig
})
2025-08-06 15:01:12 +08:00
}
2025-08-07 10:00:14 +08:00
graphVis.setDragHideLine(false) //拖拽时隐藏连线
graphVis.setShowDetailScale(0.1) //展示细节的比例
graphVis.setZoomRange(0.1, 5) //缩放区间
registCustomePaintFunc() //注册自定义绘图方法
registEvents()
2025-08-06 15:01:12 +08:00
}
2025-08-07 10:20:33 +08:00
// const groupCount = 3 // 分组数量
// const width = 900,
// height = 500 // 画布大小
// const groupCenters = getGroupCenters(groupCount, width, height)
// assignNodePositions(graph.value.nodes, groupCenters)
2025-08-07 10:00:14 +08:00
createGraph()
2025-08-07 10:20:33 +08:00
console.log(graph.value)
2025-08-07 10:21:15 +08:00
2025-08-07 10:00:14 +08:00
graphVis.addGraph({ ...toRaw(graph.value) })
runForceLayout()
2025-08-06 15:01:12 +08:00
}
2025-08-07 10:20:33 +08:00
onMounted(() => {
2025-08-06 15:01:12 +08:00
initChart()
2025-08-07 10:21:15 +08:00
2025-08-06 15:01:12 +08:00
})
2025-08-07 10:00:14 +08:00
onUnmounted(() => {
if (graphVis) {
graphVis.destroy() // 如果 GraphVis 提供了销毁方法
graphVis = null
}
})
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>