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.
- 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
|
+
}
|