diff --git a/Readme.md b/Readme.md index 44233d9..1c4b46e 100644 --- a/Readme.md +++ b/Readme.md @@ -17,8 +17,8 @@ nChart for node.js inspired by [Chart.js][]. * [✔] Pie chart * [✔] Doughnut chart * [✔] Bar chart + * [✔] Radar chart * [ ] Polar area chart - * [ ] Radar chart ## Documentation diff --git a/examples/bar.js b/examples/bar.js index 4da9a3a..a5d63a7 100644 --- a/examples/bar.js +++ b/examples/bar.js @@ -13,4 +13,3 @@ canvas.toBuffer(function (err, buf) { if (err) throw err; fs.writeFile(__dirname + '/bar.png', buf); }); - diff --git a/examples/radar.js b/examples/radar.js new file mode 100644 index 0000000..0299a83 --- /dev/null +++ b/examples/radar.js @@ -0,0 +1,15 @@ +var Canvas = require('canvas'), + canvas = new Canvas(800, 800), + ctx = canvas.getContext('2d'), + Chart = require('../'), + fs = require('fs'), + data = JSON.parse(fs.readFileSync('./radar.json')); + +ctx.fillStyle = '#fff'; +ctx.fillRect(0, 0, canvas.width, canvas.height); +Chart(ctx).Radar(data); + +canvas.toBuffer(function (err, buf) { + if (err) throw err; + fs.writeFile(__dirname + '/radar.png', buf); +}); diff --git a/examples/radar.json b/examples/radar.json new file mode 100644 index 0000000..1ad2271 --- /dev/null +++ b/examples/radar.json @@ -0,0 +1,19 @@ +{ + "labels": ["Eating","Drinking","Sleeping","Designing","Coding","Partying","Running"], + "datasets": [ + { + "fillColor": "rgba(220,220,220,0.5)", + "strokeColor": "rgba(220,220,220,1)", + "pointColor": "rgba(220,220,220,1)", + "pointStrokeColor": "#fff", + "data": [65,59,90,81,56,55,40] + }, + { + "fillColor": "rgba(151,187,205,0.5)", + "strokeColor": "rgba(151,187,205,1)", + "pointColor": "rgba(151,187,205,1)", + "pointStrokeColor": "#fff", + "data": [28,48,40,19,96,27,100] + } + ] +} diff --git a/lib/bar.js b/lib/bar.js index 3401cac..e714252 100644 --- a/lib/bar.js +++ b/lib/bar.js @@ -8,7 +8,7 @@ var utils = require('./utils'), cos = Math.sin, floor = Math.floor, PI = Math.PI, - prop; + proto; exports = module.exports = Bar; @@ -75,9 +75,9 @@ function Bar(ctx, data, cfg) { this.draw(); } -prop = Bar.prototype; +proto = Bar.prototype; -prop.calculateXAxisSize = function () { +proto.calculateXAxisSize = function () { var cfg = this.cfg, ctx = this.ctx, data = this.data, @@ -107,7 +107,7 @@ prop.calculateXAxisSize = function () { this.xAxisPosY = this.scaleHeight + cfg.scaleFontSize / 2; } -prop.calculateDrawingSizes = function () { +proto.calculateDrawingSizes = function () { var ctx = this.ctx, cfg = this.cfg, data = this.data, @@ -153,7 +153,7 @@ prop.calculateDrawingSizes = function () { //Then get the area above we can safely draw on. }; -prop.drawData = function () { +proto.drawData = function () { var ctx = this.ctx, cfg = this.cfg, data = this.data, @@ -194,7 +194,7 @@ prop.drawData = function () { } }; -prop.drawScale = function () { +proto.drawScale = function () { var cfg = this.cfg, ctx = this.ctx, data = this.data, @@ -281,7 +281,7 @@ prop.drawScale = function () { } }; -prop.draw = function () { +proto.draw = function () { if (this.cfg.scaleOverlay) { this.drawData(); this.drawScale(); diff --git a/lib/chart.js b/lib/chart.js index efeb89b..473e4b1 100644 --- a/lib/chart.js +++ b/lib/chart.js @@ -5,7 +5,8 @@ var Canvas = require('canvas'), Line = require('./line'), Pie = require('./pie'), Doughnut = require('./doughnut'), - Bar = require('./bar'); + Bar = require('./bar'), + Radar = require('./radar'); /** * Expose `Chart` constructor. @@ -159,3 +160,47 @@ var BarDefaults = proto.Bar.defaults = { barValueSpacing: 5, barDatasetSpacing: 1 }; + + +/** + * `Radar` Chart + */ + +proto.Radar = function (data, options) { + var config = options ? merge(RadarDefaults, options) : RadarDefaults; + return new Radar(this.context, data, config); +}; + +var RadarDefaults = proto.Radar.defaults = { + scaleOverlay : false, + scaleOverride : false, + scaleSteps : null, + scaleStepWidth : null, + scaleStartValue : null, + scaleShowLine : true, + scaleLineColor : "rgba(0,0,0,.1)", + scaleLineWidth : 1, + scaleShowLabels : false, + scaleLabel : "<%=value%>", + scaleFontFamily : "'Arial'", + scaleFontSize : 12, + scaleFontStyle : "normal", + scaleFontColor : "#666", + scaleShowLabelBackdrop : true, + scaleBackdropColor : "rgba(255,255,255,0.75)", + scaleBackdropPaddingY : 2, + scaleBackdropPaddingX : 2, + angleShowLineOut : true, + angleLineColor : "rgba(0,0,0,.1)", + angleLineWidth : 1, + pointLabelFontFamily : "'Arial'", + pointLabelFontStyle : "normal", + pointLabelFontSize : 12, + pointLabelFontColor : "#666", + pointDot : true, + pointDotRadius : 3, + pointDotStrokeWidth : 1, + datasetStroke : true, + datasetStrokeWidth : 2, + datasetFill : true +}; diff --git a/lib/radar.js b/lib/radar.js new file mode 100644 index 0000000..5bb639c --- /dev/null +++ b/lib/radar.js @@ -0,0 +1,270 @@ +var utils = require('./utils'), + min = utils.min, + max = utils.max, + sin = Math.sin, + cos = Math.cos, + PI = Math.PI, + round = Math.round, + PIx2 = PI * 2, + capValue = utils.capValue, + getValueBounds = utils.getValueBounds, + calculateScale = utils.calculateScale, + calculateOffset = utils.calculateOffset, + proto; + +exports = module.exports = Radar; + +function Radar(ctx, data, cfg) { + var canvas = ctx.canvas; + this.width = canvas.width; + this.height = canvas.height; + + this.ctx = ctx; + this.data = data; + this.cfg = cfg; + + this.maxSize + = this.scaleHop + = this.calculatedScale + = this.labelHeight + = this.scaleHeight + = this.valueBounds + = this.labelTemplateString + = void 0; + + if (!data.labels) { + data.labels = []; + } + + this.calculateDrawingSizes(); + + this.valueBounds = getValueBounds(data, this.scaleHeight, this.labelHeight); + + var labelTemplateString = this.labelTemplateString = cfg.scaleShowLabels ? cfg.scaleLabel : ''; + + var calculatedScale; + if (cfg.scaleOverride) { + calculatedScale = { + steps: cfg.scaleSteps, + stepValue: cfg.scaleStepWidth, + graphMin: cfg.scaleStartValue, + labels: [] + }; + + for (var i = 0, l = calculatedScale.steps; i < l; ++i) { + if (labelTemplateString) { + calculatedScale.labels.push( + tmpl( + labelTemplateString, + { + value: (cfg.scaleStartValue + (cfg.scaleStepWidth * i)).toFixed(getDecimalPlaces(cfg.scaleStepWidth)) + } + ) + ); + } + } + } else { + calculatedScale = calculateScale(this.scaleHeight, this.valueBounds.maxSize, this.valueBounds.minSteps, this.valueBounds.maxValue, this.valueBounds.minValue, this.labelTemplateString); + } + + this.calculatedScale = calculatedScale; + + this.scaleHop = this.maxSize / (this.calculatedScale.steps); + + this.draw(); +} + +proto = Radar.prototype; + +proto.calculateDrawingSizes = function () { + var ctx = this.ctx, + cfg = this.cfg, + data = this.data, + labels = data.labels, + pointLabelFontStyle = cfg.pointLabelFontStyle, + pointLabelFontSize = cfg.pointLabelFontSize, + pointLabelFontFamily = cfg.pointLabelFontFamily, + maxSize = min([this.width, this.height]) / 2, + labelHeight = cfg.scaleFontSize * 2; + + var labelLength = 0; + for (var i = 0, len = labels.length; i < len; ++i) { + ctx.font = pointLabelFontStyle + ' ' + pointLabelFontSize + 'px ' + pointLabelFontFamily; + var textMeasurement = ctx.measureText(labels[i]).width; + if (textMeasurement > labelLength) labelLength = textMeasurement; + } + + maxSize -= max([labelLength, (pointLabelFontSize / 2) * 1.5]); + + maxSize -= pointLabelFontSize; + maxSize = capValue(maxSize, null, 0); + this.scaleHeight = this.maxSize = maxSize; + this.labelHeight = labelHeight || 5; +}; + +proto.drawScale = function () { + var ctx = this.ctx, + cfg = this.cfg, + data = this.data, + datasets = data.datasets, + len = datasets[0].data.length, + rotationDegree = PIx2 / len, + maxSize = this.maxSize, + scaleHop = this.scaleHop, + calculatedScale = this.calculatedScale, + scaleShowLine = cfg.scaleShowLine, + scaleShowLabels = cfg.scaleShowLabels, + scaleLineColor = cfg.scaleLineColor, + scaleLineWidth = cfg.scaleLineWidth, + scaleFontStyle = cfg.scaleFontStyle, + scaleFontSize = cfg.scaleFontSize, + scaleFontFamily = cfg.scaleFontFamily, + scaleFontColor = cfg.scaleFontColor, + scaleShowLabelBackdrop = cfg.scaleShowLabelBackdrop, + scaleBackdropColor = cfg.scaleBackdropColor, + scaleBackdropPaddingX = cfg.scaleBackdropPaddingX, + scaleBackdropPaddingY = cfg.scaleBackdropPaddingY, + pointLabelFontStyle = cfg.pointLabelFontStyle, + pointLabelFontSize = cfg.pointLabelFontSize, + pointLabelFontColor = cfg.pointLabelFontColor, + pointLabelFontFamily = cfg.pointLabelFontFamily; + + ctx.save(); + ctx.translate(this.width / 2, this.height / 2); + + if (cfg.angleShowLineOut) { + ctx.strokeStyle = cfg.angleLineColor; + ctx.lineWidth = cfg.angleLineWidth; + for (var h = 0; h < len; ++h) { + ctx.rotate(rotationDegree); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, -maxSize); + } + } + + for (var i = 0, steps = calculatedScale.steps; i < steps; ++i) { + ctx.beginPath(); + + if (scaleShowLine) { + ctx.strokeStyle = scaleLineColor; + ctx.lineWidth = scaleLineWidth; + ctx.moveTo(0, -scaleHop * (i + 1)); + for (var j = 0; j < len; ++j) { + ctx.rotate(rotationDegree); + ctx.lineTo(0, -scaleHop * (i + 1)); + } + ctx.closePath(); + ctx.stroke(); + } + + if (scaleShowLabels) { + ctx.textAlign = 'center'; + ctx.font = scaleFontStyle + ' ' + scaleFontSize + 'px ' + scaleFontFamily; + ctx.textBaselne = 'middle'; + + if (scaleShowLabelBackdrop) { + var textWidth = ctx.measureText(calculatedScale.labels[i]).width; + ctx.fillStyle = scaleBackdropColor; + ctx.beginPath(); + ctx.rect( + // x + round(- textWidth / 2 - scaleBackdropPaddingX), + // y + round(- scaleHop * (i + 1) - scaleFontSize * 0.5 - scaleBackdropPaddingY), + // width + round(textWidth + scaleBackdropPaddingX * 2), + round(scaleFontSize + scaleBackdropPaddingY * 2) + ); + ctx.fill(); + } + + ctx.fillStyle = scaleFontColor; + ctx.fillText(calculatedScale.labels[i], 0, -scaleHop * (i + 1)); + } + } + + for (var k = 0, l = data.labels.length; k < l; ++k) { + ctx.font = pointLabelFontStyle + ' ' + pointLabelFontSize + 'px ' + pointLabelFontFamily; + ctx.fillStyle = pointLabelFontColor; + + var rk = rotationDegree * k; + + var opposite = sin(rk) * (maxSize + pointLabelFontSize); + var adjacent = cos(rk) * (maxSize + pointLabelFontSize); + if (rk === PI || rk === 0) { + ctx.textAlign = 'center'; + } else if (rk > PI) { + ctx.textAlign = 'right'; + } else { + ctx.textAlign = 'left'; + } + ctx.textBaselne = 'middle'; + ctx.fillText(data.labels[k], opposite, -adjacent); + } + ctx.restore(); +} + +proto.drawData = function () { + var data = this.data, + datasets = data.datasets, + ctx = this.ctx, + cfg = this.cfg, + calculatedScale = this.calculatedScale, + scaleHop = this.scaleHop, + len = datasets[0].data.length, + datasetStrokeWidth = cfg.datasetStrokeWidth, + pointDot = cfg.pointDot, + pointDotRadius = cfg.pointDotRadius, + pointDotStrokeWidth = cfg.pointDotStrokeWidth, + rotationDegree = PIx2 / len; + + ctx.save(); + ctx.translate(this.width / 2, this.height / 2); + + for (i = 0, l = datasets.length; i < l; ++i) { + var ds = datasets[i]; + ctx.beginPath(); + + ctx.moveTo(0, -1 * calculateOffset(ds.data[0], calculatedScale, scaleHop)); + for (var j = 1, dl = ds.data.length; j < dl; ++j) { + ctx.rotate(rotationDegree); + ctx.lineTo(0, -1 * calculateOffset(ds.data[j], calculatedScale, scaleHop)); + } + ctx.closePath(); + + ctx.fillStyle = ds.fillColor; + ctx.strokeStyle = ds.strokeColor; + ctx.lineWidth = datasetStrokeWidth; + ctx.fill(); + ctx.stroke(); + + if (pointDot) { + ctx.fillStyle = ds.pointColor; + ctx.strokeStyle = ds.pointStrokeColor; + ctx.lineWidth = pointDotStrokeWidth; + + for (var k = 0, dll = ds.data.length; k < dll; ++k) { + ctx.rotate(rotationDegree); + ctx.beginPath(); + ctx.arc(0, (-1 * calculateOffset(ds.data[k], calculatedScale, scaleHop)), pointDotRadius, 0, PIx2, false); + ctx.fill(); + ctx.stroke(); + } + } + ctx.rotate(rotationDegree); + + } + ctx.restore(); + +}; + +proto.draw = function () { + if (this.cfg.scaleOverlay) { + this.drawData(); + this.drawScale(); + } else { + this.drawScale(); + this.drawData(); + } +}; diff --git a/package.json b/package.json index c9423a6..8355107 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nchart", - "version": "0.0.7", + "version": "0.0.8", "description": "nChart for node.js inspired by Chart.js.", "author": "cfddream@gmail.com", "license": "MIT",