前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >D3库实践笔记之几类特定图表与布局 |可视化系列37

D3库实践笔记之几类特定图表与布局 |可视化系列37

作者头像
蛰虫始航
发布2020-11-25 15:06:43
1.9K0
发布2020-11-25 15:06:43
举报
文章被收录于专栏:蛰虫始航蛰虫始航

布局(Layout)可以看成是D3对图形元素的一种排布方式,在绘制柱状图时,是在横平竖直的直角坐标系下,确定矩形的左上角坐标,就可以画出随着高度变化的一系列柱子,以体现数据值的差异,而如果要画饼图呢,有一列数据[30,10,6],映射到饼图的不同楔形里,是一个个手动计算角度和初始位置么?根据图形语法,只需要将坐标系变成极坐标,一系列数据很容易对应为角度。

布局和比例尺一样,也属于一种映射,能够将我们提供的数据重新映射/变换成新格式,以便于在某些更特定的图表中的使用。

饼图布局

在v3.x版本中,d3的布局在d3.layout接口下,通过d3.layout.pie()创建一个饼状图布局,而到v5x及最新的v6之后,是d3.pie(),不再使用d3.layout系列,在控制台输入d3.layout 是undefined。在使用饼图布局后,不需要把SVG整个画布的坐标系转成极坐标系,而是将系列数据做转换。

对于一个数组dataset = [76,37,90,60,50],通过arcs=d3.pie()(dataset)转换成适合生成饼图的格式,在套上前几篇都用过的生成svg和添加形状的框架,一个饼图就诞生了。传入arcs绘制每个楔形用的是SVG的<path>标签。

d3-pie

绘制饼图的具体代码如下:

代码语言:javascript
复制
var dataset = [76,37,90,60,50]; //要绘制的数据
var cls=["#F3D2AC","#F3B8B1","#CC99CC","#7DC8CA","#CAE9E0"];
var outerRadius = 500/2, innerRadius = 0;
var arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
//var arcs=d3.pie()(dataset);
var pie = d3.pie().sort(null);
var arcs=pie(dataset); //不使用排序
var svg = d3.select("body")
               .append("svg")
               .attr("width",'600')
               .attr("height",'500')
//为每个要绘制的扇形创建新的分组( g ), 并进行数据绑定
var a= svg.selectAll("g.arc")
          .data(arcs)
          .enter()
          .append("g")
          .attr("class", "arc")
          .attr("transform", "translate(" + outerRadius + ", " + outerRadius
                                            + ")");
    a.append("path")  //绘制每个楔形
     .attr("fill",function(d, i) {return cls[i];})
     .attr("d", arc);
    a.append("text")  //添加文本标签
     .attr("transform", function(d) {
                return "translate(" + arc.centroid(d) + ")";
            })
     .attr("text-anchor", "middle")
     .text(d => d.value);

直接使用d3.pie()(dataset)得到的数据序列arcs绘制的饼图是经过排序的,饼图效果是从12点钟开始第一个楔形,顺时针从大到小排列,从上图也可看出,数据的索引没变,arcs[0]还是76,但起始角度为0的数据是90,因此可以重写一下pie函数pie = d3.pie().sort(null),会按照数据的顺序排列饼图的每个楔形。实际中使用排序还是没排序的效果根据需求选择。innerRadius设置为0是饼图,当其大于0可以得到环状图。outerRadius可以理解为整个图表的半径,因为生成的SVG是[600,500]像素,因此把outerRadius设置为高度的一半,绘制的饼图效果较好。

饼图还有两个实用的参数是cornerRadius和padAngle,

•cornerRadius:应用在d3.arc()上,设置每个楔形弧段边缘的圆角效果,类似于<rect>标签的rx属性,rx用来绘制圆角矩形;•padAngle:应用在d3.pie()上,设置每个楔形排列的间隔;

调整这三个参数生成的“饼图”,效果如下:

d3-pie-inner-radius

堆叠布局

d3.stack()将数据变成适合堆叠图的数据格式。

代码语言:javascript
复制
//初始数据
var data=[{"y":76,"z":37},{"y":37,"z":46},
          {"y":90,"z":53},{"y":60,"z":81},{"y":50,"z":60}];
var stack= d3.stack()
           .keys(["y", "z"]);
var ds=stack(data);
//输出的ds:
// [[[0,76],[0,37],[0,90],[0,60],[0,50]],
//  [[76,113],[37,83],[90,143],[60,141],[50,110]] ]

基于这一格式的数据就可以绘制堆叠柱状图以及垂直的堆叠条形图。

d3-stack-bar

直方图

将序列数据处理为适合直方图的形式用d3.bin()

代码语言:javascript
复制
var data=[5.1, 4.9, 8.6, 6.2, 5.1, 7.1, 6.7, 6.1, 5, 5, 5.2, 7.9, 10.5, 5.9, 5.5, 5.6, 6.5, 7.7, 5.7, 6.5, 6.1, 5, 5, 5.2, 7.9,6.7];

var bins= d3.bin().thresholds(10)(data);
var margin = ({top: 20, right: 20, bottom: 30, left: 40});
var x = d3.scaleLinear()
    .domain([bins[0].x0, bins[bins.length - 1].x1])
    .range([40, 400]);
var y = d3.scaleLinear()
    .domain([0, d3.max(bins, d => d.length)]).nice()
    .range([470,20]);
xAxis = g => g
    .attr("transform", `translate(0,${470})`)
    .call(d3.axisBottom(x).ticks(500 / 80 ).tickSizeOuter(0))
    .call(g => g.append("text")
        .attr("x", 450).attr("y", -4)
        .attr("fill", "currentColor")
        .attr("font-weight", "bold")
        .attr("text-anchor", "end")
        .text(data.x));
yAxis = g => g
    .attr("transform", `translate(${40},0)`)
    .call(d3.axisLeft(y).ticks(500/ 80))
    .call(g => g.select(".domain").remove())
    .call(g => g.select(".tick:last-of-type text").clone()
        .attr("x", 4).attr("text-anchor", "start")
        .attr("font-weight", "bold").text(data.y));

var svg = d3.select("body").append("svg")
               .attr("width",'600').attr("height",'500');
    svg.append("g")
       .attr("fill", "steelblue")
       .selectAll("rect").data(bins).join("rect")
       .attr("x", d => x(d.x0) + 1)
       .attr("width", d => Math.max(0, x(d.x1) - x(d.x0) - 1))
       .attr("y", d => y(d.length)).style("fill","#1EAFAE")
       .attr("height", d => y(0) - y(d.length));

  svg.append("g").call(xAxis);
  svg.append("g").call(yAxis);

d3-hist

力导向图

力导向(force-directed)图布局效果通过d3.forceSimulation(nodes).force()实现,将输入的节点表和关系表转换为带坐标点方便SVG里绘制circleline的数据。

代码语言:javascript
复制
var data={"nodes":[{"id":"a","group":1},{"id":"b","group":1},{"id":"c","group":2},
                   {"id":"d","group":1},{"id":"e","group":2}],
          "links":[{"source": "a", "target": "b", "value": 5},
                   {"source": "a", "target": "c", "value": 4},
                   {"source": "b", "target": "d", "value": 1},
                   {"source": "c", "target": "e", "value": 2},
                   {"source": "a", "target": "e", "value": 5}]};

var links = data.links.map(d => Object.create(d));
var nodes = data.nodes.map(d => Object.create(d));
var simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).id(d => d.id))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(400 / 2,400/ 2));

var svg = d3.select("body").append("svg")
            .attr("width",'600').attr("height",'500');
  const link = svg.append("g")
      .attr("stroke", "#ba5c25").attr("stroke-opacity", 0.6)
      .selectAll("line").data(links).join("line")
      .attr("stroke-width", d => Math.sqrt(d.value));

  const node = svg.append("g")
      .attr("stroke", "#fff")
      .attr("stroke-width", 1.5)
    .selectAll("circle").data(nodes)
    .append("circle").attr("r", 5).attr("fill", "#CAE9E0");
  node.append("title").text(d => d.id);
  simulation.on("tick", () => {
    link
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);
    node
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);
  });

弦图

弦图(Chord Diagram)用于表示一组元素之间的联系。输入的数据仍然是节点表nodes和节点间关系表links,弦图将数据节点环状分布,内部通过弦连接,弦的宽度反应连接的强度(values)。数据需要转换为一个NxN的矩阵,矩阵中的a、b、c等在弦图的外圆上用相互分隔的几段弧来表示,对应节点。节点的长度为该元素所在行的总和。

在d3中通过d3.chordDirected()(matrix)得到需要的数据,具体代码如下,因为还需要绘制节点的排布效果,因此会调用d3.arc()

代码语言:javascript
复制
var data={"nodes":[{"id":"a","group":1},{"id":"b","group":1},{"id":"c","group":2},
                   {"id":"d","group":1},{"id":"e","group":2}],
          "links":[{"source": "a", "target": "b", "value": 5},{"source": "a", "target": "c", "value": 4},
                   {"source": "b", "target": "d", "value": 1},{"source": "c", "target": "e", "value": 2},
                   {"source": "c", "target": "a", "value": 6},{"source": "b", "target": "c", "value": 3},
                   {"source": "e", "target": "b", "value": 2},{"source": "a", "target": "e", "value": 5}]};
var links = data.links;
var nodes = data.nodes;
var names= Array.from(new Set(links.flatMap(d => [d.source, d.target])));
var index = new Map(names.map((name, i) => [name, i]));
var matrix = Array.from(index, () => new Array(names.length).fill(0));
for (var {source, target, value} of links) matrix[index.get(source)][index.get(target)]+= value;
var innerRadius = 200,outerRadius = 206;
var width = 440,height = 440;
var ribbon = d3.ribbonArrow().radius(innerRadius - 0.5).padAngle(1 / innerRadius);
var color = d3.scaleOrdinal(names, d3.schemeCategory10)
var chord = d3.chordDirected().padAngle(12 / innerRadius)
    .sortSubgroups(d3.descending).sortChords(d3.descending);
var chords = chord(matrix);
var svg = d3.select("body").append("svg").attr("viewBox", [-600 / 2, -600 / 2, 600, 600]);
var formatValue = x => `${x.toFixed(0)}B`;
var arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
    svg.append("path")
      .attr("fill", "none")
      .attr("d", d3.arc()({outerRadius, startAngle: 0, endAngle: 2 * Math.PI}));

    svg.append("g")
      .attr("fill-opacity", 0.75)
      .selectAll("g")
      .data(chords)
      .join("path")
      .attr("d", ribbon)
      .attr("fill", d => color(names[d.target.index]))
      .style("mix-blend-mode", "multiply");
    svg.append("g")
      .selectAll("g")
      .data(chords.groups)
      .join("g")
      .call(g => g.append("path")
        .attr("d", arc)
        .attr("fill", d => color(names[d.index]))
        .attr("stroke", "#fff"))
      .call(g => g.append("text")
        .attr("dy", -3)
      .append("textPath")
        .attr("startOffset", d => d.startAngle * outerRadius)
        .text(d => names[d.index]));

d3-chord

分层树图

要绘制思维导图等分层的树图,在d3中使用的是d3.hierarchy(data)转换为层级数据,再通过d3.tree()(data)建立树的节点,用svg里的<circle>绘制节点,用<path>绘制边,从而有流线的效果。一个绘制示例如下:

代码语言:javascript
复制
var data={"name":"a","children":[{"name":"b","children":[{"name":"c","value":710}]},
                      {"name":"d","value":250},
                      {"name":"e","value":863}]};
var width = 500;
var root = d3.hierarchy(data);
    root.dx = 30;
    root.dy = width / (root.height*5 + 1);
    root= d3.tree().nodeSize([root.dx, root.dy])(root);
var svg = d3.select("body").append("svg")
        .attr("viewBox", [-20, -100,350,300]);;

var link = svg.append("g").attr("fill", "none")
    .attr("stroke", "#555").attr("stroke-opacity", 0.4)
    .attr("stroke-width", 1.5).selectAll("path")
    .data(root.links()).join("path")
    .attr("d", d3.linkHorizontal().x(d => d.y).y(d => d.x));

var node = svg.append("g").attr("stroke-linejoin", "round")
      .attr("stroke-width", 3).selectAll("g")
      .data(root.descendants()).join("g")
      .attr("transform", d => `translate(${d.y},${d.x})`);

    node.append("circle").attr("fill", d => d.children ? "#555" : "#999")
        .attr("r", 2.5).attr("fill","#1EAFAE");

    node.append("text") //文本标签
        .attr("dy", "0.31em").attr("x", d => d.children ? -6 : 6)
        .attr("text-anchor", d => d.children ? "end" : "start")
        .text(d => d.data.name).clone(true).attr("stroke", "white");

d3-tree

总结

布局实现的是数据的变换,从序列数据或二维数据变换为方便绘制一些主题图的数据,例如变成饼图的每个楔形、变成直方图的分箱统计、力导向图的坐标点和连接线等。在d3的v3.x版本里,饼图、直方图等数据转换函数汇总在layout下。通过d3.layout.pie()使用,而v5.x之后的版本没有了layout的集合,而是使用d3.pie()(data)

本篇笔记学习和实践了饼图、堆叠柱图、直方图、力导向图、弦图及层级树图的绘制。还可以深入学习的有树状图(d3.treemap())、径向堆叠柱状图、汇聚气泡图(d3.pack())、桑吉图(d3.sankey())等等。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 蛰虫始航 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 饼图布局
  • 堆叠布局
  • 直方图
  • 力导向图
  • 弦图
  • 分层树图
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档