cover_image

如何在小程序中绘制图表?

resource小程序微信小程序小程序开发图标数据可视化
minapp.com

<p><img class="aligncenter size-full wp-image-783591" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/02/charts-featured.jpg" alt="charts-featured" width="1200" height="750" srcset="http://www.ifanr.com/wp-content/uploads/2017/02/charts-featured.jpg 1200w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-featured-360x225.jpg 360w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-featured-768x480.jpg 768w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-featured-1024x640.jpg 1024w" sizes="(max-width: 1200px) 100vw, 1200px" /></p> <p><span style="color: #808080;">文 | musiq1989</span></p> <p>由于微信小程序本身框架的限制,很难集成目前已有的图表工具,显示图表目前有两种方案:</p> <ol> <li>服务器端渲染图表,输出图片,微信小程序中直接显示渲染好的图片;</li> <li>利用微信小程序 API 中提供的 <code>canvas</code> 组件支持,自行绘制图表。</li> </ol> <p>前一种方案已经有非常多类似服务可选,比如 Highcharts 提供了服务端渲染的能力。<strong>但这种方式需要后台有一套渲染服务,并且有一定的网络开销。</strong></p> <p>那么,如何利用 <code>canvas</code> 组件,在小程序中绘制图表呢?下面,我们就来尝试一下。</p> <h3 id="articleHeader1"><span style="color: #605eac;">API</span></h3> <p>首先,我们在模板文件中使用 <code>&lt;canvas&gt;&lt;/canvas&gt;</code> 声明一个 canvas 组件,再使用 <code>wx.createContext()</code> 获取绘图上下文 context。</p> <p>接下来,我们调用 <code>wx.drawCanvas()</code> 进行绘制:</p> <pre><code class="javascript">wx.drawCanvas({ canvasId: <span class="hljs-string">'firstCanvas'</span>, actions: context.getActions() <span class="hljs-comment">// 获取绘图动作数组</span> });</code></pre> <h3 id="articleHeader2"><span style="color: #605eac;">开始图表的绘制</span></h3> <h4 id="articleHeader3">绘制折线图</h4> <pre><code class="javascript"><span class="hljs-comment">// 获取绘图上下文 context</span> <span class="hljs-keyword">var</span> context = wx.createContext(); <span class="hljs-comment">// 设置描边颜色</span> context.setStrokeStyle(<span class="hljs-string">"#7cb5ec"</span>); <span class="hljs-comment">// 设置线宽</span> context.setLineWidth(<span class="hljs-number">4</span>); context.moveTo(<span class="hljs-number">50</span>, <span class="hljs-number">70</span>); context.lineTo(<span class="hljs-number">150</span>, <span class="hljs-number">150</span>); context.lineTo(<span class="hljs-number">250</span>, <span class="hljs-number">30</span>); context.lineTo(<span class="hljs-number">350</span>, <span class="hljs-number">120</span>); context.lineTo(<span class="hljs-number">450</span>, <span class="hljs-number">150</span>); context.lineTo(<span class="hljs-number">550</span>, <span class="hljs-number">95</span>); <span class="hljs-comment">// 对当前路径进行描边</span> context.stroke(); wx.drawCanvas({ canvasId: <span class="hljs-string">'testCanvas'</span>, actions: context.getActions() });</code></pre> <p><code><span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif;">需要注意的是,</span>moveTo()</code> <strong>方法不会记录到路径中。</strong></p> <p>我们来看看效果图:</p> <p><img class="aligncenter size-full wp-image-783587" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/02/charts-1.jpg" alt="charts-1" width="1000" height="194" srcset="http://www.ifanr.com/wp-content/uploads/2017/02/charts-1.jpg 1000w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-1-360x70.jpg 360w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-1-768x149.jpg 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></p> <p>&nbsp;</p> <p>好像没有想象中难,看上去效果还不错。</p> <h4 id="articleHeader4">绘制每个数据点的标识图案</h4> <pre><code class="javascript">... context.beginPath(); <span class="hljs-comment">// 设置描边颜色</span> context.setStrokeStyle(<span class="hljs-string">"#ffffff"</span>); <span class="hljs-comment">// 设置填充颜色</span> context.setFillStyle(<span class="hljs-string">"#7cb5ec"</span>); context.moveTo(<span class="hljs-number">50</span> + <span class="hljs-number">7</span>, <span class="hljs-number">70</span>); <span class="hljs-comment">// 绘制圆形区域</span> context.arc(<span class="hljs-number">50</span>, <span class="hljs-number">70</span>, <span class="hljs-number">8</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI, <span class="hljs-literal">false</span>); context.moveTo(<span class="hljs-number">150</span> + <span class="hljs-number">7</span>, <span class="hljs-number">150</span>); context.arc(<span class="hljs-number">150</span>, <span class="hljs-number">150</span>, <span class="hljs-number">8</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI, <span class="hljs-literal">false</span>); ... context.closePath(); <span class="hljs-comment">// 填充路径</span> context.fill(); context.stroke();</code></pre> <p>效果图:</p> <p><img class="aligncenter size-full wp-image-783588" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/02/charts-2.jpg" alt="charts-2" width="1000" height="214" srcset="http://www.ifanr.com/wp-content/uploads/2017/02/charts-2.jpg 1000w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-2-360x77.jpg 360w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-2-768x164.jpg 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></p> <p>&nbsp;</p> <p><strong>为了避免之前绘制的折线路径影响到标识图案的路径,这一部分包裹在了 <code>beginPath()</code> 和 <code>closePath()</code> 之间。</strong></p> <h4 id="articleHeader5">绘制横坐标</h4> <p>我们规定的参数格式是这样的:</p> <pre><code class="javascript">opts = { width: <span class="hljs-number">640</span>, <span class="hljs-comment">// 画布区域宽度</span> height: <span class="hljs-number">400</span>, <span class="hljs-comment">// 画布区域高度</span> categories: [<span class="hljs-string">'2016-08'</span>, <span class="hljs-string">'2016-09'</span>, <span class="hljs-string">'2016-10'</span>, <span class="hljs-string">'2016-11'</span>, <span class="hljs-string">'2016-12'</span>, <span class="hljs-string">'2017'</span>] }</code></pre> <p>我们根据参数中的 <code>categories</code> 来绘制横坐标。先稍微整理下思路:</p> <ol> <li>根据 <code>categories</code> 数均分画布宽度;</li> <li>计算出横坐标中每个分类的起始点;</li> <li>绘制文案(这儿会多一些代码,后面会具体提到)。</li> </ol> <pre><code class="javascript"><span class="hljs-keyword">var</span> eachSpacing = <span class="hljs-built_in">Math</span>.floor(opts.width / opts.categories.length); <span class="hljs-keyword">var</span> points = []; <span class="hljs-comment">// 起始点x坐标</span> <span class="hljs-keyword">var</span> startX = <span class="hljs-number">0</span>; <span class="hljs-comment">// 起始点y坐标</span> <span class="hljs-keyword">var</span> startY = opts.height - <span class="hljs-number">30</span>; <span class="hljs-comment">// 终点x坐标</span> <span class="hljs-keyword">var</span> endX = opts.width; <span class="hljs-comment">// 终点y坐标</span> <span class="hljs-keyword">var</span> endY = opts.height; <span class="hljs-comment">// 计算每个分类的起始点x坐标</span> opts.categories.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(item, index)</span> </span>{ points.push(startX + index * eachSpacing); }); points.push(endX); <span class="hljs-comment">// 绘制横坐标</span> context.beginPath(); context.setStrokeStyle(<span class="hljs-string">"#cccccc"</span>); context.setLineWidth(<span class="hljs-number">1</span>); <span class="hljs-comment">// 绘制坐标轴横线</span> context.moveTo(startX, startY); context.lineTo(endX, startY); <span class="hljs-comment">// 绘制坐标轴各区块竖线</span> points.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(item, index)</span> </span>{ context.moveTo(item, startY); context.lineTo(item, endY); }); context.closePath(); context.stroke(); context.beginPath(); <span class="hljs-comment">// 设置字体大小</span> context.setFontSize(<span class="hljs-number">20</span>); <span class="hljs-comment">// 设置字体填充颜色</span> context.setFillStyle(<span class="hljs-string">'#666666'</span>); opts.categories.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(item, index)</span> </span>{ context.fillText(item, points[index], startY + <span class="hljs-number">28</span>); }); context.closePath(); context.stroke();</code></pre> <p>效果图:</p> <p><img class="aligncenter size-full wp-image-783589" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/02/chart-3.jpg" alt="chart-3" width="1000" height="392" srcset="http://www.ifanr.com/wp-content/uploads/2017/02/chart-3.jpg 1000w, http://www.ifanr.com/wp-content/uploads/2017/02/chart-3-360x141.jpg 360w, http://www.ifanr.com/wp-content/uploads/2017/02/chart-3-768x301.jpg 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></p> <p>&nbsp;</p> <p>效果不错,除了文字没有居中&#8230;&#8230;.</p> <p>查阅微信小程序官方提供的文档,小程序并没有提供 HTML 5 <code>canvas</code> 中的 <code>mesureText</code>(获取文案宽度)的方法。</p> <p>下面是我们自己简单的实现,并不是绝对精确,但误差基本可以忽略。</p> <pre><code class="javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mesureText</span> <span class="hljs-params">(text)</span> </span>{ <span class="hljs-keyword">var</span> text = text.split(<span class="hljs-string">''</span>); <span class="hljs-keyword">var</span> width = <span class="hljs-number">0</span>; text.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(item)</span> </span>{ <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/[a-zA-Z]/</span>.test(item)) { width += <span class="hljs-number">14</span>; } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/[0-9]/</span>.test(item)) { width += <span class="hljs-number">11</span>; } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/\./</span>.test(item)) { width += <span class="hljs-number">5.4</span>; } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/-/</span>.test(item)) { width += <span class="hljs-number">6.5</span>; } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/[\u4e00-\u9fa5]/</span>.test(item)) { width += <span class="hljs-number">20</span>; } }); <span class="hljs-keyword">return</span> width; }</code></pre> <p>这里分别处理了字母、数字、点(.)、横线(-)以及汉字这几个常用字符。</p> <p>上面的代码稍微修改下:</p> <pre><code class="javascript">opts.categories.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(item, index)</span> </span>{ <span class="hljs-keyword">var</span> offset = eachSpacing / <span class="hljs-number">2</span> - mesureText(item) / <span class="hljs-number">2</span>; context.fillText(item, points[index] + offset, startY + <span class="hljs-number">28</span>); });</code></pre> <p>&nbsp;</p> <p><img class="aligncenter size-full wp-image-783590" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/02/charts-4.jpg" alt="charts-4" width="1000" height="370" srcset="http://www.ifanr.com/wp-content/uploads/2017/02/charts-4.jpg 1000w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-4-360x133.jpg 360w, http://www.ifanr.com/wp-content/uploads/2017/02/charts-4-768x284.jpg 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></p> <p><strong>大功告成!</strong></p> <p>如何在折线上绘制出每个数据点的数值文案呢?大家可以自己动手,尝试一下。</p> <p><span style="color: #808080;">原文地址:</span><span style="color: #605eac;">https://segmentfault.com/a/1190000007649376</span><br /> <span style="color: #808080;">项目地址:</span><span style="color: #605eac;">https://github.com/xiaolin3303/wx-charts</span></p> <p><strong><span style="color: #605eac;"><span style="color: #000000;">本文由知晓程序授权转载,关注微信号 zxcx0101,在知晓程序后台回复「<span style="color: #605eac;">1228</span>」获得全网第一本《小程序入门指南》电子书。</span></span></strong></p> <p><img class="alignnone size-full wp-image-777107" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/01/zxcx-qun.png" alt="zxcx-qun" width="1000" height="622" srcset="http://www.ifanr.com/wp-content/uploads/2017/01/zxcx-qun.png 1000w, http://www.ifanr.com/wp-content/uploads/2017/01/zxcx-qun-360x224.png 360w, http://www.ifanr.com/wp-content/uploads/2017/01/zxcx-qun-768x478.png 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></p> <p><img class="aligncenter wp-image-779041 size-full" src="http://ifanr-cdn.b0.upaiyun.com/wp-content/uploads/2017/01/new_zx-1.png" alt="new_zx" width="670" height="1166" srcset="http://www.ifanr.com/wp-content/uploads/2017/01/new_zx-1.png 670w, http://www.ifanr.com/wp-content/uploads/2017/01/new_zx-1-360x627.png 360w, http://www.ifanr.com/wp-content/uploads/2017/01/new_zx-1-588x1024.png 588w" sizes="(max-width: 670px) 100vw, 670px" /></p>