首页 > 解决方案 > d3 访问器函数迭代

问题描述

这是我的数据。

let data = [
    {
      "sample_date": "2017-07-04T00:00:00.000Z",
      "ubiome": [
            {
              "count_norm": 1283,
              "tax_name": "Bacteroides fragilis",
              "tax_rank": "species"
            },
            {
              "count_norm": 3708,
              "tax_name": "Bacteroides thetaiotaomicron",
              "tax_rank": "species"
            },
            {
              "count_norm": 731,
              "tax_name": "Bacteroides uniformis",
              "tax_rank": "species"
            },
            {
              "count_norm": 62226,
              "tax_name": "Bacteroides vulgatus",
              "tax_rank": "species"
            },
            {
              "count_norm": 2139,
              "tax_name": "Parabacteroides distasonis",
              "tax_rank": "species"
            }
      ]
    },
    {
      "sample_date": "2017-07-04T00:00:00.000Z",
      "ubiome": [
        {
          "count_norm": 1283,
          "tax_name": "Bacteroides fragilis",
          "tax_rank": "species"
        },
        {
          "count_norm": 3708,
          "tax_name": "Bacteroides thetaiotaomicron",
          "tax_rank": "species"
        },
        {
          "count_norm": 731,
          "tax_name": "Bacteroides uniformis",
          "tax_rank": "species"
        },
        {
          "count_norm": 62226,
          "tax_name": "Bacteroides vulgatus",
          "tax_rank": "species"
        },
        {
          "count_norm": 2139,
          "tax_name": "Parabacteroides distasonis",
          "tax_rank": "species"
        }
      ]
    }
]

我正在尝试使用 d3 来构建热图。我知道我需要使用访问器函数来使用样本日期以及 tax_name 和 count_norm 来绘制 x、y 和 rect 值。我只是无法获得访问器功能来超越数据的第一级......

var cells = svg.selectAll('rect')
      .data(data)
      .enter().append('g').append('rect')
      .attr('class', 'cell')
      .attr('width', cellSize)
      .attr('height', cellSize)
      .attr('y', function(d) { return yScale(d.name); })
      .attr('x', function(d) { return xScale(d.rank); })
      .attr('fill', function(d) { return color(d.value); });

如何构建访问器函数来获取更多嵌套数据?

这是关于我所在位置的更新。我无法让 x & y 位置在数据中工作。

let data = [{
    "sample_date": "2017-07-04T00:00:00.000Z",
    "ubiome": [{
        "count_norm": 1283,
        "tax_name": "Bacteroides fragilis",
        "tax_rank": "species"
      },
      {
        "count_norm": 3708,
        "tax_name": "Bacteroides thetaiotaomicron",
        "tax_rank": "species"
      },
      {
        "count_norm": 731,
        "tax_name": "Bacteroides uniformis",
        "tax_rank": "species"
      },
      {
        "count_norm": 62226,
        "tax_name": "Bacteroides vulgatus",
        "tax_rank": "species"
      },
      {
        "count_norm": 2139,
        "tax_name": "Parabacteroides distasonis",
        "tax_rank": "species"
      }
    ]
  },
  {
    "sample_date": "2017-07-10T00:00:00.000Z",
    "ubiome": [{
        "count_norm": 1200,
        "tax_name": "Bacteroides Noway",
        "tax_rank": "species"
      },
      {
        "count_norm": 3700,
        "tax_name": "Bacteroides thetaiotaomicron",
        "tax_rank": "species"
      },
      {
        "count_norm": 700,
        "tax_name": "Bacteroides uniformis",
        "tax_rank": "species"
      },
      {
        "count_norm": 62000,
        "tax_name": "Bacteroides vulgatus",
        "tax_rank": "species"
      },
      {
        "count_norm": 2100,
        "tax_name": "Parabacteroides distasonis",
        "tax_rank": "species"
      }
    ]
  }
];

var dates = [];
var ubiomeonly = [];

var itemSize = 30,
  cellSize = itemSize - 1,
  margin = {
    top: 120,
    right: 20,
    bottom: 20,
    left: 110
  };

var width = 750 - margin.right - margin.left,
  height = 500 - margin.top - margin.bottom;

for (i = 0; i < data.length; i++) {
  var adate = moment(data[i].sample_date).format("YYYY-MM-DD")
  dates.push(adate);
};

var bacteria = [];
for (i = 0; i < data.length; i++) {
  bacteria.push(data[i].ubiome.slice(0, data[i].ubiome.length));
}
var bacteriaList = d3.merge(bacteria).map(function(d) {
  return d.tax_name
});
bacteriaList = d3.set(bacteriaList).values();

var x_elements = dates,
  y_elements = bacteriaList;

var xScale = d3.scaleOrdinal()
  .domain(x_elements)
  .range([0, x_elements.length * itemSize]);

var xAxis = d3.axisTop()
  .scale(xScale)
  .tickFormat(function(d) {
    return d;
  });

var yScale = d3.scaleOrdinal()
  .domain(y_elements)
  .range([0, y_elements.length * itemSize]);

var yAxis = d3.axisLeft()
  .scale(yScale)
  .tickFormat(function(d) {
    return d;
  });

var colorScale = d3.scaleThreshold()
  .domain([0, 10000])
  .range(["#2980B9", "#E67E22", "#27AE60", "#27AE60"]);

var svg = d3.select('#heatmap')
  .data(data)
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var cells = svg.selectAll('rect')
  .data(function(d) {
    return d.ubiome;
  })
  .enter().append('g').append('rect')
  .attr('class', 'cell')
  .attr('width', cellSize)
  .attr('height', cellSize)
  .attr('y', function(d, i) {
    return yScale(d + i);
  })
  .attr('x', function(d) {  return xScale(d.sample_date); })
  .attr('fill', function(d) {
    return colorScale(d.count_norm);
  });

svg.append("g")
  .attr("class", "y axis")
  .call(yAxis)
  .selectAll('text')
  .attr('font-weight', 'normal');

svg.append("g")
  .attr("class", "x axis")
  .call(xAxis)
  .selectAll('text')
  .attr('font-weight', 'normal')
  .style("text-anchor", "start")
  .attr("dx", ".8em")
  .attr("dy", ".5em")
  .attr("transform", function(d) {
    return "rotate(-65)";
  });
<!DOCTYPE html>
<html>

<head>
  <title></title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta charset="utf-8">

</head>

<body>
  <div id="wrapper">
    <h1>uBiome Bacterial Counts</h1>
    <div id="heatmap"></div>
  </div>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
</body>

</html>

标签: javascriptobjectd3.jsmultidimensional-array

解决方案


您需要使用嵌套选择。通过两个selectAll().data().enter()循环,我们可以在父元素中附加嵌套的子元素——每个元素都有自己的数据。只有一个selectAll().data().enter(),我们只是在 DOM 中为数据数组中的每个项目创建元素。数据数组中的每个项目都可能有一些属性,它本身就是一个数组(或者本身就是一个数据数组)——但实际上你还没有对子数组做任何事情。

由于您的数组中只有两个项目,因此只会创建两个元素。由于这些项目都没有nameorrank属性,因此访问这些属性将导致未定义。

这是上面代码的简化示例,我p为示例数据数组中的每个项目附加了一个。每个p都将其文本设置为该元素的基准。嵌套数据仍然只是每个数据的属性p。由于您的示例数据数组有两个项目,因此只创建了两个元素:

let data = [{"sample_date":"2017-07-04T00:00:00.000Z","ubiome":[{"count_norm":1283,"tax_name":"Bacteroidesfragilis","tax_rank":"species"},{"count_norm":3708,"tax_name":"Bacteroidesthetaiotaomicron","tax_rank":"species"},{"count_norm":731,"tax_name":"Bacteroidesuniformis","tax_rank":"species"},{"count_norm":62226,"tax_name":"Bacteroidesvulgatus","tax_rank":"species"},{"count_norm":2139,"tax_name":"Parabacteroidesdistasonis","tax_rank":"species"}]},{"sample_date":"2017-07-04T00:00:00.000Z","ubiome":[{"count_norm":1283,"tax_name":"Bacteroidesfragilis","tax_rank":"species"},{"count_norm":3708,"tax_name":"Bacteroidesthetaiotaomicron","tax_rank":"species"},{"count_norm":731,"tax_name":"Bacteroidesuniformis","tax_rank":"species"},{"count_norm":62226,"tax_name":"Bacteroidesvulgatus","tax_rank":"species"},{"count_norm":2139,"tax_name":"Parabacteroidesdistasonis","tax_rank":"species"}]}];

var body = d3.select("body");

body.selectAll("p")
  .data(data)
  .enter()
  .append("p")
  .text(function(d) { return JSON.stringify(d); })
  .style("background-color", function(d,i)  { return ["yellow","skyblue"][i]; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

在您的示例中,您在示例中使用了嵌套元素, arect在 a 中,但是您将每个g元素附加一个(否则您将需要使用另一个输入循环)并且共享与 相同的数据,这不会创建具有所需基准的元素。rectgrectg

现在我们有两个(即将成为父级)元素,其基准具有嵌套数据作为某些属性,我们可以为每个父级输入新元素。为此,我们现在可以selectAll().data().enter()使用这两个元素进行循环。请记住,d以下是每个 的基准p

var parents = body.selectAll("p")
 .data(data)
 .enter()
 .append("p");

var children = parents.selectAll("span")
 .data(function(d) { return d.ubiome; }) // d is the parent datum here : {"sample_date":"time","ubiome":[child,child]}
 .enter()
 .append("span")
 .attr("x", function(d) { })  // d is the child datum here

在这里,我们根据每个父母的特定数据为父母创建一个选择的孩子。现在我们可以通过访问父数据来使用包含每个孩子信息的属性。下面的代码片段p再次为每个父级创建一个父级(再次按颜色区分),子级选择使用父级的基准并span为每个子级创建一个(按边框区分)。我已经设置了每个跨度以显示其基准。这给了我们两个 parent p,每个都有五个 child spans

let data = [{"sample_date":"2017-07-04T00:00:00.000Z","ubiome":[{"count_norm":9876,"tax_name":"Bacteroidesfragilis","tax_rank":"species"},{"count_norm":3708,"tax_name":"Bacteroidesthetaiotaomicron","tax_rank":"species"},{"count_norm":731,"tax_name":"Bacteroidesuniformis","tax_rank":"species"},{"count_norm":62226,"tax_name":"Bacteroidesvulgatus","tax_rank":"species"},{"count_norm":2139,"tax_name":"Parabacteroidesdistasonis","tax_rank":"species"}]},{"sample_date":"2017-07-04T00:00:00.000Z","ubiome":[{"count_norm":1283,"tax_name":"Bacteroidesfragilis","tax_rank":"species"},{"count_norm":3708,"tax_name":"Bacteroidesthetaiotaomicron","tax_rank":"species"},{"count_norm":731,"tax_name":"Bacteroidesuniformis","tax_rank":"species"},{"count_norm":62226,"tax_name":"Bacteroidesvulgatus","tax_rank":"species"},{"count_norm":2139,"tax_name":"Parabacteroidesdistasonis","tax_rank":"species"}]}];

var body = d3.select("body");

var parents = body.selectAll("p")
  .data(data)
  .enter()
  .append("p")
  .style("background-color", function(d,i) { return ["yellow","lightblue"][i]; })

var children = parents.selectAll("span")
  .data(function(d) { return d.ubiome; }) 
  .enter()
  .append("span")
  .text(function(d) { return JSON.stringify(d); })
span { 
  display: block;
  border: 1px dotted black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

我在d这里没有访问子项的特定属性,但是您可以看到跨度的基准对应于父项的 ubiome 数组。所以访问孩子的特定属性应该非常简单。


这是应用于您的代码段的上述逻辑:

let data = [{
    "sample_date": "2017-07-04T00:00:00.000Z",
    "ubiome": [{
        "count_norm": 1283,
        "tax_name": "Bacteroides fragilis",
        "tax_rank": "species"
      },
      {
        "count_norm": 3708,
        "tax_name": "Bacteroides thetaiotaomicron",
        "tax_rank": "species"
      },
      {
        "count_norm": 731,
        "tax_name": "Bacteroides uniformis",
        "tax_rank": "species"
      },
      {
        "count_norm": 62226,
        "tax_name": "Bacteroides vulgatus",
        "tax_rank": "species"
      },
      {
        "count_norm": 2139,
        "tax_name": "Parabacteroides distasonis",
        "tax_rank": "species"
      }
    ]
  },
  {
    "sample_date": "2017-07-10T00:00:00.000Z",
    "ubiome": [{
        "count_norm": 1200,
        "tax_name": "Bacteroides Noway",
        "tax_rank": "species"
      },
      {
        "count_norm": 3700,
        "tax_name": "Bacteroides thetaiotaomicron",
        "tax_rank": "species"
      },
      {
        "count_norm": 700,
        "tax_name": "Bacteroides uniformis",
        "tax_rank": "species"
      },
      {
        "count_norm": 62000,
        "tax_name": "Bacteroides vulgatus",
        "tax_rank": "species"
      },
      {
        "count_norm": 2100,
        "tax_name": "Parabacteroides distasonis",
        "tax_rank": "species"
      }
    ]
  }
];

var dates = [];
var ubiomeonly = [];

var itemSize = 30,
  cellSize = itemSize - 1,
  margin = {
    top: 120,
    right: 20,
    bottom: 20,
    left: 110
  };

var width = 750 - margin.right - margin.left,
  height = 500 - margin.top - margin.bottom;

dates = data.map(function(d) { 
  return d.sample_date;
})


var bacteria = [];
for (i = 0; i < data.length; i++) {
  bacteria.push(data[i].ubiome.slice(0, data[i].ubiome.length));
}
var bacteriaList = d3.merge(bacteria).map(function(d) {
  return d.tax_name
});
bacteriaList = d3.set(bacteriaList).values();

var y_elements = dates,
  x_elements = bacteriaList;

var xScale = d3.scaleBand()
  .domain(x_elements)
  .range([0, x_elements.length * itemSize]);

var xAxis = d3.axisTop()
  .scale(xScale)
  .tickFormat(function(d) {
    return d;
  });
  

var yScale = d3.scaleBand()
  .domain(y_elements)
  .range([0, y_elements.length * itemSize]);

var yAxis = d3.axisLeft()
  .scale(yScale)
  .tickFormat(function(d) {
    return  moment(d).format("YYYY-MM-DD");
  });

var colorScale = d3.scaleThreshold()
  .domain([0, 10000])
  .range(["#2980B9", "#E67E22", "#27AE60", "#27AE60"]);

var svg = d3.select('#heatmap')
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var parents = svg.selectAll(null)
  .data(data)
  .enter().append('g')
  .attr("transform",function(d) { return "translate(0," + yScale(d.sample_date) + ")" });
  
var children = parents.selectAll('rect')
  .data(function(d) { return d.ubiome; })
  .enter()
  .append('rect')
  .attr('class', 'cell')
  .attr('width', cellSize)
  .attr('height', cellSize)
  .attr('x', function(d) {  return xScale(d.tax_name); })
  .attr('fill', function(d) {
    return colorScale(d.count_norm);
  });

svg.append("g")
  .attr("class", "y axis")
  .call(yAxis)
  .selectAll('text')
  .attr('font-weight', 'normal');

svg.append("g")
  .attr("class", "x axis")
  .call(xAxis)
  .selectAll('text')
  .attr('font-weight', 'normal')
  .style("text-anchor", "start")
  .attr("dx", ".8em")
  .attr("dy", ".5em")
  .attr("transform", function(d) {
    return "rotate(-65)";
  });
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<h1>uBiome Bacterial Counts</h1>
<div id="heatmap"></div>

我在这里做了什么?

首先,但与嵌套数据无关,我对您的日子和物种都使用带刻度(这些刻度是为占用特定宽度/高度的事物设计的,例如条形图,它们仍然是序数)。除了格式化轴标签以简化比例之外,我也不会转换您的日期字符串。我还使 x 始终指代水平刻度/数据/轴,并且与 y 相同(因此为什么您的轴被翻转,我也没有将数据绑定到您的 svg,因为它是不必要的)

其次,我为每个采样周期(父元素)创建了一个组。g我根据日期的缩放值(包含在父数据中)对这些父 s 应用翻译。通过这样做,我将每个父母视为一行(我只翻译 y 值)。虽然我不必在上面的文本示例中放置任何内容,但我应用了背景颜色

第三,我正在为每个父元素创建子元素,这些子元素是根据物种定位的(因为该行已经在 parent 上使用 translate 定位g)。物种是从孩子的数据(矩形)访问的 - 原始数据集中二级数组之一中的项目。为了设置每个孩子的基准,我在嵌套selectAll().data().enter()循环中使用父基准(d.ubiome - 不是整个基准),就像上面的文本示例一样。

在此模式中,每个父数据都是原始数组中的一个项目。每个子数据都是包含在该子数据各自父数据中的某个数组中的一个项目。因此我们为什么使用parent.selectAll().data(function(d) { return d.ubiome }).enter()

而已。我没有访问子级的任何父级数据,因为行定位子级,但是如果您需要访问父级数据,您可以使用几种方法,一种是从访问器函数中选择父级: d3.select(this.parentElement).data(),或者使用局部变量。


您可能会问为什么 d3 不将嵌套数据绑定到嵌套元素,这似乎是您所期望的行为。嵌套元素通常与其父元素共享相同的数据,因为这有助于标记(父元素 g 中的圆圈和文本)之类的事情。具有包含数组的属性的数据可能允许数据的多种表示(通过时间动画、数据的不同分类或表示等),因此数组不是嵌套数据,它们将永远用它们自己的元素表示。有时数据集会有多个包含数组的属性(它们本身包含数组,例如:geojson),这种行为如何知道要为孩子使用哪个数组?


推荐阅读