Apply mapping to attached data - d3.js

I have a bunch of data attached to DOM elements, and I'd like to double each one. My naive approach was to select them all and call data with a function on the selection. I thought this would be sufficient since the data is stored on the elements rather than the selection itself.
However, this doesn't do what I want at all. Bizarrely it results in a selection with the data [undefined] rather than [2, 4, 6] and doesn't affect the data attached to the elements. Even stranger, if I call it with the identity function I get an exception: Cannot read property 'length' of undefined
What's going on here? Does D3 only allow data to be called with a function on an enter/exit selection?
// Create a bunch of elements with attached data.
const selection = d3.selectAll('p').data([1,2,3]).enter().append('p');
console.log(selection.data());
// This doesn't work, but seems like it should:
selection.data(d => d * 2);
console.log(selection.data());
// This throws an exception??
selection.data(d => d);
console.log(selection.data());
// Is this the simplest way that works?
selection.data(selection.data().map(d => d * 2));
console.log(selection.data());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>

The first two examples you have are quite interesting.
While the first example might seem to be a workable solution:
selection.data(d => d * 2);
console.log(selection.data());
It won't work as expected. First, .data expects an array but it appears as though you are trying to pass each item individually. But also, d here is undefined, the function passed to .data() is called only once, not for each element in the selection. So there are no individual datums to work with in this example (you could have a datum to work with here in a nested selection (see docs)), I've only changed the above to log d and we can see it's undefined:
// Create a bunch of elements with attached data.
const selection = d3.selectAll('p').data([1,2,3]).enter().append('p');
// This doesn't work, but seems like it should:
selection.data(function(d) { console.log(d); return d * 2; })
console.log(selection.data());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Why does the second example throw an error while the first doesn't? When multiplying undefined by 2 we get NaN, within the data method the method looks to determine the input data array's length, on NaN length is undefined, on undefined length is an error, I believe this is most likely why one produces and error and the other doesn't:
console.log(NaN.length);
console.log((undefined * 2).length);
console.log(undefined.length);
The non valid lengths also likely explain why no exit selection is created and the data isn't updated: the size of the exit selection is based on the size of the input data array. Data remains unchanged in the first example because within d3-selection the for loop to update the data never loops over the data as the for condition is never met:
for (var i = 0; i < dataLength; ++i) { // bind data
D3 is more designed to bind (new or updated) data to elements rather than to directly manipulate the data bound to those elements. But you can achieve the result you want, either as indicated in your third option, or from the get go with Xavier's comment, alternatively you could write a simple function and pass it to selection.each() (though there are many other solutions too):
// Create a bunch of elements with attached data.
const selection = d3.selectAll('p').data([1,2,3])
.enter()
.append('p')
.each(double);
function double() {
d3.select(this).datum(d => d*2);
}
console.log(selection.data());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>

Related

How we can slice elements fetched using cy.get() and then run tests on those specific elements only?

cy.get('li').slice(0,5)
The above line of code doesn't work as it Property 'slice' does not exist on type 'Chainable<JQuery>'
Is there a way to slice elements that we have fetched using cy.get()
Cypress's cy.get() yields a Chainable, which allows Cypress to easily pass elements (and other things) between Cypress commands. To use traditional JS functions, you'll just need to use .then().
cy.get('li').then(($els) => {
const $sliced = $els.slice(0, 5);
// whatever you need to do with $sliced.
// to easily iterate over these, do something like the following
cy.wrap($sliced) // add the $sliced variable into the Cypress chain
.each(($el) => {
// whatever needs to be tested on each element
});
});
As a small tweak to #agoff's answer, you can use the .invoke() command to use slice method on your jQuery array, where the first argument will be the method name and the following will be the method arguments.
cy.get('li')
.invoke('slice', '0', '5')
.should('have.length', 5)
Here is a working example.
There are a couple of other ways to do it,
add pseudo-selector :lt(5) to take first 5 items
cy.get('li:lt(5)')
apply a filter to the set
cy.get('li')
.filter((index,el) => index < 5)

Using RXJS like a cascaded forEach loop?

How is it possible with RXJS to make a cascaded forEach loop? Currently, I have 4 observables containing simple string lists, called x1 - x4. What I want to achieve now is to run over all variation and to call a REST-Api with an object of variation data. Usually, I would do something like that with a forEach, but how to do with RXJS? Please see the abstracted code:
let x1$ = of([1,2]);
let x2$ = of([a,b,c,d,e,f]);
let x3$ = of([A,B,C,D,E,F]);
let x4$ = of([M,N,O,P]);
x1$.forEach(x1 => {
x2$.forEach(x2 => {
x3$.forEach(x3 => {
x4$.forEach(x4 => {
let data = {
a: x1,
b: x2,
c: x3,
d: x4
}
return this.restService.post('/xxxx', data)
})
})
})
})
Is something like that possible with RXJS in an elegant way?
Let's assume you have a function combineLists which represent the plain-array version of the logic to turn static lists into an array of request observables:
function combineLists(lists: unknown[][]) {
const [x1s, x2s, x3s, x4s] = lists;
// Calculate combinations, you can also use your forEach instead
const combinations = x1s
.flatMap(a => x2s
.flatMap(b => x3s
.flatMap(c => x4s
.flatMap(d => ({a, b, c, d})))));
return combinations.map(combination => this.restService.post('/xxxx', combination));
}
Since your input observables are one-offs as well, we can use e.g. forkJoin. This waits for all of them to complete and then runs with their respective plain values. At this point you're back to computing the combinations with your preferred method.
forkJoin([x1$, x2$, x3$, x4$]).pipe(
map(combineLists),
);
Assuming your REST call is typed to return T, the above produces Observable<Observable<T>[]>. How you proceed from here depends on what data structure you're looking for / how you want to continue working with this. This didn't seem to be part of your question anymore, but I'll give a couple hints nonetheless:
If you want a Observable<T>, you can just add e.g. a mergeAll() operator. This observable will just emit the results of all individual requests after another in whichever order they arrive.
forkJoin([x1$, x2$, x3$, x4$]).pipe(
map(combineLists),
mergeAll(),
);
If you want an Observable<T[]> instead, which collects the results into a single emission, you could once again forkJoin the produced array of requests. This also preserves the order.
forkJoin([x1$, x2$, x3$, x4$]).pipe(
map(combineLists),
switchMap(forkJoin),
);
Some words of caution:
Don't forget to subscribe to make it actually do something.
You should make sure to handle errors on all your REST calls. This must happen right at the call itself, not after this entire pipeline, unless you want one single failed request to break the entire pipe.
Keep in mind that forkJoin([]) over an empty array doesn't emit anything.
Triggering a lot of requests like this probably means the API should be changed (if possible) as the number of requests grows exponentially.

If you specify a data method the second argument in D3.js, existing elements are undefined

If you specify a data method the second argument in D3.js, existing elements are undefined
<div id="hoge">
<p>C</p>
<p>A</p>
<p>Z</p>
<p>X</p>
</div>
<script>
dataset = [4,9];
d3.select("#hoge").selectAll("p").data(dataset, function(d,i){
console.log(d);
return d;
})
.enter()
.append("p")
.text(function(d){
return d;
})
console.log
UNDEFINED
UNDEFINED
UNDEFINED
UNDEFINED
4
9
Why UNDEFINED?
Why not a following?
C
A
Z
X
4
9
It still wasn't clear to me when I read the #Ben Lyall answer (sorry Ben, its not you, it's me...) and it's certainly not clear to me from reading the wiki (I think they got it backwards), so I traced through the code to see what's going on...
First of all, it's worth remembering that a selection is an array of groups and a group is an array of nodes. If there is more than one group, the same data is bound to all groups.
When binding data to each group, d3 invokes the key function in two separate phases, first to build an associative array for all the nodes in the group, using the returned value from the key function as the key and second to derive a key for each data array element. This is then used to look up the array of nodes and if the look-up succeeds, the data element is bound to its matching node and the node is added to the update selection.
In the first phase the key is invoked once for every node in the group with the this context set to the node. The d argument is the bound data on the node and the i argument is the index of the node in the group. The value returned for each node and the node itself are added to a collection of key-value pairs with the node as value and the return value of the key function as the key.
In the second phase the key is invoked once for every element in the first dimension of the data array with the this context set to the data array. The d argument is the element of the data array and the i argument is the index of the element in the data array. The value returned is used to look up the collection and if a match is found, that node is put in the update selection.
Hence, as #Ben Lyall said, the d in the key function, in terms of defining the keys, is the data previously bound on the node. (d3 adds a __data__ member on the nodes). When the key is used to qualify data elements, d is the data array element. That's why you see undefined for the four nodes and the expected value for the two data elements.
That being said, there is no reason that the key has to be a function of d.
Try this...
<div id="hoge">
<p>C</p>
<p>A</p>
<p>Z</p>
<p>X</p>
</div>
<script>
dataset = [4, 9];
d3.select("#hoge").selectAll("p").data(dataset, function (d, i) {
d = d || this.textContent;
console.log(d);
return d;
})
.enter()
.append("p")
.text(function (d) {
return d;
})
</script>
In phase1, d is the bound data, which is undefined, so the key function returns the text of the nodes. In phase2, d is the data values so they are returned. Because none of the keys in phase2 match the ones in phase1, there is no update group.
Just to be perverse, you can also confuse it with this...
d = d || dataset[i];
Because the key function returns the same values for i = 0 and 1 in both phases, the first 2 nodes are considered update nodes and because all of the data is bound according to the key function, the enter group is empty.
But the most useful case is if you have data like this...
dataset = [4, 9, 'X', 'C'];
d3.select("#hoge").selectAll("p").data(dataset, function (d, i) {
d = d || this.textContent;
console.log(d);
return d;
}).attr('class', 'update')
.enter()
.append("p").attr('class', 'enter')
.text(function (d) {
return d;
})
this is the result...
<div id="hoge">
<p class="update">C</p>
<p>A</p>
<p>Z</p>
<p class="update">X</p>
<p class="enter">4</p>
<p class="enter">9</p>
</div>
In phase1, all of the nodes are included in the collection with their text content as key. In phase2, the data values are used to look up the collection and there is a match on C and X, so they are in the update collection and the numbers, which have no match, are in the enter selection.
Because if you specify a second argument to the .data function, the result of that argument will be the value that is bound to the element. With no second argument, the data is bound by index. Because you're returning d, it will be bound by value. Since there is no data bound to the existing p elements, their data is undefined, which doesn't match any values in your dataset array, so it therefore treats all the data in your dataset array as new values, creating new placeholder p elements for them. The undefined ones will be in the exit selection.
See https://github.com/mbostock/d3/wiki/Selections#data for further explanation.
You should probably just use .data(dataset) and then this would bound data to the first two paragraphs. Remaining two elements should be available in .exit(), so you can .remove() them or something else (maybe add some other data to them, like default data).

Returning other values from d3.call

Per the docs, "The call operator always returns the current selection, regardless of the return value of the specified function." I'd like to know if there is a variant of call or reasonable workaround for getting call-behavior that returns values other than the selection.
Motivation:
I've got a chart and a datebrush, each encapsulated in a function
function trends_datebrush() {
// Setup
function chart(_selection) {
_selection.each(function(_data) {
// Do things
...});
}
return chart;
};
(The chart follows a similar format but isn't called datebrush).
These are instantiated with:
d3.select("someDiv")
.datum("data")
.call(trends_datebrush());
// And then we call the chart
I'd like to return a subselection from brush to be used as the data variable in the chart call. As is I need to make them both aware of some higher order global state, which gets messy especially since I want other control functions to drill down on the data. If I could override call, then I could do something like
d3.select("someDiv")
.datum("data")
.call(trends_datebrush())
.call(trends_chart());
And then if I were to implement some new filter I could throw it into the chain with another call statement.
tl;DR: Looking for ways to get chain chart calls s.t. they can pass transformed data to each other. I want monadic D3 charts! Except I don't really know monads so I might be misusing the word.

Order $each by name

I am trying to figure why my ajax $each alters the way my list of names gets printed?
I have an json string like this:
[{"name":"Adam","len":1,"cid":2},{"name":"Bo","len":1,"cid":1},{"name":"Bob","len":1,"cid":3},{"name":"Chris","len":1,"cid":7},{"name":"James","len":1,"cid":5},{"name":"Michael","len":1,"cid":6},{"name":"Nick","len":1,"cid":4},{"name":"OJ","len":1,"cid":8}]
Here all the names are sorted in alphabetic order, but when getting them out they are sorted by "cid"? Why, and how can I change this?
Here is my jQuery:
var names = {};
$.getJSON('http://mypage.com/json/names.php', function(data){
$.each(data.courses, function (k, vali) {
names[vali.cid] = vali.name;
});
});
I guess its because "names[vali.cid]", but I need that part to stay that way. Can it still be done?
Hoping for help and thanks in advance :-.)
Ordering inside an object is not really defined or predictable when you iterate over it. I would suggest sorting the array based on an internal property:
var names = [];
$.getJSON('http://mypage.com/json/names.php', function(data){
$.each(data.courses, function (k, vali) {
names.push({name: vali.name, cid: vali.cid});
});
names.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
});
Now you have an array that is ordered and you can iterate over it in a predictable order as well.
There is no "ajax $each" - you probably mean the jQuery function.
With "when getting them out" I presume you mean something like console.debug(names) after your $each call
Objects aren't ordered in javascript per definition, so there is no more order in your variable "names". Still, most javascript implementations today (and all the ones probably important to you - the ones used in the most used browsers) employ a stable order in objects which normally depends on the order you insert stuff.
All this said, there can probably be 3 reasons you're not getting what you're expecting:
Try console.debug(data) and see what you get - the order as you want it?
As you don't explicitly state how you debug your stuff, the problem could be in the way you output and not the data is stored. Here too try console.debug(names).
You're using a function which dereferences on expection, like console.*. This means if you console.debug() an object, the displayed values will depend on the moment you unfold the displayed tree in your browser, not when the line was called!

Resources