User Input with Range Sliders to generate D3js Donut Graph - javascript

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.

Related

Adding color dynamically to a multiple line chart but it is not working in d3.js

I have created this curve line chart and I filled by purple color only. Then I decided to add color range and I used color scaling method is Category10 but it is not working. I am new to D3.js and it has been my headache for the past week.
In order to make it look more presentable, I need to add colors and I did tried to add this way by using .style attribute but
function buildLine(data){
var xScale=d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.spend;})])
.range([0, w]);
var yScale=d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.alpha;})])
.range([h/2, 0]);
var yAxisGen=d3.svg.axis().scale(yScale).orient("left");
var xAxisGen=d3.svg.axis().scale(xScale).orient("bottom");
var svg=d3.select("body").append("svg").attr({width:w,height:h});
var yAxis= svg.append("g")
.call(yAxisGen)
.attr("class","axis")
.attr("transform", "translate(50, " +10 +")");
var xAxisTranslate = h/2 + 10;
var xAxis= svg.append("g")
.call(xAxisGen)
.attr("class","axis")
.attr("transform", "translate(50, " + xAxisTranslate +")");
Adding color function here
var color=d3.scale.category10
var lineFun = d3.svg.line()
.x(function (d) { return xScale(d.x); })
.y(function (d) { return yScale(d.y); })
.interpolate("basis");
Here I tried to add it dynamically. '.style does not work. WHy?'
var viz = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr({
d: function(d) {
return lineFun(d.arr)
},
"stroke": "purple",
"stroke-width": 2,
"fill": "none",
"class":"line"
})
.style("stroke",function(){
return d.color=color(d.arr);
})
.attr("transform", "translate(48, " + 10 +")")
;
}
d3.json("sample-data.json",function(error,data){
//check the file loaded properly
if (error) { //is there an error?
console.log(error); //if so, log it to the console
} else { //If not we're golden!
//Now show me the money!
ds=data; //put the data in the global var
}
data.forEach(function(jsonData){
var lineData = d3.range(0, jsonData.spend, 100)
.map(x => [x, (jsonData.alpha * (1 - Math.pow(2.71828, (-jsonData.beta * x))))] );
/* console.log("this is the data:",lineData);*/
//i think line date returns an array with each item in it also an array of 2 items
var arr = [];
for (var i = 0; i < lineData.length; i++) {
arr.push({
x: lineData[i][0],
y: lineData[i][1]
});
}
jsonData.arr = arr;
console.log(jsonData);
});
buildLine(data);
});
These are the problems:
You have to call the scale function:
var color=d3.scale.category10()
//parentheses here ----------^
You cannot use the argument d if you don't set the anonymous function parameter:
.style("stroke",function(d){
//parameter here-----^
return d.color=color(d.arr);
})
scale.category10() works on a "first come, first served" basis. You just need the index:
.style("stroke",function(d,i){
return d.color=color(i);
});
Here is a demo showing how to use that scale:
var color = d3.scale.category10();
var divs = d3.select("body").selectAll(null)
.data(d3.range(10))
.enter()
.append("div")
.style("background-color", function(d, i) {
return color(i)
})
div {
width: 20px;
height: 20px;
display: inline-block;
margin: 2px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>

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

Reposition nodes in a multi-foci d3 force layout

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.

Creating a multilayer pie chart with D3

How can I create a multi layer pie chart with d3.js which looks like below
Every section doesn't have an inner subsection and when it has a subsection then it has darker color than the outer subsection as shown in the above image.
I tried searching for multilayer pie chart but what all I could do is this.
http://jsfiddle.net/ZpQ3x/
Here is corresponding javascript code
var dataset = {
final: [7000],
process: [1000, 1000, 1000, 7000],
initial: [10000],
};
var width = 660,
height = 500,
cwidth = 75;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class","wrapper")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return Object.keys(dataset)[i];
});
var gsLabels = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return "label_" + Object.keys(dataset)[i];
});
var count = 0;
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
d._tmp = d.endAngle;
d.endAngle = d.startAngle;
if(Object.keys(dataset)[j] === "final"){
d.arc = d3.svg.arc().innerRadius(cwidth*j).outerRadius(cwidth*(j+1));
}
else{
d.arc = d3.svg.arc().innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1));
}
return d.arc(d);
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle, d._tmp);
return function(t) {
d.endAngle = i(t);
return d.arc(d);
}
});
Thank you very much.
I have changed your dataset into a single JSON.
Just to ensure that mentioned above array x and x1 are related together i made data set like this.
data = [{
major: 100,//this is the X array first element
minor: 70,//this is the X1 array first element
grp: 1//here grp is for coloring the segment
}, {
major: 100,
minor: 30,
grp: 2
}, {
major: 100,
minor: 50,
grp: 3
}, {
major: 140,
minor: 70,
grp: 4
}, {
major: 80,
minor: 10,
grp: 5
}];
I have made two arc function.
var arcMajor = d3.svg.arc()
.outerRadius(function (d) {
return radius - 10;
})
.innerRadius(0);
//this for making the minor arc with variable radius as per scale
var arcMinor = d3.svg.arc()
.outerRadius(function (d) {
// scale for calculating the radius range([20, radius - 40])
return scale((d.data.major - d.data.minor));
})
This is the code which makes the path.
//this makes the major arc
g.append("path")
.attr("d", function (d) {
return arcMajor(d);
})
.style("fill", function (d) {
return d3.rgb(color(d.data.grp));
});
//this makes the minor arcs
g.append("path")
.attr("d", function (d) {
return arcMinor(d);
})
.style("fill", function (d) {
return d3.rgb(color(d.data.grp)).darker(2);//for making the inner path darker
});
Working code here with comments
Hope this helps!

D3 adding two donut charts on top of each other.

I'm looking to somehow get two donut charts on top of one another, or atleast just the arcs. I want to hide one specific arc, and show the other on click, and then revert on click again.
I figured out you can simply hide an arc on click by selecting that slice, and doing d3.select("the arc").attr("visibility", "hidden");
So I want to hide one slice, and show the other. I want the arcs to take up the same spot, so showing the other appears to only change the arc.
Thank you,
Brian
As far as I understand your problem, you want to update a particular arc on click.
So, instead of creating two donuts, one on top of another, just create one donut chart and update it whenever the arc is clicked.
$(document).ready(function() {
var width = 400,
height = 250,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.value(function(d) {
return d.apples;
})
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 70)
.outerRadius(radius - 20);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var data = [{
"apples": 53245,
"oranges": 200
}, {
"apples": 28479,
"oranges": 200
}, {
"apples": 19697,
"oranges": 200
}, {
"apples": 24037,
"oranges": 200
}];
var path = svg.datum(data).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
}) // store the initial angles
.on("click", function(d) {
var key = d.data.getKeyByValue(d.value);
var oppKey = (key === "apples") ? "oranges" : "apples";
change(oppKey);
});
function change(keyVal) {
var value = keyVal;
pie.value(function(d) {
return d[value];
}); // change the value function
path = path.data(pie); // compute the new angles
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
function type(d) {
d.apples = +d.apples;
d.oranges = +d.oranges;
return d;
}
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Object.prototype.getKeyByValue = function(value) {
for (var prop in this) {
if (this.hasOwnProperty(prop)) {
if (this[prop] === value)
return prop;
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Categories