268 lines
8.4 KiB
JavaScript
268 lines
8.4 KiB
JavaScript
import { event, select, selectAll} from 'd3-selection';
|
|
import { values, set, map } from 'd3-collection';
|
|
import { drag } from 'd3-drag';
|
|
import { zoom } from 'd3-zoom';
|
|
import { forceSimulation, forceLink, forceManyBody, forceCenter, forceX, forceY, forceRadial } from 'd3-force';
|
|
// import { Wiki, Page } from './wiki.js';
|
|
import EventEmitter from 'eventemitter3';
|
|
// import { json } from 'd3-fetch';
|
|
|
|
export class ForceNet {
|
|
|
|
constructor (symbols) {
|
|
var width = 600,
|
|
height = 600;
|
|
|
|
this.symbols = symbols || {};
|
|
this.events = new EventEmitter();
|
|
this.active_page = null;
|
|
// this.nodes = {};
|
|
this.simulation = forceSimulation()
|
|
//.velocityDecay(0.1)
|
|
.force("link", forceLink().id(d => d.title).distance(800))
|
|
.force("charge", forceManyBody().distanceMin(800))
|
|
.force("radial", forceRadial(180, width/2, height/2));
|
|
// .force("center", forceCenter(width / 2, height / 2));
|
|
this.svg = null;
|
|
// this.historylinks = {};
|
|
}
|
|
|
|
on (message, callback, context) {
|
|
this.events.on(message, callback, context);
|
|
}
|
|
|
|
init_svg (svg) {
|
|
this.svg = select(svg || "svg");
|
|
this.zoom = zoom()
|
|
.scaleExtent([1 / 16, 16])
|
|
.on("zoom", () => {
|
|
this.content.attr("transform", event.transform);
|
|
// console.log("transform", event.transform, this.content.attr("transform"));
|
|
});
|
|
this.rect = this.svg.append("rect")
|
|
.attr("width", 1000)
|
|
.attr("height", 1000)
|
|
.style("fill", "none")
|
|
.style("pointer-events", "all")
|
|
.call(this.zoom);
|
|
this.content = this.svg.append("g")
|
|
.attr("id", "content"),
|
|
this.linksg = this.content.append("g")
|
|
.attr("class", "links");
|
|
this.nodesg = this.content.append("g")
|
|
.attr("class", "nodes");
|
|
}
|
|
|
|
dragstarted (d) {
|
|
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
}
|
|
|
|
dragged (d) {
|
|
d.fx = event.x;
|
|
d.fy = event.y;
|
|
}
|
|
|
|
dragended(d) {
|
|
if (!event.active) this.simulation.alphaTarget(0);
|
|
d.fx = null;
|
|
d.fy = null;
|
|
}
|
|
|
|
link_key (a, b) {
|
|
return (a < b) ? ("link_"+a+"_"+b) : ("link_"+b+"_"+a);
|
|
}
|
|
|
|
/*
|
|
link_key (p1, p2) {
|
|
return (p1.title < p2.title) ?
|
|
("link_"+p1.title+"_"+p2.title) :
|
|
("link_"+p2.title+"_"+p1.title);
|
|
}
|
|
*/
|
|
make_link (p1, p2) {
|
|
return (p1.title < p2.title) ?
|
|
{source: p1, target: p2 } :
|
|
{source: p2, target: p1 };
|
|
}
|
|
|
|
register_link (from_page, to_page) {
|
|
var lkey = this.link_key(from_page, to_page);
|
|
if (this.links[lkey] === undefined) {
|
|
this.links[lkey] = this.make_link(from_page, to_page);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get_symbol (d, def) {
|
|
return "symbols.svg#Main";
|
|
/*
|
|
for (var i=0, l=d.cats.length; i<l; i++) {
|
|
if (this.symbols[d.cats[i]]) {
|
|
return this.symbols[d.cats[i]];
|
|
}
|
|
}
|
|
return this.symbols.default || def;
|
|
*/
|
|
}
|
|
|
|
update_graph (graph) {
|
|
console.log("UPDATE GRAPH", graph.nodes.length, graph.links.length);
|
|
var link = this.linksg.selectAll("line")
|
|
.data(graph.links, d => { return this.link_key(d.source.title, d.target.title) });
|
|
var link_enter = link.enter()
|
|
.append("line");
|
|
|
|
link.exit().each(d => {
|
|
d.source.linked = false;
|
|
d.target.linked = false;
|
|
}).remove();
|
|
|
|
link_enter.merge(link).each(d => {
|
|
d.source.linked = true;
|
|
d.target.linked = true;
|
|
});
|
|
|
|
var node = this.nodesg
|
|
.selectAll("g.page")
|
|
.data(graph.nodes, function (d) { return d.title });
|
|
|
|
node.exit().remove();
|
|
|
|
var that = this;
|
|
var node_enter = node.enter().append("g")
|
|
.attr("class", "page")
|
|
// .attr("class", d=>"page "+this.wiki.get_ns_classname(d.ns))
|
|
.on("click", function(d) {
|
|
that.events.emit("nodeclick", d, this);
|
|
// this.set_active_node(d.title);
|
|
})
|
|
.on("mouseover", function (d) {
|
|
// console.log("mouseover", this);
|
|
select(this).classed("mouse", true);
|
|
})
|
|
.on("mouseout", function (d) {
|
|
// console.log("mouseout", this);
|
|
select(this).classed("mouse", false);
|
|
})
|
|
.call(drag()
|
|
.on("start", this.dragstarted.bind(this))
|
|
.on("drag", this.dragged.bind(this))
|
|
.on("end", this.dragended.bind(this)));
|
|
|
|
node_enter.append("use")
|
|
.attr("xlink:href", d => this.get_symbol(d, "default"))
|
|
.attr("class", "testcolor");
|
|
|
|
// {
|
|
// for (var i=0, l=d.cats.length; i<l; i++) {
|
|
// if (this.symbols[d.cats[i]]) {
|
|
// return this.symbols[d.cats[i]];
|
|
// }
|
|
// }
|
|
// return this.symbols.default || "default";
|
|
// });
|
|
|
|
// node_enter.append("circle")
|
|
// .attr("r", 6);
|
|
|
|
node_enter.append("text")
|
|
.text(d => d.title)
|
|
.attr("x", 10);
|
|
|
|
//node_enter.append("title")
|
|
// .text(function(d) { return d.title; });
|
|
|
|
node = node_enter.merge(node);
|
|
link = link_enter.merge(link);
|
|
|
|
node.classed("active", d=>d.active);
|
|
|
|
|
|
this.simulation
|
|
.nodes(graph.nodes)
|
|
.on("tick", ticked);
|
|
|
|
this.simulation.force("link")
|
|
.links(graph.links);
|
|
|
|
this.simulation.force("radial").radius(d => d.linked ? null : 200);
|
|
|
|
function ticked() {
|
|
link
|
|
.attr("x1", function(d) { return d.source.x; })
|
|
.attr("y1", function(d) { return d.source.y; })
|
|
.attr("x2", function(d) { return d.target.x; })
|
|
.attr("y2", function(d) { return d.target.y; });
|
|
|
|
// node
|
|
// .attr("cx", function(d) { return d.x; })
|
|
// .attr("cy", function(d) { return d.y; });
|
|
node
|
|
.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
}
|
|
|
|
// document.querySelector("#page").style.background = "purple";
|
|
// return;
|
|
|
|
this.update_nodes();
|
|
this.update_forces();
|
|
// this.simulation.alphaTarget(0.3).restart();
|
|
}
|
|
|
|
update_nodes () {
|
|
var nodes = this.nodesg.selectAll("g.page");
|
|
console.log("update_nodes", nodes.size());
|
|
nodes.classed("active", d=>d.active);
|
|
nodes.classed("active2", d=>d.active2);
|
|
nodes.classed("highlight", d=>d.highlight);
|
|
nodes.sort((a, b) => {
|
|
// console.log("sort", a, b);
|
|
var x = a.active ? 10 : (a.active2 ? 5 : 0),
|
|
y = b.active ? 10 : (b.active2 ? 5 : 0);
|
|
return x - y;
|
|
});
|
|
var links = this.linksg.selectAll("line");
|
|
links.classed("active2", d=>d.active2);
|
|
links.classed("history", d=>d.type == "history");
|
|
links.sort((a, b) => {
|
|
// console.log("sort", a, b);
|
|
var x = a.active2 ? 10 : (a.history ? 5 : 0),
|
|
y = b.active2 ? 10 : (b.history ? 5 : 0);
|
|
return x - y;
|
|
});
|
|
}
|
|
|
|
update_forces () {
|
|
var force = this.simulation.force("link");
|
|
console.log("update_forces:force", force);
|
|
this.simulation.force("link").strength(d => {
|
|
if (d.source.active || d.target.active) {
|
|
return 1;
|
|
} else {
|
|
return 1;
|
|
// same as d3.force's defaultStrength
|
|
// return 0.5 * (1 / Math.min(d.source.count, d.target.count));
|
|
}
|
|
});
|
|
this.simulation.alphaTarget(0.3).restart();
|
|
}
|
|
|
|
activate_linked_nodes (page, active) {
|
|
// deactivate linked links/nodes
|
|
for (let i=0, l=this.links.length; i<l; i++) {
|
|
let link = this.links[i];
|
|
if (link.source == page || link.target == page) {
|
|
link.active2 = active;
|
|
link.source.active2 = active;
|
|
link.target.active2 = active;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|