├── Demo.html ├── README.md ├── chart.meter.js ├── chart.pie.js ├── chart.radar.js └── icon.png /Demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Canvas图表 6 | 7 | 8 | 12 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # canvas-chart 2 | canvas实现的几个图表组件。 [Demo](http://yscoder.github.io/canvas-chart/Demo.html) 3 | -------------------------------------------------------------------------------- /chart.meter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @authors 王昱森 4 | * @date 2015-08-14 11:31:56 5 | * @version 1.0.3 6 | */ 7 | var Meter = (function () { 8 | 9 | var options = { 10 | 11 | styles: { 12 | sAngle: 0.93, 13 | eAngle: 2.07, 14 | area: { 15 | radius: 30, 16 | colors: { 17 | '0': '#1266BC', 18 | '0.15': '#67C6F2', 19 | '0.27': '#45F5E6', 20 | '0.75': '#FFDE00', 21 | '0.93': '#F5694B', 22 | '1': '#FF0202' 23 | }, 24 | lineWidth: 1, 25 | scaleLength: 9, 26 | scaleWidth: 0.2, 27 | lineColor: '#fff' 28 | }, 29 | range: { 30 | color: '#F4674B', 31 | width: 2, 32 | arrow: { 33 | height: 15, 34 | radius: 4 35 | } 36 | }, 37 | value: { 38 | margin: -50, 39 | color: '#F4674B', 40 | font: 'bold 52px Microsoft YaHei' 41 | }, 42 | title: { 43 | margin: -5, 44 | color: '#F4674B', 45 | font: 'bold 20px Microsoft YaHei' 46 | }, 47 | subTitle: { 48 | margin: 25, 49 | color: '#999', 50 | font: '14px Microsoft YaHei' 51 | }, 52 | label: { 53 | radius: 28, 54 | color: '#aaa', 55 | background: '#f5f5f5', 56 | font: '12px Microsoft YaHei' 57 | }, 58 | inner: { 59 | radius: 97, 60 | color: '#999', 61 | dashedWidth: 3 62 | } 63 | } 64 | }; 65 | 66 | var element, 67 | context, 68 | styles, 69 | sAngle, 70 | eAngle, 71 | areaStyle, 72 | rangeStyle, 73 | valueStyle, 74 | titleStyle, 75 | subTitleStyle, 76 | labelStyle, 77 | innerStyle; 78 | 79 | var extend = function(obj1, obj2){ 80 | for(var k in obj2) { 81 | if(obj1.hasOwnProperty(k) && typeof obj1[k] == 'object') { 82 | extend(obj1[k], obj2[k]); 83 | } else { 84 | obj1[k] = obj2[k]; 85 | } 86 | } 87 | } 88 | 89 | var calcLocation = function(r, end){ 90 | 91 | return { 92 | x: options.centerPoint.x + r * Math.cos(Math.PI * end), 93 | y: options.centerPoint.y + r * Math.sin(Math.PI * end) 94 | }; 95 | } 96 | 97 | var calcValueRange = function(value){ 98 | var data = options.data.area, 99 | index = data.length - 1; 100 | 101 | for (var i = index; i >= 0; i--) { 102 | if(value >= data[i].min && value < data[i].max){ 103 | index = i; 104 | } 105 | }; 106 | var r = (eAngle - sAngle)/data.length, 107 | s = r * index + sAngle, 108 | e = r * (index + 1) + sAngle, 109 | o = data[index]; 110 | 111 | return { 112 | range: (value - o.min)/(o.max - o.min) * (e - s) + s, 113 | index: index 114 | }; 115 | } 116 | 117 | var drawCircle = function(opts, flag) { 118 | var x = opts.x || options.centerPoint.x, 119 | y = opts.y || options.centerPoint.y, 120 | s = opts.start || 0, 121 | e = opts.end || 2; 122 | 123 | context.beginPath(); 124 | context.moveTo(x, y); 125 | 126 | switch(flag){ 127 | case 1: 128 | context.setLineDash && context.setLineDash([innerStyle.dashedWidth]); 129 | case 2: 130 | context.arc(x, y, opts.r, Math.PI*s, Math.PI*e); 131 | context.closePath(); 132 | context.strokeStyle = opts.style; 133 | context.stroke(); 134 | break; 135 | default: 136 | context.arc(x, y, opts.r, Math.PI*s, Math.PI*e); 137 | context.closePath(); 138 | context.fillStyle = opts.style; 139 | context.fill(); 140 | break; 141 | } 142 | } 143 | 144 | var drawArea = function(){ 145 | var grad = context.createLinearGradient(0, 0, options.radius*2, 0); 146 | for(var k in areaStyle.colors) { 147 | grad.addColorStop(k, areaStyle.colors[k]); 148 | } 149 | 150 | drawCircle({ 151 | r: options.radius, 152 | start: sAngle, 153 | end: eAngle, 154 | style: grad 155 | }); 156 | 157 | drawCircle({ 158 | r: options.radius - areaStyle.radius, 159 | style: '#fff' 160 | }); 161 | } 162 | 163 | var drawValueRange = function(valueRange){ 164 | 165 | var r = options.radius - areaStyle.radius; 166 | 167 | drawCircle({ 168 | r: r, 169 | start: sAngle, 170 | end: valueRange.range, 171 | style: labelStyle.background 172 | }); 173 | 174 | drawCircle({ 175 | r: r - labelStyle.radius, 176 | start: sAngle, 177 | end: valueRange.range, 178 | style: rangeStyle.color 179 | }); 180 | 181 | drawCircle({ 182 | r: r - labelStyle.radius - rangeStyle.width, 183 | style: '#fff' 184 | }); 185 | } 186 | 187 | var fillText = function(opts){ 188 | context.font = opts.font; 189 | context.fillStyle = opts.color; 190 | context.textAlign = opts.align || 'center'; 191 | context.textBaseline = opts.vertical || 'middle'; 192 | context.moveTo(opts.x, opts.y); 193 | context.fillText(opts.text, opts.x, opts.y); 194 | } 195 | 196 | var drawInnerContent = function(valueRange, value){ 197 | drawCircle({ 198 | r: innerStyle.radius, 199 | start: sAngle, 200 | end: eAngle, 201 | style: innerStyle.color 202 | }, 1); 203 | 204 | drawCircle({ 205 | r: innerStyle.radius - 1, 206 | style: '#fff' 207 | }); 208 | 209 | var data = options.data; 210 | 211 | fillText({ 212 | font: valueStyle.font, 213 | color: valueStyle.color, 214 | text: value, 215 | x: options.radius, 216 | y: options.radius + valueStyle.margin 217 | }); 218 | 219 | fillText({ 220 | font: titleStyle.font, 221 | color: titleStyle.color, 222 | text: data.title.replace('{t}', data.area[valueRange.index].text).replace('{v}', value), 223 | x: options.radius, 224 | y: options.radius + titleStyle.margin 225 | }); 226 | 227 | fillText({ 228 | font: subTitleStyle.font, 229 | color: subTitleStyle.color, 230 | text: data.subTitle, 231 | x: options.radius, 232 | y: options.radius + subTitleStyle.margin 233 | }); 234 | } 235 | 236 | var drawArrow = function(valueRange){ 237 | var r = options.radius - areaStyle.radius - labelStyle.radius, 238 | loc = calcLocation(r, valueRange.range), 239 | x = loc.x - 1, 240 | y = loc.y + 0.5; 241 | 242 | drawCircle({ 243 | x: x, 244 | y: y, 245 | r: rangeStyle.arrow.radius, 246 | style: rangeStyle.color 247 | }); 248 | 249 | var a = calcLocation(r - rangeStyle.arrow.height, valueRange.range), 250 | b = calcLocation(r, valueRange.range - 0.01), 251 | c = calcLocation(r, valueRange.range + 0.01); 252 | 253 | context.beginPath(); 254 | context.moveTo(a.x - 1, a.y + 0.5); 255 | context.lineTo(b.x - 1, b.y + 0.5); 256 | context.lineTo(c.x - 1, c.y + 0.5); 257 | context.closePath(); 258 | context.fillStyle = rangeStyle.color; 259 | context.fill(); 260 | 261 | drawCircle({ 262 | x: x, 263 | y: y, 264 | r: rangeStyle.arrow.radius - rangeStyle.width, 265 | style: '#fff' 266 | }); 267 | } 268 | 269 | var drawLine = function(line) { 270 | context.beginPath(); 271 | context.moveTo(line.start.x, line.start.y); 272 | context.lineTo(line.end.x, line.end.y); 273 | context.closePath(); 274 | context.strokeStyle = line.style; 275 | context.lineWidth = line.width || 1; 276 | context.stroke(); 277 | } 278 | 279 | var drawTickMarks = function(){ 280 | var scaleLength = areaStyle.scaleLength, 281 | data = options.data.area, 282 | len = scaleLength * data.length, 283 | range = (eAngle - sAngle)/len; 284 | 285 | for(var j = 1; j < len; j++){ 286 | drawLine({ 287 | start: calcLocation(options.radius, sAngle + range * j), 288 | end: calcLocation(options.radius - areaStyle.radius, sAngle + range * j), 289 | style: areaStyle.lineColor, 290 | width: j % scaleLength == 0 ? areaStyle.lineWidth: areaStyle.scaleWidth 291 | }); 292 | } 293 | 294 | var lblArr = []; 295 | for(var i = 0; i < data.length; i++){ 296 | var o = data[i]; 297 | // 如果不需兼容IE9以下则不用join 298 | if(lblArr.join('').indexOf(o.min) == -1) { 299 | lblArr.push(o.min); 300 | } 301 | lblArr.push(o.text); 302 | lblArr.push(o.max); 303 | 304 | } 305 | 306 | var lblLen = lblArr.length - 1, 307 | lblRange = (eAngle - sAngle)/lblLen, 308 | lblOpt = labelStyle, 309 | lblR = options.radius - areaStyle.radius - lblOpt.radius/2; 310 | 311 | for(var k = 0; k <= lblLen; k++){ 312 | var loc = calcLocation(lblR, sAngle + lblRange * k); 313 | lblOpt.x = loc.x; 314 | lblOpt.y = loc.y; 315 | lblOpt.text = lblArr[k]; 316 | fillText(lblOpt); 317 | } 318 | 319 | } 320 | 321 | var drawing = function(w, h) { 322 | var value = options.data.value, 323 | valueTemp = options.data.area[0].min; 324 | 325 | var timer = setInterval(function(){ 326 | context.clearRect(0, 0, w, h); 327 | context.fillStyle = "#fff"; 328 | context.fillRect(0, 0, w, h); 329 | 330 | valueTemp = valueTemp + 10 > value ? value: valueTemp + 10; 331 | var valueRange = calcValueRange(valueTemp); 332 | 333 | drawArea(); 334 | drawValueRange(valueRange); 335 | drawInnerContent(valueRange, valueTemp); 336 | drawArrow(valueRange); 337 | drawTickMarks(); 338 | 339 | if(valueTemp === value) { 340 | clearInterval(timer); 341 | } 342 | }, 10); 343 | } 344 | 345 | var exports = {}; 346 | 347 | exports.setOptions = function(opts){ 348 | extend(options, opts); 349 | 350 | styles = options.styles; 351 | sAngle = styles.sAngle; 352 | eAngle = styles.eAngle; 353 | areaStyle = styles.area; 354 | rangeStyle = styles.range; 355 | valueStyle = styles.value; 356 | titleStyle = styles.title; 357 | subTitleStyle = styles.subTitle; 358 | labelStyle = styles.label; 359 | innerStyle = styles.inner; 360 | 361 | element = typeof options.element == 'string' ? document.getElementById(options.element) : options.element; 362 | context = element.getContext('2d'); 363 | return exports; 364 | }; 365 | 366 | exports.init = function(){ 367 | drawing(element.offsetWidth, element.offsetHeight); 368 | return exports; 369 | } 370 | 371 | return exports; 372 | })(); 373 | -------------------------------------------------------------------------------- /chart.pie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @authors 王昱森 4 | * @date 2015-09-02 12:57:00 5 | * @version $Id$ 6 | */ 7 | 8 | Object.prototype.extend = function (obj) { 9 | for(var k in obj) { 10 | if(this.hasOwnProperty(k) && typeof this[k] == 'object') { 11 | extend(this[k], obj[k]); 12 | } else { 13 | this[k] = obj[k]; 14 | } 15 | } 16 | } 17 | 18 | var Pie = function (element, opt) { 19 | this.opts = { 20 | colors: ['#CAF44A', '#FFBD30', '#FFF335', '#FF6D4C'], 21 | valueStyle: { 22 | formate: '{v}人', 23 | color: '#fff', 24 | font: '16px Microsoft YaHei' 25 | } 26 | }; 27 | 28 | this.opts.extend(opt); 29 | this.element = typeof element == 'string' ? document.getElementById(element) : element; 30 | this.r = this.element.width / 2; 31 | this.context = this.element.getContext('2d'); 32 | } 33 | 34 | Pie.prototype = { 35 | calcLocation: function(r, end){ 36 | 37 | return { 38 | x: this.r + r * Math.cos(Math.PI * end), 39 | y: this.r + r * Math.sin(Math.PI * end) 40 | }; 41 | }, 42 | drawCircle: function(opts) { 43 | var s = opts.s || 0, 44 | e = opts.e || 2; 45 | 46 | this.context.beginPath(); 47 | this.context.moveTo(this.r, this.r); 48 | this.context.arc(this.r, this.r, this.r, Math.PI*s, Math.PI*e); 49 | this.context.closePath(); 50 | this.context.fillStyle = opts.style; 51 | this.context.fill(); 52 | }, 53 | fillText: function(opts){ 54 | var style = this.opts.valueStyle, 55 | text = style.formate.replace('{v}', opts.text); 56 | 57 | this.context.font = style.font; 58 | this.context.fillStyle = style.color; 59 | this.context.textAlign = style.align || 'center'; 60 | this.context.textBaseline = style.vertical || 'middle'; 61 | this.context.moveTo(opts.loc.x, opts.loc.y); 62 | this.context.fillText(text, opts.loc.x, opts.loc.y); 63 | }, 64 | drawRange: function (sum, values) { 65 | var ends = [], txtEnds = []; 66 | for (var i = 0, len = values.length; i < len; i++) { 67 | var s = i === 0 ? 1.5 : ends[i-1], 68 | e = values[i] / sum * 2 + s, 69 | txtEnd = s + (e - s) / 2; 70 | 71 | ends.push(e); 72 | txtEnds.push(txtEnd); 73 | 74 | this.drawCircle({ 75 | s: s, 76 | e: e, 77 | style: this.opts.colors[i] 78 | }); 79 | }; 80 | 81 | for (var i = 0, len = txtEnds.length; i < len; i++) { 82 | this.fillText({ 83 | loc: this.calcLocation(this.r * 0.6, txtEnds[i]), 84 | text: values[i] 85 | }); 86 | } 87 | 88 | }, 89 | draw: function () { 90 | var sum = 0, values = [], data = this.opts.data; 91 | for (var i = 0, len = data.length; i < len; i++) { 92 | var value = data[i].value; 93 | values.push(value); 94 | sum += value; 95 | }; 96 | 97 | this.drawRange(sum, values); 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /chart.radar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @authors 王昱森 4 | * @date 2015-08-14 11:31:44 5 | * @version 1.0.3 6 | */ 7 | 8 | var Radar = (function(){ 9 | 10 | var options = { 11 | 12 | styles: { 13 | offset: { 14 | top: 15, 15 | left: 0 16 | }, 17 | border: { 18 | width: 2, 19 | color: '#2EC8CA' 20 | }, 21 | splitLine: { 22 | color: '#ccc' 23 | }, 24 | title: { 25 | font: 'bold 52px Microsoft YaHei', 26 | color: '#F56948' 27 | }, 28 | valueRange: { 29 | border: { 30 | width: 4, 31 | color: '#FF0101' 32 | }, 33 | background: '#F56948', 34 | arrow: 2 35 | }, 36 | inner: { 37 | radius: 70, 38 | background: '#fff' 39 | }, 40 | label: { 41 | image: '', 42 | font: '16px Microsoft YaHei', 43 | color: '#666' 44 | } 45 | } 46 | }; 47 | 48 | var element, 49 | styles, 50 | borderStyle, 51 | splitLineStyle, 52 | titleStyle, 53 | valueRangeStyle, 54 | innerStyle, 55 | labelStyle; 56 | 57 | var extend = function(obj1, obj2){ 58 | for(var k in obj2) { 59 | if(obj1.hasOwnProperty(k) && typeof obj1[k] == 'object') { 60 | extend(obj1[k], obj2[k]); 61 | } else { 62 | obj1[k] = obj2[k]; 63 | } 64 | } 65 | } 66 | 67 | var calcLocation = function(cp, r, end){ 68 | 69 | return { 70 | x: cp[0] + r * Math.cos(Math.PI * end), 71 | y: cp[1] + r * Math.sin(Math.PI * end) 72 | }; 73 | } 74 | 75 | var drawLine = function(line) { 76 | var lines = line.lines; 77 | context.beginPath(); 78 | context.moveTo(lines[0].x, lines[0].y); 79 | 80 | for(var i = 1; i < lines.length; i++){ 81 | context.lineTo(lines[i].x, lines[i].y); 82 | } 83 | 84 | context.closePath(); 85 | 86 | if(line.style) { 87 | context.strokeStyle = line.style; 88 | context.lineWidth = line.width || 1; 89 | context.stroke(); 90 | } 91 | 92 | if(line.fill) { 93 | context.fillStyle = line.fill; 94 | context.fill(); 95 | } 96 | } 97 | 98 | var fillText = function(opts){ 99 | context.font = opts.font; 100 | context.fillStyle = opts.color; 101 | context.textAlign = opts.align || 'center'; 102 | context.textBaseline = opts.vertical || 'middle'; 103 | context.moveTo(opts.x, opts.y); 104 | context.fillText(opts.text, opts.x, opts.y); 105 | } 106 | 107 | var drawIcon = function(borderLoc, polar) { 108 | 109 | var img = new Image(); 110 | img.src = labelStyle.image; 111 | img.onload = function(){ 112 | for(var n = 0; n < borderLoc.length; n++){ 113 | var text = polar[n].text, 114 | icon = polar[n].icon, 115 | loc = borderLoc[n], 116 | x = loc.x + icon.l, 117 | y = loc.y + icon.t; 118 | 119 | context.drawImage(img, icon.sx, icon.sy, icon.w, icon.h, x, y, icon.w, icon.h); 120 | 121 | fillText({ 122 | font: labelStyle.font, 123 | color: labelStyle.color, 124 | text: text, 125 | x: x + icon.w/2, 126 | y: y + icon.h + 10 127 | }); 128 | } 129 | } 130 | } 131 | 132 | var drawInner = function(cp, valueRangeLoc, borderLoc, innerLoc, valueSum) { 133 | drawLine({ 134 | lines: borderLoc, 135 | style: borderStyle.color, 136 | width: borderStyle.width 137 | }); 138 | 139 | drawLine({ 140 | lines: valueRangeLoc, 141 | style: valueRangeStyle.border.color, 142 | width: valueRangeStyle.border.width, 143 | fill: valueRangeStyle.background 144 | }); 145 | 146 | for(var j = 0; j < borderLoc.length; j++){ 147 | drawLine({ 148 | lines: [{x: cp[0], y: cp[1]}, borderLoc[j]], 149 | style: splitLineStyle.color 150 | }); 151 | } 152 | 153 | drawLine({ 154 | lines: innerLoc, 155 | fill: innerStyle.background 156 | }); 157 | 158 | fillText({ 159 | font: titleStyle.font, 160 | color: titleStyle.color, 161 | text: options.title.replace('{v}', valueSum), 162 | x: cp[0], 163 | y: cp[1] 164 | }); 165 | 166 | for (var k = valueRangeLoc.length - 1; k >= 0; k--) { 167 | var x = valueRangeLoc[k].x, 168 | y = valueRangeLoc[k].y; 169 | 170 | context.beginPath(); 171 | context.moveTo(x, y); 172 | context.arc(x, y, valueRangeStyle.arrow, 0, Math.PI*2); 173 | context.closePath(); 174 | context.strokeStyle = valueRangeStyle.border.color; 175 | context.lineWidth = valueRangeStyle.border.width; 176 | context.stroke(); 177 | context.fillStyle = '#fff'; 178 | context.fill(); 179 | } 180 | } 181 | 182 | var calcRedrawPath = function(borderLoc){ 183 | var startLoc = borderLoc[0]; 184 | var minX = startLoc.x, 185 | minY = startLoc.y, 186 | maxX = startLoc.x, 187 | maxY = startLoc.y; 188 | 189 | for(var i = 1; i < borderLoc.length; i ++) { 190 | var loc = borderLoc[i]; 191 | minX = loc.x < minX ? loc.x : minX; 192 | minY = loc.y < minY ? loc.y : minY; 193 | maxX = loc.x > maxX ? loc.x : maxX; 194 | maxY = loc.y > maxY ? loc.y : maxY; 195 | } 196 | 197 | var borderW = borderStyle.width; 198 | return { 199 | x: minX - borderW, 200 | y: minY - borderW, 201 | w: maxX - minX + borderW * 2, 202 | h: maxY - minY + borderW * 2 203 | }; 204 | } 205 | 206 | var drawing = function(cp, w, h){ 207 | var polar = options.polar, 208 | polarCount = polar.length, 209 | radius = options.radius, 210 | data = options.data; 211 | angles = [], 212 | borderLoc = []; 213 | 214 | 215 | var dataTemp = []; 216 | for(var i = 0; i < polarCount; i++) { 217 | dataTemp.push(0); 218 | 219 | var end = 1.5 + i * (2/polarCount); 220 | angles.push(end); 221 | borderLoc.push(calcLocation(cp, radius, end)); 222 | } 223 | 224 | context.fillStyle = "#fff"; 225 | context.fillRect(0, 0, w, h); 226 | 227 | drawIcon(borderLoc, polar); 228 | 229 | var redrawPath = calcRedrawPath(borderLoc); 230 | 231 | var timer = setInterval(function(){ 232 | 233 | var eqCount = 0, 234 | valueSum = 0, 235 | valueRangeLoc = [], 236 | innerLoc = []; 237 | 238 | for(var i = 0; i < polarCount; i++){ 239 | dataTemp[i] = dataTemp[i] + 5 > data[i] ? data[i]: dataTemp[i] + 5; 240 | if(dataTemp[i] === data[i]) { 241 | ++eqCount; 242 | } 243 | 244 | var end = angles[i]; 245 | 246 | // inner 247 | var ir = innerStyle.radius; 248 | innerLoc.push(calcLocation(cp, innerStyle.radius, end)); 249 | 250 | // valueRange 251 | var vr = dataTemp[i]/polar[i].max * (radius - ir) + ir; 252 | valueRangeLoc.push(calcLocation(cp, vr, end)); 253 | 254 | valueSum += dataTemp[i]; 255 | } 256 | 257 | if(eqCount === polarCount) { 258 | clearInterval(timer); 259 | } 260 | 261 | context.clearRect(redrawPath.x, redrawPath.y, redrawPath.w, redrawPath.h); 262 | context.fillStyle = "#fff"; 263 | context.fillRect(redrawPath.x, redrawPath.y, redrawPath.w, redrawPath.h); 264 | 265 | drawInner(cp, valueRangeLoc, borderLoc, innerLoc, valueSum); 266 | 267 | }, 10); 268 | 269 | 270 | } 271 | 272 | var exports = {}; 273 | 274 | exports.setOptions = function(opts){ 275 | extend(options, opts); 276 | 277 | styles = options.styles; 278 | borderStyle = styles.border; 279 | splitLineStyle = styles.splitLine; 280 | titleStyle = styles.title, 281 | valueRangeStyle = styles.valueRange, 282 | innerStyle = styles.inner; 283 | labelStyle = styles.label; 284 | 285 | element = typeof options.element == 'string' ? document.getElementById(options.element) : options.element; 286 | context = element.getContext('2d'); 287 | return exports; 288 | }; 289 | 290 | exports.init = function(){ 291 | var w = element.offsetWidth, 292 | h = element.offsetHeight; 293 | 294 | var ofs = options.styles.offset; 295 | drawing([w/2 + ofs.left, h/2 + ofs.top], w, h); 296 | 297 | return exports; 298 | } 299 | 300 | return exports; 301 | 302 | })(); 303 | 304 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/canvas-chart/fbf8a8e491f417e561f1e73946abae4866e4451f/icon.png --------------------------------------------------------------------------------