脚本宝典收集整理的这篇文章主要介绍了

原生Canvas绘制饼图,我是不是被骗代码了

脚本宝典小编觉得挺不错的,现在分享给大家,也给大家做个参考,希望能帮助你少写一行代码,多一份安全和惬意。
<p><code></p> <p>  这回算是真明白了什么叫"林子大了什么鸟都有!"之前就有听说面试骗代码的情况,但也仅仅只是听说。这回是真亲身遇到了。来来来,自带小板凳,准备好瓜子。好好看看我被骗的经历。顺便也看看使用原生Canvas绘制饼图,使用插件(比如E<a href="http://www.js-code.com/tag/char" title="char" target="_blank">char</a>t)也就分分钟的事情,但多了解一些原生的东西,总不会有错的。 <br />  正文开始.....</p> <h3 id="articleHeader0">我是不是被骗代码了???</h3> <p>  还是前段时间面试时发生的事情。3月21号晚八点,此时心态已处于第三阶段(详情可查看<a href="https://segmentfault.com/a/1190000014028422?_ea=3562530">面试总结</a>),突然收到一封邮件,如下: <br />   <br /><span class="img-wrap"><img data-src="/img/remote/1460000014218700?w=691&amp;h=237" src="/img/remote/1460000014218700?w=691&amp;h=237" alt="邮件1" title="邮件1" style="cursor: pointer; display: inline;"></span><br />   <br />  巧了,3月22有两场面试,还是两家我觉得不错的公司(南方+、爱范儿科技),我误以为就是这两家其中一家的测试。 <br />  熬到22号两点,饼图倒是画出来了,只是线条还有很大问题。当时的想法是通过计算位置,使用<a href="http://www.js-code.com/tag/div" title="浏览关于“div”的文章" target="_blank" class="tag_link">div</a>来画线条。这有两个问题:一是无法实现拆线;二是会不准。因为白天还有面试,所以就直接发了半成品过去,并询问是什么公司。对话如下: <br />   <br /><span class="img-wrap"><img data-src="/img/remote/1460000014218701?w=769&amp;h=397" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="邮件2" title="邮件2" style="cursor: pointer;"></span><br />   <br />  居然还没约面试,只有想会是哪家公司呢?反正没往骗代码上想!3月23,继续尝试了一下,线条也通过canvas来绘制,解决了之前的两个问题,还处理考虑挤一起的需求,算得上已经实现需求。效果如下: <br /><span class="img-wrap"><img data-src="/img/remote/1460000014218702?w=450&amp;h=400" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="效果" title="效果" style="cursor: pointer;"></span><br />  3月23晚上,发送过去。3月25晚上,收到回复确是这样:</p> <blockquote><p>你好,舒同学。看了你的作品,能否再完善一下?因为这是仿支付宝的饼图,所以希望是适配于移动设备的,另外APP里的Webview好像要在6.0以上才支持<a href="http://www.js-code.com/tag/es6" title="es6" target="_blank">es6</a>语法,想把它转成<a href="http://www.js-code.com/tag/es5" title="es5" target="_blank">es5</a>语法的,麻烦舒同学了</p></blockquote> <p>  到这里我才开始觉得不对劲。 <strong>为啥要ES6转ES5,又体现不了什么技术能力,又不是实际使用;手机适配的问题,我这大小是可配置的并没有写死</strong> 。所以,马上询问是什么公司。回复如下:</p> <blockquote><p>林老师,测试题的目的应该就是了解一下应聘者的能力。我想,题目做到现在,我大概的代码风格和技术能力,你应该了解了。 <br />请问贵公司是?</p></blockquote> <p>  然后。。。然后就再没收到回复。。。。 <br />  这里我才想到自己是不是被骗代码了?可现在都不敢相信呀,这种代码也有人骗么?可如果不是,难道我这代码写得太low了,所以连个面试机会都拿不到? <br />  所以,这里贴上代码,分享一下生Canvas绘制饼图的想法,同时也让大家帮忙看看,这样的代码能不能得到一次面试机会呀![笑哭]*10 <br />   <br />   <br />  </p> <h3 id="articleHeader1">饼图绘制代码</h3> <p><strong>稍微有些难的几个点:</strong></p> <ol> <li>会用到三角函数各种计算坐标,如果早已忘记,需要回头看看;</li> <li>如何处理点会挤在一的情况;</li> <li>canvas的画弧方法arc的0度是从笛卡坐标的90度开始,角度不一致需要区分;</li> </ol> <p>  下面是完整的代码,有完成的注释,代码比注释还多。</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="<a href="http://www.js-code.com/tag/button" title="button" target="_blank">button</a>" <a href="http://www.js-code.com/tag/class" title="class" target="_blank">class</a>="copyCode code-tool" data-toggle="tooltip" data-placement="<a href="http://www.js-code.com/tag/top" title="top" target="_blank">top</a>" data-clipboard-<a href="http://www.js-code.com/tag/text" title="text" target="_blank">text</a>="<!DOCTYPE html><br /> <html lang=&quot;en&quot;><br /> <head><br /> <meta charset=&quot;UTF-8&quot;><br /> <title>饼图</title><br /> </head><br /> <body></p> <p> <script> /** * 绘制饼图函数 * 使用到的ES6语法有函数<a href="http://www.js-code.com/tag/%e9%bb%98%e8%ae%a4%e5%8f%82%e6%95%b0" title="默认参数" target="_blank">默认参数</a>、解构、字符模板 * 如果不熟悉,可以看看阮老师的《<a href="http://www.js-code.com/tag/ecmascript" title="ECMAScript" target="_blank">ECMAScript</a> 6 入门》 * 网址 http://<a href="http://www.js-code.com/tag/es6" title="es6" target="_blank">es6</a>.ruany<a href="http://www.js-code.com/tag/if" title="if" target="_blank">if</a>eng.com/ * 函数的<a href="http://www.js-code.com/tag/%e9%bb%98%e8%ae%a4%e5%8f%82%e6%95%b0" title="默认参数" target="_blank">默认参数</a> * r 圆环的圆半径 data 数据项 * width 图表宽度 height 图表高度 */ <a href="http://www.js-code.com/tag/function" title="function" target="_blank">function</a> addPie({r = 100,width = 450,height = 400,data = []} = {}) {</p> <p> <a href="http://www.js-code.com/tag/let" title="let" target="_blank">let</a> cns = <a href="http://www.js-code.com/tag/do" title="do" target="_blank">do</a>cument.createElement('canvas'); //创建一个canvas <a href="http://www.js-code.com/tag/let" title="let" target="_blank">let</a> ctx = cns.getCon<a href="http://www.js-code.com/tag/text" title="text" target="_blank">text</a>('2d'); //获取canvas操作对象 <a href="http://www.js-code.com/tag/let" title="let" target="_blank">let</a> w = width; <a href="http://www.js-code.com/tag/let" title="let" target="_blank">let</a> h = height; //将width、height赋值给w、h <a href="http://www.js-code.com/tag/let" title="浏览关于“let”的文章" target="_blank" class="tag_link">let</a> orig<a href="http://www.js-code.com/tag/in" title="in" target="_blank">in</a>X = w / 2; //原点x值 let orig<a href="http://www.js-code.com/tag/in" title="in" target="_blank">in</a>Y = h / 2; //原点y值 let po<a href="http://www.js-code.com/tag/int" title="int" target="_blank">int</a>s = []; //用于保存数据项线条起点坐标 let leftPo<a href="http://www.js-code.com/tag/int" title="int" target="_blank">int</a>s = []; //保存在左边的点 let rightPo<a href="http://www.js-code.com/tag/int" title="浏览关于“int”的文章" target="_blank" class="tag_link">int</a>s = []; //保存在右边的点,分出左右是为了计算两点垂直间距是否靠太近 let fontSize = 12; //设置字体大小,像素</p> <p> //total保存总花费,用于计算数据项占比 let total = data.reduce(<a href="http://www.js-code.com/tag/function" title="function" target="_blank">function</a>(v, item) { <a href="http://www.js-code.com/tag/return" title="return" target="_blank">return</a> v + item.cost; }, 0)</p> <p> /** * sAngel 起始角弧度 * arc方法绘制弧线/圆时,弧的圆形的三点钟位置是 0 度 * 也就是0弧度对应笛卡坐标的90度位置 * 为了让饼图从笛卡坐标的0度开始 * 起始角弧度需要设置为-.5 * <a href="http://www.js-code.com/tag/Math" title="Math" target="_blank">Math</a>.PI */ let sAngel = -.5 * <a href="http://www.js-code.com/tag/Math" title="Math" target="_blank">Math</a>.PI; let eAngel = -.5 * <a href="http://www.js-code.com/tag/Math" title="浏览关于“Math”的文章" target="_blank" class="tag_link">Math</a>.PI; //结束角弧度,初始值等于sAngel let aAngel = Math.PI * 2; //整圆弧度,用于计算数据项弧度 let po<a href="http://www.js-code.com/tag/in" title="浏览关于“in”的文章" target="_blank" class="tag_link">in</a>tR = r + 10; //计算线条起始点的半径 let minPadding = 30; //设置数据项两点最小间距</p> <p> //设置canvas和画布大小 cns.width = ctx.width = w; cns.height = ctx.height = h;</p> <p> let cAngel; //数据项中间位置的弧度值,用于计算线条起始点</p> <p> <a href="http://www.js-code.com/tag/for" title="for" target="_blank">for</a> (let i = 0, len = data.<a href="http://www.js-code.com/tag/length" title="length" target="_blank">length</a>; i < len; i++) { /* 绘制不同消费的份额 */ /** * 计算结束角弧度 * 等于上一项数据起始弧度值(sAngel) * 加数据占比(data[i].cost/total)乘以整圆弧度(aAngel) */ eAngel = sAngel + data[i].cost/total * aAngel ; //画弧 _drawArc(ctx, { origin: [originX, originY], color: data[i].color, r, sAngel, eAngel }) /** * 计算cAngel值 * cAngel是用于计算线条起始点 * 等于当前数据项的起始弧度:sAngel * 加上当前数据项所占弧度的一半:(eAngel - sAngel) / 2 * 因为arc方法0弧度对应笛卡坐标的90度位置,我们让sAngel从 -0.5 * Math.PI开始的 * 所以cAngel还要加 0.5 * Math.PI */ cAngel = 0.5 * Math.PI + sAngel + (eAngel - sAngel) / 2; /** * 保存每个数据项线条的起始点 * 根据三角函数 * 已知半径/斜边长:pointR, 通过正弦函数可以计算出对边长度 * 原点x坐标加对边长度,就是线条起始点x坐标 * 通过余弦函数可以计算出邻边长度 * 原点y坐标减邻边长度,就是线条起始点y坐标 */ points.push([originX + Math.sin(cAngel) * pointR, originY - Math.cos(cAngel) * pointR]) sAngel = eAngel; //设置下一数据项的起始角度为当前数据项的结束角度 } <a href="http://www.js-code.com/tag/for" title="for" target="_blank">for</a> (let i = 0, len = points.<a href="http://www.js-code.com/tag/length" title="length" target="_blank">length</a>; i < len; i++) { /* 绘制起始点的小圆点,并分出左右 */ // 绘制起始点的小圆点 _drawArc(ctx, { origin: points[i], color: data[i].color, r: 2 }) <a href="http://www.js-code.com/tag/if" title="if" target="_blank">if</a> (points[i][0] < originX) { /* x坐标小于原点x坐标,在左边 */ leftPoints.push({ point: points[i], /** * <a href="http://www.js-code.com/tag/top" title="top" target="_blank">top</a>标记坐标是否在y轴正方向(是不是在上方) * 用于判断当两点挤在一起时,是优先向下还是向上移动线条线束点坐标 */ <a href="http://www.js-code.com/tag/top" title="浏览关于“top”的文章" target="_blank" class="tag_link">top</a>: points[i][1] < originY, //y坐标小于原点y坐标。表示在上方 /** * endPoint保存线条结束点坐标 * y值不变,在左边时结束点x为零 */ endPoint: [0, points[i][1]] }); } <a href="http://www.js-code.com/tag/else" title="else" target="_blank">else</a> { /* 否则在右边*/ rightPoints.push({ point: points[i], top: points[i][1] < originY, //y坐标小于原点y坐标。表示在上方 endPoint: [w, points[i][1]] //y值不变,在右边时结束点x为图表宽度w }); } } _makeUseable(rightPoints); //处理右边挤在一起的情况 _makeUseable(leftPoints.reverse(), <a href="http://www.js-code.com/tag/true" title="true" target="_blank">true</a>); //处理左边挤在一起的情况 leftPoints.reverse(); //为什么要翻转一下,看_makeUseable函数</p> <p> let i = 0; <a href="http://www.js-code.com/tag/for" title="浏览关于“for”的文章" target="_blank" class="tag_link">for</a> (let j = 0, len = rightPoints.<a href="http://www.js-code.com/tag/length" title="浏览关于“length”的文章" target="_blank" class="tag_link">length</a>; j < len; j++) { // 绘制右侧线条、文本 _drawLine(ctx, {data:data[i], point:rightPoints[j], w, direct: 'right'}); i++; } for (let j = 0, len = leftPoints.length; j < len; j++) { // 绘制左侧线条、文本 _drawLine(ctx, {data:data[i], point:leftPoints[j], w}); i++; } /* 再绘制一个圆盖住饼图,实现圆环效果 */ _drawArc(ctx, { origin: [originX, originY], r: r / 5 * 3 }) <a href="http://www.js-code.com/tag/document" title="document" target="_blank">document</a>.body.appendChild(cns); /* 添加到body中 */</p> <p> /* 画弧函数 */ <a href="http://www.js-code.com/tag/function" title="function" target="_blank">function</a> _drawArc(ctx, {color = '#fff',origin = [0, 0],r = 100,sAngel = 0, eAngel = 2 * Math.PI}) { ctx.beginPath(); //开始 ctx.strokeStyle = color; //设置线条颜色 ctx.fillStyle = color; //设置填充色 ctx.moveTo(...origin); //移动原点 ctx.arc(origin[0], origin[1], r, sAngel, eAngel); //画弧 ctx.fill(); //填充 ctx.stroke();//绘制已定义的路径,可省略 }</p> <p> /* 画线和文本 函数 */ <a href="http://www.js-code.com/tag/function" title="function" target="_blank">function</a> _drawLine (ctx, {direct='left',data={},point={},w = 200}) {</p> <p> ctx.beginPath(); //开始 ctx.moveTo(...point.point); //移动画笔到线条起点 ctx.strokeStyle = data.color; //设置线条颜色 <a href="http://www.js-code.com/tag/if" title="浏览关于“if”的文章" target="_blank" class="tag_link">if</a> (point.turingPoint) //存在折点 ctx.lineTo(...point.turingPoint); //画一条到折点的线 ctx.lineTo(...point.endPoint);//画一条到结束点的线 ctx.stroke();//绘制已定义的路径 ctx.font = `${fontSize}px 微软雅黑`; //设置字体相关 ctx.fillStyle = '#000'; //设置字体颜色 ctx.<a href="http://www.js-code.com/tag/text" title="浏览关于“text”的文章" target="_blank" class="tag_link">text</a>Align = direct;//设置文字对齐方式 //绘制数据项花费文字,垂直上移两个像素 ctx.fillText(data.cost,direct === 'left'?0:w, point.endPoint[1] - 2); //绘制数据项名称,垂直下移fontSize个像素 ctx.fillText(data.category, direct === 'left'?0:w, point.endPoint[1] + fontSize); }</p> <p> <a href="http://www.js-code.com/tag/function" title="浏览关于“function”的文章" target="_blank" class="tag_link">function</a> _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.endPoint[1] * 和下一数据项结束点垂直间距是否大于等于最小间距:minPadding * 只有数据线条结束点垂直间距大于等于最小间距,才会返回true */ return arr[index + 1].endPoint[1] - p.endPoint[1] >= minPadding; } }) }</p> <p> function _makeUseable(arr, left) {// 处理挤在一起的情况 let diff, turingAngel, x, maths = Math.sin,diffH, l;</p> <p> /** * 这里的思路是 * 如果数据是非可用的(会挤在一起,_isUseable判断) * 就一直循环移动数据,直至可用 * 数据项过多时会出现死循环 * 因为需求上说数据项不会过多,并且还要让大家帮我看看能不能获得面试机会 * 所以这里不做修改 * 可能会有更好的算法,我这鱼木脑袋只想到这种的 * 欢迎大家提供更好的思路或算法 */ <a href="http://www.js-code.com/tag/while" title="while" target="_blank">while</a> (!_isUseable(arr)) { //每次循环处理一次,直至数据不会挤在一起</p> <p> for (let i = 0, len = arr.length - 1; i < len; i++) { //遍历<a href="http://www.js-code.com/tag/%e6%95%b0%e7%bb%84" title="数组" target="_blank">数组</a></p> <p> diff = arr[i + 1].endPoint[1] - arr[i].endPoint[1]; //计算两点垂直间距</p> <p> if (diff < minPadding) { //小于最小间距,表示会挤到一起 if (arr[i].top &amp;&amp; arr[i + 1].top) { //是在上部的点,向上移动 /** * 判断当前的点是否还可以向上移动 * 上方第一个点最往上只可以移动到y值为0 * 之后依次最往上只能移动动y值为:i * minPadding * 所以下面判断应该是:arr[i].endPoint[1] - (minPadding - diff) > i * minPadding */ /** * 上面左边leftPoints的点需要翻转一下的原因是 * 左边leftPoints的点最上面的点是排在最后的 */ if (arr[i].endPoint[1] - (minPadding - diff) > 0 &amp;&amp; arr[i].endPoint[1] > i * minPadding) { //当前点还能向上移动 //向上移动到不挤(满足最小间距) arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff); } <a href="http://www.js-code.com/tag/else" title="else" target="_blank">else</a> { //当前点不向上移动到满足最小间距的位置 //先把当前点移动到能够移动的最上位置 arr[i].endPoint[1] = i * minPadding; //再把下个点移动,使满足最小间距 arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff); }</p> <p> } <a href="http://www.js-code.com/tag/else" title="浏览关于“else”的文章" target="_blank" class="tag_link">else</a> { //是在下部的点,向下移动 /** * 判断当前点的下个点是否还可以向下移动 * 下方最后一个点最往下只可以移动到y值为h,即图表高度 * 之前的点依次最往下只能移动动y值为:h - (len - i - 1) * minPadding * 所以下面判断应该是:arr[i + 1].endPoint[1] + (minPadding - diff) < h - (len - i - 1) * minPadding */ if (arr[i + 1].endPoint[1] + (minPadding - diff) < h &amp;&amp; arr[i + 1].endPoint[1] < h - (len - i - 1) * minPadding) { //当前点的下个点还能向下移动 //当前点的下个点向下移动到不挤(满足最小间距) arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff) } else { //当前点的下个点不能向下移动 //先把当前点的下个点向下移动能够移动的最下位置 arr[i + 1].endPoint[1] = h - (len - i - 1) * minPadding; //再把当前点移动,使满足最小间距 arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff); } } <a href="http://www.js-code.com/tag/break" title="break" target="_blank">break</a>; //每次移动完成直接退出循环,判断一次是否已经不挤 } } }</p> <p> /** * 遍历已经可用的数据 * 起点和结束点不在同一水平线上 * 需要设置折点 * 这里通过设置折线角度,计算出折点位置 * 回头一想,其实可以用更简单的方法,想复杂了 */ for (let i = 0, len = arr.length; i < len; i++) { //起点和结束点y值不等,则不在同一水平线,需要设置折点 if (arr[i].point[1] !== arr[i].endPoint[1]) { turingAngel = 1 / 3 * Math.PI; //默认折线角度设置60度 //计算出起点和结束点高度差 diffH = arr[i].endPoint[1] - arr[i].point[1]; //计算出起点和结束点水平距离l l = Math.abs(arr[i].endPoint[0] - arr[i].point[0]); /** * x 这里的本意是 * 想计算出折点和起始点的水平距离x * 因为起始点到折点的水平距离 * 不能大于起始点到结束的水平距离-40(留40放文字) * 通过x可以确定折点的x坐标值 * 所以已知对边和角度,应该使用正切函数求邻边边长 * 这里却使用了正弦求了斜边 */ x = Math.abs(maths(turingAngel) * diffH); /** * 如果始点到折点的水平距离 * 大于起始点到结束的水平距离-40(留40放文字) * 减小角度,计算新折点 */ while (x > (l - 40)) { turingAngel /= 2; x = maths(turingAngel) * (arr[i].endPoint[1] - arr[i].point[1]); } //通过x可以确定折点的x坐标值,y坐标就是结束点的y坐标 arr[i].turingPoint = [arr[i].point[0] + (left ? -x : x), arr[i].endPoint[1]] } } }</p> <p> }</p> <p> //调用绘图函数 addPie({ data: [{ cost: 4.94, category: '通讯', color: &quot;#e95e45&quot;, }, { cost: 4.78, category: '服装美容', color: &quot;#20b6ab&quot;, }, { cost: 4.00, category: '交通出行', color: &quot;#ef7340&quot;, }, { cost: 3.00, category: '饮食', color: &quot;#eeb328&quot;, }, { cost: 49.40, category: '其他', color: &quot;#f79954&quot;, }, { cost: 28.77, category: '生活日用', color: &quot;#00a294&quot;, }] })</p> <p> </script><br /> </body><br /> </html>" title="" data-original-title="复制"></span> </div> </p></div> <pre class="xml"><code class="html">&lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt; &lt;meta <a href="http://www.js-code.com/tag/char" title="浏览关于“char”的文章" target="_blank" class="tag_link">char</a>set="UTF-8"&gt; &lt;title&gt;饼图&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;script&gt; /** * 绘制饼图函数 * 使用到的ES6语法有函数<a href="http://www.js-code.com/tag/%e9%bb%98%e8%ae%a4%e5%8f%82%e6%95%b0" title="浏览关于“默认参数”的文章" target="_blank" class="tag_link">默认参数</a>、解构、字符模板 * 如果不熟悉,可以看看阮老师的《<a href="http://www.js-code.com/tag/ecmascript" title="浏览关于“ECMAScript”的文章" target="_blank" class="tag_link">ECMAScript</a> 6 入门》 * 网址 http://<a href="http://www.js-code.com/tag/es6" title="浏览关于“es6”的文章" target="_blank" class="tag_link">es6</a>.ruanyifeng.com/ * 函数的默认参数 * r 圆环的圆半径 data 数据项 * width 图表宽度 height 图表高度 */ function addPie({r = 100,width = 450,height = 400,data = []} = {}) { let cns = <a href="http://www.js-code.com/tag/document" title="浏览关于“document”的文章" target="_blank" class="tag_link">document</a>.createElement('canvas'); //创建一个canvas let ctx = cns.getContext('2d'); //获取canvas操作对象 let w = width; let h = height; //将width、height赋值给w、h let originX = w / 2; //原点x值 let originY = h / 2; //原点y值 let points = []; //用于保存数据项线条起点坐标 let leftPoints = []; //保存在左边的点 let rightPoints = []; //保存在右边的点,分出左右是为了计算两点垂直间距是否靠太近 let fontSize = 12; //设置字体大小,像素 //total保存总花费,用于计算数据项占比 let total = data.reduce(function(v, item) { <a href="http://www.js-code.com/tag/return" title="浏览关于“return”的文章" target="_blank" class="tag_link">return</a> v + item.cost; }, 0) /** * sAngel 起始角弧度 * arc方法绘制弧线/圆时,弧的圆形的三点钟位置是 0 度 * 也就是0弧度对应笛卡坐标的90度位置 * 为了让饼图从笛卡坐标的0度开始 * 起始角弧度需要设置为-.5 * Math.PI */ let sAngel = -.5 * Math.PI; let eAngel = -.5 * Math.PI; //结束角弧度,初始值等于sAngel let aAngel = Math.PI * 2; //整圆弧度,用于计算数据项弧度 let pointR = r + 10; //计算线条起始点的半径 let minPadding = 30; //设置数据项两点最小间距 //设置canvas和画布大小 cns.width = ctx.width = w; cns.height = ctx.height = h; let cAngel; //数据项中间位置的弧度值,用于计算线条起始点 for (let i = 0, len = data.length; i &lt; len; i++) { /* 绘制不同消费的份额 */ /** * 计算结束角弧度 * 等于上一项数据起始弧度值(sAngel) * 加数据占比(data[i].cost/total)乘以整圆弧度(aAngel) */ eAngel = sAngel + data[i].cost/total * aAngel ; //画弧 _drawArc(ctx, { origin: [originX, originY], color: data[i].color, r, sAngel, eAngel }) /** * 计算cAngel值 * cAngel是用于计算线条起始点 * 等于当前数据项的起始弧度:sAngel * 加上当前数据项所占弧度的一半:(eAngel - sAngel) / 2 * 因为arc方法0弧度对应笛卡坐标的90度位置,我们让sAngel从 -0.5 * Math.PI开始的 * 所以cAngel还要加 0.5 * Math.PI */ cAngel = 0.5 * Math.PI + sAngel + (eAngel - sAngel) / 2; /** * 保存每个数据项线条的起始点 * 根据三角函数 * 已知半径/斜边长:pointR, 通过正弦函数可以计算出对边长度 * 原点x坐标加对边长度,就是线条起始点x坐标 * 通过余弦函数可以计算出邻边长度 * 原点y坐标减邻边长度,就是线条起始点y坐标 */ points.push([originX + Math.sin(cAngel) * pointR, originY - Math.cos(cAngel) * pointR]) sAngel = eAngel; //设置下一数据项的起始角度为当前数据项的结束角度 } for (let i = 0, len = points.length; i &lt; len; i++) { /* 绘制起始点的小圆点,并分出左右 */ // 绘制起始点的小圆点 _drawArc(ctx, { origin: points[i], color: data[i].color, r: 2 }) if (points[i][0] &lt; originX) { /* x坐标小于原点x坐标,在左边 */ leftPoints.push({ point: points[i], /** * top标记坐标是否在y轴正方向(是不是在上方) * 用于判断当两点挤在一起时,是优先向下还是向上移动线条线束点坐标 */ top: points[i][1] &lt; originY, //y坐标小于原点y坐标。表示在上方 /** * endPoint保存线条结束点坐标 * y值不变,在左边时结束点x为零 */ endPoint: [0, points[i][1]] }); } else { /* 否则在右边*/ rightPoints.push({ point: points[i], top: points[i][1] &lt; originY, //y坐标小于原点y坐标。表示在上方 endPoint: [w, points[i][1]] //y值不变,在右边时结束点x为图表宽度w }); } } _makeUseable(rightPoints); //处理右边挤在一起的情况 _makeUseable(leftPoints.reverse(), <a href="http://www.js-code.com/tag/true" title="浏览关于“true”的文章" target="_blank" class="tag_link">true</a>); //处理左边挤在一起的情况 leftPoints.reverse(); //为什么要翻转一下,看_makeUseable函数 let i = 0; for (let j = 0, len = rightPoints.length; j &lt; len; j++) { // 绘制右侧线条、文本 _drawLine(ctx, {data:data[i], point:rightPoints[j], w, direct: 'right'}); i++; } for (let j = 0, len = leftPoints.length; j &lt; len; j++) { // 绘制左侧线条、文本 _drawLine(ctx, {data:data[i], point:leftPoints[j], w}); i++; } /* 再绘制一个圆盖住饼图,实现圆环效果 */ _drawArc(ctx, { origin: [originX, originY], r: r / 5 * 3 }) <a href="http://www.js-code.com/tag/do" title="浏览关于“do”的文章" target="_blank" class="tag_link">do</a>cument.body.appendChild(cns); /* 添加到body中 */ /* 画弧函数 */ function _drawArc(ctx, {color = '#fff',origin = [0, 0],r = 100,sAngel = 0, eAngel = 2 * Math.PI}) { ctx.beginPath(); //开始 ctx.strokeStyle = color; //设置线条颜色 ctx.fillStyle = color; //设置填充色 ctx.moveTo(...origin); //移动原点 ctx.arc(origin[0], origin[1], r, sAngel, eAngel); //画弧 ctx.fill(); //填充 ctx.stroke();//绘制已定义的路径,可省略 } /* 画线和文本 函数 */ function _drawLine (ctx, {direct='left',data={},point={},w = 200}) { ctx.beginPath(); //开始 ctx.moveTo(...point.point); //移动画笔到线条起点 ctx.strokeStyle = data.color; //设置线条颜色 if (point.turingPoint) //存在折点 ctx.lineTo(...point.turingPoint); //画一条到折点的线 ctx.lineTo(...point.endPoint);//画一条到结束点的线 ctx.stroke();//绘制已定义的路径 ctx.font = `${fontSize}px 微软雅黑`; //设置字体相关 ctx.fillStyle = '#000'; //设置字体颜色 ctx.textAlign = direct;//设置文字对齐方式 //绘制数据项花费文字,垂直上移两个像素 ctx.fillText(data.cost,direct === 'left'?0:w, point.endPoint[1] - 2); //绘制数据项名称,垂直下移fontSize个像素 ctx.fillText(data.category, direct === 'left'?0:w, point.endPoint[1] + fontSize); } function _isUseable(arr) { // 判断是否会有数据挤在一起(两点最小间距是否都大于等于minPadding) if (arr.length &lt;= 1) return true; return arr.every(function(p, index, arr) { if (index === arr.length-1) { //因为是当前项和下一项比较,所以index === arr.length-1直接返回true return true; } else { /** * 判断当前数据项结束点:p.endPoint[1] * 和下一数据项结束点垂直间距是否大于等于最小间距:minPadding * 只有数据线条结束点垂直间距大于等于最小间距,才会返回true */ return arr[index + 1].endPoint[1] - p.endPoint[1] &gt;= minPadding; } }) } function _makeUseable(arr, left) {// 处理挤在一起的情况 let diff, turingAngel, x, maths = Math.sin,diffH, l; /** * 这里的思路是 * 如果数据是非可用的(会挤在一起,_isUseable判断) * 就一直循环移动数据,直至可用 * 数据项过多时会出现死循环 * 因为需求上说数据项不会过多,并且还要让大家帮我看看能不能获得面试机会 * 所以这里不做修改 * 可能会有更好的算法,我这鱼木脑袋只想到这种的 * 欢迎大家提供更好的思路或算法 */ <a href="http://www.js-code.com/tag/while" title="浏览关于“while”的文章" target="_blank" class="tag_link">while</a> (!_isUseable(arr)) { //每次循环处理一次,直至数据不会挤在一起 for (let i = 0, len = arr.length - 1; i &lt; len; i++) { //遍历<a href="http://www.js-code.com/tag/%e6%95%b0%e7%bb%84" title="浏览关于“数组”的文章" target="_blank" class="tag_link">数组</a> diff = arr[i + 1].endPoint[1] - arr[i].endPoint[1]; //计算两点垂直间距 if (diff &lt; minPadding) { //小于最小间距,表示会挤到一起 if (arr[i].top &amp;&amp; arr[i + 1].top) { //是在上部的点,向上移动 /** * 判断当前的点是否还可以向上移动 * 上方第一个点最往上只可以移动到y值为0 * 之后依次最往上只能移动动y值为:i * minPadding * 所以下面判断应该是:arr[i].endPoint[1] - (minPadding - diff) &gt; i * minPadding */ /** * 上面左边leftPoints的点需要翻转一下的原因是 * 左边leftPoints的点最上面的点是排在最后的 */ if (arr[i].endPoint[1] - (minPadding - diff) &gt; 0 &amp;&amp; arr[i].endPoint[1] &gt; i * minPadding) { //当前点还能向上移动 //向上移动到不挤(满足最小间距) arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff); } else { //当前点不向上移动到满足最小间距的位置 //先把当前点移动到能够移动的最上位置 arr[i].endPoint[1] = i * minPadding; //再把下个点移动,使满足最小间距 arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff); } } else { //是在下部的点,向下移动 /** * 判断当前点的下个点是否还可以向下移动 * 下方最后一个点最往下只可以移动到y值为h,即图表高度 * 之前的点依次最往下只能移动动y值为:h - (len - i - 1) * minPadding * 所以下面判断应该是:arr[i + 1].endPoint[1] + (minPadding - diff) &lt; h - (len - i - 1) * minPadding */ if (arr[i + 1].endPoint[1] + (minPadding - diff) &lt; h &amp;&amp; arr[i + 1].endPoint[1] &lt; h - (len - i - 1) * minPadding) { //当前点的下个点还能向下移动 //当前点的下个点向下移动到不挤(满足最小间距) arr[i + 1].endPoint[1] = arr[i + 1].endPoint[1] + (minPadding - diff) } else { //当前点的下个点不能向下移动 //先把当前点的下个点向下移动能够移动的最下位置 arr[i + 1].endPoint[1] = h - (len - i - 1) * minPadding; //再把当前点移动,使满足最小间距 arr[i].endPoint[1] = arr[i].endPoint[1] - (minPadding - diff); } } <a href="http://www.js-code.com/tag/break" title="浏览关于“break”的文章" target="_blank" class="tag_link">break</a>; //每次移动完成直接退出循环,判断一次是否已经不挤 } } } /** * 遍历已经可用的数据 * 起点和结束点不在同一水平线上 * 需要设置折点 * 这里通过设置折线角度,计算出折点位置 * 回头一想,其实可以用更简单的方法,想复杂了 */ for (let i = 0, len = arr.length; i &lt; len; i++) { //起点和结束点y值不等,则不在同一水平线,需要设置折点 if (arr[i].point[1] !== arr[i].endPoint[1]) { turingAngel = 1 / 3 * Math.PI; //默认折线角度设置60度 //计算出起点和结束点高度差 diffH = arr[i].endPoint[1] - arr[i].point[1]; //计算出起点和结束点水平距离l l = Math.abs(arr[i].endPoint[0] - arr[i].point[0]); /** * x 这里的本意是 * 想计算出折点和起始点的水平距离x * 因为起始点到折点的水平距离 * 不能大于起始点到结束的水平距离-40(留40放文字) * 通过x可以确定折点的x坐标值 * 所以已知对边和角度,应该使用正切函数求邻边边长 * 这里却使用了正弦求了斜边 */ x = Math.abs(maths(turingAngel) * diffH); /** * 如果始点到折点的水平距离 * 大于起始点到结束的水平距离-40(留40放文字) * 减小角度,计算新折点 */ while (x &gt; (l - 40)) { turingAngel /= 2; x = maths(turingAngel) * (arr[i].endPoint[1] - arr[i].point[1]); } //通过x可以确定折点的x坐标值,y坐标就是结束点的y坐标 arr[i].turingPoint = [arr[i].point[0] + (left ? -x : x), arr[i].endPoint[1]] } } } } //调用绘图函数 addPie({ 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", }] }) &lt;/script&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <h3 id="articleHeader2">写在最后</h3> <p>  因为是单个测试题目,所以没有用图表库。之所以没用SVG去实现,是因为之前只有接触过canvas。不过,后续真可以考虑使用svg来实现一下。</p> <p></code></p>

总结

以上是脚本宝典为你收集整理的

原生Canvas绘制饼图,我是不是被骗代码了

全部内容,希望文章能够帮你解决

原生Canvas绘制饼图,我是不是被骗代码了

所遇到的程序开发问题,欢迎加入QQ群277859234一起讨论学习。如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典网站推荐给程序员好友。 本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。

80%的人都看过