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

370 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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"
:is-auto-play="false"
:start-time="new Date(timeList[0])"
:end-time="new Date(timeList[timeList.length - 1])"
@click:pointerDown="handlePointerDown"
@slide:pointerUp="handlePointerDown"
></TimeAxis>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, toRaw, watch } from "vue"
import { storeToRefs } from "pinia"
import { convertToUtcIsoString } from "@/utils/transform"
import { paintNodeFunction, paintLineFunction } from "@/utils/customePaint"
import TimeAxis from "@/components/timeAxis.vue"
import GraphVis from "@/assets/package/graphvis.esm.min.js"
const props = defineProps({
store: {
required: true
}
})
const emit = defineEmits(["click:pointerDownAndSlide"])
const { timeList, graph } = storeToRefs(props.store)
let isPlay = ref(false)
let graphVis = null
let forceSimulator = null
let currentSelectNode = ref(null)
const storeId = props.store.$id
const defaultConfig = {
node: {
label: {
show: true,
font: "normal 14px KaiTi",
color: "250,250,250",
textPosition: "Middle_Center", //Middle_Center
textOffsetY: 10
},
shape: "circle",
size: storeId == "anomalousGroup" ? 15 : 40,
borderColor: "200,50,50",
borderWidth: 0,
selected: {
borderWidth: 5,
borderColor: "100,250,100",
showShadow: true, // 是否展示阴影
shadowBlur: 10, //阴影范围大小
shadowColor: "50,250,30" // 选中是的阴影颜色
}
},
link: {
label: {
// 连线标签
show: false, // 是否显示
color: "245,245,245", // 字体颜色
font: "normal 11px KaiTi", // 字体大小及类型
background: "255,255,255" //文字背景色(设置后文字居中,一般与画布背景色一致)
},
lineType: "straight", // curver
showArrow: false,
lineWidth: 1,
colorType: "both",
color: "240,240,240",
selected: {
color: "100,250,100"
}
},
highLightNeiber: true // 相邻节点高亮开关
}
const registCustomePaintFunc = (curStoreId) => {
graphVis.definedNodePaintFunc(paintNodeFunction(curStoreId)) //自定义节点绘图方法
graphVis.definedLinkPaintFunc(paintLineFunction) //自定义关系绘图方法
}
// 处理时间轴点击事件和拉动
const handlePointerDown = (time) => {
const utcTime = convertToUtcIsoString(time)
emit("click:pointerDownAndSlide", utcTime)
}
const registEvents = () => {
const simulation = graphVis.getSimulationLayout()
forceSimulator = simulation.forceSimulation()
//全局记录包裹层元素
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)
//如果拖动结束需要固定拖拽的节点,则注释下面两行,保留最后拖动的位置即可
currentSelectNode.value.fx = null
currentSelectNode.value.fy = null
currentSelectNode.value = null
}
})
}
//公用连线或节点高亮函数
const highLightAboutNodesOrLinks = (type) => {
graphVis.cancelAllSelected()
const { newNodes, newLinks } = graph.value
if (type == "nodes") {
//实现高亮节点逻辑
console.log(graphVis.nodes)
graphVis.nodes.forEach((node) =>
newNodes.forEach((newNode) => {
if (node.id === newNode.name) {
node.selected = true
}
})
)
} else if (type == "links") {
//实现连线高亮逻辑
graphVis.links.forEach((link) =>
newLinks.forEach((newLink) => {
if (link.source.id === newLink.source && link.target.id === newLink.target) {
link.selected = true
}
})
)
} else {
return
}
}
const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
function handleLayoutSuccess() {
//处理四个关系图的差异函数
const handleGroupDiscoveryDiff = () => {
return
}
const handleGroupStructureDiff = () => {
highLightAboutNodesOrLinks("links")
}
const handleGroupMemberDiff = () => {
highLightAboutNodesOrLinks("nodes")
}
const handleAnomalousGroup = () => {
// 当前时间从 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_ALPHA = 0.3
const RED = "220,50,60"
// 时间门控:达到阈值就把各组异常节点染红
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
})
graphVis.addNodesInGroup(
graphVis.nodes.filter((n) => props.store.graphAbnormalData.groupA.includes(n.id)),
{
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
}
)
}
if (shouldB) {
graphVis.nodes = graphVis.nodes.map((n) => {
if (props.store.graphAbnormalData.groupB.includes(n.id)) {
n.fillColor = RED
}
return n
})
graphVis.addNodesInGroup(
graphVis.nodes.filter((n) => props.store.graphAbnormalData.groupB.includes(n.id)),
{
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
}
)
}
if (shouldC) {
graphVis.nodes = graphVis.nodes.map((n) => {
if (props.store.graphAbnormalData.groupC.includes(n.id)) {
n.fillColor = RED
}
return n
})
graphVis.addNodesInGroup(
graphVis.nodes.filter((n) => props.store.graphAbnormalData.groupC.includes(n.id)),
{
shape: "circle",
color: RED,
alpha: GROUP_ALPHA
}
)
}
// graphVis.autoGroupLayout(graphVis.nodes)
}
new Map([
["groupDiscovery", () => handleGroupDiscoveryDiff()],
["groupStructure", () => handleGroupStructureDiff()],
["groupMember", () => handleGroupMemberDiff()],
["anomalousGroup", () => handleAnomalousGroup()]
]).get(storeId)?.()
graphVis.zoomFit() //场景视图大小自适应缩放
}
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, [])
}
clusterNodesMap.get(cluster).push(node)
})
let colorMap = {
0: "75,241,184", // 绿色
1: "250,222,37", // 黄色
6: "69,192,242" // 蓝色
}
clusterNodesMap.forEach((nodes, cluster) => {
const color = colorMap[cluster]
nodes.forEach((node) => {
node.fillColor = color
node.color = color
})
if (storeId !== "anomalousGroup") {
graphVis.addNodesInGroup(nodes, {
shape: "polygon", //circle|rect|polygon|bubbleset
color: color,
alpha: 0.2
})
}
})
// graphVis.autoGroupLayout(graphVis.nodes)
}
// 仅对“异常群体模块”生效:时间变化时强制重绘一次
if (storeId === "anomalousGroup") {
watch(
() => props.store.currentUtc,
() => {
// 不改 graph 数据结构,直接用当前 graph 强制走一遍addGraph → clusterAnalyze → runForceLayout
if (graphVis) {
updateChart(toRaw(graph.value))
}
}
)
}
// 实例化 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(storeId) //注册自定义绘图方法
registEvents()
}
const initChart = () => {
createGraph()
graphVis.addGraph({ ...toRaw(graph.value) })
clusterAnalyze()
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
}
// 添加更新图表的函数
const updateChart = (newGraphData) => {
if (!graphVis) {
initChart()
return
}
graphVis.clearAll()
graphVis.addGraph({ ...toRaw(newGraphData) })
// 重新运行力导向布局
clusterAnalyze()
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
}
watch(
graph,
(newValue) => {
if (newValue) {
updateChart(newValue)
}
},
{ deep: true }
)
</script>
<style scoped lang="less">
.groupGraph-component {
width: 100%;
height: 100%;
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>