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