基于D3精美柱状图

3,054 阅读8分钟

预览

D3的安装

npm安装:npm install d3
yarn安装:yarn add d3

简单了解相关SVG知识

什么是SVG

SVG是一种XML应用,用来表示矢量图形。所有的图形有关信息被存储为纯文本,具有XML的开放性、可移植性和可交互性。

坐标定位

以页面的左上角为(0,0)坐标点,坐标以像素为单位,x轴正方向是向右,y轴正方向是向下。

矩形的表示以及特性

rect元素,使用x,y,width,height表示一个矩形。
fill属性——填充矩形的颜色,默认为黑色

<svg>
    <rect x=0 y=0 width=50 height=200 fill="red"></rect>
</svg>

效果展示

线段的表示以及特性

line元素,使用x1,y1,x2,y2属性指定线段的起止点坐标。默认为0 stroke属性——笔画颜色 stroke-width属性——笔画的宽度

<svg>
    <line y2=100 stroke="red"></line>
</svg>

效果展示

简单了解相关D3知识

选择集

  • d3.select(selector)
    选中符合条件的第一个元素,选择条件为 selector 字符串。如果没有元素被选中则返回空选择集。
  • d3.selectAll(selector)
    选择所有与指定的 selector 匹配的元素。被选中的元素顺序会按其在文档中的顺序排列(从上到下)。如果没有元素被选中,或者 selector 为 null 或 undefined 则返回空选择集。
d3.selectAll("p")			    //选中所有p元素
d3.select("p")			            //选中第一个p元素
d3.select("#div").selectAll("p")	    //选中第一个id为div下的所有p元素
d3.select("#div").select("p")	            //选中第一个id为div下的第一个p元素

样式、属性和文本的添加

D3里面是支持脸上调用的,和JQuery一样

//给所有的的p标签添加一个active类名,添加文本,改变字体颜色
d3.selectAll("p")
  .attr("class", "active")
  .text("我是p标签")
  .style("color", "green")

数据绑定

  • data()
    绑定一个数组到选择集上,也就是一一对应的关系
//绑定数据之后,文本,属性,样式就可以通过函数的方式使用数组的值
<body>
  <p>dog</p>
  <p>cat</p>
  <p>pig</p>
  
  <script>
    var dataset = ["deer","bitch","tigger"];
    var p = d3.selectAll("p")
      .data(dataset)
      .text((d,i) => {//i是数组的下标,d是数组的值
        return "第"+i+"个动物是"+d;
      });
  </script>
</body>

运行结果:
第0个动物是deer

第1个动物是bitch

第2个动物是tigger

敏锐的你学或许已经发现问题

enter、exit和update的理解

在使用data时,数组和选择集是一一对应的,那么问题来了,数据个数和选择集不匹配怎么办呢?

情形一:数组[1, 2, 3, 4, 5]绑定到三个p标签上。
不难想象,此时数组的最后两个元素没有可以与之绑定的元素,此时D3会建立两个空的元素与数组最后的两个数据相对于,那么这部分就称为Enter。

情形一:数组[1, 2]绑定到三个p标签上。
显而易见,最后一个p标签没有可以绑定的数据,此时没有数据绑定的部分就称为Exit

情形三:数组[1, 2, 3]绑定到三个p标签上。
此时数组和选择集是一一对应的,元素与数据对应的部分就称为Update

元素的添加

  • append()在选择集尾部插入元素
//在body元素内部的最后位置添加一个新的p标签
<body>
  <p>dog</p>
  <p>cat</p>
  <p>pig</p>
  
<script>
    var dataset = ["deer","bitch","tigger","duck"];
    var p = d3.select("body")
	.append("p")
	.text("another animal")
	.style("color","red");
  </script>
</body>

运行结果:

比例尺

比例尺在D3.js中是一个很重要的东西,我们可以这样理解d3.js中的比例尺——一种映射关系,从domain映射到range域。

  • scaleLinear 线性比例尺
    使用d3.scaleLinear()创造一个线性比例尺,而domain()是输入域,range()是输出域,相当于将domain中的数据集映射到range的数据集中。
var scaleLinear = d3.scaleLinear().domain([0,10]).range([0,300]);
console.log(scaleLinear(0))//0
console.log(scaleLinear(5))//150
console.log(scaleLinear(10))//300
  • scaleBand 序数比例尺
    d3.scaleBand()并不是一个连续性的比例尺,domain()中使用一个数组,不过range()需要是一个连续域。
//domain数组里的元素可以是字符串等
let scale = d3.scaleBand().domain([1,2,3,4]).range([0,100])
console.log(scale(1))//0
console.log(scale(2))//25
//当输入不是domain()中的数据集时返回undefined
console.log(scale(5))//undefined

了解一些基础之后,那就开始我们的画图之旅吧

如何绘制

准备工作

  1. CSS样式
<style lang="scss" scoped>
.toolTip {
  position: absolute;
  height: 56px;
  width: 110px;
  border: 0.5px solid #cccccc;
  border-radius: 5px;
  padding-left: 10px;
  display: none;
  font-size: 14px;
  flex-flow: column;
  justify-content: space-around;
  background: #fff;
  .icon {
    display: inline-block;
    height: 8px;
    width: 8px;
    border-radius: 50%;
    margin-right: 5px;
  }
}
</style>
  1. HTML结构
<template>
  <div>
    <h3>国外疫情TOP10——百度</h3>
    <!-- tooltip -->
    <div class="toolTip">
      <div class="country">美国</div>
      <div class="incre">
        <span class="icon"></span>新增:<span class="amount">18951</span>
      </div>
    </div>
    <svg width="1000" height="600"></svg>
  </div>
</template>

矩形的绘制

  1. 定义一些常量来记录一些状态,方便修改。
  2. 由amount的大小通过线性比例尺来决定矩形的高度。
  3. 调整整个图形的位置
<script>
import * as d3 from "d3";
export default {
  data() {
    return {
      epidemicData: [
        { name: "美国", amount: 20685 },
        { name: "俄罗斯", amount: 10899 },
        { name: "巴西", amount: 8446 },
        { name: "印度", amount: 3475 },
        { name: "英国", amount: 3403 },
        { name: "秘鲁", amount: 3237 },
        { name: "墨西哥", amount: 1997 },
        { name: "土耳其", amount: 1704 },
        { name: "巴基斯坦", amount: 1662 },
        { name: "智利", amount: 1658 }
      ]
    };
  },

  mounted() {
    const rectWidth = 20; //定义矩形的宽度
    const yLength = 420; //柱状图y轴的长度
    //定义矩形的高度
    const rectHeight = d3
      .scaleLinear()
      .domain([0, this.maxHeight() + 5000])
      .range([0, yLength]);
    const interval = 20; //定义矩形间的间隔
    //柱状图x轴的长度
    const xLength = (rectWidth + interval) * this.epidemicData.length;
    const baseLine = 450; //定义一个基线的位置,调整柱状图的位置

    let svg = d3.select("svg");
    //g——分组,可以简单的将他视为一个容器的作用
    let g = svg.append("g");
    let rect = g
      .attr("transform", "translate(" + 100 + ", " + 0 + ")")
      .selectAll("rect")
      .data(this.epidemicData)
      .enter()
      .append("rect")
      .attr("x", (d, i) => i * (rectWidth + interval))
      .attr("y", d => baseLine - rectHeight(d.amount))
      .attr("width", rectWidth)
      .attr("height", d => rectHeight(d.amount))
      .attr("fill", d => {
        return this.retColor(d.amount);
      });
  },

  methods: {
    //返回最大值
    maxHeight() {
      return this.epidemicData.reduce((total, curVal) => {
        return total > curVal.amount ? total : curVal.amount;
      }, 0);
    },
    //根据数值范围返回颜色字符串
    retColor(amount) {
      if (amount < 6000) {
        return "#f6b46c";
      } else if (amount < 12000) {
        return "#ea774d";
      } else {
        return "#d92121";
      }
    }
  }
};
</script>

效果展示:

还是好看吧

添加X轴

  1. 先生成普通的x轴
  2. 隐藏刻度
  3. 调整刻度文本的样式
  4. 调整x轴的位置
mounted(){
    //画出x轴
    const xTick = this.epidemicData.length; //x轴的刻度数目
    //为坐标轴定义一个序数比例尺
    let xScale = d3
      .scaleBand()
      .domain(this.address())
      .range([0, xLength]);
    //定义一个朝下的坐标轴
    let xAxis = d3
      .axisBottom(xScale)
      .ticks(xTick)
      .tickPadding(30); //设置刻度和刻度文本之间的间距
    let gx = g
      .append("g")
      .call(xAxis)
      .attr("transform", "translate(" + -45 + ", " + baseLine + ")");
    //更改x轴的文本的样式
    gx.selectAll("text")
      .style("color", "#bebebe")
      .attr("transform", "rotate(" + -45 + ")");
    //将x轴的刻度隐藏起来
    gx.selectAll("line").attr("stroke", "");
    gx.selectAll("path").attr("stroke", "");
},
methods:{
    //将epidemicData中的国家单独抽成一个数组
    address() {
      return this.epidemicData.reduce((total, curVal) => {
        total.push(curVal.name);
        return total;
      }, []);
    }
}

添加y轴

  1. 先生成普通的y轴
  2. 调节刻度
  3. 调整y轴的位置
mounted(){
const yTick = 6; //y轴刻度数目
    //画出y轴
    let gy = g.append("g");
    let yScale = d3
      .scaleLinear()
      .domain([0, this.maxHeight() + 5000])
      .range([yLength, 0]);
    let yAxis = d3.axisLeft(yScale).ticks(yTick);
    
    gy.call(yAxis).attr(
      "transform",
      "translate(" + -10 + ", " + (baseLine - yLength) + ")"
    );
    //更改y轴的刻度的样式
    gy.selectAll("line")
      .attr("stroke", "#CCC")
      .attr("x2", xLength)
      .attr("stroke-width", 0.5);
    gy.selectAll("path").attr("stroke", "");
    gy.selectAll("text").style("color", "#bebebe");
}

此时你画出的图形,y轴刻度应该遮挡住了我们的柱状图,因为d3没有向z-index这种直接调节层级的方法, 所以我们只能将y轴分组的添加提到最前面去。即将let gy = g.append("g");提到前面

添加了x轴和y轴以后的样子

添加动画

  1. 添加鼠标移动到柱状图上颜色改变,鼠标移除颜色还原的动画
  2. 添加tooltip动画
  3. 节流
mounted(){
    const _this = this; //将Vue中的this用_this保存下来
    rect
      .on("mousemove", _this.throttle(_this.moveEvent, 200))
      .on("mouseover", function() {
        d3.select(this)
          .transition()
          .duration(0)
          .attr("fill", "#f9d774");
      })
      .on("mouseout", function(d) {
        d3.select(".toolTip").style("display", "none");
        d3.select(this)
          .transition()
          .attr("fill", _this.retColor(d.amount));
      });
},
methods:{
    //节流
    throttle(fn, duration) {
      let lastTime = 0;
      return function(params) {
        let now = new Date().getTime();
        if (now - lastTime > duration) {
          fn(params);
          lastTime = new Date().getTime();
        }
      };
    },
    //在矩形上移动产生的变化
    moveEvent(d) {
      let mouse = d3.event;
      let yPosition = mouse.pageY + 20;
      let xPosition = mouse.pageX + 20;
      let toolTip = d3
        .select(".toolTip")
        .style("left", xPosition + "px")
        .style("top", yPosition + "px")
        .style("display", "flex");
      d3.select(".country").text(d.name);
      d3.select(".amount").text(d.amount);
      d3.select(".icon").style("background", "red");
    }
}

最后

若有不足之处还请指教,如有疑惑可留下评论一起探讨。

码字不易,喜欢的伙伴点个赞支持一下啦❤