Wie zeichne ich einen Graph in Javascript D3.js?
Ich möchte ein Graphendiagramm erstellen. Dazu habe ich eine JSON-Datei erstellt. Die Skills ("java, python, HTML, json") sind die Knoten und die Indexe (KayO, BenBeck, Borea usw.) sind die Kanten. Der Knoten muss eine Mindestgröße haben und darf nicht zu groß werden. Anschließend möchte ich mit einem Klick auf den Knoten die Liste der Publikationen auf der rechten Seite aufrufen können. Dabei soll der aktuell ausgewählte Knoten in der Visualisierung hervorgehoben werden. Ich habe bereits aus diesem Beispiel (https://bl.ocks.org/heybignick/3faf257bbbbc7743bb72310d03b86ee8) was implementiert. Aber leider komme ich nicht weiter.
Diese ist meine JSON Datei:
const persona = {
"KayO": {
firstname: "Kay",
lastname: "Ohran",
City: "California",
skills: "java, python, HTML, json",
},
BenBeck: {
firstname: "Ben",
lastname: "Beckamm",
City: "New York",
skills: "css, ruby, php, training, simulator, java, web, webgl, json",
};
Das ist mein Code
const bib = persona;
console.table(bib);
const graph = {nodes: [{id: "a"}, {id: "b"}], links: [{source: "a", target: "b"}]};
const linkColor = d3.scaleLinear().domain([0, 1]).range(["#eee", "#000"]);
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
const simulation = d3
.forceSimulation()
.force("link", d3.forceLink().id(function (d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
//linien -> Kanten
var link = svg.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links).enter().append("line")
.attr("stroke", "#aaa");
//kreise -> Knoten
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle").data(graph.nodes).enter().append("circle")
.attr("r", 5).call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));
node.append("title").text(d => d.id);
simulation.nodes(graph.nodes).on("tick", ticked)
simulation.force("link").links(graph.links);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("cx", d => d.x).attr("cy", d => d.y);
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
1 Antwort
Diese ist meine JSON Datei:
Das ist kein JSON, sondern JavaScript-Code, bei dem eine Variable angelegt wird, die auf ein JavaScript-Objekt zeigt. Beachte zudem die Aktualisierung meiner Antwort zu deiner letzten Frage.
Die Skills ("java, python, HTML, json") sind die Knoten und die Indexe (KayO, BenBeck, Borea usw.) sind die Kanten.
Demzufolge müsstest du erst einmal alle Skills ermitteln und zu diesen dann eine Node-Liste erstellen.
let allSkills = [];
for (const person in persona) {
const skills = persona[person].skills.split(", ");
allSkills = allSkills.concat(skills.filter(item => allSkills.indexOf(item) < 0));
}
const nodes = allSkills.map(skill => ({ name: skill }));
Die Kanten so zu ermitteln, wird etwas schwieriger. Bereits dein Ausgangsformat ist ungünstig. Besser wäre es, wenn die Skills nicht als Strings gespeichert würden, sondern ebenso schon als Listen.
skills: [ "css", "ruby", "php", "training", /* etc. ... */ ]
Beim Sammeln aller Skills könntest du im gleichen Lauf auch die Kanten schon kreieren. So wie ich es verstehe, müsste jeder Skill durch die Person selbst miteinander verbunden werden.
let allSkills = [];
const allEdges = [];
for (const person in persona) {
const skills = persona[person].skills.split(", ");
// ...
for (let i = 0; i < skills.length - 1; ++i) {
for (let j = i + 1; j < skills.length; ++j) {
allEdges.push(({ source: skills[i], target: skills[j], label: person }));
}
}
}
Für jedes Kantenobjekt würde ich zugleich ein label-Property hinzufügen. Ein Beispiel für einen Graph mit beschrifteten Kanten findest du hier. Beim Hinzufügen des TextPath muss nur der Rückgabewert für den text-Callback geändert werden.
edgelabels.append('textPath')
.attr(/* ... */)
.style(/* ... */)
.text((d, i) => d.label);
Anschließend möchte ich mit einem Klick auf den Knoten die Liste der Publikationen auf der rechten Seite aufrufen können.
An die Nodes kannst du einen Klick-Handler anhängen.
node.on("click", currentNode => {
// ...
});
Das currentNode-Objekt sollte auf das angeklickte Node-Objekt aus deinem Graph zeigen. Das heißt, du hättest Zugriff auf das name-Property. Mit Hilfe dessen kannst du dein persona-Objekt filtern.
Ich habe dies mal versucht doch leider bekomme ich zwei Fehlermeldungen:
Uncaught Error: node not found: java
und
Uncaught TypeError: Cannot create property 'vx' on string 'python'
Ich habe mir mal überbelgt ob ich ein Objekt erstellen soll, wie z.B:
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
und Data wäre dann:
data = ({nodes: Array.from(new Set(links.flatMap(l => [l.source, l.target])), id => ({id})), links})
DOch leider weiß ich nicht wie ich auf die Werte zugreifen kann welche ich hier ausgebe:
let allSkills = [];
const allEdges = [];
for(const person in persona){
const skills = persona[person].skills.split(",").map(s => s.trim());
allSkills = allSkills.concat(skills.filter(item => allSkills.indexOf(item) < 0));
for (let i = 0; i < skills.length - 1; ++i) {
for (let j = i + 1; j < skills.length; ++j) {
allEdges.push(({ source: skills[i], target: skills[j], id: person }));
}
}
}
const nodes = allSkills.map(skill => ({ name: skill }));
Ich habe hier:
allEdges.push(({ source: skills[i], target: skills[j], label: person }));
einen Fehler gemacht. Die Indizes sollten für source und target verwendet werden.
allEdges.push(({ source: i, target: j, label: person }));
Ich glaub eher, dass der Fehler am Javascript Version liegt. Ich nutze derzeit die Version 6 "https://d3js.org/d3.v6.js". Mit Ihrer Version klappt es voll und ganz. Doch bei mir kommt immer die Fehlermeldung "Uncaught TypeError: Cannot read property 'force' of undefined".
Es liegt nicht an JavaScript, sondern an der Version der d3-Bibliothek. Hier: https://www.d3-graph-gallery.com/network findest du bei den unteren Beispielen auch eines, welches Version 6 verwendet.
Ich habe deine Methode ausgeführt und es klappt. Nun frage ich mich wie ich die erstellte Methode als Knoten und Kanteausgebe. Wie ich kriege ich das was hier ausgegeben wird :
In diesen Graph-Placeholder?
Denn wie du oben siehst gibt ja die const-Deklaration von graph die daten aus: