D3 错误地包装圆圈

shy*_*cha 3 javascript charts d3.js

我试图用 D3 创建气泡图。一切都与示例中的完全一样,但后来我注意到数据渲染不正确。

所以我进行了一个实验:我将四个具有不同子项组合的“组”创建一个总值为100: 1 x 1002 x 503 x 33.33的组4 x 25。例如我有这样的数据:

[{
  title: "X",
  children: [
    {
      title: "100",
      weight: 100
    },
  ]
},
{
  title: "X",
  children: [
    {
      title: "50",
      weight: 50
    },
    {
      title: "50",
      weight: 50
    },
  ]
},
{
  title: "X",
  children: [
    {
      title: "33",
      weight: 33.33
    },
    {
      title: "33",
      weight: 33.33
    },
    {
      title: "33",
      weight: 33.33
    },
  ]
},
{
  title: "X",
  children: [
    {
      title: "25",
      weight: 25
    },
    {
      title: "25",
      weight: 25
    },
    {
      title: "25",
      weight: 25
    },
    {
      title: "25",
      weight: 25
    },
  ]
}]
Run Code Online (Sandbox Code Playgroud)

然后我像这样渲染图表:

const rootNode = d3.hierarchy(data);

rootNode.sum(d => d.weight || 0);

const bubbleLayout = d3.pack()
    .size([chartHeight, chartHeight])
    .radius(d => d.data.weight); // toggling this line on and off makes no difference

let nodes = null;

try {
    nodes = bubbleLayout(rootNode).descendants();
} catch (e) {
    console.error(e);
    throw e;
}
Run Code Online (Sandbox Code Playgroud)

但由此产生的泡沫无论如何都不是:

该死的3

要定义此渲染器的不正确性,请考虑屏幕截图中间的气泡:没有子项的蓝色气泡的半径为100,其实际大小为180 px。它右侧的两个气泡都有半径50,因此它们应该很180 px宽(当沿着同一轴放置时)。但发生的情况是它们的总直径是256 px,这让我认为这是不正确的渲染:

在此输入图像描述 在此输入图像描述

问题是:为什么会发生这种情况以及如何使该图表看起来正确,以便 的圆圈与r = 100同时具有r = 50两者的两个圆圈具有相同的大小?

And*_*eid 5

根据这个问题,我不一定清楚最终目标,但我们可以仔细研究每种可能性以确保完整性。

\n

我认为您希望代际圆具有相同的面积缩放因子或直径缩放因子(面积直径与跨代的每个节点的某些特定值成比例)。

\n

或者,您可能只想让面积或直径与一代中每个节点的某些特定值成比例,尽管我认为这种可能性较小。

\n

除了这些组织策略之外,我们还可以使面积或直径与叶节点的某些值成比例。

\n

鉴于评论中的讨论以及最近关于该主题的另一个问题,我将借此机会回顾一下上述每项组织策略。理想情况下,它涵盖了这个问题和相关问题。

\n

以下是基于上述的六种策略:

\n

面积比例

\n
    \n
  1. 打包圆圈,使叶子(无子项)圆圈具有成比例的面积
  2. \n
  3. 打包圆,使一代圆具有成比例的面积
  4. \n
  5. 打包圆圈,使所有或多个世代的圆圈面积成比例。
  6. \n
\n

直径/半径的比例

\n
    \n
  1. 打包圆圈,使叶子(无子项的儿童)圆圈具有成比例的直径
  2. \n
  3. 打包圆,使一代圆具有成比例的直径
  4. \n
  5. 包装圆圈,以便所有或多个世代的圆圈具有成比例的直径。
  6. \n
\n

结果

\n

本质上:可以用 来实现一、二、四和五d3.pack()。三是不可能的。六不是一个圆包。

\n

1. 叶子的比例面积

\n

这是 的预期行为d3.pack(),不需要太多讨论。只有叶子才会有成比例的面积,任何父母都会由其孩子的最小外接圆组成。它们的半径取决于包围子项所需的半径。

\n

2. 单代人的面积比例

\n

d3.pack()开箱即用也可以实现这一点,但需要有所不同。d3.pack()将为叶节点提供与某个大小值成比例的面积。如果不重新编写模块,就无法改变这一点(这已经是所有 d3 模块中最不友好的篡改模块)。

\n

该算法无法为任意生成提供比例面积,因此除非我们使用多个圆包,否则我们无法完成此策略:

\n

例子

\n

如果我们想要扩展最高级别的父级(根的第一代后代,在本节的其余部分中称为父级),那么我们可以创建一个父级圈包。父圆包只会被提供包含根和父辈的层次结构。由于所有父级都是此截断层次结构中的叶子,因此它们都将根据某个分配的值按比例缩放面积。g然后我们为每个节点使用 a 绘制这个圆包。

\n

在我们使循环包中的每个父节点为其自己的后代生成自己的循环包之后(这也有一个截断的层次结构,删除原始根,而是根将成为每个循环包的父节点)。子圆包内每个叶节点的面积将根据某个指定值按比例调整大小。每个子圆包之间叶节点的缩放将有所不同,因为这些现在单独打包的层次结构的性质和结构将决定叶缩放。

\n

这种方法要求我们跟踪父节点的半径来设置子圆包的大小并正确定位子包中的圆(我在下面的代码片段中为后者使用了局部变量)。这与实现一样困难,代码与在同一页面上附加两个圆形包时的代码基本相同。

\n

这是一个粗略的演示:

\n

\r\n
\r\n
var svg = d3.select("svg"), diameter = +svg.attr("width"), g = svg.append("g").attr("transform", "translate(2,2)"), colors = ["#ffffcc","#a1dab4","#41b6c4","#225ea8"];\n\nvar pack = d3.pack().size([diameter - 4, diameter - 4]);\n    \nvar local = d3.local();\n\nvar root = {"name": "root","children": [{"name": "Node A","size": 100},{"name": "Node B","size": 100},{"name": "Node C","size": 100}]}\nvar children = [{"name":"NodeA","children":[{"name":"Node1","size":34},{"name":"Node2","size":33},{"name":"Node3","size":33}]},{"name":"NodeB","children":[{"name":"Node1","size":50},{"name":"Node2","size":50}]},{"name":"NodeC","children":[{"name":"Node1","children":[{"name":"Nodea","size":15},{"name":"Nodeb","size":12},{"name":"Nodec","size":10}]},{"name":"Node2","size":10},{"name":"Node3","size":13},{"name":"Node4","size":9},{"name":"Node5","size":6},{"name":"Node6","size":10},{"name":"Node7","size":15}]}]\n    \n// parent pack:\nroot = d3.hierarchy(root)\n    .sum(function(d) { return d.size; })\n    .sort(function(a, b) { return b.value - a.value; });\n      \n// Parent Circle Pack\nvar node = g.selectAll(null)\n    .data(pack(root).descendants())\n    .enter().append("g")\n    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })\n    .attr("fill", function(d) { return colors[d.depth]; });\n\n// Parent circle:   \nnode.append("circle")\n    .attr("r", function(d) { return d.r; });\n        \n// get radii\nvar radii = pack(root).descendants().filter(function(d) { return d.depth == 1; }).map(function(d) { return d.r; });\n      \n// Create child pack data:  \nvar childRoots = children.map(function(child,i) {\nvar childPack = d3.pack().size([radii[i]*2 - 2, radii[i]*2 - 2]);\n      \nvar childRoot =  d3.hierarchy(child)\n    .sum(function(d) { return d.size; })\n    .sort(function(a,b) { return b.value - a.value; });\n        \n    return childPack(childRoot).descendants(); \n})    \n      \n// Swap node data for child node data, but keep the original data handy.\nvar childNodes = node.each(function(d,i) {\n        local.set(this, d);  // but store the data in the local variable.\n    })\n    .filter(function(d,i) {\n        return i > 0;\n    })\n    .data(childRoots)\n    .selectAll("g")\n    .data(function(d) { return d; })\n    .enter()\n    .append("g")\n    .attr("transform", function(d) { var offset = local.get(this).r; return "translate(" + (d.x-offset) + "," + (d.y-offset) + ")"; })\n    .attr("fill", function(d) { return colors[d.depth+1]; });\n\n// Append child elements to each node:\nchildNodes.filter(function(d) { return d.depth > 0 })  // skip parent - it\'s already drawn.\n    .append("circle")\n    .attr("r", function(d) { return d.r; });\n        \nchildNodes.filter(function(d) { return !d.children })\n    .append("text")\n    .text(function(d) { return d.data.name; })\n    .attr("fill","black")       \n    .style("text-anchor","middle")\n    .attr("dy", 5);
Run Code Online (Sandbox Code Playgroud)\r\n
<svg width="600" height="600"></svg>\n<script src="https://d3js.org/d3.v4.min.js"></script>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

每个父节点的大小值为 100,这恰好是每个父节点的所有最深子(叶)节点大小的累积总和。每个顶级父节点的大小也相同:

\n

在此输入图像描述

\n

当然,如果我们想按比例扩展这一代,我们可以养活父循环包根的孙辈。

\n

3. 各代人面积比例

\n

让我们使用一个简单的两代圈包:一个父母和一些孩子。

\n

如果父级与其子级具有相同的面积比例因子,则子级的累积面积将等于其父级的面积。

\n

如果我们要将这些孩子打包到他们的父母中,我们必须以不会产生空隙空间的方式进行。当处理多个子圈时,这是不可能的。

\n

空空间就是为什么这是不可能的 - 多个子代的父代的面积总是大于其子代面积的总和。

\n

如果代际比例至关重要,那么树形图可以实现这一点,d3 文档中描述的权衡是:

\n
\n

尽管圆形堆积不像树形图那样有效地使用空间,但 \xe2\x80\x9cwasted\xe2\x80\x9d 空间更显着地揭示了层次\n结构。(文档

\n
\n

例外情况

\n
    \n
  • 如果父母的尺寸值大于其孩子的累积尺寸值,则根据这些值,可能会出现循环包装。为了证明其局限性,请考虑两个同等大小孩子的父母。具有这两个子项的最有效的圆形包装将要求父项的面积是子项组合面积的两倍(注意,如果它大于 2 倍,那么我们就不是圆形包装,因为我们没有使用最小外接圆或者孩子们不要碰)。

    \n
  • \n
  • 同样,如果各代之间或父代中有足够的叶节点,则可能(取决于值和层次结构)跨代具有相同的面积缩放因子,以便两代的累积大小值(以及面积)节点不相等。

    \n
  • \n
  • 如果所有节点只有一个或零个子节点。

    \n
  • \n
\n

前两个项目符号可能需要手动更正/验证值才能仍然是圆形包,如果它们偏离了圆形包装(最小封闭圆形作为父级 - 无填充或边距),则 d3.pack() 不再是正确的工具。

\n

为了完整起见,我添加了这些例外情况,我认为除了单身儿童产生的例外情况之外,它们的可能性极小(但如果与父母的比例相同,则无论如何都完全覆盖父母)。

\n

4. 叶片的比例直径

\n

如果d3.pack()假设尺寸值应与叶圆的面积成正比,那么我们可以使用面积和直径之间的关系来获得尺寸值,该值将为叶节点创建与直径成比例的面积:

\n
size = Math.pow(size/2,2);\n
Run Code Online (Sandbox Code Playgroud)\n

我们将初始大小值视为直径,并找出具有该直径的圆的面积(按比例计算,因此我们不需要 \xcf\x80,因为我们会将每个结果乘以 \xcf\x80)。这是一个快速演示:

\n

\r\n
\r\n
size = Math.pow(size/2,2);\n
Run Code Online (Sandbox Code Playgroud)\r\n
var svg = d3.select("svg"), diameter = +svg.attr("width"), g = svg.append("g").attr("transform", "translate(2,2)"), colors = ["#ffffcc","#a1dab4","#41b6c4","#225ea8"];\n\nvar pack = d3.pack().size([diameter - 4, diameter - 4]);\n    \nvar root = {"name": "root","children": [{"name": "Node A","size": 100},{"name": "Node B",children:[{"name": "Node 1", "size":50},{"name": "Node 2", "size":50}]}]}\n    \nroot = d3.hierarchy(root)\n    .sum(function(d) { return Math.pow(d.size/2,2); })\n    .sort(function(a, b) { return b.value - a.value; });\n\nvar node = g.selectAll(null)\n    .data(pack(root).descendants())\n    .enter().append("g")\n    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })\n    .attr("fill", function(d) { return colors[d.depth]; });\n\nnode.append("circle")\n    .attr("r", function(d) { return d.r; });
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

以及片段的视觉效果:

\n

在此输入图像描述

\n

左侧(叶)圆的大小为 100,右侧(父)圆有两个子(叶)圆,每个子圆的大小为 50(累计 100)。通过这种方式缩放,我们似乎对叶子和父对象进行了相同的缩放。这只是在处理两个相同大小的子圆圈时发生的一个令人高兴的巧合。

\n

5. 单代的比例直径

\n

利用直径和面积之间的关系,我们可以创建缩放值传递给 d3.pack(),它表示给定直径的面积(与上面#4 中的相同)。

\n

一旦我们获得了面积值,该过程与缩放单代面积相同(与上面的#2相同)。就是这样。

\n

6. 各代直径的比例

\n

我们可以看到为什么策略 4 和 5 中的这种比例性在大多数情况下不能跨代延续。在一个包中,孩子们必须以允许最小包围圈的方式进行触摸。对于两个子节点,最小外接圆的直径始终等于子节点的直径之和。但如果我们每个父母有两个以上的孩子,我们就会遇到问题。

\n

我们可以看到,即使每个父级的子级圆的直径总和相同,有五个子级的父级也不会与有两个子级的父级具有相同的大小:

\n

在此输入图像描述

\n

这里,叶节点的直径(或半径)都是成比例的。例如,左侧大的第一代叶子的大小值为 100 - 宽度为 298 像素 (1: 2.98),右侧大圆圈中的两个叶子的大小值为 50,宽度为 149 像素(1:2.98)。下圆中的五片叶子的大小值为 20,宽度为 59.6 像素 (1: 2.98)。

\n

尽管叶节点中的直径(或半径)成比例,但一旦向上移动,这种比例就会消失:底部的五个子圆和右侧的两个子圆具有相同的累积直径(并且数据中的累积大小值相同),但父母的大小明显不同。

\n

但是,我们可以创建一个保留各代直径比例的布局,但不能使用d3.pack(). 在这种情况下,我们不是包装圆 - 我们是包装直径,直径是线。我们正在包装一维线(恰好用圆圈表示)。

\n

让我们假设一个简单的单亲多个孩子的例子。如果缩放因子在各代之间保持一致,则父代的直径必须等于子代直径的总和。只有一种方法可以用最小外接圆来实现这一点:

\n

在此输入图像描述

\n

如果你要把这个应用到所有世代,那么所有的圆圈都将锚定在一条线上——因为我们实际上是在进行线包装。

\n

d3.pack在这里不起作用,因为它将 2d 圆打包在 2d 空间中,我们只需将 1d 线打包在一维线上即可实现此策略。

\n

这个策略可能可以通过一些相当简单的数学来实现。

\n

例外情况

\n

在某些情况下也有例外,例如策略 #3 中检查的情况。

\n

还有一个例外:在层次结构中,每个节点都有两个大小相等的子节点。我不确定,d3 可能只是将其绘制在一条线上,但它可以与d3.pack. 然而,尚不清楚为什么某种树布局在这里不优越。

\n

概括

\n

肥皂盒

\n

圆形封装是一种以层次结构传递定量数据的糟糕方法。正如上面迈克的引述所述,它更适合传达层次结构。我还敢说,人们对圈子的真实判断是很差的。我还建议,如果叶子分散在不同的代中,那么用相同的比例因子调整叶子节点的大小对于读者来说可能并不直观。如果需要快速直观地定量了解基础值,则循环堆积并不是理想的解决方案。也就是说,

\n

结论

\n

圆形填充不会也不可能代表具有一致面积比例因子的所有区域:圆形填充意味着空隙空间,空隙空间意味着父圆的面积将大于其子圆面积的总和。如果您需要所有代都具有恒定的面积比例,则可能需要树形图。是的,#3、#6 中指出了一些例外情况,但这些本质上是理论上的,几乎没有实际用途

\n

圆形堆积只能表示具有恒定面积比例因子的一代或所有叶节点 - 不能同时表示两者。任何一种方法都可以通过以下方式完成d3.pack

\n

圆形堆积可以按比例表示任一叶或一代的直径。同样,这两种方法都可以通过 来完成d3.pack

\n

一些或所有世代的直径成比例的圆形堆积是不可能的。可以进行布局 - 但它不是圆形包装。我们可以收紧上图中三个子圆的排列,但是这样我们就没有最小外接圆(因此我们没有圆包装)。让它们排成一行也不是循环堆积。因为这d3.pack()没有用——因为我们不再打包圆圈了。

\n

可能还有其他布局选项,不使用最小外接圆或针对不同代使用不同的尺寸比例(这可能在实践中,而不是理论上)也总是需要放弃最小外接圆)。这使我们远远超出了圈子包装,我不确定有什么可以提供帮助。

\n