小白用svg画饼图 | 🏆 技术专题第三期征文

1,985 阅读1分钟

饼图

上次年中总结的时候说过,试用svg画了个饼图,这篇文章就详细说一下过程.本文是在vue-cli脚手架搭建的项目下,写的一个组件


环境

版本

  • vue2.*

  • snapsvg --> 0.5.1

  • imports-loader --> 0.8.0

    这里需要注意的是imports-loader的版本,版本不同会导致下面的引入方式出错

安装

npm install --save snapsvg || yarn add snapsvg
npm install --save-dev imports-loader@0.8.0 || yarn add imports-loader@0.8.0 --save-dev

引入

// 在vue组件中引入,
const snap = require(`imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js`)

动手

思路

  1. 根据数据算出占比
  2. 根据占比算角度
  3. 根据角度和初始坐标计算结束的坐标

代码

pie.vue

<template>
	<svg :id="id" :width="width" :height="height"></svg>
</template>

<script>
const snap = require(`imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js`);

export default {
  name: "svg-pie",
  props: {
    width: {
      // eslint-disable-next-line vue/require-prop-type-constructor
      type: Number | String,
      default: "100%",
    },
    height: {
      // eslint-disable-next-line vue/require-prop-type-constructor
      type: Number | String,
      default: "100%",
    },
    r: {
      type: Number,
      default: 70,
    },
    id: {
      type: String,
      required: true,
    },
    title: {
      type: String,
      required: true,
    },
    num: {
      type: Number,
      required: true,
    },
    cx: {
      type: Number,
      default: 85,
    },
    cy: {
      type: Number,
      default: 85,
    },
    data: {
      type: Array,
      required: true,
    },
  },
  mounted() {
    this.init();
  },
  watch: {
    data() {
      this.init();
    },
  },
  methods: {
    init() {
      // 如果data没有数据 return
      if (this.data.length < 0) return;
      // snap svg实例
      let s = snap(`#${this.id}`);
      // 半径
      let r = this.r;
      let cx = this.cx;
      let cy = this.cy;
      let sx = cx;
      let sy = cy - r;
      let srartX = sx;
      let srartY = sy;
      let endX;
      let endY;
      let angles = 0;
      let largeArcFlag;
      let dataArry = this.data;
      let lh = 16;
      let domArr = [];
      dataArry.forEach((v, i) => {
        // 计算角度
        let angle = (360 * v.ratio) / 100;
        angles += angle;
        let radian;
         // 这里计算下一个坐标  很繁琐 需要优化
        if (angles < 90) {
          radian = ((2 * Math.PI) / 360) * angles;
          endX = sx + r * Math.sin(radian);
          endY = sy + r - r * Math.cos(radian);
        } else if (angles === 90) {
          endX = sx + r;
          endY = sy + r;
        } else if (angles < 180) {
          radian = ((2 * Math.PI) / 360) * (180 - angles);
          endX = sx + Math.abs(r * Math.cos(radian));
          endY = sy + r + Math.abs(r * Math.sin(radian));
        } else if (angles === 180) {
          endX = sx;
          endY = sy + 2 * r;
        } else if (angles < 270) {
          radian = ((2 * Math.PI) / 360) * (270 - angles);
          endX = sx - Math.abs(r * Math.sin(radian));
          endY = sy + r + Math.abs(r * Math.cos(radian));
        } else if (angles === 270) {
          endX = sx - r;
          endY = sy + r;
        } else if (angles === 360) {
          endX = sx;
          endY = sy;
        } else {
          radian = ((2 * Math.PI) / 360) * (360 - angles);
          endX = sx - Math.abs(r * Math.sin(radian));
          endY = sy + r - Math.abs(r * Math.cos(radian));
        }
        largeArcFlag = v.ratio > 50 ? 1 : 0;
        let dom = s
          .path(
            `M${srartX},${srartY}A${r},${r},${angles},${largeArcFlag},1,${endX},${endY}`
          )
          .attr({
            stroke: v.color,
            strokeWidth: 20,
            fillOpacity: 0,
            cursor: "pointer",
          })
          .hover(
            function () {
              this.animate({
                  strokeWidth: 35,
                },
                100
              );
            },
            function () {
              this.animate({
                  strokeWidth: 20,
                },
                100
              );
            }
          );
        domArr.push(dom);
        s.circle(cx - r, cy + r + 20 + lh, 3)
          .attr({
            fill: v.color,
            strokeWidth: 0,
            stroke: "#000",
          })
          .hover(
            function () {
              domArr[i].animate({
                  strokeWidth: 35,
                },
                100
              );
            },
            function () {
              domArr[i].animate({
                  strokeWidth: 20,
                },
                100
              );
            }
          );
        let titleDom = s
          .text(cx - r + 10, cy + r + lh + 25, v.title)
          .hover(
            function () {
              domArr[i].animate({
                  strokeWidth: 35,
                },
                100
              );
            },
            function () {
              domArr[i].animate({
                  strokeWidth: 20,
                },
                100
              );
            }
          )
          .attr({
            cursor: "pointer",
          });
        if (v.blodArr) {
          v.blodArr.forEach((val) => {
            titleDom.node.childNodes[val].style.fontWeight = "bold";
          });
        }
        lh += 20;
        srartX = endX;
        srartY = endY;
      });
      s.circle(cx, cy, r - 7.5).attr({
        fill: "#e3eaef",
        strokeWidth: 0,
      });
      s.circle(cx, cy, r - 15).attr({
        fill: "#fff",
        strokeWidth: 0,
      });
      s.text(cx, cy - 8, this.title).attr({
        textAnchor: "middle",
      });
      s.text(cx, cy + 20, this.num).attr({
        fontSize: "16px",
        fill: "#6462fe",
        fontWeight: "bold",
        textAnchor: "middle",
      });
    },
  },
};
</script>

使用

App.vue

<template>
<div id="app">
  <Pie id="course" title="总数" :num="22" :r="60" :width="600" :height="600" :cy="100" :data="data" />
</div>
</template>

<script>
import Pie from "./components/pie.vue";

export default {
  name: "App",
  components: {
    Pie,
  },
  data() {
    return {
      data: [{
          blodArr: [2, 5],
          color: "#635ffd",
          num: 11,
          percent: 50,
          ratio: 50,
          title: ["类型一", " ", 11, "个", " ", 50, "%"]
        },
        {
          blodArr: [2, 5],
          color: "#666",
          num: 11,
          percent: 50,
          ratio: 50,
          title: ["类型二", " ", 11, "个", " ", 50, "%"]
        }
      ],
    };
  },
};
</script>

各属性的意义

属性名说明
title中间圆盘的内容
num中间圆盘的内容
r半径
widthsvg宽
heightsvg高
cy圆心y坐标
cx圆心x坐标
data数据
data各字段详解
key说明
color该饼图的颜色
num数量
ratio占比
title说明, 可以是字符串和数组
blodArrtitle内容要加粗的下标(title为数组时生效)

成果

总结

难点在于path画线时的坐标计算, 在这里对我数学老师道歉, 我感觉自己就是个废人了!!! 看完点个赞吧!!

🏆 技术专题第三期 | 数据可视化的那些事......