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)
Related
I make a copy of crypto bubbles where I want the coins to float around like bubbles now I run into the problem that a squares are completely filled and some are not this is because of the shadow? How do I solve this?
import React, { Component } from "react";
import "../styles/style.css";
import $ from "jquery";
import * as d3 from "d3";
const crypto = require("../data/cryptoData.json");
const coins = [];
var data = [];
for (let i = 0; i < crypto.length; i++) {
var coin = crypto[i];
var minplus = String(coin.market_data.price_change_percentage_24h);
if (minplus.includes("-")) {
coin["color"] = "red";
} else {
coin["color"] = "green";
}
coins.push(coin);
}
coins.forEach((coin) => {
var text = coin.symbol;
var r = coin.market_data.market_cap.usd / 3500000000;
if (r < 5) {
r = 20;
}
data.push({
text: text,
category: coin.market_data.price_change_percentage_24h + "%",
image: coin.image.large,
color: coin.color,
r: r,
r_change_1: coin.market_data.market_cap.USD / 3500000000,
r_change_2: coin.market_data.market_cap.USD / 3500000000,
});
});
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function (d) {
var r = d.r + 10,
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.r + quad.point.r;
if (l < r) {
l = ((l - r) / l) * (1 + 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;
});
};
}
const bubbleCloud = (element) => {
var container = d3.select(".bubble-cloud");
var $container = $(".bubble-cloud");
var containerWidth = $container.width();
var containerHeight = $container.height();
var svgContainer = container
.append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight)
.call(
d3.behavior.zoom().on("zoom", function () {
node.attr(
"transform",
"translate(" +
d3.event.translate +
")" +
" scale(" +
d3.event.scale +
")"
);
})
);
// prepare layout
var force = d3.layout
.force()
.size([containerWidth, containerHeight])
.gravity(0)
.charge(0)
.friction(1);
// load data
force.nodes(data).start();
// create item groups
var node = svgContainer
.selectAll(".node")
.data(data)
.enter()
.append("g")
.attr("class", "node")
.call(force.drag);
// create circles
var defs = node.append("defs");
function makeFiter(id, color) {
//make a filter if filter id not present
if (defs.selectAll("#" + id).empty()) {
var filter = defs.append("filter").attr("id", id).attr("height", "130%");
filter
.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", 20)
.attr("result", "blur");
filter
.append("feOffset", 0)
.attr("in", "blur")
.attr("result", "offsetBlur");
filter
.append("feFlood")
.attr("in", "offsetBlur")
.attr("flood-color", color)
.attr("flood-opacity", "1")
.attr("result", "offsetColor");
filter
.append("feComposite")
.attr("in", "offsetColor")
.attr("in2", "offsetBlur")
.attr("operator", "in")
.attr("result", "offsetBlur");
var feMerge = filter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "offsetBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
}
return "url(#" + id + ")"; //return the filter id
}
function getFilter(d) {
if (d.color === "red") {
return makeFiter("fill-text-red", "red");
} else if (d.color === "green") {
return makeFiter("fill-text-green", "green");
}
}
defs
.selectAll(null)
.data(data)
.enter()
.append("pattern")
.attr("id", function (d) {
return d.image;
})
.attr("height", "100%")
.attr("width", "100%")
.attr("patternContentUnits", "objectBoundingBox")
.append("image")
.attr("height", 1)
.attr("width", 1)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", function (d) {
return d.image;
});
node
.append("circle")
.attr("r", 1e-6)
.style("fill", function (d) {
return "url(#" + d.image + ")";
})
.attr("filter", function (d) {
return getFilter(d);
});
// create labels
node
.append("text")
.text(function (d) {
return d.text;
})
.classed("text", true)
.style({
fill: "#ffffff",
"text-anchor": "middle",
"font-size": "1vw",
"font-weight": "bold",
"text-transform": "uppercase",
"font-family": "Tahoma, Arial, sans-serif",
})
.attr("stroke", "black")
.attr("stroke-width", "1px");
node
.append("text")
.text(function (d) {
return d.category;
})
.classed("category", true)
.style({
fill: "#ffffff",
"text-anchor": "middle",
"font-size": "12px",
"font-weight": "bold",
"text-transform": "uppercase",
"font-family": "Tahoma, Arial, sans-serif",
})
.attr("stroke", "black")
.attr("stroke-width", "1px");
node
//.append("line")
//.classed("line", true)
//.attr({
// x1: 0,
// y1: 0,
// x2: 0,
// y2: 0,
//})
.attr("stroke-width", 1)
.attr("stroke", function (d) {
return d.stroke;
});
// put circle into movement
force.on("tick", function (e) {
d3.selectAll("circle")
.each(collide(0.1))
.attr("r", function (d) {
return d.r;
})
.attr("cx", function (d) {
// boundaries
if (d.x <= d.r) {
d.x = d.r + 1;
}
if (d.x >= containerWidth - d.r) {
d.x = containerWidth - d.r - 1;
}
return d.x;
})
.attr("cy", function (d) {
// boundaries
if (d.y <= d.r) {
d.y = d.r + 1;
}
if (d.y >= containerHeight - d.r) {
d.y = containerHeight - d.r - 1;
}
return d.y;
});
d3.selectAll("line").attr({
x1: function (d) {
return d.x - d.r + 10;
},
y1: function (d) {
return d.y;
},
x2: function (d) {
return d.x + d.r - 10;
},
y2: function (d) {
return d.y;
},
});
d3.selectAll(".text")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y - 10;
});
d3.selectAll(".category")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y + 20;
});
force.alpha(0.1);
});
};
class Bubbles extends Component {
render() {
return <div className="bubble-cloud" ref={bubbleCloud}></div>;
}
}
export default Bubbles;
I want it to look like the Ethereum image but have no idea what i have to change...
What properties need to be changed?
Use a smaller stdDeviation or enlarge your filter region. Something like this might work:
var filter = defs.append("filter").attr("id", id)
.attr("height", "300%")
.attr("width", "300%")
.attr("x", "-100%")
.attr("y", "-100%");
An alternative is to express the drop shadows as a % of the box size rather than in absolute units. Something like this:
var filter = defs.append("filter").attr("id", id)
.attr("primitiveUnits", "objectBoundingBox");
filter
.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "0.1")
.attr("result", "blur");
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.
I am creating an arc diagram where I'd like to, hopefully, find a way to prevent the overlap of arcs. There's an example of the working bl.ock here.
The darker lines in this case are overlapping lines where multiple nodes share the same edge. I'd like to prevent that, perhaps by doing two passes: the first would alternate the arc to go above the nodes rather than below, giving a sort of helix appearance; the second would draw a slightly larger arc if an arc already exists above/below to help differentiate the links.
var width = 1000,
height = 500,
margin = 20,
pad = margin / 2,
radius = 6,
yfixed = pad + radius;
var color = d3.scale.category10();
// Main
//-----------------------------------------------------
function arcDiagram(graph) {
var radius = d3.scale.sqrt()
.domain([0, 20])
.range([0, 15]);
var svg = d3.select("#chart").append("svg")
.attr("id", "arc")
.attr("width", width)
.attr("height", height);
// create plot within svg
var plot = svg.append("g")
.attr("id", "plot")
.attr("transform", "translate(" + pad + ", " + pad + ")");
// fix graph links to map to objects
graph.links.forEach(function(d,i) {
d.source = isNaN(d.source) ? d.source : graph.nodes[d.source];
d.target = isNaN(d.target) ? d.target : graph.nodes[d.target];
});
linearLayout(graph.nodes);
drawLinks(graph.links);
drawNodes(graph.nodes);
}
// layout nodes linearly
function linearLayout(nodes) {
nodes.sort(function(a,b) {
return a.uniq - b.uniq;
})
var xscale = d3.scale.linear()
.domain([0, nodes.length - 1])
.range([radius, width - margin - radius]);
nodes.forEach(function(d, i) {
d.x = xscale(i);
d.y = yfixed;
});
}
function drawNodes(nodes) {
var gnodes = d3.select("#plot").selectAll("g.node")
.data(nodes)
.enter().append('g');
var nodes = gnodes.append("circle")
.attr("class", "node")
.attr("id", function(d, i) { return d.name; })
.attr("cx", function(d, i) { return d.x; })
.attr("cy", function(d, i) { return d.y; })
.attr("r", 5)
.style("stroke", function(d, i) { return color(d.gender); });
nodes.append("text")
.attr("dx", function(d) { return 20; })
.attr("cy", ".35em")
.text(function(d) { return d.name; })
}
function drawLinks(links) {
var radians = d3.scale.linear()
.range([Math.PI / 2, 3 * Math.PI / 2]);
var arc = d3.svg.line.radial()
.interpolate("basis")
.tension(0)
.angle(function(d) { return radians(d); });
d3.select("#plot").selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("transform", function(d,i) {
var xshift = d.source.x + (d.target.x - d.source.x) / 2;
var yshift = yfixed;
return "translate(" + xshift + ", " + yshift + ")";
})
.attr("d", function(d,i) {
var xdist = Math.abs(d.source.x - d.target.x);
arc.radius(xdist / 2);
var points = d3.range(0, Math.ceil(xdist / 3));
radians.domain([0, points.length - 1]);
return arc(points);
});
}
Any pointers on how I might start approaching the problem?
Here is a bl.ock for reference. It shows your original paths in gray, and the proposed paths in red.
First store the counts for how many times a given path occurs:
graph.links.forEach(function(d,i) {
var pathCount = 0;
for (var j = 0; j < i; j++) {
var otherPath = graph.links[j];
if (otherPath.source === d.source && otherPath.target === d.target) {
pathCount++;
}
}
d.pathCount = pathCount;
});
Then once you have that data, I would use an ellipse instead of a radial line since it appears the radial line can only draw a curve for a circle:
d3.select("#plot").selectAll(".ellipse-link")
.data(links)
.enter().append("ellipse")
.attr("fill", "transparent")
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("cx", function(d) {
return (d.target.x - d.source.x) / 2 + radius;
})
.attr("cy", pad)
.attr("rx", function(d) {
return Math.abs(d.target.x - d.source.x) / 2;
})
.attr("ry", function(d) {
return 150 + d.pathCount * 20;
})
.attr("transform", function(d,i) {
var xshift = d.source.x - radius;
var yshift = yfixed;
return "translate(" + xshift + ", " + yshift + ")";
});
Note that changing the value for ry above will change the heights of different curves.
Finally you'll have to use a clippath to restrict the area of each ellipse that's actually shown, so that they only display below the nodes. (This is not done in the bl.ock)
In this D3 diagram, the circles are filled with radial gradients, and changing opacity is used for fading in and fading out:
var width = 400,
height = 400,
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)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
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 fade(opacity) {
return function(d) {
node.transition().duration(1000)
.style("fill-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
})
.style("stroke-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
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>
(Same code as a jsfiddle)
How to use color for fading in and fading out, instead of opacity? For example, let say we want to make all circles gray while in "faded out" state", and bring them back to their original color in their "normal state"? You can't just transition the fill property as a color value, because the fill is a URL reference to a <radialGradient> element.
If you were using solid color fills, it would be straightforward to transition them to gray and then back to color -- just use the d3 transition of the fill property instead of the fill-opacity and stroke-opacity properties.
However, the colors in this case aren't actually associated with the elements in your selection. Instead, they are specified within the <stop> elements of the <radialGradient> created for each category. (Actually, they are currently created for each individual circle -- see my note below.) Therefore, you need to select these elements to transition the stop colors.
Because you're transitioning all elements in a given category at the same time, you wouldn't need to create additional gradient elements -- you just need a way to select the gradients associated with those categories, and transition them.
Here's your original code for creating the gradient elements, and referencing them to color the circles:
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 + ")";
})
.call(force.drag)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
The fade() function you're currently using generates a separate event handler function for each element, which will then select all the circles and transition them to the specified opacity, or to full opacity, according to whether they are in the same cluster as the circle that received the event:
function fade(opacity) {
return function(d) {
node.transition().duration(1000)
.style("fill-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
})
.style("stroke-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
To transition the gradients instead, you need to select the gradients instead of the circles, and check which cluster they are associated with. Since the gradient elements are attached to the same data objects as the nodes, you can reuse the isSameCluster() method. You just need to change the inner function within the fade() method:
function fade(saturation) {
return function(d) {
grads.transition().duration(1000)
.select("stop:last-child") //select the second (colored) stop
.style("stop-color", function(o) {
var c = color(o.cluster);
var hsl = d3.hsl(c);
return isSameCluster(d, o) ?
c :
d3.hsl(hsl.h, hsl.s*saturation, hsl.l);
});
};
};
Some notes:
In order to select the correct stop element within the gradient, I'm using the :last-child pseudoclass. You could also just give the stop elements a normal CSS class when you create them.
To desaturate the color by the specified amount, I'm using d3's color functions to convert the color to a HSL (hue-saturation-luminance) value, and then multiply the saturation property. I multiply it, instead of setting it directly, in case any of your starting colors aren't 100% saturated. However, I would recommend using similarly saturated colors to get a consistent effect.
In the working example, I also changed your color palette so that you wouldn't have any gray colors to start out with (for the first 10 clusters, anyway). You'll probably need to create a custom palette with similar saturation values for all colors.
If you want the final value for the fade-out effect to always be an identical gray gradient, you could probably simplify the code quite a bit -- remove all the hsl calculations, and use a boolean parameter instead of a numerical saturation value. Or even just have two functions, one that resets all the colors, without needing to test for which cluster is which, and one that tests for clusters and sets values to gray accordingly.
Working snippet:
var width = 400,
height = 400,
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.category20()
.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)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
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 fade(saturation) {
return function(d) {
grads.transition().duration(1000)
.select("stop:last-child") //select the second (colored) stop
.style("stop-color", function(o) {
var c = color(o.cluster);
var hsl = d3.hsl(c);
return isSameCluster(d, o) ?
c :
d3.hsl(hsl.h, hsl.s*saturation, hsl.l);
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
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>
Note:
Currently, you're creating a separate <radialGradient> for each circle, when you really only need one gradient per cluster. You could improve the overall performance by using your clusters array as the data for the gradient selection instead of your nodes array. However, you would need to then change the id values for the gradients to be based on the cluster data rather than on the index of the node.
Using filters, as suggested by Robert Longson in the comments, would be another option. However, if you wanted a transition effect, you would still need to select the filter elements and transition their attributes. At least for now. When CSS filter functions are more widely supported, you would be able to directly transition a filter: grayscale(0) to filter: grayscale(1).
thanks for reading -- I am designing an interactive foreign language teaching tool using the D3js force bubble layout. In essence, users can select a language textbook chapter, part of speech, or specific word and the interface will display the word(s) in a series of bubbles each clustered together by their part of speech (adjective, noun, verb, etc.). These bubbles are free floating, no links between them.
When you click a word I have it open up more bubbles with information about that specific word (lexeme, meaning in english, frequency, etc.), but I want those bubbles to be attached to the original bubble with links. I'm having a lot of trouble conceptualizing how to do this dynamically (source -> target), even though it should be pretty straightforward.
First I draw the bubbles based on user input, some of which I've left out so the code doesn't get too long:
var nodes = d3.range(n).map(function(j) { return createNode(colorList, j); });
function createNode(info, j){
var keys = objectKeys;
var cluster, r;
if (info[j].Lexeme.length < 3) {
var radius = info[j].Lexeme.length * 15;
}
else {
var radius = info[j].Lexeme.length * 5;
}
$.each(keys, function(x, y){
if (info[j].POS == x){
cluster = y;
}
});
d = {cluster: cluster, radius: radius, info: info[j]};
if (!clusters[cluster] || (r > clusters[cluster].radius)) clusters[cluster] = d;
return d;
}
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(.00001)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function (d, i) { return "node_n" + i; })
.style("fill", function(d) {return d.info.fillColor;})
.on("click", nodeSelect)
.on("dblclick", infoNode)
.call(force.drag);
//Add the SVG Text Element to the svgContainer
var text = svg.selectAll("text")
.data(nodes)
.enter()
.append("text");
//Add SVG Text Element Attributes
text
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.text( function (d) { return d.info.Lexeme; })
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "#000000");
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(5 * e.alpha * e.alpha))
.each(collide(.3))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
text
.attr("transform", transform);
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function nodeSelect(d) {
if (d3.select(this).style("opacity") == 1 && nodeSelected == 0) {
nodeSelected = 1;
d3.select(this).style("opacity", ".3");
}
else if (d3.select(this).style("opacity") == .3 && nodeSelected == 1){
nodeSelected = 0;
d3.select(this).style("opacity", "1");
}
}
// 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;
});
};
}
function infoNodes(d){
$.each(d.info, function(i, val){
if (val != "" && val !="NA" && i != "fillColor"){
var b = d.cluster;
var r = 50;
var q = {cluster: b, radius: r, info: val, fillColor: d.info.fillColor};
if (!clusters[cluster] || (r > clusters[cluster].radius)) clusters[cluster] = q;
nodes.push(q);
}
});
update(d);
}
function update(j) {
node = svg.selectAll("circle")
.data(nodes)
.call(force.drag);
node.enter().append("circle")
.attr("class", function (d, i) { return "info_" + d;})
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function (d) { if (d.info.length < 5 || d.info.length == null) { return 30; } else { return d.info.length * 3.3; } })
.style("fill", function(d) {return d.fillColor;})
.call(force.drag);
text = svg.selectAll("text")
.data(nodes)
.call(force.drag);
text.enter().append("text")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.text( function (d) { return d.info; })
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "#000000")
.call(force.drag);
force.start();
}
The function can draw info nodes and update, but I can't figure out how to draw info nodes attached to the node you click with lines that connect them.
Let me know if more explanation is needed.