how to make this D3js sunburst chart display all layers of data? - d3.js

My D3js sunburst chart is only displaying 2 layers of data even though there are more layers.
I want it to display all layers of data
A live version of the chart is here: D3 Chart
The code is below. please advise on how to make the chart display all of the layers of data.
I need to see more than just two layers.
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer()).define(["md"], function(md){return(
md`# D3 Zoomable Sunburst
This variant of a [sunburst diagram](/#mbostock/d3-sunburst), a radial orientation of D3’s [hierarchical partition layout](https://github.com/d3/d3-hierarchy/blob/master/README.md#partition), shows only two layers of the [Flare visualization toolkit](https://flare.prefuse.org) package hierarchy at a time. Click a node to zoom in, or click the center to zoom out.`
)});
main.variable(observer("chart")).define("chart", ["partition","data","d3","DOM","width","color","arc","format","radius"], function(partition,data,d3,DOM,width,color,arc,format,radius)
{
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.select(DOM.svg(width, width))
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`)
.on("mouseleave",mouseleave);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); })
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("d", d => arc(d.current))
.on("mouseover",mouseover);
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);
//percentage text
const percentage_text=svg.append("text")
.attr("id","title")
.attr("x", (width / 2))
.attr("y", (width / 2))
.attr("text-anchor", "middle")
.style("font-size", "2.5em");
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);
function clicked(p) {
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(750);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
//mouse over
const totalSize = root.descendants()[0].value;
function mouseover(d){
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%"; }
percentage_text.text(percentageString);
var sequenceArray = d.ancestors().reverse();
sequenceArray.shift(); // remove root node from the array
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
g.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
//mouse leave
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Deactivate all segments during transition.
//d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(200)
.style("opacity", 1)
.on("end", function() {
d3.select(this).on("mouseover", mouseover);
});
percentage_text.text("");
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
return svg.node();
}
);
main.variable(observer("data")).define("data", ["d3"], async function(d3){return(
await d3.json("data.json")
)});
main.variable(observer("partition")).define("partition", ["d3"], function(d3){return(
data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, root.height + 1])
(root);
}
)});
main.variable(observer("color")).define("color", ["d3","data"], function(d3,data){return(
d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
)});
main.variable(observer("format")).define("format", ["d3"], function(d3){return(
d3.format(",d")
)});
main.variable(observer("width")).define("width", function(){return(
974
)});
main.variable(observer("radius")).define("radius", ["width"], function(width){return(
width / 6
)});
main.variable(observer("arc")).define("arc", ["d3","radius"], function(d3,radius){return(
d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
)});
main.variable(observer("d3")).define("d3", ["require"], function(require){return(
require("d3#5")
)});
main.variable(observer()).define(["partition","data"], function(partition,data){return(
partition(data).descendants()[1]
)});
return main;
}

I've made a couple of changes, firstly making the example more minimal by removing hover and click events.
I needed to change the arc radius and the partition method as per this example, and took text positioning logic from this fiddle.
const width = 500,
radius = width / 2,
format = d3.format(",d");
const svg = d3.select('svg')
.attr("width", width)
.attr("height", width)
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius)
.innerRadius(d => Math.sqrt(d.y0))
.outerRadius(d => Math.sqrt(d.y1) - 1);
d3.json("https://gist.githubusercontent.com/MargretWG/b3f9e0a383408c6e6a45fc652e83a26c/raw/8756e2320d05a774e96983234beff81b01409315/hierarchy.json").then(data => {
const root = partition(data);
root.each(d => d.current = d);
const color = d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1));
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => {
while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", d => d.children ? 0.6 : 0.4)
.attr("d", d => arc(d.current));
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("transform", (d) => `translate(${arc.centroid(d)}) rotate(${getAngle(d)})`)
.text(d => d.data.name);
});
const partition = data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, radius * radius])
(root);
}
function getAngle(d) {
// Offset the angle by 90 deg since the '0' degree axis for arc is Y axis, while
// for text it is the X axis.
var thetaDeg = (180 / Math.PI * (arc.startAngle()(d) + arc.endAngle()(d)) / 2 - 90);
// If we are rotating the text by more than 90 deg, then "flip" it.
// This is why "text-anchor", "middle" is important, otherwise, this "flip" would
// a little harder.
return (thetaDeg > 90) ? thetaDeg - 180 : thetaDeg;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

Related

drag grouped elements move not continuous

I try move a grouped element but after click and drag, the elements jumped away.
demo()
function demo() {
var tooltip = d3.select('body')
.append('div')
.attr('id','tooltip')
.style('position','absolute')
.style('opacity',0)
.style('background','lightsteelblue')
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200)
.style("background", "#ececec")
add_grid(svg);
var data = [
{
text: "O",
x: 50,
y: 50
},
];
var g = svg.append('g')
var fontsize = 20;
var box = g.selectAll(".box")
.data(data)
.join('g')
.attr('class','box')
.attr("pointer-events", "all")
box.call(
d3.drag()
.on("start",function(event,d) {
d3.select(this).raise().classed("active", true);
d3.select('#tooltip')
.transition().duration(100)
.style('opacity', 1)
})
.on("drag",function(event,d) {
d.x = event.x
d.y = event.y
d3.select(this).attr('transform',`translate(${d.x},${d.y})`)
var desc = "(" + d.x.toFixed(1) +"," + d.y.toFixed(1) + ")"
d3.select('#tooltip')
.style('left', (event.x+2) + 'px')
.style('top', (event.y-2) + 'px')
.text(desc)
})
.on("end", function dragEnd(event,d) {
d3.select(this).classed("active", false);
d3.select('#tooltip').style('opacity', 0)}
))
.on('mouseover', function(event,d) {
})
.on('mouseout', function(event,d) {
})
.on('mousemove', function(event,d) {
})
.on("mousedown", function(){
})
.on("mouseup", function(){
});
var txt = box.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline",'text-before-edge')//'central')//text-bottom
.attr("font-size", fontsize)
.attr("x", (d) => d.x)
.attr("y", (d) => d.y)
var tspan = txt.selectAll(".tspan")
.data((d) => d.text.split("\n"))
.join("tspan")
.attr("class", "tspan")
.attr("x", function (d) {
let x = +d3.select(this.parentNode).attr("x");
return x;
})
.attr("y", function (d,i) {
let y = +d3.select(this.parentNode).attr("y");
return y + i*fontsize * .9;
})
.text((d) => d);
box.each((d,i,n) => {
var bbox = d3.select(n[i]).node().getBBox()
var padding = 2
bbox.x -= padding
bbox.y -= padding
bbox.width += 2*padding
bbox.height += 2*padding
d.bbox = bbox
})
.attr('transform',d => `translate(${0},${-d.bbox.height/2})`)
.append('rect')
.attr('x',d => d.bbox.x)
.attr('y',d => d.bbox.y)
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr('stroke','red')
.attr('fill','none')
add_dots(svg,data)
function add_dots(svg,data) {
svg.selectAll('.dots')
.data(data)
.join('circle')
.attr('class','dots')
.attr('r',2)
.attr('cx',d => d.x)
.attr('cy',d => d.y)
.attr('fill','red')
}
function add_grid(svg) {
var w = +svg.attr("width");
var step = 10;
var mygrid = function (d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`;
};
var grid = [];
for (var i = 0; i < w; i += step) {
grid.push(i);
}
svg
.append("g")
.selectAll(null)
.data(grid)
.enter()
.append("path")
.attr("d", (d) => mygrid(d))
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 0.5);
}
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>

import {legend} from "#d3/color-legend" What is this in a html code? is there a <script src="***"> available?

I am following this code block:
https://observablehq.com/#d3/stacked-bar-chart
I want to make a same legend,
but I am not writing d3 in a js file,
I am using html with script,
I am wondering is there a link available to embed it in html? many thanks!
I don't think you can, ObservableHQ has it's own ecosystem for packages like this.
If you click on the name d3/color-legend, you go to this page. You can copy the code contents, but you can't seem to be able to download it, especially not in a format that can be imported with <script src="">. You can try instead to copy it into your code base with very few changes:
function legend({
color,
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height);
let x;
// Continuous
if (color.interpolate) {
const n = Math.min(color.domain().length, color.range().length);
x = color.copy().rangeRound(d3.quantize(d3.interpolate(marginLeft, width - marginRight), n));
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.copy().domain(d3.quantize(d3.interpolate(0, 1), n))).toDataURL());
}
// Sequential
else if (color.interpolator) {
x = Object.assign(color.copy()
.interpolator(d3.interpolateRound(marginLeft, width - marginRight)), {
range() {
return [marginLeft, width - marginRight];
}
});
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
// scaleSequentialQuantile doesn’t implement ticks or tickFormat.
if (!x.ticks) {
if (tickValues === undefined) {
const n = Math.round(ticks + 1);
tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1)));
}
if (typeof tickFormat !== "function") {
tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat);
}
}
}
// Threshold
else if (color.invertExtent) {
const thresholds = color.thresholds ? color.thresholds() // scaleQuantize
:
color.quantiles ? color.quantiles() // scaleQuantile
:
color.domain(); // scaleThreshold
const thresholdFormat = tickFormat === undefined ? d => d :
typeof tickFormat === "string" ? d3.format(tickFormat) :
tickFormat;
x = d3.scaleLinear()
.domain([-1, color.range().length - 1])
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.range())
.join("rect")
.attr("x", (d, i) => x(i - 1))
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", d => d);
tickValues = d3.range(thresholds.length);
tickFormat = i => thresholdFormat(thresholds[i], i);
}
// Ordinal
else {
x = d3.scaleBand()
.domain(color.domain())
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.domain())
.join("rect")
.attr("x", x)
.attr("y", marginTop)
.attr("width", Math.max(0, x.bandwidth() - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", color);
tickAdjust = () => {};
}
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x)
.ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined)
.tickFormat(typeof tickFormat === "function" ? tickFormat : undefined)
.tickSize(tickSize)
.tickValues(tickValues))
.call(tickAdjust)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", marginLeft)
.attr("y", marginTop + marginBottom - height - 6)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(title));
return svg.node();
}
function ramp(color, n = 256) {
var canvas = document.createElement('canvas');
canvas.width = n;
canvas.height = 1;
const context = canvas.getContext("2d");
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
legend({
color: d3.scaleSequential([0, 100], d3.interpolateViridis),
title: "Temperature (°F)"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>

How to run a D3 example

For example https://observablehq.com/#d3/zoomable-treemap
If you paste the fragments of script into a <script> tag in a HTML file the (obviously) it doesn't work.
How are you supposed to run these things?
And what the heck is italic f?
Why are no statements terminated with a semicolon and why are things declared without var?
Surely this cannot possibly work?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<h1>H1</h1?
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script type="text/javascript">
chart = {
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = d3.create("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "10px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", d => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`);
node.append("rect")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", d => d === root ? "#fff" : d.children ? "#ccc" : "#ddd")
.attr("stroke", "#fff");
node.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d === root ? name(d) : d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
return svg.node();
}
data = FileAttachment("flare-2.json").json()
treemap = data => d3.treemap()
.tile(tile)
(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
name = d => d.ancestors().reverse().map(d => d.data.name).join("/")
width = 954
height = 924
format = d3.format(",d")
d3 = require("d3#5")
</script>
</body>
</html>
If you're using Observable as a prototype tool you first have to know that they have a different flavor of Javascript. You can't just copy and paste the cells to a pure html/js file, but it's indeed very easy to use their ecosystem
Here are some links from their documentation
Observable’s not JavaScript
If you're prototyping and wants to quick embed your code
Downloading and Embedding Notebooks
Handy Embed Code Generator
Another resource on how to convert notebooks to standalone
Observable to Standalone
Here is another similar questions with answer
D3 example from Observable on my wordpress site
Hopefully that works for you
Here is how you can port your code from ObservableHQ to vanilla.js. You should be able to run this example with little to no modification.
<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="text/javascript">
'use strict';
var data = {};
const width = 954;
const height = 924;
const format = d3.format(",d");
d3.json("/api/graph/flare.json").then(function (dt) {
data = dt;
chart();
})
.catch(function (error) {
console.log(error);
});
function chart(){
treemap = dt => d3.treemap()
.tile(tile)
(d3.hierarchy(dt)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
const name = d => d.ancestors().reverse().map(d => d.data.name).join("/")
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = d3.create("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "10px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
}
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", d => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`);
node.append("rect")
.attr("id", d => (d.leafUid = d3.select("leaf")).id)
.attr("fill", d => d === root ? "#fff" : d.children ? "#ccc" : "#ddd")
.attr("stroke", "#fff");
node.append("clipPath")
.attr("id", d => (d.clipUid = d3.select("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d === root ? name(d) : d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
return svg.node();
}
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}

On drag, force is applied to node before mouse click is released

I've been implementing a force layout in d3js and I can zoom and drag the layout / nodes.
When I'm dragging a node, the behavior is fine, except that when I stopped moving, the node instantly starts moving back to it's original position, even though I haven't released the mouse click and so the event should still be considered in progress.
I tried to fix the behavior by setting d.fx and d.fy to current values, which doesn't solve anything.
Here's the code:
const WIDTH = 1600;
const HEIGHT = 900;
const V_MARGIN = 20;
const H_MARGIN = 50;
const ALPHA_DECAY = 0.03;
const VELOCITY_DECAY = 0.6;
const LINK_DISTANCE = Math.min(WIDTH, HEIGHT) / 10;
const CHARGE_FORCE = -Math.min(WIDTH, HEIGHT) / 3;
const ITERATIONS = 16;
const CIRCLE_WIDTH = 3;
const ON_HOVER_OPACITY = 0.1;
const c10 = d3.scaleOrdinal(d3.schemeCategory10);
const SVG = d3.select('body')
.append('svg')
.attr('width', WIDTH)
.attr('height', HEIGHT)
;
const g = SVG.append('g')
.attr('class', 'everything')
;
d3.json('got_social_graph.json')
.then(data => {
const nodes = data.nodes;
const links = data.links;
//Create force layout
const force = d3.forceSimulation()
.nodes(nodes)
.alphaDecay(ALPHA_DECAY)
.velocityDecay(VELOCITY_DECAY)
.force('links', d3.forceLink(links).distance(LINK_DISTANCE))
.force("collide",d3.forceCollide(d => d.influence > 15 ? d.influence : 15).iterations(ITERATIONS))
.force('charge_force', d3.forceManyBody().strength(CHARGE_FORCE))
.force('center_force', d3.forceCenter((WIDTH - H_MARGIN) / 2, (HEIGHT - V_MARGIN) / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0))
;
//Create links
const link = g.append('g')
.attr('class', 'link')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => d.weight / 10)
;
//Create nodes elements
const node = g.append('g')
.attr('class', 'node')
.selectAll('circle')
.data(nodes)
.enter()
.append('g')
;
//Append circles to nodes
const circle = node.append('circle')
.attr('r', d => d.influence > 10 ? d.influence : 10)
.attr('stroke-width', CIRCLE_WIDTH)
.attr('stroke', d => c10(d.zone * 10))
.attr('fill', 'black')
;
/*const label = node.append('text')
.attr('x', 12)
.attr('y', '0.25em')
.attr('font-size', d => d.influence * 1.5 > 9 ? d.influence * 1.5 : 9)
.text(d => d.character)
;*/
//Refresh force layout data
force.on('tick', () => {
node.attr('cx', d => d.x = Math.max(H_MARGIN, Math.min(WIDTH - H_MARGIN, d.x - H_MARGIN)))
.attr('cy', d => d.y = Math.max(V_MARGIN, Math.min(HEIGHT - V_MARGIN, d.y - V_MARGIN)))
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
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)
;
});
//Handle mouse events
circle
.on('click', (d, k, n) => {
d3.select(n[k])
})
.on('mouseover', (d, k, n) => {
})
.on('mouseout', (d, k, n) => {
circle
.attr('opacity', 1)
})
;
//Handle translation events
node.call(d3.drag()
.on('start', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
})
.on('drag', (d, n, k) => {
d3.select(n[k])
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y)
})
.on('end', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0);
}))
;
//Handle zoom events
g.call(d3.zoom()
.scaleExtent([0.8, 2.5])
.on('start', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d3.event.sourceEvent.stopPropagation();
})
.on('zoom', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
g.attr('transform', d3.event.transform)
})
.on('end', () => {
if (!d3.event.active) force.alphaTarget(0);
})
);
});
I want the node to stay over my mouse cursor until the click is released. The node doesn't need to be sticky.

How to separate different "gravitational fields" in D3?

I have a force enabled SVG visualisation where I want smaller circles to be attracted to bigger circles. This attraction works by calculating the elements' centre point and change it in iterations for every "tick" in the visualisation, to keep the items from going over the centre of the nodes I use a function to change the charge of the items depending on their size.
I used Mike's code here as a basis: http://mbostock.github.io/d3/talk/20110921/#14
My problem comes here - it seems like the bigger circles are affecting each others "gravitational fields" - is there a way I can separate them from eachother?
Force layout setup:
var w = 1280,
h = 800,
color = d3.scale.category10();
var force = d3.layout.force()
.gravity(0.0)
.charge(function(d){
return -10 * d.r;
})
.size([w, h]);
Element drawing:
var g = svg.selectAll("g.node")
.data(nodes)
.enter().append("svg:g")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
;
g.append("svg:circle")
.attr("r", 40)
.attr("transform", function(d) { return "translate(" + 0 + ","+ 0 + ")"; })
.style("fill", fill)
.call(force.drag);
g.append("svg:text")
.attr("x", 0)
.attr("dy", ".31em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.label;
});
Animation loop:
force.on("tick", function(e) {
var k = e.alpha * 0.5;
nodes.forEach(function(node) {
var center = nodes[node.type];
dx = center.x - node.x;
dy = center.y - node.y;
node.x += dx * k;
node.y += dy * k;
});
svg.selectAll(".circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
Adding smaller circles:
svg.on("mousemove", function() {
var p1 = d3.svg.mouse(this),
node = {
type: Math.random() * 3 | 0,
x: p1[0],
y: p1[1],
r: 1.5,
px: (p0 || (p0 = p1))[0],
py: p0[1]
};
p0 = p1;
svg.append("svg:circle")
.attr("class", "circle")
.data([node])
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 4.5)
.style("fill", fill);
nodes.push(node);
force.start();
});

Resources