# D3 D3

- 来源：[E10 UI 组件库](https://www.e-cology.com.cn/ui/d3/freepass)
- 平台：PC
- 维护者：赵俊飞
- Issue：http://gitlab.ecology10.cn/FRONTEND/weapp-ui/-/issues?state=all&search=[D3]

## 代码演示

### 选择元素节点

可以使用各种W3C标准选择器来选择元素，包含属性值，类和ID等。

```jsx
import React, { Component } from 'react'
import { D3 } from "@weapp/ui";
import { Button } from "@weapp/ui"

let d3: any = null;
D3().then(Instance => {
  const { selectAll, select } = Instance;
  d3 = {
    selectAll, select
  }
});

function setColorByTagName() {
  d3.selectAll(".selections p").style("color", "blue");
}
function setColorById() {
  d3.select(".selections #p").style("color", "red");
}
function setColorByClassName() {
  d3.selectAll(".selections .p").style("color", "green");
}

export default class Eelections extends Component {
  render() {
    return (
      <div className="d3-demo selections">
        <Button onClick={setColorByTagName}>通过tagName设置样式</Button>
        <Button onClick={setColorByClassName}>通过class设置样式</Button>
        <Button onClick={setColorById}>通过id设置样式</Button>
        <p>
          tagName
        </p>
        <p className="p">
          class
        </p>
        <p id="p">
          id
        </p>
      </div>
    )
  }
}
```

### 动态属性

动态设置样式 还可以根通过绑定数据计算样式

```jsx
import React, { PureComponent } from 'react'
import { D3 } from "@weapp/ui";
import { Button } from "@weapp/ui"

let d3: any = null;
D3().then(Instance => {
  const { selectAll, select } = Instance;
  d3 = {
    selectAll, select
  }
});

function computed() {
  d3.selectAll("p")
    .data([4, 8, 15, 16, 23, 42])
    .style("font-size", function (d: string) { return d + "px"; });
}

function randomColor() {
  d3.selectAll(".dynamic-properties p").style("color", function () {
    return "hsl(" + Math.random() * 360 + ",100%,50%)";
  });
}

export default class DynamicProperties extends PureComponent {

  render() {
    return (
      <div className="d3-demo dynamic-properties">
        <Button onClick={randomColor}>随机着色</Button>
        <Button onClick={computed}>字体大小</Button>
        <p>
          这是一行文字
        </p>
        <p className="p">
          这是一行文字
        </p>
        <p id="p">
          这是一行文字
        </p>
      </div>
    )
  }
}
```

### 进入和退出

使用D3的输入和退出选择，您可以为传入数据创建新节点，并删除不再需要的传出节点。

```jsx
import React, { Component } from 'react'
import { D3 } from "@weapp/ui";

let d3: any = null;
D3().then(Instance => {
  const { selectAll, select } = Instance;
  d3 = {
    selectAll, select
  }
  initDom();
});

function getPnodes() {
  return d3?.select(".enterAndQuit").selectAll("p")
};
function getSpanNodes() {
  return d3?.select(".enterAndQuit").selectAll("span")
};

//根据data更新dom节点
function initDom() {
  //data里面有5个数据 而只有两个p标签 使用enter来添加elements
  getPnodes()
    .data([4, 8, 15, 16, 23, 42])
    .enter().append("p")
    .text((d: string) => "I’m number " + d + "!");

  //data里面有两个数据 却有3个sapn 使用exit来删除多余的elements
  getSpanNodes()
    .data([4, 8])
    .exit().remove()
}

/*如果数据个数多于dom元素个数那么说明有DOM元素的缺口，我们需要 add elements
如果数据个数少于dom元素个数，那么说明DOM元素有剩余，我们需要 remove elements
D3可以帮助我们非常方便地管理这两种情况，分别通过 .enter来添加 而通过.exit来删除
*/

export default class EnterAndQuit extends Component {

  render() {
    return (
      <div className="d3-demo enterAndQuit">
        <p>
          第一个p标签
        </p>
        <p>
          第二个p标签
        </p>
        <span>第一个span</span>  <span>第二个span</span><span>第三个span</span>
      </div>
    )
  }
}
```

### transitions过渡

D3对变换的关注自然会扩展到动画过渡

```jsx
import React, { Component } from 'react'
import { D3 } from "@weapp/ui";
import { Button } from "@weapp/ui"

let d3: any = null;
D3().then(Instance => {
  const { select } = Instance;
  d3 = {
    select
  }
});

function lengthTransition() {
  d3.select(".d3_transiton_item1").transition().style("width", "200px");
}

function backgroundTransition() {
  d3.select(".d3_transiton_item2").transition().style("background-color", "rgb(255,77,79)");
}

export default class Transition extends Component {
  render() {
    return (
      <div className="d3-demo d3_transition">
        <Button type="primary" className="d3_transiton_item1" onClick={lengthTransition}>改变长度</Button>
        <br /><br />
        <Button type="primary" className="d3_transiton_item2" onClick={backgroundTransition}>改变背景色</Button>
      </div>
    )
  }
}
```

### 树结构

可缩放和拖拽的组织型树结构

```jsx
import { PureComponent } from 'react'
import { D3 } from "@weapp/ui"
let d3: any = null;

//辅助函数: 递归获取tree的节点数量
function getCount(parent: any) {
  var count = 0;
  if (Array.isArray(parent.children)) {
    count += parent.children.length;
    parent.children.forEach(function (child: any) {
      if (Array.isArray(child.children)) {
        count += getCount(child);
      }
    });
  }
  return count;
}

//数据源
var root = {
  "x0": null,
  "y0": null,
  'name': '中国',
  'children': [{
    'name': '浙江',
    "_name": 'zhejiang',
    'children': []
  },
  {
    'name': '广西',
    'children': [],
    "_name": 'guangxi',
  },
  {
    'name': '黑龙江',
    "_name": 'heilongjiang',
  }
  ]
}

function createTree() {
  //布局和尺寸参数
  var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
    height = 800 - margin.top - margin.bottom;

  //常规参数
  var i = 0,
    duration = 750,
    rectW = 120,
    rectH = 60;

  //使用hierarchy层级布局生成树结构
  var tree = d3.tree().nodeSize([130, 60]);
  var hierarchyData = d3.hierarchy(root)

  //path连线
  function linkVertical(d: any) {
    // return "M" + d.source.x + "," + d.source.y       //曲线
    //     + "C" + (d.source.x + d.target.x) / 2 + "," + d.source.y
    //     + " " + (d.source.x + d.target.x) / 2 + "," + d.target.y
    //     + " " + d.target.x + "," + d.target.y;
    let sourceX = d.source.x, //折线
      sourceY = d.source.y + rectH,
      targetX = d.target.x,
      targetY = d.target.y;
    return "M" + sourceX + "," + sourceY +
      "V" + ((targetY - sourceY) / 2 + sourceY) +
      "H" + targetX +
      "V" + targetY;
  }

  //在页面中添加svg 支持拖拽和缩放
  var svg = d3.select("#body").append("svg").attr("width", 1000).attr("height", 1000)
    .call(d3.zoom().scaleExtent([1, 3]).on("zoom",
      function redraw(event: any) {
        svg.attr("transform", event.transform);
      }
    )).append("g").attr("transform", "translate(" + 350 + "," + 20 + ")").append("g");

  (root as any).x0 = 0;
  (root as any).y0 = height / 2;

  // root.children.forEach(collapse); 
  update(root);
  d3.select("#body").style("height", "800px");

  function update(source: any) {
    let treeData = tree(hierarchyData);
    let nodes = treeData.descendants().reverse();
    let links = treeData.links()
    // Normalize for fixed-depth.
    nodes.forEach(function (d: any) {
      d.y = d.depth * 180;
    });

    // Update the nodes…
    var node = svg.selectAll("g.node")
      .data(nodes, function (d: any) {
        return d.id || (d.id = ++i);
      });

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function (d: any) {
        return "translate(" + source.x0 + "," + source.y0 + ")";
      })

    nodeEnter.append("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      .attr("stroke", "black")
      .attr('y', 0)
      .attr('x', -rectW / 2)
      .attr("stroke-width", 1)
      .style("fill", function (d: any) {
        return d._children ? "lightsteelblue" : "rgb(93,156,236)";
      }).on("click", function (e: any, node: any) {
        click(node)
      });

    nodeEnter.append("text")
      .attr("x", rectW / 2 - rectW / 2)
      .attr("y", rectH / 2)
      .attr("dy", ".35em")
      .attr("text-anchor", "middle")
      .text(function (d: any) {
        return d.data.name;
      });

    // Transition nodes to their new position.

    let nodeUpdate = nodeEnter.merge(node);
    nodeUpdate.transition()
      .duration(750)
      .attr("transform", function (d: any) {
        return "translate(" + d.x + "," + d.y + ")";
      });

    nodeUpdate.select("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      .attr("stroke", "black")
      .attr("stroke-width", 1)
      .style("fill", function (d: any) {
        return d._children ? "lightsteelblue" : "rgb(93,156,236)";
      });

    nodeUpdate.select("text")
      .style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function (d: any) {
        return "translate(" + source.x + "," + source.y + ")";
      })
      .remove();

    nodeExit.select("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      //.attr("width", bbox.getBBox().width)""
      //.attr("height", bbox.getBBox().height)
      .attr("stroke", "black")
      .attr("stroke-width", 1);

    nodeExit.select("text");

    // Update the links…
    var link = svg.selectAll("path.link")
      .data(links, function (d: any) {
        return d.target.id;
      });

    // Enter any new links at the parent's previous position.
    let linkEnter = link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("x", rectW / 2)
      .attr("y", rectH / 2)
      .attr("d", function (d: any) {
        var o = {
          x: source.x0,
          y: source.y0
        };
        return linkVertical({
          source: o,
          target: o
        });
      });

    let linkUpdate = linkEnter.merge(link);

    // Transition links to their new position.
    linkUpdate.transition()
      .duration(duration)
      .attr("d", linkVertical);

    // Transition exiting nodes to the parent's new position.
    link.exit().transition()
      .duration(duration)
      .attr("d", function (d: any) {
        var o = {
          x: source.x,
          y: source.y
        };
        return linkVertical({
          source: o,
          target: o
        });
      })
      .remove();

    // Stash the old positions for transition.
    nodes.forEach(function (d: any) {
      d.x0 = d.x;
      d.y0 = d.y;
    });

    if (source.id === getCount(root) + 1) {
      //折叠区域渲染 使用延迟渲染优化显示效果
      setTimeout(function () {
        renderCollapseArea();
      }, 200)
    }

  }

  function renderCollapseArea() {

    function handleClick(e: any) {
      let target = e.target.dataset;
      let {
        mclass,
        type,
        collapsed,
        self
      } = target;
      if (type !== "btn") {
        return
      }
      if (collapsed === "false") { //展开
        d3.select(`.${mclass}`).transition().style("display", "none");
        e.target.dataset.collapsed = "true";
        document.getElementsByClassName(`${self}`)[0].innerHTML = "+"
      } else {
        d3.select(`.${mclass}`).transition().style("display", "block")
        e.target.dataset.collapsed = "false";
        document.getElementsByClassName(`${self}`)[0].innerHTML = "-"
      }
    }
    window.handleClick = handleClick;
  }

  renderCollapseArea();
  // Toggle children on click.
  function click(d: any) {
    if (d.id === getCount(root) + 1) {
      //折叠前隐藏div区域
      setTimeout(function () {
        d3.selectAll("foreignObject").style("display", "none");
      }, 200)
    }
    if (d.children) {
      d._children = d.children;
      d.children = null;
      update(d);
    } else if (d._children) {
      d.children = d._children;
      d._children = null;
      update(d);
    } else if (!d.children && !d._children) {
      let _y = (d.depth + 1) * 2;
      getNode(d, function (children: any) {
        children.forEach((node: any) => {
          node.depth = d.depth + 1;
          node.height = 0;
          node.x = d.x;
          node.x0 = d.x0;
          node.y = _y;
          node.y0 = _y;
          node.parent = d;
        })
        d.children = children;
        d._children = d.children;
        update(d);
      })
    }
  }

  //异步请求node
  function getNode(d: any, callback: Function) {
    //d是被点击的节点 将这个作为参数去获取不同的子节点
    //模拟接口
    setTimeout(() => {
      let res: any = {
        zhejiang: [{ data: { name: "杭州" } },
        { data: { name: "宁波" } },
        { data: { name: "温州" } }],
        guangxi: [
          { data: { name: "桂林" } },
          { data: { name: "南宁" } },
          { data: { name: "柳州" } },
          { data: { name: "防城港" } }
        ]
      }
      let _res = res[`${d.data._name}`];
      _res && callback(_res);
    }, 100);
  }
}

export default class tree extends PureComponent {
  render() {
    D3().then(Instance => {
      const { selectAll, zoom, select, tree, hierarchy, linkHorizontal } = Instance;
      d3 = {
        selectAll, zoom, select, tree, hierarchy, linkHorizontal
      };
      d3 && createTree()
    });
    return (
      <div id='d3-demo svgContainer'>
        <div id="body"></div>
      </div>
    )
  }
}
```

## API

### 参数说明如下：

#### V4 版本 API

[https://github.com/tianxuzhang/d3.v4-API-Translation](https://github.com/tianxuzhang/d3.v4-API-Translation)

#### D3 中文网

[https://d3js.org.cn/introduce/](https://d3js.org.cn/introduce/)

#### CHANGELOG

[https://github.com/d3/d3/blob/master/CHANGES.md#hierarchies-d3-hierarchy](https://github.com/d3/d3/blob/master/CHANGES.md#hierarchies-d3-hierarchy)
