D3不正确包装圈子 [英] D3 packs circles incorrectly

查看:57
本文介绍了D3不正确包装圈子的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



因此,我进行了一个实验:我将四个具有不同子组合的组放入一个总价值 100 1 x 100 2 x 50 3 x 33.33 4 x 25 。例如。我有这样的数据:

  [{
title: X,
孩子们:[
{
标题: 100,
重量:100
},
]
},
{
标题: X,
的孩子:[
{
标题: 50,
的体重:50
},
{
标题: 50,
重量:50
},
]
},
{
标题: X,
子级:[
{
标题: 33,
重量:33.33
},
{
标题: 33,
重量:33.33
},
{
标题: 33,
重量:33.33
},
]
},
{
标题: X,
子级:[
{
标题: 25,
重量:25
},
{
标题: 25,
重量:25
},
{
标题: 25,
重量:25
} ,
{
标题: 25,
重量:25
},
]
}]

然后我按如下方式绘制图表:

  const rootNode = d3.hierarchy(data); 

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

const bubbleLayout = d3.pack()
.size([chartHeight,chartHeight])
.radius(d => d.data.weight); //切换此行为开和关没有区别

let nodes = null;

试试{
个节点= bubbleLayout(rootNode).descendants();
} catch(e){
console.error(e);
投掷e;
}

但由此产生的泡沫甚至都没有:





要定义此渲染器的不正确性,请考虑中间的气泡屏幕截图:蓝色的没有孩子的半径为 100 ,其实际大小为 180 px 。右边的两个气泡的半径均为 50 ,因此它们的宽度应<< c> c 180 px (放入时)相同的轴)。但是发生的是它们的总直径为 256 px ,这使我认为这是不正确的渲染:






问题 >:发生这种情况的原因以及如何正确显示此图表,以使 r = 100 的圆与具有 r = 2的圆的大小相同50 都是?

解决方案

基于这个问题,我不一定要明确最终目标,但我可以尽一切可能。



我认为您希望世代之间的圆具有相同的面积比例因子或直径比例因子(面积或直径



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



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



鉴于评论中的讨论以及对此主题的另一个近期



如果我们想按比例缩放这一代,自然可以为父级圈子提供根的孙子。



3。世代相传的地区比例



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



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



如果我们要将这些孩子打包成他们的父母,我们必须以不会产生空白空间的方式这样做。与圈子打交道时这是不可能的。



无效的空间是不可能的-多于一个孩子的父母的面积总是大于其孩子的面积之和。



如果代际比例至关重要,则可以通过树形图实现此目标,d3文档中所述的权衡是:


尽管圆形包装不能像
树形图那样有效地利用空间,但浪费的空间更显着地揭示了分层
的结构。 (



左侧的(叶子)圆的大小为100,右侧的(父)圆有两个孩子(叶子) ),每个尺寸为50(总计100)。似乎通过这种缩放,我们对树叶和父母的缩放比例相同。这是一个快乐的巧合,发生在与两个相同大小的子圆圈打交道时。



在一个孩子必须触摸的背包中。如果孩子的大小相同且相互接触,则最小围成的圆圈总是的直径等于孩子的总和。仅当处理两个相同大小的孩子时,这种模式才是正确的。



我们可以看到有五个孩子的父母看起来不会像有两个孩子的父母一样大小。即使每个父母的孩子圆圈的直径总和都是相同的:





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



尽管叶节点中的直径(或半径)成比例,但只要向上移动层次结构,就会消失:底部的五个子圆圈和右侧的两个子圆具有相同的累积直径(数据中具有相同的累积大小值),但是父母的大小明显不同。



5。单个世代的比例直径



使用直径和面积之间的关系,我们可以创建缩放值以传递到d3.pack(),该值代表给定直径的面积(相同就像上面的#4)。



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



6。世代相传直径的比例



这与区域不同,是可能的。但不能使用 d3.pack()。我们这里不是包装圈-我们是包装直径,直径是线。我们正在打包一维线(恰好用圆圈表示)。



我们假设一个简单的单亲多个孩子的例子。如果比例因子在各代之间保持一致,则父母的直径必须等于孩子的直径之和。只有一种方法可以达到最小包围圆的目的:





如果将其应用于所有世代,则所有圆都将锚定在一条直线上,因为实际上我们是在打包。



d3.pack 在这里不起作用,因为它在2d空间中封装了2d圆,我们只需要将1d线装在1d线上以实现此策略。



可以通过一些非常简单的数学来实现此策略。



例外



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



还有其他情况例外:每个节点都有两个大小相等的子级的层次结构。我不确定d3可能只是将其绘制在一条线上,但是可以与 d3.pack 一起使用。但是,目前尚不清楚为什么某种树形布局在这里不会更好。



摘要



Soapbox



圆形包装是一种糟糕的方法,无法在层次结构中传达定量数据。如上面引用Mike的话所述,最好传达层次结构。我会进一步冒险,认为人们对圈子的判断很差。我还建议,如果叶子散布在不同的世代中,那么对于读者来说,以相同比例因子调整叶子节点的大小可能不直观。如果需要对基础值进行快速直观的定量了解,则圆环填充不是理想的解决方案。就是说,



结论



圆圈包装不能且不能代表所有区域具有一致的面积比例因子:圆形填充表示空白空间,空白表示父圆的面积大于其子圆面积的总和。如果您需要所有世代都具有恒定的面积比例,则 treemap 可能就是您所需要的需要。 是的,在#3中有一些例外,但是这些本质上是理论上的,几乎没有实际用途



圆圈包装只能代表一个具有恒定面积比例因子或所有叶节点的生成-不能同时出现。两种方法都可以使用 d3.pack



圆形填充可以成比例地代表叶子或一代的直径。同样,可以使用 d3.pack 来实现这两种方法。



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



有可能是其他不使用最小封闭圆或在不同世代使用不同尺寸比例的布局选项(很可能(实际上,不是理论上)也总是需要最小封闭圆。这使我们很容易摆脱圈子的束缚,我不确定那里能提供什么帮助。


I was trying to create a bubble chart with D3. Everything worked exactly as in the example, but then I've noticed the data is rendered incorrectly.

So I ran an experiment: I've put four "groups" with different children combinations to create a group with a total value of 100: 1 x 100, 2 x 50, 3 x 33.33 and 4 x 25. E.g. I have the data like this:

[{
  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
    },
  ]
}]

Then I render the chart like this:

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;
}

But the resulting bubbles are not even in any means:

To define the incorrectness of this renderer, consider the bubbles in the middle of the screenshot: the blue one with no children has the radius of 100 and its actual size is 180 px. The two bubbles to the right of it both have radius 50, so they should be 180 px wide (when put along the same axis). But what happens is their total diameter is 256 px, which makes me think this is incorrect render:

The questions are: why this happens and how to make this chart look correctly so that the circle with r = 100 has the same size as two circles with r = 50 both?

解决方案

Based on the question I'm not necessarily clear on the end goal, but I we can go through each possibility for completeness.

I think you want inter-generational circles to have the same areal scaling factor or diameter scaling factor (area or diameter proportional to some specific value of each node across generations).

Alternatively, you might just want to have areas or diameters proportional to some specific value of each node across one generation, though I think this is less likely.

In addition to these organizing strategies, we could have areas or diameters proportional to some value of leaf nodes.

Given the discussion in the comments and another recent question on this topic, I'll take the opportunity to go over each of the above mentioned organizational strategies. Ideally that covers both this question and the linked one.

Here are the six strategies based on the above:

Proportionality of Areas

  1. Pack circles so that leaf (childless children) circles have proportional areas
  2. Pack circles so that one generation of circles has proportional areas
  3. Pack circles so that all or multiple generations have circles that are proportional in area.

Proportionality of Diameters/Radii

  1. Pack circles so that leaf (childless children) circles have proportional diameters
  2. Pack circles so that one generation of circles has proportional diameters
  3. Pack circles so that all or multiple generations have circles that have proportional diameters.

Outcomes

Essentially: One, two, four and five can be achieved with d3.pack(). Three is not possible. Six is not a circle pack.

1. Proportional Areas For Leaves

This is the expected behavior for d3.pack(), it doesn't require much discussion. Only leaves will have proportional areas, any parents will consist of circles that are a minimum enclosing circle. Their radii are determined by what is needed to enclose the children, not by any consideration for the sizing values of the children.

2. Proportional Areas For A Single Generation

This is also possible with d3.pack() out of the box - but with a twist. d3.pack() will give leaf nodes an area proportional to some sizing value. This cannot be altered without essentially re-writing the module (which is already the least friendly of all d3 modules to tamper with).

The algorithm can't give proportional areas to some arbitrary generation, so we can't accomplish this strategy unless we use multiple circle packs.

Example

If we wanted to scale the highest level parents (the root's first generation descendants, called parents for the rest of this section) then we could create a parent circle pack. The parent circle pack will only be fed a hierarchy that contains the root and the parents. Since all parents are leaves in this truncated hierarchy, they will all be scaled proportionally in area based on some assigned value. We then draw this circle pack using a g for each node.

After we make each parent node in the circle pack spawn its own circle pack for its own descendants (this is also has a truncated hierarchy, dropping the original root, instead the root will be the parent for each circle pack). The area of each of the leaf nodes within a child circle pack will be sized proportionally to some assigned value. The scaling of the leaf nodes will differ between each child circle pack because the nature and structure of these now separately packed hierarchies will determine leaf scaling.

This approach requires us to keep track of the radii of the parent nodes to set the size of the child circle packs and to correctly position the circles in the child packs (I use a local variable for the latter in the below snippet). That's about as difficult as the implementation gets, the code is largely the same as it would be if you were appending two circle packs on the same page.

Here's a crude demonstration:

var svg = d3.select("svg"), diameter = +svg.attr("width"), g = svg.append("g").attr("transform", "translate(2,2)"), colors = ["#ffffcc","#a1dab4","#41b6c4","#225ea8"];

var pack = d3.pack().size([diameter - 4, diameter - 4]);
	
var local = d3.local();

var root = {"name": "root","children": [{"name": "Node A","size": 100},{"name": "Node B","size": 100},{"name": "Node C","size": 100}]}
var 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}]}]
	
// parent pack:
root = d3.hierarchy(root)
	.sum(function(d) { return d.size; })
	.sort(function(a, b) { return b.value - a.value; });
	  
// Parent Circle Pack
var node = g.selectAll(null)
	.data(pack(root).descendants())
	.enter().append("g")
	.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
	.attr("fill", function(d) { return colors[d.depth]; });

// Parent circle:	
node.append("circle")
	.attr("r", function(d) { return d.r; });
		
// get radii
var radii = pack(root).descendants().filter(function(d) { return d.depth == 1; }).map(function(d) { return d.r; });
	  
// Create child pack data:  
var childRoots = children.map(function(child,i) {
var childPack = d3.pack().size([radii[i]*2 - 2, radii[i]*2 - 2]);
	  
var childRoot =  d3.hierarchy(child)
	.sum(function(d) { return d.size; })
	.sort(function(a,b) { return b.value - a.value; });
		
	return childPack(childRoot).descendants(); 
})	  
	  
// Swap node data for child node data, but keep the original data handy.
var childNodes = node.each(function(d,i) {
		local.set(this, d);  // but store the data in the local variable.
	})
	.filter(function(d,i) {
	    return i > 0;
	})
	.data(childRoots)
	.selectAll("g")
	.data(function(d) { return d; })
	.enter()
	.append("g")
	.attr("transform", function(d) { var offset = local.get(this).r; return "translate(" + (d.x-offset) + "," + (d.y-offset) + ")"; })
	.attr("fill", function(d) { return colors[d.depth+1]; });

// Append child elements to each node:
childNodes.filter(function(d) { return d.depth > 0 })  // skip parent - it's already drawn.
	.append("circle")
	.attr("r", function(d) { return d.r; });
		
childNodes.filter(function(d) { return !d.children })
	.append("text")
	.text(function(d) { return d.data.name; })
    .attr("fill","black")		
	.style("text-anchor","middle")
	.attr("dy", 5);

<svg width="600" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>

Each parent has a size value of 100, which is coincidentally (ok, not a coincidence, I intentionally did it) the cumulative total of all the deepest child (leaf) nodes' sizes for each parent. Each parent node is also the same size:

Naturally we could feed the parent circle pack the root's grandchildren if we wanted to scale that generation proportionately.

3. Proportionality of Areas across Generations

Let's use a simple two generation circle pack: a parent with some children.

If the parent has the same areal scaling factor as its children then the cumulative area of the children will be equal to the area of its parent.

If we are to pack these children into their parent, we must do so in a manner that does not create void space. This is not possible when dealing with circles.

Void space is why this is not possible - parent's of more than one child will always have an area greater than the sum of the areas of its children.

If intergenerational proportionality is critical then a treemap can achieve this, the tradeoff as described in the d3 documentation is:

Although circle packing does not use space as efficiently as a treemap, the "wasted" space more prominently reveals the hierarchical structure. (docs)

Exceptions

  • If parents have size values that are greater than the cumulative size values of their children then depending on the values, circle packing may be possible. To demonstrate the limited nature of this, consider a parent of two equally sized children. The most efficient circle pack with these two children will require a parent to be twice the area of the combined area of the children (note, if it is larger than 2x then we aren't circle packing as either we aren't using a minimum enclosing circle or the children don't touch).

  • Likewise it may be possible (depending on the values and hierarchy) to have the same areal scaling factor across generations if there are enough leaf nodes between the generations or in the parents so that the cumulative sizing value (and thus area) of the two generations nodes are not equal.

  • If we use the second approach above (single generation scaled proportionally) and the chosen generation only has only-children, then the children will be proportionately scaled as the only way to pack circles in another circle without voids is to pack a single circle in a parent.

The first two bullets would probably need manually corrected/validated values to still be circle packs, if they strayed from circle packing (minimum enclosing circles as parents - no padding or margins) then d3.pack() is no longer the correct tool.

I add these exceptions for completeness, I think they are extraordinarily unlikely, other than the exception arising from single children (but if scaled the same as their parents, completely cover the parents anyways).

4. Proportional Diameters for Leaves

If d3.pack() assumes that the sizing value should be proportional to the area of the leaf circle, then we can use the relationship between area and diameter to get a sizing value that will create areas proportional to diameter:

size = Math.pow(size/2,2);

We're treating the initial size value as a diameter and finding out the area of the circle with that diameter (proportionately, so we don't need π since we would multiply every result by π). Here's a quick demo:

var svg = d3.select("svg"), diameter = +svg.attr("width"), g = svg.append("g").attr("transform", "translate(2,2)"), colors = ["#ffffcc","#a1dab4","#41b6c4","#225ea8"];

var pack = d3.pack().size([diameter - 4, diameter - 4]);
	
var root = {"name": "root","children": [{"name": "Node A","size": 100},{"name": "Node B",children:[{"name": "Node 1", "size":50},{"name": "Node 2", "size":50}]}]}
	
root = d3.hierarchy(root)
	.sum(function(d) { return Math.pow(d.size/2,2); })
	.sort(function(a, b) { return b.value - a.value; });

var node = g.selectAll(null)
	.data(pack(root).descendants())
	.enter().append("g")
	.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
	.attr("fill", function(d) { return colors[d.depth]; });

node.append("circle")
	.attr("r", function(d) { return d.r; });

<svg width="600" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>

And a visual of the snippet:

The (leaf) circle on the left has a size of 100, the (parent) circle on the right has two children (leaves), each with size 50 (cumulatively 100). It may appear as though by scaling this way that we have scaled both leaves and parents the same. This is just a happy coincidence that occurs when dealing with two identically sized child circles.

In a pack the children must touch. If the children are of the same size and touching, the minimum enclosing circle will always have a diameter equal to the sum of the children's. This pattern is only true when dealing with two children of the same size.

We can see that a parent with five children will not appear the same size as a parent with two children even if the sum of the diameters of the children circles is the same for each parent:

Here, the leaf nodes are all proportionate in terms of diameter (or radius). For example, the large first generation leaf on the left has a size value of 100 - it is 298 pixels across (1 : 2.98), the two leaves in the large right hand circle have a size value of 50 each and are 149 pixels across (1 : 2.98). The five leaves in the lower circle have a size value of 20 each, and are 59.6 pixels across (1 : 2.98).

Despite the proportionality of diameter (or radii) in the leaf nodes, this is lost as soon as one moves up the hierarchy: the five children circles at the bottom and the two children circles on the right have the same cumulative diameter (and the same cumulative size value in the data), but the parents are obviously different sizes.

5. Proportional Diameters For a Single Generation

Using the relationship between diameter and area we can create scaled values to pass to d3.pack() which represent areas for given diameters (the same as in #4 above).

Once we get an area value, the procedure is the same as scaling areas for a single generation (the same as #2 above). That's it.

6. Proportionality of Diameters across Generations

Unlike areas, this is possible. But not with d3.pack(). We aren't packing circles here - we are packing diameters and diameters are lines. We are packing one dimensional lines (which happen to be represented by a circle).

Let's assume a simple one parent multiple children example. The parent's diameter must be equal to the sum of the children's diameters if the scaling factor is consistent across generations. There is only one way to achieve this with a minimum enclosing circle:

If you were to apply this to all generations, then all circles would be anchored on a line - as we are in fact line packing.

d3.pack won't work here as it packs 2d circles in 2d space, where we just need to pack 1d lines on a 1d line to achieve this strategy.

This strategy can probably be achieved with some fairly simple math.

Exceptions

There are exceptions in certain cases, such as those examined in strategy #3.

There is one other exception: a hierarchy where every node has two children with equal size. I'm not sure, d3 might just plot it on a line, but it would work with d3.pack. However, it is not clear why some sort of tree layout wouldn't be superior here.

Summary

Soapbox

Circle packing is a poor method to convey quantitative data in hierarchy. It is better for conveying the hierarchy, as noted above with Mike's quote. I'd further venture that areal judgements of circles by people is poor. I'd also suggest that sizing leaf nodes with the same scale factor is likely not intuitive for the reader if leaves are scattered across different generations. If quick and intuitive quantitative understanding of the underlying values is needed, circle packing is not the ideal solution. That said,

Conclusions

Circle packing does not and cannot represent all areas with a consistent areal scale factor: circle packing means void space, void space means parent circles will have areas greater than the sum of their child circle areas. If you need all generations to have a constant areal scale, a treemap may be what you need. yes, there are some exceptions noted in #3, but these are essentially theoretical with little practical use.

Circle packing can only represent either one generation with a constant areal scale factor or all leaf nodes - not both. Either approach can be accomplished with d3.pack

Circle packing can represent diameters proportionally for either leaves or one generation. Again either approach can be accomplished with d3.pack.

Circle packing with diameters proportionate across some or all generations is not possible. A layout can be made - but it is not circle packing. We could tighten the arrangement of the three child circles in the image above, but then we don't have a minimum enclosing circle (and thus we don't have circle packing). Leaving them in a line also isn't circle packing. For this d3.pack() is of no use - as we aren't packing circles anymore.

There could be other layout options that don't use minimum enclosing circles or that use different size scales for different generations (which would likely (in practice, not theory) always require ditching minimum enclosing circles as well). This moves us well outside circle packing and I'm not sure what is out there that can help.

这篇关于D3不正确包装圈子的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆