This commit is contained in:
duanhao 2025-08-07 10:03:04 +08:00
commit ced8b052d1
8 changed files with 1695 additions and 25227 deletions

View 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);
}

File diff suppressed because it is too large Load Diff

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

View File

@ -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
View 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);
}

View File

@ -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 = () => {
}
},
// isAbnormalfalse===>:
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
})

View File

@ -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 .1500.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">