Published
Edited
Jan 29, 2021
Fork of Fruits
2 forks
Insert cell
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("viewBox", [-width, -height, 2 * width, 2 * height]);

////////////////////////////////
// ZOOM //
const zoom_g = svg.append("g");
function zoomed({ transform }) {
zoom_g.attr("transform", transform);
}
svg.call(
d3
.zoom()
.scaleExtent([0.2, 6])
.on("zoom", zoomed)
);
////////////////////////////////

let link = zoom_g.append("g").selectAll("line");
let node = zoom_g.append("g").selectAll("circle");
let plus = zoom_g.append("g").selectAll("g");
let title = zoom_g.append("g").selectAll("text");

let simulation = d3.forceSimulation().on('tick', updateLinksAndNodes);
let collisionForce = d3.forceCollide();
let linkForce = d3.forceLink().id(d => d.id);

function restartSimulation() {
if (simulation.alpha() <= 0.3) {
simulation.alpha(0.3);
simulation.restart();
}
}

// Kollisionsradius
function getCollisionRadius(node) {
if (node.item.parentId == null) return node.radius + 30;
return node.radius + 10;
}

////////////////////////////////
// TICK-FUNKTION (wird bei jedem Tick des Force-Renderings aufgerufen)
function updateLinksAndNodes() {
if (linkForce) {
linkForce.distance(getDistance);
linkForce.strength(config.linkStrength);
}

if (collisionForce) {
collisionForce.radius(getCollisionRadius);
}

simulation.force(
"charge",
d3.forceManyBody().strength(node => node.radius * config.manyBodyStrength)
);

node
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.call(drag(simulation, updateNodeList));

title
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => d.item.title)
.call(wrap);

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);

plus.attr("transform", d => {
const x = (d.target.x - d.source.x) / 2 + d.source.x;
const y = (d.target.y - d.source.y) / 2 + d.source.y;
return `translate(${x}, ${y})`;
});
}

////////////////////////////////
// Funktion, um Nodes und force zu initialisieren
function updateNodeList() {
validateData();

const nodes = getNodes(data);
const links = filterVisibleLinks(getLinks(data), nodes);

simulation
.nodes(nodes)
.force("charge", d3.forceManyBody().strength(config.manyBodyStrength))
.force('link', linkForce.links(links))
.force('attractCenteredNodes', attractCenter().strength(0.8)) // Einbinden der Custom force
.force('x', d3.forceX().strength((config.centerStrength * 100) / width))
.force('y', d3.forceY().strength((config.centerStrength * 100) / height))
.force('collision', d3.forceCollide())
.velocityDecay(.25);

node = node
.data(nodes, d => d.id)
.join(
enter =>
enter.append("circle").call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.duration(config.transitionDuration)
.attr("r", d => d.radius)
),
update => update,
exit =>
exit
.transition()
.ease(d3.easeCubicOut)
.duration(config.transitionDuration)
.attr("opacity", 0.5)
.attr("r", Math.E / 4)
.attr("cx", d =>
d.item.parent.visible
? d.item.parent.node.x
: getRootNode(d).node.x
)
.attr("cy", d =>
d.item.parent.visible
? d.item.parent.node.y
: getRootNode(d).node.y
)
.remove()
)
.attr('cx', d => d.x)
.attr("cy", d => d.y)
.attr('fill', d => d.fill)
.attr("stroke-width", d => {
if (d.item.type === 'user') return "5px";
return d.item.isOpen ? 0.2 * d.radius + "px" : "0px";
})
.attr("stroke", d => {
if (d.item.type === 'user') return '#ce1717';
return d.item.isOpen ? d.item.color + '60' : d.item.color;
})
.attr('cursor', d => (d.item.children.length > 0 ? 'pointer' : ''))
.on("click", (e, d) => {
if (d.item.children.length > 0) {
clickItem(d.id);
updateNodeList();
restartSimulation();
}
})
.on("contextmenu", e => e.preventDefault())
.on("mousedown", (e, d) => {
// node "wünscht" ins Zentrum gerückt zu werden.
// Deshalb wird ein Property mit dem aktuellen Zeitstempel geschrieben.

// alle anderen Nodes zurücksetzen
nodes.forEach(n => {
if (n.centeredRequested) {
n.centeredRequested = null;
}
});

d.centeredRequested = new Date();
})
.on("mouseup", (e, d) => {
if (!d.centeredRequested) {
return;
}

const startTime = d.centeredRequested;
const endTime = new Date();
const shouldCenter = endTime - startTime > 500;

if (shouldCenter) {
// alle anderen Nodes zurücksetzen
nodes.forEach(n => {
if (n.centered) {
n.centered = false;
}
});

d.centered = true;
updateNodeList();
restartSimulation();
}

d.centeredRequested = null;
});

title = title
.data(nodes, d => d.id)
.join(
enter =>
enter
.append("text")
.style('opacity', 0)
.text(d => d.item.title)
.call(g =>
g
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(config.transitionDuration)
.style('opacity', 1)
),
update => update,
exit => exit.remove()
)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("font-size", d => d.radius / 4 + "px")
.attr(
"font-family",
"-apple-system, system-ui, avenir, helvetica, roboto, noto, arial"
);

link = link
.data(links, d => d.target.id)
.join(enter =>
enter
.append("line")
.attr("stroke-opacity", "0")
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)
.attr("stroke", d =>
d.type === 'parent' ? d.source.fill : "#696969"
)
.call(enter =>
enter
.transition()
.ease(d3.easeCubicOut)
.delay(0)
.duration(config.transitionDuration)
.attr("stroke-opacity", 1)
)
);

plus = plus
.data(links.filter(l => l.type === 'user' || l.type === 'context'))
.join(enter =>
enter.append("g").attr("transform", d => {
const x = (d.target.x - d.source.x) / 2 + d.source.x;
const y = (d.target.y - d.source.y) / 2 + d.source.y;
return `translate(${x}, ${y})`;
})
);

plus
.append("circle")
.attr("stroke", "black")
.attr("fill", "white")
.attr("r", "15")
.attr("cx", "0")
.attr("cy", "0");

plus
.append("line")
.attr("x1", "-10")
.attr("y1", "0")
.attr("x2", "10")
.attr("y2", "0")
.attr("stroke", "black");

plus
.append("line")
.attr("x1", "0")
.attr("y1", "-10")
.attr("x2", "0")
.attr("y2", "10")
.attr("stroke", "black");
}

updateNodeList();

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more