├── 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
--------------------------------------------------------------------------------