可缩放的旭日形图(D3.js)中的残破图例和工具提示 [英] broken legend and tooltip in zoomable sunburst chart (D3.js)

查看:80
本文介绍了可缩放的旭日形图(D3.js)中的残破图例和工具提示的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个可缩放的朝阳图,存在以下问题:

I have a zoomable sunburst chart with the following issues:

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

  1. Legend is displaying vertically instead of horizontal. I thought that float:left on class legend would do the trick but labels display on new line instead.

允许用户禁用图例中的类别以重新计算朝阳图.

Allow users to disable categories in the legend to recalculate the sunburst chart.

工具提示未显示.我到底想念什么?

Tooltip is not showing up. What in the world am I missing?

我想将总计添加到甜甜圈的中间,该甜甜圈会在缩放过渡时动态变化.如何做到这一点?

I want to append the grand total in the middle of the doughnut which dynamically changes upon zoom transitions. How does one go about that?

我为乱码表示歉意.我大约在两周前开始学习D3,下面的代码混合了许多不同的教程和堆栈溢出论坛.

I apologize for the messy code. I started learning D3 about 2 weeks ago and the code below is a mix of a bunch of different tutorials and stack overflow forums.

提前谢谢!

// 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>

推荐答案

这是一个有效的演示:

带有HTML传奇和TOOLTIP的SUNBURST图表

  1. CSS属性(如floatbackground-color)不适用于SVG元素.要使用SVG实现此目的,您必须使用'transform('+(i*100)+',0)',它将从左侧进行转换.看看有关转换属性的文档.

  1. CSS properties like float, background-color do not work for SVG elements. To achieve that using SVG, you gotta use 'transform('+(i*100)+',0)' which will transform from the left. Have a look at the docs for the transform attribute.

但是会有两个主要问题:

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

a) The value 100 which would not align the texts correctly based on the respective widths (i.e. they would overlap which looks bad). A solution to that would be to position legends based on calculation of width of every key.

b)您还必须基于页面宽度来计算位置,即自动换行必须手动完成. SVG 元素不要介意离开页面:P

b) You'll have to calculate the positions based on the page width as well i.e. legend wrapping has to be done manually. SVG elements DO NOT mind going off the page :P

为解决上述问题,在这里转换为 HTML 很有意义.现在都是<div><span>.

To overcome the above the issues, converting to HTML makes sense here. It's all <div>s and <span>s now.

因此,CSS将位于以下行中(请查看代码以获取详细信息):

And accordingly CSS would be on the lines of the following (check the code for details):

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

  • 对于基于图例单击的数据过滤,这将需要编写一些代码.如果您遇到任何问题,建议您先尝试一下,然后再试一次.很抱歉,建议您不要编写完整的代码来解决问题.但是,我已经为您提供了起点:

  • For the data filtering based on legends click, that'd be a bit of code to write. I'd suggest you try it first and get back if you face any issues with it. I'm sorry it's not advisable to write the entire code for a question. But I've given you the starting point for it:

    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渲染图表过滤数据.那也应该很简单.如果不是,请提出一个新问题.

    You'll notice how this works out by clicking on the legends. Take a look at it and try filtering the data based on this key and rerender the chart. That should be fairly simple as well. If not, get back with a new question.

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

    You had multiple tooltips on your page. I fixed that bug and added a few classes and CSS.

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

    然后我从HTML代码中删除了<div class="tooltip"></div>.

    And I got rid of the <div class="tooltip"></div> from the HTML code.

    您能否详细说明这一点?我不确定这里到底有什么要求.也许您可以尝试我创建的小提琴,让我知道您是否在中间粘贴该标签.

    Can you elaborate more on this point? I'm not sure of what exactly is the requirement here. Maybe you can try on the fiddle I created and let me know if you're stuck appending that label at the center.

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

    Hope this helps (and yes, please fix the minor bugs, if any). :)

    这篇关于可缩放的旭日形图(D3.js)中的残破图例和工具提示的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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