How to update a D3 chart dynamically - javascript

I am trying to create an easy way to create and update donut charts (And other charts, later) in an application. This is built on top of D3.
I've created a drawDonutChart as shown below:
var data = [50, 50];
var dataTwo = [75, 25];
var options = {
colors: ["#0074D9", "#7FDBFF"],
};
function createDonutChart(data, height, width, domElement, options) {
var radius = Math.min(width, height);
var color = d3.scale.category20();
var donut = d3.layout.pie().sort(null);
var arc = d3.svg.arc()
.innerRadius(radius/4)
.outerRadius(radius/2);
var svg = d3.select(domElement).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(donut(data))
.enter().append("path")
.attr("fill", function(d, i) {
if (options.colors == null || options.colors == "undefined") {
return color(i);
} else {
return options.colors[i];
}
}).attr("d", arc);
}
var donutChart = createDonutChart(data, 50, 50, ".slices", options);
when you have a in the page, this will draw a donut chart there.
I would like to write a function updateDonutChart(originalChart, newData), pass it (donutChart, dataTwo) and have it transition this data into the chart instead of the old data.
I've been looking at multiple examples on the D3 website but I haven't been able to get something to work this way (where you simply pass the old chart, and new data). It might be simple, I am just new to D3 :)
Thanks in advance for the help.

So, it wasn't clear to me from the signature of your update call if you wanted to create a new SVG under a possibly new DOM element every time you updated the chart. Also, I am not sure if you are using the word transition to specify a D3 transition of just a change in the data (although that would be simple enough to add). I any case, a simple adaptation to your code to do what you want is in this FIDDLE.
function updateDonutChart(data, options) {...

Related

d3.js format axis uniformly in millions

I found this thread and it got me halfway to where I need to be and I'm wondering if anyone knows how I can adjust the solution to fit my needs.
So if I have some values in the thousands, and some values in the millions, does anyone know how I can set all of the ticks to be formatted in the millions? For instance, if I have a value for 800k it would show up as 0.8million instead.
This is what I get when using the above solution without adjusting it.
You don't need to use a SI prefix in this case. Given this domain:
var scale = d3.scaleLinear().domain([200000,1800000])
Going from 200 thousand to 1.8 million, you can simply divide the tick value by 1,000,000 and add a "million" string.
Here is the demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var scale = d3.scaleLinear().domain([200000,1800000]).range([20,550]);
var axis = d3.axisBottom(scale).tickFormat(function(d){return d/1000000 + " Million"});
var gX = svg.append("g").attr("transform", "translate(20, 50)").call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
EDIT: According to the comments, you want to show "million" only in the last tick. Thus, we have to check if the tick is the last one and conditionally formatting it:
var axis = d3.axisBottom(scale).tickFormat(function(d){
if(this.parentNode.nextSibling){
return d/1000000;
} else {
return d/1000000 + " Million";
}
});
Here is the demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 100);
var scale = d3.scaleLinear().domain([200000,1800000]).range([20,550]);
var axis = d3.axisBottom(scale).tickFormat(function(d){
if(this.parentNode.nextSibling){
return d/1000000} else { return d/1000000 + " Million"}});
var gX = svg.append("g").attr("transform", "translate(20, 50)").call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>

Unable to get zoom by dragging a rectangle over a d3 series chart to work properly

I am trying to get zoom to work by dragging a rectangle over my series plot to identify the interval of zooming. Here is my plunkr
http://plnkr.co/edit/isaHzvCO6fTNlXpE18Yt?p=preview
You can see the issue by drawing a rectangle with the mouse over the chart - The new chart overshoots the boundary of the X and Y axes. I thought my group under the svg would take care of the bounds of the series (path) but I am clearly mistaken. After staring at it for a long time, I could not figure it out. Please ignore the angular aspect of the plunkr. I think the issue is somewhere in the
//Build series group
var series = svgGroup.selectAll(".series")
.data(data)
.enter().append("g")
.attr("class", "series");
//Build each series using the line function
series.append("path")
.attr("class", "line")
.attr("d", function (d) {
return line(d.series);
})
.attr("id", function (d) {
//While generating the id for each series, map series name to the path element.
//This is useful later on for dealing with legend clicks to enable/disable plots
legendMap[d.name] = this;
//Build series id
return buildPathId(d.name);
})
.style("stroke", function (d) {
//Use series name to get the color for plotting
return colorFcn(d.name);
})
.style("stroke-width", "1px")
.style("fill", "none");
Any help with this is appreciated.
Thank you very much.
I think the method renderChartWithinSpecifiedInterval(minX, maxX, minY, maxY, pixelCoordinates) maybe has some problem there.
It seems the parameter like max_x passed in line 130 are a very big value like time seconds
var svg = renderChartWithinSpecifiedInterval(min_X, max_X, min_Y, max_Y, false);
max_X,min_X are value like 1415171404335
min_Y = 0, max_Y = 100
But in dragDrop call in line 192
function gEnd(d,i){
svg.selectAll(".zoom-rect").remove();
var svgGp = svg.select("g");
var groupTransform = d3.transform(svgGp.attr("transform"));
var xOffset = groupTransform.translate[0];
var yOffset = groupTransform.translate[1];
var xBegin = Math.min(xStart,xDyn) - xOffset;
var xEnd = Math.max(xStart,xDyn) - xOffset;
var yBegin = Math.min(yStart,yDyn) - yOffset;
var yEnd = Math.max(yStart,yDyn) - yOffset;
renderChartWithinSpecifiedInterval(xBegin, xEnd, yBegin, yEnd, true);
//It seems here the parameters values are all pixels
like xBegin = 100, xEnd = 200
}
hope it helps!

d3.js Two Dimensional Array Bar Chart

I am trying to make a bar chart containing 3 'groups' of data in d3.js. I have been able to implement the example from "Let's Make a Bar Chart" sample http://bost.ocks.org/mike/bar/2/, but am wondering how you would implement that with a 2 dimensional data set instead.
I'm having a bit of a tough time wrapping my head around what I feel should be a fairly straight forward process (select first index of 2d array -> iterate through that array and display the values of each element in bars -> select second index of array -> iterate through that array and display the value of each element in bars etc...), so examples would be hugely appreciated.
The code thus far is:
var data = [4, 8, 15, 16, 23, 42];*/
var width = Math.max(500, innerWidth), height = Math.max(500, innerHeight), barHeight = 80;
var x = d3.scale.linear().domain([0, d3.max(data)]).range([0, width]);
var chart = d3.select(".chart").attr("width", width).attr("height", barHeight * data.length * 3);
var bar = chart.selectAll("g").data(data).enter().append("g").attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect").attr("width", x).attr("height", barHeight - 1);
bar.append("text").attr("x", function(d) { return x(d) - 3; }).attr("y", barHeight / 2).attr("dy", ".35em").text(function(d) { return d; });
however I would like to change the "data" variable to be:
var data = [[40,45,5],
[50,49,2],
[60,62,4],
[40,41,6],
[42,40,3],
[65,67,10],
[70,67,17],
[66,65,2],
[45,44,3],
[39,38,7],
[38,38,8],
[45,40,4],
[43,35,3],
[50,65,8],
[51,50,6]];

d3.js / svg - how to dynamically append text to my arcs

I am trying to complete the last bit of a d3 project which dynamically creates these blue arcs, over which I need to place arc text, as shown in this image:
The image above is something I've done by placing the arc text statically, through trial and error, but I want to place it dynamically, based on the blue arcs which sit beneath the text. This is the code that dynamically creates the arcs:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { console.log(d.__data__.key); return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group");
arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5);
The {exp:} content is preparsed data I'm pulling from my content management system in expression engine if it looks confusing.
So, I have my arcs. Now you'll notice in the groupData code block I have a console.log statement, that will give me the names I want to appear in the arc text:
console.log(d.__data__.key);
Now, the code I was using to place the arc text statically was this:
var arcData = [
{aS: 0, aE: 45,rI:radius - chartConfig.linePadding + chartConfig.arcPadding,rO:radius - chartConfig.linePadding + chartConfig.textPadding-chartConfig.arcPadding}
];
var arcJobsData = d3.svg.arc().innerRadius(arcData[0].rI).outerRadius(arcData[0].rO).startAngle(degToRad(1)).endAngle(degToRad(15));
var g = d3.select(".chart").append("svg:g").attr("class","arcs");
var arcJobs = d3.select(".arcs").append("svg:path").attr("d",arcJobsData).attr("id","arcJobs").attr("class","arc");
g.append("svg:text").attr("x",3).attr("dy",15).append("svg:textPath").attr("xlink:href","#arcJobs").text("JOBS").attr("class","arcText"); //x shifts x pixels from the starting point of the arc. dy shifts the text y units from the top of the arc
And in this above code, the only thing left that I should need to do is dynamically assign an ID to the arcs, and then reference that ID in the xlink:href attribute, as well as replace the text("JOBS") with text that pulls from d.data__key. Given the code above which dynamically creates the arcs, and given that I know how to dynamically create and retrieve the text I want to place in the arcs using d.__data.key, I should be able to finish this thing off, but I can't figure out how write code in d3 that will take the data and place it in the arcs. Can anybody help with this?
You should give this blog post on nested selections a read; I believe it'll explain what you're trying to do.
Here's the gist. When you add data to your selection, assign the selection to a variable:
var g = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { /* stuff */ }));
That way, you can perform subselections on it, which will receive a single element of the data bound to your g selection. You can use this to add your arcs and text:
g.enter().append('group') // Question: Are you sure you mean 'group' here?
.attr('class', 'group')
g.selectAll('g.arc')
.data(function(d, i) { return d; })
enter().append('path')
// Setup the path here
g.selectAll('text')
.data(function(d, i) { return d; })
.enter().append('text')
.attr('text', function(d) { return d.__data__.key })
The functions that are being used to do data binding in the nested selections (i.e., the g.selectAll()s) are being passed a single element of the data attached to g as d, and i is its index.
Figured this out. Changed the structure of things a bit so it made a little more sense, but essentially what I did is this:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group"); //MH - why do we need this group - these elements are empty. Shouldn't this just be an array? Find out how to delete the svg elements without getting rid of the data, which is needed below.
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) { return (findStartAngle(d.__data__.children)-2) * pi / 180;})
.endAngle(function(d) { console.log(d.__data__.key); return (findEndAngle(d.__data__.children)+2) * pi / 180});
var arc_and_text = arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:g")
.attr("class","arc_and_text");
var arc_path = arc_and_text.append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) { return "arc" + i; })
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5); //MH: (d.__data__.key) gives names of groupings
var arc_text = arc_and_text.append("text")
.attr("class","arc_text")
.attr("x", 3)
.attr("dy", 15);
arc_text.append("textPath")
.attr("xlink:href", function(d, i) { return "#arc" + i; })
.attr("class","arc_text_path")
.style("fill","#ffffff")
.text(function(d, i) { return d.__data__.key; });
D3 still mystifies me a bit, and I'm sure this code could be much improved, but it works.

Adding labels to a pv.Layout.Stack (streamgraph) in Protovis

I'm working with the Protovis library to do a streamgraph of data. I want to label the different layers with the "words" array. I can't seem to get the words to line up how I'd like. I want them to be inserted where the graph is the largest for that particular layer, similar to this site:
http://mbostock.github.com/protovis/ex/jobs.html
var words = [
"tasty","delicious","yum","scrumpious","dry"];
var data = [
[23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19,2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35],
[43,2,46,78,46,25,54,72,85,119,23,52,6,3,16,35,24,12,35,119,23,52,6,3,16,35,24,12,35,119,2,5,65,33,81,61,55,122,3,19],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,1,5,12,95,14,12,8,84,115,15,27,6,31,6,35],
[2,5,6,3,1,6,5,12,32,191,142,22,75,139,27,32,26,13,161,35,21,52,64,35,21,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35],
[2,5,65,33,81,61,55,122,3,19,54,72,85,119,23,52,6,3,16,35,2,5,65,33,81,61,55,123,5,142,54,58,8,11,53,2,64,3,16,35]];
var w = 800,
h = 300,
x = pv.Scale.linear(0, 40).range(0, w),
y = pv.Scale.linear(0, 600).range(0, h);
var vis = new pv.Panel()
.canvas('streamgraph')
.width(w)
.height(h);
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) });
vis.render();
Try this:
vis.add(pv.Layout.Stack)
.layers(data)
.order("inside-out")
.offset("wiggle")
.x(x.by(pv.index))
.y(y)
.layer.add(pv.Area)
.fillStyle(pv.ramp("#aad", "#556").by(Math.random))
.strokeStyle(function () { this.fillStyle().alpha(.5) })
// this is new code:
.anchor("center").add(pv.Label)
.def("max", function(d) {return pv.max.index(d)})
.visible(function() {return this.index == this.max() })
.text(function(d, p) {return words[this.parent.index]});
Basically this adds a whole bunch of labels to your areas, But then only makes them visible at the index where the value is the maximum, by defining a function max on the series. I adapted this code from the code in the link you sent.

Categories

Resources