可缩放旭日形图 (D3.js) 中损坏的图例和工具提示

Posted

技术标签:

【中文标题】可缩放旭日形图 (D3.js) 中损坏的图例和工具提示【英文标题】:broken legend and tooltip in zoomable sunburst chart (D3.js) 【发布时间】:2018-07-31 13:23:42 【问题描述】:

我有一个可缩放的旭日形图,但存在以下问题:

    图例是垂直显示而不是水平显示。我认为类图例上的 float:left 可以解决问题,但标签会显示在新行上。

    允许用户禁用图例中的类别以重新计算旭日形图。

    工具提示未显示。我到底错过了什么?

    我想在甜甜圈的中间附加总计,它会随着缩放转换而动态变化。怎么做呢?

对于混乱的代码,我深表歉意。我大约 2 周前开始学习 D3,下面的代码混合了一堆不同的教程和堆栈溢出论坛。

提前谢谢你!

// define json object
var root = 
 "name": "TOTAL",
 "children": [
  
   "name": "UNASSIGNED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  ,
  
   "name": "CLOSED",
   "children": [
    "name": "high", "size": 1701,
    "name": "med", "size": 584,
    "name": "low", "size": 606
   ]
  ,
  
   "name": "ATTACHED",
   "children": [
    "name": "high", "size": 220,
    "name": "med", "size": 179,
    "name": "low", "size": 322
   ]
  ,
  
   "name": "NOTIFIED",
   "children": [
    "name": "high", "size": 883,
    "name": "med", "size": 132,
    "name": "low", "size": 1066
   ]
  ,
  
   "name": "INTEGRATED",
   "children": [
    "name": "high", "size": 883,
    "name": "med", "size": 132,
    "name": "low", "size": 416
   ]
  ,
  
   "name": "DELIVERED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  ,
  
   "name": "ESCALATED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  ,
  
   "name": "COMMITTED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  ,
  
   "name": "VERIFIED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  ,
  
   "name": "SUBMITTED",
   "children": [
    "name": "high", "size": 170,
    "name": "med", "size": 701,
    "name": "low", "size": 410
   ]
  
 ]


// set width, height, and radius
var width = 650,
    height = 475,
    radius = (Math.min(width, height) / 2) - 10; // lowest number divided by 2. Then subtract 10

// legend dimensions
var legendRectSize = 15; // defines the size of the colored squares in legend
var legendSpacing = 6; // defines spacing between squares

var formatNumber = d3.format(",d"); // formats floats

var x = d3.scaleLinear() // continuous scale. preserves proportional differences
    .range([0, 2 * Math.PI]); // setting range from 0 to 2 * circumference of a circle

var y = d3.scaleSqrt() // continuous power scale 
    .range([0, radius]); // setting range from 0 to radius

// setting color scheme
var color = 
    'TOTAL': '#FFF',
    'UNASSIGNED': '#DADFE1',
    'ASSIGNED_TO_EDITOR': '#5BCAFF',
    'ATTACHED': '#87D37C',
    'ASSIGNED_TO_MENTOR': '#F64747',
    'ASSIGNED_TO_REVIEWER': '#7BDDDD',
    'ASSIGNED_TO_APPROVER': '#1e90ff',
    'INTEGRATION_FAILED': '#F1A9A0',
    'DELIVERED': '#4183D7',
    'INTEGRATED': '#90C695',
    'PUBLISHED': '#E4F1FE',
    'COMMIT_FAILED': '#F62459',
    'NOTIFIED': '#4ECDC4',
    'BLOCKED': '#D24D57',
    'ESCALATED': '#DB0A5B',
    'SUBMITTED': '#86a531',
    'REVIEWED': '#bfba00',
    'APPROVED': '#C86DEF',
    'ASSIGNED_TO_VERIFIER': '#D2527F',
    'COMMITTED': '#5AD427',
    'VERIFIED': '#81CFE0',
    'CLOSED': '#CF000F'
;

var partition = d3.partition(); // subdivides layers

// define arcs
var arc = d3.arc()
    .startAngle(function(d)  return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); )
    .endAngle(function(d)  return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); )
    .innerRadius(function(d)  return Math.max(0, y(d.y0)); )
    .outerRadius(function(d)  return Math.max(0, y(d.y1)); );

// define tooltip
var tooltip = d3.select('body') // select element in the DOM with id 'chart'
  .append('div') // append a div element to the element we've selected    
  .style("opacity","0")
  .style("position","absolute");

tooltip.append('div') // add divs to the tooltip defined above                            
  .attr('class', 'label'); // add class 'label' on the selection                         

tooltip.append('div') // add divs to the tooltip defined above                     
  .attr('class', 'count'); // add class 'count' on the selection                  

tooltip.append('div') // add divs to the tooltip defined above  
  .attr('class', 'percent'); // add class 'percent' on the selection

// define SVG element
var svg = d3.select("#chart").append("svg")
    .attr("width", width) // set width
    .attr("height", height) // set height
  .append("g") // append g element
    .attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
  
root = d3.hierarchy(root);

root.sum(function(d)  return d.size; );// must call sum on the hierarchy first

var path = svg.selectAll("path")
      .data(partition(root).descendants()) // path for each descendant
    .enter().append("path")
      .attr("d", arc) // draw arcs
      .style("fill", function (d)  return color[(d.children ? d : d.parent).data.name]; )
    .on("click", click)
      .append("title")
      .text(function(d)  return d.data.name + "\n" + formatNumber(d.value); 
    );

// mouse event handlers are attached to path so they need to come after its definition
path.on('mouseover', function(d)   // when mouse enters div
 var total = d.data.size
 var percent = Math.round(1000 * d.value / total) / 10; // calculate percent
 tooltip.select('.label').html(d.data.name); // set current label           
 tooltip.select('.count').html(total); // set current count            
 tooltip.select('.percent').html(percent + '%'); // set percent calculated above          
 tooltip.style('display', 'block'); // set display                     
);                                                           

path.on('mouseout', function()  // when mouse leaves div                        
  tooltip.style('display', 'none'); // hide tooltip for that element
 );

path.on('mousemove', function(d)  // when mouse moves                  
  tooltip.style('top', (d3.event.layerY + 10) + 'px') // always 10px below the cursor
    .style('left', (d3.event.layerX + 10) + 'px'); // always 10px to the right of the mouse
  );

function click(d) 
  svg.transition()
      .duration(750)
      .tween("scale", function() 
        var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
            yd = d3.interpolate(y.domain(), [d.y0, 1]),
            yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
        return function(t)  x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); ;
      )
    .selectAll("path")
      .attrTween("d", function(d)  return function()  return arc(d); ; );


d3.select(self.frameElement).style("height", height + "px");

// define legend element

var legendWidth = legendRectSize + legendSpacing; // height of element is the height of the colored square plus the spacing
var width= 500;
var height = 75; // height of element is the height of the colored square plus the spacing
var offset =  80; // vertical offset of the entire legend = height of a single element & 
var svgw = 20;
var svgh = 20;

var legendContainer = d3.select("#legend").append("svg")
    .attr("width", width) // set width
    .attr("height", height) // set height
  .append("g") // append g element
    .attr("transform", function(d, i) 
              return "translate(" + i * 20 + ",0)";
    );

var legend = legendContainer.selectAll('.legend') // selecting elements with class 'legend'
  .data(d3.entries(color)) // refers to an array of labels from our dataset
  .enter() // creates placeholder
  .append('g') // replace placeholders with g elements
  .attr('class', 'legend') // each g is given a legend class
  .style('background-color', 'orange')
  .attr('transform', function(d, i)              
      return "translate(0," + i * 20 + ")" //return translation       
   );

// adding colored squares to legend
legend.append('rect') // append rectangle squares to legend
  .attr('x', 0)
  .attr('y', 0)
  .attr('width', 10) // width of rect size is defined above                        
  .attr('height', 10) // height of rect size is defined above                      
  .style('fill', function (d)  return color[d.key]; ) // each fill is passed a color

// adding text to legend
legend.append('text')                                    
  .attr('x', 20)
  .attr('y', 10)
  .attr("text-anchor", "start")
  .text(function(d)  return d.key; ); // return label

function getRootmostAncestorByWhileLoop(node) 
    while (node.depth > 1) node = node.parent;
    return node;
html, body 
  height: 100%;


path 
  stroke: #fff;


/* legend */

#legend 
  background-color:yellow;

.legend 
  font-size: 14px;
  float: left;
  margin-right:1em;

rect 
  stroke-width: 2;


/* #tooltip 
	position: absolute;
	width: 200px;
	height: auto;
	padding: 10px;
	background-color: white;
	-webkit-border-radius: 10px;
	-moz-border-radius: 10px;
	border-radius: 10px;
	-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
	-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
	box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
	pointer-events: none;

			
#tooltip.hidden 
	display: none;

			
#tooltip p 
	margin: 0;
	font-family: sans-serif;
	font-size: 16px;
	line-height: 20px;
 */

.tooltip 
  opactiy: 0;
  positon: absolute;
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>D3.js Donut Chart</title>
        <link href="https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300|Pacifico" rel="stylesheet">
        <link href="styles.css" rel="stylesheet">
    </head>
    <body>

      <div id="chart"></div>

<div id="tooltip" class="hidden">
	<p><span id="category"><strong>Important Label Heading</strong></span></p>
	<p><span id="value">100</span></p>
</div>

<div id="legend"></div>


        <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
        <script src="script.js"></script> <!-- remove if no javascript -->
    </body>
</html>

【问题讨论】:

2.工具提示对我来说显示得很好。至于 1. 您正在使用转换来显示页面上的每个矩形/文本比之前的低 - return "translate(0," + i * 20 + ")" //return translation 【参考方案1】:

这是一个工作演示:

SUNBURST CHART WITH HTML LEGENDS AND TOOLTIP

    floatbackground-color 等 CSS 属性不适用于 SVG 元素。要使用 SVG 实现这一点,您必须使用 'transform('+(i*100)+',0)' ,它将从左侧转换。看看docs for the transform attribute.

    但是会有两个主要问题:

    a) 值 100 不会根据各自的宽度正确对齐文本(即它们会重叠,看起来很糟糕)。 解决方案 是根据每个键的宽度计算来定位图例。

    b) 您还必须根据页面宽度计算位置,即 legend wrapping 必须手动完成。 SVG 元素不介意离开页面:P

    为了克服上述问题,在这里转换为 HTML 是有意义的。现在都是&lt;div&gt;s 和&lt;span&gt;s。

    相应地,CSS 将位于以下行(查看代码了解详细信息):

    div#legend .rect 
      width: 10px;
      height: 10px;
      margin-right: 4px;
      display: inline-block;
    
    

    对于基于图例点击的数据过滤,这将是一些要编写的代码。我建议您先尝试一下,如果您遇到任何问题,请返回。很抱歉,不建议为一个问题编写整个代码。但我已经给了你它的起点:

    legend.on('click', function(d) 
        if(d3.select(this).classed('clicked')) 
        d3.select(this).classed('clicked', false)
           .style('background-color', function(d)  return color[d.key]; );   
       // filter data and rerender
     else 
        d3.select(this).classed('clicked', true)
           .style('background-color', 'transparent');
      // filter data and rerender
    
    

    单击图例,您会注意到这是如何工作的。查看它并尝试根据此key 过滤数据并重新渲染图表。这也应该相当简单。如果没有,请返回一个新问题。

    您的页面上有多个工具提示。我修复了这个错误并添加了一些类和 CSS。

    var tooltip = d3.select('body') // select element in the DOM with id 'chart'
       .append('div').classed('tooltip', true);
    

    我从 HTML 代码中删除了 &lt;div class="tooltip"&gt;&lt;/div&gt;

    您能否详细说明这一点?我不确定这里的具体要求是什么。也许您可以尝试使用我创建的小提琴,如果您无法在中心附加该标签,请告诉我。

希望这会有所帮助(是的,请修复小错误,如果有的话)。 :)

【讨论】:

非常感谢您花时间向我解释错误!我已经到了可以判断启用了哪些类别的地步。但是,当我禁用时,整个图表就会消失。我很接近吧? LOL codepen.io/lisaofalltrades/pen/rJYNRv?editors=0011 至于#4,让我尝试一下,然后回复你。我只想在旭日形中间显示图表的总和。 是的,当然!一定要回到我身边。就#3 而言,图表并未根据过滤后的数据重绘(使用启用标志)。为此,sum 计算和绑定到路径的数据必须更改,即partition(filteredRoot)(观察那里的过滤根)。今天下班后我来帮你。 好的,我试了一下。我能够将文本附加到甜甜圈的中间并更好地重新对齐图例。当我禁用图例中的类别并重新计算总和时,我在重绘图表时仍然遇到一些麻烦。如果你有时间,你能看看我的代码吗?先感谢您。你真棒! codepen.io/lisaofalltrades/pen/rJYNRv 现在这么近了!剩下的唯一一件事是当我禁用图例中的类别时,图表不会重绘。我使用 .tween 还是 .attrTween。或两者? codepen.io/lisaofalltrades/pen/rJYNRv?editors=0011 嘿,对不起,我放了一个月的长假,所以无法回复(我今天回来了)。现在看起来很棒。我会看一下在 legend-toggle 上重绘图表的情况,并通过 EOD 提供修复。

以上是关于可缩放旭日形图 (D3.js) 中损坏的图例和工具提示的主要内容,如果未能解决你的问题,请参考以下文章

来自 CSV 的 d3 可缩放树形图

如何在 R 或 Python 中制作旭日形图?

D3.js-柱形图

Plotly 旭日形图周围的值注释

python - 如何在python中的旭日形图的所有层中设置每个类别的颜色?

D3.js:为啥我的图例刻度线的文字消失了?