Reposition nodes in a multi-foci d3 force layout - javascript

I have three sets of nodes in a multi-foci force layout. Each node is already rendered in HTML.
Here's what my force layout code looks like:
var node = this.svg.selectAll('path')
.data(data);
// foci is a dictionary that assigns the x and y value based
// on what group a node belongs to.
var foci = {
"Blue" : {
"x" : xScale(0),
"y": height / 2
},
"Red": {
"x" : xScale(1),
"y": height / 2
},
"Purple": {
"x" : xScale(2),
"y": height / 2
},
};
// This helped me position the nodes to their assigned clusters.
var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);
var force = d3.forceSimulation(data)
.force('x', forceX)
.force('y', forceY)
.force("collide", d3.forceCollide(8))
.on('tick', function() {
node
.attr('transform', (d) => {
return 'translate(' + (d.x - 100) + ',' + (-d.y + 25) + ')';
});
});
What I have been able to accomplish so far is redraw the layouts based on a change in the dropdown, which reinitilizes d3.forceSimulation() and makes the clusters snap back on the page, as you can see in the gif below.
That is not what I want. I'm trying to make the rearranging as seamless as possible.
UPDATE: By not reinitializing the d3.forceSimulation(), I can bind the new data to the nodes and change their colors.

Instead of reinitialising d3.forceSimulation() you can simply reheat the simulation, using restart():
Restarts the simulation’s internal timer and returns the simulation. In conjunction with simulation.alphaTarget or simulation.alpha, this method can be used to “reheat” the simulation during interaction, such as when dragging a node, or to resume the simulation after temporarily pausing it with simulation.stop.
I created a demo to show you, using parts of your code. In this demo, the button randomizes the color of each data point. After that, we reheat the simulation:
force.alpha(0.8).restart();
Check it, clicking "Randomize":
var width = 500, height = 200;
var svg = d3.select("#svgdiv")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = d3.range(100).map(function(d, i){
return {
group: Math.random()*2 > 1 ? "blue" : "red",
id: i
}
});
var xScale = d3.scaleOrdinal()
.domain([0, 1])
.range([100, width-100]);
var foci = {
"blue" : {
"x" : xScale(0),
"y": height / 2
},
"red": {
"x" : xScale(1),
"y": height / 2
}
};
var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("fill", (d)=>d.group);
var force = d3.forceSimulation(data)
.velocityDecay(0.65)
.force('x', forceX)
.force('y', forceY)
.force("collide", d3.forceCollide(8));
force.nodes(data)
.on('tick', function() {
node
.attr('transform', (d) => {
return 'translate(' + (d.x) + ',' + (d.y) + ')';
});
});
d3.select("#btn").on("click", function(){
data.forEach(function(d){
d.group = Math.random()*2 > 1 ? "blue" : "red"
})
node.transition().duration(500).attr("fill", (d)=>d.group);
setTimeout(function(){
force.nodes(data);
force.alpha(0.8).restart();
}, 1500)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="btn">Randomize</button>
<div id="svgdiv"><div>
PS: I put the reheat inside a setTimeout, so you can first see the circles changing colours and, then, moving to the foci positions.

Related

How to assign different color to each node on a sunburst?

I am creating a sunburst for big data. To make it more readable, I need to assign different color for each node (ideally different shades of the same color for every subtree).
I've already tried with :
d3.scaleSequential()
d3.scale.ordinal()
d3.scale.category20c()
I think it can work but I am not sure where to put it exactly. For the moment it works only with one color for every subtree.
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleSequential().domain([1,10]).interpolator(d3.interpolateViridis);
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var partition = d3.partition() //.layout
.size([2 * Math.PI, radius]);
d3.json("file:///c:\\Users\\c1972519\\Desktop\\Stage\\tests_diagrams\\figure_4.8_ex3\\data2.json", function(error, nodeData){
if (error) throw error;
var root = d3.hierarchy(nodeData)
.sum(function(d){
return d.size;
});
partition(root);
var arc = d3.arc()
.startAngle(function(d) { return d.x0; })
.endAngle(function(d) { return d.x1; })
.innerRadius(function(d) { return d.y0; })
.outerRadius(function(d) { return d.y1; });
var arcs = g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function(d){return color(d)});
}
So I would like to have different shade on every subtree to make it more readable.
Anyone have an idea?
can you try with scaleLinear.
var x = d3.scaleLinear([10, 130], [0, 960]);
or
var color = d3.scaleLinear([10, 100], ["brown", "steelblue"]);
Example:
https://bl.ocks.org/starcalibre/6cccfa843ed254aa0a0d
Documentation:
https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear
Linear Scales
d3.scaleLinear([[domain, ]range]) <>
Constructs a new continuous scale with the specified domain and range, the default interpolator and clamping disabled. If either domain or range are not specified, each defaults to [0, 1]. Linear scales are a good default choice for continuous quantitative data because they preserve proportional differences. Each range value y can be expressed as a function of the domain value x: y = mx + b.

d3 Resetting elements to their original color

Long post, simple question.
I've taken a sunburst template and added highlighting functionality. When you hover over a "node" it highlights all other nodes with the same name. This can be found in the following code (mouseover and mouseout are at the bottom):
! function() {
// Stuff to build the sunburst
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));
});
// Create the svg element
var svg = d3.select("#vis").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
// Load the json file
d3.json("static/js/wheel.json", function(error, root) {
if (error) throw error;
root = d3.hierarchy(root);
root.sum(function(d) {
return d.size;
});
var path = svg.selectAll("path")
.data(partition(root).descendants())
.enter().append("g")
path.append("path")
.attr("d", arc)
.attr("name", function(name) {
return name.data.name
})
.style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
//Zoom in function
.on("click", click)
//When mouse enters node
.on("mouseover", function() {
var name = d3.select(this).attr("name");
var col = d3.select(this).style("fill");
d3.selectAll("path")
.filter(function(d) {
return d3.select(this).attr("name") === name;
})
.style('fill', 'orange')
.style('stroke', '#ff0d3c')
.style("stroke-width", "3");
})
//mouse leaves node
.on("mouseout", function() {
d3.selectAll("path").style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
.style("stroke", "#000")
.style("stroke-width", "1")
})
This works like a charm, it highlights the nodes and returns them to their original color after moving the mouse off. But now I've also added an input textbox to the html which highlights nodes with the same name as the textbox text. (it highlights as you type):
d3.select("#highlightWord").on("input", function() {
// Reset all the nodes to their original color here?
var name = this.value;
// Highlight all nodes that match textbox
d3.selectAll("path")
.filter(function (d) {
return d3.select(this).attr("name") === name;
})
.style('fill', 'orange')
.style('stroke','#ff0d3c')
.style("stroke-width","3");
});
Problem is, I don't know how to reset the color from here after the textbox no longer matches the node names. I think the easiest way would be to reset all the nodes every time the textbox is updated, so that it will reset every node and then change those that match. But I have no clue how to get the original node colors/style down here. Result is that everytime a node matches the textbox it will be highlighted forever, until the mouseout function is triggered.
I would greatly appreciate if someone could tell me how to reset the nodes after they don't match the textbox anymore, thanks in advance!

Plot density function with 2 or 3 colored areas?

I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});

User Input with Range Sliders to generate D3js Donut Graph

Im trying to create a d3js donut graph based on a number of input range sliders.
Currently I have something thrown together from other peoples work - this:
var width = 500,
height = width,
radius = Math.min(width, height) / 2,
slices = 5,
range = d3.range(slices),
color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius/1.6);
// Set up the <svg>, then for each slice create a <g> and <path>.
var paths = d3.select("svg")
.attr({ width: width, height: height })
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll(".arc")
.data(range)
.enter()
.append("g")
.attr({ class : "arc", style: "stroke: #fff;" })
.append("path")
.style("fill", function(d, i) { return color(i); });
// Create as many input elements as there are slices.
var inputs = d3.select("form")
.selectAll(".field")
.data(range)
.enter()
.append("div")
.attr("class", "field")
.append("label")
.style("background", function (d) { return '#FFFFFF'; })
.append("input")
.attr({
type : "range",
min : 0,
max : 4000,
value : 1,
step : 1,
value : 500,
class : "range",
id : function(d, i) { return "v" + i; },
oninput : "update()"
});
// update() sets the <path>s to the pie slices that correspond
// to the slider values. It is called when the page loads and
// every time a slider is moved.
function getTotal () {
var total = 0;
d3.selectAll('.range').each(function () {
total = total + parseInt(this.value);
});
return total;
}
function showValues () {
d3.selectAll('.range').each(function () {
var perct = this.value + '%';
d3.select(this.parentNode.nextSibling).html(perct);
});
}
function update () {
var data = range.map(
function(i) { return document.getElementById("v" + i).value }
);
paths.data(pie(data)).attr("d", arc);
}
update();
I need to sent the value for and range for each input, ideal in the html markup, as well as display the value and label for each input.
I've been seeing a lot of different types of d3js graphs that come close to this, but I haven't seen on that really gets all these elements together.
This comes kinda close from this, but I also need the segments to update with the range slider.Thanks in advance
This is a big problem, and any help would be appreciated.

D3.js attrTween Returning Null (Unexpected Behavior)

I have a very basic D3 SVG which essentially consists of a couple arcs.
No matter what I use (attr, attrTween, and call) I cannot seem to get the datum via the first argument of the callback--it is always coming back null (I presume it's some kind of parse error, even though the path renders correctly?)
I might be overlooking something basic as I am relatively new to the library...
var el = $('#graph'),
width = 280,
height = 280,
twoPi = Math.PI * 2,
total = 0;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(110)
.outerRadius(130),
svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
meter = svg.append('g').attr('class', 'progress');
/* Add Meter Background */
meter.append('path')
.attr('class', 'background')
.attr('d', arc.endAngle(twoPi))
.attr('transform', 'rotate(180)');
/* Add in Icon */
meter.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'fa fa-user')
.attr('y',30)
.text('')
/* Create Meter Progress */
var percentage = 0.4,
foreground = meter.append('path').attr('class', 'foreground')
.attr('transform', 'rotate(180)')
.attr('d', arc.endAngle(twoPi*percentage)),
setAngle = function(transition, newAngle) {
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
/*transition.attrTween('d', function(d) { console.log(this)
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) { d.endAngle = interpolate(t); return arc(d); };
});*/
};
setTimeout(function() {
percentage = 0.8;
foreground.transition().call(setAngle, percentage*twoPi);
},2000);
It's this block of code that seems to be problematic:
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
Returning:
undefined 0 "M7.959941299845452e-15,-130A130,130 0 0,1 76.4120827980215,105.17220926874317L64.65637775217205,88.99186938124421A110,110 0 0,0 6.735334946023075e-15,-110Z"
I tried using the interpolator to parse the i value as a string since I cannot seem to acquire "d," however that had a parsing error returning a d attribute with multiple NaN.
This all seems very strange seeing as it's a simple path calculated from an arc???
The first argument of basically all callbacks in D3 (d here) is the data element that is bound to the DOM element you're operating on. In your case, no data is bound to anything and therefore d is undefined.
I've updated your jsfiddle here to animate the transition and be more like the pie chart examples. The percentage to show is bound to the path as the datum. Then all you need to do is bind new data and create the tween in the same way as for any of the pie chart examples:
meter.select("path.foreground").datum(percentage)
.transition().delay(2000).duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc.endAngle(twoPi*interpolate(t))();
};
});

Categories