jekyll-graph 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +16 -0
- data/LICENSE +676 -0
- data/README.md +192 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jekyll-graph.gemspec +39 -0
- data/lib/jekyll-graph/config.rb +82 -0
- data/lib/jekyll-graph/context.rb +13 -0
- data/lib/jekyll-graph/jekyll-graph.js +330 -0
- data/lib/jekyll-graph/page.rb +16 -0
- data/lib/jekyll-graph/tags.rb +33 -0
- data/lib/jekyll-graph/version.rb +9 -0
- data/lib/jekyll-graph.rb +295 -0
- metadata +92 -0
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
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,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,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
|
+
}
|