I'm trying to make a data visualisation with d3.js. Im new to d3 and i think i did not quite understand completely how to change data with a click, so i need your help!
Thats what i've got so far(which i'm quite happy that it works):
/*-------------------------data is parsed and proceedet above----------------------------------------------------*/
var SVGWidth = 1670;
var SVGeight = 800;
var kreis = 1;
var ringArea = 1;
var width = 1;
var multi = 3.5;
var color = d3.scale.ordinal().range(['#FFFFFF', '#DADAD9', '#9D9C9C']);
var arc = d3.svg.arc().innerRadius(function(d) {
donutWidth = Math.sqrt(d.data.WHI1 / Math.PI + Math.pow(d.data.E1, 2)) * multi - d.data.E1 * multi
return donutWidth;
}).outerRadius(function(d, i) {
width = Math.sqrt(d.data.WHI1 / Math.PI + Math.pow(d.data.E1, 2)) * multi
return width;
});
var pie = d3.layout.pie().value(function(d) {
return d.country;
}).sort(null);
var svg = d3.select('#chart')
.append('svg')
.attr('width', SVGWidth)
.attr('height', SVGeight)
.append("g")
svg.selectAll("g")
.data(data1995).
enter().append("g")
.attr({
transform: function(d, i) {
var pos = coord2Pt(geo[i][1], geo[i][2], 1.0);
return "translate(" + pos.x + ", " + pos.y + ")";
}
}).selectAll('path').data(function(country, i) {
return pie(country.map(function(value) {
return {
country: value,
WHI1: WHI1[i],
E1: E1[i]
};
}));
})
.enter()
.append('path')
.attr('d', function(d) {
return arc(d);
}).attr('fill', function(d, i) {
return color(i);
});
});
})(window.d3);
This results in this Visualisation:
Worldmap donut
What i'm now trying to do is to change my Data from "data1995" to "data2015", both "WHI1" to "WHI2" and both "E1" to "E2" with a click on the "test" button. In addition it would be great if they would change with a transition.
I'm not shure if I'm on the right path to accomplish this but is tried this so far:
d3.select("button").on('click', function() {
console.log("click")
d3.selectAll('path').transition().duration(500).attr("fill", function(d, i) {
return color(i);
}).attr("d", arc).each(function(d) {
this._current = d;
});
function change(data2015) {
return pie(country.map(function(value) {
return {
country: value,
WHI2: WHI2[i],
E2: E2[i]
};
}));
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
Thats what I've found on another thread on here (update d3 pie chart with new data.json)
but in fact i'm not even shure if i translated it right to my script and i'm curious if that would work with all of my instances of the Donut.
I suggest you to add a button with a onclick property, for example an update() function.
This function loads data2015 and change WHI1 to WHI2 and E1 to E2.
So this function is quite similar to what you have already written.
I think this approach is better because you do not need to update the chart as frequently as in the link you posted
I've found an example links, look at these:
http://www.d3noob.org/2013/02/update-d3js-data-dynamically-button.html
http://bl.ocks.org/d3noob/7030f35b72de721622b8
Related
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 currently am using the D3 library to create a donut chart. I have two states for the chart that you tween between when a button is pushed. The pie slices, poly lines, and text labels all tween correctly around the chart when the button is pushed. However some circles I have do not tween correctly when I press the button. The circles remain in place however new circles are drawn in the ending location of the transform.
I have a jsfiddle that shows the behavior here: http://jsfiddle.net/Qh9X5/7847/
The code in question that controls the circles is specifically here:
/* ------- OUTER POINTS -------*/
var outerPoint = fndAfter.select(".outerPoints").selectAll(".outerPoint")
.data(pie(data), key);
outerPoint.enter()
.append("circle")
.attr("r", "2");
outerPoint.transition().duration(1000)
.attrTween("transform", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = radius * 0.624 * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate(" + pos + ")";
};
});
outerPoint.exit()
.remove();
/* ------- INNER POINTS -------*/
var innerPoint = fndAfter.select(".innerPoints").selectAll(".innerPoint")
.data(pie(data), key);
innerPoint.enter()
.append("circle")
.attr("r", "2");
innerPoint.transition().duration(1000)
.attrTween("transform", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = radius * 0.624 * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate(" + midPoint(midPoint(arc.centroid(d2), outerArc.centroid(d2)), outerArc.centroid(d2)) + ")";
};
});
innerPoint.exit()
.remove();
I was under the impression that the attrTween should work the same for all transform functions no matter what element it is.
The problem is not with attrTween. You use selectAll('.innerPoint') to define your selection, but you never apply the innerPoint class to the circles, so d3 never finds any matching selection and appends new circles every time.
Working fiddle: http://jsfiddle.net/Qh9X5/7849/
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>
I am new to programming so apologies if the answer to this is obvious but after hours of searching I can't find out what's wrong.
I simply want to tween an arc in D3.js (in this case change the endAngle to 0). I've been through lots of examples but I must be missing something. I have built a function to change arc colour on clicking which works but it is the second function 'arcTween' to change the arc endAngle of the outermost arcs that doesn't work. Can you help?
Many thanks
Full JS fiddle here: http://jsfiddle.net/vaaa052h/
Extracts below
var chartArea = d3.select("#chart")
.append("svg") // d3 SVG function
.attr("width", 210)
.attr("height", 210);
var arcGroup = chartArea.append("g") // d3 g grouping function
.attr("transform", "translate(" + transX + "," + transY + ")")
.attr("class", "arc");
var arc = d3.svg.arc()
.innerRadius(function (d) {
return radius[level];
})
.outerRadius(function (d) {
return radius[level + 1];
})
.startAngle(function (d) {
return minAngArc;
})
.endAngle(function (d) {
return maxAngArc;
});
//////// chart building ///////////////
arcGroup.append("path")
.attr("d", arc)
.attr("fill", color(0, random, 0, i, j, k))
.attr("opacity", opacity(rating))
.on("click", arcTween());
////// click functions //////////
function arcTween(d) {
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, 0);
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
I made a couple of changes in this updated fiddle: http://jsfiddle.net/henbox/a8r326m5/1/
First, when you set up the click handler, avoid calling it on page load by using:
.on("click", arcTween);
instead of
.on("click", arcTween());
as per Lars' explanation here. This will stop you getting "Object [object global] has no method 'getAttribute'" errors in the console
Second, bind some data to the path elements so we can manipulate it later:
arcGroup.append("path")
.datum({endAngle:maxAngArc, startAngle:minAngArc})
....
And thirdly, use this data in the arcTween function. By setting maxAngArc and minAngArc, and then tweening the value of maxAngArc to minAngArc (I've asumed you mean to do this rather than tweening to 0), you should get the behaviour you want. The tween function:
function arcTween(d) {
maxAngArc = d.endAngle;
minAngArc = d.startAngle;
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, d.startAngle);
return function (t) {
maxAngArc = interpolate(t);
return arc(d);
};
});
};
I'm still learning to program and I'm currently trying out the d3 library.
So far I'm pretty happy with the result. fiddle
Q: If you check out the link (or part of the code under this question) you should try to plot a point. This is only possible on the x-axis ticks. You'll see it animates but it's not exactly what I want. I just want it to animate the newly added line. I have checked out .enter() and .append() but I was getting errors. I might be doing something wrong.
function lines(x, y) {
this.x = x;
this.y = y+h;
}
var lineArray = [{x: 0, y: h}, {x: 1, y: h}];
var lineArrayPrevious = lineArray[lineArray.length -1].x;
var d3line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("monotone");
var path = svg.append("path").attr("d", d3line(lineArray)).attr("class", "line");
canPlot = true;
function plot() {
var m = d3.mouse(this);
if (m[0]-20 > lineArray[lineArray.length - 1].x) {
var lineX = lineArray.push(new lines(m[0], m[1]));
svg.selectAll("path")
.data(lineArray)
.attr("d", d3line(lineArray));
var point = svg.append("circle")
.attr("cx", function(d, i) { return m[0]; })
.attr("cy", function(d, i) { return m[1]+h; })
.attr("r", 0).transition().delay(150).attr("r", 6);
var totalLength = path.node().getTotalLength();
console.log();
path.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition().duration(700).ease("linear").attr("stroke-dashoffset", 0).delay(200);
canPlot = true;
} else { console.log("error"); canPlot = false; }
}
Excuse my bad code, I'm learning and will clean it up eventually.
Q2: How hard would it be to make a circle that follows the mouse's y-position and moves on the ticks when you get near one?
Q3: If we solve my first question, would it be easy to get the lines to animate/update automatically when we do question 2?
Thanks in advance.
I've updated your jsfiddle here to include the points that you're asking for.
Regarding question 1, I've changed the way the line is drawn such that it can be interpolated from the previous to the current point in a transition. The relevant code is this.
svg.select("path.line")
.attr("d", d3line(lineArray))
.transition().duration(700)
.attrTween('d', pathTween)
.each("end", function() {
var lineX = lineArray.push(new lines(m[0], m[1]));
});
var last = lineArray[lineArray.length-1];
function pathTween() {
var xi = d3.interpolate(last.x, m[0]),
yi = d3.interpolate(last.y, m[1] + h);
return function(t) {
return d3line(lineArray.concat([{x: xi(t), y: yi(t)}]));
};
}
Note that the new data point is only added to the array of points once the transition finishes.
Regarding your second question, this is taken care of by attaching handlers to all tick marks and append a marker on mouse over:
d3.selectAll(".xaxis > .tick").on("mouseenter", mousein)
.on("mousemove", mousemove)
.on("mouseleave", mouseout);
function mousein() {
svg.append("circle").attr("class", "marker").attr("r", 3)
.attr("pointer-events", "none");
}
function mousemove() {
d3.select("circle.marker")
.attr("transform", d3.select(this).attr("transform"))
.attr("cy", d3.mouse(this)[1] + h);
}
function mouseout() {
d3.select("circle.marker").remove();
}