Merge branch 'master' of http://172.16.20.1:3000/duanhao/SocialNetworks_duan
This commit is contained in:
		
						commit
						ced8b052d1
					
				
							
								
								
									
										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
											
										
									
								
							| 
						 | 
				
			
			@ -208,10 +208,22 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
 | 
			
		|||
 | 
			
		||||
    //根据时间参数获取节点数据
 | 
			
		||||
    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)
 | 
			
		||||
      if (res.code != 200) return
 | 
			
		||||
      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
 | 
			
		||||
        .filter((node) => {
 | 
			
		||||
          if (!newSet.has(node.name)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,8 +234,8 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
 | 
			
		|||
        })
 | 
			
		||||
        .map((node) => ({
 | 
			
		||||
          id: node.name,
 | 
			
		||||
          name: node.name,
 | 
			
		||||
          groupId: node.groupId
 | 
			
		||||
          label: node.name,
 | 
			
		||||
          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",
 | 
			
		||||
          群体三: "#fddc33"
 | 
			
		||||
        }
 | 
			
		||||
        if(props.moduleName == "群体成员演化分析"){
 | 
			
		||||
        if (props.moduleName == "群体成员演化分析") {
 | 
			
		||||
          color = {
 | 
			
		||||
            分裂指数: "#2AB8FD",
 | 
			
		||||
            合并指数: "#02D7DA",
 | 
			
		||||
| 
						 | 
				
			
			@ -147,11 +147,11 @@ const initChart = () => {
 | 
			
		|||
            扩展指数: "#EB57B0"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if(props.moduleName == "异常行为分析"){
 | 
			
		||||
        if (props.moduleName == "异常行为分析") {
 | 
			
		||||
          color = {
 | 
			
		||||
            社团组一: "#2AB8FD",
 | 
			
		||||
            社团组二: "#02D7DA",
 | 
			
		||||
            社团组三: "#FFDA09",
 | 
			
		||||
            社团组三: "#FFDA09"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        const listHtml = params
 | 
			
		||||
| 
						 | 
				
			
			@ -242,32 +242,33 @@ const initChart = () => {
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    // 判断是否为异常群体模块:isAbnormal默认为false===>正常模块:异常群体模块
 | 
			
		||||
    series: !props.isAbnormal ? props.chartData.seriesList.map((series) => ({
 | 
			
		||||
      ...series,
 | 
			
		||||
      type: "line",
 | 
			
		||||
      itemStyle: {
 | 
			
		||||
        color: "#061a2f",
 | 
			
		||||
        borderColor: series.themeColor, // 使用线条颜色作为边框色
 | 
			
		||||
        borderWidth: 2
 | 
			
		||||
      },
 | 
			
		||||
      symbol: "circle",
 | 
			
		||||
      symbolSize: 10,
 | 
			
		||||
      // 确保lineStyle存在
 | 
			
		||||
      lineStyle: {
 | 
			
		||||
        color: series.themeColor,
 | 
			
		||||
        width: 1
 | 
			
		||||
      }
 | 
			
		||||
    }))
 | 
			
		||||
    : props.chartData.seriesList.map((series) => ({
 | 
			
		||||
      ...series,
 | 
			
		||||
      type: "bar",
 | 
			
		||||
      itemStyle: {
 | 
			
		||||
        color: series.themeColor,
 | 
			
		||||
        borderColor: series.themeColor, // 使用线条颜色作为边框色
 | 
			
		||||
        borderWidth: 1,
 | 
			
		||||
        borderRadius: [8,8,0,0]
 | 
			
		||||
      }
 | 
			
		||||
    }))
 | 
			
		||||
    series: !props.isAbnormal
 | 
			
		||||
      ? props.chartData.seriesList.map((series) => ({
 | 
			
		||||
          ...series,
 | 
			
		||||
          type: "line",
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            color: "#061a2f",
 | 
			
		||||
            borderColor: series.themeColor, // 使用线条颜色作为边框色
 | 
			
		||||
            borderWidth: 2
 | 
			
		||||
          },
 | 
			
		||||
          symbol: "circle",
 | 
			
		||||
          symbolSize: 10,
 | 
			
		||||
          // 确保lineStyle存在
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: series.themeColor,
 | 
			
		||||
            width: 1
 | 
			
		||||
          }
 | 
			
		||||
        }))
 | 
			
		||||
      : props.chartData.seriesList.map((series) => ({
 | 
			
		||||
          ...series,
 | 
			
		||||
          type: "bar",
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            color: series.themeColor,
 | 
			
		||||
            borderColor: series.themeColor, // 使用线条颜色作为边框色
 | 
			
		||||
            borderWidth: 1,
 | 
			
		||||
            borderRadius: [8, 8, 0, 0]
 | 
			
		||||
          }
 | 
			
		||||
        }))
 | 
			
		||||
  }
 | 
			
		||||
  chartInstance.setOption(option)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +288,9 @@ watch(
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  console.log("111")
 | 
			
		||||
 | 
			
		||||
  console.log(props.moduleName)
 | 
			
		||||
  // 获取容器宽度
 | 
			
		||||
  sliderContainerWidth.value = document.querySelector(".slider-container").offsetWidth
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
      <TimeAxis
 | 
			
		||||
        v-if="timeList.length"
 | 
			
		||||
        :time-list="timeList"
 | 
			
		||||
        :is-auto-play="true"
 | 
			
		||||
        :is-auto-play="false"
 | 
			
		||||
        :start-time="new Date(timeList[0])"
 | 
			
		||||
        :end-time="new Date(timeList[timeList.length - 1])"
 | 
			
		||||
        @click:pointerDown="handlePointerDown"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,28 +17,225 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<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 * as echarts from "echarts"
 | 
			
		||||
import TimeAxis from "@/components/timeAxis.vue"
 | 
			
		||||
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({
 | 
			
		||||
  store: {
 | 
			
		||||
    required: true
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
let chart = null
 | 
			
		||||
const emit = defineEmits(["click:pointerDownAndSlide"])
 | 
			
		||||
// 解构 store 中的 state
 | 
			
		||||
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 utcTime = convertToUtcIsoString(time)
 | 
			
		||||
  emit("click:pointerDownAndSlide", utcTime)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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", () => {
 | 
			
		||||
    graphVis.refreshView() //刷新视图
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  curForceSimulator.on("end", () => {
 | 
			
		||||
    curForceSimulator.alpha(0)
 | 
			
		||||
    curForceSimulator.stop()
 | 
			
		||||
  })
 | 
			
		||||
  forceSimulator = curForceSimulator
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const initChart = () => {
 | 
			
		||||
  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
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
  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
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    graphVis.setDragHideLine(false) //拖拽时隐藏连线
 | 
			
		||||
    graphVis.setShowDetailScale(0.1) //展示细节的比例
 | 
			
		||||
    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>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user