SocialNetworks/src/views/keyNodeRecognition3/components/communityNode.vue
2025-07-15 16:01:27 +08:00

325 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="communityNode-component">
<div id="container" class="container" ref="teamContainer"></div>
</div>
</template>
<script setup>
import { defineProps, onMounted, ref, defineEmits, defineExpose, watch } from "vue";
import * as echarts from "echarts";
import anchor from "@/assets/json/anchor_community.json";
import community from "@/assets/json/community_nodes.json";
import nodeHoverImg from "@/assets/images/nodeHover.png";
import { storeToRefs } from "pinia";
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index";
import csvUrl from "@/assets/json/community_edges.csv?url";
const keyNodeStore = useKeyNodeRecognitionStore();
const { currentUser } = storeToRefs(keyNodeStore);
const props = defineProps({});
const emit = defineEmits(["click:anchorNode"]);
const teamContainer = ref(null);
//当前在userPanel中选择的某个user
const currentSelectedUser = ref(null);
let chart = null;
//当选中userPanel中的某个用户的时候更新当前组件的用户
watch(currentUser, (val) => {
if (val) {
currentSelectedUser.value = val;
const option = chart.getOption();
//实现选中只高亮一个
chart.dispatchAction({
//先让所有的先取消高亮
type: "downplay",
seriesIndex: 0
});
const nodeIndex = option.series[0].data.findIndex((item) =>
item.anchorList.includes(currentSelectedUser.value.name)
);
chart.dispatchAction({
//再让单独一个高亮
type: "highlight",
dataIndex: nodeIndex
});
// 2. 平移到节点
}
});
const initChart = async () => {
// 初始化图表
chart = echarts.init(document.getElementById("container"));
//处理data
//统计每一个锚点在某些社团这个社团包含锚点的个数key: 社团id value: 这个社团含有几个锚点)
//统计每个社团包含的锚点id列表
const anchorCommunityInfo = {};
Object.entries(anchor).forEach(([anchorId, communityId]) => {
if (!anchorCommunityInfo[communityId]) {
//如果该社团不包含锚点
anchorCommunityInfo[communityId] = []; //就初始化该社团的锚点数组
}
anchorCommunityInfo[communityId].push(anchorId); //把这个社团中的所有锚点id存到列表中处理一对多的关系一个社团对应多个锚点
});
//包含锚点的社团 (绿色闪烁的节点)
const includeAnchorCommunity = Object.entries(anchorCommunityInfo).map(
([communityId, anchorList]) => ({
name: parseInt(communityId), // 社团id
anchorCount: anchorList.length, // 该社团包含锚点的个数
anchorList: anchorList // 该社团包含的锚点id数组
})
);
// 创建包含锚点的社团名称集合(提高查找效率)
const includedCommunityId = includeAnchorCommunity.map((node) => node.name.toString());
//筛选出不包含锚点的社团(不闪烁的蓝色节点)
const uncontainedAnchorCommunity = Object.keys(community)
.filter((node) => !includedCommunityId.includes(node))
.map((node) => ({ name: parseInt(node), anchorCount: 0 }));
//合并成功后统一设置id避免重复id出现导致echarts渲染失败,并且取出每个社团中有多少个节点
const nodes = includeAnchorCommunity.concat(uncontainedAnchorCommunity).map((node, index) => ({
id: index,
total: Object.values(community)[node.name].length,
category: node.anchorCount > 0 ? 1 : 0, //(1:含有锚点的社团分类0不含锚点的社团)
...node
}));
//处理links
// 先把csv文件里的数据进行格式化
let links = [];
const linksCsv = await fetch(csvUrl);
const csvText = await linksCsv.text();
const lines = csvText.split("\n");
links = lines
.slice(1)
.map((line) => {
if (!line) return null;
const values = line.split(",");
return {
source: parseInt(values[0]),
target: parseInt(values[1]),
edge: parseInt(values[2])
};
})
.filter(Boolean);
const data = { nodes, links };
console.log(data);
const categories = [
{ name: "普通社团", category: 0 },
{ name: "含锚点社团", category: 1 }
];
// // 初始选项
const option = {
//图例配置
legend: [
{
data: categories.map((c) => ({
name: c.name,
itemStyle: {
color:
c.category === 0
? new echarts.graphic.LinearGradient(
1,
0,
0,
0, // 从左到右渐变x0=1, x1=0
[
{ offset: 0, color: "#49c3ed" }, // 起始色(最右)
{ offset: 0.5, color: "#5fa3e0" }, // 中间色
{ offset: 1, color: "#7286d4" } // 结束色(最左)
]
) // 分类0的渐变
: new echarts.graphic.LinearGradient(
1,
0,
0,
0, // 从左到右渐变 (x0=1, x1=0)
[
{ offset: 0, color: "#7ff2c1" },
{ offset: 0.37, color: "#85e7d2" },
{ offset: 1, color: "#8bdbe4" }
]
) // 分类1的渐变
}
})),
right: 15,
bottom: 13,
icon: "circle",
orient: "vertical",
itemWidth: 16,
itemHeight: 16,
itemGap: 12,
backgroundColor: "rgba(0,67,125,0.56)", // 半透明深蓝
borderRadius: 8, // 圆角
borderColor: "#c2f2ff", // 淡蓝色边框
borderWidth: 0.3,
padding: [12, 20, 12, 20], // 上右下左
textStyle: {
color: "#fff",
fontSize: 16,
fontWeight: "normal"
}
}
],
//hover上去的窗口
tooltip: {
trigger: "item",
trigger: "item",
backgroundColor: "rgba(0,0,0,0)", // 透明背景
borderColor: "rgba(0,0,0,0)", // 透明边框
borderWidth: 0,
extraCssText: "box-shadow:none;padding:0;",
formatter: function (params) {
if (params.dataType === "node") {
return `<div
style="
width: 107px;
height: 68px;
border-radius: 4px;
background: url('${nodeHoverImg}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
">
<div style="color:#fff;letter-spacing: 0.14px;">
<div >节点数:${params.data.total}</div>
<div>锚点数:${params.data.anchorCount}</div>
</div>
</div>`;
}
return "";
}
},
//权值设置样式
edgeLabel: {
show: false,
position: "middle",
formatter: function (params) {
return params.data.edge;
},
fontSize: 14
},
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.4,
categories: categories,
force: {
initLayout: null,
edgeLength: 1000,
repulsion: 1000,
gravity: 0.4,
friction: 0.6,
layoutAnimation: true,
coolingFactor: 0.1
},
animationDurationUpdate: 3500, // 节点移动更平滑
data: data.nodes.map((node) => ({
...node,
symbolSize: node.total / 20 < 30 ? 30 : node.total / 20,
itemStyle: {
color: {
type: "radial",
x: 0.97,
y: 0.38,
r: 0.86,
colorStops:
node.anchorCount === 0
? [
// anchorCount为0的节点渐变深蓝到青
{ offset: 0, color: "#49c3ed" }, // 最右侧
{ offset: 0.5, color: "#5fa3e0" }, // 中间
{ offset: 1, color: "#7286d4" } // 最左侧
]
: [
// anchorCount为1的节点渐变原配色
{ offset: 0, color: "#7ff2c1" },
{ offset: 0.37, color: "#85e7d2" },
{ offset: 1, color: "#8bdbe4" }
]
},
opacity: 1,
borderColor: "#46C6AD",
borderWidth: 1,
shadowBlur: 4,
borderType: "dashed",
shadowColor: "rgba(19, 27, 114, 0.25)"
},
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: "#c4a651",
borderColor: "#fcd267",
borderWidth: node.category == 0 ? 1 : 5,
borderType: "solid"
}
}
})),
links: data.links,
lineStyle: {
color: "#37ACD7",
width: 1
}
}
]
};
chart.setOption(option);
};
const handleClickNode = () => {
chart.on("click", function (params) {
//params.data.name才是社团id
if (params.dataType === "node" && params.data.category == 1) {
emit("click:anchorNode", params.data);
}
});
};
onMounted(() => {
initChart();
handleClickNode();
});
</script>
<style scoped lang="less">
.communityNode-component {
width: 100%;
height: 100%;
.container {
width: 100%;
height: 450px;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>