jekyll-graph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Jekyll-Graph
2
+
3
+ ⚠️ This is gem is under active development! ⚠️
4
+
5
+ ⚠️ Expect breaking changes and surprises until otherwise noted (likely by v0.1.0 or v1.0.0). ⚠️
6
+
7
+ Jekyll-Graph generates data and renders a graph that allows visitors to navigate a jekyll site by clicking nodes in the graph. Nodes are generated from the site's markdown files. Links for the tree graph are generated from `jekyll-namespaces` and links for the net-web graph from `jekyll-wikilinks`.
8
+
9
+ This gem is part of the [jekyll-bonsai](https://manunamz.github.io/jekyll-bonsai/) project. 🎋
10
+
11
+ ## Installation
12
+
13
+ Follow the instructions for installing a [jekyll plugin](https://jekyllrb.com/docs/plugins/installation/) for `jekyll-graph`.
14
+
15
+ ## Usage
16
+
17
+ 1. Add `{% force_graph %}` to the site head:
18
+
19
+ ```html
20
+ <head>
21
+
22
+ ...
23
+
24
+ {% force_graph %}
25
+
26
+ </head>
27
+ ```
28
+
29
+ 2. Add a graph div in your html where you want the graph to be rendered:
30
+
31
+ ```html
32
+ <div id="jekyll-graph"></div>
33
+ ```
34
+
35
+ 3. Subclass `JekyllGraph` class in javascript like so:
36
+
37
+ ```javascript
38
+ import JekyllGraph from './jekyll-graph.js';
39
+
40
+ export default class JekyllGraphSubClass {
41
+ ...
42
+ }
43
+
44
+ // subclass
45
+ // Hook up the instance properties
46
+ Object.setPrototypeOf(JekyllGraphSubClass.prototype, JekyllGraph.prototype);
47
+
48
+ // Hook up the static properties
49
+ Object.setPrototypeOf(JekyllGraphSubClass, JekyllGraph);
50
+
51
+ ```
52
+ Call `this.drawNetWeb()` and `this.drawTree()` to actually draw the graph. You could do this simply on initialization or on a button click, etc.
53
+
54
+ Unless otherwise defined, the `jekyll-graph.js` file will be generated into `_site/assets/scripts/`.
55
+
56
+ ## Configuration
57
+
58
+ Default configs look like this:
59
+
60
+ ```yml
61
+ graph:
62
+ enabled: true
63
+ exclude: []
64
+ assets_path: "/assets"
65
+ scripts_path: "/assets/js"
66
+ tree:
67
+ enabled: true
68
+ force:
69
+ charge:
70
+ strength_x:
71
+ x_val:
72
+ strength_y:
73
+ y_val:
74
+ net_web:
75
+ enabled: true
76
+ force:
77
+ charge:
78
+ strength_x:
79
+ x_val:
80
+ strength_y:
81
+ y_val:
82
+ ```
83
+
84
+ `enabled`: Turn off the plugin by setting to `false`.
85
+ `exclude`: Exclude specific jekyll document types (`posts`, `pages`, `collection_items`).
86
+ `assets_path`: Custom graph file location from the root of the generated `_site/` directory.
87
+ `scripts_path`: Custom graph scripts location from the assets location of the generated `_site/` directory (If `assets_path` is set, but `scripts_path` is not, the location will default to `_site/<assets_path>/js/`).
88
+ `tree.enabled` and `net_web.enabled`: Toggles on/off the `tree` and `net_web` graphs, respectively.
89
+ `tree.force` and `net_web.force`: These are force variables from d3's simulation forces. You can check out the [docs for details](https://github.com/d3/d3-force#simulation_force).
90
+
91
+ Force values will likely need to be played with depending on the div size and number of nodes. [jekyll-bonsai](https://manunamz.github.io/jekyll-bonsai/) currently uses these values:
92
+
93
+ ```yaml
94
+ graph:
95
+ tree:
96
+ # enabled: true
97
+ dag_lvl_dist: 100
98
+ force:
99
+ charge: -100
100
+ strength_x: 0.3
101
+ x_val: 0.9
102
+ strength_y: 0.1
103
+ y_val: 0.9
104
+ net_web:
105
+ # enabled: true
106
+ force:
107
+ charge: -300
108
+ strength_x: 0.3
109
+ x_val: 0.75
110
+ strength_y: 0.1
111
+ y_val: 0.9
112
+ ```
113
+
114
+ No configurations are strictly necessary for plugin defaults to work.
115
+
116
+ ## Colors
117
+
118
+ Graph colors are determined by css variables which may be defined like so -- any valid css color works (hex, rgba, etc.):
119
+
120
+ ```CSS
121
+ /* nodes */
122
+ /* glow */
123
+ --graph-node-current-glow: yellow;
124
+ --graph-node-tagged-glow: green;
125
+ --graph-node-visited-glow: blue;
126
+ /* color */
127
+ --graph-node-stroke-color: grey;
128
+ --graph-node-missing-color: transparent;
129
+ --graph-node-unvisited-color: brown;
130
+ --graph-node-visited-color: green;
131
+ /* links */
132
+ --graph-link-color: brown;
133
+ --graph-particles-color: grey;
134
+ /* label text */
135
+ --graph-text-color: black;
136
+ /* */
137
+ ```
138
+
139
+ ## Data
140
+ Graph data is generated in the following format:
141
+
142
+ For the net-web graph, `graph-net-web.json`,`links` are built from `backlinks` and `attributed` metadata generated in `jekyll-wikilinks`:
143
+ ```json
144
+ // graph-net-web.json
145
+ {
146
+ "nodes": [
147
+ {
148
+ "id": "<some-id>",
149
+ "url": "<relative-url>", // site.baseurl is handled for you here
150
+ "label": "<note's-title>",
151
+ "neighbors": {
152
+ "nodes": [<neighbor-node>, ...],
153
+ "links": [<neighbor-link>, ...],
154
+ }
155
+ },
156
+ ...
157
+ ],
158
+ "links": [
159
+ {
160
+ "source": "<a-node-id>",
161
+ "target": "<another-node-id>",
162
+ },
163
+ ...
164
+ ]
165
+ }
166
+ ```
167
+ For the tree graph, `graph-tree.json`, `links` are built from a tree data structure constructed in `jekyll-namespaces`:
168
+ ```json
169
+ // graph-tree.json
170
+ {
171
+ "nodes": [
172
+ {
173
+ "id": "<some-id>",
174
+ "url": "<relative-url>", // site.baseurl wil be handled for you here
175
+ "label": "<note's-title>",
176
+ "relatives": {
177
+ "nodes": [<relative-node>, ...],
178
+ "links": [<relative-link>, ...],
179
+ }
180
+ },
181
+ ...
182
+ ],
183
+ "links": [
184
+ {
185
+ "source": "<a-node-id>",
186
+ "target": "<another-node-id>",
187
+ },
188
+ ...
189
+ ]
190
+ }
191
+ ```
192
+ Unless otherwise defined, both json files are generated into `_site/assets/`.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "jekyll/graph"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/jekyll-graph/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "jekyll-graph"
7
+ spec.version = Jekyll::Graph::VERSION
8
+ spec.authors = ["manunamz"]
9
+ spec.email = ["manunamz@pm.me"]
10
+
11
+ spec.summary = "Add d3 graph generation to jekyll."
12
+ # spec.description = "TODO: Write a longer description or delete this line."
13
+ spec.homepage = "https://github.com/manunamz/jekyll-graph"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+
16
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/manunamz/jekyll-graph"
20
+ spec.metadata["changelog_uri"] = "https://github.com/manunamz/jekyll-graph/blob/main/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ # spec.add_dependency "example-gem", "~> 1.0"
33
+
34
+ # For more information and examples about making a new gem, checkout our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+
37
+ spec.add_runtime_dependency "jekyll-namespaces", "~> 0.0.2"
38
+ spec.add_runtime_dependency "jekyll-wikilinks", "~> 0.0.6"
39
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ module Graph
6
+
7
+ class PluginConfig
8
+ CONFIG_KEY = "graph"
9
+ ENABLED_KEY = "enabled"
10
+ EXCLUDE_KEY = "exclude"
11
+ NET_WEB_KEY = "net_web"
12
+ PATH_ASSETS_KEY = "assets_path"
13
+ PATH_SCRIPTS_KEY = "scripts_path"
14
+ TREE_KEY = "tree"
15
+ TYPE_KEY = "type"
16
+
17
+ def initialize(config)
18
+ @config ||= config
19
+ @testing ||= config['testing'] if config.keys.include?('testing')
20
+ Jekyll.logger.debug("Excluded jekyll types in graph: ", option(EXCLUDE_KEY)) unless disabled?
21
+ end
22
+
23
+ # options
24
+
25
+ def disabled?
26
+ return option(ENABLED_KEY) == false
27
+ end
28
+
29
+ def disabled_net_web?
30
+ return option_net_web(ENABLED_KEY) == false
31
+ end
32
+
33
+ def disabled_tree?
34
+ return option_tree(ENABLED_KEY) == false
35
+ end
36
+
37
+ def excluded?(type)
38
+ return false unless option(EXCLUDE_KEY)
39
+ return option(EXCLUDE_KEY).include?(type.to_s)
40
+ end
41
+
42
+ def has_custom_write_path?
43
+ return !!option(PATH_ASSETS_KEY)
44
+ end
45
+
46
+ def has_custom_scripts_path?
47
+ return !!option(PATH_SCRIPTS_KEY)
48
+ end
49
+
50
+ def option(key)
51
+ @config[CONFIG_KEY] && @config[CONFIG_KEY][key]
52
+ end
53
+
54
+ def option_net_web(key)
55
+ @config[CONFIG_KEY] && @config[CONFIG_KEY][NET_WEB_KEY] && @config[CONFIG_KEY][NET_WEB_KEY][key]
56
+ end
57
+
58
+ def option_tree(key)
59
+ @config[CONFIG_KEY] && @config[CONFIG_KEY][TREE_KEY] && @config[CONFIG_KEY][TREE_KEY][key]
60
+ end
61
+
62
+ # attrs
63
+
64
+ def baseurl
65
+ return @config['baseurl']
66
+ end
67
+
68
+ def path_assets
69
+ return has_custom_write_path? ? option(PATH_ASSETS_KEY) : "/assets"
70
+ end
71
+
72
+ def path_scripts
73
+ return has_custom_scripts_path? ? option(PATH_SCRIPTS_KEY) : File.join(path_assets, "js")
74
+ end
75
+
76
+ def testing
77
+ return @testing
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Context
4
+ attr_reader :site
5
+
6
+ def initialize(site)
7
+ @site = site
8
+ end
9
+
10
+ def registers
11
+ { :site => site }
12
+ end
13
+ end
@@ -0,0 +1,330 @@
1
+ // don't need frontmatter because liquid is handled internally...somehow...
2
+ export default class JekyllGraph {
3
+
4
+ constructor() {
5
+ this.graphDiv = document.getElementById('jekyll-graph');
6
+ }
7
+
8
+ // d3
9
+ drawNetWeb () {
10
+ let assetsPath = '{{ site.graph.assets_path }}' !== '' ? '{{ site.graph.assets_path }}' : '/assets';
11
+ fetch(`{{ site.baseurl }}${assetsPath}/graph-net-web.json`).then(res => res.json()).then(data => {
12
+
13
+ // neighbors: replace ids with full object
14
+ data.nodes.forEach(node => {
15
+ let neighborNodes = [];
16
+ node.neighbors.nodes.forEach(nNodeId => {
17
+ neighborNodes.push(data.nodes.find(node => node.id === nNodeId));
18
+ });
19
+ let neighborLinks = [];
20
+ node.neighbors.links.forEach(nLink => {
21
+ neighborLinks.push(data.links.find(link => link.source === nLink.source && link.target === nLink.target));
22
+ });
23
+ node.neighbors.nodes = neighborNodes;
24
+ node.neighbors.links = neighborLinks;
25
+ });
26
+
27
+ const highlightNodes = new Set();
28
+ const highlightLinks = new Set();
29
+ let hoverNode = null;
30
+ let hoverLink = null;
31
+
32
+ const Graph = ForceGraph()
33
+
34
+ (this.graphDiv)
35
+ // container
36
+ .height(this.graphDiv.parentElement.clientHeight)
37
+ .width(this.graphDiv.parentElement.clientWidth)
38
+ // node
39
+ .nodeCanvasObject((node, ctx) => this.nodePaint(node, ctx, hoverNode, hoverLink, "net-web"))
40
+ // .nodePointerAreaPaint((node, color, ctx, scale) => nodePaint(node, nodeTypeInNetWeb(node), ctx))
41
+ .nodeId('id')
42
+ .nodeLabel('label')
43
+ .onNodeClick((node, event) => this.goToPage(node, event))
44
+ // link
45
+ .linkSource('source')
46
+ .linkTarget('target')
47
+ .linkColor(() => getComputedStyle(document.documentElement).getPropertyValue('--graph-link-color'))
48
+ // forces
49
+ // .d3Force('link', d3.forceLink()
50
+ // .id(function(d) {return d.id;})
51
+ // .distance(30)
52
+ // .iterations(1))
53
+ // .links(data.links))
54
+
55
+ .d3Force('charge', d3.forceManyBody()
56
+ .strength(Number('{{ site.graph.net_web.force.charge }}')))
57
+ // .d3Force('collide', d3.forceCollide())
58
+ // .d3Force('center', d3.forceCenter())
59
+ .d3Force('forceX', d3.forceX()
60
+ .strength(Number('{{ site.graph.net_web.force.strength_x }}'))
61
+ .x(Number('{{ site.graph.net_web.force.x_val }}')))
62
+ .d3Force('forceY', d3.forceY()
63
+ .strength(Number('{{ site.graph.net_web.force.strength_y }}'))
64
+ .y(Number('{{ site.graph.net_web.force.y_val }}')))
65
+
66
+ // hover
67
+ .autoPauseRedraw(false) // keep redrawing after engine has stopped
68
+ .onNodeHover(node => {
69
+ highlightNodes.clear();
70
+ highlightLinks.clear();
71
+ if (node) {
72
+ highlightNodes.add(node);
73
+ node.neighbors.nodes.forEach(node => highlightNodes.add(node));
74
+ node.neighbors.links.forEach(link => highlightLinks.add(link));
75
+ }
76
+ hoverNode = node || null;
77
+ })
78
+ .onLinkHover(link => {
79
+ highlightNodes.clear();
80
+ highlightLinks.clear();
81
+ if (link) {
82
+ highlightLinks.add(link);
83
+ highlightNodes.add(link.source);
84
+ highlightNodes.add(link.target);
85
+ }
86
+ hoverLink = link || null;
87
+ })
88
+ .linkDirectionalParticles(4)
89
+ .linkDirectionalParticleWidth(link => highlightLinks.has(link) ? 2 : 0)
90
+ .linkDirectionalParticleColor(() => getComputedStyle(document.documentElement).getPropertyValue('--graph-particles-color'))
91
+ // zoom
92
+ // (fit to canvas when engine stops)
93
+ // .onEngineStop(() => Graph.zoomToFit(400))
94
+ // data
95
+ .graphData(data);
96
+
97
+ elementResizeDetectorMaker().listenTo(
98
+ this.graphDiv,
99
+ function(el) {
100
+ Graph.width(el.offsetWidth);
101
+ Graph.height(el.offsetHeight);
102
+ }
103
+ );
104
+ });
105
+ }
106
+
107
+ drawTree () {
108
+ let assetsPath = '{{ site.graph.assets_path }}' !== '' ? '{{ site.graph.assets_path }}' : '/assets';
109
+ fetch(`{{ site.baseurl }}${assetsPath}/graph-tree.json`).then(res => res.json()).then(data => {
110
+
111
+ // relatives: replace ids with full object
112
+ data.nodes.forEach(node => {
113
+ let relativeNodes = [];
114
+ node.relatives.nodes.forEach(nNodeId => {
115
+ relativeNodes.push(data.nodes.find(node => node.id === nNodeId));
116
+ });
117
+ let relativeLinks = [];
118
+ node.relatives.links.forEach(nLink => {
119
+ relativeLinks.push(data.links.find(link => link.source === nLink.source && link.target === nLink.target));
120
+ });
121
+ node.relatives.nodes = relativeNodes;
122
+ node.relatives.links = relativeLinks;
123
+ });
124
+
125
+ const highlightNodes = new Set();
126
+ const highlightLinks = new Set();
127
+ let hoverNode = null;
128
+ let hoverLink = null;
129
+
130
+ const Graph = ForceGraph()
131
+
132
+ (this.graphDiv)
133
+ // dag-mode (tree)
134
+ .dagMode('td')
135
+ .dagLevelDistance(Number('{{ site.graph.tree.dag_lvl_dist }}'))
136
+ // container
137
+ .height(this.graphDiv.parentElement.clientHeight)
138
+ .width(this.graphDiv.parentElement.clientWidth)
139
+ // node
140
+ .nodeCanvasObject((node, ctx) => this.nodePaint(node, ctx, hoverNode, hoverLink, "tree"))
141
+ // .nodePointerAreaPaint((node, color, ctx, scale) => nodePaint(node, nodeTypeInNetWeb(node), ctx))
142
+ .nodeId('id')
143
+ .nodeLabel('label')
144
+ .onNodeClick((node, event) => this.goToPage(node, event))
145
+ // link
146
+ .linkSource('source')
147
+ .linkTarget('target')
148
+ .linkColor(() => getComputedStyle(document.documentElement).getPropertyValue('--graph-link-color'))
149
+ // forces
150
+ // .d3Force('link', d3.forceLink()
151
+ // .id(function(d) {return d.id;})
152
+ // .distance(30)
153
+ // .iterations(1))
154
+ // .links(data.links))
155
+
156
+ .d3Force('charge', d3.forceManyBody()
157
+ .strength(Number('{{ site.graph.tree.force.charge }}')))
158
+ // .d3Force('collide', d3.forceCollide())
159
+ // .d3Force('center', d3.forceCenter())
160
+ .d3Force('forceX', d3.forceX()
161
+ .strength(Number('{{ site.graph.tree.force.strength_x }}'))
162
+ .x(Number('{{ site.graph.tree.force.x_val }}')))
163
+ .d3Force('forceY', d3.forceY()
164
+ .strength(Number('{{ site.graph.tree.force.strength_y }}'))
165
+ .y(Number('{{ site.graph.tree.force.y_val }}')))
166
+
167
+ // hover
168
+ .autoPauseRedraw(false) // keep redrawing after engine has stopped
169
+ .onNodeHover(node => {
170
+ highlightNodes.clear();
171
+ highlightLinks.clear();
172
+ if (node) {
173
+ highlightNodes.add(node);
174
+ node.relatives.nodes.forEach(node => highlightNodes.add(node));
175
+ node.relatives.links.forEach(link => highlightLinks.add(link));
176
+ }
177
+ hoverNode = node || null;
178
+ })
179
+ .onLinkHover(link => {
180
+ highlightNodes.clear();
181
+ highlightLinks.clear();
182
+ if (link) {
183
+ highlightLinks.add(link);
184
+ highlightNodes.add(link.source);
185
+ highlightNodes.add(link.target);
186
+ }
187
+ hoverLink = link || null;
188
+ })
189
+ .linkDirectionalParticles(4)
190
+ .linkDirectionalParticleWidth(link => highlightLinks.has(link) ? 2 : 0)
191
+ .linkDirectionalParticleColor(() => getComputedStyle(document.documentElement).getPropertyValue('--graph-particles-color'))
192
+ // zoom
193
+ // (fit to canvas when engine stops)
194
+ // .onEngineStop(() => Graph.zoomToFit(400))
195
+ // data
196
+ .graphData(data);
197
+
198
+ elementResizeDetectorMaker().listenTo(
199
+ this.graphDiv,
200
+ function(el) {
201
+ Graph.width(el.offsetWidth);
202
+ Graph.height(el.offsetHeight);
203
+ }
204
+ );
205
+ });
206
+ }
207
+
208
+ // draw helpers
209
+
210
+ nodePaint(node, ctx, hoverNode, hoverLink, gType) {
211
+ let fillText = true;
212
+ let radius = 6;
213
+ //
214
+ // nodes color
215
+ //
216
+ if (this.isVisitedPage(node)) {
217
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-visited-color');
218
+ } else if (this.isMissingPage(node)) {
219
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-missing-color')
220
+ } else if (!this.isVisitedPage(node) && !this.isMissingPage(node)) {
221
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-unvisited-color');
222
+ } else {
223
+ console.log("WARN: Not a valid base node type.");
224
+ }
225
+ ctx.beginPath();
226
+ //
227
+ // hover behavior
228
+ //
229
+ if (node === hoverNode) {
230
+ // hoverNode
231
+ radius *= 2;
232
+ fillText = false; // node label should be active
233
+ } else if (hoverNode !== null && gType === "net-web" && hoverNode.neighbors.nodes.includes(node)) {
234
+ // neighbor to hoverNode
235
+ } else if (hoverNode !== null && gType === "net-web" && !hoverNode.neighbors.nodes.includes(node)) {
236
+ // non-neighbor to hoverNode
237
+ fillText = false;
238
+ } else if (hoverNode !== null && gType === "tree" && hoverNode.relatives.nodes.includes(node)) {
239
+ // neighbor to hoverNode
240
+ } else if (hoverNode !== null && gType === "tree" && !hoverNode.relatives.nodes.includes(node)) {
241
+ // non-neighbor to hoverNode
242
+ fillText = false;
243
+ } else if ((hoverNode === null && hoverLink !== null) && (hoverLink.source === node || hoverLink.target === node)) {
244
+ // neighbor to hoverLink
245
+ fillText = true;
246
+ } else if ((hoverNode === null && hoverLink !== null) && (hoverLink.source !== node && hoverLink.target !== node)) {
247
+ // non-neighbor to hoverLink
248
+ fillText = false;
249
+ } else {
250
+ // no hover (default)
251
+ }
252
+ ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
253
+ //
254
+ // glow behavior
255
+ //
256
+ if (this.isCurrentPage(node)) {
257
+ // turn glow on
258
+ ctx.shadowBlur = 30;
259
+ ctx.shadowColor = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-current-glow');
260
+ } else if (this.isTag(node)) {
261
+ // turn glow on
262
+ ctx.shadowBlur = 30;
263
+ ctx.shadowColor = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-tagged-glow');
264
+ } else if (this.isVisitedPage(node)) {
265
+ // turn glow on
266
+ ctx.shadowBlur = 20;
267
+ ctx.shadowColor = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-visited-glow');
268
+ } else {
269
+ // no glow
270
+ }
271
+ ctx.fill();
272
+ // turn glow off
273
+ ctx.shadowBlur = 0;
274
+ ctx.shadowColor = "";
275
+ //
276
+ // draw node borders
277
+ //
278
+ ctx.lineWidth = radius * (2 / 5);
279
+ ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--graph-node-stroke-color');
280
+ ctx.stroke();
281
+ //
282
+ // node labels
283
+ //
284
+ if (fillText) {
285
+ // add peripheral node text
286
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--graph-text-color');
287
+ ctx.fillText(node.label, node.x + radius + 1, node.y + radius + 1);
288
+ }
289
+ }
290
+
291
+ isCurrentPage(node) {
292
+ return !this.isMissingPage(node) && window.location.pathname.includes(node.url);
293
+ }
294
+
295
+ isTag(node) {
296
+ // if (!isPostPage) return false;
297
+ const semTags = Array.from(document.getElementsByClassName("sem-tag"));
298
+ const tagged = semTags.filter((semTag) =>
299
+ !this.isMissingPage(node) && semTag.hasAttribute("href") && semTag.href.includes(node.url)
300
+ );
301
+ return tagged.length !== 0;
302
+ }
303
+
304
+ isVisitedPage(node) {
305
+ if (!this.isMissingPage(node)) {
306
+ var visited = JSON.parse(localStorage.getItem('visited'));
307
+ for (let i = 0; i < visited.length; i++) {
308
+ if (visited[i]['url'] === node.url) return true;
309
+ }
310
+ }
311
+ return false;
312
+ }
313
+
314
+ isMissingPage(node) {
315
+ return node.url === '';
316
+ }
317
+
318
+ // user-actions
319
+
320
+ // from: https://stackoverflow.com/questions/63693132/unable-to-get-node-datum-on-mouseover-in-d3-v6
321
+ // d3v6 now passes events in vanilla javascript fashion
322
+ goToPage(node, e) {
323
+ if (!this.isMissingPage(node)) {
324
+ window.location.href = node.url;
325
+ return true;
326
+ } else {
327
+ return false;
328
+ }
329
+ }
330
+ }