diff --git a/lib/graphHighlight.ts b/lib/graphHighlight.ts new file mode 100644 index 00000000..8410b1e9 --- /dev/null +++ b/lib/graphHighlight.ts @@ -0,0 +1,68 @@ +import { select } from "d3"; + +type Node = { id: string | number; [s: symbol]: Edge[] }; + +type Edge = { + id: string | number; + [s: symbol]: Node; +}; + +type Symbols = { + sourceNodeSym: symbol; + + targetNodeSym: symbol; + + edgeSym: symbol; +}; + +export function addHighlightOnHover( + symbols: Symbols, + nodes: Node[], + nodesSelection: d3.Selection, + linksSelection: d3.Selection +) { + nodesSelection.on("mouseover", function (e, d) { + select(this).classed("-active", true); + const node = nodes.find((n) => n.id === d.id); + const connectedEdges = node[symbols.edgeSym]; + + const connectedNodesIds = new Set( + connectedEdges + .map((edge) => [ + edge[symbols.sourceNodeSym].id, + edge[symbols.targetNodeSym].id, + ]) + .flat() + ); + + nodesSelection + .classed("-fadeout", (p) => d !== p && !connectedNodesIds.has(p.id)) + .classed("-half-active", (p) => { + return p !== d && connectedNodesIds.has(p.id); + }); + linksSelection + .classed( + "-fadeout", + (p) => + p[symbols.sourceNodeSym].id !== d.id && + p[symbols.targetNodeSym].id !== d.id + ) + .classed("-active", (p) => { + return ( + p[symbols.sourceNodeSym].id === d.id || + p[symbols.targetNodeSym].id === d.id + ); + }); + }); + + nodesSelection.on("mouseleave", function () { + linksSelection + .classed("-active", false) + .classed("-fadeout", false) + .classed("-half-active", false); + nodesSelection + .classed("-active", false) + .classed("-fadeout", false) + .classed("-half-active", false); + }); +} diff --git a/stanzas/chord-diagram/drawChordDiagram.js b/stanzas/chord-diagram/drawChordDiagram.ts similarity index 68% rename from stanzas/chord-diagram/drawChordDiagram.js rename to stanzas/chord-diagram/drawChordDiagram.ts index 06ff61f1..38c93f1d 100644 --- a/stanzas/chord-diagram/drawChordDiagram.js +++ b/stanzas/chord-diagram/drawChordDiagram.ts @@ -1,10 +1,20 @@ import * as d3 from "d3"; +import { addHighlightOnHover } from "../../lib/graphHighlight"; + +interface ExtendedChords extends d3.Chords { + groups: (d3.ChordGroup & { + color: string; + tooltip: string; + label: string; + id: string; + })[]; +} export function drawChordDiagram(svg, nodes, edges, { symbols, ...params }) { const names = nodes.map((node) => node[params.nodeLabelParams.dataKey]); const matrix = (() => { - const index = new Map(names.map((name, i) => [name, i])); + const index = new Map(names.map((name, i) => [name, i])); const matrix = Array.from(index, () => new Array(names.length).fill(0)); for (const edge of edges) { matrix[index.get(edge.source)][index.get(edge.target)] += @@ -39,15 +49,25 @@ export function drawChordDiagram(svg, nodes, edges, { symbols, ...params }) { .sortSubgroups(d3.descending) .sortChords(d3.descending); - const chords = chord(matrix); + const chords = chord(matrix) as ExtendedChords; const edgeColorScale = params.color(); + chords.forEach((chord) => { + chord[symbols.sourceNodeSym] = nodes[chord.source.index]; + chord[symbols.targetNodeSym] = nodes[chord.target.index]; + }); + + // Nodes (arcs) chords.groups.forEach((node) => { - node.color = edgeColorScale("" + node.index); + node.id = nodes[node.index][params.nodeLabelParams.dataKey]; + + node[symbols.nodeColorSym] = edgeColorScale("" + node.index); + node[symbols.edgeSym] = edges.filter( + (edge) => edge.source === node.id || edge.target === node.id + ); node.tooltip = nodes[node.index][params.tooltipParams.dataKey]; node.label = nodes[node.index][params.nodeLabelParams.dataKey]; - node.id = nodes[node.index][params.nodeLabelParams.dataKey]; }); const rootGroup = svg @@ -66,7 +86,7 @@ export function drawChordDiagram(svg, nodes, edges, { symbols, ...params }) { .attr("d", ribbon) .classed("link", true) .classed("chord", true) - .style("fill", (d) => chords.groups[d.source.index].color); + .style("fill", (d) => chords.groups[d.source.index][symbols.nodeColorSym]); const arcsG = rootGroup .append("g") @@ -79,7 +99,7 @@ export function drawChordDiagram(svg, nodes, edges, { symbols, ...params }) { const arcs = arcsG .append("path") .attr("d", arc) - .attr("fill", (d) => d.color); + .attr("fill", (d) => d[symbols.nodeColorSym]); arcsG.call((g) => g @@ -125,47 +145,6 @@ export function drawChordDiagram(svg, nodes, edges, { symbols, ...params }) { } if (params.highlightAdjEdges) { - arcsG.on("mouseenter", onHighlight); - arcsG.on("mouseleave", onHighlightOff); - } - - function onHighlight(e, d) { - const node = nodes[d.index]; - const connectedEdges = node[symbols.edgeSym]; - const connectedNodesIds = connectedEdges - .map((edge) => [ - edge[symbols.sourceNodeSym].id, - edge[symbols.targetNodeSym].id, - ]) - .flat(); - - d3.select(this).classed("active", true); - arcsG.classed("fadeout", (p) => { - return d.index !== p.index; - }); - arcsG.classed("half-active", (p) => { - return d.index !== p.index && connectedNodesIds.includes(p.id); - }); - ribbons.classed( - "fadeout", - (p) => p.source.index !== d.index && p.target.index !== d.index - ); - ribbons.classed("active", (p) => { - return p.source.index === d.index || p.target.index === d.index; - }); - // ribbons.each(function (p) { - // const isActive = p.source.index === d.index || p.target.index === d.index - // if (isActive) { - // } - // nodes[p.source.index] - // }); - } - function onHighlightOff() { - arcsG.classed("active", false); - arcsG.classed("fadeout", false); - arcsG.classed("half-active", false); - - ribbons.classed("active", false); - ribbons.classed("fadeout", false); + addHighlightOnHover(symbols, nodes, arcsG, ribbons); } } diff --git a/stanzas/chord-diagram/drawCircleLayout.js b/stanzas/chord-diagram/drawCircleLayout.ts similarity index 80% rename from stanzas/chord-diagram/drawCircleLayout.js rename to stanzas/chord-diagram/drawCircleLayout.ts index 809dfeeb..cd52f82f 100644 --- a/stanzas/chord-diagram/drawCircleLayout.js +++ b/stanzas/chord-diagram/drawCircleLayout.ts @@ -1,5 +1,5 @@ import * as d3 from "d3"; - +import { addHighlightOnHover } from "../../lib/graphHighlight"; export default function ( svg, nodes, @@ -113,7 +113,7 @@ export default function ( .data(nodes) .enter() .append("g") - .attr("class", "node-group") + .attr("class", "node") .attr( "transform", (d) => @@ -124,7 +124,6 @@ export default function ( const nodeCircles = nodeGroups .append("circle") - .attr("class", "node") .style("fill", (d) => d[symbols.nodeColorSym]) .attr("r", (d) => { return d[symbols.nodeSizeSym]; //OK @@ -161,43 +160,13 @@ export default function ( } } + // comment + if (tooltipParams.show) { nodeCircles.attr("data-tooltip", (d) => d[tooltipParams.dataKey]); } if (highlightAdjEdges) { - nodeGroups.on("mouseover", function (e, d) { - // highlight current node - d3.select(this).classed("active", true); - // fade out all other nodes, highlight a little connected ones - nodeGroups - .classed("fadeout", (p) => d !== p) - .classed("half-active", (p) => { - return ( - p !== d && - d[symbols.edgeSym].some( - (edge) => - edge[symbols.sourceNodeSym] === p || - edge[symbols.targetNodeSym] === p - ) - ); - }); - - // fadeout not connected edges, highlight connected ones - links - .classed("fadeout", (p) => !d[symbols.edgeSym].includes(p)) - .classed("active", (p) => d[symbols.edgeSym].includes(p)); - }); - - nodeGroups.on("mouseleave", function () { - links - .classed("active", false) - .classed("fadeout", false) - .classed("half-active", false); - nodeGroups - .classed("active", false) - .classed("fadeout", false) - .classed("half-active", false); - }); + addHighlightOnHover(symbols, nodes, nodeGroups, links); } } diff --git a/stanzas/chord-diagram/index.ts b/stanzas/chord-diagram/index.ts index 2365713a..0920001b 100644 --- a/stanzas/chord-diagram/index.ts +++ b/stanzas/chord-diagram/index.ts @@ -27,6 +27,8 @@ interface Datum { id: string; } +//log + export default class ChordDiagram extends MetaStanza { tooltip: ToolTip; _chartArea: d3.Selection; diff --git a/stanzas/chord-diagram/style.scss b/stanzas/chord-diagram/style.scss index d1e2a53c..561088f3 100644 --- a/stanzas/chord-diagram/style.scss +++ b/stanzas/chord-diagram/style.scss @@ -1,18 +1,15 @@ // Load the repository-wide global style here. The actual file is located at ../common.scss for details. @use "@/common.scss"; -svg { - background-color: var(--togostanza-background-color); -} - .label, .node, +.node-group, .link, -.active, -.fadeout, -.half-active { +.-active, +.-fadeout, +.-half-active { transition: var(--togostanza-fadeout-transition); - transition-property: opacity, stroke-width, stroke-opacity; + transition-property: opacity, stroke-width, stroke-opacity, fill-opacity; } .label { @@ -25,54 +22,61 @@ svg { .node { stroke: var(--togostanza-border-color); stroke-width: var(--togostanza-border-width); + cursor: pointer; &.-selected { - text { - fill: var(--togostanza-theme-selected_border_color); - } + opacity: 1 !important; + } + + &.-active { + opacity: 1; + stroke-width: calc(var(--togostanza-border-width) * 2.5px); + stroke-opacity: 1; + } + + &.-half-active { + opacity: calc(var(--togostanza-fadeout-opacity) + 0.3); + stroke-width: calc(var(--togostanza-border-width) * 1.6px); + } + + &.-fadeout { + opacity: var(--togostanza-fadeout-opacity); } } .link { - stroke-opacity: var(--togostanza-edge-opacity); stroke-linecap: round; fill: none; stroke: var(--togostanza-edge-color); + stroke-opacity: var(--togostanza-edge-opacity); + &.chord { - stroke: none; - } -} + stroke-width: 0.5px; + stroke-opacity: 0; + fill-opacity: calc(var(--togostanza-edge-opacity)); -.active { - & > .node { - stroke-width: calc(var(--togostanza-border-width) * 2.5px); - stroke-opacity: 1; - } - & > .link { - stroke-opacity: 1; - } - & > .label { - opacity: 1; - } -} + &.-active { + fill-opacity: var(--togostanza-edge-opacity); + stroke: var(--togostanza-edge-color); + stroke-width: 0.5px; + stroke-opacity: var(--togostanza-edge-opacity); + } -.fadeout { - opacity: var(--togostanza-fadeout-opacity); - & > .link { - stroke-opacity: var(--togostanza-fadeout-opacity); + &.-fadeout { + fill-opacity: var(--togostanza-fadeout-opacity); + stroke-opacity: 0; + } } -} -.half-active { - opacity: 0.8; - & > .node { - stroke-width: calc(var(--togostanza-border-width) * 1.6px); - stroke-opacity: 0.8; + &.-active { + stroke-opacity: var(--togostanza-edge-opacity); } - & > .label { - opacity: 0.8; + &.-fadeout { + stroke-opacity: calc( + var(--togostanza-fadeout-opacity) * var(--togostanza-edge-opacity) + ); } } @@ -81,8 +85,21 @@ svg { } .ribbons { - fill-opacity: 0.5; + fill-opacity: var(--togostanza-fadeout-opacity); > path { mix-blend-mode: multiply; } } + +svg { + background-color: var(--togostanza-background-color); + + :has(.-selected) { + .node { + opacity: var(--togostanza-fadeout-opacity); + } + .link { + opacity: var(--togostanza-fadeout-opacity); + } + } +} diff --git a/stanzas/force-graph/drawForceLayout.js b/stanzas/force-graph/drawForceLayout.js index a6940e27..7af5c3a8 100644 --- a/stanzas/force-graph/drawForceLayout.js +++ b/stanzas/force-graph/drawForceLayout.js @@ -1,4 +1,5 @@ import * as d3 from "d3"; +import { addHighlightOnHover } from "../../lib/graphHighlight"; function straightLink(d) { const start = { x: d.source.x, y: d.source.y }; @@ -181,7 +182,7 @@ export default function ( .data(nodes) .enter() .append("g") - .attr("class", "node-group") + .attr("class", "node") .attr("transform", (d) => { return `translate(${d.x},${d.y})`; }) @@ -189,7 +190,7 @@ export default function ( const nodeCircles = nodeGroups .append("circle") - .attr("class", "node") + .attr("cx", 0) .attr("cy", 0) .attr("r", (d) => d[symbols.nodeSizeSym]) @@ -210,14 +211,12 @@ export default function ( .text((d) => d[nodeLabelParams.dataKey]); } - let isDragging = false; - function drag(simulation) { function dragstarted(event) { if (!event.active) { simulation.alphaTarget(0.3).restart(); } - isDragging = true; + event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } @@ -228,7 +227,6 @@ export default function ( } function dragended(event) { - isDragging = false; if (!event.active) { simulation.alphaTarget(0); } @@ -244,44 +242,6 @@ export default function ( } if (highlightAdjEdges) { - nodeGroups.on("mouseover", function (e, d) { - if (isDragging) { - return; - } - // highlight current node - d3.select(this).classed("active", true); - // fade out all other nodes, highlight a little connected ones - nodeGroups - .classed("fadeout", (p) => d !== p) - .classed("half-active", (p) => { - return ( - p !== d && - d[symbols.edgeSym].some( - (edge) => - edge[symbols.sourceNodeSym] === p || - edge[symbols.targetNodeSym] === p - ) - ); - }); - - // fadeout not connected edges, highlight connected ones - links - .classed("fadeout", (p) => !d[symbols.edgeSym].includes(p)) - .classed("active", (p) => d[symbols.edgeSym].includes(p)); - }); - - nodeGroups.on("mouseleave", function () { - if (isDragging) { - return; - } - links - .classed("active", false) - .classed("fadeout", false) - .classed("half-active", false); - nodeGroups - .classed("active", false) - .classed("fadeout", false) - .classed("half-active", false); - }); + addHighlightOnHover(symbols, nodes, nodeGroups, links); } } diff --git a/stanzas/force-graph/index.js b/stanzas/force-graph/index.js index b8ba92b3..31bbfe25 100644 --- a/stanzas/force-graph/index.js +++ b/stanzas/force-graph/index.js @@ -5,6 +5,8 @@ import MetaStanza from "../../lib/MetaStanza"; import { handleApiError } from "../../lib/apiError"; import drawForceLayout from "./drawForceLayout"; +import { getMarginsFromCSSString } from "@/lib/utils"; + import { appendCustomCss, downloadCSVMenuItem, @@ -22,7 +24,7 @@ import { export default class ForceGraph extends MetaStanza { _graphArea; selectedEventParams = { - targetElementSelector: ".node-group", + targetElementSelector: ".node", selectedElementClassName: "-selected", selectedElementSelector: ".-selected", idPath: "id", @@ -175,25 +177,27 @@ export default class ForceGraph extends MetaStanza { symbols, }); - this._graphArea.selectAll("circle.node").on("click", (_, d) => { - toggleSelectIds({ - selectedIds: this.selectedIds, - targetId: d.id, - }); - updateSelectedElementClassNameForD3({ - drawing: this._graphArea, - selectedIds: this.selectedIds, - ...this.selectedEventParams, - }); - if (this.params["event-outgoing_change_selected_nodes"]) { - emitSelectedEvent({ - rootElement: this.element, + if (this.params["event-outgoing_change_selected_nodes"]) { + this._graphArea.selectAll(".node").on("click", (_, d) => { + toggleSelectIds({ + selectedIds: this.selectedIds, targetId: d.id, + }); + updateSelectedElementClassNameForD3({ + drawing: this._graphArea, selectedIds: this.selectedIds, - dataUrl: this.params["data-url"], + ...this.selectedEventParams, }); - } - }); + if (this.params["event-outgoing_change_selected_nodes"]) { + emitSelectedEvent({ + rootElement: this.element, + targetId: d.id, + selectedIds: this.selectedIds, + dataUrl: this.params["data-url"], + }); + } + }); + } }; handleApiError({ @@ -219,39 +223,3 @@ export default class ForceGraph extends MetaStanza { } } } - -function getMarginsFromCSSString(str) { - const splitted = str.trim().split(/\W+/); - - const res = { - TOP: 0, - RIGHT: 0, - BOTTOM: 0, - LEFT: 0, - }; - - switch (splitted.length) { - case 1: - res.TOP = res.RIGHT = res.BOTTOM = res.LEFT = parseInt(splitted[0]); - break; - case 2: - res.TOP = res.BOTTOM = parseInt(splitted[0]); - res.LEFT = res.RIGHT = parseInt(splitted[1]); - break; - case 3: - res.TOP = parseInt(splitted[0]); - res.LEFT = res.RIGHT = parseInt(splitted[1]); - res.BOTTOM = parseInt(splitted[2]); - break; - case 4: - res.TOP = parseInt(splitted[0]); - res.RIGHT = parseInt(splitted[1]); - res.BOTTOM = parseInt(splitted[2]); - res.LEFT = parseInt(splitted[3]); - break; - default: - break; - } - - return res; -} diff --git a/stanzas/force-graph/metadata.json b/stanzas/force-graph/metadata.json index eaa4bba2..024f4ae3 100644 --- a/stanzas/force-graph/metadata.json +++ b/stanzas/force-graph/metadata.json @@ -185,7 +185,7 @@ "stanza:description": "Color applied to the selected item" }, { - "stanza:key": "--togostanza-edge-default_color", + "stanza:key": "--togostanza-edge-color", "stanza:type": "color", "stanza:default": "#bdbdbd", "stanza:description": "Egdes default color" diff --git a/stanzas/force-graph/style.scss b/stanzas/force-graph/style.scss index 2c940910..1fcc16dd 100644 --- a/stanzas/force-graph/style.scss +++ b/stanzas/force-graph/style.scss @@ -1,16 +1,12 @@ // Load the repository-wide global style here. The actual file is located at ../common.scss for details. @use "@/common.scss"; -svg > defs > marker > path { - fill: var(--togostanza-edge-default_color); -} - .label, .node, .link, -.active, -.fadeout, -.half-active { +.-active, +.-fadeout, +.-half-active { transition: all 0.1s ease-in-out; transition-property: opacity, stroke-width, stroke-opacity; } @@ -24,51 +20,49 @@ svg > defs > marker > path { stroke: var(--togostanza-border-color); stroke-width: var(--togostanza-border-width); cursor: pointer; -} -.node-group { &.-selected { - text { - fill: var(--togostanza-theme-selected_border_color); - } + opacity: 1 !important; } -} -.link { - stroke-opacity: var(--togostanza-edge-opacity); - stroke-linecap: round; - fill: none; - stroke: var(--togostanza-edge-default_color); -} - -.active { - > .node { + &.-active { + opacity: 1; stroke-width: calc(var(--togostanza-border-width) * 2.5px); stroke-opacity: 1; } - > .link { - stroke-opacity: 1; + + &.-half-active { + opacity: calc(var(--togostanza-fadeout-opacity) + 0.3); + stroke-width: calc(var(--togostanza-border-width) * 1.6px); } - > .label { - opacity: 1; + + &.-fadeout { + opacity: var(--togostanza-fadeout-opacity); } } -.fadeout { - opacity: 0.05; - > .link { - stroke-opacity: 0.05; +.link { + stroke: var(--togostanza-edge-color); + opacity: var(--togostanza-edge-opacity); + + &.-fadeout { + opacity: calc( + var(--togostanza-fadeout-opacity) * var(--togostanza-edge-opacity) + ); } } -.half-active { - opacity: 0.8; - > .node { - stroke-width: calc(var(--togostanza-border-width) * 1.6px); - stroke-opacity: 0.8; +svg { + & > defs > marker > path { + fill: var(--togostanza-edge-color); } - > .label { - opacity: 0.6; + &:has(.-selected) { + .node { + opacity: var(--togostanza-fadeout-opacity); + } + .link { + opacity: var(--togostanza-fadeout-opacity); + } } }