D3JsDay27What's the tree?Let me see—树状图(tree diagram)

树状图介绍

以下节录自维基百科树状结构

树状结构(英语:Tree structure),又译树形结构,或称树状图(tree diagram)是一种将阶层式的构造性质,以图象方式表现出来的方法。它的名称来自於以树的象徵来表现出构造之间的关系,虽然在图象的呈现上,它是一个上下颠倒的树,其根部在上方,是资料的开头,而下方的资料称为叶子。

树形结构是一层次的巢状结构。 一个树形结构的外层和内层有相似的结构, 所以,这种结构多可以递回的表示。树状结构只是一个概念,可以用许多种不同形式来展现。在数学的图论与集合论中,对於树状结构的性质探讨是一个重要课题。在计算机科学中,则以树状资料结构作为讨论主题。

简单说,树状结构可以了解整个具有父子关系的结构,另外这边d3的tree和计算机科学当中的tree资料结构有点不一样,这边仅代表d3JS绘制出tree的方法前所需要的资料样貌

hierarchy()阶层化

在做树状图之前首先要先了解一下d3-hierarchy

表示巢状资料结构呈现像树一样,除了根节点之外,每一个节点都有一个父节点,同样的除了叶子以外每一个都有一个或多个子节点。

这边举例JSON档案如下

const treeData =
{
  "name": "Eve",
  "children": [
    {
      "name": "Cain"
    },
    {
      "name": "Seth",
      "children": [
        {
          "name": "Enos"
        },
        {
          "name": "Noam"
        }
      ]
    },
    {
      "name": "Abel"
    },
    {
      "name": "Awan",
      "children": [
        {
          "name": "Enoch"
        }
      ]
    },
    {
      "name": "Azura"
    }
  ]
};

资料来自官网

d3官网hierarchy

这时候我们可以将treeData做阶层化并且观看其console.log()会出现什麽东西程序码如下

const hierarchyData = d3.hierarchy(treeData, function(d){
    return d.children;
})
console.log(hierarchyData);

https://ithelp.ithome.com.tw/upload/images/20211012/20125095itoqaHxSgR.png

这边值得一提的是官网说可以指定要访问哪一个栏位当作children来走访生成,如果没有指定的话就会预设值是访问d.children的栏位,由於我们原始资料treeData就有children这个key,因此就算没有在hierarchy函数内设定function的话也会预设访问children这个key。

接下来观看console.log後可以发现hierarchy()将会把整个原先的物件转换後增加一些key来说明阶层关系,物件最外层的就是对应到刚刚原始资料的最外层,展开里面可以发现纪录了depthheightxy

https://ithelp.ithome.com.tw/upload/images/20211012/201250951xkqCZ4kIE.png

接下来我们要设置树的大小,这边设定size的宽是800和高600

const tree = d3.tree().size([800,600]);

制作树枝(节)—使用links()

使用tree的布局产生树枝的头(source)和尾(target)
官网API说明如下

https://ithelp.ithome.com.tw/upload/images/20211012/20125095p9aNh3Ggv5.png

将转换後的阶层化数据带入成tree布局後使用links(),这边可以使用console.log来观看其经过转换函式的样貌

console.log(tree(hierarchyData).links());

观看开发者人员工具将会生成一个阵列储存每个节的sourcetarget
如下图
https://ithelp.ithome.com.tw/upload/images/20211012/201250951vVR5BFnTP.png

d3官方link说明

画树枝

有了经过link()转换後的资料,接下来就可以开始画图了
我们将刚刚生成的数据使用svg的line绘制进行data绑定之後,svg的x1和y1的属性值设定为刚刚的source,svg的x2和y2属性设定为target,程序码如下

let padding = 20;
let width =800-padding-padding;
let height = 600-padding-padding;
const svg = d3.select(".container")
            .append("svg")
            .attr("width", width+padding+padding)
            .attr("height", height+padding+padding)
            .append('g')
            .attr('transform', `translate(${padding},${padding})`)

const tree = d3.tree().size([800,600]);
let hierarchyData = d3.hierarchy(treeData, function(d){
    return d.children;
})
const g = svg.append("g")                
g.selectAll("line").data(tree(hierarchyData).links())
.join("line")
.attr("x1",(d)=>(d.source.x))
.attr("y1",d=>( d.source.y))
.attr("x2",d=>( d.target.x))
.attr("y2",d=>( d.target.y))
.style('stroke', "black")
.style('stroke-width', "2px");

如下图
https://ithelp.ithome.com.tw/upload/images/20211012/20125095x5W7CdTfPE.png

制作节点—使用descendants()

接下来使用node.descendants()来制作节点

官方API指出会产生所有後代节点的阵列,换句话说就是将刚刚的hierarchyData做扁平化处理

https://ithelp.ithome.com.tw/upload/images/20211012/20125095Y1Fmb8pIyw.png

可以撰写程序码观看内容

console.log(tree(hierarchyData).descendants());

如下图可以发现它将所有子节点给取出做成一个阵列了

https://ithelp.ithome.com.tw/upload/images/20211012/201250954w9NuwevL4.png

画出节点

接下来我们在svg插入一个g元素里面预计使用join生成许多g後把资料绑定在上面以便里面放入circle和text显示节点样貌和内容。

在刚刚创建class名为g-node的元素选取起来後插入circle和text记得微调一下text的位置让它不要和circle重叠即可

具体程序码如下

 let gNode = svg.append("g")
  .selectAll("g")
  .data(tree(hierarchyData).descendants())
  .join("g")
  .classed("g-node",true);

  d3.selectAll(".g-node")
  .append("circle")
  .attr('cx', function(d) {return d.x;})
  .attr('cy', function(d) {return d.y;})
  .attr('r', 10)
  .attr("fill", "lightgreen")
  .attr('stroke', "black")
  .attr('stroke-width', 1);


  d3.selectAll(".g-node")
  .append("text")
  .attr('x', function(d) {return d.x;})
  .attr('y', function(d) {return d.y;})
  .attr("dy", ".5em")
  .attr("dx", "1em")
  .style("text-anchor", "start")
  .text(function(d) {return d.data.name})

最後你应该会看到如下图
https://ithelp.ithome.com.tw/upload/images/20211012/20125095TGuVT50lew.png

树枝(节)做成曲线—使用linkVertical()

方法一 手写curve

这边的树枝由於使用的是svg的line元素,因此整个线段会是笔直的线段,如果要有点曲线的样貌,必须改用path元素,这里有两种方法一种是手写MC的设定,另一种是使用d3的link产生器
这边简单带一下手写的方法

g.selectAll("path")
  .data(tree(hierarchyData).descendants().slice(1))
  .join("path")
  .attr("d", function(d) {
       return "M" + d.x + "," + d.y
         + "C" + d.x + "," + (d.y + d.parent.y) / 2
         + " " + d.parent.x + "," +  (d.y + d.parent.y) / 2
         + " " + d.parent.x + "," + d.parent.y;
       })
  .attr("stroke","black").attr("fill","none");

将原本joinline元素改成path,另外注意要带入的资料如这行所示
tree(hierarchyData).descendants().slice(1),使用.descendants()的转换後并且要使用slice删除第一笔,不然会报错,因为该笔的y是null。

接下来应该会看到如下图

https://ithelp.ithome.com.tw/upload/images/20211012/20125095xaD0v2Phs4.png

方法二 使用link产生器 —linkVertical()

这边主要介绍第二种方法使用linkVertical

官方API说明它将会回传Link产生器,用来呈现树状图的显示

https://ithelp.ithome.com.tw/upload/images/20211012/20125095VjiGdjETjC.png

可以先撰写以下程序码观看呈现样貌会比较好理解

<svg width="800" height="600" id="multiLink"></svg>
<script>
    let linkGen = d3.linkHorizontal();

    let multiLinkData = [
    {source: [50,50], target: [175,25]},
    {source: [50,50], target: [175,50]},
    {source: [50,50], target: [175,75]},
    ];

    d3.select("#multiLink")
    .selectAll("path")
    .data(multiLinkData)
    .join("path")
    .attr("d", linkGen)
    .attr("fill", "none")
    .attr("stroke", "black");
</script>

之後你应当会看到如下图

https://ithelp.ithome.com.tw/upload/images/20211012/20125095oZzx4EVO92.png

因此我们要绘制这种图形的时候先备资料含有source和target的物件来作为线段的起始点和终点,还记得刚刚我们使用tree(hierarchyData).links()所转换的资料吗?其中的key就含有sourcetarget

我们撰写程序码如下,最後呈现的图将会与刚刚手动的效果一样,另外我们也可以console.log()观看在转换过程当中所呈现的内容是什麽

const linkMkr = d3.linkVertical().x(d=>d.x).y(d=>d.y); //使用link产生器指定成垂直的样貌
g.selectAll("path").data(tree(hierarchyData)
  .links())
  .join("path")
  .attr("d",d=>{
    console.log(linkMkr(d));
    return linkMkr(d)}
    )
  .attr("stroke","black").attr("fill","none");

其实就是将原本的资料转换成pathM、C的相关数值而已如下图

https://ithelp.ithome.com.tw/upload/images/20211012/20125095bWH6BEeNHk.png

最後呈现应该会和刚刚手写的图一样

水平树状图范例

除了垂直的树状图以外也可以使用linkHorizontal()产生水平的树状图,这里使用树状图来呈现web的子集技术如下图

https://ithelp.ithome.com.tw/upload/images/20211012/201250956koMehgjZ9.png

程序码如下

<style>
  .node circle {
    fill: rgb(255, 255, 255);
    stroke: rgb(255, 130, 130);
    stroke-width: 3px;
  }
  .node text {
    font: 12px sans-serif;
  }
  .link {
    fill: none;
    stroke: #ccc;
    stroke-width: 2px;
  }
</style>
<body>
  <script>
    let treeData = {
      name: "Web",
      children: [
        {
          name: "Javascript",
          children: [
            { name: "Typescript" },
            { name: "Dart" },
            { name: "CoffeeScript" },
          ],
        },
        {
          name: "HTML",
          children: [{ name: "pug" }],
        },
        {
          name: "CSS",
          children: [{ name: "SCSS" }, { name: "LESS" }, { name: "Stylus" }],
        },
      ],
    };
    //设定边界
    let margin = { top: 20, right: 90, bottom: 30, left: 90 },
      width = 660 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;
    
      //设定树宽高

    let tree = d3.tree().size([width, height]);

    //将数据阶层化
    let hierarchyData = d3.hierarchy(treeData, function (d) {
      return d.children;
    });

    // 将阶层化的数据带入tree layout
    hierarchyData = tree(hierarchyData);

    let svg = d3
      .select("body")
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom),
      g = svg
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    //创建一个水平link产生器
    let linkMkr = d3
      .linkHorizontal()
      .x((d) => d.y)
      .y((d) => d.x);
    //绘制出树枝
    g.selectAll("path").data(hierarchyData.links())
    .join("path")
    .attr("d",d=>{
      return linkMkr(d);
    })
    .attr("stroke","black").attr("fill","none");

    
    let node = g
      .selectAll(".node")
      .data(hierarchyData.descendants())
      .join("g")
      .attr("class", "node")
      .attr("transform", function (d) {
        return "translate(" + d.y + "," + d.x + ")";
      });

    node.append("circle").attr("r", 10);

    node
      .append("text")
      .attr("dy", "-1.5em")
      .style("text-anchor", function (d) {
        return "middle";
      })
      .text(function (d) {
        return d.data.name;
      });
  </script>
</body>

image树状图范例

另外svg里面也可以带入image,使用<image>标签,资料物件里面涵盖image的URL就可以呈现,如下图使用树状图来表示蕨类植物的父子关系,线的style改变颜色,可能代表蕨类有是长子或是不孕等等情况

https://ithelp.ithome.com.tw/upload/images/20211012/20125095E5c4DEIWj2.png

图片来源:Unsplash

程序码如下

<style>
  .node text {
    font: 12px;
  }
  .link {
    fill: none;
    stroke-width: 2px;
  }
</style>
<body>
  <script>
    let treeData = {
      name: "祖先",
      level: "red",
      icon: "https://images.unsplash.com/photo-1616504152265-535fb626017d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=2070&q=80",
      children: [
        {
          name: "孩子",
          level: "orange",
          icon: "https://images.unsplash.com/photo-1483718983629-1100e0808b32?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1170&q=",
          children: [
            {
              name: "长孙",
              icon: "https://images.unsplash.com/photo-1616504152528-c46d3292a6ff?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1170&q=80",
              level: "orange",
            },
            {
              name: "我雌性",
              icon: "https://images.unsplash.com/photo-1616504152528-c46d3292a6ff?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1170&q=80",
              level: "green",
            },
          ],
        },
        {
          name: "孩子不孕",
          icon: "https://images.unsplash.com/photo-1483718983629-1100e0808b32?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1170&q=",
          level: "red",
        },
      ],
    };

    let margin = { top: 20, right: 90, bottom: 30, left: 90 },
      width = 660 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;

    let tree = d3.tree().size([height, width]);

    let nodes = d3.hierarchy(treeData, function (d) {
      return d.children;
    });

    nodes = tree(nodes);

    let svg = d3
        .select("body")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom),
      g = svg
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    let linkMkr = d3
      .linkHorizontal()
      .x((d) => d.y)
      .y((d) => d.x);
    let link = g
      .selectAll("path")
      .data(nodes.links())
      .join("path")
      .attr("class", "link")
      .style("stroke", function (d) {
        return d.target.data.level;
      })
      .attr("d", function (d) {
        return linkMkr(d);
      })
      .attr("stroke", "black")
      .attr("fill", "none");
    let node = g
      .selectAll(".node")
      .data(nodes.descendants())
      .enter()
      .append("g")
      .attr("transform", function (d) {
        return "translate(" + d.y + "," + d.x + ")";
      });

    node
      .append("image")
      .attr("xlink:href", function (d) {
        return d.data.icon;
      })
      .attr("x", "-50px")
      .attr("y", "-50px")
      .attr("width", "100px")
      .attr("height", "100px");

    node
      .append("text")
      .attr("x", "0")
      .attr("y", "-50")
      .style("text-anchor", "middle")
      .text(function (d) {
        return d.data.name;
      });
  </script>
</body>

最後本日附上githubPage的页面

原始范例树状图范例
Image树状图范例
水平web技术子集树状图
link产生器范例


<<:  Day 27 - 资料视觉化与API - 将资料转化成艺术

>>:  【第27天】探讨与改善-增加训练样本(二)

Day 08 - 今天的我没办法产好CODE

因为心情太低落了,掉着泪看教学,我真的很认真的在学,但为什麽要被批评得一无是处。 也许在学技能的同时...

顺着藏宝图的指示,可以寻获庞大的财富

Ingress Service是给予User透过特定的Port来访问Pod,当有多个Service连...

修改word 作者属性

删除作者 作法1 : 直接修改 删除作者,删掉,从这边删掉,按确定就可以了 作法2 : 移除档案属性...

DAY4 起手式--Nuxt.js(细)说pages(上)

你知道 pages 的 vue 元件多了什麽功能吗? 经过昨天的介绍,我们都知道 pages 是拿来...

.Net Core Web Api_笔记23_api结合EFCore资料库操作part1_专案前置准备

专案前置准备 建立并配置好visual studio .net core web api专案 .ne...