CSS 基本多面板图形布局 D3.js

CSS basic Multipanel Graph layout D3.js

我正在尝试为 D3 中的多面板图创建自定义布局。布局应为 3 列宽,前 2 列拆分为两行。最终结果应该是左侧有 4 个大小相等的矩形,右侧有一个从上到下的列(右侧栏)。我尝试使用以下代码这样做:

<!DOCTYPE html>

<script type="text/javascript" src="d3.min.js"></script>

<script type="text/javascript">

    var dataset;    
    var quadheight = "289"; 
    var quadwidth = "450";

    var maincontainer = d3.select("body")
        .append("div")
        .attr("class","main-container")
        .attr("id","main-cont");

    var ltq = maincontainer
        .append("svg")
        .attr("id", "left-top-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var rtq = maincontainer
        .append("svg")
        .attr("id", "right-top-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var menu = d3.select("body")
        .append("div")
        .attr("id","menu-div");

    var lbq = maincontainer
        .append("svg")
        .attr("id", "left-bottom-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var rbq = maincontainer
        .append("svg")
        .attr("id", "right-bottom-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

</script>

但是我无法理解如何将边栏(最后一列)放在右侧。任何帮助将不胜感激。由于我在前端开发方面缺乏经验,我意识到这可能不是实现相同结果的最佳解决方案,因此任何其他更好的解决方案也会很棒,包括普通 CSS/HTML 解决方案(无需在 [= 中动态构建布局30=]).

提前致谢, 图迈尼

使用您当前的代码,您可以正确构建象限元素,但是您没有将它们定位在任何地方。您可以获取代码并手动将每个象限放置在您想要的位置;然后添加一个额外的列并手动计算应该放在哪里...或者您可以使用 D3 布局的强大功能来帮助您:

设置布局

这是设置,这里我强制布局以 600 像素的固定高度填充整个 body 元素的宽度。简单来说,D3 布局只是一组 pre-built 行为,它们以特定方式作用于您提供的数据,我选择使用分区布局,因为这大致符合您想要实现的目标。

var body = d3.select('body'),
    w = body[0][0].offsetWidth,
    h = 600,
    x = d3.scale.linear().range([0, w]),
    y = d3.scale.linear().range([0, h]);

上面的设置很漂亮 straight-froward,这里唯一奇怪的是我使用 d3.scale.linear() 给我两个函数 xy。您可以很容易地计算出要在此处使用的数学表达式,但使用 D3 等库的目的是减少您自己的工作量。基本上,稍后调用时生成的函数 x() 会将 01 之间的任何传入值转换为 0w 范围内的值(其中 w 是计算 body[0][0].offsetWidth 时 body 的宽度)。 y 也是如此,只是范围在 0h 之间。因此,如果我要执行 x(0.5),结果值 returned 将是 w 的一半。这些函数也将在 0 和 1 之外工作(因此名称比例,如数字比例),因此如果我调用 y(2),returned 值将等于 2*h,即1200.

除了 .value(function(d){ return d.size; }) 部分,本节的其余部分只是附加和设置一些包装元素的样式。如果您实际上使用分区布局来显示值的视觉差异,则会使用此位,但您没有,所以它实际上对这段代码没有影响。它存在的唯一原因是因为分区布局需要为每个 rectangles/items 定义一个值,没有它就无法正确显示。

var partition = d3.layout.partition()
      .value(function(d) { return d.size; }),
    visual = body
      .append("div")
        .attr("class", "chart")
        .style("width", w + "px")
        .style("height", h + "px")
      .append("svg:svg")
        .attr("width", w)
        .attr("height", h)
      .append("g")
        .classed("container", true);

为什么要分区,是什么?

基本上我稍微滥用了分区布局来实现您的要求。分区布局按层次顺序用均匀宽度的单元格填充可用 space;这些单元格的高度受每个单元格 "value" 和 parent 拥有的兄弟单元格数量的影响。在这里解释得更清楚:

The partition layout produces adjacency diagrams: a space-filling variant of a node-link tree diagram. Rather than drawing a link between parent and child in the hierarchy, nodes are drawn as solid areas (either arcs or rectangles), and their placement relative to other nodes reveals their position in the hierarchy. The size of the nodes encodes a quantitative dimension that would be difficult to show in a node-link diagram.

https://github.com/mbostock/d3/wiki/Partition-Layout

更深入地了解分区布局超出了这个问题的范围,真正最好的学习方法是自己编写示例或教程——D3 网站为每个可用的不同选项提供了几个示例布局。

你需要从上面拿走的是分区布局负责显示 children 的垂直尺寸小于 parent 的尺寸(通常是水平顺序).

------> working direction is left to right by default.

+--------++--------++--------+
|        || Child  |+--------+  <-- even smaller children
|        ||  One   |+--------+
| parent |+--------++--------+
|        || Child  |+--------+
|        ||  Two   |+--------+
+--------++--------++--------+

d3 和数据

完成上述设置后,您需要用数据填充显示,以便显示任何内容。在您的问题代码中,您手动定义了每个项目,但 D3 旨在直接从 object 列表生成视觉元素(因此您不必担心自己创建每个项目)。以下是一个简单的示例,但许多 D3 布局依赖于传递一个数据列表,并且生成的每个单独的视觉元素都直接与该列表中的一个项目相关联。

因此,例如,您可以有四种颜色的列表并将其输入到 D3 布局中。然后布局会自动创建四个 SVG 圆圈,每个圆圈填充一种颜色。

因为我选择使用分区布局,所以我用来播种可视化的实际数据不是一列和四个象限(正如您的问题可能假设的那样),而是一个 parent房屋分为两半,每半又包含一个 child。数据还包含一个 "size" 属性,这是布局正常工作所必需的——并且与上面已经解释过的 .value(function(d) { return d.size; }) 相关联。该值不需要代表任何实际值,只要它们对于您希望同等大小的所有单元格都相同即可。

var root = {
  "name": "full column",
  "class": "full-col",
  "children": [
    {
      "name": "top right quad",
      "class": "quad",
      "size": 1,
      "children": [{
        "name": "top left quad",
        "size": 1,
        "class": "quad"
      }]
    },
    {
      "name": "bottom right quad",
      "class": "quad",
      "size": 1,
      "children": [{
        "name": "top left quad",
        "size": 1,
        "class": "quad"
      }]
    }
  ]
};

以上可以这样形象化:

+--------++--------++--------+
|        || Child  || Child  |
|        ||   A    || of A   |
| parent |+--------++--------+
|        || Child  || Child  |
|        ||   B    || of B   |
+--------++--------++--------+

获得数据结构后,需要将其输入 D3。下面的 visual object 是对外部包装 SVG 元素的引用,我们的所有其他元素都将添加到该元素中。

在 D3 中,就像 .attr.style 方法一样,您可以将 .data() 绑定到 selected 组 SVG 元素。如果给 .data() 一个 array/list 项目,它会将每个数组项目应用于当前 selection 中找到的每个元素,以线性顺序。例如,d3.selectAll('circle').data([{color: 'red'}, {color: 'blue'}]); 会将第一个 object {color: 'red'} 设置为第一个找到的圆的 "data",将第二个 object {color: 'blue'} 设置为第一个找到的圆第二。在不传递参数的情况下调用 .data() 将 return当前 selection.

中第一项的声音数据

希望上面的内容是有道理的,因为这就是下面发生的一切——尽管我们的数据项首先由 partition.nodes() 处理——我们的每个数据项都被绑定到一个 g 元素。将我们的项目传递给 partition.nodes() 只是用分区布局将使用的特征准备我们的数据——例如 dx 不在我们的 data-set 中,是由 partition.nodes 添加的——同时它使分区布局了解我们的数据 nodes/items.

kxky 只是速记变量,稍后用作单元格定位计算的一部分。在根级别,root.dx 包含沿 x 函数的单元格划分数,因此 kx 将包含我们布局中列的宽度。

/// must run this first to prep the root object with .dx and .dy
var g = visual.selectAll("g").data(partition.nodes(root)),
    kx = w / root.dx,
    ky = h / 1;

我遇到了一个不存在的人

现在数据已就位并且已被 D3 的分区代码解析,您可以着手构建实际的可视化显示。

如果您知道数据绑定到元素,并且数据项的数量与所选视觉元素的相同数量相关联,那就太好了。然而,在上面可能会混淆的部分是 visual.selectAll("g") 因为我们的布局中还没有 g 元素存在——那么你怎么能 select 它们呢?为了使这部分有意义,您需要了解 enterexit.

进出

D3 文档将这些定义为特殊类型的 sub-selection。对于任何 selection,一旦您执行了 .enter().exit(),从那里开始的任何链式方法都会对尚未创建(或删除)的元素进行操作。只有当数据项的数量(绑定到当前 selection)与这些数据项的视觉表示(select由当前select离子)。如果数据项多于视觉项,则会创建视觉项(按照我们计划的说明)。如果视觉项多于数据,则视觉项被销毁。进入和退出 sub-selections.

将忽略任何已经存在的视觉项目

为了简单起见,并且不会以很长的内联链结束,我将以下内容分成几部分。首先,我定义了一些指令来创建一个环绕 g 元素,每当新的数据项 "enters" 出现在场景中并需要视觉曝光时。我在 ge 中保留了对 "on enter" sub-selection 的引用(它还引用了尚未创建的 g 元素)。接下来我计划添加一个矩形元素作为理论 g 元素的 child,然后是一些文本的同级元素。因此,无论何时向布局添加新数据项,都应创建三个新元素。

/// for each data item, append a g element
var ge = g.enter()
    .append("svg:g")
        .attr("transform", function(d) {
            /// calculate the cells position in our layout
            return "translate(" + (-x(d.y)) + "," + y(d.x) + ")";
        });

/// for each item append a rectangle
ge.append("svg:rect")
    .attr("width", root.dy * kx)
    .attr("height", function(d) { return d.dx * ky; })
    .attr("class", function(d) { return d["class"]; });

/// for each item append some text
ge.append("svg:text")
    .attr("transform", function(d) {
      return "translate(8," + d.dx * ky / 2 + ")";
    })
    .attr("dy", ".35em")
    .text(function(d) { return d.name; });

因为我们的selection of visual.selectAll("g")没有找到任何元素,这意味着"on enter plan"被绑定到visual.selectAll("g")的每个数据项触发(这是我们的 root 数据项)。立即创建我们的五个 g 元素,每个元素都有其内部矩形和文本元素。

D3 布局以这种方式设计的原因是您可以轻松地修改您的 data-set、re-run 视觉创建代码,并让您的视觉表示正确有效地更新。对于您的示例,这可能有些矫枉过正,但如果您想了解 D3,最好锁定这些想法。

数据函数

理解 D3 的另一个关键理解是您可以将属性和样式设置为固定值的方式,或者您可以通过函数中的 return 值来设置它们。

在上面的代码中,您会看到许多类似于.attr("height", function(d){ return d.dx * ky; }) 的行。一开始他的做法可能看起来很奇怪,但是一旦您了解 d 参数被传递给绑定到当前 selected/being-created/being-destroyed 元素的特定数据项,它应该开始有意义了。

它基本上是一种概括元素创建的方法,就像我们所做的那样,以描述许多元素。但是随后允许您根据您传入的数据或其他代码定义的数据来具体说明它们的某些属性。

上面的 height 示例相当复杂,因为它依赖于分区布局生成的值,真正理解它的唯一方法是深入了解代码和 console.log 值,直到您理解为止布局在做什么。我还建议您这样做以了解一些位置和大小计算。一个更简单的例子——只是为了理解前提——是 .attr("class", function(d) { return d["class"]; })。所有这些代码所做的就是将当前 selected 元素的 class 属性设置为我们数据项中定义的 class 属性 的属性。

改变方向

剩下的就是解释一下这段代码:

  /// force the layout to work from the right side to left
  visual.attr("transform", "translate("+(w - root.dy * kx)+",0)")

以上所有操作都是将视觉容器移到 SVG 的右侧 canvas,减去第一列的宽度。这样做的原因是为了让事情简单化计算每个单元格的位置。在计算每个单元格的 x 位置时——也就是这一行 "translate(" + (-x(d.y)) + "," + y(d.x) + ")"——很容易通过使 x 值为负值来使布局从右到左 运行。减去一列宽的原因可以更形象地解释:

首先,如果我们不减去一个列宽,第一个单元格将在 0,0 处呈现 — 这将出现在屏幕之外,因为视觉元素的位置 0,0 将恰好位于视口。

+----------------------+
| svg                  +----------------------+
|                      | visual container     |
|       +------+------+|+------+              |
|       |Cell  |Cell  |||Cell  |              |
|       |      |      |||      |              |
|       +------+------+||      |              |
|       |Cell  |Cell  |||      |              |
|       |      |      |||      |              |
|       +------+------+|+------+              |
|                      +----------------------+
+----------------------+

如果我们从 svg 视口右侧减去一列的宽度,则视觉容器的 0,0 位于我们第一个单元格的正确位置,该单元格将位于 0,0。

+----------------------+
| svg           +----------------------+
|               | visual container     |
|+------+------+|+------+              |
||Cell  |Cell  |||Cell  |              |
||      |      |||      |              |
|+------+------+||      |              |
||Cell  |Cell  |||      |              |
||      |      |||      |              |
|+------+------+|+------+              |
|               +----------------------+
+----------------------+

可能有多种方法可以解决上述问题,您可以更改 scale.linear() x 函数,或者使单元格转换代码更复杂。但对我来说,这是更 straight-forward 的方法。

综合起来

您应该在下面找到一个工作示例。这是否是您实现目标的最佳方式,实际上取决于您打算如何处理布局本身。如果你打算可视化数据,那么 D3 是最好的路线,如果你打算创建一个更复杂的 HTML 界面,那么也许不同的方法是最好的,也许只是 table layout using CSS, or a flex-box approach.

var body = d3.select('body'),
    w = body[0][0].offsetWidth,
    h = 600,
    x = d3.scale.linear().range([0, w]),
    y = d3.scale.linear().range([0, h]);

var partition = d3.layout.partition()
      .value(function(d) { return d.size; }),
    visual = body
      .append("div")
        .attr("class", "chart")
        .style("width", w + "px")
        .style("height", h + "px")
      .append("svg:svg")
        .attr("width", w)
        .attr("height", h)
      .append("g")
        .classed("container", true);

(function(){
    
  var root = {
    "name": "full column",
    "class": "full-col",
    "children": [
      {
        "name": "top right quad",
        "class": "quad",
        "size": 1,
        "children": [{
          "name": "top left quad",
          "size": 1,
          "class": "quad"
        }]
      },
      {
        "name": "bottom right quad",
        "class": "quad",
        "size": 1,
        "children": [{
          "name": "top left quad",
          "size": 1,
          "class": "quad"
        }]
      }
    ]
  };
  
  /// we must run this first to prep the root object with .dx and .dy
  var g = visual.selectAll("g").data(partition.nodes(root)),
      kx = w / root.dx,
      ky = h / 1;
  
  /// force the layout to work from the right side to left
  visual.attr("transform", "translate("+(w - root.dy * kx)+",0)")
  /// for each data item, append a g element
  var ge = g.enter()
      .append("svg:g")
          .attr("transform", function(d) {
              return "translate(" + (-x(d.y)) + "," + y(d.x) + ")";
          });
  
  /// for each item append a rectangle
  ge.append("svg:rect")
      .attr("width", root.dy * kx)
      .attr("height", function(d) { return d.dx * ky; })
      .attr("class", function(d) { return d["class"]; });

  /// for each item append some text
  ge.append("svg:text")
      .attr("transform", function(d) {
        return "translate(8," + d.dx * ky / 2 + ")";
      })
      .attr("dy", ".35em")
      .text(function(d) { return d.name; });
  
})();
.chart {
  display: block;
  margin: auto;
  margin-top: 30px;
  font-size: 12px;
}

rect {
  stroke: #fff;
  fill: darkred;
  fill-opacity: .8;
}

rect.full-col {
  fill: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

可能有人会觉得去那儿很费力气,就是为了排几个长方形。但是使用 D3 之类的东西来管理布局的好处在于,您可以快速更改内容,只需更改数据即可。

http://jsfiddle.net/9gem72vk/