第三个模块时间轴自动推进展示每个时间切片的关系图
This commit is contained in:
parent
6230c865e3
commit
c9e63e815c
BIN
src/assets/images/head/hiddenTitle.png
Normal file
BIN
src/assets/images/head/hiddenTitle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
BIN
src/assets/images/head/linkedPredictionStruct.png
Normal file
BIN
src/assets/images/head/linkedPredictionStruct.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
|
|
@ -11,7 +11,7 @@
|
|||
>
|
||||
<div
|
||||
class="time-section"
|
||||
:style="{ left: getTimeSectionLeft(time) + 'px' }"
|
||||
:style="{ left: nowSize(getTimeSectionLeft(time)) + 'px' }"
|
||||
@pointerdown.stop="handleSectionPointerDown(time)"
|
||||
></div>
|
||||
</el-tooltip>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<div
|
||||
class="timeLine-point"
|
||||
@pointerdown.stop="handlePointPointerDown"
|
||||
:style="{ left: `${currentPosition - 9}px` }"
|
||||
:style="{ left: `${nowSize(currentPosition - 9)}px` }"
|
||||
></div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
|
@ -53,12 +53,12 @@ const props = defineProps({
|
|||
},
|
||||
startTime: {
|
||||
//起始时间
|
||||
type: Date,
|
||||
|
||||
default: new Date("2024-05-16 16:56:04")
|
||||
},
|
||||
endTime: {
|
||||
//结束时间
|
||||
type: Date,
|
||||
|
||||
default: new Date("2024-05-23 10:16:56")
|
||||
},
|
||||
initPosition: {
|
||||
|
|
@ -81,7 +81,7 @@ const props = defineProps({
|
|||
const startTime = ref(props.startTime) //开始时间
|
||||
const endTime = ref(props.endTime) //结束时间
|
||||
const timeList = ref(props.timeList) //时间列表
|
||||
const currentTime = ref(new Date("2024-05-16 16:56:04")) // 当前选中的时间
|
||||
const currentTime = ref(props.startTime) // 当前选中的时间
|
||||
const currentPosition = ref(props.initPosition) // 初始位置
|
||||
const isPlaying = ref(props.isAutoPlay) // 是否自动播放
|
||||
const axisRef = ref(null) // 轴的引用
|
||||
|
|
@ -90,7 +90,7 @@ const axisWidth = nowSize(415) // 轴的长度(px)
|
|||
const startTimeMs = startTime.value.getTime() // 起始时间的毫秒数
|
||||
const endTimeMs = endTime.value.getTime() // 结束时间的毫秒数
|
||||
const totalDuration = endTimeMs - startTimeMs // 计算总持续时间
|
||||
const step = 4 // 每次移动的像素数(px)
|
||||
const step = nowSize(4) // 每次移动的像素数(px)
|
||||
let playTimer = null // 自动播放定时器
|
||||
|
||||
const emit = defineEmits(["click:pointerDown", "slide:pointerUp"])
|
||||
|
|
@ -138,7 +138,7 @@ watch(
|
|||
// 让 active-needle 标定在 timeList 最后一个时间点
|
||||
const showHidden = computed(() => {
|
||||
if (!timeList.value || timeList.value.length === 0) return {}
|
||||
const left = getTimeSectionLeft.value(timeList.value[timeList.value.length - 1]) + 5 // +5px 保持和 time-section 对齐
|
||||
const left = getTimeSectionLeft.value(timeList.value[timeList.value.length - 1]) + nowSize(5) // +5px 保持和 time-section 对齐
|
||||
return { left: `${left}px` }
|
||||
})
|
||||
|
||||
|
|
@ -153,17 +153,41 @@ const pause = () => {
|
|||
}
|
||||
|
||||
// 自动播放控制
|
||||
// 已处理过的时间点索引
|
||||
let processedIndices = new Set()
|
||||
|
||||
// 重置已处理时间点
|
||||
const resetProcessedIndices = () => {
|
||||
processedIndices.clear()
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
if (!isPlaying.value) return
|
||||
if (!isPlaying.value || timeList.value.length === 0) return
|
||||
// 重置已处理索引,确保每次播放都从当前位置重新开始处理
|
||||
playTimer = setInterval(() => {
|
||||
if (currentPosition.value >= axisWidth) {
|
||||
pause()
|
||||
return
|
||||
}
|
||||
// 持续移动进度条
|
||||
currentPosition.value = Math.min(axisWidth, currentPosition.value + step)
|
||||
currentTime.value = getTimeFromPosition(currentPosition.value)
|
||||
emit("slide:pointerUp", currentTime.value)
|
||||
}, 300) // 每300ms移动一次
|
||||
// 检查当前位置是否到达或超过某个时间切片
|
||||
for (let i = 0; i < timeList.value.length; i++) {
|
||||
if (processedIndices.has(i)) continue
|
||||
const time = timeList.value[i]
|
||||
const left = getTimeSectionLeft.value(time)
|
||||
// 如果当前位置超过了这个时间切片的位置,并且还没有处理过
|
||||
if (
|
||||
currentPosition.value >= left - nowSize(2) &&
|
||||
currentPosition.value <= left + nowSize(2)
|
||||
) {
|
||||
currentTime.value = time // 使用精确的时间值
|
||||
emit("slide:pointerUp", time) // 只在timeList中的时间点发送请求
|
||||
processedIndices.add(i)
|
||||
}
|
||||
}
|
||||
// 如果到达终点,停止播放
|
||||
if (currentPosition.value >= axisWidth) {
|
||||
pause()
|
||||
}
|
||||
}, 500) // 每500ms移动一小步
|
||||
}
|
||||
|
||||
// 根据位置计算时间
|
||||
|
|
@ -177,6 +201,7 @@ const getTimeFromPosition = (position) => {
|
|||
const handlePointerDown = (e) => {
|
||||
if (e.target.classList.contains("timeLine-point")) return
|
||||
pause() // 拖动或点击时暂停自动播放
|
||||
resetProcessedIndices() // 重置已处理时间点
|
||||
const rect = axisRef.value.getBoundingClientRect()
|
||||
const position = Math.max(0, Math.min(axisWidth, e.clientX - rect.left))
|
||||
currentPosition.value = position
|
||||
|
|
@ -189,6 +214,7 @@ const handlePointPointerDown = (e) => {
|
|||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
isDragging.value = true
|
||||
resetProcessedIndices() // 重置已处理时间点
|
||||
// 缓存轴的边界矩形,避免重复计算
|
||||
const rect = axisRef.value.getBoundingClientRect()
|
||||
const axisLeft = rect.left
|
||||
|
|
@ -216,9 +242,10 @@ const handlePointPointerDown = (e) => {
|
|||
document.addEventListener("pointerup", handlePointerUp)
|
||||
}
|
||||
|
||||
const timeSectionWidth = 4 // 与样式保持一致
|
||||
const timeSectionWidth = nowSize(4) // 与样式保持一致
|
||||
const handleSectionPointerDown = (time) => {
|
||||
pause()
|
||||
resetProcessedIndices() // 重置已处理时间点
|
||||
// 计算该时间点的中心位置
|
||||
const left = getTimeSectionLeft.value(time) + timeSectionWidth / 2
|
||||
currentPosition.value = left
|
||||
|
|
@ -232,6 +259,7 @@ const reset = () => {
|
|||
currentTime.value = getTimeFromPosition(props.initPosition)
|
||||
// 清理旧定时器
|
||||
pause()
|
||||
resetProcessedIndices() // 重置已处理时间点
|
||||
// 重新开始播放
|
||||
if (props.isAutoPlay) {
|
||||
isPlaying.value = true
|
||||
|
|
|
|||
|
|
@ -186,8 +186,6 @@ export const useGroupDiscoveryStore = defineStore("groupDiscovery", {
|
|||
//获取群体列表数据
|
||||
async initializeGroupList(time = "") {
|
||||
const res = await getGroupEvolutionGroupList(time)
|
||||
console.log("群体列表:", res)
|
||||
|
||||
if (res.code != 200) return
|
||||
const iconMap = {
|
||||
节点数: nodePrefix,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, toRaw, watch } from "vue"
|
||||
import { onMounted, ref, toRaw, watch } from "vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { convertToUtcIsoString } from "@/utils/transform"
|
||||
import { paintNodeFunction, paintLineFunction } from "@/utils/customePaint"
|
||||
|
|
@ -59,6 +59,12 @@ const legendsMap = {
|
|||
{ icon: abnormalLeg, text: "异常社团" }
|
||||
]
|
||||
}
|
||||
const zoomSize = new Map([
|
||||
["groupDiscovery", 0.15],
|
||||
["groupStructure", 0.15],
|
||||
["groupMember", 0.15],
|
||||
["anomalousGroup", 0.7]
|
||||
])
|
||||
const emit = defineEmits(["click:pointerDownAndSlide"])
|
||||
const { timeList, graph } = storeToRefs(props.store)
|
||||
const graphTitle = props.store.graphTitle
|
||||
|
|
@ -166,11 +172,8 @@ const registEvents = () => {
|
|||
const highLightAboutNodesOrLinks = (type) => {
|
||||
graphVis.cancelAllSelected()
|
||||
const { newNodes, newLinks } = graph.value
|
||||
console.log(graphVis.nodes)
|
||||
console.log("进来highLightAboutNodesOrLinks")
|
||||
if (type == "nodes") {
|
||||
//实现高亮节点逻辑
|
||||
console.log("进来if")
|
||||
graphVis.nodes.forEach((node) =>
|
||||
newNodes.forEach((newNode) => {
|
||||
if (node.id === newNode.name) {
|
||||
|
|
@ -195,62 +198,41 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
|
|||
function handleLayoutSuccess() {
|
||||
//处理四个关系图的差异函数
|
||||
const handleGroupDiscoveryDiff = () => {
|
||||
console.log("进来handleGroupDiscoveryDiff")
|
||||
return
|
||||
}
|
||||
|
||||
const handleGroupStructureDiff = () => {
|
||||
console.log("进来handleGroupStructureDiff")
|
||||
highLightAboutNodesOrLinks("links")
|
||||
}
|
||||
|
||||
const handleGroupMemberDiff = () => {
|
||||
console.log("进来handleGroupMemberDiff")
|
||||
highLightAboutNodesOrLinks("nodes")
|
||||
}
|
||||
|
||||
const handleAnomalousGroup = () => {
|
||||
// 当前时间从 store 取;没有就用 T0
|
||||
// 当前时间从 store 取
|
||||
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 greenRed = "85, 125, 15"
|
||||
const blueRed = "15, 106, 125"
|
||||
const yellowRed = "125, 114, 15"
|
||||
|
||||
// 时间门控:达到阈值就把各组异常节点染红
|
||||
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
|
||||
n.fillColor = greenRed
|
||||
const abnormalData = props.store.graphAbnormalData
|
||||
const abnormalGroupConfigs = [
|
||||
{ timeThreshold: "2024-06-19T08:57:55Z", groupKey: "groupA", color: "85, 125, 15" }, // 绿色
|
||||
{ timeThreshold: "2024-06-19T10:58:03Z", groupKey: "groupB", color: "125, 114, 15" }, // 黄色
|
||||
{ timeThreshold: "2024-06-19T12:58:04Z", groupKey: "groupC", color: "15, 106, 125" } // 蓝色
|
||||
]
|
||||
// 计算当前应激活的异常组配置
|
||||
const activeConfigs = abnormalGroupConfigs.filter((config) => now >= config.timeThreshold)
|
||||
// 单次遍历处理所有异常节点,避免多次map操作
|
||||
graphVis.nodes = graphVis.nodes.map((node) => {
|
||||
// 检查节点是否属于任何激活的异常组
|
||||
for (const config of activeConfigs) {
|
||||
const abnormalIds = abnormalData[config.groupKey]
|
||||
// 如果节点在异常组中,则设置对应的颜色
|
||||
if (abnormalIds && abnormalIds.includes(node.id)) {
|
||||
node.fillColor = config.color
|
||||
break // 一旦找到匹配的异常组,就不再检查其他组
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
if (shouldB) {
|
||||
graphVis.nodes = graphVis.nodes.map((n) => {
|
||||
if (props.store.graphAbnormalData.groupB.includes(n.id)) {
|
||||
n.fillColor = yellowRed
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
if (shouldC) {
|
||||
graphVis.nodes = graphVis.nodes.map((n) => {
|
||||
if (props.store.graphAbnormalData.groupC.includes(n.id)) {
|
||||
n.fillColor = blueRed
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
new Map([
|
||||
|
|
@ -259,11 +241,10 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => {
|
|||
["groupMember", () => handleGroupMemberDiff()],
|
||||
["anomalousGroup", () => handleAnomalousGroup()]
|
||||
]).get(storeId)?.()
|
||||
// isPlay.value = true
|
||||
graphVis.zoomFit() //场景视图大小自适应缩放
|
||||
isPlay.value = true
|
||||
graphVis.moveCenter(zoomSize.get(storeId)) //组件布局完毕后自动让整体关系图大小缩放到指定比例
|
||||
}
|
||||
graphVis.excuteLocalLayout(layoutType, layoutConfig, isAsync, () => handleLayoutSuccess())
|
||||
// graphVis.autoGroupLayout(graphVis.getGraphData())
|
||||
graphVis.excuteLocalLayout(layoutType, layoutConfig, isAsync, handleLayoutSuccess())
|
||||
}
|
||||
// 根据节点的cluster属性进行分组
|
||||
const clusterAnalyze = () => {
|
||||
|
|
@ -324,7 +305,7 @@ const createGraph = () => {
|
|||
}
|
||||
graphVis.setDragHideLine(false) //拖拽时隐藏连线
|
||||
graphVis.setShowDetailScale(0.1) //展示细节的比例
|
||||
graphVis.setZoomRange(0.1, 5) //缩放区间
|
||||
graphVis.setZoomRange(0.1, 10) //缩放区间
|
||||
registCustomePaintFunc(storeId) //注册自定义绘图方法
|
||||
registEvents()
|
||||
}
|
||||
|
|
@ -333,9 +314,21 @@ const initChart = () => {
|
|||
createGraph()
|
||||
graphVis.addGraph({ ...toRaw(graph.value) })
|
||||
clusterAnalyze()
|
||||
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
|
||||
runDiffForceLayout({ strength: -300, ajustCluster: true }, "simulation", false)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时,如果有时间列表且当前没有图表数据,请求第一个时间切片的数据
|
||||
if (
|
||||
timeList.value.length > 0 &&
|
||||
(!graph.value || !graph.value.nodes || graph.value.nodes.length === 0)
|
||||
) {
|
||||
const firstTime = timeList.value[0]
|
||||
const utcTime = convertToUtcIsoString(firstTime)
|
||||
emit("click:pointerDownAndSlide", utcTime)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加更新图表的函数
|
||||
const updateChart = (newGraphData) => {
|
||||
if (!graphVis) {
|
||||
|
|
@ -346,7 +339,7 @@ const updateChart = (newGraphData) => {
|
|||
graphVis.addGraph({ ...toRaw(newGraphData) })
|
||||
// 重新运行力导向布局
|
||||
clusterAnalyze()
|
||||
runDiffForceLayout({ strength: -500, ajustCluster: true }, "simulation", true)
|
||||
runDiffForceLayout({ strength: -300, ajustCluster: true }, "simulation", false)
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ provide("statisticsList", characterHiddenStore.statisticsList)
|
|||
.top-container {
|
||||
width: 100%;
|
||||
height: vh(110);
|
||||
background-image: url(@/assets/images/linkPrediction/title/page-title.png);
|
||||
background-image: url("@/assets/images/head/hiddenTitle.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ provide("statisticsList", socialGroupsStore.statisticsList)
|
|||
.top-container {
|
||||
width: 100%;
|
||||
height: vh(110);
|
||||
background-image: url(@/assets/images/linkPrediction/title/page-title.png);
|
||||
background-image: url("@/assets/images/head/linkedPredictionStruct.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user