From d26b02a743d345ac0451f70886b168e67294420b Mon Sep 17 00:00:00 2001 From: Moritz Warning Date: Mon, 5 May 2025 18:07:40 +0200 Subject: [PATCH] add selection feature Select a node or link while holding the CTRL key. --- src/js/netjsongraph.config.js | 60 +++++++++++++++++++ src/js/netjsongraph.core.js | 110 ++++++++++++++++++++++++++++++++++ src/js/netjsongraph.js | 4 ++ src/js/netjsongraph.render.js | 43 +++++++------ 4 files changed, 198 insertions(+), 19 deletions(-) diff --git a/src/js/netjsongraph.config.js b/src/js/netjsongraph.config.js index dbde0ce6..046d4f3a 100644 --- a/src/js/netjsongraph.config.js +++ b/src/js/netjsongraph.config.js @@ -85,10 +85,39 @@ const NetJSONGraphDefaultConfig = { legendHoverLink: true, emphasis: { focus: "none", + scale: 2, lineStyle: { color: "#3acc38", opacity: 1, }, + itemStyle: { + //color: "#3acc38", + color: { + type: "radial", + x: 0.5, + y: 0.5, + r: 0.5, + colorStops: [ + { + offset: 0, + color: "#ffebc4", + }, + { + offset: 0.5, + color: "#ffebc4", + }, + { + offset: 0.51, + color: "#ffffff33", + }, + { + offset: 1, + color: "#ffffff33", + }, + ], + opacity: 1, + }, + }, }, nodeStyle: { color: "#ffebc4", @@ -172,6 +201,37 @@ const NetJSONGraphDefaultConfig = { nodeStyle: { color: "#1566a9", }, + emphasis: { + focus: "none", + scale: 2, + itemStyle: { + color: { + type: "radial", + x: 0.5, + y: 0.5, + r: 0.5, + colorStops: [ + { + offset: 0, + color: "#1566a9", + }, + { + offset: 0.5, + color: "#1566a9", + }, + { + offset: 0.51, + color: "green", + }, + { + offset: 1, + color: "green", + }, + ], + opacity: 1, + }, + }, + }, nodeSize: "17", }, linkConfig: { diff --git a/src/js/netjsongraph.core.js b/src/js/netjsongraph.core.js index 33c89b36..eb64c051 100644 --- a/src/js/netjsongraph.core.js +++ b/src/js/netjsongraph.core.js @@ -1,6 +1,115 @@ import NetJSONGraphDefaultConfig from "./netjsongraph.config"; import NetJSONGraphUpdate from "./netjsongraph.update"; +class Selection { + constructor() { + this.selected = new Set(); + } + + getSetId(item) { + if (item.node) { + // map node + return item.node.id; + } else if (item.link) { + // map link + return (item.link.source + "=>" + item.link.target); + } else if (item.id) { + // graph node + return item.id; + } else { + // graph link + return (item.source + "=>" + item.target); + } + } + + isSelected(item) { + let id = this.getSetId(item); + return this.selected.has(id); + } + + toggleSelection(item) { + let id = this.getSetId(item); + + if (this.selected.has(id)) { + this.selected.delete(id) + return false; + } else { + this.selected.add(id); + return true; + } + } + + changeSelection(echarts, params) { + const multiSelectKey = (window.event.ctrlKey || window.event.metaKey); + const isSelectionEmpty = (this.selected.size === 0); + + if (multiSelectKey) { + if (this.toggleSelection(params.data)) { + echarts.dispatchAction( + { type: 'highlight', seriesIndex: params.seriesIndex, dataType: params.dataType, dataIndex: params.dataIndex} + ) + } else { + echarts.dispatchAction( + { type: 'downplay', seriesIndex: params.seriesIndex, dataType: params.dataType, dataIndex: params.dataIndex} + ) + } + } else if (!isSelectionEmpty) { + const option = echarts.getOption(); + let nodeData = []; + let linkData = []; + if (option.leaflet) { + // map data + nodeData = option.leaflet[0].mapOptions.nodeConfig.data; + linkData = option.leaflet[0].mapOptions.linkConfig.data; + } else { + // graph data + nodeData = option.series[0].nodes; + linkData = option.series[0].links; + } + const nodeIndexes = nodeData.map((node, index) => index); + const linkIndexes = linkData.map((link, index) => index); + + // downplay all items + echarts.dispatchAction( + { type: 'downplay', seriesIndex: 0, batch: [ + {dataType: "node", dataIndex: nodeIndexes}, + {dataType: "edge", dataIndex: linkIndexes}, + ] + } + ) + + this.selected.clear(); + } + } + + // called when switching between graph/map view + highlightSelected(echarts) { + const option = echarts.getOption(); + let nodeData = []; + let linkData = []; + if (option.leaflet) { + // map data + nodeData = option.leaflet[0].mapOptions.nodeConfig.data; + linkData = option.leaflet[0].mapOptions.linkConfig.data; + } else { + // graph data + nodeData = option.series[0].nodes; + linkData = option.series[0].links; + } + + const nodeIndexes = nodeData.map((node, index) => this.isSelected(node) ? index : -1); + const linkIndexes = linkData.map((link, index) => this.isSelected(link) ? index : -1); + + echarts.dispatchAction( + { type: 'highlight', seriesIndex: 0, batch: [ + {dataType: "node", dataIndex: nodeIndexes}, + {dataType: "edge", dataIndex: linkIndexes}, + ] + } + ) + } +} + class NetJSONGraph { /** * @constructor @@ -10,6 +119,7 @@ class NetJSONGraph { */ constructor(JSONParam) { this.utils = new NetJSONGraphUpdate(); + this.selection = new Selection(); this.config = {...NetJSONGraphDefaultConfig}; this.JSONParam = this.utils.isArray(JSONParam) ? JSONParam : [JSONParam]; } diff --git a/src/js/netjsongraph.js b/src/js/netjsongraph.js index b48c8514..474a2d20 100644 --- a/src/js/netjsongraph.js +++ b/src/js/netjsongraph.js @@ -115,6 +115,8 @@ class NetJSONGraph { * @returns {Object} - The graph configuration. */ onLoad() { + this.utils.echartsSetEventHandler(this); + if (this.config.metadata && this.type === "netjson") { this.gui.createMetaInfoContainer(this.graph); this.utils.updateMetadata.call(this); @@ -151,6 +153,8 @@ class NetJSONGraph { document.querySelector(".leaflet-control-zoom").style.display = "block"; } + + this.selection.highlightSelected(this.echarts); }; } this.utils.hideLoading.call(this); diff --git a/src/js/netjsongraph.render.js b/src/js/netjsongraph.render.js index 74bf8511..9b8f4102 100644 --- a/src/js/netjsongraph.render.js +++ b/src/js/netjsongraph.render.js @@ -32,6 +32,30 @@ echarts.use([ ]); class NetJSONGraphRender { + echartsSetEventHandler(self) { + self.echarts.on( + "click", + (params) => { + self.selection.changeSelection(self.echarts, params); + + const clickElement = self.config.onClickElement.bind(self); + if (params.componentSubType === "graph") { + return clickElement( + params.dataType === "edge" ? "link" : "node", + params.data, + ); + } + if (params.componentSubType === "graphGL") { + return clickElement("node", params.data); + } + return params.componentSubType === "lines" + ? clickElement("link", params.data.link) + : !params.data.cluster && clickElement("node", params.data.node); + }, + {passive: true}, + ); + } + /** * @function * @name echartsSetOption @@ -95,25 +119,6 @@ class NetJSONGraphRender { ); echartsLayer.setOption(self.utils.deepMergeObj(commonOption, customOption)); - echartsLayer.on( - "click", - (params) => { - const clickElement = configs.onClickElement.bind(self); - if (params.componentSubType === "graph") { - return clickElement( - params.dataType === "edge" ? "link" : "node", - params.data, - ); - } - if (params.componentSubType === "graphGL") { - return clickElement("node", params.data); - } - return params.componentSubType === "lines" - ? clickElement("link", params.data.link) - : !params.data.cluster && clickElement("node", params.data.node); - }, - {passive: true}, - ); return echartsLayer; }