Array.map () vs d3.selectAll (). Data.enter ()

I'm trying to figure out what is the use of using d3.selectAll.data.enter () to cycle through a dataset and plot.

var data = [4, 8, 15, 16, 23, 42]; var x = d3.scale.linear() .domain([0, d3.max(data)]) .range([0, 420]); let chartsvg = d3.select(".chart").append("svg"); chartsvg.selectAll("rect") .data(data) .enter() .append("rect") .attr("x", 0) .attr("y", function(d, i) { return 25*i; }) .attr("width", function(d) { return x(d); }) .attr("height", 20) .attr("fill", "#f3b562"); 

I see great benefits from d3 functions such as scale, axes, etc. But it seems that using Array.map () to cycle through a dataset, I can achieve the same functionality with much cleaner code and fewer lines, especially when I create a much more complex visualization, and not just such a histogram, like this.

  var data = [4, 8, 15, 16, 23, 42]; var x = d3.scale.linear() .domain([0, d3.max(data)]) .range([0, 420]); let chartsvg = d3.select(".chart").append("svg"); data.map(function(d, i){ chartsvg.append("rect") .attr("x", 0) .attr("y", 25*i) .attr("width", x(d)) .attr("height", 20) .attr("fill", "#f3b562"); }); 
+5
source share
1 answer

D3 stands for data-driven documents.

The most powerful feature in D3, which gives the name of the library, is its ability to bind data to DOM elements. By doing this, you can manipulate these DOM elements based on related data in several ways, for example (but not limited to):

  • Sorting
  • Filter
  • Translate
  • Style
  • Append
  • Delete

And so on...

If you don't bind data to DOM elements, for example, using the map() approach in your question (which is similar to forEach() ), you can keep a couple of lines at the beginning, but you end up with awkward code to deal with the latter. Let's get a look:

Map Approach ()

Here is a very simple code that uses most of your snippet to create a histogram using the map() approach:

 var h = 250, w = 500, p = 40; var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); var data = [{ group: "foo", value: 14, name: "A" }, { group: "foo", value: 35, name: "B" }, { group: "foo", value: 87, name: "C" }, { group: "foo", value: 12, name: "D" }, { group: "bar", value: 84, name: "E" }, { group: "bar", value: 65, name: "F" }, { group: "bar", value: 34, name: "G" }, { group: "baz", value: 98, name: "H" }, { group: "baz", value: 12, name: "I" }, { group: "baz", value: 43, name: "J" }, { group: "baz", value: 66, name: "K" }, { group: "baz", value: 42, name: "L" }]; var color = d3.scaleOrdinal(d3.schemeCategory10); var xScale = d3.scaleLinear() .range([0, w - p]) .domain([0, d3.max(data, function(d) { return d.value })]); var yScale = d3.scaleBand() .range([0, h]) .domain(data.map(function(d) { return d.name })) .padding(0.1); data.map(function(d, i) { svg.append("rect") .attr("x", p) .attr("y", yScale(d.name)) .attr("width", xScale(d.value)) .attr("height", yScale.bandwidth()) .attr("fill", color(d.group)); }); var axis = d3.axisLeft(yScale); var gY = svg.append("g").attr("transform", "translate(" + p + ",0)") .call(axis); 
 <script src="https://d3js.org/d3.v4.min.js"></script> 

It seems to be a good result, all the bars are there. However, the data is not tied to these rectangular boxes. Save this code, we will use it in the next task.

Enter options

Now let's try the same code, but using the idiomatic "enter" option:

 var h = 250, w = 500, p = 40; var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); var data = [{ group: "foo", value: 14, name: "A" }, { group: "foo", value: 35, name: "B" }, { group: "foo", value: 87, name: "C" }, { group: "foo", value: 12, name: "D" }, { group: "bar", value: 84, name: "E" }, { group: "bar", value: 65, name: "F" }, { group: "bar", value: 34, name: "G" }, { group: "baz", value: 98, name: "H" }, { group: "baz", value: 12, name: "I" }, { group: "baz", value: 43, name: "J" }, { group: "baz", value: 66, name: "K" }, { group: "baz", value: 42, name: "L" }]; var color = d3.scaleOrdinal(d3.schemeCategory10); var xScale = d3.scaleLinear() .range([0, w - p]) .domain([0, d3.max(data, function(d) { return d.value })]); var yScale = d3.scaleBand() .range([0, h]) .domain(data.map(function(d) { return d.name })) .padding(0.1); svg.selectAll(null) .data(data, function(d) { return d.name }) .enter() .append("rect") .attr("x", p) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }) .attr("height", yScale.bandwidth()) .attr("fill", function(d) { return color(d.group) }); var axis = d3.axisLeft(yScale); var gY = svg.append("g").attr("transform", "translate(" + p + ",0)") .call(axis); 
 <script src="https://d3js.org/d3.v4.min.js"></script> 

As you can see, it is slightly longer than the previous map() method, 2 lines longer.

However, it actually associates data with these rectangular boxes. If you specify in the D3 console the selection of one of these rectangular boxes, you will see something like this (in Chrome):

 > Selection > _groups: Array(1) > 0: Array(1) > 0: rect > __data__: Object group: "bar" name: "G" value: 34 

Since this code actually associates data with DOM elements, you can manipulate it in a way that will be cumbersome (to say the least) using the map() approach. I will show this in the next fragment, which will be used to propose a problem.

call

Since your question speaks of cleaner code and fewer lines, you are faced with a task.

I created 3 buttons, one for each group in the data array (and a fourth for all groups). When you click a button, it filters the data and updates the chart accordingly:

 var h = 250, w = 500, p = 40; var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); var g1 = svg.append("g") var g2 = svg.append("g") var data = [{ group: "foo", value: 14, name: "A" }, { group: "foo", value: 35, name: "B" }, { group: "foo", value: 87, name: "C" }, { group: "foo", value: 12, name: "D" }, { group: "bar", value: 84, name: "E" }, { group: "bar", value: 65, name: "F" }, { group: "bar", value: 34, name: "G" }, { group: "baz", value: 98, name: "H" }, { group: "baz", value: 12, name: "I" }, { group: "baz", value: 43, name: "J" }, { group: "baz", value: 66, name: "K" }, { group: "baz", value: 42, name: "L" }]; var color = d3.scaleOrdinal(d3.schemeCategory10); var xScale = d3.scaleLinear() .range([0, w - p]) .domain([0, d3.max(data, function(d) { return d.value })]); var yScale = d3.scaleBand() .range([0, h]) .domain(data.map(function(d) { return d.name })) .padding(0.1); var axis = d3.axisLeft(yScale); var gY = g2.append("g").attr("transform", "translate(" + p + ",0)") .call(axis); draw(data); function draw(data) { yScale.domain(data.map(function(d) { return d.name })) var rects = g1.selectAll("rect") .data(data, function(d) { return d.name }) rects.enter() .append("rect") .attr("x", p) .attr("y", function(d) { return yScale(d.name) }) .attr("width", 0) .attr("height", yScale.bandwidth()) .attr("fill", function(d) { return color(d.group) }) .transition() .duration(1000) .attr("width", function(d) { return xScale(d.value) }); rects.transition() .duration(1000) .attr("x", p) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }) .attr("height", yScale.bandwidth()) .attr("fill", function(d) { return color(d.group) }); rects.exit() .transition() .duration(1000) .attr("width", 0) .remove(); gY.transition().duration(1000).call(axis); }; d3.selectAll("button").on("click", function() { var thisValue = this.id; var newData = thisValue === "all" ? data : data.filter(function(d) { return d.group === thisValue; }); draw(newData) }); 
 <script src="https://d3js.org/d3.v4.min.js"></script> <button id="foo">Foo</button> <button id="bar">Bar</button> <button id="baz">Baz</button> <button id="all">All</button> <br> <br> 

Cleaner code is opinion based, but we can easily measure its size.

So here is the problem: try creating code that does the same thing, but using the map() approach, that is, without binding any data. Do all the transitions that I am doing here. The code you try to recreate is all the code inside the on("click") function.

After that, we compare the size of your code and the size of the idiomatic options "enter", "update" and "exit".

Chalenge # 2

This task number 2 can be even more interesting to demonstrate the capabilities of D3 when it comes to data binding.

In this new code, I sort the original data array after 1 second and redraw the chart. Then, by clicking the refresh button, I associate another data array with bars.

The good thing here is the key function that links each lane to each data point, using, in this case, the name property:

 .data(data, function(d) { return d.name }) 

Here is the code, wait 1 second before clicking "update":

 var h = 250, w = 500, p = 40; var svg = d3.select("body") .append("svg") .attr("width", w) .attr("height", h); var data2 = [{ group: "foo", value: 10, name: "A" }, { group: "foo", value: 20, name: "B" }, { group: "foo", value: 30, name: "C" }, { group: "foo", value: 40, name: "D" }, { group: "bar", value: 50, name: "E" }, { group: "bar", value: 60, name: "F" }, { group: "bar", value: 70, name: "G" }, { group: "baz", value: 80, name: "H" }, { group: "baz", value: 85, name: "I" }, { group: "baz", value: 90, name: "J" }, { group: "baz", value: 95, name: "K" }, { group: "baz", value: 100, name: "L" }]; var data = [{ group: "foo", value: 14, name: "A" }, { group: "foo", value: 35, name: "B" }, { group: "foo", value: 87, name: "C" }, { group: "foo", value: 12, name: "D" }, { group: "bar", value: 84, name: "E" }, { group: "bar", value: 65, name: "F" }, { group: "bar", value: 34, name: "G" }, { group: "baz", value: 98, name: "H" }, { group: "baz", value: 12, name: "I" }, { group: "baz", value: 43, name: "J" }, { group: "baz", value: 66, name: "K" }, { group: "baz", value: 42, name: "L" }]; var color = d3.scaleOrdinal(d3.schemeCategory10); var xScale = d3.scaleLinear() .range([0, w - p]) .domain([0, d3.max(data, function(d) { return d.value })]); var yScale = d3.scaleBand() .range([0, h]) .domain(data.map(function(d) { return d.name })) .padding(0.1); svg.selectAll(".bars") .data(data, function(d) { return d.name }) .enter() .append("rect") .attr("class", "bars") .attr("x", p) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }) .attr("height", yScale.bandwidth()) .attr("fill", function(d) { return color(d.group) }) var axis = d3.axisLeft(yScale); var gY = svg.append("g").attr("transform", "translate(" + p + ",0)") .call(axis); setTimeout(function() { data.sort(function(a, b) { return d3.ascending(a.value, b.value) }); yScale.domain(data.map(function(d) { return d.name })); svg.selectAll(".bars").data(data, function(d) { return d.name }) .transition() .duration(500) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }); gY.transition().duration(1000).call(axis); }, 1000) d3.selectAll("button").on("click", function() { svg.selectAll(".bars").data(data2, function(d) { return d.name }) .transition() .duration(500) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }); gY.transition().duration(1000).call(axis); }) 
 <script src="https://d3js.org/d3.v4.min.js"></script> <button>Update</button> <br> <br> 

Your task here is the same: change the code inside .on("click") , and that is just the way ...

 svg.selectAll(".bars").data(data2, function(d) { return d.name }) .transition() .duration(500) .attr("y", function(d) { return yScale(d.name) }) .attr("width", function(d) { return xScale(d.value) }); gY.transition().duration(1000).call(axis); 

... to code that does the same , but for your approach map() .

Keep in mind that since I sorted the columns, you can no longer change them by the index of the data array!

Conclusion

The map() approach can save you 2 lines the first time you draw elements. However, this will make things terribly cumbersome.

+9
source

Source: https://habr.com/ru/post/1268528/


All Articles