feat: 按指定格式渲染nodes和links数据

This commit is contained in:
于磊奇 2025-06-14 14:27:40 +08:00
parent a1382b84ca
commit 027a4166c5
2 changed files with 2062 additions and 0 deletions

1433
src/data/graphData.ts Normal file

File diff suppressed because it is too large Load Diff

629
src/views/index.vue Normal file
View File

@ -0,0 +1,629 @@
<template>
<div class="sky-page-box sky-pageBgc-r" ref="pageBoxRef" id="visualize_2d">
<!-- 时间轴 -->
<div class="sky-time-box sky-flex-rsc">
<div>{{ dateFormat(temporalTimeMin, 'YYYY/MM/DD') }}</div>
<div class="sky-position-r sky-w-50 sky-ml-10 sky-mr-15 sky-mt-2 sky-w-500px h-5px">
<!-- 此处更换时间轴图标 -->
<img v-for="(item, index) in init_time_left" :key="index" src="./assets/img/initNode_time.svg?url"
style="position: absolute; left: 0%; transform: translate(-50%, -30%); z-index: 10"
:style="{ left: item }" alt="" />
<img v-for="(item, index) in importantNode_time_left" :key="index"
src="./assets/img/importantNode_time.svg?url"
style="position: absolute; transform: translate(-40%, -30%); z-index: 10" :style="{ left: item }"
alt="" />
<t-slider :min="temporalTimeMin" :max="temporalTimeMax" :show-tooltip="true" class="sky-timeSlider"
v-model="temporalTime" :label="handleTimeLabel" @change-end="changeTemporalTimeEnd">
<template #Lable="{ value }">
<span>当前值: {{ value * 2 }}</span>
</template>
<template #button>
<img src="./user/lao.png"
style="position: absolute; transform: translate(-50%, -100%)"
:style="{ left: temporalTick }" alt="" />
</template>
</t-slider>
</div>
<div>{{ dateFormat(temporalTimeMax, 'YYYY/MM/DD') }}</div>
<PlayCircleIcon v-if="temporalPlayShow" class="sky-fs-20 sky-hover-c-primary sky-ml-10"
@click="handleTemporaPlay" />
<PauseCircleIcon v-else class="sky-fs-20 sky-hover-c-primary sky-ml-10" @click="handleTemporaPause" />
</div>
<!--图显示区域-->
<div class="sky-graph-area sky-flex-rss sky-graph-area-h1">
<!-- 绘图面板区域 -->
<div ref="graphPanelRef" id="sky-graph-panel" style="width: 100%; height: calc(100% - 2px)"></div>
</div>
<t-loading v-if="isLayouting" class="sky-layoutLoading" text="正在布局..." size="small"></t-loading>
</div>
</template>
<script lang="tsx">
export default {
name: '',
}
</script>
<script lang="tsx" setup>
import { onMounted, reactive, ref, nextTick, toRaw, onBeforeUnmount, Ref } from 'vue'
import { PlayCircleIcon, PauseCircleIcon } from 'tdesign-icons-vue-next'
import GraphVis from '../assets/graphvis/graphvis.esm.min3.js'
import { getDefaultConfig } from '../assets/graphvis/defaultConfig.js'
import { getLayoutDefaultConfig } from '../assets/graphvis/layoutParamsDict.js'
import { MessagePlugin } from 'tdesign-vue-next'
import timeData from '../data/graphData'
import dayjs from 'dayjs'
import { id } from 'element-plus/es/locales.mjs'
const props = defineProps({
graphData: {
type: Object,
default: () => {
return {
nodes: [
{
id: '人民网',
type: 'user',
properties: { name: 'Lionel Roob', special_type: ['initial'], },
personProperties: {url:('src/views/user/boss.png')},
}
],
links: [
],
}
},
},
duSize: {
// node size
type: Boolean,
default: true,
},
})
const dateFormat = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return '暂无'
return dayjs(date).format(format)
}
let LicenseKey =
'01D01J01E01A01D01I01A01E01C01A01D00Z03003903303402P03202N03702R02X02R03303103402P037037'
const isLayouting = ref(false)
let visualizeData = reactive({
nodes: [],
links: [],
})
const initVisualizeData = () => {
const { nodes, links } = props.graphData
if (Array.isArray(links) && Array.isArray(nodes) && (links.length || nodes.length)) {
visualizeData = {
links: initLinkStyle(links, nodes),
nodes: initNodeStyle(nodes, links),
}
if (!validate) {
MessagePlugin.warning('暂不支持进入时序动态网络')
return
}
createGraph() //
nextTick(() => {
initGraphData() //
})
} else {
MessagePlugin.error('数据有误')
}
}
//
let graphVis: any = null
let graphPanelRef = ref(null)
const createGraph = () => {
graphVis = new GraphVis({
container: document.getElementById('sky-graph-panel'),
licenseKey: LicenseKey,
config: getDefaultConfig(),
})
graphVis.setDragHideLine(false) //线
graphVis.setShowDetailScale(0.1) //
graphVis.setZoomRange(0.1, 5) //
}
//
const initGraphData = () => {
graphVis.addGraph(visualizeData)
nextTick(() => {
layoutEvent('fastForce')
graphVis.zoomFit()
})
}
//
const setNodeSize = (nodelist = []) => {
let leg = nodelist.length
let size = 10
if (leg <= 10) {
size = 26
} else if (leg <= 30) {
size = 24
} else if (leg <= 50) {
size = 22
} else if (leg <= 100) {
size = 20
} else if (leg <= 500) {
size = 18
} else if (leg <= 1000) {
size = 16
} else if (leg <= 5000) {
size = 14
} else if (leg <= 10000) {
size = 12
} else {
size = 10
}
return size
}
//
const initNodeStyle = (nodeList = [], linkList = [], doAlgStyle = false) => {
let nodeIdCount = {}
let nodeIdsAll = []
let resNodes = []
linkList.forEach((link) => {
let source = link.source
if (source) {
if (nodeIdCount[source]) {
nodeIdCount[source]++
} else {
nodeIdsAll.push(source)
nodeIdCount[source] = 1
}
}
let target = link.target
if (target) {
if (nodeIdCount[target]) {
nodeIdCount[target]++
} else {
nodeIdsAll.push(target)
nodeIdCount[target] = 1
}
}
})
let descCountArr = Array.from(new Set(Object.values(nodeIdCount).sort()))
let ratio = descCountArr.length > 10 ? 10 : descCountArr.length
let maxVal = Math.max(...descCountArr)
let minVal = Math.min(...descCountArr)
let size = setNodeSize(nodeList)
let isolateNum = 0
let nodeObj = {} //
let maxSize = 0
nodeList.forEach((node) => {
if (!node.style) {
node.style = {}
}
//
let propName = node.properties?.name || node.properties?.author_name || ''
if (propName) {
node.label = propName
}
if (!node.color) {
node.color = '255,96,96'
}
if (!node.size) {
node.size = size
}
//
if (props.duSize && nodeIdCount[node.id] && maxVal > minVal) {
let enlargeNum = ((20 + 3 * ratio) * (nodeIdCount[node.id] - minVal)) / (maxVal - minVal)
node.size = Number(node.size) + Math.ceil(enlargeNum)
}
maxSize = Math.max(maxSize, node.size)
if (!nodeObj[node.id]) {
//
nodeObj[node.id] = 1
if (nodeIdsAll.includes(node.id)) {
//
resNodes.push({
...node,
})
} else if (isolateNum < 10) {
// 10
isolateNum++
resNodes.push({
...node,
})
}
} else {
nodeObj[node.id]++
}
})
handleTemporalTimeRange()
handleImportantTag(nodeList, linkList)
return resNodes
}
const setLinkWidth = (linkList = [], nodeList = []) => {
let leg = linkList.length + nodeList.length
let wd = 1
if (leg <= 10) {
wd = 3.25
} else if (leg <= 30) {
wd = 3
} else if (leg <= 50) {
wd = 2.75
} else if (leg <= 100) {
wd = 2.5
} else if (leg <= 500) {
wd = 2.25
} else if (leg <= 1000) {
wd = 2
} else if (leg <= 2000) {
wd = 1.75
} else if (leg <= 5000) {
wd = 1.5
} else if (leg <= 10000) {
wd = 1.25
} else {
wd = 1
}
return wd
}
//
let validate = false
//
const initLinkStyle = (links = [], nodes = []) => {
let wd = setLinkWidth(links, nodes)
let resLinks = []
links.forEach((link) => {
if (!link.lineWidth) {
link.lineWidth = wd
}
if (link.enlarge) {
const enlarge = Number(link.enlarge) || 1.5
link.lineWidth = Number(link.lineWidth) * enlarge
}
if (link.ranks) {
validate = true
}
//
if (!link.properties) {
link.properties = {}
}
if (!link.ranks) {
link.ranks = []
}
if (link.ranks) {
link.properties.ranks = link.ranks
temporalTimeRange.push(...link.properties.ranks)
}
resLinks.push({
...link,
})
})
return resLinks
}
//
let currentLayout = ref({
preLayout: 'fastForce',
type: '',
icon: 'el-icon-video-play',
text: '执行布局',
status: false,
layoutConfigText: '布局参数设置',
})
const layoutEvent = (layoutType, params = {}) => {
// console.log(12)
// if (stopOperation(true,false)) return;
currentLayout.value.type = layoutType
runLayout(params)
}
const runLayout = (params) => {
if (currentLayout.value.status) {
graphVis.stopLayout()
endLayoutCallback()
return
}
let layoutType = currentLayout.value.type
currentLayout.value.icon = 'el-icon-loading' //
currentLayout.value.text = '布局中,点击停止'
currentLayout.value.status = true
currentLayout.value.layoutConfigText = '布局中,点击停止'
let graphData = graphVis.getVisibleData() //
let layoutConfig = getLayoutDefaultConfig(layoutType, params)
graphVis.excuteWorkerLayout(graphData, layoutType, layoutConfig, false, function () {
endLayoutCallback()
MessagePlugin.success('布局成功')
})
}
const endLayoutCallback = () => {
//
currentLayout.value.icon = 'el-icon-video-play'
currentLayout.value.text = '开始布局'
currentLayout.value.status = false
currentLayout.value.preLayout = currentLayout.value.type
currentLayout.value.layoutConfigText = '布局参数设置'
graphVis.moveCenter && graphVis.moveCenter()
toolBarEvent('zoom', 'auto')
}
const toolBarEvent = (eventType, type) => {
if (eventType == 'zoom') {
graphVis.setZoom(type)
}
}
//
let temporalTime = ref(0)
let temporalTimeMin = ref(0)
let temporalTimeMax = ref(0)
let temporalTimeRange = []
let temporalPlayShow = ref(true)
let temporaInterval = null
let temporalTick = ref('0%')
// left
let importantNode_time_left = ref([])
let init_time_left = ref([])
const handleTemporalTimeRange = () => {
temporalTimeRange = temporalTimeRange.map((time) => new Date(time).getTime())
temporalTimeMin.value = Math.min(...temporalTimeRange)
temporalTimeMax.value = Math.max(...temporalTimeRange)
temporalTime.value = temporalTimeMax.value
}
const set_important_left_map = (node, map, ratio) => {
if (
!map.has(node.id) &&
(node.properties?.special_type?.includes('post_importantNode') ||
node.properties?.special_type?.includes('user_importantNode'))
) {
map.set(node.id, `${Math.round(ratio * 100)}%`)
}
}
const set_init_left_map = (node, map, ratio) => {
if (!map.has(node.id) && node.properties?.special_type?.includes('initial')) {
map.set(node.id, `${Math.round(ratio * 100)}%`)
}
}
const handleImportantTag = (nodeList, linkList) => {
const temporalTimeRange_copy = [...temporalTimeRange]
let important_left_map = new Map()
let init_left_map = new Map()
temporalTimeRange_copy.sort()
temporalTimeRange_copy.forEach((curTime) => {
const curLinks = linkList.filter((link) => {
let ranks = link.properties.ranks.map((time) => new Date(time).getTime())
if (ranks.includes(curTime)) {
return true
}
})
curLinks.forEach((link) => {
const source = nodeList.find((node) => node.id === link.source)
const target = nodeList.find((node) => node.id === link.target)
if (source && target) {
const ratio =
(curTime - temporalTimeMin.value) / (temporalTimeMax.value - temporalTimeMin.value)
// important_left_map
set_important_left_map(source, important_left_map, ratio)
set_important_left_map(target, important_left_map, ratio)
// init_left_map
set_init_left_map(source, init_left_map, ratio)
set_init_left_map(target, init_left_map, ratio)
}
})
})
importantNode_time_left.value = Array.from(important_left_map.values())
init_time_left.value = Array.from(init_left_map.values())
}
const handleTimeLabel = () => {
return dateFormat(temporalTime.value)
}
const changeTemporalTimeEnd = () => {
temporalGraphRefresh(temporalTime.value)
}
const temporalGraphRefresh = (time) => {
graphVis.nodes.forEach((node) => (node.visible = false))
graphVis.links.forEach((item) => {
let ranks = item.properties.ranks.map((time) => new Date(time).getTime())
ranks.sort()
//
const link_time = ranks.filter((t) => t <= time)
const count = link_time.length
if (count > 0) {
item.visible = true
let source = graphVis.findNodeById(item.source.id)
let target = graphVis.findNodeById(item.target.id)
source.visible = true
target.visible = true
if (count >= 2) {
item.label = count
item.showlabel = true
} else {
item.showlabel = false
}
} else {
item.visible = false
}
})
graphVis.refreshView()
}
const handleTemporaPlay = () => {
let step = Math.floor((temporalTimeMax.value - temporalTimeMin.value) / 20)
//
if (temporalTime.value === temporalTimeMax.value) {
temporalTime.value = temporalTimeMin.value
temporalGraphRefresh(temporalTime.value)
}
// 1
// let step = 60 * 60 * 1000;
temporalPlayShow.value = false
temporaInterval = setInterval(() => {
let nextTime = temporalTime.value + step
if (nextTime >= temporalTimeMax.value) {
nextTime = temporalTimeMax.value
clearInterval(temporaInterval)
temporalPlayShow.value = true
temporaInterval = null
}
temporalTime.value = nextTime
const ratio =
(temporalTime.value - temporalTimeMin.value) / (temporalTimeMax.value - temporalTimeMin.value)
temporalTick.value = `${Math.round(ratio * 100)}%`
temporalGraphRefresh(nextTime)
}, 1000)
}
const handleTemporaPause = () => {
clearInterval(temporaInterval)
temporalPlayShow.value = true
temporaInterval = null
}
onMounted(() => {
initVisualizeData()
})
onBeforeUnmount(() => {
graphVis = null
})
</script>
<style lang="less" scoped>
.sky-page-box {
height: 100%;
width: 100%;
min-width: 600px;
min-height: 600px;
position: relative;
}
.sky-graph-area-h1 {
height: calc(100% - 80px);
}
.sky-layoutLoading {
position: fixed;
bottom: 0;
right: 0;
}
.sky-time-box {
position: absolute;
bottom: 16px;
right: 80px;
z-index: 10;
background-color: #fff;
border-radius: 8px;
padding: 14px;
}
:deep(.sky-timeSlider .t-slider__button) {
transform: translateY(-75%);
width: 18px;
height: 24px;
background-image: url('../src/assets/img/slider_curr.svg');
border: none;
box-shadow: none;
border-radius: 0%;
}
:deep(.sky-timeSlider .t-slider .t-slider__rail .t-slider__track) {
background-color: #bc1b22;
}
</style>
<style lang="less">
.sky-pageBgc-r {
background: #f5f1f1 !important;
}
.sky-flex-rsc {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.sky-flex-rss {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.sky-position-r {
position: relative;
}
each(range(100), {
.sky-w-@{value} {
width: (@value * 1%);
}
}) each(range(2000), {
.sky-w-@{value}px {
width: (@value * 1px);
}
}) each(range(200), {
.sky-m-@{value} {
margin:(@value * 1px)
}
.sky-ml-@{value} {
margin-left:(@value * 1px)
}
.sky-mr-@{value} {
margin-right:(@value * 1px)
}
.sky-mb-@{value} {
margin-bottom:(@value * 1px)
}
.sky-mt-@{value} {
margin-top:(@value * 1px)
}
}) each(range(50), {
.sky-fs-@{value} {
font-size: (@value * 1px);
}
}) .sky-hover-c-primary {
cursor: pointer;
&:hover {
color: var(--td-brand-color);
}
}
</style>