D3.js "Invalid value for <path> attribute..." yet chart still updates - javascript

I'm rolling through Mike Bostock's unbelievable collection of examples, and I am currently trying to get my own version of Pie Update II working (http://bl.ocks.org/mbostock/1346410). I've co-mingled with code shown here (http://jonsadka.com/blog/how-to-create-adaptive-pie-charts-with-transitions-in-d3/) to help me better understand what's actually going on. By this, I mean I've been tweaking the mbostock example by individually adding bits of code from the other example to see what works and what doesn't.
Regardless, I have a simple HTML page set up with radio buttons declared like so:
<form>
<label><input type="radio" name="dataset" value="photoCounts2" checked> Photos</label>
<label><input type="radio" name="dataset" value="videoCounts2"> Videos</label>
</form>
I have an array of integers of equal size (10), named videoCounts2 and photoCounts2. I'm trying to be able to click on a radio button and have the pie chart adjust to the corresponding dataset. I don't require a smooth transition just yet, I'd first like to get the initial transition working. Here is the JavaScript:
/* ----------------------------------------------------------------------- */
// Global vars
/* ----------------------------------------------------------------------- */
var width = 960,
height = 500,
radius = Math.min(width, height) / 2;
/* ----------------------------------------------------------------------- */
// Data Agnostic - Setup page elements
/* ----------------------------------------------------------------------- */
var color = d3.scale.category20();
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
/* ----------------------------------------------------------------------- */
// Data Dependent
/* ----------------------------------------------------------------------- */
var path = g.datum(photoCounts2).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
d3.selectAll("input")
.on("change", change);
function change() {
console.log("made it to change");
data = ((this.value === "photoCounts2") ? photoCounts2 : videoCounts2); // This needs to be changed if the dataset is different than video count.
console.log(this.value);
// Transition the pie chart
g.datum(data).selectAll("path")
.data(pie)
.transition()
.attr("d", arc);
// Add any new data
g.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
// Remove any unused data
g.datum(data).selectAll("path")
.data(pie)
.exit()
.remove();
}
I actually get the desired functionality, and nothing visually breaks, but I continually get the following error printed to the console when I switch between datasets (by choosing a different radio input):
Error: Invalid value for <path> attribute d="M1.4083438190194563e-14,-230A230,230 0 0.000006912,1 217.6110300447649,-74.45790482200897L141.92023698571626,-48.559503144788465A150,150 0 0.000006912,0 9.184850993605149e-15,-150Z"
(anonymous function) # d3.js:8717
tick # d3.js:8914
(anonymous function) # d3.js:8906
d3_timer_mark # d3.js:2159
d3_timer_step # d3.js:2139
I truly believe I've done my due diligence looking elsewhere for solutions, but I'm stumped. Any help is greatly appreciated!

You're seeing the error because some of the intermediate d values for the paths that are generated by the transition are invalid. As each of these is only visible for a few milliseconds, you don't actually see the error.
The root cause of the problem is that D3's default transition can't interpolate pie chart segments correctly. To make it work properly, you need to use a custom attribute tween, e.g. as in this example:
path.transition().duration(750).attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
This tells D3 how to generate all the intermediate values; in this case, by interpolating the angle of the segment and using the arc generator to generate the path for the changed angle (as opposed to interpolating the path itself without knowledge on how it was generated in the first place).

Related

How to update bar graph data whenever array is updated in d3 v6.3.1?

I'm trying to update a bargraph created using d3.js to display values from a regularly updated array. Currently, I have a function d3Data that is called upon page load(using jQuery) and as a function invoked whenever buttons are clicked on the page. This d3 data updates the array and then calls another function d3New that is supposed to rerender the bar graph.
The bar graph is able to render along with the bar rectangles if hard coded data in the array is used. However, since I initialize the starting array as empty I am unable to see the rectangles as it seems my bar graph doesn't display rectangles based on updated values in this array.
Here is my logic for displaying the rectangles within the bar graph:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect")
rects.exit().remove()
rects.attr("x", function(d, i) { return (i * 2.0 + 1.3) * barWidth; })
.attr("y", function(d,i) {
return Math.min(yScale(0), yScale(d))
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("fill", function(d,i) { return color[i];})
I understand the enter() function intially joins the data to the rectangle elements and the exit function is used in order to remove any previous rectangle element values upon rectangle rerender. But, no rectangles are rendered to the screen and not sure why? Here is what it looks like:
Any help would be great
edit:
Here is some more of the two functions:
function d3Data() {
var dataArray = [];
for (var key in gradeFrequency) {
dataArray.push(gradeFrequency[key]);
}
d3New(dataArray);
}
function d3New(data) {
var height = 500;
var width = 500;
var margin = {left: 100, right: 10, top: 100, bottom: 20};
var color = ["#C6C7FF", "#8E8EFC", "#5455FF", "#8E8EFC", "#C6C7FF"];
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate("+ [margin.left + "," + margin.top] + ")");
var barWidth = 30;
var chartHeight = height-margin.top-margin.left;
var xScale= d3.scaleBand()
.domain(["A", "B", "C", "D", "F"])
.range([100, 450])
.padding([0.8])
// Draw the axis
svg.append("g")
.attr("transform", "translate(-100,300)")
.call(d3.axisBottom(xScale));
var yScale = d3.scaleLinear()
.domain([0, 1.0])
.range([chartHeight, 0]);
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
I figured out how to fix my problem. Had to add:
d3.selectAll("svg").remove();
to the start of the function in order to remove previous outdated graphs and also add the attributes for "rect" before the .exit().remove(). So instead of:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
rects.attr(..).attr(..).attr(..)
I did:
rects.enter().append("rect").merge("rect").attr(..).attr(..).attr(..) and so on.
rects.exit().remove()
Since the attributes for the rectangles need to be updated as well they had to go before the .exit() and .remove() calls

How to use d3fc-label-label.js on a map?

I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });

How to add a legend to the motion chart

I use this script that creates a motion chart (see the original source code and a simplified fiddle). I wonder if it's possible to add the legend to this chart that would show the meaning of each color. The meaning should correspond to the field name from the input JSON.
Normally I create a legend as follows:
var colors = ["#F0E5FF","#E1CCFF","#C499FF","#AC79F2","#8D4CE5","#6100E5","#C94D8C"];
var colorScaleDomain = [300, 1000, 2000, 5000, 10000, 20000, 50000];
var colorScale = d3.scale.quantile()
.domain(colorScaleDomain)
.range(colors);
However, not sure how to adapt it to my current case when I use colorScale = d3.scale.category10().
Since d3.scale.category10() operates in a first-come first-served basis, that is, it adds values to the domain as new data comes in, you just need to pass the name in the same order that you painted the circles.
However, for this to work, you'll have to change your function color():
function color(d) {
return d.name;
}
Now, both the circles and the legends will have the same domain in the color scale.
Now, create the legends:
var legendGroup = svg.append("g")
.attr("transform", "translate(600,50)");
var legend = legendGroup.selectAll(".legend")
.data(SPX.map(d => d.name))
.enter()
.append("g")
.attr("transform", (d, i) => "translate(0," + 20 * i + ")")
var legendRects = legend.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", d => colorScale(d));
var legendText = legend.append("text")
.attr("x", 14)
.attr("y", 8)
.text(d => d);
Here is your fiddle, I put the legends in the top-right corner: http://jsfiddle.net/8yd04e0p/

How to implement on click function to D3 pie chart

I was using the example at this link to draw my pie chart: http://blog.stephenboak.com/2011/08/07/easy-as-a-pie.html
I made two modification to the original code:
1 only three arcs are allowed
function update() {
arraySize = 3;// it is total number of sectors
streakerDataAdded = d3.range(arraySize).map(fillArray); // value is fed into streakerDataAdded
oldPieData = filteredPieData;
pieData = donut(streakerDataAdded);//piedata is donut and value fed into
2 I manually set the value for the arc
var totalOctets = 0;
filteredPieData = pieData.filter(filterData);
filteredPieData[0].value=4000;
filteredPieData[1].value=8000;
filteredPieData[2].value=16000;
totalOctets=filteredPieData[0].value+filteredPieData[1].value+filteredPieData[2].value;
filteredPieData[0].name="Silver";
filteredPieData[1].name="Gold";
filteredPieData[2].name="Copper";
//angle
filteredPieData[0].startAngle=0;
filteredPieData[0].endAngle=filtere
dPieData[0].value/totalOctets*2*Math.PI;
filteredPieData[1].startAngle=filteredPieData[0].endAngle;
filteredPieData[1].endAngle=filteredPieData[1].startAngle+filteredPieData[1].value/totalOctets*2*Math.PI;
filteredPieData[2].startAngle=filteredPieData[1].endAngle;
filteredPieData[2].endAngle=filteredPieData[2].startAngle+filteredPieData[2].value/totalOctets*2*Math.PI;
The complete code is here:http://jsfiddle.net/jsXC5/
So far, so good. The desired function would be that if I click on one of the gold, a function is called and the value for the gold arc increase by 1000 while the values for silver and copper both decrease by 500.
I tried by doing:
paths = arc_group.selectAll("path").data(filteredPieData);
paths.enter().append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(tweenDuration)
.attrTween("d", pieTween)
.on("click", function(d,i){
filteredPieData[1].value=filteredPieData[1].value+1000;
filteredPieData[0].value=filteredPieData[0].value-500;
filteredPieData[2].value=filteredPieData[2].value-500;
});
But it does not work. Help please!
just solved it. The onlick function should be added directly below .attr instead of .attrTween

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