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!
Related
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) };
}
Im new in typescript and svg and d3, So I have this code
this.main
.selectAll(this.appCssConstants.sliceLabel.selectorName)
.data(root)
.enter()
.append("text")
.style("fill", "blue")
.classed(this.appCssConstants.sliceLabel.className, true)
// font size + slice padding
.attr("dy", (d, i) => {
return labelshift - d.depth * labelshiftmulti;
})
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d, i) => "#sliceLabel_" + i)
.text(dataPoint => dataPoint.data.name)
.attr("textLength")
.each(this.wrapPathText(labelPadding));
...
private wrapPathText(padding?: number): (slice: HierarchyRectangularNode<SunburstDataPoint>, index: number) => void {
const self = this;
return function () {
const selection: Selection<BaseType, any, BaseType, any> = d3Select(this);
const width = (<SVGPathElement>d3Select(selection.attr("xlink:href")).node()).getTotalLength();
self.wrapText(selection, padding, width);
};
}
private wrapText(selection: Selection<BaseType, any, BaseType, any>, padding?: number, width?: number): void {
let node: SVGTextElement = <SVGTextElement>selection.node(),
textLength: number = node.getComputedTextLength(),
text: string = selection.text();
width = width || 0;
padding = padding || 0;
while (textLength > (width - 2 * padding) && text.length > 0) {
text = text.slice(0, -1);
selection.text(text + "\u2026");
textLength = node.getComputedTextLength();
}
if (textLength > (width - 2 * padding)) {
selection.text("");
}
}
I have a loop ".each" in first part of the code that calls method wrapPathText
and wrapPathText have a for loop that takes long long time in the loop for slicing text
My question is - Is there a way to optimize this loop? For example removing more than one letter in each loopto fit the string?
Another question - Is there a way to fit a string with specific giving length (in my case 'width - 2 * padding') for example by the help of a specific attribute without using the unoptimized for loop?
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 have a array of values [date, value].
I can find the max value with:
max_value = d3.max(data_array, function(d) { return d[1]; } );
How do I get the d[0] value of d[1] maximum so I can mark the position of maximum value on chart?
Use javascript array filter function.
var max_value = d3.max(data_array, function(d) { return d[1]; } );
var requiredArr = data_array.filter(function(d){
return d[1]==max_value;
});
var dateWithMaxValue = requiredArr[0];
If you don't care about performance too much, here's a one-liner:
// d is [x, y] where x is what you're looking for
var d = data_array.sort(function (a, b) { return b[1] - a[1]; })[0];
Here's a simple demo.
Im just getting started on d3.js and was going through Nick's source code on github here and got stuck at the part where he is passing a function as data into d3.js.
The var x in the function assigned to next var gets incremented from 0 to the loop counter as i show in the jsbin link below. I cant quite wrap my head around how x gets incremented automatically and how does it know the loop counter that it needs to get incremented upto everytime.
the next variable is called from >> newdata from the >>render function ?
I just setup a jsbin here
This part:
.data(newData);
is simply going to call the newData function and bind the return to the selection.
So each call to render in the setInterval simply pushes the next function into his data array.
This part then:
selection.attr("class", "v-bar")
.style("height", function (d, i) {
return d(i) + "px"; // <- E
})
.select("span")
.text(function(d, i){
return d(i); // <- F
});
Calls d which is the next function for each element in the data array. It's passing the index position in the data array.
So the first render call is:
15 + 0 * 0;
Second is:
15 + 0 * 0;
15 + 1 * 1;
Third is:
15 + 0 * 0;
15 + 1 * 1;
15 + 2 * 2;
First, for simplification, this
var selection = d3.select("#container").selectAll("div")
.data(newData); // <- D
is just like writing
var arrayOfFunctions = newData();
var selection = d3.select("#container").selectAll("div")
.data(arrayOfFunctions); // <- D
So, for example, calling this code 3 times (via setInterval) builds up arrayOfFunctions like this:
arrayOfFunctions = [
function (x) { return 15 + x * x; },
function (x) { return 15 + x * x; },
function (x) { return 15 + x * x; }
]
(Note: it's not literally like that, because in actuality they're just pointers to the same function next)
So nothing about that increments x. But once it binds those functions to DOM elements (via data(arrayOfFunctions) and runs through this bit:
selection.attr("class", "v-bar")
.style("height", function (d, i) {
return d(i) + "px"; // <- E
})
d is function (x) { return 15 + x * x; } and i (which is 0, 1, or 2) is passed in as x to that function when it calls d(i).
And that's what essentially increments x.