jekyll-graph 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }