diff --git a/src/chart.js b/src/chart.js index df17bb92c3f..c8c910f199d 100644 --- a/src/chart.js +++ b/src/chart.js @@ -52,9 +52,10 @@ require('./charts/Chart.Scatter')(Chart); var plugins = []; plugins.push( - require('./plugins/plugin.filler')(Chart), - require('./plugins/plugin.legend')(Chart), - require('./plugins/plugin.title')(Chart) + require('./plugins/plugin.filler')(Chart), + require('./plugins/plugin.gridlines')(Chart), + require('./plugins/plugin.legend')(Chart), + require('./plugins/plugin.title')(Chart) ); Chart.plugins.register(plugins); diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ffe13cbff82..801df6846b3 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -9,6 +9,10 @@ defaults._set('scale', { display: true, position: 'left', offset: false, + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, // grid line settings gridLines: { @@ -672,7 +676,7 @@ module.exports = function(Chart) { }, // Actually draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + // @param {rectangle} chartArea : the area of the chart to draw all ticks and labels on draw: function(chartArea) { var me = this; var options = me.options; @@ -732,7 +736,7 @@ module.exports = function(Chart) { } // Common properties - var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; + var tx1, ty1, tx2, ty2, labelX, labelY; var textAlign = 'middle'; var textBaseline = 'middle'; var tickPadding = optionTicks.padding; @@ -760,11 +764,9 @@ module.exports = function(Chart) { labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - tx1 = tx2 = x1 = x2 = xLineValue; + tx1 = tx2 = xLineValue; ty1 = yTickStart; ty2 = yTickEnd; - y1 = chartArea.top; - y2 = chartArea.bottom; } else { var isLeft = options.position === 'left'; var labelXOffset; @@ -789,9 +791,7 @@ module.exports = function(Chart) { tx1 = xTickStart; tx2 = xTickEnd; - x1 = chartArea.left; - x2 = chartArea.right; - ty1 = ty2 = y1 = y2 = yLineValue; + ty1 = ty2 = yLineValue; } itemsToDraw.push({ @@ -799,33 +799,45 @@ module.exports = function(Chart) { ty1: ty1, tx2: tx2, ty2: ty2, - x1: x1, - y1: y1, - x2: x2, - y2: y2, labelX: labelX, labelY: labelY, - glWidth: lineWidth, - glColor: lineColor, - glBorderDash: borderDash, - glBorderDashOffset: borderDashOffset, + tmWidth: lineWidth, + tmColor: lineColor, + tmBorderDash: borderDash, + tmBorderDashOffset: borderDashOffset, rotation: -1 * labelRotationRadians, - label: label, + label: '' + label, major: tick.major, textBaseline: textBaseline, textAlign: textAlign }); }); - // Draw all of the tick labels, tick marks, and grid lines at the correct places + // When offsetGridLines is enabled, there should be one more tick mark than there + // are ticks in scale.ticks array, therefore the missing gridLine has to be manually added + if (gridLines.offsetGridLines) { + var aliasPixel = helpers.aliasPixel(gridLines.lineWidth); + itemsToDraw.push({ + tx1: !isHorizontal ? xTickStart : chartArea.right + aliasPixel, + ty1: !isHorizontal ? chartArea.bottom + aliasPixel : yTickStart, + tx2: !isHorizontal ? xTickEnd : chartArea.right + aliasPixel, + ty2: !isHorizontal ? chartArea.bottom + aliasPixel : yTickEnd, + tmWidth: helpers.valueAtIndexOrDefault(gridLines.lineWidth, ticks.length), + tmColor: helpers.valueAtIndexOrDefault(gridLines.color, ticks.length), + tmBorderDash: gridLines.borderDash, + tmBorderDashOffset: gridLines.borderDashOffset + }); + } + + // Draw all of the tick labels and tick marks at the correct places helpers.each(itemsToDraw, function(itemToDraw) { if (gridLines.display) { context.save(); - context.lineWidth = itemToDraw.glWidth; - context.strokeStyle = itemToDraw.glColor; + context.lineWidth = itemToDraw.tmWidth; + context.strokeStyle = itemToDraw.tmColor; if (context.setLineDash) { - context.setLineDash(itemToDraw.glBorderDash); - context.lineDashOffset = itemToDraw.glBorderDashOffset; + context.setLineDash(itemToDraw.tmBorderDash); + context.lineDashOffset = itemToDraw.tmBorderDashOffset; } context.beginPath(); @@ -835,16 +847,11 @@ module.exports = function(Chart) { context.lineTo(itemToDraw.tx2, itemToDraw.ty2); } - if (gridLines.drawOnChartArea) { - context.moveTo(itemToDraw.x1, itemToDraw.y1); - context.lineTo(itemToDraw.x2, itemToDraw.y2); - } - context.stroke(); context.restore(); } - if (optionTicks.display) { + if (optionTicks.display && itemToDraw.label) { // Make sure we draw text in the correct color and font context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); @@ -901,30 +908,39 @@ module.exports = function(Chart) { context.restore(); } - if (gridLines.drawBorder) { - // Draw the line at the edge of the axis - context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); - context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); - var x1 = me.left; - var x2 = me.right; - var y1 = me.top; - var y2 = me.bottom; + // gridLines.drawBorder is deprecated + if (gridLines.drawBorder && options.borderColor && options.borderWidth) { + // Draw the scale border + context.lineWidth = options.borderWidth; + context.strokeStyle = options.borderColor; + if (context.setLineDash) { + context.setLineDash(helpers.getValueOrDefault(options.borderDash, globalDefaults.borderDash)); + context.lineDashOffset = helpers.getValueOrDefault(options.borderDashOffset, globalDefaults.borderDashOffset); + } + + var x1 = Math.round(me.left); + var x2 = Math.round(me.right); + var y1 = Math.round(me.top); + var y2 = Math.round(me.bottom); - var aliasPixel = helpers.aliasPixel(context.lineWidth); + var borderAliasPixel = helpers.aliasPixel(context.lineWidth); if (isHorizontal) { y1 = y2 = options.position === 'top' ? me.bottom : me.top; - y1 += aliasPixel; - y2 += aliasPixel; + y1 += borderAliasPixel; + y2 += borderAliasPixel; } else { x1 = x2 = options.position === 'left' ? me.right : me.left; - x1 += aliasPixel; - x2 += aliasPixel; + x1 += borderAliasPixel; + x2 += borderAliasPixel; } context.beginPath(); + context.moveTo(x1, y1); context.lineTo(x2, y2); + context.stroke(); + context.restore(); } } }); diff --git a/src/plugins/plugin.gridlines.js b/src/plugins/plugin.gridlines.js new file mode 100644 index 00000000000..d4635481ea8 --- /dev/null +++ b/src/plugins/plugin.gridlines.js @@ -0,0 +1,182 @@ +'use strict'; + +var defaults = require('../core/core.defaults'); +var helpers = require('../helpers/index'); +var MODEL_KEY = '$gridlines'; + +// This is copied from core.scale.js, which is also required here and im not sure where it should be placed for both of them to access it +function getLineValue(scale, index, offsetGridLines) { + var lineValue = scale.getPixelForTick(index); + + if (offsetGridLines) { + if (index === 0) { + lineValue -= (scale.getPixelForTick(1) - lineValue) / 2; + } else { + lineValue -= (lineValue - scale.getPixelForTick(index - 1)) / 2; + } + } + return lineValue; +} + +module.exports = function() { + function getGridLine(chart, scale, position, tickIndex) { + var chartArea = chart.chartArea; + var scaleOptions = scale.options; + var gridLines = scaleOptions.gridLines; + + var lineWidth, lineColor, borderDash, borderDashOffset; + + if (tickIndex !== undefined && tickIndex === scale.zeroLineIndex) { + lineWidth = gridLines.zeroLineWidth; + lineColor = gridLines.zeroLineColor; + borderDash = gridLines.zeroLineBorderDash; + borderDashOffset = gridLines.zeroLineBorderDashOffset; + } else { + lineWidth = helpers.getValueAtIndexOrDefault(gridLines.lineWidth, tickIndex); + lineColor = helpers.getValueAtIndexOrDefault(gridLines.color, tickIndex); + borderDash = helpers.getValueOrDefault(gridLines.borderDash, defaults.global.borderDash); + borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, defaults.global.borderDashOffset); + } + + var x1, x2, y1, y2; + + if (scale.isHorizontal()) { + x1 = x2 = position; + y1 = chartArea.top; + y2 = chartArea.bottom; + } else { + x1 = chartArea.left; + x2 = chartArea.right; + y1 = y2 = position; + } + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2, + width: lineWidth, + color: lineColor, + borderDash: borderDash, + borderDashOffset: borderDashOffset + }; + } + + function collectVisibleGridLines(chart) { + var lines = []; + + // Temporary object that stores already collected gridLine positions to prevent gridLines from overlapping + var gridLinesHash = { + horizontal: {}, + vertical: {} + }; + + // Marks scale border positions to prevent overlapping of gridLines and scale borders + helpers.each(chart.scales, function(scale) { + var scaleOptions = scale.options; + var glHashByOrientation = gridLinesHash[!scale.isHorizontal() ? 'horizontal' : 'vertical']; + var borderPosition; + + // gridLines.drawBorder is deprecated + if (scaleOptions.gridLines.drawBorder && scaleOptions.borderColor !== null && scaleOptions.borderWidth !== 0 && scaleOptions.borderWidth !== null) { + if (scale.isHorizontal()) { + borderPosition = scale.position === 'top' ? scale.bottom : scale.top; + } else { + borderPosition = scale.position === 'left' ? scale.right : scale.left; + } + + glHashByOrientation[Math.round(borderPosition)] = true; + } + }); + + // Collects gridLines + helpers.each(chart.scales, function(scale) { + var scaleOptions = scale.options; + var glHashByOrientation = gridLinesHash[scale.isHorizontal() ? 'horizontal' : 'vertical']; + var position; + + if (scaleOptions.display && scaleOptions.gridLines.display && scaleOptions.gridLines.drawOnChartArea) { + for (var tickIndex = 0, ticksCount = scale.ticks.length; tickIndex < ticksCount; tickIndex++) { + if (helpers.isNullOrUndef(scale.ticks[tickIndex])) { + continue; + } + + position = getLineValue(scale, tickIndex, scaleOptions.gridLines.offsetGridLines && ticksCount > 1); + + if (glHashByOrientation[position] === undefined) { + glHashByOrientation[position] = true; + lines.push(getGridLine(chart, scale, position, tickIndex)); + } + } + + // When offsetGridLines is enabled, there should be one more gridLine than there + // are ticks in scale.ticks array, therefore the missing gridLine has to be manually added + if (scaleOptions.gridLines.offsetGridLines) { + position = Math.round(!scale.isHorizontal() ? scale.bottom : scale.right); + + if (glHashByOrientation[position] === undefined) { + glHashByOrientation[position] = true; + lines.push(getGridLine(chart, scale, position, undefined)); + } + } + } + }); + + return lines; + } + + function drawGridLines(ctx, lines) { + var aliasPixel; + var x1, x2, y1, y2; + + for (var i = 0, ilen = lines.length; i < ilen; i++) { + var line = lines[i]; + + ctx.lineWidth = line.width; + ctx.strokeStyle = line.color; + if (ctx.setLineDash) { + ctx.setLineDash(line.borderDash); + ctx.lineDashOffset = line.borderDashOffset; + } + + aliasPixel = helpers.aliasPixel(ctx.lineWidth); + x1 = line.x1; + x2 = line.x2; + y1 = line.y1; + y2 = line.y2; + + if (y1 === y2) { + y1 += aliasPixel; + y2 += aliasPixel; + } else { + x1 += aliasPixel; + x2 += aliasPixel; + } + + ctx.beginPath(); + + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + + ctx.stroke(); + ctx.restore(); + } + } + + return { + id: 'gridlines', + + afterUpdate: function(chart) { + chart[MODEL_KEY] = { + lines: collectVisibleGridLines(chart) + }; + }, + + beforeDraw: function(chart) { + var model = chart[MODEL_KEY]; + if (model) { + drawGridLines(chart.ctx, model.lines); + } + } + }; +}; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index dc865fb01b2..fcad459ae4b 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -109,7 +109,10 @@ describe('Core helper tests', function() { axisProp: 456 }, { display: true, - + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, @@ -147,7 +150,10 @@ describe('Core helper tests', function() { type: 'linear' }, { display: true, - + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 1665235ae8d..039e9b9cd7a 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -11,7 +11,10 @@ describe('Category scale tests', function() { var defaultConfig = Chart.scaleService.getScaleDefaults('category'); expect(defaultConfig).toEqual({ display: true, - + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 60dd07698c0..fa24aafe691 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -9,7 +9,10 @@ describe('Linear Scale', function() { var defaultConfig = Chart.scaleService.getScaleDefaults('linear'); expect(defaultConfig).toEqual({ display: true, - + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 9640b1d3d62..0d239e61344 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -9,6 +9,10 @@ describe('Logarithmic Scale tests', function() { var defaultConfig = Chart.scaleService.getScaleDefaults('logarithmic'); expect(defaultConfig).toEqual({ display: true, + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 707ce0b7c52..13ba930a2bd 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -16,6 +16,10 @@ describe('Test the radial linear scale', function() { }, animate: true, display: true, + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { circular: false, color: 'rgba(0, 0, 0, 0.1)', diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 19189040ee4..db8672a126d 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -55,6 +55,10 @@ describe('Time scale tests', function() { var defaultConfig = Chart.scaleService.getScaleDefaults('time'); expect(defaultConfig).toEqual({ display: true, + borderColor: 'rgba(0, 0, 0, 0.4)', + borderWidth: 0, + borderDash: [], + borderDashOffset: 0.0, gridLines: { color: 'rgba(0, 0, 0, 0.1)', drawBorder: true,