关系图
This commit is contained in:
parent
90124585f1
commit
68e8d78100
62
src/assets/customeGraph/customePaint.js
Normal file
62
src/assets/customeGraph/customePaint.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
//自定义绘制节点的方法
|
||||||
|
export const paintNodeFunction = function (ctx, onlyShape) {
|
||||||
|
|
||||||
|
//默认绘制数据类型图片
|
||||||
|
if(this.properties.typeIcon){
|
||||||
|
if(this.selected || this.showSelected){
|
||||||
|
this.drawShape(ctx, onlyShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.alpha < 1){
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = this.alpha;
|
||||||
|
ctx.drawImage(this.properties.typeIcon,-this.width/2, -this.height/2,this.width,this.height);
|
||||||
|
ctx.restore();
|
||||||
|
}else{
|
||||||
|
ctx.drawImage(this.properties.typeIcon,-this.width/2, -this.height/2,this.width,this.height);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
this.drawShape(ctx, onlyShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
//按节点类型绘制节点边框
|
||||||
|
if(this.properties.type != 'normal'){
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0,0,this.radius+6,0,Math.PI*2);
|
||||||
|
ctx.lineWidth = 8;
|
||||||
|
ctx.strokeStyle = `rgba(${this.fillColor},${this.alpha * 0.4 })`;
|
||||||
|
ctx.stroke();
|
||||||
|
}else{
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0,0,this.radius+8,0,Math.PI*2);
|
||||||
|
ctx.lineWidth = 12;
|
||||||
|
ctx.setLineDash([4,4]);
|
||||||
|
ctx.strokeStyle = `rgba(${this.fillColor},${this.alpha * 0.6 })`;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paintText(ctx); //调用内部方法绘制文字
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//自定义连线的方法
|
||||||
|
export const paintLineFunction = function (ctx, needHideText) {
|
||||||
|
this.drawOriginalLine(ctx, needHideText); //内置默认的绘制方法
|
||||||
|
|
||||||
|
//指定路径,用于鼠标检测选中
|
||||||
|
// this.path = [
|
||||||
|
// { x: this.source.cx, y: this.source.cy },
|
||||||
|
// { x: this.target.cx, y: this.target.cy }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// //绘制路径
|
||||||
|
// ctx.beginPath();
|
||||||
|
// ctx.moveTo(this.source.cx,this.source.cy);
|
||||||
|
// ctx.lineTo(this.target.cx,this.target.cy);
|
||||||
|
// this.setLineStyle(ctx);
|
||||||
|
// ctx.stroke();
|
||||||
|
|
||||||
|
// //绘制连线上文字(内部方法)
|
||||||
|
// this.paintTextOnLineWithAngle(ctx, this.source, this.target);
|
||||||
|
|
||||||
|
}
|
||||||
1300
src/assets/customeGraph/data.js
Normal file
1300
src/assets/customeGraph/data.js
Normal file
File diff suppressed because it is too large
Load Diff
10
src/assets/package/graphvis.esm.min.js
vendored
Normal file
10
src/assets/package/graphvis.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -205,10 +205,22 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
|
||||||
|
|
||||||
//根据时间参数获取节点数据
|
//根据时间参数获取节点数据
|
||||||
async initialGraphByUtcTime(utcTime = "") {
|
async initialGraphByUtcTime(utcTime = "") {
|
||||||
|
const setColor = (groupId) => {
|
||||||
|
const colorMap = {
|
||||||
|
0: "50,141,120",
|
||||||
|
1: "133,129,48",
|
||||||
|
6: "12,112,144"
|
||||||
|
}
|
||||||
|
return colorMap[parseInt(groupId)]
|
||||||
|
}
|
||||||
const res = await getRelationGraphByUtcTime(utcTime)
|
const res = await getRelationGraphByUtcTime(utcTime)
|
||||||
if (res.code != 200) return
|
if (res.code != 200) return
|
||||||
const newSet = new Set()
|
const newSet = new Set()
|
||||||
this.graph["links"] = res.data.links
|
this.graph["links"] = res.data.links.map((link) => ({
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
color: setColor(link.type)
|
||||||
|
}))
|
||||||
this.graph["nodes"] = res.data.nodes
|
this.graph["nodes"] = res.data.nodes
|
||||||
.filter((node) => {
|
.filter((node) => {
|
||||||
if (!newSet.has(node.name)) {
|
if (!newSet.has(node.name)) {
|
||||||
|
|
@ -219,8 +231,8 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
|
||||||
})
|
})
|
||||||
.map((node) => ({
|
.map((node) => ({
|
||||||
id: node.name,
|
id: node.name,
|
||||||
name: node.name,
|
label: node.name,
|
||||||
groupId: node.groupId
|
color: setColor(node.groupId)
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
71
src/utils/customePaint.js
Normal file
71
src/utils/customePaint.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
//自定义绘制节点的方法
|
||||||
|
export const paintNodeFunction = function (ctx, onlyShape) {
|
||||||
|
//默认绘制数据类型图片
|
||||||
|
if (this.properties.typeIcon) {
|
||||||
|
if (this.selected || this.showSelected) {
|
||||||
|
this.drawShape(ctx, onlyShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.alpha < 1) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.globalAlpha = this.alpha
|
||||||
|
ctx.drawImage(
|
||||||
|
this.properties.typeIcon,
|
||||||
|
-this.width / 2,
|
||||||
|
-this.height / 2,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
)
|
||||||
|
ctx.restore()
|
||||||
|
} else {
|
||||||
|
ctx.drawImage(
|
||||||
|
this.properties.typeIcon,
|
||||||
|
-this.width / 2,
|
||||||
|
-this.height / 2,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.drawShape(ctx, onlyShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
//按节点类型绘制节点边框
|
||||||
|
if (this.properties.type != "normal") {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(0, 0, this.radius + 6, 0, Math.PI * 2)
|
||||||
|
ctx.lineWidth = 8
|
||||||
|
ctx.strokeStyle = `rgba(${this.fillColor},${this.alpha * 0.4})`
|
||||||
|
ctx.stroke()
|
||||||
|
} else {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(0, 0, this.radius + 8, 0, Math.PI * 2)
|
||||||
|
ctx.lineWidth = 12
|
||||||
|
ctx.setLineDash([4, 4])
|
||||||
|
ctx.strokeStyle = `rgba(${this.fillColor},${this.alpha * 0.6})`
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paintText(ctx) //调用内部方法绘制文字
|
||||||
|
}
|
||||||
|
|
||||||
|
//自定义连线的方法
|
||||||
|
export const paintLineFunction = function (ctx, needHideText) {
|
||||||
|
this.drawOriginalLine(ctx, needHideText) //内置默认的绘制方法
|
||||||
|
|
||||||
|
//指定路径,用于鼠标检测选中
|
||||||
|
// this.path = [
|
||||||
|
// { x: this.source.cx, y: this.source.cy },
|
||||||
|
// { x: this.target.cx, y: this.target.cy }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// //绘制路径
|
||||||
|
// ctx.beginPath();
|
||||||
|
// ctx.moveTo(this.source.cx,this.source.cy);
|
||||||
|
// ctx.lineTo(this.target.cx,this.target.cy);
|
||||||
|
// this.setLineStyle(ctx);
|
||||||
|
// ctx.stroke();
|
||||||
|
|
||||||
|
// //绘制连线上文字(内部方法)
|
||||||
|
// this.paintTextOnLineWithAngle(ctx, this.source, this.target);
|
||||||
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ const initChart = () => {
|
||||||
群体二: "#00d6da",
|
群体二: "#00d6da",
|
||||||
群体三: "#fddc33"
|
群体三: "#fddc33"
|
||||||
}
|
}
|
||||||
if(props.moduleName == "群体成员演化分析"){
|
if (props.moduleName == "群体成员演化分析") {
|
||||||
color = {
|
color = {
|
||||||
分裂指数: "#2AB8FD",
|
分裂指数: "#2AB8FD",
|
||||||
合并指数: "#02D7DA",
|
合并指数: "#02D7DA",
|
||||||
|
|
@ -147,11 +147,11 @@ const initChart = () => {
|
||||||
扩展指数: "#EB57B0"
|
扩展指数: "#EB57B0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(props.moduleName == "异常行为分析"){
|
if (props.moduleName == "异常行为分析") {
|
||||||
color = {
|
color = {
|
||||||
社团组一: "#2AB8FD",
|
社团组一: "#2AB8FD",
|
||||||
社团组二: "#02D7DA",
|
社团组二: "#02D7DA",
|
||||||
社团组三: "#FFDA09",
|
社团组三: "#FFDA09"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const listHtml = params
|
const listHtml = params
|
||||||
|
|
@ -242,32 +242,33 @@ const initChart = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 判断是否为异常群体模块:isAbnormal默认为false===>正常模块:异常群体模块
|
// 判断是否为异常群体模块:isAbnormal默认为false===>正常模块:异常群体模块
|
||||||
series: !props.isAbnormal ? props.chartData.seriesList.map((series) => ({
|
series: !props.isAbnormal
|
||||||
...series,
|
? props.chartData.seriesList.map((series) => ({
|
||||||
type: "line",
|
...series,
|
||||||
itemStyle: {
|
type: "line",
|
||||||
color: "#061a2f",
|
itemStyle: {
|
||||||
borderColor: series.themeColor, // 使用线条颜色作为边框色
|
color: "#061a2f",
|
||||||
borderWidth: 2
|
borderColor: series.themeColor, // 使用线条颜色作为边框色
|
||||||
},
|
borderWidth: 2
|
||||||
symbol: "circle",
|
},
|
||||||
symbolSize: 10,
|
symbol: "circle",
|
||||||
// 确保lineStyle存在
|
symbolSize: 10,
|
||||||
lineStyle: {
|
// 确保lineStyle存在
|
||||||
color: series.themeColor,
|
lineStyle: {
|
||||||
width: 1
|
color: series.themeColor,
|
||||||
}
|
width: 1
|
||||||
}))
|
}
|
||||||
: props.chartData.seriesList.map((series) => ({
|
}))
|
||||||
...series,
|
: props.chartData.seriesList.map((series) => ({
|
||||||
type: "bar",
|
...series,
|
||||||
itemStyle: {
|
type: "bar",
|
||||||
color: series.themeColor,
|
itemStyle: {
|
||||||
borderColor: series.themeColor, // 使用线条颜色作为边框色
|
color: series.themeColor,
|
||||||
borderWidth: 1,
|
borderColor: series.themeColor, // 使用线条颜色作为边框色
|
||||||
borderRadius: [8,8,0,0]
|
borderWidth: 1,
|
||||||
}
|
borderRadius: [8, 8, 0, 0]
|
||||||
}))
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
chartInstance.setOption(option)
|
chartInstance.setOption(option)
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +288,7 @@ watch(
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log("111");
|
console.log("111")
|
||||||
|
|
||||||
console.log(props.moduleName)
|
console.log(props.moduleName)
|
||||||
// 获取容器宽度
|
// 获取容器宽度
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<TimeAxis
|
<TimeAxis
|
||||||
v-if="timeList.length"
|
v-if="timeList.length"
|
||||||
:time-list="timeList"
|
:time-list="timeList"
|
||||||
:is-auto-play="true"
|
:is-auto-play="false"
|
||||||
:start-time="new Date(timeList[0])"
|
:start-time="new Date(timeList[0])"
|
||||||
:end-time="new Date(timeList[timeList.length - 1])"
|
:end-time="new Date(timeList[timeList.length - 1])"
|
||||||
@click:pointerDown="handlePointerDown"
|
@click:pointerDown="handlePointerDown"
|
||||||
|
|
@ -17,151 +17,225 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, onMounted, watch, nextTick } from "vue"
|
import {
|
||||||
|
defineProps,
|
||||||
|
defineEmits,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
nextTick,
|
||||||
|
ref,
|
||||||
|
toRaw,
|
||||||
|
watchEffect,
|
||||||
|
watch
|
||||||
|
} from "vue"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import * as echarts from "echarts"
|
|
||||||
import TimeAxis from "@/components/timeAxis.vue"
|
|
||||||
import { convertToUtcIsoString } from "@/utils/transform"
|
import { convertToUtcIsoString } from "@/utils/transform"
|
||||||
import nodeHoverImg from "@/assets/images/nodeHover.png"
|
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"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
store: {
|
store: {
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let chart = null
|
|
||||||
const emit = defineEmits(["click:pointerDownAndSlide"])
|
const emit = defineEmits(["click:pointerDownAndSlide"])
|
||||||
// 解构 store 中的 state
|
// 解构 store 中的 state
|
||||||
const { timeList, graph } = storeToRefs(props.store)
|
const { timeList, graph } = storeToRefs(props.store)
|
||||||
|
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) //自定义关系绘图方法
|
||||||
|
}
|
||||||
// 处理时间轴点击事件和拉动
|
// 处理时间轴点击事件和拉动
|
||||||
const handlePointerDown = (time) => {
|
const handlePointerDown = (time) => {
|
||||||
const utcTime = convertToUtcIsoString(time)
|
const utcTime = convertToUtcIsoString(time)
|
||||||
emit("click:pointerDownAndSlide", utcTime)
|
emit("click:pointerDownAndSlide", utcTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
const initChart = () => {
|
const registEvents = () => {
|
||||||
if (chart == null) {
|
//全局记录包裹层元素
|
||||||
chart = echarts.init(document.getElementById("container"))
|
const containerDom = document.getElementById("container")
|
||||||
}
|
graphVis.registEventListener("node", "mouseOver", function (event, node) {
|
||||||
if (Object.keys(graph.value).length === 0) {
|
containerDom.style.cursor = "pointer"
|
||||||
chart.clear()
|
})
|
||||||
return
|
graphVis.registEventListener("node", "mouseOut", function (event, node) {
|
||||||
}
|
containerDom.style.cursor = ""
|
||||||
|
})
|
||||||
|
//节点开始拖动
|
||||||
|
graphVis.registEventListener("node", "mousedrag", function (event, node) {
|
||||||
|
currentSelectNode.value = node
|
||||||
|
|
||||||
function variableColorAndPos(groupId) {
|
//开始拖动,必须要设置的属性
|
||||||
const resultMap = {
|
currentSelectNode.value.fx = node.x
|
||||||
0: { color: "#1f8473", x: 100, y: 100 },
|
currentSelectNode.value.fy = node.y
|
||||||
1: { color: "#807d2c", x: 300, y: 300 },
|
forceSimulator.alphaTarget(0.3).restart()
|
||||||
6: { color: "#0c7090", x: 600, y: 600 }
|
})
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
return resultMap[parseInt(groupId)]
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const option = {
|
graphVis.registEventListener("scene", "mouseDragEnd", function (event, client) {
|
||||||
//hover上去的窗口
|
if (currentSelectNode.value != null) {
|
||||||
tooltip: {
|
forceSimulator.alphaTarget(0)
|
||||||
trigger: "item",
|
//如果拖动结束需要固定拖拽的节点,则注释下面两行,保留最后拖动的位置即可
|
||||||
backgroundColor: "rgba(0,0,0,0)", // 透明背景
|
//that.currentSelectNode.fx = null;
|
||||||
borderColor: "rgba(0,0,0,0)", // 透明边框
|
//that.currentSelectNode.fy = null;
|
||||||
borderWidth: 0,
|
currentSelectNode.value = null
|
||||||
extraCssText: "box-shadow:none;padding:0;",
|
}
|
||||||
formatter: function (params) {
|
})
|
||||||
if (params.dataType === "node") {
|
}
|
||||||
return `<div
|
const runForceLayout = () => {
|
||||||
style="
|
let viewCenter = graphVis.getViewCenter()
|
||||||
padding:10px 15px;
|
let simulation = graphVis.getSimulationLayout()
|
||||||
height: 68px;
|
let curForceSimulator = simulation.forceSimulation()
|
||||||
border-radius: 4px;
|
curForceSimulator
|
||||||
background: url('${nodeHoverImg}');
|
.nodes(graphVis.nodes)
|
||||||
background-size: cover;
|
.force("center", simulation.forceCenter(viewCenter.x, viewCenter.y))
|
||||||
background-position: center;
|
.force("charge", simulation.forceManyBody().strength(-550).theta(0.85)) // manyBodyReuse|forceManyBody .distanceMin(300).distanceMax(400).theta(0.9)
|
||||||
background-repeat: no-repeat;
|
.force("link", simulation.forceLink(graphVis.links).distance(120).strength(0.35)) //.distance(120).strength(0.15)
|
||||||
display: flex;
|
.force(
|
||||||
align-items: center;
|
"collide",
|
||||||
justify-content: center;
|
simulation.forceCollide().radius((d) => d.radius + 5)
|
||||||
flex-direction: column;
|
) //.iterations(5)
|
||||||
">
|
.force("x", simulation.forceX())
|
||||||
<div style="color:#fff;letter-spacing: 0.14px;">
|
.force("y", simulation.forceY())
|
||||||
<div >用户名:${params.data.name}</div>
|
.alphaDecay(0.02) //设置 alpha 衰减率.迭代150,默认0.0228
|
||||||
<div >组ID:${params.data.groupId}</div>
|
//.alphaMin(0.005) //须要在 [0, 1] 之间。若是没有指定 min 则返回当前的最小 alpha 值,默认为 0.001. 在仿真内部,会不断的减少 alpha 值直到 alpha 值小于 最小 alpha
|
||||||
</div>
|
.velocityDecay(0.15) //默认为 0.4,较低的衰减系数可使得迭代次数更多,其布局结果也会更理性,可是可能会引发数值不稳定从而致使震荡。
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
edgeLabel: {
|
|
||||||
show: true,
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 18,
|
|
||||||
textShadowColor: "#fff",
|
|
||||||
textShadowBlur: 0,
|
|
||||||
textShadowOffsetX: 0,
|
|
||||||
textShadowOffsetY: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: "graph",
|
|
||||||
layout: "force",
|
|
||||||
animation: false,
|
|
||||||
draggable: true,
|
|
||||||
roam: true,
|
|
||||||
zoom: 0.1,
|
|
||||||
force: {
|
|
||||||
initLayout: "circular", // 初始布局使用圆形
|
|
||||||
edgeLength: 6000,
|
|
||||||
repulsion: 5000,
|
|
||||||
gravity: 0.1,
|
|
||||||
friction: 0.02,
|
|
||||||
coolingFactor: 0.1
|
|
||||||
},
|
|
||||||
|
|
||||||
animationDurationUpdate: 3500, // 节点移动更平滑
|
curForceSimulator.alpha(1).restart()
|
||||||
data: graph.value?.nodes.map((node) => ({
|
|
||||||
...node,
|
curForceSimulator.on("tick", () => {
|
||||||
symbol: "circle",
|
graphVis.refreshView() //刷新视图
|
||||||
x: variableColorAndPos(node.groupId).x,
|
})
|
||||||
y: variableColorAndPos(node.groupId).y,
|
|
||||||
symbolSize: 40,
|
curForceSimulator.on("end", () => {
|
||||||
itemStyle: {
|
curForceSimulator.alpha(0)
|
||||||
color: variableColorAndPos(node.groupId).color,
|
curForceSimulator.stop()
|
||||||
opacity: 1,
|
})
|
||||||
borderColor: "#46C6AD",
|
forceSimulator = curForceSimulator
|
||||||
borderWidth: 1,
|
|
||||||
shadowBlur: 4,
|
|
||||||
borderType: "solid",
|
|
||||||
shadowColor: "rgba(19, 27, 114, 0.25)"
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
links: graph.value?.links,
|
|
||||||
lineStyle: {
|
|
||||||
color: "#37ACD7",
|
|
||||||
width: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
chart.setOption(option)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const initChart = () => {
|
||||||
initChart()
|
const getGroupCenters = (groupCount, width, height, radius = 200) => {
|
||||||
})
|
// 三组分布在三角形顶点
|
||||||
let lastPostsLength = 0 //当列表更新时,记录上一次的长度
|
const angleStep = (2 * Math.PI) / groupCount
|
||||||
watch(
|
const centerX = width / 2
|
||||||
() => graph.value,
|
const centerY = height / 2
|
||||||
(newValue) => {
|
return Array.from({ length: groupCount }).map((_, i) => ({
|
||||||
if (newValue.nodes.length > lastPostsLength) {
|
x: centerX + Math.cos(i * angleStep) * radius,
|
||||||
nextTick(() => {
|
y: centerY + Math.sin(i * angleStep) * radius
|
||||||
initChart()
|
}))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createGraph = () => {
|
||||||
|
if (!graphVis) {
|
||||||
|
graphVis = new GraphVis({
|
||||||
|
container: document.getElementById("container"),
|
||||||
|
licenseKey: "hbsy",
|
||||||
|
config: defaultConfig
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
lastPostsLength = newValue.nodes.length //实现按需更新
|
graphVis.setDragHideLine(false) //拖拽时隐藏连线
|
||||||
},
|
graphVis.setShowDetailScale(0.1) //展示细节的比例
|
||||||
{ deep: true }
|
graphVis.setZoomRange(0.1, 5) //缩放区间
|
||||||
)
|
registCustomePaintFunc() //注册自定义绘图方法
|
||||||
|
registEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupCount = 3 // 分组数量
|
||||||
|
const width = 900,
|
||||||
|
height = 500 // 画布大小
|
||||||
|
const groupCenters = getGroupCenters(groupCount, width, height)
|
||||||
|
assignNodePositions(graph.value.nodes, groupCenters)
|
||||||
|
createGraph()
|
||||||
|
graphVis.addGraph({ ...toRaw(graph.value) })
|
||||||
|
runForceLayout()
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick() // 等待 DOM 更新
|
||||||
|
initChart()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (graphVis) {
|
||||||
|
graphVis.destroy() // 如果 GraphVis 提供了销毁方法
|
||||||
|
graphVis = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user