在Sunburst中添加工具提示和名称

时间:2019-01-25 17:13:42

标签: reactjs d3.js data-visualization react-d3

我不太擅长数据可视化。我想创建一个用户可以放大的Sunburst。我已经在朋友的帮助下完成了缩放,但是无法从数据中添加文本。这是我的可变焦森伯斯特代码。

import React from "react";
import { Group } from "@vx/group";
import { Arc } from "@vx/shape";
import { Partition } from "@vx/hierarchy";
import { arc as d3arc } from "d3-shape";
import {
  scaleLinear,
  scaleSqrt,
  scaleOrdinal,
  schemeCategory20c
} from "d3-scale";
import { interpolate } from "d3-interpolate";
import Animate from "react-move/Animate";
import NodeGroup from "react-move/NodeGroup";

const color = scaleOrdinal(schemeCategory20c);

export default class extends React.Component {
  state = {
    xDomain: [0, 1],
    xRange: [0, 2 * Math.PI],
    yDomain: [0, 1],
    yRange: [0, this.props.width / 2]
  };

  xScale = scaleLinear();
  yScale = scaleSqrt();

  arc = d3arc()
    .startAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x0))))
    .endAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x1))))
    .innerRadius(d => Math.max(0, this.yScale(d.y0)))
    .outerRadius(d => Math.max(0, this.yScale(d.y1)));

  handleClick = d => {
    this.setState({
      xDomain: [d.x0, d.x1],
      yDomain: [d.y0, 1],
      yRange: [d.y0 ? 20 : 0, this.props.width / 2]
    });
  };

  render() {
    const {
      root,
      width,
      height,
      margin = {
        top: 0,
        left: 0,
        right: 0,
        bottom: 0
      }
    } = this.props;
    const { xDomain, xRange, yDomain, yRange } = this.state;

    if (width < 10) return null;

    const radius = Math.min(width, height) / 2 - 10;

    return (
      <svg width={width} height={height}>
        <Partition top={margin.top} left={margin.left} root={root}>
          {({ data }) => {
            const nodes = data.descendants();
            return (
              <Animate
                start={() => {
                  this.xScale.domain(xDomain).range(xRange);
                  this.yScale.domain(yDomain).range(yRange);
                }}
                update={() => {
                  const xd = interpolate(this.xScale.domain(), xDomain);
                  const yd = interpolate(this.yScale.domain(), yDomain);
                  const yr = interpolate(this.yScale.range(), yRange);

                  return {
                    unused: t => {
                      this.xScale.domain(xd(t));
                      this.yScale.domain(yd(t)).range(yr(t));
                    },
                    timing: {
                      duration: 800
                    }
                  };
                }}
              >
                {() => (
                  <Group top={height / 2} left={width / 2}>
                    {nodes.map((node, i) => (
                      <path
                        d={this.arc(node)}
                        stroke="#fff"
                        fill={color(
                          (node.children ? node.data : node.parent.data).name
                        )}
                        fillRule="evenodd"
                        onClick={() => this.handleClick(node)}
                        text="H"
                        key={`node-${i}`}
                      />
                    ))}
                  </Group>
                )}
              </Animate>
            );
          }}
        </Partition>
      </svg>
    );
  }
}

当前,该可视化不显示来自data.js的数据名称。我想显示它并添加一个工具提示。我该如何实现?

1 个答案:

答案 0 :(得分:1)

class Sunburst extends React.Component {
    componentDidMount() {
        this.renderSunburst(this.props);
    }
    componentWillReceiveProps(nextProps) {
        if (!isEqual(this.props, nextProps)) {
            this.renderSunburst(nextProps);
        }
    }
    arcTweenData(a, i, node, x, arc) {  
        const oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
        function tween(t) {
            const b = oi(t);
            a.x0s = b.x0;  
            a.x1s = b.x1;   
            return arc(b);
        }
        if (i === 0) {
            const xd = d3.interpolate(x.domain(), [node.x0, node.x1]);
            return function (t) {
                x.domain(xd(t));
                return tween(t);
            };
        } else {  
            return tween;
        }
    }
    formatNameTooltip(d) {
        const name = d.data.name;
        return `${name}`;
    }
    labelName(d) {
        const name = d.data.name;
        return `${name}`;
    }
    labelVisible(d) {
        return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
    }

    labelTransform(d) {
        const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
        const y = (d.y0 + d.y1) / 2 * 130;
        return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
    }
    update(root, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self) {  
        if (firstBuild) {
            firstBuild = false; 

            function click(d) { 
                node = d; // eslint-disable-line
                self.props.onSelect && self.props.onSelect(d);
                svg.selectAll('path').transition().duration(1000);
            }
            const tooltipContent = self.props.tooltipContent;
            const tooltip = d3.select(`#${self.props.keyId}`)
                .append(tooltipContent ? tooltipContent.type : 'div')
                .style('position', 'absolute')
                .style('z-index', '10')
                .style('opacity', '0');
            if (tooltipContent) {
                Object.keys(tooltipContent.props).forEach((key) => {
                    tooltip.attr(key, tooltipContent.props[key]);
                });
            }
            svg.selectAll('path')
                .data(partition(root).descendants())
                .enter()
                .append('path')
                .style('fill', (d) => {
                    const current = d;
                    if (current.depth === 0) {
                        return '#ffff';
                    }
                    if (current.depth === 1) {
                        return '#3f51b5';
                    }
                    if (current.depth > 1) {

                        return '#f44336';
                    }
                })
                .attr('stroke', '#fff')                 // lines color
                .attr('stroke-width', '2')                  // line width             
                .on('click', d => click(d, node, svg, self, x, y, radius, arc))
                .on('mouseover', function (d) {
                    if (self.props.tooltip) {
                        d3.select(this).style('cursor', 'pointer');
                        tooltip.html(() => { const name = self.formatNameTooltip(d); return name; });
                        return tooltip.transition().duration(50).style('opacity', 1);
                    }
                    return null;
                })
                .on('mousemove', () => {
                    if (self.props.tooltip) {
                        tooltip
                            .style('top', `${d3.event.pageY - 50}px`)
                            .style('left', `${self.props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
                    }
                    return null;
                })
                .on('mouseout', function () {
                    if (self.props.tooltip) {
                        d3.select(this).style('cursor', 'default');
                        tooltip.transition().duration(50).style('opacity', 0);
                    }
                    return null;
                })
        } else {
            svg.selectAll('path').data(partition(root).descendants());
        }
        svg.selectAll('path').transition().duration(1000).attrTween('d', (d, i) => self.arcTweenData(d, i, node, x, arc));
    }
    renderSunburst(props) {
        if (props.data) {
            const self = this, // eslint-disable-line
                gWidth = props.width,
                gHeight = props.height,
                radius = (Math.min(gWidth, gHeight) / 2) - 10,
                svg = d3.select('svg').append('g').attr('transform', `translate(${gWidth / 2},${gHeight / 2})`),
                x = d3.scaleLinear().range([0, 2 * Math.PI]),
                y = props.scale === 'linear' ? d3.scaleLinear().range([0, radius]) : d3.scaleSqrt().range([0, radius]),
                partition = d3.partition(),
                arc = d3.arc()
                    .startAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x0))))
                    .endAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x1))))
                    .innerRadius(d => Math.max(0, y(d.y0)))
                    .outerRadius(d => Math.max(0, y(d.y1))),
                hueDXScale = d3.scaleLinear()
                    .domain([0, 1])
                    .range([0, 360]),
                rootData = d3.hierarchy(props.data);

            const firstBuild = true;
            const node = rootData;
            rootData.sum(d => d.size);
            self.update(rootData, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self); // GO!
        }
    }
    render() {
        return (
            <div id={this.props.keyId} className="text-center">
                <svg style={{ width: parseInt(this.props.width, 10) || 480, height: parseInt(this.props.height, 10) || 400 }} id={`${this.props.keyId}-svg`} />
            </div>
        );
    }
}

export default Sunburst;