SocialNetworks/src/views/keyNodeRecognition3/components/communityNode.vue

325 lines
9.9 KiB
Vue
Raw Normal View History

2025-07-15 11:07:24 +08:00
<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 };
2025-07-15 16:01:27 +08:00
console.log(data);
2025-07-15 11:07:24 +08:00
const categories = [
2025-07-15 16:01:27 +08:00
{ name: "普通社团", category: 0 },
{ name: "含锚点社团", category: 1 }
2025-07-15 11:07:24 +08:00
];
// // 初始选项
const option = {
//图例配置
legend: [
{
2025-07-15 16:01:27 +08:00
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的渐变
}
})),
2025-07-15 11:07:24 +08:00
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>