上篇<原生Canvas繪製餅圖>介紹瞭如何使用Canvas來繪製環圖,這篇用SVG標籤來實現一下。
html
上面是完整效果圖,下面來看看具體實現。git
以上是會用到的幾個SVG標籤,詳細說明能夠看看菜鳥教程SVG或者MDN的SVG教程算法
SVG是一種用來描述二維矢量圖形的XML標記語言,因此SVG標籤不能使用document.createElement直接建立(瀏覽器沒法識別)。須要使用document.createElementNS建立一個具備指定的命名空間URI和限定名稱的元素,SVG的命名空間是'http://www.w3.org/2000/svg'。這裏將建立SVG標籤操做寫在了createSvgTag函數裏。下面先新建一個svg元素:canvas
/** * 將建立環圖的全部操做寫在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", }] })
<path>元素的屬性d用於定義路徑,屬性d其實是一個字符串,包含了一系列路徑描述。這些路徑由下面這些指令組成:Moveto,Lineto,Curveto,Arcto,ClosePath。
咱們會用到的指令有:segmentfault
詳情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,繪製環圖:app
//遍歷計算每項數據,進行繪製 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); //添加到畫布中 }
到這裏,就已經繪製出以下環圖了:
記得還須要把相關變量先聲明一下。svg
下面咱們須要繪製線條和文字。 函數
<polyline>繪製線條須要的數據
<text>繪製文字:調整事後的線束點,就是文字的位置。
如下是完整代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>svg-pie</title> </head> <body> <div id="svgWrap" class="svg-wrap"></div> <script> /** * 將建立環圖的全部操做寫在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]; //原點座標 let drawData = [];//保存遍歷後可直接繪製的數據 let sAngel = 0;//保存每項數據的起始點角度 let eAngel = sAngel;//保存每項數據的結束角點度 let cAngel ;//保存每項數據的中點角度 let leftPoints = []; //保存在左邊的點 let rightPoints= []; //保存在右邊的點,分出左右是爲了計算兩點垂直間距是否靠太近 let tAngel = Math.PI * 2; let minPadding = 40; //設置數據項兩點最小間距 //total保存總花費,用於計算數據項佔比 let total = data.reduce(function(v, item) { return v + item.cost; }, 0) //建立svg標籤,設置畫布 let svg = createSvgTag('svg', { 'width': w + 'px', 'height': h + 'px', 'viewBox': `0 0 ${w} ${h}`, }); //遍歷計算每項數據,生成繪製數據 for(let v of data) { let itemData = Object.assign({}, v);//copy一遍,不直接修改原數據 let isLeft = false; eAngel = sAngel + v.cost / total * tAngel;//計算結束弧度 itemData.arclineStarts = [ evaluateXY(r, sAngel, origin), //計算P0座標 evaluateXY(R, sAngel, origin), //計算P1座標 evaluateXY(R, eAngel, origin), //計算P2座標 evaluateXY(r, eAngel, origin) //計算P3座標 ]; //大於Math.PI須要畫大弧,不然畫小弧 itemData.LargeArcFlag = (eAngel - sAngel) > Math.PI ? '1' : '0'; //計算線條起始點公位置 itemData.lineStart = evaluateXY(R, sAngel + (eAngel - sAngel)/2, origin); //線條起點x值小於原點x值,在左側,不然在右側 itemData.isLeft = isLeft = itemData.lineStart[0] < origin[0]; //根據線條起點左右,設置結束點 itemData.lineEnd = [(isLeft ? 0 : w), itemData.lineStart[1]]; //線條起點y值小於原點y值,在上部,不然在下部,用於確實過擠往上/下移動 itemData.top = itemData.lineStart[1] < origin[1]; //根據線條起點左右,添加到leftPoints/rightPoints,用於處理過擠 isLeft? leftPoints.push(itemData) : rightPoints.push(itemData); drawData.push(itemData)//保存到drawData數據中,繪製時遍歷 sAngel = eAngel;//將下一項數據的起始弧度設置爲當前項的結束弧度 } makeUseable(rightPoints); //處理右邊擠在一塊兒的狀況 makeUseable(leftPoints.reverse(), true); //處理左邊擠在一塊兒的狀況,爲何要翻轉一下,看makeUseable函數 //遍歷drawData開始繪製 for(let v of drawData) { let P = v.arclineStarts;//將path路四個點變量,賦值給變量p //建立path標籤(份額) 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` }) //設置線條點 let linePoints = v.lineStart[0] + ' ' + v.lineStart[1]; //設置起點 //若是有折點,添加折點 v.turingPoints && (linePoints += ',' + v.turingPoints[0] + ' ' + v.turingPoints[1]); //設置結束點 linePoints += ',' + v.lineEnd[0] + ' ' + v.lineEnd[1]; //建立polyline標籤(線條) let polyline = createSvgTag('polyline', { points: linePoints, style: `fill:none;stroke:${v.color};stroke-width:.5` }) //建立text標籤,顯示花費 let cost = createSvgTag("text", { 'x': v.lineEnd[0], 'y': v.lineEnd[1], 'dy': -2, style: `fill:${v.color};font-size:12px;text-anchor: ${v.isLeft? 'start':'end'};` }) cost.innerHTML = v.cost; //建立text標籤,顯示花費分類 let category = createSvgTag("text", { 'x': v.lineEnd[0], 'y': v.lineEnd[1], 'dy': 14, style: `fill:${v.color};font-size:12px;text-anchor: ${v.isLeft? 'start':'end'};` }) category.innerHTML = v.category; svg.appendChild(path); //path(份額)添加到畫布中 svg.appendChild(polyline);//polyline(線條)添加到畫布中 svg.appendChild(cost);//text(花費)添加到畫布中 svg.appendChild(category);//text(花費分類)添加到畫布中 } (element && element.appendChild) ? element.appendChild(svg) : document.body.appendChild(svg); //插入到DOM return svg; /** * 計算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] } /** * 將建立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; } function isUseable(arr) { // 判斷是否會有數據擠在一塊兒(兩點最小間距是否都大於等於minPadding) if (arr.length <= 1) return true; return arr.every(function(p, index, arr) { if (index === arr.length-1) { //由於是當前項和下一項比較,因此index === arr.length-1直接返回true return true; } else { /** * 判斷當前數據項結束點:p.lineEnd[1] * 和下一數據項結束點垂直間距是否大於等於最小間距:minPadding * 只有數據線條結束點垂直間距大於等於最小間距,纔會返回true */ return arr[index + 1].lineEnd[1] - p.lineEnd[1] >= minPadding; } }) } function makeUseable(arr, left) {// 處理擠在一塊兒的狀況 let diff, turingAngel, x, maths = Math.sin,diffH, l; /** * 這裏的思路是 * 若是數據是非可用的(會擠在一塊兒,isUseable判斷) * 就一直循環移動數據,直至可用 * 可能會有更好的算法,我這魚木腦殼只想到這種的 * 歡迎你們提供更好的思路或算法 */ while (!isUseable(arr)) { //每次循環處理一次,直至數據不會擠在一塊兒 for (let i = 0, len = arr.length - 1; i < len; i++) { //遍歷數組 diff = arr[i + 1].lineEnd[1] - arr[i].lineEnd[1]; //計算兩點垂直間距 if (diff < minPadding) { //小於最小間距,表示會擠到一塊兒 if (arr[i].top && arr[i + 1].top) { //是在上部的點,向上移動 /** * 判斷當前的點是否還能夠向上移動 * 上方第一個點最往上只能夠移動到y值爲0 * 以後依次最往上只能移動動y值爲:i * minPadding * 因此下面判斷應該是:arr[i].lineEnd[1] - (minPadding - diff) > i * minPadding */ /** * 上面左邊leftPoints的點須要翻轉一下的緣由是 * 左邊leftPoints的點最上面的點是排在最後的 */ if (arr[i].lineEnd[1] - (minPadding - diff) > 0 && arr[i].lineEnd[1] > i * minPadding) { //當前點還能向上移動 //向上移動到不擠(知足最小間距) arr[i].lineEnd[1] = arr[i].lineEnd[1] - (minPadding - diff); } else { //當前點不向上移動到知足最小間距的位置 //先把當前點移動到可以移動的最上位置 arr[i].lineEnd[1] = i * minPadding; //再把下個點移動,使知足最小間距 arr[i + 1].lineEnd[1] = arr[i + 1].lineEnd[1] + (minPadding - diff); } } else { //是在下部的點,向下移動 /** * 判斷當前點的下個點是否還能夠向下移動 * 下方最後一個點最往下只能夠移動到y值爲h,即圖表高度 * 以前的點依次最往下只能移動動y值爲:h - (len - i - 1) * minPadding * 因此下面判斷應該是:arr[i + 1].lineEnd[1] + (minPadding - diff) < h - (len - i - 1) * minPadding */ if (arr[i + 1].lineEnd[1] + (minPadding - diff) < h && arr[i + 1].lineEnd[1] < h - (len - i - 1) * minPadding) { //當前點的下個點還能向下移動 //當前點的下個點向下移動到不擠(知足最小間距) arr[i + 1].lineEnd[1] = arr[i + 1].lineEnd[1] + (minPadding - diff) } else { //當前點的下個點不能向下移動 //先把當前點的下個點向下移動可以移動的最下位置 arr[i + 1].lineEnd[1] = h - (len - i - 1) * minPadding; //再把當前點移動,使知足最小間距 arr[i].lineEnd[1] = arr[i].lineEnd[1] - (minPadding - diff); } } break; //每次移動完成直接退出循環,判斷一次是否已經不擠 } } } /** * 遍歷已經可用的數據 * 起點和結束點不在同一水平線上 * 須要設置折點 * 設置折點爲線束點水平距離40像素的位置 */ for (let i = 0, len = arr.length; i < len; i++) { if (arr[i].lineStart[1] !== arr[i].lineEnd[1]) { arr[i].turingPoints = [arr[i].lineEnd[0] + (left ? 40 : -40) , arr[i].lineEnd[1]]; } } } } drawPie({ element: document.getElementById('svgWrap'), 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", }] }) </script> </body> </html>
也能夠查看gitee。