d3js data joins深入理解

世有因果知因求果發表於2017-07-16

Data joins

給定一個資料陣列和一個 D3 selection  我們就可以attach或者說是'join'陣列中的每個資料到selection中的每個元素上。

這將使得我們的資料和視覺化元素之間建立緊密的聯絡並實現視覺化成為可能。

比如如果我們有以下SVG的circles:

<circle r="40" />
<circle r="40" cx="120" />
<circle r="40" cx="240" />
<circle r="40" cx="360" />
<circle r="40" cx="480" />

和下面的data陣列:

var scores = [
  {
    "name": "Andy",
    "score": 25
  },
  {
    "name": "Beth",
    "score": 39
  },
  {
    "name": "Craig",
    "score": 42
  },
  {
    "name": "Diane",
    "score": 35
  },
  {
    "name": "Evelyn",
    "score": 48
  }
]

那麼我們就可以選中這些circles並且join data到這個selection:

d3.selectAll('circle')
  .data(scores);
隨後我們就可以對這個selection中的元素來設定相應屬性為繫結的資料
d3.selectAll('circle')
  .attr('r', function(d) {
    return d.score;
  });

上面的程式碼就將每個圓的半徑設定為每個人的score值的大小。(實際使用中,我們往往還會用到scaleSqrt比例尺,因為分數大小和視覺化的半徑大小往往需要一個對映!)

從這個形象化的結果,我們就實現了資料的視覺化:score值越大,圓的半徑就越大,因此只要看到圓大就知道其分數高!

Making a data join

var myData = [ 10, 40, 20, 30 ];

var s = d3.selectAll('circle');

s.data(myData);

陣列可以包含任何型別的資料,比如物件陣列:

var cities = [
  { name: 'London', population: 8674000},
  { name: 'New York', population: 8406000},
  { name: 'Sydney', population: 4293000}
];

var s = d3.selectAll('circle');

s.data(cities);

Data-driven modification of elements

d3.selectAll('circle')
  .attr('r', function(d) {
    return d;
  });

對每個selection中的element, d3都會呼叫.attr()函式, 傳入該element的joined data d. 而上面匿名函式的返回值作為r的值設定到該元素中.

再比如以下elements:

<circle />
<circle />
<circle />
<circle />
<circle />

和如下資料

var myData = [ 10, 40, 20, 30, 50 ];

我們執行一下data join:

var s = d3.selectAll('circle');

// Do the join
s.data(myData);
s.attr('r', function(d) {
  return d;
});

傳入 .attr() 的匿名函式將被呼叫 5 次 (selection中的每一個元素呼叫一次). 第一次呼叫時d 是10 因此第一個circle的r設定為10. 第二次呼叫時為40 因此第二個circle的r設定為40...

我們也可以對d做任何其他處理之後再返回,比如

s.attr('r', function(d) {
  return 2 * d;
});

再比如,如果元素繫結的資料大於40,我們則給該元素新增一個css類,以便更改這類元素的樣式。

s.classed('high', function(d) {
  return d >= 40; // returns true or false
});

最後我們來使用 i 引數來設定其cx的位置:

s.attr('cx', function(d, i) {
  return i * 120;
});

總結如下:

var myData = [ 10, 40, 20, 30, 50 ];

var s = d3.selectAll('circle');

// Do the data join
s.data(myData);

// Modify the selected elements
s.attr('r', function(d) {
  return d;
  })
  .classed('high', function(d) {
    return d >= 40;
  })
  .attr('cx', function(d, i) {
    return i * 120;
  });

Arrays of objects

var cities = [
  { name: 'London', population: 8674000},
  { name: 'New York', population: 8406000},
  { name: 'Sydney', population: 4293000},
  { name: 'Paris', population: 2244000},
  { name: 'Beijing', population: 11510000}
];

var s = d3.selectAll('circle');

s.data(cities);

這時當我們根據joined data來修改元素屬性時,d 將代表的是joined object. 這樣對第一個元素,其 d 將是 { name: 'London', population: 8674000}.

我們再來根據每個城市的population設定circle的半徑大小:

s.attr('r', function(d) {
    var scaleFactor = 0.000005;
    return d.population * scaleFactor;
  })
  .attr('cx', function(d, i) {
    return i * 120;
  });

當然,我們並不限於修改circle元素,加入我們還有rect和text元素,我們同樣可以使用該data join並且設定這些元素的屬性:

var cities = [
  { name: 'London', population: 8674000},
  { name: 'New York', population: 8406000},
  { name: 'Sydney', population: 4293000},
  { name: 'Paris', population: 2244000},
  { name: 'Beijing', population: 11510000}
];

// Join cities to rect elements and modify height, width and position
d3.selectAll('rect')
  .data(cities)
  .attr('height', 19)
  .attr('width', function(d) {
    var scaleFactor = 0.00004;
    return d.population * scaleFactor;
  })
  .attr('y', function(d, i) {
    return i * 20;
  })

// Join cities to text elements and modify content and position
d3.selectAll('text')
  .data(cities)
  .attr('y', function(d, i) {
    return i * 20 + 13;
  })
  .attr('x', -4)
  .text(function(d) {
    return d.name;
  });

Under the hood

當d3執行data join時,d3將對每個dom元素增加一個attribute: __data__,並且將對應的data賦值給該屬性。實際上,如果我們需要設定其他額外的資料的話,也可以在.each函式中通過.attr('custom_attr','data')來新增並且在後面使用。

 

使用這種方式來檢查是否有繫結好的資料,在需要除錯程式碼行為是否符合預期時是非常有用的。

如果我們的陣列長度大於或者小魚對應的selection元素將會發生什麼?

到目前為止,資料陣列的長度和元素的個數是相同的。往往現實並非如此,這時我們就需要了解enter和exit的概念了,我們在另外一篇部落格中描述。

What’s .datum for?

在有些情況下, (比如當處理地圖相關的視覺化時 geographic visualisations) ,我們可能需要給一個只含一個元素的selection繫結一個single data,比如:

var featureCollection = {type: 'FeatureCollection', features: features};
d3.select('path#my-map')
  .datum(featureCollection);

這將新增一個 __data__ attribute 到元素上並且 assigns the joined data (featureCollection )到這個元素上。可以檢視 geographic visualisations 

大多數情況下.data用於data join, .datum用於特定場景。