├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── wxcharts-min.js └── wxcharts.js ├── example ├── area.gif ├── area.png ├── column.gif ├── column.png ├── curve-line.png ├── line.gif ├── line.png ├── pie.gif ├── pie.png ├── radar.png ├── ring.gif ├── ring.png ├── scrollLine.gif └── tooltip.gif ├── package.json ├── rollup.config.js ├── rollup.config.prod.js └── src ├── app.js ├── components ├── animation.js ├── charts-data.js ├── charts-util.js ├── draw-charts.js ├── draw-data-shape.js ├── draw-data-text.js ├── draw-tooltip.js └── draw.js ├── config.js └── util ├── event.js ├── polyfill └── index.js ├── timing.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": ["external-helpers"] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build.sh 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 xiaolin3303 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wx-charts 2 | 微信小程序图表工具,charts for WeChat small app 3 | 4 | 基于canvas绘制,体积小巧 5 | 6 | **持续优化更新中,请保持关注~ 有任何问题欢迎在[Issues](https://github.com/xiaolin3303/wx-charts/issues)中讨论,[提出issue前请先阅读此须知](https://github.com/xiaolin3303/wx-charts/issues/67)** 7 | 8 | 9 | 字节跳动大量招聘前端开发工程师(初级、高级、资深、Leader),坐标北京、上海、深圳、杭州、成都、武汉、广州、南京,感兴趣的朋友请点击[此处](https://job.toutiao.com/referral/pc/position/share/?token=MjsxNTcxOTg2MjM0Mzg3OzY3MDY0NTM0ODA3NTQwNzEwNDM7Njc1MTYwOTUzNDc5MDY4Mjg4OA%3D%3D)进行投递 10 | 11 | 12 | # 支持图表类型 13 | - 饼图 `pie` 14 | - 圆环图 `ring` 15 | - 线图 `line` 16 | - 柱状图 `column` 17 | - 区域图 `area` 18 | - 雷达图 `radar` 19 | 20 | 代码分析 [Here](https://segmentfault.com/a/1190000007649376) 21 | 22 | # 更新记录 23 | 24 | - [ ] 动画性能优化 25 | - [x] 图表滚动的支持,感谢[@jxintang](https://github.com/jxintang) 26 | - [x] [优化曲线绘制](https://github.com/xiaolin3303/wx-charts/issues/79) 27 | - [x] 新增雷达图 2017-03-17 28 | - [x] 新增tooltip 2017-03-07 29 | - [x] 新增曲线的绘制 2017-03-06 30 | - [x] 新增更新数据、终止进行中的动画方法,渲染完成事件回调 2017-02-17 31 | - [x] 新增圆环图title, subtitle 2017-01-10 32 | - [x] x轴文案碰撞避让 2016-12-30 33 | - [x] add pie chart dataLabel 2016-12-19 34 | - [x] add animation 2016-12-05 35 | - [x] build with `rollup` 2016-12-02 36 | - [x] add legend 2016-11-29 37 | 38 | # 如何使用 39 | 1、直接引用编译好的文件 `dist/wxcharts.js` 或者 `dist/wxcharts-min.js` 40 | 41 | 2、自行编译 42 | 43 | ``` 44 | git clone https://github.com/xiaolin3303/wx-charts.git 45 | npm install rollup -g 46 | npm install 47 | rollup -c 或者 rollup --config rollup.config.prod.js 48 | ``` 49 | [实际项目中如何具体使用wx-charts](https://github.com/xiaolin3303/wx-charts/issues/28) 或者 访问[wx-charts-demo](https://github.com/xiaolin3303/wx-charts-demo)查看在微信开发工具中直接运行的例子 50 | 51 | # 参数说明 52 | 53 | [参数说明 - wxCharts使用文档](https://github.com/xiaolin3303/wx-charts/issues/56) 54 | 55 | # 方法 & 事件 56 | 57 | [方法 & 事件 - wxCharts使用说明](https://github.com/xiaolin3303/wx-charts/issues/57) 58 | 59 | # Example 60 | 61 | ![pieChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/pie.png) 62 | ![ringChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/ring.png) 63 | ![lineChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/line.png) 64 | ![curveLineChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/4636c9d2fbbaaa7944ee48e02b3a595e77c099e5/example/curve-line.png) 65 | ![columnChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/column.png) 66 | ![areaChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/area.png) 67 | ![areaChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/radar.png) 68 | 69 | ![pieChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/pie.gif) 70 | ![ringChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/ring.gif) 71 | ![lineChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/line.gif) 72 | ![columnChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/column.gif) 73 | ![areaChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/area.gif) 74 | ![tooltip](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/tooltip.gif) 75 | ![scrollLineChart](https://raw.githubusercontent.com/xiaolin3303/wx-charts/master/example/scrollLine.gif) 76 | 77 | 更多内容请查看[Example - wxCharts使用说明](https://github.com/xiaolin3303/wx-charts/issues/58) 78 | 79 | # 测试 80 | 1. iPhone 6s, IOS 9.3.5 81 | 2. 小米4, ANDORID 6.0.1 82 | 83 | 兼容性问题请在[Issue](https://github.com/xiaolin3303/wx-charts/issues)中提出 84 | 85 | # 一些问题的说明汇总 86 | - [wx-charts自适应屏幕宽度的问题](https://github.com/xiaolin3303/wx-charts/issues/4) 87 | - [项目中如何具体使用wxCharts](https://github.com/xiaolin3303/wx-charts/issues/28) 88 | -------------------------------------------------------------------------------- /dist/wxcharts-min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * charts for WeChat small app v1.0 3 | * 4 | * https://github.com/xiaolin3303/wx-charts 5 | * 2016-11-28 6 | * 7 | * Designed and built with all the love of Web 8 | */ 9 | 10 | "use strict";function assign(t,e){if(null==t)throw new TypeError("Cannot convert undefined or null to object");for(var i=Object(t),a=1;a=0?r=0:Math.abs(t)>=o-n&&(r=n-o),r}function isInAngleRange(t,e,i){function a(t){for(;t<0;)t+=2*Math.PI;for(;t>2*Math.PI;)t-=2*Math.PI;return t}return t=a(t),e=a(e),i=a(i),e>i&&(i+=2*Math.PI,t=e&&t<=i}function calRotateTranslate(t,e,i){var a=t,n=i-e,o=a+(i-n-a)/Math.sqrt(2);return o*=-1,{transX:o,transY:(i-n)*(Math.sqrt(2)-1)-(i-n-a)/Math.sqrt(2)}}function createCurveControlPoints(t,e){function i(t,e){return!(!t[e-1]||!t[e+1])&&(t[e].y>=Math.max(t[e-1].y,t[e+1].y)||t[e].y<=Math.min(t[e-1].y,t[e+1].y))}var a=null,n=null,o=null,r=null;if(e<1?(a=t[0].x+.2*(t[1].x-t[0].x),n=t[0].y+.2*(t[1].y-t[0].y)):(a=t[e].x+.2*(t[e+1].x-t[e-1].x),n=t[e].y+.2*(t[e+1].y-t[e-1].y)),e>t.length-3){var s=t.length-1;o=t[s].x-.2*(t[s].x-t[s-1].x),r=t[s].y-.2*(t[s].y-t[s-1].y)}else o=t[e+1].x-.2*(t[e+2].x-t[e].x),r=t[e+1].y-.2*(t[e+2].y-t[e].y);return i(t,e+1)&&(r=t[e+1].y),i(t,e)&&(n=t[e].y),{ctrA:{x:a,y:n},ctrB:{x:o,y:r}}}function convertCoordinateOrigin(t,e,i){return{x:i.x+t,y:i.y-e}}function avoidCollision(t,e){if(e)for(;util.isCollision(t,e);)t.start.x>0?t.start.y--:t.start.x<0?t.start.y++:t.start.y>0?t.start.y++:t.start.y--;return t}function fillSeriesColor(t,e){var i=0;return t.map(function(t){return t.color||(t.color=e.colors[i],i=(i+1)%e.colors.length),t})}function getDataRange(t,e){var i=0,a=e-t;return i=a>=1e4?1e3:a>=1e3?100:a>=100?10:a>=10?5:a>=1?1:a>=.1?.1:.01,{minRange:findRange(t,"lower",i),maxRange:findRange(e,"upper",i)}}function measureText(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:10;t=String(t);var t=t.split(""),i=0;return t.forEach(function(t){/[a-zA-Z]/.test(t)?i+=7:/[0-9]/.test(t)?i+=5.5:/\./.test(t)?i+=2.7:/-/.test(t)?i+=3.25:/[\u4e00-\u9fa5]/.test(t)?i+=10:/\(|\)/.test(t)?i+=3.73:/\s/.test(t)?i+=2.5:/%/.test(t)?i+=8:i+=10}),i*e/10}function dataCombine(t){return t.reduce(function(t,e){return(t.data?t.data:t).concat(e.data)},[])}function getSeriesDataItem(t,e){var i=[];return t.forEach(function(t){if(null!==t.data[e]&&void 0!==t.data[e]){var a={};a.color=t.color,a.name=t.name,a.data=t.format?t.format(t.data[e]):t.data[e],i.push(a)}}),i}function getMaxTextListLength(t){var e=t.map(function(t){return measureText(t)});return Math.max.apply(null,e)}function getRadarCoordinateSeries(t){for(var e=2*Math.PI/t,i=[],a=0;a4&&void 0!==arguments[4]?arguments[4]:{},o=t.map(function(t){return{text:n.format?n.format(t,a[i]):t.name+": "+t.data,color:t.color}}),r=[],s={x:0,y:0};return e.forEach(function(t){void 0!==t[i]&&null!==t[i]&&r.push(t[i])}),r.forEach(function(t){s.x=Math.round(t.x),s.y+=t.y}),s.y/=r.length,{textList:o,offset:s}}function findCurrentIndex(t,e,i,a){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0,o=-1;return isInExactChartArea(t,i,a)&&e.forEach(function(e,i){t.x+n>e&&(o=i)}),o}function isInExactChartArea(t,e,i){return t.xi.padding+i.yAxisWidth+i.yAxisTitleWidth&&t.y>i.padding&&t.y2*Math.PI&&(t-=2*Math.PI),t},r=Math.atan2(e.center.y-t.y,t.x-e.center.x);r*=-1,r<0&&(r+=2*Math.PI);e.angleList.map(function(t){return t=o(-1*t)}).forEach(function(t,e){var i=o(t-a/2),s=o(t+a/2);s=i&&r<=s||r+2*Math.PI>=i&&r+2*Math.PI<=s)&&(n=e)})}return n}function findPieChartCurrentIndex(t,e){var i=-1;if(isInExactPieChartArea(t,e.center,e.radius)){var a=Math.atan2(e.center.y-t.y,t.x-e.center.x);a=-a;for(var n=0,o=e.series.length;ne.width?(a.push(o),n=i,o=[t]):(n+=i,o.push(t))}),o.length&&a.push(o),{legendList:a,legendHeight:a.length*(i.fontSize+8)+5}}function calCategoriesData(t,e,i){var a={angle:0,xAxisHeight:i.xAxisHeight},n=getXAxisPoints(t,e,i),o=n.eachSpacing,r=t.map(function(t){return measureText(t)}),s=Math.max.apply(this,r);return s+2*i.xAxisTextPadding>o&&(a.angle=45*Math.PI/180,a.xAxisHeight=2*i.xAxisTextPadding+s*Math.sin(a.angle)),a}function getRadarDataPoints(t,e,i,a,n){var o=arguments.length>5&&void 0!==arguments[5]?arguments[5]:1,r=n.extra.radar||{};r.max=r.max||0;var s=Math.max(r.max,Math.max.apply(null,dataCombine(a))),l=[];return a.forEach(function(a){var n={};n.color=a.color,n.data=[],a.data.forEach(function(a,r){var l={};l.angle=t[r],l.proportion=a/s,l.position=convertCoordinateOrigin(i*l.proportion*o*Math.cos(l.angle),i*l.proportion*o*Math.sin(l.angle),e),n.data.push(l)}),l.push(n)}),l}function getPieDataPoints(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,i=0,a=0;return t.forEach(function(t){t.data=null===t.data?0:t.data,i+=t.data}),t.forEach(function(t){t.data=null===t.data?0:t.data,t._proportion_=t.data/i*e}),t.forEach(function(t){t._start_=a,a+=2*t._proportion_*Math.PI}),t}function getPieTextMaxLength(t){t=getPieDataPoints(t);var e=0;return t.forEach(function(t){var i=t.format?t.format(+t._proportion_.toFixed(2)):util.toFixed(100*t._proportion_)+"%";e=Math.max(e,measureText(i))}),e}function fixColumeData(t,e,i,a,n,o){return t.map(function(t){return null===t?null:(t.width=(e-2*n.columePadding)/i,o.extra.column&&o.extra.column.width&&+o.extra.column.width>0?t.width=Math.min(t.width,+o.extra.column.width):t.width=Math.min(t.width,25),t.x+=(a+.5-i/2)*t.width,t)})}function getXAxisPoints(t,e,i){var a=i.yAxisWidth+i.yAxisTitleWidth,n=e.width-2*i.padding-a,o=e.enableScroll?Math.min(5,t.length):t.length,r=n/o,s=[],l=i.padding+a,h=e.width-i.padding;return t.forEach(function(t,e){s.push(l+e*r)}),!0===e.enableScroll?s.push(l+t.length*r):s.push(h),{xAxisPoints:s,startX:l,endX:h,eachSpacing:r}}function getDataPoints(t,e,i,a,n,o,r){var s=arguments.length>7&&void 0!==arguments[7]?arguments[7]:1,l=[],h=o.height-2*r.padding-r.xAxisHeight-r.legendHeight;return t.forEach(function(t,c){if(null===t)l.push(null);else{var d={};d.x=a[c]+Math.round(n/2);var x=h*(t-e)/(i-e);x*=s,d.y=o.height-r.xAxisHeight-r.legendHeight-Math.round(x)-r.padding,l.push(d)}}),l}function getYAxisTextList(t,e,i){var a=dataCombine(t);a=a.filter(function(t){return null!==t});var n=Math.min.apply(this,a),o=Math.max.apply(this,a);if("number"==typeof e.yAxis.min&&(n=Math.min(e.yAxis.min,n)),"number"==typeof e.yAxis.max&&(o=Math.max(e.yAxis.max,o)),n===o){var r=o||1;n-=r,o+=r}for(var s=getDataRange(n,o),l=s.minRange,h=s.maxRange,c=[],d=(h-l)/i.yAxisSplit,x=0;x<=i.yAxisSplit;x++)c.push(l+d*x);return c.reverse()}function calYAxisData(t,e,i){var a=getYAxisTextList(t,e,i),n=i.yAxisWidth,o=a.map(function(t){return t=util.toFixed(t,2),t=e.yAxis.format?e.yAxis.format(Number(t)):t,n=Math.max(n,measureText(t)+5),t});return!0===e.yAxis.disabled&&(n=0),{rangesFormat:o,ranges:a,yAxisWidth:n}}function drawPointShape(t,e,i,a){a.beginPath(),a.setStrokeStyle("#ffffff"),a.setLineWidth(1),a.setFillStyle(e),"diamond"===i?t.forEach(function(t,e){null!==t&&(a.moveTo(t.x,t.y-4.5),a.lineTo(t.x-4.5,t.y),a.lineTo(t.x,t.y+4.5),a.lineTo(t.x+4.5,t.y),a.lineTo(t.x,t.y-4.5))}):"circle"===i?t.forEach(function(t,e){null!==t&&(a.moveTo(t.x+3.5,t.y),a.arc(t.x,t.y,4,0,2*Math.PI,!1))}):"rect"===i?t.forEach(function(t,e){null!==t&&(a.moveTo(t.x-3.5,t.y-3.5),a.rect(t.x-3.5,t.y-3.5,7,7))}):"triangle"===i&&t.forEach(function(t,e){null!==t&&(a.moveTo(t.x,t.y-4.5),a.lineTo(t.x-4.5,t.y+4.5),a.lineTo(t.x+4.5,t.y+4.5),a.lineTo(t.x,t.y-4.5))}),a.closePath(),a.fill(),a.stroke()}function drawRingTitle(t,e,i){var a=t.title.fontSize||e.titleFontSize,n=t.subtitle.fontSize||e.subtitleFontSize,o=t.title.name||"",r=t.subtitle.name||"",s=t.title.color||e.titleColor,l=t.subtitle.color||e.subtitleColor,h=o?a:0,c=r?n:0;if(r){var d=measureText(r,n),x=(t.width-d)/2+(t.subtitle.offsetX||0),f=(t.height-e.legendHeight+n)/2;o&&(f-=(h+5)/2),i.beginPath(),i.setFontSize(n),i.setFillStyle(l),i.fillText(r,x,f),i.stroke(),i.closePath()}if(o){var u=measureText(o,a),g=(t.width-u)/2+(t.title.offsetX||0),p=(t.height-e.legendHeight+a)/2;r&&(p+=(c+5)/2),i.beginPath(),i.setFontSize(a),i.setFillStyle(s),i.fillText(o,g,p),i.stroke(),i.closePath()}}function drawPointText(t,e,i,a){var n=e.data;a.beginPath(),a.setFontSize(i.fontSize),a.setFillStyle("#666666"),t.forEach(function(t,i){if(null!==t){var o=e.format?e.format(n[i]):n[i];a.fillText(o,t.x-measureText(o)/2,t.y-2)}}),a.closePath(),a.stroke()}function drawRadarLabel(t,e,i,a,n,o){var r=a.extra.radar||{};e+=n.radarLabelTextMargin,o.beginPath(),o.setFontSize(n.fontSize),o.setFillStyle(r.labelColor||"#666666"),t.forEach(function(t,r){var s={x:e*Math.cos(t),y:e*Math.sin(t)},l=convertCoordinateOrigin(s.x,s.y,i),h=l.x,c=l.y;util.approximatelyEqual(s.x,0)?h-=measureText(a.categories[r]||"")/2:s.x<0&&(h-=measureText(a.categories[r]||"")),o.fillText(a.categories[r]||"",h,c+n.fontSize/2)}),o.stroke(),o.closePath()}function drawPieText(t,e,i,a,n,o){var r=n+i.pieChartLinePadding,s=[],l=null;t.map(function(t){return{arc:2*Math.PI-(t._start_+2*Math.PI*t._proportion_/2),text:t.format?t.format(+t._proportion_.toFixed(2)):util.toFixed(100*t._proportion_)+"%",color:t.color}}).forEach(function(t){var e=Math.cos(t.arc)*r,a=Math.sin(t.arc)*r,o=Math.cos(t.arc)*n,h=Math.sin(t.arc)*n,c=e>=0?e+i.pieChartTextPadding:e-i.pieChartTextPadding,d=a,x=measureText(t.text),f=d;l&&util.isSameXCoordinateArea(l.start,{x:c})&&(f=c>0?Math.min(d,l.start.y):e<0?Math.max(d,l.start.y):d>0?Math.max(d,l.start.y):Math.min(d,l.start.y)),c<0&&(c-=x);var u={lineStart:{x:o,y:h},lineEnd:{x:e,y:a},start:{x:c,y:f},width:x,height:i.fontSize,text:t.text,color:t.color};l=avoidCollision(u,l),s.push(l)}),s.forEach(function(t){var e=convertCoordinateOrigin(t.lineStart.x,t.lineStart.y,o),n=convertCoordinateOrigin(t.lineEnd.x,t.lineEnd.y,o),r=convertCoordinateOrigin(t.start.x,t.start.y,o);a.setLineWidth(1),a.setFontSize(i.fontSize),a.beginPath(),a.setStrokeStyle(t.color),a.setFillStyle(t.color),a.moveTo(e.x,e.y);var s=t.start.x<0?r.x+t.width:r.x,l=t.start.x<0?r.x-5:r.x+5;a.quadraticCurveTo(n.x,n.y,s,r.y),a.moveTo(e.x,e.y),a.stroke(),a.closePath(),a.beginPath(),a.moveTo(r.x+t.width,r.y),a.arc(s,r.y,2,0,2*Math.PI),a.closePath(),a.fill(),a.beginPath(),a.setFillStyle("#666666"),a.fillText(t.text,l,r.y+3),a.closePath(),a.stroke(),a.closePath()})}function drawToolTipSplitLine(t,e,i,a){var n=i.padding,o=e.height-i.padding-i.xAxisHeight-i.legendHeight;a.beginPath(),a.setStrokeStyle("#cccccc"),a.setLineWidth(1),a.moveTo(t,n),a.lineTo(t,o),a.stroke(),a.closePath()}function drawToolTip(t,e,i,a,n){var o=!1;e=assign({x:0,y:0},e),e.y-=8;var r=t.map(function(t){return measureText(t.text)}),s=9+4*a.toolTipPadding+Math.max.apply(null,r),l=2*a.toolTipPadding+t.length*a.toolTipLineHeight;e.x-Math.abs(i._scrollDistance_)+8+s>i.width&&(o=!0),n.beginPath(),n.setFillStyle(i.tooltip.option.background||a.toolTipBackground),n.setGlobalAlpha(a.toolTipOpacity),o?(n.moveTo(e.x,e.y+10),n.lineTo(e.x-8,e.y+10-5),n.lineTo(e.x-8,e.y+10+5),n.moveTo(e.x,e.y+10),n.fillRect(e.x-s-8,e.y,s,l)):(n.moveTo(e.x,e.y+10),n.lineTo(e.x+8,e.y+10-5),n.lineTo(e.x+8,e.y+10+5),n.moveTo(e.x,e.y+10),n.fillRect(e.x+8,e.y,s,l)),n.closePath(),n.fill(),n.setGlobalAlpha(1),t.forEach(function(t,i){n.beginPath(),n.setFillStyle(t.color);var r=e.x+8+2*a.toolTipPadding,l=e.y+(a.toolTipLineHeight-a.fontSize)/2+a.toolTipLineHeight*i+a.toolTipPadding;o&&(r=e.x-s-8+2*a.toolTipPadding),n.fillRect(r,l,4,a.fontSize),n.closePath()}),n.beginPath(),n.setFontSize(a.fontSize),n.setFillStyle("#ffffff"),t.forEach(function(t,i){var r=e.x+8+2*a.toolTipPadding+4+5;o&&(r=e.x-s-8+2*a.toolTipPadding+4+5);var l=e.y+(a.toolTipLineHeight-a.fontSize)/2+a.toolTipLineHeight*i+a.toolTipPadding;n.fillText(t.text,r,l+a.fontSize)}),n.stroke(),n.closePath()}function drawYAxisTitle(t,e,i,a){var n=i.xAxisHeight+(e.height-i.xAxisHeight-measureText(t))/2;a.save(),a.beginPath(),a.setFontSize(i.fontSize),a.setFillStyle(e.yAxis.titleFontColor||"#333333"),a.translate(0,e.height),a.rotate(-90*Math.PI/180),a.fillText(t,n,i.padding+.5*i.fontSize),a.stroke(),a.closePath(),a.restore()}function drawColumnDataPoints(t,e,i,a){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,o=calYAxisData(t,e,i),r=o.ranges,s=getXAxisPoints(e.categories,e,i),l=s.xAxisPoints,h=s.eachSpacing,c=r.pop(),d=r.shift();return a.save(),e._scrollDistance_&&0!==e._scrollDistance_&&!0===e.enableScroll&&a.translate(e._scrollDistance_,0),t.forEach(function(o,r){var s=o.data,x=getDataPoints(s,c,d,l,h,e,i,n);x=fixColumeData(x,h,t.length,r,i,e),a.beginPath(),a.setFillStyle(o.color),x.forEach(function(t,n){if(null!==t){var o=t.x-t.width/2+1,r=e.height-t.y-i.padding-i.xAxisHeight-i.legendHeight;a.moveTo(o,t.y),a.rect(o,t.y,t.width-2,r)}}),a.closePath(),a.fill()}),t.forEach(function(o,r){var s=o.data,x=getDataPoints(s,c,d,l,h,e,i,n);x=fixColumeData(x,h,t.length,r,i,e),!1!==e.dataLabel&&1===n&&drawPointText(x,o,i,a)}),a.restore(),{xAxisPoints:l,eachSpacing:h}}function drawAreaDataPoints(t,e,i,a){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,o=calYAxisData(t,e,i),r=o.ranges,s=getXAxisPoints(e.categories,e,i),l=s.xAxisPoints,h=s.eachSpacing,c=r.pop(),d=r.shift(),x=e.height-i.padding-i.xAxisHeight-i.legendHeight,f=[];return a.save(),e._scrollDistance_&&0!==e._scrollDistance_&&!0===e.enableScroll&&a.translate(e._scrollDistance_,0),e.tooltip&&e.tooltip.textList&&e.tooltip.textList.length&&1===n&&drawToolTipSplitLine(e.tooltip.offset.x,e,i,a),t.forEach(function(t,o){var r=t.data,s=getDataPoints(r,c,d,l,h,e,i,n);if(f.push(s),splitPoints(s).forEach(function(i){if(a.beginPath(),a.setStrokeStyle(t.color),a.setFillStyle(t.color),a.setGlobalAlpha(.6),a.setLineWidth(2),i.length>1){var n=i[0],o=i[i.length-1];a.moveTo(n.x,n.y),"curve"===e.extra.lineStyle?i.forEach(function(t,e){if(e>0){var n=createCurveControlPoints(i,e-1);a.bezierCurveTo(n.ctrA.x,n.ctrA.y,n.ctrB.x,n.ctrB.y,t.x,t.y)}}):i.forEach(function(t,e){e>0&&a.lineTo(t.x,t.y)}),a.lineTo(o.x,x),a.lineTo(n.x,x),a.lineTo(n.x,n.y)}else{var r=i[0];a.moveTo(r.x-h/2,r.y),a.lineTo(r.x+h/2,r.y),a.lineTo(r.x+h/2,x),a.lineTo(r.x-h/2,x),a.moveTo(r.x-h/2,r.y)}a.closePath(),a.fill(),a.setGlobalAlpha(1)}),!1!==e.dataPointShape){var u=i.dataPointShape[o%i.dataPointShape.length];drawPointShape(s,t.color,u,a)}}),!1!==e.dataLabel&&1===n&&t.forEach(function(t,o){drawPointText(getDataPoints(t.data,c,d,l,h,e,i,n),t,i,a)}),a.restore(),{xAxisPoints:l,calPoints:f,eachSpacing:h}}function drawLineDataPoints(t,e,i,a){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,o=calYAxisData(t,e,i),r=o.ranges,s=getXAxisPoints(e.categories,e,i),l=s.xAxisPoints,h=s.eachSpacing,c=r.pop(),d=r.shift(),x=[];return a.save(),e._scrollDistance_&&0!==e._scrollDistance_&&!0===e.enableScroll&&a.translate(e._scrollDistance_,0),e.tooltip&&e.tooltip.textList&&e.tooltip.textList.length&&1===n&&drawToolTipSplitLine(e.tooltip.offset.x,e,i,a),t.forEach(function(t,o){var r=t.data,s=getDataPoints(r,c,d,l,h,e,i,n);if(x.push(s),splitPoints(s).forEach(function(i,n){a.beginPath(),a.setStrokeStyle(t.color),a.setLineWidth(2),1===i.length?(a.moveTo(i[0].x,i[0].y),a.arc(i[0].x,i[0].y,1,0,2*Math.PI)):(a.moveTo(i[0].x,i[0].y),"curve"===e.extra.lineStyle?i.forEach(function(t,e){if(e>0){var n=createCurveControlPoints(i,e-1);a.bezierCurveTo(n.ctrA.x,n.ctrA.y,n.ctrB.x,n.ctrB.y,t.x,t.y)}}):i.forEach(function(t,e){e>0&&a.lineTo(t.x,t.y)}),a.moveTo(i[0].x,i[0].y)),a.closePath(),a.stroke()}),!1!==e.dataPointShape){var f=i.dataPointShape[o%i.dataPointShape.length];drawPointShape(s,t.color,f,a)}}),!1!==e.dataLabel&&1===n&&t.forEach(function(t,o){drawPointText(getDataPoints(t.data,c,d,l,h,e,i,n),t,i,a)}),a.restore(),{xAxisPoints:l,calPoints:x,eachSpacing:h}}function drawToolTipBridge(t,e,i,a){i.save(),t._scrollDistance_&&0!==t._scrollDistance_&&!0===t.enableScroll&&i.translate(t._scrollDistance_,0),t.tooltip&&t.tooltip.textList&&t.tooltip.textList.length&&1===a&&drawToolTip(t.tooltip.textList,t.tooltip.offset,t,e,i),i.restore()}function drawXAxis(t,e,i,a){var n=getXAxisPoints(t,e,i),o=n.xAxisPoints,r=(n.startX,n.endX,n.eachSpacing),s=e.height-i.padding-i.xAxisHeight-i.legendHeight,l=s+i.xAxisLineHeight;a.save(),e._scrollDistance_&&0!==e._scrollDistance_&&a.translate(e._scrollDistance_,0),a.beginPath(),a.setStrokeStyle(e.xAxis.gridColor||"#cccccc"),!0!==e.xAxis.disableGrid&&("calibration"===e.xAxis.type?o.forEach(function(t,e){e>0&&(a.moveTo(t-r/2,s),a.lineTo(t-r/2,s+4))}):o.forEach(function(t,e){a.moveTo(t,s),a.lineTo(t,l)})),a.closePath(),a.stroke();var h=e.width-2*i.padding-i.yAxisWidth-i.yAxisTitleWidth,c=Math.min(t.length,Math.ceil(h/i.fontSize/1.5)),d=Math.ceil(t.length/c);t=t.map(function(t,e){return e%d!=0?"":t}),0===i._xAxisTextAngle_?(a.beginPath(),a.setFontSize(i.fontSize),a.setFillStyle(e.xAxis.fontColor||"#666666"),t.forEach(function(t,e){var n=r/2-measureText(t)/2;a.fillText(t,o[e]+n,s+i.fontSize+5)}),a.closePath(),a.stroke()):t.forEach(function(t,n){a.save(),a.beginPath(),a.setFontSize(i.fontSize),a.setFillStyle(e.xAxis.fontColor||"#666666");var l=measureText(t),h=r/2-l,c=calRotateTranslate(o[n]+r/2,s+i.fontSize/2+5,e.height),d=c.transX,x=c.transY;a.rotate(-1*i._xAxisTextAngle_),a.translate(d,x),a.fillText(t,o[n]+h,s+i.fontSize+5),a.closePath(),a.stroke(),a.restore()}),a.restore()}function drawYAxisGrid(t,e,i){for(var a=t.height-2*e.padding-e.xAxisHeight-e.legendHeight,n=Math.floor(a/e.yAxisSplit),o=e.yAxisWidth+e.yAxisTitleWidth,r=e.padding+o,s=t.width-e.padding,l=[],h=0;h4&&void 0!==arguments[4]?arguments[4]:1,o=e.extra.pie||{};t=getPieDataPoints(t,n);var r={x:e.width/2,y:(e.height-i.legendHeight)/2},s=Math.min(r.x-i.pieChartLinePadding-i.pieChartTextPadding-i._pieTextMaxLength_,r.y-i.pieChartLinePadding-i.pieChartTextPadding);if(e.dataLabel?s-=10:s-=2*i.padding,t=t.map(function(t){return t._start_+=(o.offsetAngle||0)*Math.PI/180,t}),t.forEach(function(t){a.beginPath(),a.setLineWidth(2),a.setStrokeStyle("#ffffff"),a.setFillStyle(t.color),a.moveTo(r.x,r.y),a.arc(r.x,r.y,s,t._start_,t._start_+2*t._proportion_*Math.PI),a.closePath(),a.fill(),!0!==e.disablePieStroke&&a.stroke()}),"ring"===e.type){var l=.6*s;"number"==typeof e.extra.ringWidth&&e.extra.ringWidth>0&&(l=Math.max(0,s-e.extra.ringWidth)),a.beginPath(),a.setFillStyle(e.background||"#ffffff"),a.moveTo(r.x,r.y),a.arc(r.x,r.y,l,0,2*Math.PI),a.closePath(),a.fill()}if(!1!==e.dataLabel&&1===n){for(var h=!1,c=0,d=t.length;c0){h=!0;break}h&&drawPieText(t,e,i,a,s,r)}return 1===n&&"ring"===e.type&&drawRingTitle(e,i,a),{center:r,radius:s,series:t}}function drawRadarDataPoints(t,e,i,a){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,o=e.extra.radar||{},r=getRadarCoordinateSeries(e.categories.length),s={x:e.width/2,y:(e.height-i.legendHeight)/2},l=Math.min(s.x-(getMaxTextListLength(e.categories)+i.radarLabelTextMargin),s.y-i.radarLabelTextMargin);l-=i.padding,a.beginPath(),a.setLineWidth(1),a.setStrokeStyle(o.gridColor||"#cccccc"),r.forEach(function(t){var e=convertCoordinateOrigin(l*Math.cos(t),l*Math.sin(t),s);a.moveTo(s.x,s.y),a.lineTo(e.x,e.y)}),a.stroke(),a.closePath();for(var h=1;h<=i.radarGridCount;h++)!function(t){var e={};a.beginPath(),a.setLineWidth(1),a.setStrokeStyle(o.gridColor||"#cccccc"),r.forEach(function(n,o){var r=convertCoordinateOrigin(l/i.radarGridCount*t*Math.cos(n),l/i.radarGridCount*t*Math.sin(n),s);0===o?(e=r,a.moveTo(r.x,r.y)):a.lineTo(r.x,r.y)}),a.lineTo(e.x,e.y),a.stroke(),a.closePath()}(h);return getRadarDataPoints(r,s,l,t,e,n).forEach(function(t,n){if(a.beginPath(),a.setFillStyle(t.color),a.setGlobalAlpha(.6),t.data.forEach(function(t,e){0===e?a.moveTo(t.position.x,t.position.y):a.lineTo(t.position.x,t.position.y)}),a.closePath(),a.fill(),a.setGlobalAlpha(1),!1!==e.dataPointShape){var o=i.dataPointShape[n%i.dataPointShape.length];drawPointShape(t.data.map(function(t){return t.position}),t.color,o,a)}}),drawRadarLabel(r,l,s,e,i,a),{center:s,radius:l,angleList:r}}function drawCanvas(t,e){e.draw()}function Animation(t){this.isStop=!1,t.duration=void 0===t.duration?1e3:t.duration,t.timing=t.timing||"linear";var e=function(){return"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:"undefined"!=typeof setTimeout?function(t,e){setTimeout(function(){var e=+new Date;t(e)},e)}:function(t){t(null)}}(),i=null,a=function(n){if(null===n||!0===this.isStop)return t.onProcess&&t.onProcess(1),void(t.onAnimationFinish&&t.onAnimationFinish());if(null===i&&(i=n),n-it.end.x||e.end.xt.start.y||e.start.y0&&void 0!==arguments[0]?arguments[0]:{};this.opts.series=t.series||this.opts.series,this.opts.categories=t.categories||this.opts.categories,this.opts.title=assign({},this.opts.title,t.title||{}),this.opts.subtitle=assign({},this.opts.subtitle,t.subtitle||{}),drawCharts.call(this,this.opts.type,this.opts,this.config,this.context)},Charts.prototype.stopAnimation=function(){this.animationInstance&&this.animationInstance.stop()},Charts.prototype.addEventListener=function(t,e){this.event.addEventListener(t,e)},Charts.prototype.getCurrentDataIndex=function(t){var e=t.touches&&t.touches.length?t.touches:t.changedTouches;if(e&&e.length){var i=e[0],a=i.x,n=i.y;return"pie"===this.opts.type||"ring"===this.opts.type?findPieChartCurrentIndex({x:a,y:n},this.chartData.pieData):"radar"===this.opts.type?findRadarChartCurrentIndex({x:a,y:n},this.chartData.radarData,this.opts.categories.length):findCurrentIndex({x:a,y:n},this.chartData.xAxisPoints,this.opts,this.config,Math.abs(this.scrollOption.currentOffset))}return-1},Charts.prototype.showToolTip=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("line"===this.opts.type||"area"===this.opts.type){var i=this.getCurrentDataIndex(t),a=this.scrollOption.currentOffset,n=assign({},this.opts,{_scrollDistance_:a,animation:!1});if(i>-1){var o=getSeriesDataItem(this.opts.series,i);if(0!==o.length){var r=getToolTipData(o,this.chartData.calPoints,i,this.opts.categories,e),s=r.textList,l=r.offset;n.tooltip={textList:s,offset:l,option:e}}}drawCharts.call(this,n.type,n,this.config,this.context)}},Charts.prototype.scrollStart=function(t){t.touches[0]&&!0===this.opts.enableScroll&&(this.scrollOption.startTouchX=t.touches[0].x)},Charts.prototype.scroll=function(t){if(t.touches[0]&&!0===this.opts.enableScroll){var e=t.touches[0].x-this.scrollOption.startTouchX,i=this.scrollOption.currentOffset,a=calValidDistance(i+e,this.chartData,this.config,this.opts);this.scrollOption.distance=e=a-i;var n=assign({},this.opts,{_scrollDistance_:i+e,animation:!1});drawCharts.call(this,n.type,n,this.config,this.context)}},Charts.prototype.scrollEnd=function(t){if(!0===this.opts.enableScroll){var e=this.scrollOption,i=e.currentOffset,a=e.distance;this.scrollOption.currentOffset=i+a,this.scrollOption.distance=0}},module.exports=Charts; 11 | -------------------------------------------------------------------------------- /dist/wxcharts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * charts for WeChat small app v1.0 3 | * 4 | * https://github.com/xiaolin3303/wx-charts 5 | * 2016-11-28 6 | * 7 | * Designed and built with all the love of Web 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var config = { 13 | yAxisWidth: 15, 14 | yAxisSplit: 5, 15 | xAxisHeight: 15, 16 | xAxisLineHeight: 15, 17 | legendHeight: 15, 18 | yAxisTitleWidth: 15, 19 | padding: 12, 20 | columePadding: 3, 21 | fontSize: 10, 22 | dataPointShape: ['diamond', 'circle', 'triangle', 'rect'], 23 | colors: ['#7cb5ec', '#f7a35c', '#434348', '#90ed7d', '#f15c80', '#8085e9'], 24 | pieChartLinePadding: 25, 25 | pieChartTextPadding: 15, 26 | xAxisTextPadding: 3, 27 | titleColor: '#333333', 28 | titleFontSize: 20, 29 | subtitleColor: '#999999', 30 | subtitleFontSize: 15, 31 | toolTipPadding: 3, 32 | toolTipBackground: '#000000', 33 | toolTipOpacity: 0.7, 34 | toolTipLineHeight: 14, 35 | radarGridCount: 3, 36 | radarLabelTextMargin: 15 37 | }; 38 | 39 | // Object.assign polyfill 40 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 41 | function assign(target, varArgs) { 42 | if (target == null) { 43 | // TypeError if undefined or null 44 | throw new TypeError('Cannot convert undefined or null to object'); 45 | } 46 | 47 | var to = Object(target); 48 | 49 | for (var index = 1; index < arguments.length; index++) { 50 | var nextSource = arguments[index]; 51 | 52 | if (nextSource != null) { 53 | // Skip over if undefined or null 54 | for (var nextKey in nextSource) { 55 | // Avoid bugs when hasOwnProperty is shadowed 56 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 57 | to[nextKey] = nextSource[nextKey]; 58 | } 59 | } 60 | } 61 | } 62 | return to; 63 | } 64 | 65 | var util = { 66 | toFixed: function toFixed(num, limit) { 67 | limit = limit || 2; 68 | if (this.isFloat(num)) { 69 | num = num.toFixed(limit); 70 | } 71 | return num; 72 | }, 73 | isFloat: function isFloat(num) { 74 | return num % 1 !== 0; 75 | }, 76 | approximatelyEqual: function approximatelyEqual(num1, num2) { 77 | return Math.abs(num1 - num2) < 1e-10; 78 | }, 79 | isSameSign: function isSameSign(num1, num2) { 80 | return Math.abs(num1) === num1 && Math.abs(num2) === num2 || Math.abs(num1) !== num1 && Math.abs(num2) !== num2; 81 | }, 82 | isSameXCoordinateArea: function isSameXCoordinateArea(p1, p2) { 83 | return this.isSameSign(p1.x, p2.x); 84 | }, 85 | isCollision: function isCollision(obj1, obj2) { 86 | obj1.end = {}; 87 | obj1.end.x = obj1.start.x + obj1.width; 88 | obj1.end.y = obj1.start.y - obj1.height; 89 | obj2.end = {}; 90 | obj2.end.x = obj2.start.x + obj2.width; 91 | obj2.end.y = obj2.start.y - obj2.height; 92 | var flag = obj2.start.x > obj1.end.x || obj2.end.x < obj1.start.x || obj2.end.y > obj1.start.y || obj2.start.y < obj1.end.y; 93 | 94 | return !flag; 95 | } 96 | }; 97 | 98 | function findRange(num, type, limit) { 99 | if (isNaN(num)) { 100 | throw new Error('[wxCharts] unvalid series data!'); 101 | } 102 | limit = limit || 10; 103 | type = type ? type : 'upper'; 104 | var multiple = 1; 105 | while (limit < 1) { 106 | limit *= 10; 107 | multiple *= 10; 108 | } 109 | if (type === 'upper') { 110 | num = Math.ceil(num * multiple); 111 | } else { 112 | num = Math.floor(num * multiple); 113 | } 114 | while (num % limit !== 0) { 115 | if (type === 'upper') { 116 | num++; 117 | } else { 118 | num--; 119 | } 120 | } 121 | 122 | return num / multiple; 123 | } 124 | 125 | function calValidDistance(distance, chartData, config, opts) { 126 | 127 | var dataChartAreaWidth = opts.width - config.padding - chartData.xAxisPoints[0]; 128 | var dataChartWidth = chartData.eachSpacing * opts.categories.length; 129 | var validDistance = distance; 130 | if (distance >= 0) { 131 | validDistance = 0; 132 | } else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) { 133 | validDistance = dataChartAreaWidth - dataChartWidth; 134 | } 135 | return validDistance; 136 | } 137 | 138 | function isInAngleRange(angle, startAngle, endAngle) { 139 | function adjust(angle) { 140 | while (angle < 0) { 141 | angle += 2 * Math.PI; 142 | } 143 | while (angle > 2 * Math.PI) { 144 | angle -= 2 * Math.PI; 145 | } 146 | 147 | return angle; 148 | } 149 | 150 | angle = adjust(angle); 151 | startAngle = adjust(startAngle); 152 | endAngle = adjust(endAngle); 153 | if (startAngle > endAngle) { 154 | endAngle += 2 * Math.PI; 155 | if (angle < startAngle) { 156 | angle += 2 * Math.PI; 157 | } 158 | } 159 | 160 | return angle >= startAngle && angle <= endAngle; 161 | } 162 | 163 | function calRotateTranslate(x, y, h) { 164 | var xv = x; 165 | var yv = h - y; 166 | 167 | var transX = xv + (h - yv - xv) / Math.sqrt(2); 168 | transX *= -1; 169 | 170 | var transY = (h - yv) * (Math.sqrt(2) - 1) - (h - yv - xv) / Math.sqrt(2); 171 | 172 | return { 173 | transX: transX, 174 | transY: transY 175 | }; 176 | } 177 | 178 | function createCurveControlPoints(points, i) { 179 | 180 | function isNotMiddlePoint(points, i) { 181 | if (points[i - 1] && points[i + 1]) { 182 | return points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) || points[i].y <= Math.min(points[i - 1].y, points[i + 1].y); 183 | } else { 184 | return false; 185 | } 186 | } 187 | 188 | var a = 0.2; 189 | var b = 0.2; 190 | var pAx = null; 191 | var pAy = null; 192 | var pBx = null; 193 | var pBy = null; 194 | if (i < 1) { 195 | pAx = points[0].x + (points[1].x - points[0].x) * a; 196 | pAy = points[0].y + (points[1].y - points[0].y) * a; 197 | } else { 198 | pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a; 199 | pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a; 200 | } 201 | 202 | if (i > points.length - 3) { 203 | var last = points.length - 1; 204 | pBx = points[last].x - (points[last].x - points[last - 1].x) * b; 205 | pBy = points[last].y - (points[last].y - points[last - 1].y) * b; 206 | } else { 207 | pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b; 208 | pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b; 209 | } 210 | 211 | // fix issue https://github.com/xiaolin3303/wx-charts/issues/79 212 | if (isNotMiddlePoint(points, i + 1)) { 213 | pBy = points[i + 1].y; 214 | } 215 | if (isNotMiddlePoint(points, i)) { 216 | pAy = points[i].y; 217 | } 218 | 219 | return { 220 | ctrA: { x: pAx, y: pAy }, 221 | ctrB: { x: pBx, y: pBy } 222 | }; 223 | } 224 | 225 | function convertCoordinateOrigin(x, y, center) { 226 | return { 227 | x: center.x + x, 228 | y: center.y - y 229 | }; 230 | } 231 | 232 | function avoidCollision(obj, target) { 233 | if (target) { 234 | // is collision test 235 | while (util.isCollision(obj, target)) { 236 | if (obj.start.x > 0) { 237 | obj.start.y--; 238 | } else if (obj.start.x < 0) { 239 | obj.start.y++; 240 | } else { 241 | if (obj.start.y > 0) { 242 | obj.start.y++; 243 | } else { 244 | obj.start.y--; 245 | } 246 | } 247 | } 248 | } 249 | return obj; 250 | } 251 | 252 | function fillSeriesColor(series, config) { 253 | var index = 0; 254 | return series.map(function (item) { 255 | if (!item.color) { 256 | item.color = config.colors[index]; 257 | index = (index + 1) % config.colors.length; 258 | } 259 | return item; 260 | }); 261 | } 262 | 263 | function getDataRange(minData, maxData) { 264 | var limit = 0; 265 | var range = maxData - minData; 266 | if (range >= 10000) { 267 | limit = 1000; 268 | } else if (range >= 1000) { 269 | limit = 100; 270 | } else if (range >= 100) { 271 | limit = 10; 272 | } else if (range >= 10) { 273 | limit = 5; 274 | } else if (range >= 1) { 275 | limit = 1; 276 | } else if (range >= 0.1) { 277 | limit = 0.1; 278 | } else { 279 | limit = 0.01; 280 | } 281 | return { 282 | minRange: findRange(minData, 'lower', limit), 283 | maxRange: findRange(maxData, 'upper', limit) 284 | }; 285 | } 286 | 287 | function measureText(text) { 288 | var fontSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10; 289 | 290 | // wx canvas 未实现measureText方法, 此处自行实现 291 | text = String(text); 292 | var text = text.split(''); 293 | var width = 0; 294 | text.forEach(function (item) { 295 | if (/[a-zA-Z]/.test(item)) { 296 | width += 7; 297 | } else if (/[0-9]/.test(item)) { 298 | width += 5.5; 299 | } else if (/\./.test(item)) { 300 | width += 2.7; 301 | } else if (/-/.test(item)) { 302 | width += 3.25; 303 | } else if (/[\u4e00-\u9fa5]/.test(item)) { 304 | width += 10; 305 | } else if (/\(|\)/.test(item)) { 306 | width += 3.73; 307 | } else if (/\s/.test(item)) { 308 | width += 2.5; 309 | } else if (/%/.test(item)) { 310 | width += 8; 311 | } else { 312 | width += 10; 313 | } 314 | }); 315 | return width * fontSize / 10; 316 | } 317 | 318 | function dataCombine(series) { 319 | return series.reduce(function (a, b) { 320 | return (a.data ? a.data : a).concat(b.data); 321 | }, []); 322 | } 323 | 324 | function getSeriesDataItem(series, index) { 325 | var data = []; 326 | series.forEach(function (item) { 327 | if (item.data[index] !== null && typeof item.data[index] !== 'undefined') { 328 | var seriesItem = {}; 329 | seriesItem.color = item.color; 330 | seriesItem.name = item.name; 331 | seriesItem.data = item.format ? item.format(item.data[index]) : item.data[index]; 332 | data.push(seriesItem); 333 | } 334 | }); 335 | 336 | return data; 337 | } 338 | 339 | 340 | 341 | function getMaxTextListLength(list) { 342 | var lengthList = list.map(function (item) { 343 | return measureText(item); 344 | }); 345 | return Math.max.apply(null, lengthList); 346 | } 347 | 348 | function getRadarCoordinateSeries(length) { 349 | var eachAngle = 2 * Math.PI / length; 350 | var CoordinateSeries = []; 351 | for (var i = 0; i < length; i++) { 352 | CoordinateSeries.push(eachAngle * i); 353 | } 354 | 355 | return CoordinateSeries.map(function (item) { 356 | return -1 * item + Math.PI / 2; 357 | }); 358 | } 359 | 360 | function getToolTipData(seriesData, calPoints, index, categories) { 361 | var option = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; 362 | 363 | var textList = seriesData.map(function (item) { 364 | return { 365 | text: option.format ? option.format(item, categories[index]) : item.name + ': ' + item.data, 366 | color: item.color 367 | }; 368 | }); 369 | var validCalPoints = []; 370 | var offset = { 371 | x: 0, 372 | y: 0 373 | }; 374 | calPoints.forEach(function (points) { 375 | if (typeof points[index] !== 'undefined' && points[index] !== null) { 376 | validCalPoints.push(points[index]); 377 | } 378 | }); 379 | validCalPoints.forEach(function (item) { 380 | offset.x = Math.round(item.x); 381 | offset.y += item.y; 382 | }); 383 | 384 | offset.y /= validCalPoints.length; 385 | return { textList: textList, offset: offset }; 386 | } 387 | 388 | function findCurrentIndex(currentPoints, xAxisPoints, opts, config) { 389 | var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; 390 | 391 | var currentIndex = -1; 392 | if (isInExactChartArea(currentPoints, opts, config)) { 393 | xAxisPoints.forEach(function (item, index) { 394 | if (currentPoints.x + offset > item) { 395 | currentIndex = index; 396 | } 397 | }); 398 | } 399 | 400 | return currentIndex; 401 | } 402 | 403 | function isInExactChartArea(currentPoints, opts, config) { 404 | return currentPoints.x < opts.width - config.padding && currentPoints.x > config.padding + config.yAxisWidth + config.yAxisTitleWidth && currentPoints.y > config.padding && currentPoints.y < opts.height - config.legendHeight - config.xAxisHeight - config.padding; 405 | } 406 | 407 | function findRadarChartCurrentIndex(currentPoints, radarData, count) { 408 | var eachAngleArea = 2 * Math.PI / count; 409 | var currentIndex = -1; 410 | if (isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)) { 411 | var fixAngle = function fixAngle(angle) { 412 | if (angle < 0) { 413 | angle += 2 * Math.PI; 414 | } 415 | if (angle > 2 * Math.PI) { 416 | angle -= 2 * Math.PI; 417 | } 418 | return angle; 419 | }; 420 | 421 | var angle = Math.atan2(radarData.center.y - currentPoints.y, currentPoints.x - radarData.center.x); 422 | angle = -1 * angle; 423 | if (angle < 0) { 424 | angle += 2 * Math.PI; 425 | } 426 | 427 | var angleList = radarData.angleList.map(function (item) { 428 | item = fixAngle(-1 * item); 429 | 430 | return item; 431 | }); 432 | 433 | angleList.forEach(function (item, index) { 434 | var rangeStart = fixAngle(item - eachAngleArea / 2); 435 | var rangeEnd = fixAngle(item + eachAngleArea / 2); 436 | if (rangeEnd < rangeStart) { 437 | rangeEnd += 2 * Math.PI; 438 | } 439 | if (angle >= rangeStart && angle <= rangeEnd || angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd) { 440 | currentIndex = index; 441 | } 442 | }); 443 | } 444 | 445 | return currentIndex; 446 | } 447 | 448 | function findPieChartCurrentIndex(currentPoints, pieData) { 449 | var currentIndex = -1; 450 | if (isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) { 451 | var angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x); 452 | angle = -angle; 453 | for (var i = 0, len = pieData.series.length; i < len; i++) { 454 | var item = pieData.series[i]; 455 | if (isInAngleRange(angle, item._start_, item._start_ + item._proportion_ * 2 * Math.PI)) { 456 | currentIndex = i; 457 | break; 458 | } 459 | } 460 | } 461 | 462 | return currentIndex; 463 | } 464 | 465 | function isInExactPieChartArea(currentPoints, center, radius) { 466 | return Math.pow(currentPoints.x - center.x, 2) + Math.pow(currentPoints.y - center.y, 2) <= Math.pow(radius, 2); 467 | } 468 | 469 | function splitPoints(points) { 470 | var newPoints = []; 471 | var items = []; 472 | points.forEach(function (item, index) { 473 | if (item !== null) { 474 | items.push(item); 475 | } else { 476 | if (items.length) { 477 | newPoints.push(items); 478 | } 479 | items = []; 480 | } 481 | }); 482 | if (items.length) { 483 | newPoints.push(items); 484 | } 485 | 486 | return newPoints; 487 | } 488 | 489 | function calLegendData(series, opts, config) { 490 | if (opts.legend === false) { 491 | return { 492 | legendList: [], 493 | legendHeight: 0 494 | }; 495 | } 496 | var padding = 5; 497 | var marginTop = 8; 498 | var shapeWidth = 15; 499 | var legendList = []; 500 | var widthCount = 0; 501 | var currentRow = []; 502 | series.forEach(function (item) { 503 | var itemWidth = 3 * padding + shapeWidth + measureText(item.name || 'undefined'); 504 | if (widthCount + itemWidth > opts.width) { 505 | legendList.push(currentRow); 506 | widthCount = itemWidth; 507 | currentRow = [item]; 508 | } else { 509 | widthCount += itemWidth; 510 | currentRow.push(item); 511 | } 512 | }); 513 | if (currentRow.length) { 514 | legendList.push(currentRow); 515 | } 516 | 517 | return { 518 | legendList: legendList, 519 | legendHeight: legendList.length * (config.fontSize + marginTop) + padding 520 | }; 521 | } 522 | 523 | function calCategoriesData(categories, opts, config) { 524 | var result = { 525 | angle: 0, 526 | xAxisHeight: config.xAxisHeight 527 | }; 528 | 529 | var _getXAxisPoints = getXAxisPoints(categories, opts, config), 530 | eachSpacing = _getXAxisPoints.eachSpacing; 531 | 532 | // get max length of categories text 533 | 534 | 535 | var categoriesTextLenth = categories.map(function (item) { 536 | return measureText(item); 537 | }); 538 | 539 | var maxTextLength = Math.max.apply(this, categoriesTextLenth); 540 | 541 | if (maxTextLength + 2 * config.xAxisTextPadding > eachSpacing) { 542 | result.angle = 45 * Math.PI / 180; 543 | result.xAxisHeight = 2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle); 544 | } 545 | 546 | return result; 547 | } 548 | 549 | function getRadarDataPoints(angleList, center, radius, series, opts) { 550 | var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1; 551 | 552 | var radarOption = opts.extra.radar || {}; 553 | radarOption.max = radarOption.max || 0; 554 | var maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series))); 555 | 556 | var data = []; 557 | series.forEach(function (each) { 558 | var listItem = {}; 559 | listItem.color = each.color; 560 | listItem.data = []; 561 | each.data.forEach(function (item, index) { 562 | var tmp = {}; 563 | tmp.angle = angleList[index]; 564 | 565 | tmp.proportion = item / maxData; 566 | tmp.position = convertCoordinateOrigin(radius * tmp.proportion * process * Math.cos(tmp.angle), radius * tmp.proportion * process * Math.sin(tmp.angle), center); 567 | listItem.data.push(tmp); 568 | }); 569 | 570 | data.push(listItem); 571 | }); 572 | 573 | return data; 574 | } 575 | 576 | function getPieDataPoints(series) { 577 | var process = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; 578 | 579 | var count = 0; 580 | var _start_ = 0; 581 | series.forEach(function (item) { 582 | item.data = item.data === null ? 0 : item.data; 583 | count += item.data; 584 | }); 585 | series.forEach(function (item) { 586 | item.data = item.data === null ? 0 : item.data; 587 | item._proportion_ = item.data / count * process; 588 | }); 589 | series.forEach(function (item) { 590 | item._start_ = _start_; 591 | _start_ += 2 * item._proportion_ * Math.PI; 592 | }); 593 | 594 | return series; 595 | } 596 | 597 | function getPieTextMaxLength(series) { 598 | series = getPieDataPoints(series); 599 | var maxLength = 0; 600 | series.forEach(function (item) { 601 | var text = item.format ? item.format(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%'; 602 | maxLength = Math.max(maxLength, measureText(text)); 603 | }); 604 | 605 | return maxLength; 606 | } 607 | 608 | function fixColumeData(points, eachSpacing, columnLen, index, config, opts) { 609 | return points.map(function (item) { 610 | if (item === null) { 611 | return null; 612 | } 613 | item.width = (eachSpacing - 2 * config.columePadding) / columnLen; 614 | 615 | if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) { 616 | // customer column width 617 | item.width = Math.min(item.width, +opts.extra.column.width); 618 | } else { 619 | // default width should less tran 25px 620 | // don't ask me why, I don't know 621 | item.width = Math.min(item.width, 25); 622 | } 623 | item.x += (index + 0.5 - columnLen / 2) * item.width; 624 | 625 | return item; 626 | }); 627 | } 628 | 629 | function getXAxisPoints(categories, opts, config) { 630 | var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 631 | var spacingValid = opts.width - 2 * config.padding - yAxisTotalWidth; 632 | var dataCount = opts.enableScroll ? Math.min(5, categories.length) : categories.length; 633 | var eachSpacing = spacingValid / dataCount; 634 | 635 | var xAxisPoints = []; 636 | var startX = config.padding + yAxisTotalWidth; 637 | var endX = opts.width - config.padding; 638 | categories.forEach(function (item, index) { 639 | xAxisPoints.push(startX + index * eachSpacing); 640 | }); 641 | if (opts.enableScroll === true) { 642 | xAxisPoints.push(startX + categories.length * eachSpacing); 643 | } else { 644 | xAxisPoints.push(endX); 645 | } 646 | 647 | return { xAxisPoints: xAxisPoints, startX: startX, endX: endX, eachSpacing: eachSpacing }; 648 | } 649 | 650 | function getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) { 651 | var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1; 652 | 653 | var points = []; 654 | var validHeight = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 655 | data.forEach(function (item, index) { 656 | if (item === null) { 657 | points.push(null); 658 | } else { 659 | var point = {}; 660 | point.x = xAxisPoints[index] + Math.round(eachSpacing / 2); 661 | var height = validHeight * (item - minRange) / (maxRange - minRange); 662 | height *= process; 663 | point.y = opts.height - config.xAxisHeight - config.legendHeight - Math.round(height) - config.padding; 664 | points.push(point); 665 | } 666 | }); 667 | 668 | return points; 669 | } 670 | 671 | function getYAxisTextList(series, opts, config) { 672 | var data = dataCombine(series); 673 | // remove null from data 674 | data = data.filter(function (item) { 675 | return item !== null; 676 | }); 677 | var minData = Math.min.apply(this, data); 678 | var maxData = Math.max.apply(this, data); 679 | if (typeof opts.yAxis.min === 'number') { 680 | minData = Math.min(opts.yAxis.min, minData); 681 | } 682 | if (typeof opts.yAxis.max === 'number') { 683 | maxData = Math.max(opts.yAxis.max, maxData); 684 | } 685 | 686 | // fix issue https://github.com/xiaolin3303/wx-charts/issues/9 687 | if (minData === maxData) { 688 | var rangeSpan = maxData || 1; 689 | minData -= rangeSpan; 690 | maxData += rangeSpan; 691 | } 692 | 693 | var dataRange = getDataRange(minData, maxData); 694 | var minRange = dataRange.minRange; 695 | var maxRange = dataRange.maxRange; 696 | 697 | var range = []; 698 | var eachRange = (maxRange - minRange) / config.yAxisSplit; 699 | 700 | for (var i = 0; i <= config.yAxisSplit; i++) { 701 | range.push(minRange + eachRange * i); 702 | } 703 | return range.reverse(); 704 | } 705 | 706 | function calYAxisData(series, opts, config) { 707 | 708 | var ranges = getYAxisTextList(series, opts, config); 709 | var yAxisWidth = config.yAxisWidth; 710 | var rangesFormat = ranges.map(function (item) { 711 | item = util.toFixed(item, 2); 712 | item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item; 713 | yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5); 714 | return item; 715 | }); 716 | if (opts.yAxis.disabled === true) { 717 | yAxisWidth = 0; 718 | } 719 | 720 | return { rangesFormat: rangesFormat, ranges: ranges, yAxisWidth: yAxisWidth }; 721 | } 722 | 723 | function drawPointShape(points, color, shape, context) { 724 | context.beginPath(); 725 | context.setStrokeStyle("#ffffff"); 726 | context.setLineWidth(1); 727 | context.setFillStyle(color); 728 | 729 | if (shape === 'diamond') { 730 | points.forEach(function (item, index) { 731 | if (item !== null) { 732 | context.moveTo(item.x, item.y - 4.5); 733 | context.lineTo(item.x - 4.5, item.y); 734 | context.lineTo(item.x, item.y + 4.5); 735 | context.lineTo(item.x + 4.5, item.y); 736 | context.lineTo(item.x, item.y - 4.5); 737 | } 738 | }); 739 | } else if (shape === 'circle') { 740 | points.forEach(function (item, index) { 741 | if (item !== null) { 742 | context.moveTo(item.x + 3.5, item.y); 743 | context.arc(item.x, item.y, 4, 0, 2 * Math.PI, false); 744 | } 745 | }); 746 | } else if (shape === 'rect') { 747 | points.forEach(function (item, index) { 748 | if (item !== null) { 749 | context.moveTo(item.x - 3.5, item.y - 3.5); 750 | context.rect(item.x - 3.5, item.y - 3.5, 7, 7); 751 | } 752 | }); 753 | } else if (shape === 'triangle') { 754 | points.forEach(function (item, index) { 755 | if (item !== null) { 756 | context.moveTo(item.x, item.y - 4.5); 757 | context.lineTo(item.x - 4.5, item.y + 4.5); 758 | context.lineTo(item.x + 4.5, item.y + 4.5); 759 | context.lineTo(item.x, item.y - 4.5); 760 | } 761 | }); 762 | } 763 | context.closePath(); 764 | context.fill(); 765 | context.stroke(); 766 | } 767 | 768 | function drawRingTitle(opts, config, context) { 769 | var titlefontSize = opts.title.fontSize || config.titleFontSize; 770 | var subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize; 771 | var title = opts.title.name || ''; 772 | var subtitle = opts.subtitle.name || ''; 773 | var titleFontColor = opts.title.color || config.titleColor; 774 | var subtitleFontColor = opts.subtitle.color || config.subtitleColor; 775 | var titleHeight = title ? titlefontSize : 0; 776 | var subtitleHeight = subtitle ? subtitlefontSize : 0; 777 | var margin = 5; 778 | if (subtitle) { 779 | var textWidth = measureText(subtitle, subtitlefontSize); 780 | var startX = (opts.width - textWidth) / 2 + (opts.subtitle.offsetX || 0); 781 | var startY = (opts.height - config.legendHeight + subtitlefontSize) / 2; 782 | if (title) { 783 | startY -= (titleHeight + margin) / 2; 784 | } 785 | context.beginPath(); 786 | context.setFontSize(subtitlefontSize); 787 | context.setFillStyle(subtitleFontColor); 788 | context.fillText(subtitle, startX, startY); 789 | context.stroke(); 790 | context.closePath(); 791 | } 792 | if (title) { 793 | var _textWidth = measureText(title, titlefontSize); 794 | var _startX = (opts.width - _textWidth) / 2 + (opts.title.offsetX || 0); 795 | var _startY = (opts.height - config.legendHeight + titlefontSize) / 2; 796 | if (subtitle) { 797 | _startY += (subtitleHeight + margin) / 2; 798 | } 799 | context.beginPath(); 800 | context.setFontSize(titlefontSize); 801 | context.setFillStyle(titleFontColor); 802 | context.fillText(title, _startX, _startY); 803 | context.stroke(); 804 | context.closePath(); 805 | } 806 | } 807 | 808 | function drawPointText(points, series, config, context) { 809 | // 绘制数据文案 810 | var data = series.data; 811 | 812 | context.beginPath(); 813 | context.setFontSize(config.fontSize); 814 | context.setFillStyle('#666666'); 815 | points.forEach(function (item, index) { 816 | if (item !== null) { 817 | var formatVal = series.format ? series.format(data[index]) : data[index]; 818 | context.fillText(formatVal, item.x - measureText(formatVal) / 2, item.y - 2); 819 | } 820 | }); 821 | context.closePath(); 822 | context.stroke(); 823 | } 824 | 825 | function drawRadarLabel(angleList, radius, centerPosition, opts, config, context) { 826 | var radarOption = opts.extra.radar || {}; 827 | radius += config.radarLabelTextMargin; 828 | context.beginPath(); 829 | context.setFontSize(config.fontSize); 830 | context.setFillStyle(radarOption.labelColor || '#666666'); 831 | angleList.forEach(function (angle, index) { 832 | var pos = { 833 | x: radius * Math.cos(angle), 834 | y: radius * Math.sin(angle) 835 | }; 836 | var posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition); 837 | var startX = posRelativeCanvas.x; 838 | var startY = posRelativeCanvas.y; 839 | if (util.approximatelyEqual(pos.x, 0)) { 840 | startX -= measureText(opts.categories[index] || '') / 2; 841 | } else if (pos.x < 0) { 842 | startX -= measureText(opts.categories[index] || ''); 843 | } 844 | context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2); 845 | }); 846 | context.stroke(); 847 | context.closePath(); 848 | } 849 | 850 | function drawPieText(series, opts, config, context, radius, center) { 851 | var lineRadius = radius + config.pieChartLinePadding; 852 | var textObjectCollection = []; 853 | var lastTextObject = null; 854 | 855 | var seriesConvert = series.map(function (item) { 856 | var arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2); 857 | var text = item.format ? item.format(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%'; 858 | var color = item.color; 859 | return { arc: arc, text: text, color: color }; 860 | }); 861 | seriesConvert.forEach(function (item) { 862 | // line end 863 | var orginX1 = Math.cos(item.arc) * lineRadius; 864 | var orginY1 = Math.sin(item.arc) * lineRadius; 865 | 866 | // line start 867 | var orginX2 = Math.cos(item.arc) * radius; 868 | var orginY2 = Math.sin(item.arc) * radius; 869 | 870 | // text start 871 | var orginX3 = orginX1 >= 0 ? orginX1 + config.pieChartTextPadding : orginX1 - config.pieChartTextPadding; 872 | var orginY3 = orginY1; 873 | 874 | var textWidth = measureText(item.text); 875 | var startY = orginY3; 876 | 877 | if (lastTextObject && util.isSameXCoordinateArea(lastTextObject.start, { x: orginX3 })) { 878 | if (orginX3 > 0) { 879 | startY = Math.min(orginY3, lastTextObject.start.y); 880 | } else if (orginX1 < 0) { 881 | startY = Math.max(orginY3, lastTextObject.start.y); 882 | } else { 883 | if (orginY3 > 0) { 884 | startY = Math.max(orginY3, lastTextObject.start.y); 885 | } else { 886 | startY = Math.min(orginY3, lastTextObject.start.y); 887 | } 888 | } 889 | } 890 | 891 | if (orginX3 < 0) { 892 | orginX3 -= textWidth; 893 | } 894 | 895 | var textObject = { 896 | lineStart: { 897 | x: orginX2, 898 | y: orginY2 899 | }, 900 | lineEnd: { 901 | x: orginX1, 902 | y: orginY1 903 | }, 904 | start: { 905 | x: orginX3, 906 | y: startY 907 | }, 908 | width: textWidth, 909 | height: config.fontSize, 910 | text: item.text, 911 | color: item.color 912 | }; 913 | 914 | lastTextObject = avoidCollision(textObject, lastTextObject); 915 | textObjectCollection.push(lastTextObject); 916 | }); 917 | 918 | textObjectCollection.forEach(function (item) { 919 | var lineStartPoistion = convertCoordinateOrigin(item.lineStart.x, item.lineStart.y, center); 920 | var lineEndPoistion = convertCoordinateOrigin(item.lineEnd.x, item.lineEnd.y, center); 921 | var textPosition = convertCoordinateOrigin(item.start.x, item.start.y, center); 922 | context.setLineWidth(1); 923 | context.setFontSize(config.fontSize); 924 | context.beginPath(); 925 | context.setStrokeStyle(item.color); 926 | context.setFillStyle(item.color); 927 | context.moveTo(lineStartPoistion.x, lineStartPoistion.y); 928 | var curveStartX = item.start.x < 0 ? textPosition.x + item.width : textPosition.x; 929 | var textStartX = item.start.x < 0 ? textPosition.x - 5 : textPosition.x + 5; 930 | context.quadraticCurveTo(lineEndPoistion.x, lineEndPoistion.y, curveStartX, textPosition.y); 931 | context.moveTo(lineStartPoistion.x, lineStartPoistion.y); 932 | context.stroke(); 933 | context.closePath(); 934 | context.beginPath(); 935 | context.moveTo(textPosition.x + item.width, textPosition.y); 936 | context.arc(curveStartX, textPosition.y, 2, 0, 2 * Math.PI); 937 | context.closePath(); 938 | context.fill(); 939 | context.beginPath(); 940 | context.setFillStyle('#666666'); 941 | context.fillText(item.text, textStartX, textPosition.y + 3); 942 | context.closePath(); 943 | context.stroke(); 944 | 945 | context.closePath(); 946 | }); 947 | } 948 | 949 | function drawToolTipSplitLine(offsetX, opts, config, context) { 950 | var startY = config.padding; 951 | var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 952 | context.beginPath(); 953 | context.setStrokeStyle('#cccccc'); 954 | context.setLineWidth(1); 955 | context.moveTo(offsetX, startY); 956 | context.lineTo(offsetX, endY); 957 | context.stroke(); 958 | context.closePath(); 959 | } 960 | 961 | function drawToolTip(textList, offset, opts, config, context) { 962 | var legendWidth = 4; 963 | var legendMarginRight = 5; 964 | var arrowWidth = 8; 965 | var isOverRightBorder = false; 966 | offset = assign({ 967 | x: 0, 968 | y: 0 969 | }, offset); 970 | offset.y -= 8; 971 | var textWidth = textList.map(function (item) { 972 | return measureText(item.text); 973 | }); 974 | 975 | var toolTipWidth = legendWidth + legendMarginRight + 4 * config.toolTipPadding + Math.max.apply(null, textWidth); 976 | var toolTipHeight = 2 * config.toolTipPadding + textList.length * config.toolTipLineHeight; 977 | 978 | // if beyond the right border 979 | if (offset.x - Math.abs(opts._scrollDistance_) + arrowWidth + toolTipWidth > opts.width) { 980 | isOverRightBorder = true; 981 | } 982 | 983 | // draw background rect 984 | context.beginPath(); 985 | context.setFillStyle(opts.tooltip.option.background || config.toolTipBackground); 986 | context.setGlobalAlpha(config.toolTipOpacity); 987 | if (isOverRightBorder) { 988 | context.moveTo(offset.x, offset.y + 10); 989 | context.lineTo(offset.x - arrowWidth, offset.y + 10 - 5); 990 | context.lineTo(offset.x - arrowWidth, offset.y + 10 + 5); 991 | context.moveTo(offset.x, offset.y + 10); 992 | context.fillRect(offset.x - toolTipWidth - arrowWidth, offset.y, toolTipWidth, toolTipHeight); 993 | } else { 994 | context.moveTo(offset.x, offset.y + 10); 995 | context.lineTo(offset.x + arrowWidth, offset.y + 10 - 5); 996 | context.lineTo(offset.x + arrowWidth, offset.y + 10 + 5); 997 | context.moveTo(offset.x, offset.y + 10); 998 | context.fillRect(offset.x + arrowWidth, offset.y, toolTipWidth, toolTipHeight); 999 | } 1000 | 1001 | context.closePath(); 1002 | context.fill(); 1003 | context.setGlobalAlpha(1); 1004 | 1005 | // draw legend 1006 | textList.forEach(function (item, index) { 1007 | context.beginPath(); 1008 | context.setFillStyle(item.color); 1009 | var startX = offset.x + arrowWidth + 2 * config.toolTipPadding; 1010 | var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding; 1011 | if (isOverRightBorder) { 1012 | startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding; 1013 | } 1014 | context.fillRect(startX, startY, legendWidth, config.fontSize); 1015 | context.closePath(); 1016 | }); 1017 | 1018 | // draw text list 1019 | context.beginPath(); 1020 | context.setFontSize(config.fontSize); 1021 | context.setFillStyle('#ffffff'); 1022 | textList.forEach(function (item, index) { 1023 | var startX = offset.x + arrowWidth + 2 * config.toolTipPadding + legendWidth + legendMarginRight; 1024 | if (isOverRightBorder) { 1025 | startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding + +legendWidth + legendMarginRight; 1026 | } 1027 | var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding; 1028 | context.fillText(item.text, startX, startY + config.fontSize); 1029 | }); 1030 | context.stroke(); 1031 | context.closePath(); 1032 | } 1033 | 1034 | function drawYAxisTitle(title, opts, config, context) { 1035 | var startX = config.xAxisHeight + (opts.height - config.xAxisHeight - measureText(title)) / 2; 1036 | context.save(); 1037 | context.beginPath(); 1038 | context.setFontSize(config.fontSize); 1039 | context.setFillStyle(opts.yAxis.titleFontColor || '#333333'); 1040 | context.translate(0, opts.height); 1041 | context.rotate(-90 * Math.PI / 180); 1042 | context.fillText(title, startX, config.padding + 0.5 * config.fontSize); 1043 | context.stroke(); 1044 | context.closePath(); 1045 | context.restore(); 1046 | } 1047 | 1048 | function drawColumnDataPoints(series, opts, config, context) { 1049 | var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; 1050 | 1051 | var _calYAxisData = calYAxisData(series, opts, config), 1052 | ranges = _calYAxisData.ranges; 1053 | 1054 | var _getXAxisPoints = getXAxisPoints(opts.categories, opts, config), 1055 | xAxisPoints = _getXAxisPoints.xAxisPoints, 1056 | eachSpacing = _getXAxisPoints.eachSpacing; 1057 | 1058 | var minRange = ranges.pop(); 1059 | var maxRange = ranges.shift(); 1060 | context.save(); 1061 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 1062 | context.translate(opts._scrollDistance_, 0); 1063 | } 1064 | 1065 | series.forEach(function (eachSeries, seriesIndex) { 1066 | var data = eachSeries.data; 1067 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1068 | points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); 1069 | 1070 | // 绘制柱状数据图 1071 | context.beginPath(); 1072 | context.setFillStyle(eachSeries.color); 1073 | points.forEach(function (item, index) { 1074 | if (item !== null) { 1075 | var startX = item.x - item.width / 2 + 1; 1076 | var height = opts.height - item.y - config.padding - config.xAxisHeight - config.legendHeight; 1077 | context.moveTo(startX, item.y); 1078 | context.rect(startX, item.y, item.width - 2, height); 1079 | } 1080 | }); 1081 | context.closePath(); 1082 | context.fill(); 1083 | }); 1084 | series.forEach(function (eachSeries, seriesIndex) { 1085 | var data = eachSeries.data; 1086 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1087 | points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); 1088 | if (opts.dataLabel !== false && process === 1) { 1089 | drawPointText(points, eachSeries, config, context); 1090 | } 1091 | }); 1092 | context.restore(); 1093 | return { 1094 | xAxisPoints: xAxisPoints, 1095 | eachSpacing: eachSpacing 1096 | }; 1097 | } 1098 | 1099 | function drawAreaDataPoints(series, opts, config, context) { 1100 | var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; 1101 | 1102 | var _calYAxisData2 = calYAxisData(series, opts, config), 1103 | ranges = _calYAxisData2.ranges; 1104 | 1105 | var _getXAxisPoints2 = getXAxisPoints(opts.categories, opts, config), 1106 | xAxisPoints = _getXAxisPoints2.xAxisPoints, 1107 | eachSpacing = _getXAxisPoints2.eachSpacing; 1108 | 1109 | var minRange = ranges.pop(); 1110 | var maxRange = ranges.shift(); 1111 | var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 1112 | var calPoints = []; 1113 | 1114 | context.save(); 1115 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 1116 | context.translate(opts._scrollDistance_, 0); 1117 | } 1118 | 1119 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 1120 | drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context); 1121 | } 1122 | 1123 | series.forEach(function (eachSeries, seriesIndex) { 1124 | var data = eachSeries.data; 1125 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1126 | calPoints.push(points); 1127 | 1128 | var splitPointList = splitPoints(points); 1129 | 1130 | splitPointList.forEach(function (points) { 1131 | // 绘制区域数据 1132 | context.beginPath(); 1133 | context.setStrokeStyle(eachSeries.color); 1134 | context.setFillStyle(eachSeries.color); 1135 | context.setGlobalAlpha(0.6); 1136 | context.setLineWidth(2); 1137 | if (points.length > 1) { 1138 | var firstPoint = points[0]; 1139 | var lastPoint = points[points.length - 1]; 1140 | 1141 | context.moveTo(firstPoint.x, firstPoint.y); 1142 | if (opts.extra.lineStyle === 'curve') { 1143 | points.forEach(function (item, index) { 1144 | if (index > 0) { 1145 | var ctrlPoint = createCurveControlPoints(points, index - 1); 1146 | context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); 1147 | } 1148 | }); 1149 | } else { 1150 | points.forEach(function (item, index) { 1151 | if (index > 0) { 1152 | context.lineTo(item.x, item.y); 1153 | } 1154 | }); 1155 | } 1156 | 1157 | context.lineTo(lastPoint.x, endY); 1158 | context.lineTo(firstPoint.x, endY); 1159 | context.lineTo(firstPoint.x, firstPoint.y); 1160 | } else { 1161 | var item = points[0]; 1162 | context.moveTo(item.x - eachSpacing / 2, item.y); 1163 | context.lineTo(item.x + eachSpacing / 2, item.y); 1164 | context.lineTo(item.x + eachSpacing / 2, endY); 1165 | context.lineTo(item.x - eachSpacing / 2, endY); 1166 | context.moveTo(item.x - eachSpacing / 2, item.y); 1167 | } 1168 | context.closePath(); 1169 | context.fill(); 1170 | context.setGlobalAlpha(1); 1171 | }); 1172 | 1173 | if (opts.dataPointShape !== false) { 1174 | var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 1175 | drawPointShape(points, eachSeries.color, shape, context); 1176 | } 1177 | }); 1178 | if (opts.dataLabel !== false && process === 1) { 1179 | series.forEach(function (eachSeries, seriesIndex) { 1180 | var data = eachSeries.data; 1181 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1182 | drawPointText(points, eachSeries, config, context); 1183 | }); 1184 | } 1185 | 1186 | context.restore(); 1187 | 1188 | return { 1189 | xAxisPoints: xAxisPoints, 1190 | calPoints: calPoints, 1191 | eachSpacing: eachSpacing 1192 | }; 1193 | } 1194 | 1195 | function drawLineDataPoints(series, opts, config, context) { 1196 | var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; 1197 | 1198 | var _calYAxisData3 = calYAxisData(series, opts, config), 1199 | ranges = _calYAxisData3.ranges; 1200 | 1201 | var _getXAxisPoints3 = getXAxisPoints(opts.categories, opts, config), 1202 | xAxisPoints = _getXAxisPoints3.xAxisPoints, 1203 | eachSpacing = _getXAxisPoints3.eachSpacing; 1204 | 1205 | var minRange = ranges.pop(); 1206 | var maxRange = ranges.shift(); 1207 | var calPoints = []; 1208 | 1209 | context.save(); 1210 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 1211 | context.translate(opts._scrollDistance_, 0); 1212 | } 1213 | 1214 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 1215 | drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context); 1216 | } 1217 | 1218 | series.forEach(function (eachSeries, seriesIndex) { 1219 | var data = eachSeries.data; 1220 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1221 | calPoints.push(points); 1222 | var splitPointList = splitPoints(points); 1223 | 1224 | splitPointList.forEach(function (points, index) { 1225 | context.beginPath(); 1226 | context.setStrokeStyle(eachSeries.color); 1227 | context.setLineWidth(2); 1228 | if (points.length === 1) { 1229 | context.moveTo(points[0].x, points[0].y); 1230 | context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); 1231 | } else { 1232 | context.moveTo(points[0].x, points[0].y); 1233 | if (opts.extra.lineStyle === 'curve') { 1234 | points.forEach(function (item, index) { 1235 | if (index > 0) { 1236 | var ctrlPoint = createCurveControlPoints(points, index - 1); 1237 | context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); 1238 | } 1239 | }); 1240 | } else { 1241 | points.forEach(function (item, index) { 1242 | if (index > 0) { 1243 | context.lineTo(item.x, item.y); 1244 | } 1245 | }); 1246 | } 1247 | context.moveTo(points[0].x, points[0].y); 1248 | } 1249 | context.closePath(); 1250 | context.stroke(); 1251 | }); 1252 | 1253 | if (opts.dataPointShape !== false) { 1254 | var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 1255 | drawPointShape(points, eachSeries.color, shape, context); 1256 | } 1257 | }); 1258 | if (opts.dataLabel !== false && process === 1) { 1259 | series.forEach(function (eachSeries, seriesIndex) { 1260 | var data = eachSeries.data; 1261 | var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 1262 | drawPointText(points, eachSeries, config, context); 1263 | }); 1264 | } 1265 | 1266 | context.restore(); 1267 | 1268 | return { 1269 | xAxisPoints: xAxisPoints, 1270 | calPoints: calPoints, 1271 | eachSpacing: eachSpacing 1272 | }; 1273 | } 1274 | 1275 | function drawToolTipBridge(opts, config, context, process) { 1276 | context.save(); 1277 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 1278 | context.translate(opts._scrollDistance_, 0); 1279 | } 1280 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 1281 | drawToolTip(opts.tooltip.textList, opts.tooltip.offset, opts, config, context); 1282 | } 1283 | context.restore(); 1284 | } 1285 | 1286 | function drawXAxis(categories, opts, config, context) { 1287 | var _getXAxisPoints4 = getXAxisPoints(categories, opts, config), 1288 | xAxisPoints = _getXAxisPoints4.xAxisPoints, 1289 | startX = _getXAxisPoints4.startX, 1290 | endX = _getXAxisPoints4.endX, 1291 | eachSpacing = _getXAxisPoints4.eachSpacing; 1292 | 1293 | var startY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 1294 | var endY = startY + config.xAxisLineHeight; 1295 | 1296 | context.save(); 1297 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) { 1298 | context.translate(opts._scrollDistance_, 0); 1299 | } 1300 | 1301 | context.beginPath(); 1302 | context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc"); 1303 | 1304 | if (opts.xAxis.disableGrid !== true) { 1305 | if (opts.xAxis.type === 'calibration') { 1306 | xAxisPoints.forEach(function (item, index) { 1307 | if (index > 0) { 1308 | context.moveTo(item - eachSpacing / 2, startY); 1309 | context.lineTo(item - eachSpacing / 2, startY + 4); 1310 | } 1311 | }); 1312 | } else { 1313 | xAxisPoints.forEach(function (item, index) { 1314 | context.moveTo(item, startY); 1315 | context.lineTo(item, endY); 1316 | }); 1317 | } 1318 | } 1319 | context.closePath(); 1320 | context.stroke(); 1321 | 1322 | // 对X轴列表做抽稀处理 1323 | var validWidth = opts.width - 2 * config.padding - config.yAxisWidth - config.yAxisTitleWidth; 1324 | var maxXAxisListLength = Math.min(categories.length, Math.ceil(validWidth / config.fontSize / 1.5)); 1325 | var ratio = Math.ceil(categories.length / maxXAxisListLength); 1326 | 1327 | categories = categories.map(function (item, index) { 1328 | return index % ratio !== 0 ? '' : item; 1329 | }); 1330 | 1331 | if (config._xAxisTextAngle_ === 0) { 1332 | context.beginPath(); 1333 | context.setFontSize(config.fontSize); 1334 | context.setFillStyle(opts.xAxis.fontColor || '#666666'); 1335 | categories.forEach(function (item, index) { 1336 | var offset = eachSpacing / 2 - measureText(item) / 2; 1337 | context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5); 1338 | }); 1339 | context.closePath(); 1340 | context.stroke(); 1341 | } else { 1342 | categories.forEach(function (item, index) { 1343 | context.save(); 1344 | context.beginPath(); 1345 | context.setFontSize(config.fontSize); 1346 | context.setFillStyle(opts.xAxis.fontColor || '#666666'); 1347 | var textWidth = measureText(item); 1348 | var offset = eachSpacing / 2 - textWidth; 1349 | 1350 | var _calRotateTranslate = calRotateTranslate(xAxisPoints[index] + eachSpacing / 2, startY + config.fontSize / 2 + 5, opts.height), 1351 | transX = _calRotateTranslate.transX, 1352 | transY = _calRotateTranslate.transY; 1353 | 1354 | context.rotate(-1 * config._xAxisTextAngle_); 1355 | context.translate(transX, transY); 1356 | context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5); 1357 | context.closePath(); 1358 | context.stroke(); 1359 | context.restore(); 1360 | }); 1361 | } 1362 | 1363 | context.restore(); 1364 | } 1365 | 1366 | function drawYAxisGrid(opts, config, context) { 1367 | var spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 1368 | var eachSpacing = Math.floor(spacingValid / config.yAxisSplit); 1369 | var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 1370 | var startX = config.padding + yAxisTotalWidth; 1371 | var endX = opts.width - config.padding; 1372 | 1373 | var points = []; 1374 | for (var i = 0; i < config.yAxisSplit; i++) { 1375 | points.push(config.padding + eachSpacing * i); 1376 | } 1377 | points.push(config.padding + eachSpacing * config.yAxisSplit + 2); 1378 | 1379 | context.beginPath(); 1380 | context.setStrokeStyle(opts.yAxis.gridColor || "#cccccc"); 1381 | context.setLineWidth(1); 1382 | points.forEach(function (item, index) { 1383 | context.moveTo(startX, item); 1384 | context.lineTo(endX, item); 1385 | }); 1386 | context.closePath(); 1387 | context.stroke(); 1388 | } 1389 | 1390 | function drawYAxis(series, opts, config, context) { 1391 | if (opts.yAxis.disabled === true) { 1392 | return; 1393 | } 1394 | 1395 | var _calYAxisData4 = calYAxisData(series, opts, config), 1396 | rangesFormat = _calYAxisData4.rangesFormat; 1397 | 1398 | var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 1399 | 1400 | var spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 1401 | var eachSpacing = Math.floor(spacingValid / config.yAxisSplit); 1402 | var startX = config.padding + yAxisTotalWidth; 1403 | var endX = opts.width - config.padding; 1404 | var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 1405 | 1406 | // set YAxis background 1407 | context.setFillStyle(opts.background || '#ffffff'); 1408 | if (opts._scrollDistance_ < 0) { 1409 | context.fillRect(0, 0, startX, endY + config.xAxisHeight + 5); 1410 | } 1411 | context.fillRect(endX, 0, opts.width, endY + config.xAxisHeight + 5); 1412 | 1413 | var points = []; 1414 | for (var i = 0; i <= config.yAxisSplit; i++) { 1415 | points.push(config.padding + eachSpacing * i); 1416 | } 1417 | 1418 | context.stroke(); 1419 | context.beginPath(); 1420 | context.setFontSize(config.fontSize); 1421 | context.setFillStyle(opts.yAxis.fontColor || '#666666'); 1422 | rangesFormat.forEach(function (item, index) { 1423 | var pos = points[index] ? points[index] : endY; 1424 | context.fillText(item, config.padding + config.yAxisTitleWidth, pos + config.fontSize / 2); 1425 | }); 1426 | context.closePath(); 1427 | context.stroke(); 1428 | 1429 | if (opts.yAxis.title) { 1430 | drawYAxisTitle(opts.yAxis.title, opts, config, context); 1431 | } 1432 | } 1433 | 1434 | function drawLegend(series, opts, config, context) { 1435 | if (!opts.legend) { 1436 | return; 1437 | } 1438 | // each legend shape width 15px 1439 | // the spacing between shape and text in each legend is the `padding` 1440 | // each legend spacing is the `padding` 1441 | // legend margin top `config.padding` 1442 | 1443 | var _calLegendData = calLegendData(series, opts, config), 1444 | legendList = _calLegendData.legendList; 1445 | 1446 | var padding = 5; 1447 | var marginTop = 8; 1448 | var shapeWidth = 15; 1449 | legendList.forEach(function (itemList, listIndex) { 1450 | var width = 0; 1451 | itemList.forEach(function (item) { 1452 | item.name = item.name || 'undefined'; 1453 | width += 3 * padding + measureText(item.name) + shapeWidth; 1454 | }); 1455 | var startX = (opts.width - width) / 2 + padding; 1456 | var startY = opts.height - config.padding - config.legendHeight + listIndex * (config.fontSize + marginTop) + padding + marginTop; 1457 | 1458 | context.setFontSize(config.fontSize); 1459 | itemList.forEach(function (item) { 1460 | switch (opts.type) { 1461 | case 'line': 1462 | context.beginPath(); 1463 | context.setLineWidth(1); 1464 | context.setStrokeStyle(item.color); 1465 | context.moveTo(startX - 2, startY + 5); 1466 | context.lineTo(startX + 17, startY + 5); 1467 | context.stroke(); 1468 | context.closePath(); 1469 | context.beginPath(); 1470 | context.setLineWidth(1); 1471 | context.setStrokeStyle('#ffffff'); 1472 | context.setFillStyle(item.color); 1473 | context.moveTo(startX + 7.5, startY + 5); 1474 | context.arc(startX + 7.5, startY + 5, 4, 0, 2 * Math.PI); 1475 | context.fill(); 1476 | context.stroke(); 1477 | context.closePath(); 1478 | break; 1479 | case 'pie': 1480 | case 'ring': 1481 | context.beginPath(); 1482 | context.setFillStyle(item.color); 1483 | context.moveTo(startX + 7.5, startY + 5); 1484 | context.arc(startX + 7.5, startY + 5, 7, 0, 2 * Math.PI); 1485 | context.closePath(); 1486 | context.fill(); 1487 | break; 1488 | default: 1489 | context.beginPath(); 1490 | context.setFillStyle(item.color); 1491 | context.moveTo(startX, startY); 1492 | context.rect(startX, startY, 15, 10); 1493 | context.closePath(); 1494 | context.fill(); 1495 | } 1496 | startX += padding + shapeWidth; 1497 | context.beginPath(); 1498 | context.setFillStyle(opts.extra.legendTextColor || '#333333'); 1499 | context.fillText(item.name, startX, startY + 9); 1500 | context.closePath(); 1501 | context.stroke(); 1502 | startX += measureText(item.name) + 2 * padding; 1503 | }); 1504 | }); 1505 | } 1506 | function drawPieDataPoints(series, opts, config, context) { 1507 | var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; 1508 | 1509 | var pieOption = opts.extra.pie || {}; 1510 | series = getPieDataPoints(series, process); 1511 | var centerPosition = { 1512 | x: opts.width / 2, 1513 | y: (opts.height - config.legendHeight) / 2 1514 | }; 1515 | var radius = Math.min(centerPosition.x - config.pieChartLinePadding - config.pieChartTextPadding - config._pieTextMaxLength_, centerPosition.y - config.pieChartLinePadding - config.pieChartTextPadding); 1516 | if (opts.dataLabel) { 1517 | radius -= 10; 1518 | } else { 1519 | radius -= 2 * config.padding; 1520 | } 1521 | series = series.map(function (eachSeries) { 1522 | eachSeries._start_ += (pieOption.offsetAngle || 0) * Math.PI / 180; 1523 | return eachSeries; 1524 | }); 1525 | series.forEach(function (eachSeries) { 1526 | context.beginPath(); 1527 | context.setLineWidth(2); 1528 | context.setStrokeStyle('#ffffff'); 1529 | context.setFillStyle(eachSeries.color); 1530 | context.moveTo(centerPosition.x, centerPosition.y); 1531 | context.arc(centerPosition.x, centerPosition.y, radius, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI); 1532 | context.closePath(); 1533 | context.fill(); 1534 | if (opts.disablePieStroke !== true) { 1535 | context.stroke(); 1536 | } 1537 | }); 1538 | 1539 | if (opts.type === 'ring') { 1540 | var innerPieWidth = radius * 0.6; 1541 | if (typeof opts.extra.ringWidth === 'number' && opts.extra.ringWidth > 0) { 1542 | innerPieWidth = Math.max(0, radius - opts.extra.ringWidth); 1543 | } 1544 | context.beginPath(); 1545 | context.setFillStyle(opts.background || '#ffffff'); 1546 | context.moveTo(centerPosition.x, centerPosition.y); 1547 | context.arc(centerPosition.x, centerPosition.y, innerPieWidth, 0, 2 * Math.PI); 1548 | context.closePath(); 1549 | context.fill(); 1550 | } 1551 | 1552 | if (opts.dataLabel !== false && process === 1) { 1553 | // fix https://github.com/xiaolin3303/wx-charts/issues/132 1554 | var valid = false; 1555 | for (var i = 0, len = series.length; i < len; i++) { 1556 | if (series[i].data > 0) { 1557 | valid = true; 1558 | break; 1559 | } 1560 | } 1561 | 1562 | if (valid) { 1563 | drawPieText(series, opts, config, context, radius, centerPosition); 1564 | } 1565 | } 1566 | 1567 | if (process === 1 && opts.type === 'ring') { 1568 | drawRingTitle(opts, config, context); 1569 | } 1570 | 1571 | return { 1572 | center: centerPosition, 1573 | radius: radius, 1574 | series: series 1575 | }; 1576 | } 1577 | 1578 | function drawRadarDataPoints(series, opts, config, context) { 1579 | var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; 1580 | 1581 | var radarOption = opts.extra.radar || {}; 1582 | var coordinateAngle = getRadarCoordinateSeries(opts.categories.length); 1583 | var centerPosition = { 1584 | x: opts.width / 2, 1585 | y: (opts.height - config.legendHeight) / 2 1586 | }; 1587 | 1588 | var radius = Math.min(centerPosition.x - (getMaxTextListLength(opts.categories) + config.radarLabelTextMargin), centerPosition.y - config.radarLabelTextMargin); 1589 | 1590 | radius -= config.padding; 1591 | 1592 | // draw grid 1593 | context.beginPath(); 1594 | context.setLineWidth(1); 1595 | context.setStrokeStyle(radarOption.gridColor || "#cccccc"); 1596 | coordinateAngle.forEach(function (angle) { 1597 | var pos = convertCoordinateOrigin(radius * Math.cos(angle), radius * Math.sin(angle), centerPosition); 1598 | context.moveTo(centerPosition.x, centerPosition.y); 1599 | context.lineTo(pos.x, pos.y); 1600 | }); 1601 | context.stroke(); 1602 | context.closePath(); 1603 | 1604 | // draw split line grid 1605 | 1606 | var _loop = function _loop(i) { 1607 | var startPos = {}; 1608 | context.beginPath(); 1609 | context.setLineWidth(1); 1610 | context.setStrokeStyle(radarOption.gridColor || "#cccccc"); 1611 | coordinateAngle.forEach(function (angle, index) { 1612 | var pos = convertCoordinateOrigin(radius / config.radarGridCount * i * Math.cos(angle), radius / config.radarGridCount * i * Math.sin(angle), centerPosition); 1613 | if (index === 0) { 1614 | startPos = pos; 1615 | context.moveTo(pos.x, pos.y); 1616 | } else { 1617 | context.lineTo(pos.x, pos.y); 1618 | } 1619 | }); 1620 | context.lineTo(startPos.x, startPos.y); 1621 | context.stroke(); 1622 | context.closePath(); 1623 | }; 1624 | 1625 | for (var i = 1; i <= config.radarGridCount; i++) { 1626 | _loop(i); 1627 | } 1628 | 1629 | var radarDataPoints = getRadarDataPoints(coordinateAngle, centerPosition, radius, series, opts, process); 1630 | radarDataPoints.forEach(function (eachSeries, seriesIndex) { 1631 | // 绘制区域数据 1632 | context.beginPath(); 1633 | context.setFillStyle(eachSeries.color); 1634 | context.setGlobalAlpha(0.6); 1635 | eachSeries.data.forEach(function (item, index) { 1636 | if (index === 0) { 1637 | context.moveTo(item.position.x, item.position.y); 1638 | } else { 1639 | context.lineTo(item.position.x, item.position.y); 1640 | } 1641 | }); 1642 | context.closePath(); 1643 | context.fill(); 1644 | context.setGlobalAlpha(1); 1645 | 1646 | if (opts.dataPointShape !== false) { 1647 | var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 1648 | var points = eachSeries.data.map(function (item) { 1649 | return item.position; 1650 | }); 1651 | drawPointShape(points, eachSeries.color, shape, context); 1652 | } 1653 | }); 1654 | // draw label text 1655 | drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context); 1656 | 1657 | return { 1658 | center: centerPosition, 1659 | radius: radius, 1660 | angleList: coordinateAngle 1661 | }; 1662 | } 1663 | 1664 | function drawCanvas(opts, context) { 1665 | context.draw(); 1666 | } 1667 | 1668 | var Timing = { 1669 | easeIn: function easeIn(pos) { 1670 | return Math.pow(pos, 3); 1671 | }, 1672 | 1673 | easeOut: function easeOut(pos) { 1674 | return Math.pow(pos - 1, 3) + 1; 1675 | }, 1676 | 1677 | easeInOut: function easeInOut(pos) { 1678 | if ((pos /= 0.5) < 1) { 1679 | return 0.5 * Math.pow(pos, 3); 1680 | } else { 1681 | return 0.5 * (Math.pow(pos - 2, 3) + 2); 1682 | } 1683 | }, 1684 | 1685 | linear: function linear(pos) { 1686 | return pos; 1687 | } 1688 | }; 1689 | 1690 | function Animation(opts) { 1691 | this.isStop = false; 1692 | opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration; 1693 | opts.timing = opts.timing || 'linear'; 1694 | 1695 | var delay = 17; 1696 | 1697 | var createAnimationFrame = function createAnimationFrame() { 1698 | if (typeof requestAnimationFrame !== 'undefined') { 1699 | return requestAnimationFrame; 1700 | } else if (typeof setTimeout !== 'undefined') { 1701 | return function (step, delay) { 1702 | setTimeout(function () { 1703 | var timeStamp = +new Date(); 1704 | step(timeStamp); 1705 | }, delay); 1706 | }; 1707 | } else { 1708 | return function (step) { 1709 | step(null); 1710 | }; 1711 | } 1712 | }; 1713 | var animationFrame = createAnimationFrame(); 1714 | var startTimeStamp = null; 1715 | var _step = function step(timestamp) { 1716 | if (timestamp === null || this.isStop === true) { 1717 | opts.onProcess && opts.onProcess(1); 1718 | opts.onAnimationFinish && opts.onAnimationFinish(); 1719 | return; 1720 | } 1721 | if (startTimeStamp === null) { 1722 | startTimeStamp = timestamp; 1723 | } 1724 | if (timestamp - startTimeStamp < opts.duration) { 1725 | var process = (timestamp - startTimeStamp) / opts.duration; 1726 | var timingFunction = Timing[opts.timing]; 1727 | process = timingFunction(process); 1728 | opts.onProcess && opts.onProcess(process); 1729 | animationFrame(_step, delay); 1730 | } else { 1731 | opts.onProcess && opts.onProcess(1); 1732 | opts.onAnimationFinish && opts.onAnimationFinish(); 1733 | } 1734 | }; 1735 | _step = _step.bind(this); 1736 | 1737 | animationFrame(_step, delay); 1738 | } 1739 | 1740 | // stop animation immediately 1741 | // and tigger onAnimationFinish 1742 | Animation.prototype.stop = function () { 1743 | this.isStop = true; 1744 | }; 1745 | 1746 | function drawCharts(type, opts, config, context) { 1747 | var _this = this; 1748 | 1749 | var series = opts.series; 1750 | var categories = opts.categories; 1751 | series = fillSeriesColor(series, config); 1752 | 1753 | var _calLegendData = calLegendData(series, opts, config), 1754 | legendHeight = _calLegendData.legendHeight; 1755 | 1756 | config.legendHeight = legendHeight; 1757 | 1758 | var _calYAxisData = calYAxisData(series, opts, config), 1759 | yAxisWidth = _calYAxisData.yAxisWidth; 1760 | 1761 | config.yAxisWidth = yAxisWidth; 1762 | if (categories && categories.length) { 1763 | var _calCategoriesData = calCategoriesData(categories, opts, config), 1764 | xAxisHeight = _calCategoriesData.xAxisHeight, 1765 | angle = _calCategoriesData.angle; 1766 | 1767 | config.xAxisHeight = xAxisHeight; 1768 | config._xAxisTextAngle_ = angle; 1769 | } 1770 | if (type === 'pie' || type === 'ring') { 1771 | config._pieTextMaxLength_ = opts.dataLabel === false ? 0 : getPieTextMaxLength(series); 1772 | } 1773 | 1774 | var duration = opts.animation ? 1000 : 0; 1775 | this.animationInstance && this.animationInstance.stop(); 1776 | switch (type) { 1777 | case 'line': 1778 | this.animationInstance = new Animation({ 1779 | timing: 'easeIn', 1780 | duration: duration, 1781 | onProcess: function onProcess(process) { 1782 | drawYAxisGrid(opts, config, context); 1783 | 1784 | var _drawLineDataPoints = drawLineDataPoints(series, opts, config, context, process), 1785 | xAxisPoints = _drawLineDataPoints.xAxisPoints, 1786 | calPoints = _drawLineDataPoints.calPoints, 1787 | eachSpacing = _drawLineDataPoints.eachSpacing; 1788 | 1789 | _this.chartData.xAxisPoints = xAxisPoints; 1790 | _this.chartData.calPoints = calPoints; 1791 | _this.chartData.eachSpacing = eachSpacing; 1792 | drawXAxis(categories, opts, config, context); 1793 | drawLegend(opts.series, opts, config, context); 1794 | drawYAxis(series, opts, config, context); 1795 | drawToolTipBridge(opts, config, context, process); 1796 | drawCanvas(opts, context); 1797 | }, 1798 | onAnimationFinish: function onAnimationFinish() { 1799 | _this.event.trigger('renderComplete'); 1800 | } 1801 | }); 1802 | break; 1803 | case 'column': 1804 | this.animationInstance = new Animation({ 1805 | timing: 'easeIn', 1806 | duration: duration, 1807 | onProcess: function onProcess(process) { 1808 | drawYAxisGrid(opts, config, context); 1809 | 1810 | var _drawColumnDataPoints = drawColumnDataPoints(series, opts, config, context, process), 1811 | xAxisPoints = _drawColumnDataPoints.xAxisPoints, 1812 | eachSpacing = _drawColumnDataPoints.eachSpacing; 1813 | 1814 | _this.chartData.xAxisPoints = xAxisPoints; 1815 | _this.chartData.eachSpacing = eachSpacing; 1816 | drawXAxis(categories, opts, config, context); 1817 | drawLegend(opts.series, opts, config, context); 1818 | drawYAxis(series, opts, config, context); 1819 | drawCanvas(opts, context); 1820 | }, 1821 | onAnimationFinish: function onAnimationFinish() { 1822 | _this.event.trigger('renderComplete'); 1823 | } 1824 | }); 1825 | break; 1826 | case 'area': 1827 | this.animationInstance = new Animation({ 1828 | timing: 'easeIn', 1829 | duration: duration, 1830 | onProcess: function onProcess(process) { 1831 | drawYAxisGrid(opts, config, context); 1832 | 1833 | var _drawAreaDataPoints = drawAreaDataPoints(series, opts, config, context, process), 1834 | xAxisPoints = _drawAreaDataPoints.xAxisPoints, 1835 | calPoints = _drawAreaDataPoints.calPoints, 1836 | eachSpacing = _drawAreaDataPoints.eachSpacing; 1837 | 1838 | _this.chartData.xAxisPoints = xAxisPoints; 1839 | _this.chartData.calPoints = calPoints; 1840 | _this.chartData.eachSpacing = eachSpacing; 1841 | drawXAxis(categories, opts, config, context); 1842 | drawLegend(opts.series, opts, config, context); 1843 | drawYAxis(series, opts, config, context); 1844 | drawToolTipBridge(opts, config, context, process); 1845 | drawCanvas(opts, context); 1846 | }, 1847 | onAnimationFinish: function onAnimationFinish() { 1848 | _this.event.trigger('renderComplete'); 1849 | } 1850 | }); 1851 | break; 1852 | case 'ring': 1853 | case 'pie': 1854 | this.animationInstance = new Animation({ 1855 | timing: 'easeInOut', 1856 | duration: duration, 1857 | onProcess: function onProcess(process) { 1858 | _this.chartData.pieData = drawPieDataPoints(series, opts, config, context, process); 1859 | drawLegend(opts.series, opts, config, context); 1860 | drawCanvas(opts, context); 1861 | }, 1862 | onAnimationFinish: function onAnimationFinish() { 1863 | _this.event.trigger('renderComplete'); 1864 | } 1865 | }); 1866 | break; 1867 | case 'radar': 1868 | this.animationInstance = new Animation({ 1869 | timing: 'easeInOut', 1870 | duration: duration, 1871 | onProcess: function onProcess(process) { 1872 | _this.chartData.radarData = drawRadarDataPoints(series, opts, config, context, process); 1873 | drawLegend(opts.series, opts, config, context); 1874 | drawCanvas(opts, context); 1875 | }, 1876 | onAnimationFinish: function onAnimationFinish() { 1877 | _this.event.trigger('renderComplete'); 1878 | } 1879 | }); 1880 | break; 1881 | } 1882 | } 1883 | 1884 | // simple event implement 1885 | 1886 | function Event() { 1887 | this.events = {}; 1888 | } 1889 | 1890 | Event.prototype.addEventListener = function (type, listener) { 1891 | this.events[type] = this.events[type] || []; 1892 | this.events[type].push(listener); 1893 | }; 1894 | 1895 | Event.prototype.trigger = function () { 1896 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 1897 | args[_key] = arguments[_key]; 1898 | } 1899 | 1900 | var type = args[0]; 1901 | var params = args.slice(1); 1902 | if (!!this.events[type]) { 1903 | this.events[type].forEach(function (listener) { 1904 | try { 1905 | listener.apply(null, params); 1906 | } catch (e) { 1907 | console.error(e); 1908 | } 1909 | }); 1910 | } 1911 | }; 1912 | 1913 | var Charts = function Charts(opts) { 1914 | opts.title = opts.title || {}; 1915 | opts.subtitle = opts.subtitle || {}; 1916 | opts.yAxis = opts.yAxis || {}; 1917 | opts.xAxis = opts.xAxis || {}; 1918 | opts.extra = opts.extra || {}; 1919 | opts.legend = opts.legend === false ? false : true; 1920 | opts.animation = opts.animation === false ? false : true; 1921 | var config$$1 = assign({}, config); 1922 | config$$1.yAxisTitleWidth = opts.yAxis.disabled !== true && opts.yAxis.title ? config$$1.yAxisTitleWidth : 0; 1923 | config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : config$$1.pieChartLinePadding; 1924 | config$$1.pieChartTextPadding = opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding; 1925 | 1926 | this.opts = opts; 1927 | this.config = config$$1; 1928 | this.context = wx.createCanvasContext(opts.canvasId); 1929 | // store calcuated chart data 1930 | // such as chart point coordinate 1931 | this.chartData = {}; 1932 | this.event = new Event(); 1933 | this.scrollOption = { 1934 | currentOffset: 0, 1935 | startTouchX: 0, 1936 | distance: 0 1937 | }; 1938 | 1939 | drawCharts.call(this, opts.type, opts, config$$1, this.context); 1940 | }; 1941 | 1942 | Charts.prototype.updateData = function () { 1943 | var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 1944 | 1945 | this.opts.series = data.series || this.opts.series; 1946 | this.opts.categories = data.categories || this.opts.categories; 1947 | 1948 | this.opts.title = assign({}, this.opts.title, data.title || {}); 1949 | this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {}); 1950 | 1951 | drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); 1952 | }; 1953 | 1954 | Charts.prototype.stopAnimation = function () { 1955 | this.animationInstance && this.animationInstance.stop(); 1956 | }; 1957 | 1958 | Charts.prototype.addEventListener = function (type, listener) { 1959 | this.event.addEventListener(type, listener); 1960 | }; 1961 | 1962 | Charts.prototype.getCurrentDataIndex = function (e) { 1963 | var touches = e.touches && e.touches.length ? e.touches : e.changedTouches; 1964 | if (touches && touches.length) { 1965 | var _touches$ = touches[0], 1966 | x = _touches$.x, 1967 | y = _touches$.y; 1968 | 1969 | if (this.opts.type === 'pie' || this.opts.type === 'ring') { 1970 | return findPieChartCurrentIndex({ x: x, y: y }, this.chartData.pieData); 1971 | } else if (this.opts.type === 'radar') { 1972 | return findRadarChartCurrentIndex({ x: x, y: y }, this.chartData.radarData, this.opts.categories.length); 1973 | } else { 1974 | return findCurrentIndex({ x: x, y: y }, this.chartData.xAxisPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset)); 1975 | } 1976 | } 1977 | return -1; 1978 | }; 1979 | 1980 | Charts.prototype.showToolTip = function (e) { 1981 | var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 1982 | 1983 | if (this.opts.type === 'line' || this.opts.type === 'area') { 1984 | var index = this.getCurrentDataIndex(e); 1985 | var currentOffset = this.scrollOption.currentOffset; 1986 | 1987 | var opts = assign({}, this.opts, { 1988 | _scrollDistance_: currentOffset, 1989 | animation: false 1990 | }); 1991 | if (index > -1) { 1992 | var seriesData = getSeriesDataItem(this.opts.series, index); 1993 | if (seriesData.length !== 0) { 1994 | var _getToolTipData = getToolTipData(seriesData, this.chartData.calPoints, index, this.opts.categories, option), 1995 | textList = _getToolTipData.textList, 1996 | offset = _getToolTipData.offset; 1997 | 1998 | opts.tooltip = { 1999 | textList: textList, 2000 | offset: offset, 2001 | option: option 2002 | }; 2003 | } 2004 | } 2005 | drawCharts.call(this, opts.type, opts, this.config, this.context); 2006 | } 2007 | }; 2008 | 2009 | Charts.prototype.scrollStart = function (e) { 2010 | if (e.touches[0] && this.opts.enableScroll === true) { 2011 | this.scrollOption.startTouchX = e.touches[0].x; 2012 | } 2013 | }; 2014 | 2015 | Charts.prototype.scroll = function (e) { 2016 | // TODO throtting... 2017 | if (e.touches[0] && this.opts.enableScroll === true) { 2018 | var _distance = e.touches[0].x - this.scrollOption.startTouchX; 2019 | var currentOffset = this.scrollOption.currentOffset; 2020 | 2021 | var validDistance = calValidDistance(currentOffset + _distance, this.chartData, this.config, this.opts); 2022 | 2023 | this.scrollOption.distance = _distance = validDistance - currentOffset; 2024 | var opts = assign({}, this.opts, { 2025 | _scrollDistance_: currentOffset + _distance, 2026 | animation: false 2027 | }); 2028 | 2029 | drawCharts.call(this, opts.type, opts, this.config, this.context); 2030 | } 2031 | }; 2032 | 2033 | Charts.prototype.scrollEnd = function (e) { 2034 | if (this.opts.enableScroll === true) { 2035 | var _scrollOption = this.scrollOption, 2036 | currentOffset = _scrollOption.currentOffset, 2037 | distance = _scrollOption.distance; 2038 | 2039 | this.scrollOption.currentOffset = currentOffset + distance; 2040 | this.scrollOption.distance = 0; 2041 | } 2042 | }; 2043 | 2044 | module.exports = Charts; 2045 | -------------------------------------------------------------------------------- /example/area.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/area.gif -------------------------------------------------------------------------------- /example/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/area.png -------------------------------------------------------------------------------- /example/column.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/column.gif -------------------------------------------------------------------------------- /example/column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/column.png -------------------------------------------------------------------------------- /example/curve-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/curve-line.png -------------------------------------------------------------------------------- /example/line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/line.gif -------------------------------------------------------------------------------- /example/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/line.png -------------------------------------------------------------------------------- /example/pie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/pie.gif -------------------------------------------------------------------------------- /example/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/pie.png -------------------------------------------------------------------------------- /example/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/radar.png -------------------------------------------------------------------------------- /example/ring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/ring.gif -------------------------------------------------------------------------------- /example/ring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/ring.png -------------------------------------------------------------------------------- /example/scrollLine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/scrollLine.gif -------------------------------------------------------------------------------- /example/tooltip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaolin3303/wx-charts/13fdd6475d8ce782161a181c76f0e61706b25b4b/example/tooltip.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-charts", 3 | "version": "1.0.0", 4 | "description": "charts for wechat small app", 5 | "main": "src/app.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "babel-plugin-external-helpers": "^6.18.0", 9 | "babel-preset-es2015": "^6.18.0", 10 | "rollup-plugin-babel": "^2.6.1", 11 | "rollup-plugin-uglify": "^1.0.1" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "build": "rollup -c && rollup --config rollup.config.prod.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/xiaolin3303/wx-charts" 20 | }, 21 | "keywords": [ 22 | "wechat charts" 23 | ], 24 | "author": "xiaolin", 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | let banner = `/* 4 | * charts for WeChat small app v1.0 5 | * 6 | * https://github.com/xiaolin3303/wx-charts 7 | * 2016-11-28 8 | * 9 | * Designed and built with all the love of Web 10 | */ 11 | `; 12 | 13 | export default { 14 | entry: 'src/app.js', 15 | format: 'cjs', 16 | dest: 'dist/wxcharts.js', 17 | plugins: [ 18 | babel({ 19 | exclude: 'node_modules/**', 20 | }) 21 | ], 22 | banner: banner 23 | }; 24 | -------------------------------------------------------------------------------- /rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | 4 | let banner = `/* 5 | * charts for WeChat small app v1.0 6 | * 7 | * https://github.com/xiaolin3303/wx-charts 8 | * 2016-11-28 9 | * 10 | * Designed and built with all the love of Web 11 | */ 12 | `; 13 | 14 | export default { 15 | entry: 'src/app.js', 16 | format: 'cjs', 17 | dest: 'dist/wxcharts-min.js', 18 | plugins: [ 19 | babel({ 20 | exclude: 'node_modules/**', 21 | }), 22 | uglify() 23 | ], 24 | banner: banner 25 | }; 26 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Config from './config'; 2 | import { assign } from './util/polyfill/index'; 3 | import drawCharts from './components/draw-charts'; 4 | import Event from './util/event'; 5 | import { findCurrentIndex, findRadarChartCurrentIndex, findPieChartCurrentIndex, getSeriesDataItem, getToolTipData } from './components/charts-data' 6 | import { calValidDistance } from './components/charts-util'; 7 | 8 | let Charts = function(opts) { 9 | opts.title = opts.title || {}; 10 | opts.subtitle = opts.subtitle || {}; 11 | opts.yAxis = opts.yAxis || {}; 12 | opts.xAxis = opts.xAxis || {}; 13 | opts.extra = opts.extra || {}; 14 | opts.legend = opts.legend === false ? false : true; 15 | opts.animation = opts.animation === false ? false : true; 16 | let config = assign({}, Config); 17 | config.yAxisTitleWidth = opts.yAxis.disabled !== true && opts.yAxis.title ? config.yAxisTitleWidth : 0; 18 | config.pieChartLinePadding = opts.dataLabel === false ? 0 : config.pieChartLinePadding; 19 | config.pieChartTextPadding = opts.dataLabel === false ? 0 : config.pieChartTextPadding; 20 | 21 | this.opts = opts; 22 | this.config = config; 23 | this.context = wx.createCanvasContext(opts.canvasId); 24 | // store calcuated chart data 25 | // such as chart point coordinate 26 | this.chartData = {}; 27 | this.event = new Event(); 28 | this.scrollOption = { 29 | currentOffset: 0, 30 | startTouchX: 0, 31 | distance: 0 32 | } 33 | 34 | drawCharts.call(this, opts.type, opts, config, this.context); 35 | } 36 | 37 | Charts.prototype.updateData = function (data = {}) { 38 | this.opts.series = data.series || this.opts.series; 39 | this.opts.categories = data.categories || this.opts.categories; 40 | 41 | this.opts.title = assign({}, this.opts.title, data.title || {}); 42 | this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {}); 43 | 44 | drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); 45 | } 46 | 47 | Charts.prototype.stopAnimation = function () { 48 | this.animationInstance && this.animationInstance.stop(); 49 | } 50 | 51 | Charts.prototype.addEventListener = function (type, listener) { 52 | this.event.addEventListener(type, listener); 53 | } 54 | 55 | Charts.prototype.getCurrentDataIndex = function (e) { 56 | let touches = e.touches && e.touches.length ? e.touches : e.changedTouches; 57 | if (touches && touches.length) { 58 | let {x, y} = touches[0]; 59 | if (this.opts.type === 'pie' || this.opts.type === 'ring') { 60 | return findPieChartCurrentIndex({ x, y }, this.chartData.pieData); 61 | } else if (this.opts.type === 'radar') { 62 | return findRadarChartCurrentIndex({ x, y }, this.chartData.radarData, this.opts.categories.length); 63 | } else { 64 | return findCurrentIndex({ x, y }, this.chartData.xAxisPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset)); 65 | } 66 | } 67 | return -1; 68 | } 69 | 70 | Charts.prototype.showToolTip = function (e, option = {}) { 71 | if (this.opts.type === 'line' || this.opts.type === 'area') { 72 | let index = this.getCurrentDataIndex(e); 73 | let { currentOffset } = this.scrollOption; 74 | let opts = assign({}, this.opts, { 75 | _scrollDistance_: currentOffset, 76 | animation: false 77 | }); 78 | if (index > -1) { 79 | let seriesData = getSeriesDataItem(this.opts.series, index); 80 | if (seriesData.length !== 0) { 81 | let { textList, offset } = getToolTipData(seriesData, this.chartData.calPoints, index, this.opts.categories, option); 82 | opts.tooltip = { 83 | textList, 84 | offset, 85 | option 86 | }; 87 | } 88 | } 89 | drawCharts.call(this, opts.type, opts, this.config, this.context); 90 | } 91 | } 92 | 93 | Charts.prototype.scrollStart = function (e) { 94 | if (e.touches[0] && this.opts.enableScroll === true) { 95 | this.scrollOption.startTouchX = e.touches[0].x; 96 | } 97 | } 98 | 99 | Charts.prototype.scroll = function (e) { 100 | // TODO throtting... 101 | if (e.touches[0] && this.opts.enableScroll === true) { 102 | let _distance = e.touches[0].x - this.scrollOption.startTouchX; 103 | let { currentOffset } = this.scrollOption; 104 | let validDistance = calValidDistance(currentOffset + _distance, this.chartData, this.config, this.opts); 105 | 106 | this.scrollOption.distance = _distance = validDistance - currentOffset; 107 | let opts = assign({}, this.opts, { 108 | _scrollDistance_: currentOffset + _distance, 109 | animation: false 110 | }); 111 | 112 | drawCharts.call(this, opts.type, opts, this.config, this.context); 113 | } 114 | } 115 | 116 | Charts.prototype.scrollEnd = function (e) { 117 | if (this.opts.enableScroll === true) { 118 | let { currentOffset, distance } = this.scrollOption; 119 | this.scrollOption.currentOffset = currentOffset + distance; 120 | this.scrollOption.distance = 0; 121 | } 122 | } 123 | 124 | export default Charts; -------------------------------------------------------------------------------- /src/components/animation.js: -------------------------------------------------------------------------------- 1 | import Timing from '../util/timing' 2 | 3 | export default function Animation (opts) { 4 | this.isStop = false; 5 | opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration; 6 | opts.timing = opts.timing || 'linear'; 7 | 8 | let delay = 17; 9 | 10 | let createAnimationFrame = function () { 11 | if (typeof requestAnimationFrame !== 'undefined') { 12 | return requestAnimationFrame; 13 | } else if (typeof setTimeout !== 'undefined') { 14 | return function (step, delay) { 15 | setTimeout(function () { 16 | let timeStamp = +new Date(); 17 | step(timeStamp); 18 | }, delay); 19 | } 20 | } else { 21 | return function (step) { 22 | step(null); 23 | } 24 | } 25 | } 26 | let animationFrame = createAnimationFrame(); 27 | let startTimeStamp = null; 28 | let step = function (timestamp) { 29 | if (timestamp === null || this.isStop === true) { 30 | opts.onProcess && opts.onProcess(1); 31 | opts.onAnimationFinish && opts.onAnimationFinish(); 32 | return; 33 | } 34 | if (startTimeStamp === null) { 35 | startTimeStamp = timestamp; 36 | } 37 | if (timestamp - startTimeStamp < opts.duration) { 38 | let process = (timestamp - startTimeStamp) / opts.duration; 39 | let timingFunction = Timing[opts.timing]; 40 | process = timingFunction(process); 41 | opts.onProcess && opts.onProcess(process); 42 | animationFrame(step, delay); 43 | } else { 44 | opts.onProcess && opts.onProcess(1); 45 | opts.onAnimationFinish && opts.onAnimationFinish(); 46 | } 47 | }; 48 | step = step.bind(this); 49 | 50 | animationFrame(step, delay); 51 | } 52 | 53 | // stop animation immediately 54 | // and tigger onAnimationFinish 55 | Animation.prototype.stop = function () { 56 | this.isStop = true; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/charts-data.js: -------------------------------------------------------------------------------- 1 | import { getDataRange } from './charts-util' 2 | import Util from '../util/util' 3 | import { measureText, convertCoordinateOrigin, isInAngleRange } from './charts-util' 4 | 5 | function dataCombine(series) { 6 | return series.reduce(function(a, b) { 7 | return (a.data ? a.data : a).concat(b.data); 8 | }, []); 9 | } 10 | 11 | export function getSeriesDataItem(series, index) { 12 | let data = []; 13 | series.forEach((item) => { 14 | if (item.data[index] !== null && typeof item.data[index] !== 'undefined') { 15 | let seriesItem = {}; 16 | seriesItem.color = item.color; 17 | seriesItem.name = item.name; 18 | seriesItem.data = item.format ? item.format(item.data[index]) : item.data[index]; 19 | data.push(seriesItem); 20 | } 21 | }); 22 | 23 | return data; 24 | } 25 | 26 | export function getChartDataAreaBoundary (xAxisPoints) { 27 | return { 28 | leftBorder: xAxisPoints[0], 29 | rightBorder: xAxisPoints[xAxisPoints.length - 1] 30 | } 31 | } 32 | 33 | export function getMaxTextListLength(list) { 34 | let lengthList = list.map(item => measureText(item)); 35 | return Math.max.apply(null, lengthList); 36 | } 37 | 38 | export function getRadarCoordinateSeries(length) { 39 | let eachAngle = 2 * Math.PI / length; 40 | let CoordinateSeries = []; 41 | for (let i = 0; i < length; i++) { 42 | CoordinateSeries.push(eachAngle * i); 43 | } 44 | 45 | return CoordinateSeries.map(item => -1 * item + Math.PI / 2); 46 | } 47 | 48 | export function getToolTipData(seriesData, calPoints, index, categories, option = {}) { 49 | let textList = seriesData.map(item => { 50 | return { 51 | text: option.format ? option.format(item, categories[index]) : `${item.name}: ${item.data}`, 52 | color: item.color 53 | } 54 | }); 55 | let validCalPoints = []; 56 | let offset = { 57 | x: 0, 58 | y: 0 59 | }; 60 | calPoints.forEach(points => { 61 | if (typeof points[index] !== 'undefined' && points[index] !== null) { 62 | validCalPoints.push(points[index]); 63 | } 64 | }); 65 | validCalPoints.forEach(item => { 66 | offset.x = Math.round(item.x); 67 | offset.y += item.y; 68 | }) 69 | 70 | offset.y /= validCalPoints.length; 71 | return { textList, offset }; 72 | } 73 | 74 | export function findCurrentIndex (currentPoints, xAxisPoints, opts, config, offset = 0) { 75 | let currentIndex = -1; 76 | if (isInExactChartArea(currentPoints, opts, config)) { 77 | xAxisPoints.forEach((item, index) => { 78 | if (currentPoints.x + offset > item) { 79 | currentIndex = index; 80 | } 81 | }); 82 | } 83 | 84 | return currentIndex; 85 | } 86 | 87 | export function isInExactChartArea (currentPoints, opts, config) { 88 | return currentPoints.x < opts.width - config.padding 89 | && currentPoints.x > config.padding + config.yAxisWidth + config.yAxisTitleWidth 90 | && currentPoints.y > config.padding 91 | && currentPoints.y < opts.height - config.legendHeight - config.xAxisHeight - config.padding 92 | } 93 | 94 | export function findRadarChartCurrentIndex (currentPoints, radarData, count) { 95 | let eachAngleArea = 2 * Math.PI / count; 96 | let currentIndex = -1; 97 | if (isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)) { 98 | let angle = Math.atan2(radarData.center.y - currentPoints.y, currentPoints.x - radarData.center.x); 99 | angle = -1 * angle; 100 | if (angle < 0) { 101 | angle += 2 * Math.PI; 102 | } 103 | 104 | function fixAngle (angle) { 105 | if (angle < 0) { 106 | angle += 2 * Math.PI; 107 | } 108 | if (angle > 2 * Math.PI) { 109 | angle -= 2 * Math.PI; 110 | } 111 | return angle; 112 | } 113 | let angleList = radarData.angleList.map(item => { 114 | item = fixAngle(-1 * item); 115 | 116 | return item; 117 | }); 118 | 119 | angleList.forEach((item, index) => { 120 | let rangeStart = fixAngle(item - eachAngleArea / 2); 121 | let rangeEnd = fixAngle(item + eachAngleArea / 2); 122 | if (rangeEnd < rangeStart) { 123 | rangeEnd += 2 * Math.PI; 124 | } 125 | if ((angle >= rangeStart && angle <= rangeEnd) 126 | || (angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd)) { 127 | currentIndex = index; 128 | } 129 | }); 130 | } 131 | 132 | return currentIndex; 133 | } 134 | 135 | export function findPieChartCurrentIndex (currentPoints, pieData) { 136 | let currentIndex = -1; 137 | if (isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) { 138 | let angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x); 139 | angle = -angle; 140 | for (let i = 0, len = pieData.series.length; i < len; i++) { 141 | let item = pieData.series[i]; 142 | if (isInAngleRange(angle, item._start_, item._start_ + item._proportion_ * 2 * Math.PI)) { 143 | currentIndex = i; 144 | break; 145 | } 146 | } 147 | } 148 | 149 | return currentIndex; 150 | } 151 | 152 | export function isInExactPieChartArea (currentPoints, center, radius) { 153 | return Math.pow(currentPoints.x - center.x, 2) + Math.pow(currentPoints.y - center.y, 2) <= Math.pow(radius, 2); 154 | } 155 | 156 | export function splitPoints(points) { 157 | let newPoints = []; 158 | let items = []; 159 | points.forEach((item, index) => { 160 | if (item !== null) { 161 | items.push(item); 162 | } else { 163 | if (items.length) { 164 | newPoints.push(items); 165 | } 166 | items = []; 167 | } 168 | }); 169 | if (items.length) { 170 | newPoints.push(items); 171 | } 172 | 173 | return newPoints; 174 | } 175 | 176 | export function calLegendData(series, opts, config) { 177 | if (opts.legend === false) { 178 | return { 179 | legendList: [], 180 | legendHeight: 0 181 | } 182 | } 183 | let padding = 5; 184 | let marginTop = 8; 185 | let shapeWidth = 15; 186 | let legendList = []; 187 | let widthCount = 0; 188 | let currentRow = []; 189 | series.forEach((item) => { 190 | let itemWidth = 3 * padding + shapeWidth + measureText(item.name || 'undefined'); 191 | if (widthCount + itemWidth > opts.width) { 192 | legendList.push(currentRow); 193 | widthCount = itemWidth; 194 | currentRow = [item]; 195 | } else { 196 | widthCount += itemWidth; 197 | currentRow.push(item); 198 | } 199 | }); 200 | if (currentRow.length) { 201 | legendList.push(currentRow); 202 | } 203 | 204 | return { 205 | legendList, 206 | legendHeight: legendList.length * (config.fontSize + marginTop) + padding 207 | } 208 | } 209 | 210 | export function calCategoriesData(categories, opts, config) { 211 | let result = { 212 | angle: 0, 213 | xAxisHeight: config.xAxisHeight 214 | }; 215 | let { eachSpacing } = getXAxisPoints(categories, opts, config); 216 | 217 | // get max length of categories text 218 | let categoriesTextLenth = categories.map((item) => { 219 | return measureText(item); 220 | }); 221 | 222 | let maxTextLength = Math.max.apply(this, categoriesTextLenth); 223 | 224 | if ( maxTextLength + 2 * config.xAxisTextPadding > eachSpacing) { 225 | result.angle = 45 * Math.PI / 180; 226 | result.xAxisHeight = 2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle); 227 | } 228 | 229 | return result; 230 | } 231 | 232 | export function getRadarDataPoints(angleList, center, radius, series, opts, process = 1 ) { 233 | let radarOption = opts.extra.radar || {}; 234 | radarOption.max = radarOption.max || 0; 235 | let maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series))); 236 | 237 | let data = []; 238 | series.forEach(each => { 239 | let listItem = {}; 240 | listItem.color = each.color; 241 | listItem.data = []; 242 | each.data.forEach((item, index) => { 243 | let tmp = {}; 244 | tmp.angle = angleList[index]; 245 | 246 | tmp.proportion = item / maxData; 247 | tmp.position = convertCoordinateOrigin(radius * tmp.proportion * process * Math.cos(tmp.angle), radius * tmp.proportion * process * Math.sin(tmp.angle), center); 248 | listItem.data.push(tmp); 249 | }); 250 | 251 | data.push(listItem); 252 | }); 253 | 254 | return data; 255 | } 256 | 257 | export function getPieDataPoints(series, process = 1) { 258 | var count = 0; 259 | var _start_ = 0; 260 | series.forEach(function(item) { 261 | item.data = item.data === null ? 0 : item.data; 262 | count += item.data; 263 | }); 264 | series.forEach(function(item) { 265 | item.data = item.data === null ? 0 : item.data; 266 | item._proportion_ = item.data / count * process; 267 | }); 268 | series.forEach(function(item) { 269 | item._start_ = _start_; 270 | _start_ += 2 * item._proportion_ * Math.PI; 271 | }); 272 | 273 | return series; 274 | } 275 | 276 | export function getPieTextMaxLength(series) { 277 | series = getPieDataPoints(series); 278 | let maxLength = 0; 279 | series.forEach((item) => { 280 | let text = item.format ? item.format(+item._proportion_.toFixed(2)) : `${Util.toFixed(item._proportion_ * 100)}%`; 281 | maxLength = Math.max(maxLength, measureText(text)); 282 | }); 283 | 284 | return maxLength; 285 | } 286 | 287 | export function fixColumeData(points, eachSpacing, columnLen, index, config, opts) { 288 | return points.map(function(item) { 289 | if (item === null) { 290 | return null; 291 | } 292 | item.width = (eachSpacing - 2 * config.columePadding) / columnLen; 293 | 294 | if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) { 295 | // customer column width 296 | item.width = Math.min(item.width, +opts.extra.column.width); 297 | } else { 298 | // default width should less tran 25px 299 | // don't ask me why, I don't know 300 | item.width = Math.min(item.width, 25); 301 | } 302 | item.x += (index + 0.5 - (columnLen) / 2) * item.width; 303 | 304 | return item; 305 | }); 306 | } 307 | 308 | export function getXAxisPoints(categories, opts, config) { 309 | let yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 310 | let spacingValid = opts.width - 2 * config.padding - yAxisTotalWidth; 311 | let dataCount = opts.enableScroll ? Math.min(5, categories.length) : categories.length; 312 | let eachSpacing = spacingValid / dataCount; 313 | 314 | let xAxisPoints = []; 315 | let startX = config.padding + yAxisTotalWidth; 316 | let endX = opts.width - config.padding; 317 | categories.forEach(function(item, index) { 318 | xAxisPoints.push(startX + index * eachSpacing); 319 | }); 320 | if (opts.enableScroll === true) { 321 | xAxisPoints.push(startX + categories.length * eachSpacing); 322 | } else { 323 | xAxisPoints.push(endX); 324 | } 325 | 326 | return { xAxisPoints, startX, endX, eachSpacing }; 327 | } 328 | 329 | export function getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process = 1) { 330 | let points = []; 331 | let validHeight = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 332 | data.forEach(function(item, index) { 333 | if (item === null) { 334 | points.push(null); 335 | } else { 336 | let point = {}; 337 | point.x = xAxisPoints[index] + Math.round(eachSpacing / 2); 338 | let height = validHeight * (item - minRange) / (maxRange - minRange); 339 | height *= process; 340 | point.y = opts.height - config.xAxisHeight - config.legendHeight - Math.round(height) - config.padding; 341 | points.push(point); 342 | } 343 | }); 344 | 345 | return points; 346 | } 347 | 348 | export function getYAxisTextList(series, opts, config) { 349 | let data = dataCombine(series); 350 | // remove null from data 351 | data = data.filter((item) => { 352 | return item !== null; 353 | }); 354 | let minData = Math.min.apply(this, data); 355 | let maxData = Math.max.apply(this, data); 356 | if (typeof opts.yAxis.min === 'number') { 357 | minData = Math.min(opts.yAxis.min, minData); 358 | } 359 | if (typeof opts.yAxis.max === 'number') { 360 | maxData = Math.max(opts.yAxis.max, maxData); 361 | } 362 | 363 | // fix issue https://github.com/xiaolin3303/wx-charts/issues/9 364 | if (minData === maxData) { 365 | let rangeSpan = maxData || 1; 366 | minData -= rangeSpan; 367 | maxData += rangeSpan; 368 | } 369 | 370 | let dataRange = getDataRange(minData, maxData); 371 | let minRange = dataRange.minRange; 372 | let maxRange = dataRange.maxRange; 373 | 374 | let range = []; 375 | let eachRange = (maxRange - minRange) / config.yAxisSplit; 376 | 377 | for (var i = 0; i <= config.yAxisSplit; i++) { 378 | range.push(minRange + eachRange * i); 379 | } 380 | return range.reverse(); 381 | } 382 | 383 | export function calYAxisData(series, opts, config) { 384 | 385 | let ranges = getYAxisTextList(series, opts, config); 386 | let yAxisWidth = config.yAxisWidth; 387 | let rangesFormat = ranges.map(function(item) { 388 | item = Util.toFixed(item, 2); 389 | item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item; 390 | yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5); 391 | return item; 392 | }); 393 | if (opts.yAxis.disabled === true) { 394 | yAxisWidth = 0; 395 | } 396 | 397 | return { rangesFormat, ranges, yAxisWidth }; 398 | } -------------------------------------------------------------------------------- /src/components/charts-util.js: -------------------------------------------------------------------------------- 1 | import Util from '../util/util' 2 | 3 | function findRange (num, type, limit) { 4 | if (isNaN(num)) { 5 | throw new Error('[wxCharts] unvalid series data!'); 6 | } 7 | limit = limit || 10; 8 | type = type ? type : 'upper'; 9 | let multiple = 1; 10 | while (limit < 1) { 11 | limit *= 10; 12 | multiple *= 10; 13 | } 14 | if (type === 'upper') { 15 | num = Math.ceil(num * multiple); 16 | } else { 17 | num = Math.floor(num * multiple); 18 | } 19 | while (num % limit !== 0) { 20 | if (type === 'upper') { 21 | num++; 22 | } else { 23 | num--; 24 | } 25 | } 26 | 27 | return num / multiple; 28 | } 29 | 30 | export function calValidDistance (distance, chartData, config, opts) { 31 | 32 | let dataChartAreaWidth = opts.width - config.padding - chartData.xAxisPoints[0]; 33 | let dataChartWidth = chartData.eachSpacing * opts.categories.length; 34 | let validDistance = distance; 35 | if (distance >= 0) { 36 | validDistance = 0; 37 | } else if (Math.abs(distance) >= (dataChartWidth - dataChartAreaWidth)) { 38 | validDistance = dataChartAreaWidth - dataChartWidth; 39 | } 40 | return validDistance; 41 | } 42 | 43 | export function isInAngleRange(angle, startAngle, endAngle) { 44 | function adjust (angle) { 45 | while (angle < 0) { 46 | angle += 2 * Math.PI; 47 | } 48 | while (angle > 2 * Math.PI) { 49 | angle -= 2 * Math.PI; 50 | } 51 | 52 | return angle; 53 | } 54 | 55 | angle = adjust(angle); 56 | startAngle = adjust(startAngle); 57 | endAngle = adjust(endAngle); 58 | if (startAngle > endAngle) { 59 | endAngle += 2 * Math.PI; 60 | if (angle < startAngle) { 61 | angle += 2 * Math.PI; 62 | } 63 | } 64 | 65 | return angle >= startAngle && angle <= endAngle; 66 | } 67 | 68 | export function calRotateTranslate(x, y, h) { 69 | var xv = x; 70 | var yv = h - y; 71 | 72 | var transX = xv + (h - yv -xv) / Math.sqrt(2); 73 | transX *= -1; 74 | 75 | var transY = (h -yv) * (Math.sqrt(2) - 1) - (h - yv - xv) / Math.sqrt(2); 76 | 77 | return { 78 | transX, 79 | transY 80 | }; 81 | } 82 | 83 | 84 | export function createCurveControlPoints(points, i) { 85 | 86 | function isNotMiddlePoint (points, i) { 87 | if (points[i - 1] && points[i + 1]) { 88 | return points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) 89 | || points[i].y <= Math.min(points[i - 1].y, points[i + 1].y); 90 | } else { 91 | return false 92 | } 93 | } 94 | 95 | const a = 0.2; 96 | const b = 0.2; 97 | let pAx = null; 98 | let pAy = null; 99 | let pBx = null; 100 | let pBy = null; 101 | if(i < 1){ 102 | pAx = points[0].x + (points[1].x-points[0].x) * a; 103 | pAy = points[0].y + (points[1].y-points[0].y) * a; 104 | }else{ 105 | pAx = points[i].x + (points[i + 1].x - points[i-1].x) * a; 106 | pAy = points[i].y + (points[i + 1].y - points[i-1].y) * a; 107 | } 108 | 109 | if(i > points.length - 3){ 110 | let last =points.length - 1; 111 | pBx = points[last].x - (points[last].x - points[last - 1].x) * b; 112 | pBy = points[last].y - (points[last].y - points[last - 1].y) * b; 113 | }else{ 114 | pBx = points[i + 1].x - (points[i + 2].x-points[i].x) * b; 115 | pBy = points[i + 1].y - (points[i + 2].y-points[i].y) * b; 116 | } 117 | 118 | // fix issue https://github.com/xiaolin3303/wx-charts/issues/79 119 | if (isNotMiddlePoint(points, i + 1)) { 120 | pBy = points[i + 1].y; 121 | } 122 | if (isNotMiddlePoint(points, i)) { 123 | pAy = points[i].y; 124 | } 125 | 126 | return { 127 | ctrA: {x: pAx, y: pAy}, 128 | ctrB: {x: pBx, y: pBy} 129 | } 130 | } 131 | 132 | export function convertCoordinateOrigin (x, y, center) { 133 | return { 134 | x: center.x + x, 135 | y: center.y - y 136 | } 137 | } 138 | 139 | export function avoidCollision (obj, target) { 140 | if (target) { 141 | // is collision test 142 | while (Util.isCollision(obj, target)) { 143 | if (obj.start.x > 0) { 144 | obj.start.y--; 145 | } else if (obj.start.x < 0) { 146 | obj.start.y++; 147 | } else { 148 | if (obj.start.y > 0) { 149 | obj.start.y++; 150 | } else { 151 | obj.start.y--; 152 | } 153 | } 154 | } 155 | } 156 | return obj; 157 | } 158 | 159 | export function fillSeriesColor (series, config) { 160 | let index = 0; 161 | return series.map(function(item) { 162 | if (!item.color) { 163 | item.color = config.colors[index]; 164 | index = (index + 1) % config.colors.length; 165 | } 166 | return item; 167 | }); 168 | } 169 | 170 | export function getDataRange (minData, maxData) { 171 | let limit = 0; 172 | let range = maxData - minData; 173 | if (range >= 10000) { 174 | limit = 1000; 175 | } else if (range >= 1000) { 176 | limit = 100; 177 | } else if (range >= 100) { 178 | limit = 10; 179 | } else if (range >= 10) { 180 | limit = 5; 181 | } else if (range >= 1) { 182 | limit = 1; 183 | } else if (range >= 0.1) { 184 | limit = 0.1; 185 | } else { 186 | limit = 0.01; 187 | } 188 | return { 189 | minRange: findRange(minData, 'lower', limit), 190 | maxRange: findRange(maxData, 'upper', limit) 191 | } 192 | } 193 | 194 | export function measureText (text, fontSize=10) { 195 | // wx canvas 未实现measureText方法, 此处自行实现 196 | text = String(text); 197 | var text = text.split(''); 198 | var width = 0; 199 | text.forEach(function(item) { 200 | if (/[a-zA-Z]/.test(item)) { 201 | width += 7; 202 | } else if (/[0-9]/.test(item)) { 203 | width += 5.5; 204 | } else if (/\./.test(item)) { 205 | width += 2.7; 206 | } else if (/-/.test(item)) { 207 | width += 3.25; 208 | } else if (/[\u4e00-\u9fa5]/.test(item)) { 209 | width += 10; 210 | } else if (/\(|\)/.test(item)) { 211 | width += 3.73; 212 | } else if (/\s/.test(item)) { 213 | width += 2.5; 214 | } else if (/%/.test(item)) { 215 | width += 8; 216 | } else { 217 | width += 10; 218 | } 219 | }); 220 | return width * fontSize / 10; 221 | } -------------------------------------------------------------------------------- /src/components/draw-charts.js: -------------------------------------------------------------------------------- 1 | import { drawYAxisGrid, drawToolTipBridge, drawRadarDataPoints, drawCanvas, drawLegend, drawPieDataPoints, drawLineDataPoints, drawAreaDataPoints, drawColumnDataPoints, drawYAxis, drawXAxis } from './draw' 2 | import { calYAxisData, getPieTextMaxLength, calCategoriesData, calLegendData } from './charts-data' 3 | import { fillSeriesColor } from './charts-util'; 4 | import Animation from './animation' 5 | 6 | export default function drawCharts (type, opts, config, context) { 7 | let series = opts.series; 8 | let categories = opts.categories; 9 | series = fillSeriesColor(series, config); 10 | 11 | let { legendHeight } = calLegendData(series, opts, config); 12 | config.legendHeight = legendHeight; 13 | 14 | let { yAxisWidth } = calYAxisData(series, opts, config); 15 | config.yAxisWidth = yAxisWidth; 16 | if (categories && categories.length) { 17 | let { xAxisHeight, angle } = calCategoriesData(categories, opts, config); 18 | config.xAxisHeight = xAxisHeight; 19 | config._xAxisTextAngle_ = angle; 20 | } 21 | if (type === 'pie' || type === 'ring') { 22 | config._pieTextMaxLength_ = opts.dataLabel === false ? 0 : getPieTextMaxLength(series); 23 | } 24 | 25 | let duration = opts.animation ? 1000 : 0; 26 | this.animationInstance && this.animationInstance.stop(); 27 | switch (type) { 28 | case 'line': 29 | this.animationInstance = new Animation({ 30 | timing: 'easeIn', 31 | duration: duration, 32 | onProcess: (process) => { 33 | drawYAxisGrid(opts, config, context); 34 | let { xAxisPoints, calPoints, eachSpacing } = drawLineDataPoints(series, opts, config, context, process); 35 | this.chartData.xAxisPoints = xAxisPoints; 36 | this.chartData.calPoints = calPoints; 37 | this.chartData.eachSpacing = eachSpacing; 38 | drawXAxis(categories, opts, config, context); 39 | drawLegend(opts.series, opts, config, context); 40 | drawYAxis(series, opts, config, context); 41 | drawToolTipBridge(opts, config, context, process); 42 | drawCanvas(opts, context); 43 | }, 44 | onAnimationFinish: () => { 45 | this.event.trigger('renderComplete'); 46 | } 47 | }); 48 | break; 49 | case 'column': 50 | this.animationInstance = new Animation({ 51 | timing: 'easeIn', 52 | duration: duration, 53 | onProcess: (process) => { 54 | drawYAxisGrid(opts, config, context); 55 | let { xAxisPoints, eachSpacing } = drawColumnDataPoints(series, opts, config, context, process); 56 | this.chartData.xAxisPoints = xAxisPoints; 57 | this.chartData.eachSpacing = eachSpacing; 58 | drawXAxis(categories, opts, config, context); 59 | drawLegend(opts.series, opts, config, context); 60 | drawYAxis(series, opts, config, context); 61 | drawCanvas(opts, context); 62 | }, 63 | onAnimationFinish: () => { 64 | this.event.trigger('renderComplete'); 65 | } 66 | }); 67 | break; 68 | case 'area': 69 | this.animationInstance = new Animation({ 70 | timing: 'easeIn', 71 | duration: duration, 72 | onProcess: (process) => { 73 | drawYAxisGrid(opts, config, context); 74 | let { xAxisPoints, calPoints, eachSpacing } = drawAreaDataPoints(series, opts, config, context, process); 75 | this.chartData.xAxisPoints = xAxisPoints; 76 | this.chartData.calPoints = calPoints; 77 | this.chartData.eachSpacing = eachSpacing; 78 | drawXAxis(categories, opts, config, context); 79 | drawLegend(opts.series, opts, config, context); 80 | drawYAxis(series, opts, config, context); 81 | drawToolTipBridge(opts, config, context, process); 82 | drawCanvas(opts, context); 83 | }, 84 | onAnimationFinish: () => { 85 | this.event.trigger('renderComplete'); 86 | } 87 | }); 88 | break; 89 | case 'ring': 90 | case 'pie': 91 | this.animationInstance = new Animation({ 92 | timing: 'easeInOut', 93 | duration: duration, 94 | onProcess: (process) => { 95 | this.chartData.pieData = drawPieDataPoints(series, opts, config, context, process); 96 | drawLegend(opts.series, opts, config, context); 97 | drawCanvas(opts, context); 98 | }, 99 | onAnimationFinish: () => { 100 | this.event.trigger('renderComplete'); 101 | } 102 | }); 103 | break; 104 | case 'radar': 105 | this.animationInstance = new Animation({ 106 | timing: 'easeInOut', 107 | duration: duration, 108 | onProcess: (process) => { 109 | this.chartData.radarData = drawRadarDataPoints(series, opts, config, context, process); 110 | drawLegend(opts.series, opts, config, context); 111 | drawCanvas(opts, context); 112 | }, 113 | onAnimationFinish: () => { 114 | this.event.trigger('renderComplete'); 115 | } 116 | }); 117 | break; 118 | } 119 | } -------------------------------------------------------------------------------- /src/components/draw-data-shape.js: -------------------------------------------------------------------------------- 1 | export default function drawPointShape (points, color, shape, context) { 2 | context.beginPath(); 3 | context.setStrokeStyle("#ffffff"); 4 | context.setLineWidth(1); 5 | context.setFillStyle(color); 6 | 7 | if (shape === 'diamond') { 8 | points.forEach(function(item, index) { 9 | if (item !== null) { 10 | context.moveTo(item.x, item.y - 4.5); 11 | context.lineTo(item.x - 4.5, item.y); 12 | context.lineTo(item.x, item.y + 4.5); 13 | context.lineTo(item.x + 4.5, item.y); 14 | context.lineTo(item.x, item.y - 4.5); 15 | } 16 | }); 17 | } else if (shape === 'circle') { 18 | points.forEach(function(item, index) { 19 | if (item !== null) { 20 | context.moveTo(item.x + 3.5, item.y) 21 | context.arc(item.x, item.y, 4, 0, 2 * Math.PI, false) 22 | } 23 | }); 24 | } else if (shape === 'rect') { 25 | points.forEach(function(item, index) { 26 | if (item !== null) { 27 | context.moveTo(item.x - 3.5, item.y - 3.5); 28 | context.rect(item.x - 3.5, item.y - 3.5, 7, 7); 29 | } 30 | }); 31 | } else if (shape === 'triangle') { 32 | points.forEach(function(item, index) { 33 | if (item !== null) { 34 | context.moveTo(item.x, item.y - 4.5); 35 | context.lineTo(item.x - 4.5, item.y + 4.5); 36 | context.lineTo(item.x + 4.5, item.y + 4.5); 37 | context.lineTo(item.x, item.y - 4.5); 38 | } 39 | }); 40 | } 41 | context.closePath(); 42 | context.fill(); 43 | context.stroke(); 44 | } -------------------------------------------------------------------------------- /src/components/draw-data-text.js: -------------------------------------------------------------------------------- 1 | import { measureText, convertCoordinateOrigin, avoidCollision } from './charts-util' 2 | import Util from '../util/util' 3 | 4 | export function drawRingTitle(opts, config, context) { 5 | let titlefontSize = opts.title.fontSize || config.titleFontSize; 6 | let subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize; 7 | let title = opts.title.name || ''; 8 | let subtitle = opts.subtitle.name || ''; 9 | let titleFontColor = opts.title.color || config.titleColor; 10 | let subtitleFontColor = opts.subtitle.color || config.subtitleColor; 11 | let titleHeight = title ? titlefontSize : 0; 12 | let subtitleHeight = subtitle ? subtitlefontSize : 0; 13 | let margin = 5; 14 | if (subtitle) { 15 | let textWidth = measureText(subtitle, subtitlefontSize); 16 | let startX = (opts.width - textWidth) / 2 + (opts.subtitle.offsetX || 0); 17 | let startY = (opts.height - config.legendHeight + subtitlefontSize) / 2; 18 | if (title) { 19 | startY -= (titleHeight + margin) / 2; 20 | } 21 | context.beginPath(); 22 | context.setFontSize(subtitlefontSize); 23 | context.setFillStyle(subtitleFontColor); 24 | context.fillText(subtitle, startX, startY); 25 | context.stroke(); 26 | context.closePath(); 27 | } 28 | if (title) { 29 | let textWidth = measureText(title, titlefontSize); 30 | let startX = (opts.width - textWidth) / 2 + (opts.title.offsetX || 0); 31 | let startY = (opts.height - config.legendHeight + titlefontSize) / 2; 32 | if (subtitle) { 33 | startY += (subtitleHeight + margin) / 2; 34 | } 35 | context.beginPath(); 36 | context.setFontSize(titlefontSize); 37 | context.setFillStyle(titleFontColor); 38 | context.fillText(title, startX, startY); 39 | context.stroke(); 40 | context.closePath(); 41 | } 42 | } 43 | 44 | export function drawPointText (points, series, config, context) { 45 | // 绘制数据文案 46 | let data = series.data; 47 | 48 | context.beginPath(); 49 | context.setFontSize(config.fontSize); 50 | context.setFillStyle('#666666'); 51 | points.forEach(function(item, index) { 52 | if (item !== null) { 53 | let formatVal = series.format ? series.format(data[index]) : data[index]; 54 | context.fillText(formatVal, item.x - measureText(formatVal) / 2, item.y - 2); 55 | } 56 | }); 57 | context.closePath(); 58 | context.stroke(); 59 | } 60 | 61 | export function drawRadarLabel(angleList, radius, centerPosition, opts, config, context) { 62 | let radarOption = opts.extra.radar || {}; 63 | radius += config.radarLabelTextMargin; 64 | context.beginPath(); 65 | context.setFontSize(config.fontSize); 66 | context.setFillStyle(radarOption.labelColor || '#666666'); 67 | angleList.forEach((angle, index) => { 68 | let pos = { 69 | x: radius * Math.cos(angle), 70 | y: radius * Math.sin(angle) 71 | } 72 | let posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition); 73 | let startX = posRelativeCanvas.x; 74 | let startY = posRelativeCanvas.y; 75 | if (Util.approximatelyEqual(pos.x, 0)) { 76 | startX -= measureText(opts.categories[index] || '') / 2; 77 | } else if (pos.x < 0) { 78 | startX -= measureText(opts.categories[index] || ''); 79 | } 80 | context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2); 81 | }); 82 | context.stroke(); 83 | context.closePath(); 84 | } 85 | 86 | export function drawPieText (series, opts, config, context, radius, center) { 87 | let lineRadius = radius + config.pieChartLinePadding; 88 | let textRadius = lineRadius + config.pieChartTextPadding; 89 | let textObjectCollection = []; 90 | let lastTextObject = null; 91 | 92 | let seriesConvert = series.map((item) => { 93 | let arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2); 94 | let text = item.format ? item.format(+item._proportion_.toFixed(2)) : `${Util.toFixed(item._proportion_ * 100)}%`; 95 | let color = item.color; 96 | return { arc, text, color }; 97 | }); 98 | seriesConvert.forEach((item) => { 99 | // line end 100 | let orginX1 = Math.cos(item.arc) * lineRadius; 101 | let orginY1 = Math.sin(item.arc) * lineRadius; 102 | 103 | // line start 104 | let orginX2 = Math.cos(item.arc) * radius; 105 | let orginY2 = Math.sin(item.arc) * radius; 106 | 107 | // text start 108 | let orginX3 = orginX1 >= 0 ? orginX1 + config.pieChartTextPadding : orginX1 - config.pieChartTextPadding ; 109 | let orginY3 = orginY1; 110 | 111 | let textWidth = measureText(item.text); 112 | let startY = orginY3; 113 | 114 | if (lastTextObject && Util.isSameXCoordinateArea(lastTextObject.start, {x: orginX3})) { 115 | if (orginX3 > 0) { 116 | startY = Math.min(orginY3, lastTextObject.start.y); 117 | } else if (orginX1 < 0) { 118 | startY = Math.max(orginY3, lastTextObject.start.y); 119 | } else { 120 | if (orginY3 > 0) { 121 | startY = Math.max(orginY3, lastTextObject.start.y); 122 | } else { 123 | startY = Math.min(orginY3, lastTextObject.start.y); 124 | } 125 | } 126 | } 127 | 128 | if (orginX3 < 0) { 129 | orginX3 -= textWidth; 130 | } 131 | 132 | let textObject = { 133 | lineStart: { 134 | x: orginX2, 135 | y: orginY2 136 | }, 137 | lineEnd: { 138 | x: orginX1, 139 | y: orginY1 140 | }, 141 | start: { 142 | x: orginX3, 143 | y: startY 144 | }, 145 | width: textWidth, 146 | height: config.fontSize, 147 | text: item.text, 148 | color: item.color 149 | } 150 | 151 | lastTextObject = avoidCollision(textObject, lastTextObject); 152 | textObjectCollection.push(lastTextObject); 153 | }); 154 | 155 | textObjectCollection.forEach((item) => { 156 | let lineStartPoistion = convertCoordinateOrigin(item.lineStart.x, item.lineStart.y, center); 157 | let lineEndPoistion = convertCoordinateOrigin(item.lineEnd.x, item.lineEnd.y, center); 158 | let textPosition = convertCoordinateOrigin(item.start.x, item.start.y, center); 159 | context.setLineWidth(1); 160 | context.setFontSize(config.fontSize); 161 | context.beginPath(); 162 | context.setStrokeStyle(item.color); 163 | context.setFillStyle(item.color); 164 | context.moveTo(lineStartPoistion.x, lineStartPoistion.y); 165 | let curveStartX = item.start.x < 0 ? textPosition.x + item.width : textPosition.x; 166 | let textStartX = item.start.x < 0 ? textPosition.x - 5 : textPosition.x + 5; 167 | context.quadraticCurveTo(lineEndPoistion.x, lineEndPoistion.y, curveStartX, textPosition.y); 168 | context.moveTo(lineStartPoistion.x, lineStartPoistion.y); 169 | context.stroke(); 170 | context.closePath(); 171 | context.beginPath(); 172 | context.moveTo(textPosition.x + item.width, textPosition.y); 173 | context.arc(curveStartX, textPosition.y, 2, 0, 2 * Math.PI); 174 | context.closePath(); 175 | context.fill(); 176 | context.beginPath(); 177 | context.setFillStyle('#666666'); 178 | context.fillText(item.text, textStartX, textPosition.y + 3); 179 | context.closePath(); 180 | context.stroke(); 181 | 182 | context.closePath(); 183 | }); 184 | } -------------------------------------------------------------------------------- /src/components/draw-tooltip.js: -------------------------------------------------------------------------------- 1 | import { measureText } from './charts-util' 2 | import { assign } from '../util/polyfill/index'; 3 | 4 | export function drawToolTipSplitLine(offsetX, opts, config, context) { 5 | let startY = config.padding; 6 | let endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 7 | context.beginPath(); 8 | context.setStrokeStyle('#cccccc'); 9 | context.setLineWidth(1); 10 | context.moveTo(offsetX, startY); 11 | context.lineTo(offsetX, endY); 12 | context.stroke(); 13 | context.closePath(); 14 | } 15 | 16 | export function drawToolTip(textList, offset, opts, config, context) { 17 | let legendWidth = 4; 18 | let legendMarginRight = 5; 19 | let arrowWidth = 8; 20 | let isOverRightBorder = false; 21 | offset = assign({ 22 | x: 0, 23 | y: 0 24 | }, offset); 25 | offset.y -= 8; 26 | let textWidth = textList.map((item) => { 27 | return measureText(item.text); 28 | }); 29 | 30 | let toolTipWidth = legendWidth + legendMarginRight + 4 * config.toolTipPadding + Math.max.apply(null, textWidth); 31 | let toolTipHeight = 2 * config.toolTipPadding + textList.length * config.toolTipLineHeight; 32 | 33 | // if beyond the right border 34 | if (offset.x - Math.abs(opts._scrollDistance_) + arrowWidth + toolTipWidth > opts.width) { 35 | isOverRightBorder = true; 36 | } 37 | 38 | // draw background rect 39 | context.beginPath(); 40 | context.setFillStyle(opts.tooltip.option.background || config.toolTipBackground); 41 | context.setGlobalAlpha(config.toolTipOpacity); 42 | if (isOverRightBorder) { 43 | context.moveTo(offset.x, offset.y + 10); 44 | context.lineTo(offset.x - arrowWidth, offset.y + 10 - 5); 45 | context.lineTo(offset.x - arrowWidth, offset.y + 10 + 5); 46 | context.moveTo(offset.x, offset.y + 10); 47 | context.fillRect(offset.x - toolTipWidth - arrowWidth, offset.y, toolTipWidth, toolTipHeight); 48 | } else { 49 | context.moveTo(offset.x, offset.y + 10); 50 | context.lineTo(offset.x + arrowWidth, offset.y + 10 - 5); 51 | context.lineTo(offset.x + arrowWidth, offset.y + 10 + 5); 52 | context.moveTo(offset.x, offset.y + 10); 53 | context.fillRect(offset.x + arrowWidth, offset.y, toolTipWidth, toolTipHeight); 54 | } 55 | 56 | context.closePath(); 57 | context.fill(); 58 | context.setGlobalAlpha(1); 59 | 60 | // draw legend 61 | textList.forEach((item, index) => { 62 | context.beginPath(); 63 | context.setFillStyle(item.color); 64 | let startX = offset.x + arrowWidth + 2 * config.toolTipPadding; 65 | let startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding; 66 | if (isOverRightBorder) { 67 | startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding; 68 | } 69 | context.fillRect(startX, startY, legendWidth, config.fontSize); 70 | context.closePath(); 71 | }); 72 | 73 | // draw text list 74 | context.beginPath(); 75 | context.setFontSize(config.fontSize); 76 | context.setFillStyle('#ffffff'); 77 | textList.forEach((item, index) => { 78 | let startX = offset.x + arrowWidth + 2 * config.toolTipPadding + legendWidth + legendMarginRight; 79 | if (isOverRightBorder) { 80 | startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding + + legendWidth + legendMarginRight; 81 | } 82 | let startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding; 83 | context.fillText(item.text, startX, startY + config.fontSize); 84 | }); 85 | context.stroke(); 86 | context.closePath(); 87 | } -------------------------------------------------------------------------------- /src/components/draw.js: -------------------------------------------------------------------------------- 1 | import { getRadarDataPoints, getRadarCoordinateSeries, getMaxTextListLength, splitPoints, getPieDataPoints, calYAxisData, getXAxisPoints, getDataPoints, fixColumeData, calLegendData } from './charts-data' 2 | import { convertCoordinateOrigin, measureText, calRotateTranslate, createCurveControlPoints } from './charts-util' 3 | import Util from '../util/util' 4 | import drawPointShape from './draw-data-shape' 5 | import { drawPointText, drawPieText, drawRingTitle, drawRadarLabel } from './draw-data-text' 6 | import { drawToolTip, drawToolTipSplitLine } from './draw-tooltip' 7 | import { assign } from '../util/polyfill/index'; 8 | 9 | function drawYAxisTitle (title, opts, config, context) { 10 | let startX = config.xAxisHeight + (opts.height - config.xAxisHeight - measureText(title)) / 2; 11 | context.save(); 12 | context.beginPath(); 13 | context.setFontSize(config.fontSize); 14 | context.setFillStyle(opts.yAxis.titleFontColor || '#333333'); 15 | context.translate(0, opts.height); 16 | context.rotate(-90 * Math.PI / 180); 17 | context.fillText(title, startX, config.padding + 0.5 * config.fontSize); 18 | context.stroke(); 19 | context.closePath(); 20 | context.restore(); 21 | } 22 | 23 | export function drawColumnDataPoints (series, opts, config, context, process = 1) { 24 | let { ranges } = calYAxisData(series, opts, config); 25 | let { xAxisPoints, eachSpacing } = getXAxisPoints(opts.categories, opts, config); 26 | let minRange = ranges.pop(); 27 | let maxRange = ranges.shift(); 28 | let endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 29 | 30 | context.save(); 31 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 32 | context.translate(opts._scrollDistance_, 0); 33 | } 34 | 35 | series.forEach(function(eachSeries, seriesIndex) { 36 | let data = eachSeries.data; 37 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 38 | points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); 39 | 40 | // 绘制柱状数据图 41 | context.beginPath(); 42 | context.setFillStyle(eachSeries.color); 43 | points.forEach(function(item, index) { 44 | if (item !== null) { 45 | let startX = item.x - item.width / 2 + 1; 46 | let height = opts.height - item.y - config.padding - config.xAxisHeight - config.legendHeight; 47 | context.moveTo(startX, item.y); 48 | context.rect(startX, item.y, item.width - 2, height); 49 | } 50 | }); 51 | context.closePath(); 52 | context.fill(); 53 | }); 54 | series.forEach(function(eachSeries, seriesIndex) { 55 | let data = eachSeries.data; 56 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 57 | points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); 58 | if (opts.dataLabel !== false && process === 1) { 59 | drawPointText(points, eachSeries, config, context); 60 | } 61 | }); 62 | context.restore(); 63 | return { 64 | xAxisPoints, 65 | eachSpacing 66 | } 67 | } 68 | 69 | export function drawAreaDataPoints (series, opts, config, context, process = 1) { 70 | let { ranges } = calYAxisData(series, opts, config); 71 | let { xAxisPoints, eachSpacing } = getXAxisPoints(opts.categories, opts, config); 72 | let minRange = ranges.pop(); 73 | let maxRange = ranges.shift(); 74 | let endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 75 | let calPoints = []; 76 | 77 | context.save(); 78 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 79 | context.translate(opts._scrollDistance_, 0); 80 | } 81 | 82 | 83 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 84 | drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context); 85 | } 86 | 87 | series.forEach(function(eachSeries, seriesIndex) { 88 | let data = eachSeries.data; 89 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 90 | calPoints.push(points); 91 | 92 | let splitPointList = splitPoints(points); 93 | 94 | splitPointList.forEach((points) => { 95 | // 绘制区域数据 96 | context.beginPath(); 97 | context.setStrokeStyle(eachSeries.color); 98 | context.setFillStyle(eachSeries.color); 99 | context.setGlobalAlpha(0.6); 100 | context.setLineWidth(2); 101 | if (points.length > 1) { 102 | let firstPoint = points[0]; 103 | let lastPoint = points[points.length - 1]; 104 | 105 | context.moveTo(firstPoint.x, firstPoint.y); 106 | if (opts.extra.lineStyle === 'curve') { 107 | points.forEach(function(item, index) { 108 | if (index > 0) { 109 | let ctrlPoint = createCurveControlPoints(points, index - 1); 110 | context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x,ctrlPoint.ctrB.y, item.x, item.y); 111 | } 112 | }); 113 | } else { 114 | points.forEach(function(item, index) { 115 | if (index > 0) { 116 | context.lineTo(item.x, item.y); 117 | } 118 | }); 119 | } 120 | 121 | context.lineTo(lastPoint.x, endY); 122 | context.lineTo(firstPoint.x, endY); 123 | context.lineTo(firstPoint.x, firstPoint.y); 124 | } else { 125 | let item = points[0]; 126 | context.moveTo(item.x - eachSpacing / 2, item.y); 127 | context.lineTo(item.x + eachSpacing / 2, item.y); 128 | context.lineTo(item.x + eachSpacing / 2, endY); 129 | context.lineTo(item.x - eachSpacing / 2, endY); 130 | context.moveTo(item.x - eachSpacing / 2, item.y); 131 | } 132 | context.closePath(); 133 | context.fill(); 134 | context.setGlobalAlpha(1); 135 | }); 136 | 137 | if (opts.dataPointShape !== false) { 138 | let shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 139 | drawPointShape(points, eachSeries.color, shape, context); 140 | } 141 | }); 142 | if (opts.dataLabel !== false && process === 1) { 143 | series.forEach(function(eachSeries, seriesIndex) { 144 | let data = eachSeries.data; 145 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 146 | drawPointText(points, eachSeries, config, context); 147 | }); 148 | } 149 | 150 | context.restore(); 151 | 152 | return { 153 | xAxisPoints, 154 | calPoints, 155 | eachSpacing 156 | }; 157 | } 158 | 159 | export function drawLineDataPoints (series, opts, config, context, process = 1) { 160 | let { ranges } = calYAxisData(series, opts, config); 161 | let { xAxisPoints, eachSpacing } = getXAxisPoints(opts.categories, opts, config); 162 | let minRange = ranges.pop(); 163 | let maxRange = ranges.shift(); 164 | let calPoints = []; 165 | 166 | context.save(); 167 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 168 | context.translate(opts._scrollDistance_, 0); 169 | } 170 | 171 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 172 | drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context); 173 | } 174 | 175 | 176 | series.forEach(function(eachSeries, seriesIndex) { 177 | let data = eachSeries.data; 178 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 179 | calPoints.push(points); 180 | let splitPointList = splitPoints(points); 181 | 182 | splitPointList.forEach((points, index) => { 183 | context.beginPath(); 184 | context.setStrokeStyle(eachSeries.color); 185 | context.setLineWidth(2); 186 | if (points.length === 1) { 187 | context.moveTo(points[0].x, points[0].y); 188 | context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); 189 | } else { 190 | context.moveTo(points[0].x, points[0].y); 191 | if (opts.extra.lineStyle === 'curve') { 192 | points.forEach(function(item, index) { 193 | if (index > 0) { 194 | let ctrlPoint = createCurveControlPoints(points, index - 1); 195 | context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x,ctrlPoint.ctrB.y, item.x, item.y); 196 | } 197 | }); 198 | } else { 199 | points.forEach(function(item, index) { 200 | if (index > 0) { 201 | context.lineTo(item.x, item.y); 202 | } 203 | }); 204 | } 205 | context.moveTo(points[0].x, points[0].y); 206 | } 207 | context.closePath(); 208 | context.stroke(); 209 | }); 210 | 211 | if (opts.dataPointShape !== false) { 212 | let shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 213 | drawPointShape(points, eachSeries.color, shape, context); 214 | } 215 | }); 216 | if (opts.dataLabel !== false && process === 1) { 217 | series.forEach(function(eachSeries, seriesIndex) { 218 | let data = eachSeries.data; 219 | let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); 220 | drawPointText(points, eachSeries, config, context); 221 | }); 222 | } 223 | 224 | context.restore(); 225 | 226 | return { 227 | xAxisPoints, 228 | calPoints, 229 | eachSpacing 230 | }; 231 | } 232 | 233 | export function drawToolTipBridge (opts, config, context, process) { 234 | context.save(); 235 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { 236 | context.translate(opts._scrollDistance_, 0); 237 | } 238 | if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { 239 | drawToolTip(opts.tooltip.textList, opts.tooltip.offset, opts, config, context); 240 | } 241 | context.restore(); 242 | } 243 | 244 | export function drawXAxis (categories, opts, config, context) { 245 | let { xAxisPoints, startX, endX, eachSpacing } = getXAxisPoints(categories, opts, config); 246 | let startY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 247 | let endY = startY + config.xAxisLineHeight; 248 | 249 | context.save(); 250 | if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) { 251 | context.translate(opts._scrollDistance_, 0); 252 | } 253 | 254 | context.beginPath(); 255 | context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc"); 256 | 257 | if (opts.xAxis.disableGrid !== true) { 258 | if (opts.xAxis.type === 'calibration') { 259 | xAxisPoints.forEach(function(item, index) { 260 | if (index > 0) { 261 | context.moveTo(item - eachSpacing / 2, startY); 262 | context.lineTo(item - eachSpacing / 2, startY + 4); 263 | } 264 | }); 265 | } else { 266 | xAxisPoints.forEach(function(item, index) { 267 | context.moveTo(item, startY); 268 | context.lineTo(item, endY); 269 | }); 270 | } 271 | } 272 | context.closePath(); 273 | context.stroke(); 274 | 275 | // 对X轴列表做抽稀处理 276 | let validWidth = opts.width - 2 * config.padding - config.yAxisWidth - config.yAxisTitleWidth; 277 | let maxXAxisListLength = Math.min(categories.length, Math.ceil(validWidth / config.fontSize / 1.5)); 278 | let ratio = Math.ceil(categories.length / maxXAxisListLength); 279 | 280 | categories = categories.map((item, index) => { 281 | return index % ratio !== 0 ? '' : item; 282 | }); 283 | 284 | if (config._xAxisTextAngle_ === 0) { 285 | context.beginPath(); 286 | context.setFontSize(config.fontSize); 287 | context.setFillStyle(opts.xAxis.fontColor || '#666666'); 288 | categories.forEach(function(item, index) { 289 | let offset = eachSpacing / 2 - measureText(item) / 2; 290 | context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5); 291 | }); 292 | context.closePath(); 293 | context.stroke(); 294 | } else { 295 | categories.forEach(function(item, index) { 296 | context.save(); 297 | context.beginPath(); 298 | context.setFontSize(config.fontSize); 299 | context.setFillStyle(opts.xAxis.fontColor || '#666666'); 300 | let textWidth = measureText(item); 301 | let offset = eachSpacing / 2 - textWidth; 302 | let { transX, transY } = calRotateTranslate(xAxisPoints[index] + eachSpacing / 2, startY + config.fontSize / 2 + 5, opts.height); 303 | context.rotate(-1 * config._xAxisTextAngle_); 304 | context.translate(transX, transY); 305 | context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5); 306 | context.closePath(); 307 | context.stroke(); 308 | context.restore(); 309 | }); 310 | } 311 | 312 | context.restore(); 313 | } 314 | 315 | export function drawYAxisGrid (opts, config, context) { 316 | let spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 317 | let eachSpacing = Math.floor(spacingValid / config.yAxisSplit); 318 | let yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 319 | let startX = config.padding + yAxisTotalWidth; 320 | let endX = opts.width - config.padding; 321 | 322 | let points = []; 323 | for (let i = 0; i < config.yAxisSplit; i++) { 324 | points.push(config.padding + eachSpacing * i); 325 | } 326 | points.push(config.padding + eachSpacing * config.yAxisSplit + 2); 327 | 328 | context.beginPath(); 329 | context.setStrokeStyle(opts.yAxis.gridColor || "#cccccc") 330 | context.setLineWidth(1); 331 | points.forEach(function(item, index) { 332 | context.moveTo(startX, item); 333 | context.lineTo(endX, item); 334 | }); 335 | context.closePath(); 336 | context.stroke(); 337 | } 338 | 339 | export function drawYAxis (series, opts, config, context) { 340 | if (opts.yAxis.disabled === true) { 341 | return; 342 | } 343 | let { rangesFormat } = calYAxisData(series, opts, config); 344 | let yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth; 345 | 346 | let spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight; 347 | let eachSpacing = Math.floor(spacingValid / config.yAxisSplit); 348 | let startX = config.padding + yAxisTotalWidth; 349 | let endX = opts.width - config.padding; 350 | let startY = config.padding; 351 | let endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight; 352 | 353 | // set YAxis background 354 | context.setFillStyle(opts.background || '#ffffff'); 355 | if (opts._scrollDistance_ < 0) { 356 | context.fillRect(0, 0, startX, endY + config.xAxisHeight + 5); 357 | } 358 | context.fillRect(endX, 0, opts.width, endY + config.xAxisHeight + 5); 359 | 360 | let points = []; 361 | for (let i = 0; i <= config.yAxisSplit; i++) { 362 | points.push(config.padding + eachSpacing * i); 363 | } 364 | 365 | context.stroke(); 366 | context.beginPath(); 367 | context.setFontSize(config.fontSize); 368 | context.setFillStyle(opts.yAxis.fontColor || '#666666') 369 | rangesFormat.forEach(function(item, index) { 370 | let pos = points[index] ? points[index] : endY; 371 | context.fillText(item, config.padding + config.yAxisTitleWidth, pos + config.fontSize / 2); 372 | }); 373 | context.closePath(); 374 | context.stroke(); 375 | 376 | if (opts.yAxis.title) { 377 | drawYAxisTitle(opts.yAxis.title, opts, config, context); 378 | } 379 | } 380 | 381 | export function drawLegend (series, opts, config, context) { 382 | if (!opts.legend) { 383 | return; 384 | } 385 | // each legend shape width 15px 386 | // the spacing between shape and text in each legend is the `padding` 387 | // each legend spacing is the `padding` 388 | // legend margin top `config.padding` 389 | let { legendList, legendHeight } = calLegendData(series, opts, config); 390 | let padding = 5; 391 | let marginTop = 8; 392 | let shapeWidth = 15; 393 | legendList.forEach((itemList, listIndex) => { 394 | let width = 0; 395 | itemList.forEach(function (item) { 396 | item.name = item.name || 'undefined'; 397 | width += 3 * padding + measureText(item.name) + shapeWidth; 398 | }); 399 | let startX = (opts.width - width) / 2 + padding; 400 | let startY = opts.height - config.padding - config.legendHeight + listIndex * (config.fontSize + marginTop) + padding + marginTop; 401 | 402 | context.setFontSize(config.fontSize); 403 | itemList.forEach(function (item) { 404 | switch (opts.type) { 405 | case 'line': 406 | context.beginPath(); 407 | context.setLineWidth(1); 408 | context.setStrokeStyle(item.color); 409 | context.moveTo(startX - 2, startY + 5); 410 | context.lineTo(startX + 17, startY + 5); 411 | context.stroke(); 412 | context.closePath(); 413 | context.beginPath(); 414 | context.setLineWidth(1); 415 | context.setStrokeStyle('#ffffff'); 416 | context.setFillStyle(item.color); 417 | context.moveTo(startX + 7.5, startY + 5); 418 | context.arc(startX + 7.5, startY + 5, 4, 0, 2 * Math.PI); 419 | context.fill(); 420 | context.stroke(); 421 | context.closePath(); 422 | break; 423 | case 'pie': 424 | case 'ring': 425 | context.beginPath(); 426 | context.setFillStyle(item.color); 427 | context.moveTo(startX + 7.5, startY + 5); 428 | context.arc(startX + 7.5, startY + 5, 7, 0, 2 * Math.PI); 429 | context.closePath(); 430 | context.fill(); 431 | break; 432 | default: 433 | context.beginPath(); 434 | context.setFillStyle(item.color); 435 | context.moveTo(startX, startY); 436 | context.rect(startX, startY, 15, 10); 437 | context.closePath(); 438 | context.fill(); 439 | } 440 | startX += padding + shapeWidth; 441 | context.beginPath(); 442 | context.setFillStyle(opts.extra.legendTextColor || '#333333'); 443 | context.fillText(item.name, startX, startY + 9); 444 | context.closePath(); 445 | context.stroke(); 446 | startX += measureText(item.name) + 2 * padding; 447 | }); 448 | }); 449 | } 450 | export function drawPieDataPoints (series, opts, config, context, process = 1) { 451 | let pieOption = opts.extra.pie || {}; 452 | series = getPieDataPoints(series, process); 453 | let centerPosition = { 454 | x: opts.width / 2, 455 | y: (opts.height - config.legendHeight) / 2 456 | } 457 | let radius = Math.min( 458 | centerPosition.x - config.pieChartLinePadding - config.pieChartTextPadding - config._pieTextMaxLength_, 459 | centerPosition.y - config.pieChartLinePadding - config.pieChartTextPadding 460 | ); 461 | if (opts.dataLabel) { 462 | radius -= 10; 463 | } else { 464 | radius -= 2 * config.padding; 465 | } 466 | series = series.map((eachSeries) => { 467 | eachSeries._start_ += (pieOption.offsetAngle || 0) * Math.PI / 180; 468 | return eachSeries; 469 | }); 470 | series.forEach(function(eachSeries) { 471 | context.beginPath(); 472 | context.setLineWidth(2); 473 | context.setStrokeStyle('#ffffff'); 474 | context.setFillStyle(eachSeries.color); 475 | context.moveTo(centerPosition.x, centerPosition.y); 476 | context.arc(centerPosition.x, centerPosition.y, radius, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI); 477 | context.closePath(); 478 | context.fill(); 479 | if (opts.disablePieStroke !== true) { 480 | context.stroke(); 481 | } 482 | }); 483 | 484 | if (opts.type === 'ring') { 485 | let innerPieWidth = radius * 0.6; 486 | if (typeof opts.extra.ringWidth === 'number' && opts.extra.ringWidth > 0) { 487 | innerPieWidth = Math.max(0, radius - opts.extra.ringWidth); 488 | } 489 | context.beginPath(); 490 | context.setFillStyle(opts.background || '#ffffff'); 491 | context.moveTo(centerPosition.x, centerPosition.y); 492 | context.arc(centerPosition.x, centerPosition.y, innerPieWidth, 0, 2 * Math.PI); 493 | context.closePath(); 494 | context.fill(); 495 | } 496 | 497 | if (opts.dataLabel !== false && process === 1) { 498 | // fix https://github.com/xiaolin3303/wx-charts/issues/132 499 | let valid = false; 500 | for (let i = 0, len = series.length; i < len; i++) { 501 | if (series[i].data > 0) { 502 | valid = true; 503 | break; 504 | } 505 | } 506 | 507 | if (valid) { 508 | drawPieText(series, opts, config, context, radius, centerPosition); 509 | } 510 | } 511 | 512 | if (process === 1 && opts.type === 'ring') { 513 | drawRingTitle(opts, config, context); 514 | } 515 | 516 | return { 517 | center: centerPosition, 518 | radius, 519 | series 520 | } 521 | } 522 | 523 | export function drawRadarDataPoints (series, opts, config, context, process = 1) { 524 | let radarOption = opts.extra.radar || {}; 525 | let coordinateAngle = getRadarCoordinateSeries(opts.categories.length); 526 | let centerPosition = { 527 | x: opts.width / 2, 528 | y: (opts.height - config.legendHeight) / 2 529 | } 530 | 531 | let radius = Math.min( 532 | centerPosition.x - (getMaxTextListLength(opts.categories) + config.radarLabelTextMargin), 533 | centerPosition.y - config.radarLabelTextMargin 534 | ); 535 | 536 | radius -= config.padding; 537 | 538 | // draw grid 539 | context.beginPath(); 540 | context.setLineWidth(1); 541 | context.setStrokeStyle(radarOption.gridColor || "#cccccc"); 542 | coordinateAngle.forEach(angle => { 543 | let pos = convertCoordinateOrigin(radius * Math.cos(angle), radius * Math.sin(angle), centerPosition); 544 | context.moveTo(centerPosition.x, centerPosition.y); 545 | context.lineTo(pos.x, pos.y); 546 | }); 547 | context.stroke(); 548 | context.closePath(); 549 | 550 | // draw split line grid 551 | for (let i = 1; i <= config.radarGridCount; i++) { 552 | let startPos = {}; 553 | context.beginPath(); 554 | context.setLineWidth(1); 555 | context.setStrokeStyle(radarOption.gridColor || "#cccccc"); 556 | coordinateAngle.forEach((angle, index) => { 557 | let pos = convertCoordinateOrigin(radius / config.radarGridCount * i * Math.cos(angle), radius / config.radarGridCount * i * Math.sin(angle), centerPosition); 558 | if (index === 0) { 559 | startPos = pos; 560 | context.moveTo(pos.x, pos.y); 561 | } else { 562 | context.lineTo(pos.x, pos.y); 563 | } 564 | }); 565 | context.lineTo(startPos.x, startPos.y); 566 | context.stroke(); 567 | context.closePath(); 568 | } 569 | 570 | let radarDataPoints = getRadarDataPoints(coordinateAngle, centerPosition, radius, series, opts, process); 571 | radarDataPoints.forEach((eachSeries, seriesIndex) => { 572 | // 绘制区域数据 573 | context.beginPath(); 574 | context.setFillStyle(eachSeries.color); 575 | context.setGlobalAlpha(0.6); 576 | eachSeries.data.forEach((item, index) => { 577 | if (index === 0) { 578 | context.moveTo(item.position.x, item.position.y); 579 | } else { 580 | context.lineTo(item.position.x, item.position.y); 581 | } 582 | }); 583 | context.closePath(); 584 | context.fill(); 585 | context.setGlobalAlpha(1); 586 | 587 | if (opts.dataPointShape !== false) { 588 | let shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]; 589 | let points = eachSeries.data.map(item => { 590 | return item.position; 591 | }); 592 | drawPointShape(points, eachSeries.color, shape, context); 593 | } 594 | }); 595 | // draw label text 596 | drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context); 597 | 598 | return { 599 | center: centerPosition, 600 | radius, 601 | angleList: coordinateAngle 602 | } 603 | } 604 | 605 | export function drawCanvas (opts, context) { 606 | context.draw(); 607 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | yAxisWidth: 15, 3 | yAxisSplit: 5, 4 | xAxisHeight: 15, 5 | xAxisLineHeight: 15, 6 | legendHeight: 15, 7 | yAxisTitleWidth: 15, 8 | padding: 12, 9 | columePadding: 3, 10 | fontSize: 10, 11 | dataPointShape: ['diamond', 'circle', 'triangle', 'rect'], 12 | colors: ['#7cb5ec', '#f7a35c', '#434348', '#90ed7d', '#f15c80', '#8085e9'], 13 | pieChartLinePadding: 25, 14 | pieChartTextPadding: 15, 15 | xAxisTextPadding: 3, 16 | titleColor: '#333333', 17 | titleFontSize: 20, 18 | subtitleColor: '#999999', 19 | subtitleFontSize: 15, 20 | toolTipPadding: 3, 21 | toolTipBackground: '#000000', 22 | toolTipOpacity: 0.7, 23 | toolTipLineHeight: 14, 24 | radarGridCount: 3, 25 | radarLabelTextMargin: 15 26 | } 27 | 28 | export default config; -------------------------------------------------------------------------------- /src/util/event.js: -------------------------------------------------------------------------------- 1 | // simple event implement 2 | 3 | export default function Event () { 4 | this.events = {}; 5 | } 6 | 7 | Event.prototype.addEventListener = function (type, listener) { 8 | this.events[type] = this.events[type] || []; 9 | this.events[type].push(listener); 10 | } 11 | 12 | Event.prototype.trigger = function (...args) { 13 | let type = args[0]; 14 | let params = args.slice(1); 15 | if (!!this.events[type]) { 16 | this.events[type].forEach((listener) => { 17 | try { 18 | listener.apply(null, params); 19 | } catch (e) { 20 | console.error(e); 21 | } 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /src/util/polyfill/index.js: -------------------------------------------------------------------------------- 1 | // Object.assign polyfill 2 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 3 | export function assign(target, varArgs) { 4 | if (target == null) { // TypeError if undefined or null 5 | throw new TypeError('Cannot convert undefined or null to object'); 6 | } 7 | 8 | var to = Object(target); 9 | 10 | for (var index = 1; index < arguments.length; index++) { 11 | var nextSource = arguments[index]; 12 | 13 | if (nextSource != null) { // Skip over if undefined or null 14 | for (var nextKey in nextSource) { 15 | // Avoid bugs when hasOwnProperty is shadowed 16 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 17 | to[nextKey] = nextSource[nextKey]; 18 | } 19 | } 20 | } 21 | } 22 | return to; 23 | } -------------------------------------------------------------------------------- /src/util/timing.js: -------------------------------------------------------------------------------- 1 | export default { 2 | easeIn: function(pos){ 3 | return Math.pow(pos, 3); 4 | }, 5 | 6 | easeOut: function(pos){ 7 | return (Math.pow((pos - 1), 3) + 1); 8 | }, 9 | 10 | easeInOut: function(pos){ 11 | if ( (pos /= 0.5) < 1 ) { 12 | return 0.5 * Math.pow(pos, 3); 13 | } else { 14 | return 0.5 * (Math.pow((pos - 2), 3) + 2); 15 | } 16 | }, 17 | 18 | linear: function(pos) { 19 | return pos; 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | let util = { 2 | toFixed: function (num, limit) { 3 | limit = limit || 2; 4 | if (this.isFloat(num)) { 5 | num = num.toFixed(limit); 6 | } 7 | return num; 8 | }, 9 | isFloat: function (num) { 10 | return num % 1 !== 0; 11 | }, 12 | approximatelyEqual: function (num1, num2) { 13 | return Math.abs(num1 - num2) < 1e-10; 14 | }, 15 | isSameSign: function (num1, num2) { 16 | return (Math.abs(num1) === num1 && Math.abs(num2) === num2) 17 | || (Math.abs(num1) !== num1 && Math.abs(num2) !== num2) 18 | }, 19 | isSameXCoordinateArea: function (p1, p2) { 20 | return this.isSameSign(p1.x, p2.x); 21 | }, 22 | isCollision: function (obj1, obj2) { 23 | obj1.end = {}; 24 | obj1.end.x = obj1.start.x + obj1.width; 25 | obj1.end.y = obj1.start.y - obj1.height; 26 | obj2.end = {}; 27 | obj2.end.x = obj2.start.x + obj2.width; 28 | obj2.end.y = obj2.start.y - obj2.height; 29 | let flag = obj2.start.x > obj1.end.x 30 | || obj2.end.x < obj1.start.x 31 | || obj2.end.y > obj1.start.y 32 | || obj2.start.y < obj1.end.y; 33 | 34 | return !flag; 35 | }, 36 | } 37 | 38 | export default util; --------------------------------------------------------------------------------