D3 V4 Sunburst diagram layout arc calculation - javascript

This is the code to compute the coordinates of the sunburst nodes:
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
Where:
x: the minimum x-coordinate of the node position
y: the minimum y-coordinate of the node position
dx: the x-extent of the node position
dy: the y-extent of the node position
However, in the recently realeased version v4, the space-filling layouts d3.treemap and d3.partition now output x0, x1, y0, y1 on each node instead of x0, dx, y0, dy
node.x0 - the left edge of the rectangle
node.y0 - the top edge of the rectangle
node.x1 - the right edge of the rectangle
node.y1 - the bottom edge of the rectangle
What would be the correspinding code for v4 as the following does not produce the correct layout?
var arc = d3.arc()
.startAngle(function(d) { return d.x0; })
.endAngle(function(d) { return d.x0 + (d.x1 - d.x0); })
.innerRadius(function(d) { return d.y0; })
.outerRadius(function(d) { return d.y0 + (d.y1 - d.y0); });
See codepen

See codepen
var data = {...}
var width = 960;
var height = 700;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20c)
var g = d3.select('#container')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width/2 + ',' + height/2 + ')');
var partition = d3.partition()
.size([360, radius])
.padding(0)
//.round(true); //this will produce weird leaf node size if true
var root = d3.hierarchy(data, function(d) { return d.children })
.sum( function(d) {
if(d.children) {
return 0
} else {
return 1
}
})
.sort(null);
partition(root)
var xScale = d3.scaleLinear()
.domain([0, radius])
.range([0, Math.PI * 2])
.clamp(true);
var arc = d3.arc()
.startAngle(function(d) { return xScale(d.x0) })
.endAngle(function(d) { return xScale(d.x1) })
.innerRadius(function(d) { return d.y0 })
.outerRadius(function(d) { return d.y1 })
var path = g.selectAll('path')
.data(root.descendants())
.enter().append('path')
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style('stroke', '#fff')
.style("fill", function(d) { return color((d.children ? d : d.parent).data.name); })

Related

Replicate pure JS version of D3 Zoomable Sunburst (observable)

I have been trying to replicate https://beta.observablehq.com/#mbostock/d3-zoomable-sunburst this in pure JS to use it in one of my projects. I am using PHP and Ajax to load Dynamic Data in JavaScript. I think the code in the Observable link is not in pure JS but rather Node or something else.
I am a newbie in Scripting, hence it is becoming very difficult for me to understand the written code. I do know that a pure JS will need the data (flare.json) in a specific format, which will generate the expected output. I can control the JSON structure from the backend, but I am unable to generate an output like the link.
I have followed multiple examples online:
https://bl.ocks.org/mbostock/4348373
And the same in d3 version 4 (which is very similar to v5, used in the Observable example):
https://bl.ocks.org/maybelinot/5552606564ef37b5de7e47ed2b7dc099
I have been trying to convert the Observable Zoomable Sunburst into JS functions, but I am unable to make it work. I have the exact same flare.json file and tried to recreate exact functions as therein Observable one. But it still is not working.
I am attaching my work. How can I get it working?
Sample Work
I have also tried to seek help on the Google Groups for d3-js, but I haven't got any help from there too.
The closest possible output which I have achieved till now is mentioned below:
var margin = {top: 288, right: 416, bottom: 288, left: 416},
radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 5;
var hue = d3.scale.category10();
var luminance = d3.scale.sqrt()
.domain([0, 1e6])
.clamp(true)
.range([90, 20]);
var svg = d3.select("body").append("svg")
.attr("width", margin.left + margin.right)
.attr("height", margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var partition = d3.layout.partition()
.sort(function(a, b) { return d3.ascending(a.name, b.name); })
.size([2 * Math.PI, radius]);
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx ; })
.padAngle(.01)
.padRadius(radius / 3)
.innerRadius(function(d) { return radius / 3 * d.depth; })
.outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });
// d3.json("https://api.myjson.com/bins/byw4q", function(error, root) {
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
if (error) throw error;
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.size; })
.nodes(root)
.forEach(function(d) {
d._children = d.children;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition
.children(function(d, depth) { return depth < 2 ? d._children : null; })
.value(function(d) { return d.sum; });
var center = svg.append("circle")
.attr("r", radius / 3)
.on("click", zoomOut);
center.append("title")
.text("zoom out");
var path = svg.selectAll("path")
.data(partition.nodes(root).slice(1))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.each(function(d) { this._current = updateArc(d); })
.on("click", zoomIn);
function zoomIn(p) {
if (p.depth > 1) p = p.parent;
if (!p.children) return;
zoom(p, p);
}
function zoomOut(p) {
if (!p.parent) return;
zoom(p.parent, p);
}
// Zoom to the specified new root.
function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
return p.key > d.key
? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
: {depth: 0, x: 0, dx: 2 * Math.PI};
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
path = path.data(partition.nodes(root).slice(1), function(d) { return d.key; });
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
path.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
path.enter().append("path")
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.on("click", zoomIn)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
}
});
function key(d) {
var k = [], p = d;
while (p.depth) k.push(p.name), p = p.parent;
return k.reverse().join(".");
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.name));
c.l = luminance(d.sum);
return c;
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");
<!DOCTYPE html>
<meta charset="utf-8">
<style>
circle,
path {
cursor: pointer;
}
circle {
fill: none;
pointer-events: all;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
</body>
The code written is pure javascript tough, whatever the data you are getting from Ajax the same endpoint you just need to pass here,
The example I am running here means the same should work in your project as well, Instead of calling the Ajax you can pass your Json in this line
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root)
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #fff;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 960,
height = 700,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);
var y = d3.scaleSqrt()
.range([0, radius]);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var partition = d3.partition();
var arc = d3.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x0)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x1)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y0));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y1));
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
if (error) throw error;
root = d3.hierarchy(root);
root.sum(function(d) {
return d.size;
});
svg.selectAll("path")
.data(partition(root).descendants())
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
.on("click", click)
.append("title")
.text(function(d) {
return d.data.name + "\n" + formatNumber(d.value);
});
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})`;
}
svg.selectAll("text")
.attr("dy", "0.35em")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.data(root.descendants().slice(1))
.enter().append("text")
.text(d => d.data.name);
});
function click(d) {
svg.transition()
.duration(750)
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]),
yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
return function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
};
})
.selectAll("path")
.attrTween("d", function(d) {
return function() {
return arc(d);
};
});
}
d3.select(self.frameElement).style("height", height + "px");
</script>

How can I alter the zooming capability on a d3 sunburst chart?

I am trying to alter the traditional zooming feature on a sunburst chart. Traditionally when you click on a partition, that partition grows to cover 100% of the base layer while all other partitions on the same layer disappear. The children of the selected partition all grow to fill the newly created space.
My current code does just what I stated above. I would like to alter my code to allow for the selected partition to only take up 75% of the base layer. The children elements will grow to cover this new space but the remaining 25% will still contain all other non-selected partitions.
I have tried altering the 't' value that is returned from d3.interpolate() but I have had unpredictable results.
I hope my description is clear.
Does anyone have any thoughts on this?
<script>
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var color = d3.scale.category20c();
function percent(d) {
var percentage = (d.value / 956129) * 100;
return percentage.toFixed(2);
}
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>" + d.name + "</strong> <span style='color:red'>" + percent(d) + "%</span>";
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
svg.call(tip);
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) });
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
// .attr("stroke", 'black')
// .style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.style("fill", function(d, i) {
return color(i);
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
if (percent(d) > 1.35) {
return d.name;
}
})
.attr('font-size', function(d) {
if (d.value < 100000) {
return '10px'
} else {
return '20px';
}
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
function click(d) {
console.log(d)
// fade out all text elements
text.transition().attr("opacity", 0);
path
.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
});
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
console.log(d.name, x.domain())
console.log(d.name, y.domain())
console.log(d.name, y.range())
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) {
console.log(t)
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
I found the solution here: https://bl.ocks.org/mbostock/1306365. This example manages the zoom without getting rid of the sibling nodes.

D3 Partition - Show next level when clicked

I have a D3 partition which shows all the levels for the entire partition.
I would like to only show the first level when the chart loads and then show subsequent levels on click.
For example in this Tree the next level is shown on click of a node: D3Tree
Here is the code for my partition: Plunker link
$(document).ready(function(){
var width = 600,
height = 400,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var partition = d3.layout.partition()
.value(function(d) {
if(d.depth == 2)
console.log(d.depth, d);
return 1; // d.size;
});
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
d3.json("flare.json", function(error, root) {
if (error) throw error;
svg.selectAll("path")
.data(partition.nodes(root))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", click)
.append("title")
.text(function(d) { return d.name + "\n" + formatNumber(d.value); });
});
function click(d) {
svg.transition()
.duration(750)
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
})
.selectAll("path")
.attrTween("d", function(d) { return function() { return arc(d); }; });
}
d3.select(self.frameElement).style("height", height + "px");
});
I would like to do something like toggle on click:
// Toggle children.
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
Where the children get set and unset, then redrawn
To do something like the tree layout would be a little tough doing it with the help of display is a a cake walk.
When the path are drawn for the first time make all the nodes whose depth > 1 disappear using display:none:
svg.selectAll("path")
.data(partition.nodes(root))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.style("display", function(d) {
if (d.depth > 1) {
return "none";//nodes whose depth is more than 1 make its vanish
} else {
return "";
}
})
Now on node click make all nodes reappear except when root node is clicked.
.style("display", function(d1) {
if (d.depth == 0 && d1.depth > 1) {
return "none"//root node clicked so show only 2 depths.
} else {
return "";
}
})
Working code here

D3.js Sunburst Incorrect Arc Scales

I have a Zoomable Sunburst diagram exhibiting strange problems with arc sizing.
http://colinwhite.net/Sunburst/
I would expect the size the arcs to be proportional to number of children (shown in the tool tip). Yet, I have parent arcs with few children, that are proportionally larger than their peers with far more children. Arc size is not reflective of number of children. I've tried various other d3.scales which haven't helped. What am I doing wrong?
My code is largely boiler plate from the D3 examples.
var width = 760, height = 700,
radius = Math.min(width, height) / 2.25,
color = d3.scale.category20c();
var x = d3.scale.linear().range([0, 2 * Math.PI]),
y = d3.scale.sqrt().range([0, radius]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height * .52 + ")");
var partition = d3.layout.partition()
.value(function(d) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min( 2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min( 2 * Math.PI, x(d.x + d.x))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("opacity", 0);
d3.json("data/getJson.php", function(error, data) {
var treeData = genJSON(data, ['Location', 'Provider', 'Diagnosis', 'Procedure']);
console.log(treeData);
var path = svg.selectAll("path")
.data(partition.nodes(treeData))
.enter().append("svg:path")
.attr("d", arc)
.style("fill-rule", "evenodd")
.style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", click)
.on("mouseover", function(d) {
tooltip.html(function() {
return (d.children ? d : d.parent).name + " (" + d.value + ")";
});
return tooltip.transition()
.duration(50)
.style("opacity", 0.9);
})
.on("mousemove", function(d) {
return tooltip
.style("top", (d3.event.pageY-10)+"px")
.style("left", (d3.event.pageX+10)+"px");
})
.on("mouseout", function(){return tooltip.style("opacity", 0);});
function click(d) {
path.transition()
.duration(750)
.attrTween("d", arcTween(d));
}
});
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d,i) {
return i
? function(t) { return arc(d); }
: function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
};
}
The JSON is nested with this genJSON function -http://colinwhite.net/Sunburst/js/treeRemapper.js
Thanks for any help or advice.
Changing the partition call, to include a .sort(null) like this -
var partition = d3.layout.partition()
.sort(null) //<-- was missing this
.value(function(d) { return 1; });
Seems to have resolved the strange arc scale problems.

Draw a D3 circle with gradient colours

How to draw a circle with gradient color? Say, a gradient from yellow to blue.
Normally, to create a circle in yellow we can use the following code:
var cdata=[50,40];
var xscale=40;
var xspace =50;
var yscale=70;
var svg = d3.select("body")
.append("svg")
.attr("width", 1600)
.attr("height", 1600);
var circle = svg.selectAll("circle")
.data(cdata)
.enter()
.append("circle");
var circleattr = circle
.attr("cx", function(d) {
xscale = xscale+xspace;
return xscale;
})
.attr("cy", function(d) {
yscale=yscale+xspace+10;
return yscale;
})
.attr("r", function(d) {
return d;
})
.style("fill","yellow");
You have to define the gradient in the SVG first, and then fill the circle with an SVG link to the gradient element.
// Define the gradient
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#a00000")
.attr("stop-opacity", 1);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#aaaa00")
.attr("stop-opacity", 1);
// Fill the circle with the gradient
var circle = svg.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', height / 3)
.attr('fill', 'url(#gradient)');
A jsFiddle with the complete example. More details on how to define SVG gradients in the MDN Tutorial. The resulting image:
Take a look at this code snippet:
var width = 500,
height = 500,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12;
var n = 200, // total number of nodes
m = 10; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {cluster: i, radius: r};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
// Use the pack layout to initialize node positions.
d3.layout.pack()
.sort(null)
.size([width, height])
.children(function(d) { return d.values; })
.value(function(d) { return d.radius * d.radius; })
.nodes({values: d3.nest()
.key(function(d) { return d.cluster; })
.entries(nodes)
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var grads = svg.append("defs").selectAll("radialGradient")
.data(nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.cluster); });
var node = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d, i) {
return "url(#grad" + i + ")";
})
// .style("fill", function(d) { return color(d.cluster); })
.call(force.drag);
node.transition()
.duration(750)
.delay(function(d, i) { return i * 5; })
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) { return d.radius = i(t); };
});
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius +
(d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
It contains support for drawing SVG circles with gradient (and achieving a 3D look-and-feel effect by doing this) and is based on SVG radial gradients.
For each node, a gradient is defined:
var grads = svg.append("defs").selectAll("radialGradient")
.data(nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.cluster); });
Then, instead of line:
.style("fill", function(d) { return color(d.cluster); })
this line is added in the code that creates circles:
.attr("fill", function(d, i) {
return "url(#grad" + i + ")";
})
This produces this effect:(animated gif that I used has some limitations for number of colors, so gradients are not smooth as in real example)

Categories

Resources