资讯专栏INFORMATION COLUMN

SVG绘制环图

AlphaWallet / 1613人阅读

摘要:上篇原生绘制饼图介绍了如何使用来绘制环图,这篇用标签来实现一下。在标签里,插入其它元素,来进行绘图元素用于定义一个路径,使用标签来绘制每项数据的环图份额元素用于定义一条曲线,使用绘制每项数据的线条元素用于定义文本使用显示数据的文本信息。

上篇<原生Canvas绘制饼图>介绍了如何使用Canvas来绘制环图,这篇用SVG标签来实现一下。

上面是完整效果图,下面来看看具体实现。

使用的SVG元素

:SVG代码的开始标签,相当于创建一个画布。在svg标签里,插入其它SVG元素,来进行绘图;

: path元素用于定义一个路径,使用path标签来绘制每项数据的环图份额;

:polyline元素用于定义一条曲线,使用polyline绘制每项数据的线条;

: text元素用于定义文本,使用text显示数据的文本信息。

以上是会用到的几个SVG标签,详细说明可以看看菜鸟教程SVG或者MDN的SVG教程

标签创建画布

SVG是一种用来描述二维矢量图形的XML标记语言,所以SVG标签不能使用document.createElement直接创建(浏览器无法识别)。需要使用document.createElementNS创建一个具有指定的命名空间URI和限定名称的元素,SVG的命名空间是"http://www.w3.org/2000/svg"。这里将创建SVG标签操作写在了createSvgTag函数里。下面先新建一个svg元素:

/**
 * 将创建环图的所有操作写在drawPie函数内,配置一些默认参数
 * @param  {Element} element [插入SVG的元素,缺省直接插入到body]
 * @param  {Number}  R       [外弧起终点计算半径]
 * @param  {Number}  r       [内弧起终点计算半径]
 * @param  {Number}  width   [画布宽度]
 * @param  {Number}  height  [画布高度]
 * @param  {Array}   data    [图表数据]
 */
function drawPie({element, R = 140, r = 100,width = 450,height = 400,data = []} = {}) {

    let w = width;
    let h = height; //将width、height赋值给w、h
    let origin = [w / 2, h / 2]; //以画布的中心点,作为环图的原点
    
    //创建一个svgs标签
    let svg =  createSvgTag("svg", {
        "width": w + "px",
        "height": h + "px",
        "viewBox": `0 0 ${w} ${h}`,        
    }); 
    
    (element && element.appendChild) ? element.appendChild(svg) : document.body.appendChild(svg);//插入到DOM
    /**
     * 将创建SVG标签写成一个函数
     * @param  {tring} tagName    [标签名]
     * @param  {Object} attributes [标签属性]
     * @return {Element} svg标签
     */
    function createSvgTag (tagName, attributes) {
        let tag = document.createElementNS("http://www.w3.org/2000/svg", tagName)
        for (let attr in attributes) {
            tag.setAttribute(attr, attributes[attr])
        }
        return tag;
    }
    
})
//调用
drawPie({
    data: [{
        cost: 4.94,
        category: "通讯",
        color: "#e95e45",
    }, {
        cost: 4.78,
        category: "服装美容",
        color: "#20b6ab",
    }, {
        cost: 4.00,
        category: "交通出行",
        color: "#ef7340",
    }, {
        cost: 3.00,
        category: "饮食",
        color: "#eeb328",
    }, {
        cost: 49.40,
        category: "其他",
        color: "#f79954",
    }, {
        cost: 28.77,
        category: "生活日用",
        color: "#00a294",
    }]
})
绘制每项数据的环图份额

元素的属性d用于定义路径,属性d实际上是一个字符串,包含了一系列路径描述。这些路径由下面这些指令组成:Moveto,Lineto,Curveto,Arcto,ClosePath。
我们会用到的指令有:

Moveto(移动画笔到起始点),语法:"M x,y" 在这里x和y是绝对坐标,分别代表水平坐标和垂直坐标;

Lineto(绘制直线),语法:"L x, y" 在这里x和y是绝对坐标,表示直线的结束点坐标;

Arcto(绘制弧曲线路径),语法:"A rx,ry xAxisRotate LargeArcFlag,SweepFlag x,y",rx和ry分别是x和y方向的半径(绘制圆弧时,rx和ry相等);LargeArcFlag的值确定是要画小弧或大弧,0表示画小弧(即画两点之间弧长最小的弧),1表示画大弧(当弧度大于Math.PI时需要画大弧);SweepFlag用来确定画弧的方向,0逆时针方向,1顺时针方向;x和y是目的地的坐标;

ClosePath(闭合路径),语法是"Z"或"z";

详情MDN path元素d属性。
我们需要用path绘制如下的路径:

如图:份额的绘制是先使用M命令移动到P0,L命令绘制一条直线到P1,A命令从P1画弧到P2,L命令从P2绘制一条直线到P3,A命令从P3绘制一条弧线到P0,最后Z命令关闭路径。然后我们只要填充这个路径就可以完成每项份额绘制了。这里我们需要求出4个点的绝对坐标,如何计算这四个坐标?

如图,通过三角函数,我们就可以计算出每个点的位置。我们已知原点O坐标(画布中点)、外环半径R和内环半径r(我们自己给定);再通过计算出每项数据的弧度占比,我们就可以得到每项数据的起始弧度(即上一项的结束弧度,第一项为0)和结束弧度(起点+项数据的弧度占比)。x值为原点x+sin(angel)半径,y值为原点y-cos(angel)半径
这里将计算点坐标的运算写在evaluateXY函数中,如下:

/**
 * 计算Xy坐标
 * @param  {[type]} r      [半径]
 * @param  {[type]} angel  [角度]
 * @param  {[type]} origin [原点坐标]
 * @return {[Array]} 坐标
 */
function evaluateXY (r, angel, origin) {
    return [origin[0] + Math.sin(angel) * r, origin[0] - Math.cos(angel) * r]                                                                                  
}

接下来,我们遍历数据,计算出每项数据的四个点:

//遍历计算每项数据
for(let v of data) {
    let itemData = Object.assign({}, v);//copy一遍,不直接修改原数据
    eAngel = sAngel + v.cost / total * 2 * Math.PI; //计算结束弧度
    itemData.arclineStarts = [
        evaluateXY(r, sAngel, origin), //计算P0坐标
        evaluateXY(R, sAngel, origin), //计算P1坐标 
        evaluateXY(R, eAngel, origin), //计算P2坐标 
        evaluateXY(r, eAngel, origin)  //计算P3坐标
        ];

    itemData.LargeArcFlag = (eAngel - sAngel) > Math.PI ? "1" : "0";//大于Math.PI需要画大弧,否则画小弧
    drawData.push(itemData);//保存到drawData数组中,绘制时遍历
    sAngel = eAngel;//将下一项数据的起始弧度设置为当前项的结束弧度
}

下面,遍历drawData,绘制环图:

//遍历计算每项数据,进行绘制
for(let v of drawData) {
    let P = v.arclineStarts;
    let path = createSvgTag("path", {
        "fill": v.color, //设置填充色
        "stroke": "black",
        "stroke-width": "0", //画笔大小为零
        /**
         * d属性设置路径字符串
         * M ${P[0][0]} ${P[0][1]} 移动画笔到P0点
         * L ${P[1][0]} ${P[1][1]} 绘制一条直线到P1
         * A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} 绘制外环弧到P2,R为外半径,v.LargeArcFlag画大弧还是小弧
         * L ${P[3][0]} ${P[3][1]} 绘制一条直线到P3
         * A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} 绘制内环弧到P0点
         * z 关闭路径
         */
        "d": `M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]} A ${r} ${r}  0 ${v.LargeArcFlag} 0 ${P[0][0]} ${P[0][1]} z`
    })
    svg.appendChild(path); //添加到画布中
} 

到这里,就已经绘制出如下环图了:

记得还需要把相关变量先声明一下。

绘制线条、文字

下面我们需要绘制线条和文字。

绘制线条需要的数据

起点坐标:这里设置起点为每项数据的弧线中间位置,通过计算中间位置对应的弧度,就可以通过三角函数计算出这个点坐标;

线束点坐标:当线条起点在右侧时,线束点就是垂直平行起点图表最右侧位置;当线条起点在左侧时,线束点就是垂直平行起点图表最右左位置;假设起点为[sx,sy],右左结束点应该就是[w,sy]、[0,sy],w为图表宽度;

折点:需要处理数据会挤在一起的情况,就会改变结束点坐标的y值,当起点和结束点y值不相等时,就需要设置折点。

绘制文字:调整过后的线束点,就是文字的位置。
以下是完整代码:




    
    svg-pie


    

也可以查看gitee。

总结一下

难点:难的地方就在弧上各点的计算,需要好好再回忆一下数学的三角函数。

对比canvas:canvas只需有一个标签,svg实现就在DOM中增加了一堆标签。这样一来,svg的优势就在于第项都是一个标签,你可以直接针对这个标签要绑定事件和做修改。比如要实现鼠标称到某个项,放大这个项,svg只要给每个path绑定事件,修改当前的这个path就行;而canvas只能在canvase绑定事件,先通过计算鼠标位置来判断移动到了哪个份额上,然后再重绘整个canvas;同样,标签过多也是svg的缺点,我们这点标签其实没什么,一旦标签多起来,肯定是会给浏览器渲染带来负担的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94533.html

相关文章

  • Graph 数据可视化:JS 自动布局有向无环图

    摘要:可以用于模型化许多不同种类的信息,因此将一个数据结构可视化的需求也变得非常普遍。并且由于大部分图的数据都非常复杂甚至动态变化,所以自动可配置的可视化布局算法显然比人为排版更为高效且可靠。 有向无环图(DAG)布局 有向无环图及其布局算法 有向无环图(directed acyclic graph,以下简称 DAG)是一种常见的图形,其具体定义为一种由有限个顶点和有限条带有方向的边组成的图...

    zhouzhou 评论0 收藏0
  • 有向无环图自动布局

    摘要:判断是否成环执行拓扑排序,如果序列中的顶点数不等于有向图的顶点个数,则说明图中存在环。如果访问过,且不是其父节点,那么就构成环图有向无环图的最小路径覆盖图存储邻接矩阵图的邻接矩阵存储方式是用两个数组来表示图。 何为有向无环图? 1、首先它是一个图,然后它是一个有向图,其次这个有向图的任意一个顶点出发都没有回到这个顶点的路径,是为有向无环图2、DAG(Directed Acyclic G...

    shenhualong 评论0 收藏0
  • FireFox下Canvas使用图像合成绘制SVG的Bug

    摘要:本文适合适合对绘制图形学前端可视化感兴趣的读者阅读。结论已经明显浏览器下,用下绘制绘制图的时候,的设置将不生效。下面是一段用于测试的代码,表示用源图像的形状去挖空目标图像。后续绘制用临时的替代图片。 本文适合适合对canvas绘制、图形学、前端可视化感兴趣的读者阅读。 楔子 所有的事情都会有一个起因。最近产品上需要做一个这样的功能:给一些图形进行染色处理。想想这还不是顺手拈来的事情,早...

    mudiyouyou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<