diff --git a/package.json b/package.json index 716ff84..b29dadd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-link-graph-ui", - "version": "1.2.3", + "version": "1.3.0", "scripts": { "dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", "dev": "webpack --mode development --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", diff --git a/src/index.ts b/src/index.ts index 2549495..608e583 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ var deepEqual = require("fast-deep-equal"); interface Edge { source: string; target: string; + sourceDistanceToCurrentNode?: number; + targetDistanceToCurrentNode?: number; focused: boolean; } @@ -14,7 +16,7 @@ interface Node { id: string; title: string; focused: boolean; - distanceToCurrentNote?: number; + distanceToCurrentNode?: number; } interface GraphData { @@ -24,7 +26,8 @@ interface GraphData { nodeFontSize: number; nodeDistanceRatio: number; showLinkDirection: boolean; - maxDegree: number; + // maxDegree > 0 + graphIsSelectionBased: boolean; } let data: GraphData; @@ -211,7 +214,7 @@ async function fetchData() { nodeDistanceRatio: (await joplin.settings.value("SETTING_NODE_DISTANCE")) / 100.0, showLinkDirection, - maxDegree + graphIsSelectionBased: maxDegree > 0 }; notes.forEach(function (note, id) { @@ -231,6 +234,8 @@ async function fetchData() { data.edges.push({ source: id, target: link, + sourceDistanceToCurrentNode: notes.get(id).distanceToCurrentNote, + targetDistanceToCurrentNode: notes.get(link).distanceToCurrentNote, focused: id === selectedNote.id || link === selectedNote.id, }); @@ -248,7 +253,7 @@ async function fetchData() { id: id, title: note.title, focused: note.linkedToCurrentNote, - distanceToCurrentNote: note.distanceToCurrentNote + distanceToCurrentNode: note.distanceToCurrentNote }); }); diff --git a/src/manifest.json b/src/manifest.json index 71b181d..02845fa 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "id": "io.treymo.LinkGraph", "app_min_version": "1.7", - "version": "1.2.3", + "version": "1.3.0", "name": "Link Graph UI", "description": "Visualize the connections between Joplin notes.", "author": "Trey Mo", diff --git a/src/ui/index.js b/src/ui/index.js index 18463cb..7e8e14d 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -19,6 +19,29 @@ function update() { }); } +function addMarkerEndDef(defs, distance) { + defs + .append("marker") + .attr("id", `line-marker-end-${distance}`) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 20) + .attr("refY", 0) + .attr("markerWidth", 15) + .attr("markerHeight", 15) + .attr("markerUnits", "userSpaceOnUse") + .attr("orient", "auto") + .style("fill", `var(--distance-${distance}-primary-color, var(--distance-remaining-primary-color))`) + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); +} + +function minimalDistanceOfLink(link) { + return Math.min( + link.sourceDistanceToCurrentNode, + link.targetDistanceToCurrentNode + ); +} + document.getElementById("redrawButton").addEventListener("click", update); update(); @@ -31,6 +54,12 @@ function buildGraph(data) { width = window.innerWidth; height = window.innerHeight; + if (data.graphIsSelectionBased) { + document + .querySelector("#note_graph") + .classList.add("mode-selection-based-graph"); + } + d3.select("#note_graph > svg").remove(); svg = d3 .select("#note_graph") @@ -47,13 +76,10 @@ function buildGraph(data) { return d.id; }) - if (data.maxDegree > 0) { + if (data.graphIsSelectionBased) { // we are in selection-based graph forceLink.strength((link) => { - const minDistance = Math.min( - link.source.distanceToCurrentNote, - link.target.distanceToCurrentNote - ); + const minDistance = minimalDistanceOfLink(link); if (minDistance === 0) { return 1; } else if (minDistance === 1) { @@ -75,20 +101,15 @@ function buildGraph(data) { .force("center", d3.forceCenter(width / 2, height / 2)); if (data.showLinkDirection) { - svg - .append("defs") - .append("marker") - .attr("id", "arrow") - .attr("viewBox", "0 -5 10 10") - .attr("refX", 20) - .attr("refY", 0) - .attr("markerWidth", 15) - .attr("markerHeight", 15) - .attr("markerUnits", "userSpaceOnUse") - .attr("orient", "auto") - .attr("class", "line-marker-end") - .append("svg:path") - .attr("d", "M0,-5L10,0L0,5"); + const defs = svg.append("defs"); + // For now add arrows for ten layers (excl. center). + // todo: make more dynamic + const COUNT_LAYERS = 10; + for (let i = 0; i < COUNT_LAYERS; i++) { + addMarkerEndDef(defs, i); + } + // marker, if whole graph is shown + addMarkerEndDef(defs, "default"); } //add zoom capabilities @@ -116,8 +137,26 @@ function updateGraph(data) { .append("line") .classed("adjacent-line", (d) => d.focused); + // provide distance classes for links + if (data.graphIsSelectionBased) { + link.attr("class", function (d) { + const linkIsInward = + d.sourceDistanceToCurrentNode > d.targetDistanceToCurrentNode; + return [ + ...this.classList, + `distance-${minimalDistanceOfLink(d)}`, + ...(linkIsInward ? ["inward-link"] : []), + ].join(" "); + }); + } + if (data.showLinkDirection) { - link.attr("marker-end","url(#arrow)"); + link.attr("marker-end", (d) => { + if (data.graphIsSelectionBased) { + const minDistance = minimalDistanceOfLink(d); + return `url(#line-marker-end-${minDistance})`; + } else return `url(#line-marker-end-default)`; + }); } // Draw nodes. @@ -129,8 +168,9 @@ function updateGraph(data) { .enter() .append("g"); - node - .append("circle") + const circle = node.append("circle"); + + circle .classed("current-note", (d) => d.id === data.currentNoteID) .classed("adjacent-note", (d) => d.focused) .on("click", function (_, i) { @@ -140,8 +180,18 @@ function updateGraph(data) { }); }); - node - .append("text") + // provide distance classes for circles + if (data.graphIsSelectionBased) { + circle.attr("class", function (d) { + return [...this.classList, `distance-${d.distanceToCurrentNode}`].join( + " " + ); + }); + } + + const nodeLabel = node.append("text"); + + nodeLabel .attr("class", "node-label") .attr("font-size", data.nodeFontSize + "px") .text(function (d) { @@ -150,6 +200,15 @@ function updateGraph(data) { .attr("x", (d) => (d.id === data.currentNoteID ? 20 : 14)) .attr("y", 5); + // provide distance classes for node labels + if (data.graphIsSelectionBased) { + nodeLabel.attr("class", function (d) { + return [...this.classList, `distance-${d.distanceToCurrentNode}`].join( + " " + ); + }); + } + // update simulation nodes, links, and alpha simulation.nodes(data.nodes).on("tick", ticked); diff --git a/src/webview.css b/src/webview.css index 2b907d3..8ef4797 100644 --- a/src/webview.css +++ b/src/webview.css @@ -29,27 +29,37 @@ button { } circle { - fill: var(--joplin-background-color3); - stroke: var(--joplin-color4); + fill: var(--distance-remaining-primary-color); + stroke: var(--distance-remaining-secondary-color); r: 10; } .adjacent-note { - fill: var(--joplin-color4); + fill: var(--distance-1-primary-color); r: 13; } .current-note { - fill: var(--joplin-color); + fill: var(--distance-0-primary-color); + stroke: var(--distance-0-secondary-color); r: 18; } line { - stroke: var(--joplin-color4); + stroke: var(--distance-remaining-primary-color); +} + +line.distance-0 { + stroke: var(--distance-0-primary-color); + stroke-width: 3px; +} + +line.distance-1 { + stroke: var(--distance-1-primary-color); } .adjacent-line { - stroke: var(--joplin-color); + stroke: var(--distance-0-primary-color); stroke-width: 3px; } @@ -57,7 +67,27 @@ line { fill: var(--joplin-color); } -.line-marker-end { - fill: var(--joplin-search-marker-background-color); +#note_graph { + /* distance colors for selection-based graph */ + --distance-0-primary-color: var(--joplin-color); + --distance-0-secondary-color: var(--joplin-color4); + --distance-1-primary-color: var(--joplin-color4); + --distance-1-secondary-color: var(--joplin-color); + --distance-remaining-primary-color: var(--joplin-background-color-hover3); + --distance-remaining-secondary-color: var(--joplin-color4); +} + +/* + * Make nodes far away from selected nodes less apparent: + * Remove stroke for circles > distance-2 in selection-based graphs + */ +#note_graph.mode-selection-based-graph circle:not(.distance-0):not(.distance-1):not(.distance-2) { + stroke: unset; } +/* + * Mark links which are directed inwards/towards selected note + */ +#note_graph.mode-selection-based-graph .inward-link { + stroke-dasharray: 3; +}