|
@@ -0,0 +1,792 @@
|
|
|
|
+import { ref,reactive } from "vue";
|
|
|
|
+import r from "@/utils/request.ts";
|
|
|
|
+import * as d3 from 'd3';
|
|
|
|
+import d3ContextMenu from 'd3-context-menu';
|
|
|
|
+
|
|
|
|
+/**********data_def*********/
|
|
|
|
+export const dataNodes = ref([]);
|
|
|
|
+export const dataEdges = ref([]);
|
|
|
|
+//节点配色方案
|
|
|
|
+export const nodeColors = reactive({
|
|
|
|
+ 'Department': { 'color' :"#990000", 'name':'科室','visible':true },
|
|
|
|
+ 'Food':{ 'color' :"#ffff00", 'name':'食品','visible':true },
|
|
|
|
+ 'Drug':{ 'color' :"rgb(201, 153, 90)", 'name':'药品','visible':true },
|
|
|
|
+ 'Disease':{ 'color' :"rgb(15, 150, 55)", 'name':'疾病','visible':true },
|
|
|
|
+ 'Symptom':{ 'color' :"rgb(121, 139, 165)", 'name':'症状','visible':true },
|
|
|
|
+ 'Check':{ 'color' :"rgb(40, 108, 210)", 'name':'检查','visible':true },
|
|
|
|
+ '_default':{ 'color' :"#CDCDCD", 'name':'缺省','visible':true },
|
|
|
|
+ });
|
|
|
|
+//用户选中的节点列表
|
|
|
|
+const selectedNodes = [];
|
|
|
|
+//图数据
|
|
|
|
+const mindGraph = {
|
|
|
|
+ svg: null,
|
|
|
|
+ svgContainer: null,
|
|
|
|
+ width: 0,
|
|
|
|
+ height: 0,
|
|
|
|
+ click: null,
|
|
|
|
+ rootG: null,
|
|
|
|
+ linkGroup: null,
|
|
|
|
+ linkTextGroup: null,
|
|
|
|
+ nodeGroup: null,
|
|
|
|
+ nodeTextGroup: null,
|
|
|
|
+ simulation: null,
|
|
|
|
+}
|
|
|
|
+/**********node_context_menu*********/
|
|
|
|
+const node_menu = [
|
|
|
|
+ {
|
|
|
|
+ title:function (data, event) {
|
|
|
|
+ return `${data.name}`;
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ divider: true
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '探索...',
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ console.log('Item clicked', 'element:', this, 'data:', data, 'event:', event);
|
|
|
|
+ loadData(data.id);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '修改链接...',
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ nodeLinksEdit.value(data);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '融合到其他节点...',
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ mergeToOtherNode.value(data);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '链接到其他节点...',
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ linkToOtherNode.value(data);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ divider: true
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '编辑操作'
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: function (data, event) {
|
|
|
|
+ return '从当前画布移除';
|
|
|
|
+ },
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ console.log("remove node from graph:" + data.name);
|
|
|
|
+ removeNodeFromGraph(data.id);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ title: '从数据库中删除',
|
|
|
|
+ disabled: function (data, event) {
|
|
|
|
+ return false;
|
|
|
|
+ },
|
|
|
|
+ action: function (data, event) {
|
|
|
|
+ deleteFromDatabase.value(data);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+];
|
|
|
|
+
|
|
|
|
+/**********callback_functions*********/
|
|
|
|
+export const deleteFromDatabase = ref((data)=>{});
|
|
|
|
+export const mergeToOtherNode = ref((data)=>{});
|
|
|
|
+export const linkToOtherNode = ref((data)=>{});
|
|
|
|
+export const nodeLinksEdit = ref((data)=>{});
|
|
|
|
+
|
|
|
|
+/**********ui_handler*********/
|
|
|
|
+export function toggleNodeVisibility(category){
|
|
|
|
+ var {nodes, links} = initData();
|
|
|
|
+ nodeColors[category].visible = !nodeColors[category].visible
|
|
|
|
+
|
|
|
|
+ console.dir(nodeColors);
|
|
|
|
+ d3.select(".node-texts").selectAll("text").data(nodes).style('display',(d)=>{
|
|
|
|
+ if (nodeColors[d.category].visible) return 'block' ;
|
|
|
|
+ return 'none'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ d3.select(".nodes").selectAll("circle").data(nodes).style('display',(d)=>{
|
|
|
|
+ if (nodeColors[d.category].visible) return 'block' ;
|
|
|
|
+ return 'none'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ d3.select(".links").selectAll("path").data(links).style('display',(d)=>{
|
|
|
|
+ var visible = true;
|
|
|
|
+ if (nodeColors[d.source.category].visible == false ){
|
|
|
|
+ visible = false;
|
|
|
|
+ }
|
|
|
|
+ if (nodeColors[d.target.category].visible == false ){
|
|
|
|
+ visible = false;
|
|
|
|
+ }
|
|
|
|
+ if (visible) return 'block' ;
|
|
|
|
+ return 'none'
|
|
|
|
+ });
|
|
|
|
+ d3.select(".link-texts").selectAll("text").data(links).style('display',(d)=>{
|
|
|
|
+
|
|
|
|
+ if (nodeColors[d.source.category].visible == false ){
|
|
|
|
+ return 'none';
|
|
|
|
+ }
|
|
|
|
+ if (nodeColors[d.target.category].visible == false ){
|
|
|
|
+ return 'none';
|
|
|
|
+ }
|
|
|
|
+ return 'block';
|
|
|
|
+ });
|
|
|
|
+ mindGraph.simulation.nodes(nodes);
|
|
|
|
+ mindGraph.simulation.force("link", d3.forceLink(links))
|
|
|
|
+}
|
|
|
|
+/**********graph_apis*********/
|
|
|
|
+/** 清空所有图形数据 */
|
|
|
|
+export function clearGraph() {
|
|
|
|
+
|
|
|
|
+ dataNodes.value.length = 0;
|
|
|
|
+ dataEdges.value.length = 0;
|
|
|
|
+ //dataLinks.value.length = 0;
|
|
|
|
+ var {nodes, links} = initData();
|
|
|
|
+ //remove
|
|
|
|
+ var graphNodes = d3.selectAll(".node").data(nodes, function(d) {
|
|
|
|
+ return d.id;
|
|
|
|
+ });
|
|
|
|
+ graphNodes.exit().remove();
|
|
|
|
+ var graphNodeText = d3.selectAll(".nodeText").data(nodes, function(d) {
|
|
|
|
+ return d.id;
|
|
|
|
+ });
|
|
|
|
+ graphNodeText.exit().remove();
|
|
|
|
+ var graphLink = d3.selectAll(".link").data(links, function(d) {
|
|
|
|
+ return d.index;
|
|
|
|
+ });
|
|
|
|
+ graphLink.exit().remove();
|
|
|
|
+ var graphLinkText = d3.selectAll(".linkText").data(links, function(d) {
|
|
|
|
+ return d.index;
|
|
|
|
+ });
|
|
|
|
+ graphLinkText.exit().remove();
|
|
|
|
+}
|
|
|
|
+/**从服务器加载数据并重新绘制 */
|
|
|
|
+export function loadData(id) {
|
|
|
|
+ const response = r.request<string[]>({
|
|
|
|
+ url: "/api/nodes/"+id+"/3/1",
|
|
|
|
+ method: "get"
|
|
|
|
+ });
|
|
|
|
+ response.then(result => {
|
|
|
|
+ addGraphNodes(result["nodes"],false);
|
|
|
|
+ addGraphEdges(result["edges"],false);
|
|
|
|
+ draw();
|
|
|
|
+ });
|
|
|
|
+}
|
|
|
|
+/**从图中移除一个节点 */
|
|
|
|
+export function removeNodeFromGraph(node_id){
|
|
|
|
+ var nodes = dataNodes.value;
|
|
|
|
+ var links = dataEdges.value;
|
|
|
|
+ dataNodes.value = nodes.filter(function(d){
|
|
|
|
+ return d.id != node_id;
|
|
|
|
+ });
|
|
|
|
+ dataEdges.value = links.filter(function(d){
|
|
|
|
+ if (d.src_id == node_id || d.dest_id == node_id) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ return true;
|
|
|
|
+ });
|
|
|
|
+ draw();
|
|
|
|
+};
|
|
|
|
+/**获取节点数据 */
|
|
|
|
+export function getGraphNodes() {
|
|
|
|
+ return dataNodes.value;
|
|
|
|
+}
|
|
|
|
+export function getGraphEdges() {
|
|
|
|
+ return dataEdges.value;
|
|
|
|
+}
|
|
|
|
+/**增加、替换节点数据 */
|
|
|
|
+export function addGraphNodes(nodes, replaceData = true){
|
|
|
|
+ if (replaceData) {
|
|
|
|
+ dataNodes.value = nodes;
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ nodes?.forEach(e => {
|
|
|
|
+ var existed = false;
|
|
|
|
+ for (var i=0;i<dataNodes.value.length; i++){
|
|
|
|
+ if (dataNodes.value[i].id == e.id){
|
|
|
|
+ existed = true;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (!existed){
|
|
|
|
+ dataNodes.value.push(e);
|
|
|
|
+ } else {
|
|
|
|
+ console.log(e.name + ' already existed, skip');
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+}
|
|
|
|
+/**增加、替换边数据 */
|
|
|
|
+export function addGraphEdges(edges, replaceData = true){
|
|
|
|
+ console.log(`addGraphEdges(): replace mode = ${replaceData}`);
|
|
|
|
+ if (replaceData) {
|
|
|
|
+ dataEdges.value = edges;
|
|
|
|
+ } else {
|
|
|
|
+ edges?.forEach(e => {
|
|
|
|
+ var existed = false;
|
|
|
|
+ for (var i=0;i<dataEdges.value.length; i++){
|
|
|
|
+ if (dataEdges.value[i].id == e.id){
|
|
|
|
+ existed = true;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (!existed){
|
|
|
|
+ dataEdges.value.push(e);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ console.log(`addGraphEdges(): finished, total edges = ${dataEdges.value.length}`);
|
|
|
|
+}
|
|
|
|
+/**初始化图 */
|
|
|
|
+export function initGraph(funClick) {
|
|
|
|
+ mindGraph.svgContainer = d3.select(document.getElementById("mind-map"));
|
|
|
|
+ mindGraph.svg = d3.select(document.getElementById("mindSvg"));
|
|
|
|
+ const svgNode = mindGraph.svg.node();
|
|
|
|
+ mindGraph.width = svgNode.clientWidth; //getAttribute("width");
|
|
|
|
+ mindGraph.height = svgNode.clientHeight; //getAttribute("height");
|
|
|
|
+ //if (mindGraph.rootG == null){
|
|
|
|
+ mindGraph.rootG = mindGraph.svg.append("g");
|
|
|
|
+ //}
|
|
|
|
+ mindGraph.click = funClick;
|
|
|
|
+}
|
|
|
|
+/**更新图 */
|
|
|
|
+export function updateGraph(){
|
|
|
|
+ var {nodes, links} = initData();
|
|
|
|
+ console.log(`draw(): total nodes ${nodes.length}, total edges: ${links.length}`);
|
|
|
|
+ var {node, link, nodeText, linkText} = drawNodesAndLinks(nodes, links);
|
|
|
|
+
|
|
|
|
+ mindGraph.simulation.stop();
|
|
|
|
+ mindGraph.simulation.nodes(nodes);
|
|
|
|
+ mindGraph.simulation.force("link", d3.forceLink(links));
|
|
|
|
+
|
|
|
|
+ mindGraph.simulation.alphaMin(0.05).alphaDecay(0.05);
|
|
|
|
+ mindGraph.simulation.velocityDecay(0.4);
|
|
|
|
+ mindGraph.simulation.alphaTarget(0.3).restart()
|
|
|
|
+}
|
|
|
|
+/**重新绘制图 */
|
|
|
|
+export function draw(){
|
|
|
|
+ console.log("draw(): draw graph");
|
|
|
|
+
|
|
|
|
+ mindGraph.rootG.selectAll("g").remove();
|
|
|
|
+
|
|
|
|
+ mindGraph.linkGroup = mindGraph.rootG.append("g").attr("class", "links");
|
|
|
|
+ mindGraph.linkTextGroup = mindGraph.rootG.append("g").attr("class", "link-texts");
|
|
|
|
+ mindGraph.nodeGroup = mindGraph.rootG.append("g").attr("class", "nodes");
|
|
|
|
+ mindGraph.nodeTextGroup = mindGraph.rootG.append("g").attr("class", "node-texts");
|
|
|
|
+
|
|
|
|
+ //var nodesData = dataNodes.value;
|
|
|
|
+ //var edges = dataEdges.value;
|
|
|
|
+ var {nodes, links} = initData();
|
|
|
|
+
|
|
|
|
+ console.log(`draw(): total nodes ${nodes.length}, total edges: ${links.length}`);
|
|
|
|
+
|
|
|
|
+ var width = mindGraph.width;
|
|
|
|
+ var height = mindGraph.height;
|
|
|
|
+ //simulation part
|
|
|
|
+ var svg = mindGraph.rootG;
|
|
|
|
+
|
|
|
|
+ var {node, link, nodeText, linkText} = drawNodesAndLinks(nodes, links);
|
|
|
|
+
|
|
|
|
+ mindGraph.simulation = d3.forceSimulation(nodes);
|
|
|
|
+ mindGraph.simulation.force("link", d3.forceLink(links).id(d => d.id).strength(0.3))
|
|
|
|
+ .force("charge", d3.forceManyBody().strength(-400))
|
|
|
|
+ .force("x",d3.forceX())
|
|
|
|
+ .force("y", d3.forceY());
|
|
|
|
+ //.force("center", d3.forceCenter(width / 2, height / 2));
|
|
|
|
+
|
|
|
|
+ mindGraph.simulation.alphaMin(0.05).alphaDecay(0.05);
|
|
|
|
+ mindGraph.simulation.velocityDecay(0.4);
|
|
|
|
+
|
|
|
|
+ mindGraph.simulation.on("end", ()=>{
|
|
|
|
+ console.log("simulation end");
|
|
|
|
+ });
|
|
|
|
+ mindGraph.simulation.on("tick", () => {
|
|
|
|
+
|
|
|
|
+ // 更新节点位置
|
|
|
|
+ d3.selectAll(".node")
|
|
|
|
+ .attr("cx", d => d.x)
|
|
|
|
+ .attr("cy", d => d.y);
|
|
|
|
+
|
|
|
|
+ // 更新节点文本位置
|
|
|
|
+ d3.selectAll(".nodeText")
|
|
|
|
+ .attr("x", d => d.x ) // 同步移动文本,考虑 dx 的偏移
|
|
|
|
+ .attr("y", d => d.y ); // 同步移动文本,考虑 dy 的偏移
|
|
|
|
+ // 更新链接上的文本位置和旋转
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ d3.selectAll(".link").attr("d", function(d) {
|
|
|
|
+ const x1 = d.source.x;
|
|
|
|
+ const y1 = d.source.y;
|
|
|
|
+ const x2 = d.target.x;
|
|
|
|
+ const y2 = d.target.y;
|
|
|
|
+
|
|
|
|
+ // 计算路径的1/3点
|
|
|
|
+ const dx = x2 - x1;
|
|
|
|
+ const dy = y2 - y1;
|
|
|
|
+ const len = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
+ const scale = len * 4 / 5 ; // 1/3长度的比例因子
|
|
|
|
+ const thirdPointX = x1 + (dx / len) * scale;
|
|
|
|
+ const thirdPointY = y1 + (dy / len) * scale;
|
|
|
|
+
|
|
|
|
+ // 创建一个包含两个线段的路径字符串,只在1/3处添加箭头
|
|
|
|
+ return `M${x1},${y1}L${thirdPointX},${thirdPointY} M${thirdPointX},${thirdPointY}L${x2},${y2}`;
|
|
|
|
+ }).attr("marker-mid", "url(#arrow)"); // 应用箭头标记到中间位置
|
|
|
|
+
|
|
|
|
+ d3.selectAll(".linkText").each(function(d) {
|
|
|
|
+ const thisText = d3.select(this);
|
|
|
|
+ const xMid = (d.source.x + d.target.x) / 2;
|
|
|
|
+ const yMid = (d.source.y + d.target.y) / 2;
|
|
|
|
+ let angle = Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x) * (180 / Math.PI);
|
|
|
|
+
|
|
|
|
+ // 如果角度在90度到270度之间,则翻转文本以保持可读性
|
|
|
|
+ if (angle > 90 || angle < -90) {
|
|
|
|
+ angle += 180;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ thisText
|
|
|
|
+ .attr("transform", `translate(${xMid},${yMid}) rotate(${angle})`);
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+/**********graph_funcs*********/
|
|
|
|
+function initData() {
|
|
|
|
+ var nodes = dataNodes.value?.map((node, i)=>{
|
|
|
|
+ return {
|
|
|
|
+ id: node.id,
|
|
|
|
+ name: node.name,
|
|
|
|
+ category: node.category,
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ var links = dataEdges.value?.map((link, i) => {
|
|
|
|
+ return {
|
|
|
|
+ source: nodes.find(node => node.id === link.src_id),
|
|
|
|
+ target: nodes.find(node => node.id === link.dest_id),
|
|
|
|
+ text: link.category,
|
|
|
|
+ id: link.id // 用于生成唯一ID
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {nodes, links};
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function drawNodesAndLinks(nodes, links){
|
|
|
|
+ const link = mindGraph.linkGroup
|
|
|
|
+ .attr("class", "links")
|
|
|
|
+ .selectAll("path")
|
|
|
|
+ .data(links)
|
|
|
|
+ .enter().append("path")
|
|
|
|
+ .attr("id", (d)=> d.id)
|
|
|
|
+ .attr("class", "link")
|
|
|
|
+ .attr("stroke", "#CDCDCD")
|
|
|
|
+ .attr("fill", "none");
|
|
|
|
+ console.log(`drawNodesAndLinks(): ${links.length} links finished `);
|
|
|
|
+ mindGraph.linkGroup.selectAll("path").data(links).exit().remove();
|
|
|
|
+ const node = mindGraph.nodeGroup
|
|
|
|
+ .attr("class", "nodes")
|
|
|
|
+ .selectAll("circle")
|
|
|
|
+ .data(nodes)
|
|
|
|
+ .enter().append("circle")
|
|
|
|
+ .attr("class", "node")
|
|
|
|
+ .attr("r", 10)
|
|
|
|
+ .attr("id", (d)=> d.id)
|
|
|
|
+ .attr("class", "node")
|
|
|
|
+ .attr("fill", (d)=>{
|
|
|
|
+ var color = nodeColors["_default"]['color'];
|
|
|
|
+ var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+ if (keys.indexOf(d.category)>=0){
|
|
|
|
+ return nodeColors[d.category]['color'];
|
|
|
|
+ }
|
|
|
|
+ return color;
|
|
|
|
+ }).attr('stroke', (d)=>{
|
|
|
|
+ var color = nodeColors["_default"]['color'];
|
|
|
|
+ var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+ if (keys.indexOf(d.category)>=0){
|
|
|
|
+ return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+ }
|
|
|
|
+ return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+ })
|
|
|
|
+ .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended))
|
|
|
|
+ .on('click', (event, d) => {
|
|
|
|
+ mindGraph.click(d.id);
|
|
|
|
+ //console.log(d3.select( event.currentTarget).data());
|
|
|
|
+ if (selectedNodes.includes(d.id)){
|
|
|
|
+ console.log('clear current node selection state');
|
|
|
|
+ d3.select( event.currentTarget).attr('stroke', (d)=>{
|
|
|
|
+ var color = nodeColors["_default"]['color'];
|
|
|
|
+ var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+ if (keys.indexOf(d.category)>=0){
|
|
|
|
+ return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+ }
|
|
|
|
+ return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+ });
|
|
|
|
+ d3.select( event.currentTarget).attr('r',8);
|
|
|
|
+ d3.select( event.currentTarget).attr('stroke-width','1');
|
|
|
|
+ selectedNodes.pop(d.id);
|
|
|
|
+ } else {
|
|
|
|
+
|
|
|
|
+ console.log('clear previous nodes selection state');
|
|
|
|
+ var selectedCircles = d3.selectAll("circle").filter(function() {
|
|
|
|
+ var stroke = d3.select(this).attr("stroke");
|
|
|
|
+ if (stroke && stroke.toLowerCase() === 'red') {
|
|
|
|
+ d3.select(this).attr('stroke', (d)=>{
|
|
|
|
+ var color = nodeColors["_default"]['color'];
|
|
|
|
+ var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+ if (keys.indexOf(d.category)>=0){
|
|
|
|
+ return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+ }
|
|
|
|
+ return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+ });
|
|
|
|
+ d3.select(this).attr('r',8);
|
|
|
|
+ d3.select(this).attr('stroke-width','1');
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ selectedNodes.length = 0;
|
|
|
|
+ console.log('set current selection state');
|
|
|
|
+ selectedNodes.push(d.id);
|
|
|
|
+ d3.select( event.currentTarget).attr('stroke','red');
|
|
|
|
+ d3.select( event.currentTarget).attr('r',12);
|
|
|
|
+ d3.select( event.currentTarget).attr('stroke-width','2');
|
|
|
|
+ }
|
|
|
|
+ // if (timer == null){
|
|
|
|
+ // timer = setTimeout(()=>{
|
|
|
|
+ // timer = null;
|
|
|
|
+ // console.log("click node "+ d.id);
|
|
|
|
+ // funcClick(d.id);
|
|
|
|
+ // }, 250);
|
|
|
|
+ // return;
|
|
|
|
+ // }
|
|
|
|
+ // console.log("db click node");
|
|
|
|
+ // clearTimeout(timer);
|
|
|
|
+ // timer = null;
|
|
|
|
+ //loadData(d.id);
|
|
|
|
+ }).on('contextmenu', d3ContextMenu(node_menu, {
|
|
|
|
+ onOpen: function(data, event){
|
|
|
|
+ console.log("context menu open");
|
|
|
|
+ },
|
|
|
|
+ onClose: function(data, event){
|
|
|
|
+ console.log("context menu close");
|
|
|
|
+ },
|
|
|
|
+ position: function(data, event){
|
|
|
|
+ console.log("context menu position");
|
|
|
|
+ var bounds = this.getBoundingClientRect();
|
|
|
|
+ return {
|
|
|
|
+ left: bounds.left + bounds.width + 10,
|
|
|
|
+ top: bounds.top
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }));
|
|
|
|
+
|
|
|
|
+ console.log(`drawNodesAndLinks(): ${nodes.length} nodes finished `);
|
|
|
|
+ mindGraph.nodeGroup.selectAll("circle").data(nodes).exit().remove();
|
|
|
|
+ const nodeText = mindGraph.nodeTextGroup
|
|
|
|
+ .attr("class", "node-texts")
|
|
|
|
+ .selectAll("text")
|
|
|
|
+ .data(nodes)
|
|
|
|
+ .enter().append("text")
|
|
|
|
+ .attr("id", (d)=> d.id)
|
|
|
|
+ .style("font-size", "12px")
|
|
|
|
+ .style("fill","#333333")
|
|
|
|
+ .style("pointer-events", "none") // 确保文本不会干扰鼠标事件
|
|
|
|
+ .attr("class", "nodeText")
|
|
|
|
+ .attr("dx", 16) // 将文本偏移一定距离,以避免覆盖圆圈
|
|
|
|
+ .attr("dy", ".35em") // 垂直对齐文本
|
|
|
|
+ .text(d => d.name); // 设置文本内容为节点数据中的text属性
|
|
|
|
+
|
|
|
|
+ mindGraph.nodeTextGroup.selectAll("text").data(nodes).exit().remove();
|
|
|
|
+ // 绘制链接上的文本
|
|
|
|
+ const linkText = mindGraph.linkTextGroup
|
|
|
|
+ .attr("class", "link-texts")
|
|
|
|
+ .selectAll("text")
|
|
|
|
+ .data(links)
|
|
|
|
+ .enter().append("text")
|
|
|
|
+ .attr("id", (d)=> d.id)
|
|
|
|
+ .attr("class", "linkText")
|
|
|
|
+ .style("text-anchor", "middle")
|
|
|
|
+ .style("dominant-baseline", "central")
|
|
|
|
+ .style("font-size","6")
|
|
|
|
+ .style("fill","#CDCDCD")
|
|
|
|
+ .attr("pointer-events", "none") // 确保文本不会干扰鼠标事件
|
|
|
|
+ .text(d => d.text);
|
|
|
|
+
|
|
|
|
+ mindGraph.linkTextGroup.selectAll("text").data(links).exit().remove();
|
|
|
|
+ function dragstarted(event, d) {
|
|
|
|
+ if (!event.active) mindGraph.simulation.alphaTarget(0.3).restart();
|
|
|
|
+ console.log(`drag(): started at ${d.x},${d.y}`)
|
|
|
|
+ d.fx = d.x;
|
|
|
|
+ d.fy = d.y;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function dragged(event, d) {
|
|
|
|
+ d.fx = event.x;
|
|
|
|
+ d.fy = event.y;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function dragended(event, d) {
|
|
|
|
+ if (!event.active) mindGraph.simulation.alphaTarget(0);
|
|
|
|
+
|
|
|
|
+ console.log(`drag(): end at ${d.fx},${d.fy}`);
|
|
|
|
+
|
|
|
|
+ d.fx = null;
|
|
|
|
+ d.fy = null;
|
|
|
|
+ }
|
|
|
|
+ return {node, link, nodeText, linkText};
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// export function draw2() {
|
|
|
|
+// console.log("draw graph");
|
|
|
|
+
|
|
|
|
+// console.log(`draw(): total nodes ${nodes.length}, total edges: ${edges.length}`);
|
|
|
|
+// dataLinks.value = edges?.map((link, i) => {
|
|
|
|
+// return {
|
|
|
|
+// source: nodes.find(node => node.id === link.src_id),
|
|
|
|
+// target: nodes.find(node => node.id === link.dest_id),
|
|
|
|
+// text: link.category,
|
|
|
|
+// index: link.id // 用于生成唯一ID
|
|
|
|
+// }
|
|
|
|
+// }
|
|
|
|
+// );
|
|
|
|
+
|
|
|
|
+// console.log(`draw(): total nodes ${nodes.length}, total links: ${dataLinks.value.length}`);
|
|
|
|
+// // 将字符串ID转换为节点对象引用
|
|
|
|
+// // Create a force simulation after converting links
|
|
|
|
+
|
|
|
|
+// // Add paths to the SVG
|
|
|
|
+// const linkPath = mindGraph.linkGroup.selectAll("path")
|
|
|
|
+// .data(dataLinks.value)
|
|
|
|
+// .enter().append("path")
|
|
|
|
+// .attr("class","link")
|
|
|
|
+// .attr("id", (d, i) => `linkPath-${i}`) // 为每个路径添加唯一ID
|
|
|
|
+// .attr("stroke-width", 1.5)
|
|
|
|
+// .attr("stroke", "#999")
|
|
|
|
+// .attr("fill", "none")
|
|
|
|
+// .attr("marker-end", "url(#arrow)")
|
|
|
|
+// .on('click', (event, d) => {
|
|
|
|
+// console.log(`Link clicked: ${d.text}`);
|
|
|
|
+// // 在这里添加你的链接点击逻辑
|
|
|
|
+// });
|
|
|
|
+// console.log(`draw(): create linkPath finished`);
|
|
|
|
+// // Add text labels to the links
|
|
|
|
+// const linkText = d3.select(".link-texts").selectAll("text")
|
|
|
|
+// .data(dataLinks.value)
|
|
|
|
+// .enter().append("text")
|
|
|
|
+// .attr('class', 'linkText')
|
|
|
|
+// .style("text-anchor", "middle")
|
|
|
|
+// .style("pointer-events", "none") // 将文本居中对齐
|
|
|
|
+// .each(function(d) {
|
|
|
|
+// const textElement = d3.select(this);
|
|
|
|
+// textElement.append("textPath")
|
|
|
|
+// .attr("href", d => `#linkPath-${d.index}`) // 使用 href 而不是 xlink:href
|
|
|
|
+// .attr("startOffset", "50%") // 文本从路径的50%处开始
|
|
|
|
+// .attr("fill", "#555") // 设置文本颜色
|
|
|
|
+// .text(d => d.text); // 设置文本内容
|
|
|
|
+// console.log(d.text);
|
|
|
|
+// });
|
|
|
|
+// // .append("textPath")
|
|
|
|
+// // .attr("href", d => `#linkPath-${d.index}`) // 使用 href 而不是 xlink:href
|
|
|
|
+// // .attr("startOffset", "50%") // 文本从路径的50%处开始
|
|
|
|
+// // .attr("fill", "#555") // 设置文本颜色
|
|
|
|
+// // .text(d => d.text) // 设置文本内容
|
|
|
|
+// // .on('click', (event, d) => {
|
|
|
|
+// // console.log(`Link clicked: ${d.prop_name}`);
|
|
|
|
+// // // 在这里添加你的链接点击逻辑
|
|
|
|
+// // });
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// console.log(`draw(): create linkText finished`);
|
|
|
|
+// // Add nodes to the SVG
|
|
|
|
+// const node = mindGraph.nodeGroup.selectAll("circle")
|
|
|
|
+// .data(nodes)
|
|
|
|
+// .enter().append("circle")
|
|
|
|
+// .attr("r", 8)
|
|
|
|
+// .attr("class", "node")
|
|
|
|
+// .attr("fill", (d)=>{
|
|
|
|
+// var color = nodeColors["_default"]['color'];
|
|
|
|
+// var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+// if (keys.indexOf(d.category)>=0){
|
|
|
|
+// return nodeColors[d.category]['color'];
|
|
|
|
+// }
|
|
|
|
+// return color;
|
|
|
|
+// }).attr('stroke', (d)=>{
|
|
|
|
+// var color = nodeColors["_default"]['color'];
|
|
|
|
+// var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+// if (keys.indexOf(d.category)>=0){
|
|
|
|
+// return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+// }
|
|
|
|
+// return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+// }).call(d3.drag()
|
|
|
|
+// .on("start", dragstarted)
|
|
|
|
+// .on("drag", dragged)
|
|
|
|
+// .on("end", dragended))
|
|
|
|
+// .on('click', (event, d) => {
|
|
|
|
+// mindGraph.click(d.id);
|
|
|
|
+// //console.log(d3.select( event.currentTarget).data());
|
|
|
|
+// if (selectedNodes.includes(d.id)){
|
|
|
|
+// console.log('clear current node selection state');
|
|
|
|
+// d3.select( event.currentTarget).attr('stroke', (d)=>{
|
|
|
|
+// var color = nodeColors["_default"]['color'];
|
|
|
|
+// var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+// if (keys.indexOf(d.category)>=0){
|
|
|
|
+// return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+// }
|
|
|
|
+// return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+// });
|
|
|
|
+// d3.select( event.currentTarget).attr('r',8);
|
|
|
|
+// d3.select( event.currentTarget).attr('stroke-width','1');
|
|
|
|
+// selectedNodes.pop(d.id);
|
|
|
|
+// } else {
|
|
|
|
+
|
|
|
|
+// console.log('clear previous nodes selection state');
|
|
|
|
+// var selectedCircles = d3.selectAll("circle").filter(function() {
|
|
|
|
+// var stroke = d3.select(this).attr("stroke");
|
|
|
|
+// if (stroke && stroke.toLowerCase() === 'red') {
|
|
|
|
+// d3.select(this).attr('stroke', (d)=>{
|
|
|
|
+// var color = nodeColors["_default"]['color'];
|
|
|
|
+// var keys = Object.keys(nodeColors);
|
|
|
|
+
|
|
|
|
+// if (keys.indexOf(d.category)>=0){
|
|
|
|
+// return d3.color(nodeColors[d.category]['color']).darker(1);
|
|
|
|
+// }
|
|
|
|
+// return d3.color(color).darker(1);;
|
|
|
|
+
|
|
|
|
+// });
|
|
|
|
+// d3.select(this).attr('r',8);
|
|
|
|
+// d3.select(this).attr('stroke-width','1');
|
|
|
|
+// }
|
|
|
|
+// });
|
|
|
|
+// selectedNodes.length = 0;
|
|
|
|
+// console.log('set current selection state');
|
|
|
|
+// selectedNodes.push(d.id);
|
|
|
|
+// d3.select( event.currentTarget).attr('stroke','red');
|
|
|
|
+// d3.select( event.currentTarget).attr('r',12);
|
|
|
|
+// d3.select( event.currentTarget).attr('stroke-width','2');
|
|
|
|
+// }
|
|
|
|
+// // if (timer == null){
|
|
|
|
+// // timer = setTimeout(()=>{
|
|
|
|
+// // timer = null;
|
|
|
|
+// // console.log("click node "+ d.id);
|
|
|
|
+// // funcClick(d.id);
|
|
|
|
+// // }, 250);
|
|
|
|
+// // return;
|
|
|
|
+// // }
|
|
|
|
+// // console.log("db click node");
|
|
|
|
+// // clearTimeout(timer);
|
|
|
|
+// // timer = null;
|
|
|
|
+// //loadData(d.id);
|
|
|
|
+// }).on('contextmenu', d3ContextMenu(node_menu, {
|
|
|
|
+// onOpen: function(data, event){
|
|
|
|
+// console.log("context menu open");
|
|
|
|
+// },
|
|
|
|
+// onClose: function(data, event){
|
|
|
|
+// console.log("context menu close");
|
|
|
|
+// },
|
|
|
|
+// position: function(data, event){
|
|
|
|
+// console.log("context menu position");
|
|
|
|
+// var bounds = this.getBoundingClientRect();
|
|
|
|
+// return {
|
|
|
|
+// left: bounds.left + bounds.width + 10,
|
|
|
|
+// top: bounds.top
|
|
|
|
+// }
|
|
|
|
+// }
|
|
|
|
+
|
|
|
|
+// }));
|
|
|
|
+
|
|
|
|
+// console.log(`draw(): create nodes finished`);
|
|
|
|
+// // Add text labels to the nodes
|
|
|
|
+// const nodeText = mindGraph.nodeTextGroup.selectAll("text")
|
|
|
|
+// .data(nodes)
|
|
|
|
+// .enter().append("text")
|
|
|
|
+// .attr("class", "nodeText")
|
|
|
|
+// .attr("dx", 16) // 将文本偏移一定距离,以避免覆盖圆圈
|
|
|
|
+// .attr("dy", ".35em") // 垂直对齐文本
|
|
|
|
+// .text(d => d.name); // 设置文本内容为节点数据中的text属性
|
|
|
|
+// console.log(`draw(): create nodeText finished`);
|
|
|
|
+// //箭头离本体的距离
|
|
|
|
+// // Update positions on each tick of the simulation
|
|
|
|
+
|
|
|
|
+// if (mindGraph.simulation == null){
|
|
|
|
+// console.log(`draw(): create simulation`);
|
|
|
|
+// mindGraph.simulation = d3.forceSimulation(nodes)
|
|
|
|
+// .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(1))
|
|
|
|
+// .force("charge", d3.forceManyBody().strength(-30).distanceMax(80)).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
|
|
|
|
+// } else {
|
|
|
|
+// console.log(`draw(): previous simulation existed, skipped.`);
|
|
|
|
+
|
|
|
|
+// mindGraph.simulation = d3.forceSimulation(nodes)
|
|
|
|
+// .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(1))
|
|
|
|
+// .force("charge", d3.forceManyBody().strength(-30).distanceMax(80)).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
|
|
|
|
+
|
|
|
|
+// //.force("link", d3.forceLink(links).id(d => d.id).distance(120))
|
|
|
|
+// //.force("charge", d3.forceManyBody()).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
|
|
|
|
+// }
|
|
|
|
+// //const simulation = d3.forceSimulation(nodes)
|
|
|
|
+// // .force("link", d3.forceLink(dataLinks.value).id(d => d.id).distance(120))
|
|
|
|
+// // .force("charge", d3.forceManyBody()).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
|
|
|
|
+// //.force("x", d3.forceX(width/2))
|
|
|
|
+// //.force("y", d3.forceY(height/2));
|
|
|
|
+
|
|
|
|
+// //.force("center", d3.forceCenter(400, 300));
|
|
|
|
+// mindGraph.simulation.on("end", () => {
|
|
|
|
+// console.log("simulation end");
|
|
|
|
+// //simulation.force("link", null);
|
|
|
|
+// //simulation.force("charge", null);
|
|
|
|
+// //simulation.force("center", null);
|
|
|
|
+// });
|
|
|
|
+// //let maxTick = 80;
|
|
|
|
+// //let tickCount = 0;
|
|
|
|
+// mindGraph.simulation.on("tick", simulation_ticked);
|
|
|
|
+// //mindGraph.simulation.force("link").links(links).distance(d => { return 200 });
|
|
|
|
+
|
|
|
|
+// // Drag handlers
|
|
|
|
+// function dragstarted(event, d) {
|
|
|
|
+// if (!event.active) {
|
|
|
|
+// mindGraph.simulation.alphaTarget(0.3).restart();
|
|
|
|
+// console.log("dragstarted(): event.active");
|
|
|
|
+// }
|
|
|
|
+// event.subject.fx = event.subject.x;
|
|
|
|
+// event.subject.fy = event.subject.y;
|
|
|
|
+// // d.fx = d.x;
|
|
|
|
+// // d.fy = d.y;
|
|
|
|
+// // console.log("drag started");
|
|
|
|
+// }
|
|
|
|
+
|
|
|
|
+// function dragged(event, d) {
|
|
|
|
+// // d.fx = event.x;
|
|
|
|
+// // d.fy = event.y;
|
|
|
|
+
|
|
|
|
+// event.subject.fx = event.x;
|
|
|
|
+// event.subject.fy = event.y;
|
|
|
|
+// }
|
|
|
|
+
|
|
|
|
+// function dragended(event, d) {
|
|
|
|
+// if (!event.active) mindGraph.simulation.alphaTarget(0);
|
|
|
|
+// // console.log("drag end ");
|
|
|
|
+// // d.fx = null;
|
|
|
|
+// // d.fy = null;
|
|
|
|
+// event.subject.fx = null;
|
|
|
|
+// event.subject.fy = null;
|
|
|
|
+// }
|
|
|
|
+// }
|