I am attempting to make a reusable chart in D3. The figure draws a line using .curve(d3.curveStepAfter) and I am attempting to animate the lines using the stroke-dasharray attribute with a tween function. The following code is how I initialize the visualization:
selection
.append('g')
.selectAll('path.line')
.data(grouped_data)
.join(
(enter) => enter
.append('path')
.attr('class', 'step_line')
.call(initializeLine)
.call(lineTransition),
(update) => update,
(exit) => exit.remove());
const initilizeLine = (enter) => {
enter
.attr('d', (d) => drawCurve(d[1]))
.attr('stroke', d => {
return color(d[0]);
})
.attr('fill', 'none');
}
const lineTween = () => {
let l = this.getTotalLength(),
i = d3.interpolateString('0,' + l, l + ',' + l);
return function(t) { return i(t) };
}
const lineTransition = (enter) => {
enter
.transition()
.duration(2500)
.attrTween('stroke-dasharray', lineTween);
}
The lines draw correctly in the svg, but the transition doesn't work. When I look in the console I see the error this.getTotalLength is not a function. This is because the enter object is being passed. I need to be able to pass the path to the lineTween function, but am scratching my head.
In the context of your arrow function this is the window. Therefore, either use a regular function or use the third and second parameters combined to get the element you want:
const lineTween = (_, j, n) => {
let l = n[j].getTotalLength(),
i = d3.interpolateString('0,' + l, l + ',' + l);
return function(t) { return i(t) };
}
I've some code inside a selection.join() pattern:
const nodeWidth = (node) => node.getBBox().width;
const toolTip = selection
.selectAll('g')
.data(data)
.join(
(enter) => {
const g = enter
.append('g')
g.append('text')
.attr('x', 17.5)
.attr('y', 10)
.text((d) => d.text);
let offset = 0;
g.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
},
(update) => {
update
.select('text')
.text((d) => d.text);
let offset = 0;
update.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
}
);
as you can see, in the enter and update section I need to call a couple of functions to calculate several nodes transformations. In particular, the code stores in the accumulation var offset the length of the previous text element. This properly spaces text elements (ie, text0 <- 10 px -> text1 <- 10 px -> ...).
As you can see, the "transform functions" in the enter and update section are identical. I'm trying to define them just in one place and call them where I need. E.g.,
(update) => {
update.attr('transform', foo);
selection.attr('transform', bar);
}
However, I cannot refactor the code this way because it looks like I cannot pass in neither the offset value nor this to the function passed to attr().
Is there a way to do it?
EDIT:
As per Gerardo Furtado's hint (if I got it right), you can define foo as follows:
const foo = function(d, i, n, offset) {
let x = offset;
offset += nodeWidth(n[i]) + 10;
return `translate(${x}, 0)`;
}
then in the selection.join¡ you have to call foo this way:
(update) => {
let offset = 0;
update.attr('transform', (d, i, n) => foo(d, i, n, offset));
}
However, refactoring this way, offset is ever equal to 0. A possibile solution here: https://stackoverflow.com/a/21978425/4820341
Have a look at Function.prototype.bind().
const doSomething = (d) => {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
}
Calling the function inside (enter) and (update)
selection.attr('transform', doSomething.bind(d));
This way the function gets executed in the current scope.
I guess this is what you are looking for. Please be aware that I could not test my code!
I am trying to get font awesome icons to draw in d3 by filling an array with axios calls to an api. I'm having an issue with the what i believe is the asynchronous nature of axios. When i log the array i get all of the elements to display although when i call a single element it returns undefined.
I think i saw a reason for calling console.log on the whole array works because console.log() is also asynchronous so when i call the whole array it waits until all elements are finished loading. Then when i call a single element it calls immediately. This is why i believe i can see elements when i call the whole array.
The main issue i have is getting my weathericons array elements to be defined when i make a call to an element in the .then function of the axios.all.
This is currently what i have...
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
}))
// After everything is loaded from the server can we append our data
.then(
svg.append('svg:foreignObject') //d3 chart
.data(options.labels)
.attr('alt', "")
.attr("height", xScale.bandwidth() > 100 ? 100 : xScale.bandwidth())
.attr("width", xScale.bandwidth() > 100 ? 100 : xScale.bandwidth())
.attr("transform", function(d, j) {
var height_adj = 60 * (xScale.bandwidth() > 1 ? 1 : xScale.bandwidth()) / 50;
return "translate(" + (xScale(options.labels[j]) + (xScale.bandwidth() / 2) - ((xScale.bandwidth() > 100 ? 100 : xScale.bandwidth()) / 2))
+ "," + (yScale(0) - height_adj) + ") scale(" + (xScale.bandwidth() > 100 ? 100 : xScale.bandwidth()) / 50 + ")"
})
.append('i')
.attr('class', function(d, j){
return 'fa fa-' + weathericons[j] + '-o"'; //This is what i need to work
})
)
Im trying to have the weathericons array elements call for all the data so i get an icon with every data point. I just can't seem to figure out a good fix to this.
Is there a way to have the array fill completely before calling the .then statement?
You definitely make the obvious mistake that the entire expression starting with svg.append() is the first argument to the 2nd then() but the argument to then() should be a function.
Change it like so
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
}))
.then(() => {
svg.append('svg:foreignObject')
...
})
But since you don't return a promise (or anything for that matter) from the first then(), the second is unnecessary. You could just write
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
svg.append ...
}))
I use D3.js and I am able to filter out nodes by their "fill" attribute(in this case the color red) with the help of this code:
node.filter(function() {
return d3.select(this).attr("fill") === "red"
})
Now, my next goal is to filter them out by their radius, that means the "r" attribute like you can see below. Unfortunally the code does not filter them out. Is there someone who could help me maybe? Thanks so much!
node.filter(function() {
// return d3.select(this).attr("fill") === "red"
return d3.select(this).attr("r") === 12
})
Your problem here is the strict equality:
return d3.select(this).attr("r") === 12
//strict equality here--------^ ^--- this is a number
You are comparing the result of the getter attr("r") with a number. However, attr("r") returns a string, not a number.
The reason for that is easy to understand if you have a look at the source code: the getter uses getAttributeNS, which:
returns the string value of the attribute with the specified namespace and name. (emphasis mine)
Therefore, using the strict equality, you have "12" === 12, and that returns false.
Solution:
Use the equality (==) operator instead or, alternatively, compare with a string:
return d3.select(this).attr("r") === "12"
//this is a string ---------------^
Here is a simple demo. I'm filtering the circles with 12px radii...
var filteredCircles = circles.filter(function() {
return d3.select(this).attr("r") === "12"
}).attr("fill", "red")
... and painting them red. Check it:
var data = d3.range(9);
var svg = d3.select("svg");
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 10 + 30 * d
})
.attr("r", function(d) {
return d % 2 ? 12 : 8
})
.attr("fill", "teal");
var filteredCircles = circles.filter(function() {
return d3.select(this).attr("r") === "12"
}).attr("fill", "red")
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I am working on this pie-chart in D3.js.
This is the data:
DATA.JSON
[
{
"key":"amministrazione",
"categoria":"funzioni",
"val2015":404571081,
"val2013":374545999
},
{
"key":"sociale",
"categoria":"funzioni",
"val2015":235251679,
"val2013":258973653
},
{
"key":"territorio e ambiente",
"categoria":"funzioni",
"val2015":286164667,
"val2013":274949400
},
{
"key":"viabilità e trasporti",
"categoria":"funzioni",
"val2015":144185664,
"val2013":140619534
},
{
"key":"istruzione",
"categoria":"funzioni",
"val2015":168774925,
"val2013":170016208
},
{
"key":"cultura",
"categoria":"funzioni",
"val2015":55868045,
"val2013":55735535
},
{
"key":"sport",
"categoria":"funzioni",
"val2015":27219432,
"val2013":31244800
},
{
"key":"turismo",
"categoria":"funzioni",
"val2015":9544845,
"val2013":7674419
},
{
"key":"sviluppo economico",
"categoria":"funzioni",
"val2015":14790363,
"val2013":16635868
},
{
"key":"servizi produttivi",
"categoria":"funzioni",
"val2015":4334,
"val2013":4440
},
{
"key":"polizia locale",
"categoria":"funzioni",
"val2015":99007202,
"val2013":102065987
},
{
"key":"giustizia",
"categoria":"funzioni",
"val2015":12147068,
"val2013":12880138
},
{
"key":"anticipazioni di cassa",
"categoria":"rimborso prestiti",
"val2015":304323808,
"val2013":304323808
},
{
"key":"finanziamenti a breve termine",
"categoria":"rimborso prestiti",
"val2015":0,
"val2013":0
},
{
"key":"prestiti obbligazionari",
"categoria":"rimborso prestiti",
"val2015":38842996,
"val2013":36652213
},
{
"key":"quota capitale di debiti pluriennali",
"categoria":"rimborso prestiti",
"val2015":0,
"val2013":47152
},
{
"key":"quota capitale di mutui e prestiti",
"categoria":"rimborso prestiti",
"val2015":128508755,
"val2013":329885961
},
{
"key":"spese per conto terzi",
"categoria":"altro",
"val2015":232661261,
"val2013":236921438
},
{
"key":"disavanzo di amministrazione",
"categoria":"altro",
"val2015":0,
"val2013":0
}
]
It shows how the governmental budget is allocated to different functions (i.e. "key"). A value is given for each year (e.g. "val2015", "val2013") and each function is part of a macro-category (i.e. "funzioni", "rimborso prestiti", or "altro").
I am trying to create a color() function that dynamically changes its domain and range depending on:
the colorRange arbitrarily assigned as domain: greenRange for "funzioni", redRange for "rimborso prestiti" and blueRange for "altro"
the number of functions ("key") in each category that have a positive value, thus ignoring functions for which no resources were allocate during a given year. Done through the count() function (which works)
Then creates X number of shades for each ranging depending on the count() function of point 2
And assigns the appropriate color to each of the pie's wedges
This is my starting point:
var greenRange = ["rgb(199,233,192)", "rgb(0,68,27)"]; //range for the first 12 wedges of the pie (assuming they are all >0)
var redRange = ["rgb(252,187,161)", "rgb(103,0,13)"]; //range for the following 5 wedges of the pie (same assumption)
var blueRange = ["rgb(198,219,239)", "rgb(8,48,107)"]; //range for the last 3 wedges of the pie (same assumption)
I tried two options but neither works.
OPTION 1
function draw () {
//(1) count the number of data points with value > 0 in each category - This works well!
var countFunzioni=0;
dataset.forEach (function (d) {if (d.categoria=="funzioni" && d.val2015>0) { countFunzioni += 1;}})
var countRimborso=0;
dataset.forEach (function (d) {if (d.categoria=="rimborso prestiti" && d.val2015>0) { countRimborso += 1;}})
var countAltro=0;
dataset.forEach (function (d) {if (d.categoria=="altro" && d.val2015>0) { countAltro += 1;}})
//(2) create a color method for each category based on a the count calculated above and the range I determined
var colorFunzioni = d3.scale.linear()
.domain([0, countFunzioni])
.range(redRange);
var colorRimborso = d3.scale.linear()
.domain([0, countRimborso])
.range(redRange);
var colorAltro = d3.scale.linear()
.domain([0, countAltro])
.range(blueRange);
//draw the chart
chart = d3.select("#visualizationInner")
.append("svg")
.attr("id", "visualization")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
//draw and color the paths
var path = chart.datum(dataset).selectAll("path")
.data(pie)
.enter()
.append("path")
//(3) return the appropriate color method depending on the datum's category
.attr("fill", function(d, i) {
if (d.data.categoria=="funzioni") {return colorFunzioni(i);}
else if (d.data.categoria=="rimborso prestiti") {return colorRimborso(i);}
else if (d.data.categoria=="altro") {return colorAltro(i);}
})
.style("fill-opacity", 0.75)
.attr("d", arc);
}
Which returns this result:
This goes close, however it assigns a range of red colors to the first 12 wedges (which should get the greenRange instead) and no color to the wedges pertaining to the other categoreis
OPTION 2
function draw () {
//(1) same as above
//(2) create a color method that adapts to each category's count and range
var color = d3.scale.linear()
.domain([0, function (d) {
if (d.data.categoria=="funzioni") {return countFunzioni;}
else if (d.data.categoria=="rimborso prestiti") {return countRimborso;}
else if (d.data.categoria=="altro") {return countAltro;}
}])
.range(function (d) {
if (d.cdata.ategoria=="funzioni") {return greenRange;}
else if (d.data.categoria=="rimborso prestiti") {return redRange;}
else if (d.data.categoria=="altro") {return blueRange;}
});
////(3) return the appropriate color method depending on the datum's category
.attr("fill", function(d, i) {return color(i);}
}
This does not get any coloring done.
Any idea how to solve this?
Option1 Remarks:
.attr("fill", function(d, i) {
if (d.data.categoria=="funzioni") {return colorFunzioni(i);}
else if (d.data.categoria=="rimborso prestiti") {return colorRimborso(i);}
else if (d.data.categoria=="altro") {return colorAltro(i);}
})
The trouble is the above is written as if 'i' will maintain separate tallies for the three categories. It doesn't though, it keeps an index for all elements in your selection, and as soon as the first 12 items in the selections are done, the next items are going to be out of range of any of the scales you described and return "#000000" - this is why the first 12 are coloured (and the first 12 may be red because you assign the red range to two scales, and the green range isn't used) and the rest aren't.
As a quick fix, keep a tally in the data itself of where it occurs in each category like this:
dataset.forEach (function (d) {if (d.categoria=="altro" && d.val2015>0) { countAltro += 1; d.catIndex = countAltro; }})
do this for each category
and then in the fill attr function do:
else if (d.data.categoria=="altro") {return colorAltro(d.data.catIndex);}
and again that needs done for each category.
As a separate thing, you can get rid of those else-if's by assigning the colors like this:
var colorMap = {
funzioni: colorFunzioni,
altro: colorAltro,
"rimborso prestiti": colorRimborso
}
and then later doing
.attr("fill", function(d, i) {
var scale = colorMap[d.data.categoria];
if (scale) return scale(d.data.catIndex)
})