diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index ca8a7d20c38..a0b4b990b72 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -214,9 +214,31 @@ module.exports = function() { helpers.distanceBetweenPoints = function(pt1, pt2) { return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); }; + + /** + * Provided for backward compatibility, not available anymore + * @function Chart.helpers.aliasPixel + * @deprecated since version 2.8.0 + * @todo remove at version 3 + */ helpers.aliasPixel = function(pixelWidth) { return (pixelWidth % 2 === 0) ? 0 : 0.5; }; + + /** + * Returns the aligned pixel value to avoid anti-aliasing blur + * @param {Chart} chart - The chart instance. + * @param {Number} pixel - A pixel value. + * @param {Number} width - The width of the element. + * @returns {Number} The aligned pixel value. + * @private + */ + helpers._alignPixel = function(chart, pixel, width) { + var devicePixelRatio = chart.currentDevicePixelRatio; + var halfWidth = width / 2; + return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; + }; + helpers.splineCurve = function(firstPoint, middlePoint, afterPoint, t) { // Props to Rob Spencer at scaled innovation for his post on splining between points // http://scaledinnovation.com/analytics/splines/aboutSplines.html diff --git a/src/core/core.scale.js b/src/core/core.scale.js index e3a7979440c..42fae75ee42 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -580,7 +580,7 @@ module.exports = Element.extend({ pixel += tickWidth / 2; } - var finalVal = me.left + Math.round(pixel); + var finalVal = me.left + pixel; finalVal += me.isFullWidth() ? me.margins.left : 0; return finalVal; } @@ -598,7 +598,7 @@ module.exports = Element.extend({ var innerWidth = me.width - (me.paddingLeft + me.paddingRight); var valueOffset = (innerWidth * decimal) + me.paddingLeft; - var finalVal = me.left + Math.round(valueOffset); + var finalVal = me.left + valueOffset; finalVal += me.isFullWidth() ? me.margins.left : 0; return finalVal; } @@ -683,14 +683,17 @@ module.exports = Element.extend({ return; } + var chart = me.chart; var context = me.ctx; var globalDefaults = defaults.global; var optionTicks = options.ticks.minor; var optionMajorTicks = options.ticks.major || optionTicks; var gridLines = options.gridLines; var scaleLabel = options.scaleLabel; + var position = options.position; var isRotated = me.labelRotation !== 0; + var isMirrored = optionTicks.mirror; var isHorizontal = me.isHorizontal(); var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); @@ -698,6 +701,8 @@ module.exports = Element.extend({ var tickFont = parseFontOptions(optionTicks); var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); var majorTickFont = parseFontOptions(optionMajorTicks); + var tickPadding = optionTicks.padding; + var labelOffset = optionTicks.labelOffset; var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; @@ -708,11 +713,27 @@ module.exports = Element.extend({ var itemsToDraw = []; - var axisWidth = helpers.valueAtIndexOrDefault(me.options.gridLines.lineWidth, 0); - var xTickStart = options.position === 'right' ? me.left : me.right - axisWidth - tl; - var xTickEnd = options.position === 'right' ? me.left + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; - var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; + var axisWidth = gridLines.drawBorder ? helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0, 0) : 0; + var alignPixel = helpers._alignPixel; + var borderValue, tickStart, tickEnd; + + if (position === 'top') { + borderValue = alignPixel(chart, me.bottom, axisWidth); + tickStart = me.bottom - tl; + tickEnd = borderValue - axisWidth / 2; + } else if (position === 'bottom') { + borderValue = alignPixel(chart, me.top, axisWidth); + tickStart = borderValue + axisWidth / 2; + tickEnd = me.top + tl; + } else if (position === 'left') { + borderValue = alignPixel(chart, me.right, axisWidth); + tickStart = me.right - tl; + tickEnd = borderValue - axisWidth / 2; + } else { + borderValue = alignPixel(chart, me.left, axisWidth); + tickStart = borderValue + axisWidth / 2; + tickEnd = me.left + tl; + } var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. @@ -738,66 +759,58 @@ module.exports = Element.extend({ } // Common properties - var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; - var textAlign = 'middle'; + var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY, textAlign; var textBaseline = 'middle'; - var tickPadding = optionTicks.padding; + var lineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); if (isHorizontal) { var labelYOffset = tl + tickPadding; - if (options.position === 'bottom') { - // bottom - textBaseline = !isRotated ? 'top' : 'middle'; - textAlign = !isRotated ? 'center' : 'right'; - labelY = me.top + labelYOffset; - } else { - // top - textBaseline = !isRotated ? 'bottom' : 'middle'; - textAlign = !isRotated ? 'center' : 'left'; - labelY = me.bottom - labelYOffset; - } - - var xLineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); - if (xLineValue < me.left - epsilon) { + if (lineValue < me.left - epsilon) { lineColor = 'rgba(0,0,0,0)'; } - xLineValue += helpers.aliasPixel(lineWidth); - - labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - tx1 = tx2 = x1 = x2 = xLineValue; - ty1 = yTickStart; - ty2 = yTickEnd; - y1 = chartArea.top; - y2 = chartArea.bottom + axisWidth; - } else { - var isLeft = options.position === 'left'; - var labelXOffset; + tx1 = tx2 = x1 = x2 = alignPixel(chart, lineValue, lineWidth); + ty1 = tickStart; + ty2 = tickEnd; + labelX = me.getPixelForTick(index) + labelOffset; // x values for optionTicks (need to consider offsetLabel option) - if (optionTicks.mirror) { - textAlign = isLeft ? 'left' : 'right'; - labelXOffset = tickPadding; + if (position === 'top') { + y1 = alignPixel(chart, chartArea.top, axisWidth) + axisWidth / 2; + y2 = chartArea.bottom; + textBaseline = !isRotated ? 'bottom' : 'middle'; + textAlign = !isRotated ? 'center' : 'left'; + labelY = me.bottom - labelYOffset; } else { - textAlign = isLeft ? 'right' : 'left'; - labelXOffset = tl + tickPadding; + y1 = chartArea.top; + y2 = alignPixel(chart, chartArea.bottom, axisWidth) - axisWidth / 2; + textBaseline = !isRotated ? 'top' : 'middle'; + textAlign = !isRotated ? 'center' : 'right'; + labelY = me.top + labelYOffset; } + } else { + var labelXOffset = (isMirrored ? 0 : tl) + tickPadding; - labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; - - var yLineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); - if (yLineValue < me.top - epsilon) { + if (lineValue < me.top - epsilon) { lineColor = 'rgba(0,0,0,0)'; } - yLineValue += helpers.aliasPixel(lineWidth); - labelY = me.getPixelForTick(index) + optionTicks.labelOffset; + tx1 = tickStart; + tx2 = tickEnd; + ty1 = ty2 = y1 = y2 = alignPixel(chart, lineValue, lineWidth); + labelY = me.getPixelForTick(index) + labelOffset; - tx1 = xTickStart; - tx2 = xTickEnd; - x1 = chartArea.left; - x2 = chartArea.right + axisWidth; - ty1 = ty2 = y1 = y2 = yLineValue; + if (position === 'left') { + x1 = alignPixel(chart, chartArea.left, axisWidth) + axisWidth / 2; + x2 = chartArea.right; + textAlign = isMirrored ? 'left' : 'right'; + labelX = me.right - labelXOffset; + } else { + x1 = chartArea.left; + x2 = alignPixel(chart, chartArea.right, axisWidth) - axisWidth / 2; + textAlign = isMirrored ? 'right' : 'left'; + labelX = me.left + labelXOffset; + } } itemsToDraw.push({ @@ -867,7 +880,7 @@ module.exports = Element.extend({ if (helpers.isArray(label)) { var lineCount = label.length; var lineHeight = tickFont.size * 1.5; - var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; + var y = isHorizontal ? 0 : -lineHeight * (lineCount - 1) / 2; for (var i = 0; i < lineCount; ++i) { // We just make sure the multiline element is a string here.. @@ -891,11 +904,11 @@ module.exports = Element.extend({ if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' + scaleLabelY = position === 'bottom' ? me.bottom - halfLineHeight - scaleLabelPadding.bottom : me.top + halfLineHeight + scaleLabelPadding.top; } else { - var isLeft = options.position === 'left'; + var isLeft = position === 'left'; scaleLabelX = isLeft ? me.left + halfLineHeight + scaleLabelPadding.top : me.right - halfLineHeight - scaleLabelPadding.top; @@ -914,26 +927,24 @@ module.exports = Element.extend({ context.restore(); } - if (gridLines.drawBorder) { + if (axisWidth) { // 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 + axisWidth; - var y1 = me.top; - var y2 = me.bottom + axisWidth; + var firstLineWidth = axisWidth; + var lastLineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, ticks.length - 1, 0); + var x1, x2, y1, y2; - var aliasPixel = helpers.aliasPixel(context.lineWidth); if (isHorizontal) { - y1 = y2 = options.position === 'top' ? me.bottom : me.top; - y1 += aliasPixel; - y2 += aliasPixel; + x1 = alignPixel(chart, me.left, firstLineWidth) - firstLineWidth / 2; + x2 = alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2; + y1 = y2 = borderValue; } else { - x1 = x2 = options.position === 'left' ? me.right : me.left; - x1 += aliasPixel; - x2 += aliasPixel; + y1 = alignPixel(chart, me.top, firstLineWidth) - firstLineWidth / 2; + y2 = alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2; + x1 = x2 = borderValue; } + context.lineWidth = axisWidth; + context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c3245d9cf1e..b135bbc923a 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -124,8 +124,8 @@ var positioners = { } return { - x: Math.round(x / count), - y: Math.round(y / count) + x: x / count, + y: y / count }; }, @@ -619,8 +619,8 @@ var exports = module.exports = Element.extend({ model.footer = me.getFooter(tooltipItems, data); // Initial positioning and colors - model.x = Math.round(tooltipPosition.x); - model.y = Math.round(tooltipPosition.y); + model.x = tooltipPosition.x; + model.y = tooltipPosition.y; model.caretPadding = opts.caretPadding; model.labelColors = labelColors; model.labelTextColors = labelTextColors; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index dd8b01783bf..33a91f3a2c6 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -90,7 +90,7 @@ module.exports = function() { widthOffset += (valueWidth / 2); } - return me.left + Math.round(widthOffset); + return me.left + widthOffset; } var valueHeight = me.height / offsetAmt; var heightOffset = (valueHeight * (index - me.minIndex)); @@ -99,7 +99,7 @@ module.exports = function() { heightOffset += (valueHeight / 2); } - return me.top + Math.round(heightOffset); + return me.top + heightOffset; }, getPixelForTick: function(index) { return this.getPixelForValue(this.ticks[index], index + this.minIndex, null); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 71e156e0f09..4a76a773c40 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -477,8 +477,8 @@ module.exports = function(Chart) { var me = this; var thisAngle = me.getIndexAngle(index) - (Math.PI / 2); return { - x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + me.xCenter, - y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + me.yCenter + x: Math.cos(thisAngle) * distanceFromCenter + me.xCenter, + y: Math.sin(thisAngle) * distanceFromCenter + me.yCenter }; }, getPointPositionForValue: function(index, value) { diff --git a/test/fixtures/controller.line/point-style.png b/test/fixtures/controller.line/point-style.png index d8b6ed6b475..8faa96e4ab2 100644 Binary files a/test/fixtures/controller.line/point-style.png and b/test/fixtures/controller.line/point-style.png differ diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png index c5437ed24af..c64bf330755 100644 Binary files a/test/fixtures/controller.radar/point-style.png and b/test/fixtures/controller.radar/point-style.png differ diff --git a/test/fixtures/core.scale/tick-drawing.png b/test/fixtures/core.scale/tick-drawing.png index fb80cd01232..b59f3a6f947 100644 Binary files a/test/fixtures/core.scale/tick-drawing.png and b/test/fixtures/core.scale/tick-drawing.png differ diff --git a/test/fixtures/core.tooltip/opacity.png b/test/fixtures/core.tooltip/opacity.png index 142dcc05f60..8571abc38eb 100644 Binary files a/test/fixtures/core.tooltip/opacity.png and b/test/fixtures/core.tooltip/opacity.png differ diff --git a/test/fixtures/scale.radialLinear/border-dash.png b/test/fixtures/scale.radialLinear/border-dash.png index eea0db5ff00..b7bacfad7c4 100644 Binary files a/test/fixtures/scale.radialLinear/border-dash.png and b/test/fixtures/scale.radialLinear/border-dash.png differ diff --git a/test/fixtures/scale.radialLinear/indexable-gridlines.png b/test/fixtures/scale.radialLinear/indexable-gridlines.png index c6b9d87e89a..02c0268d879 100644 Binary files a/test/fixtures/scale.radialLinear/indexable-gridlines.png and b/test/fixtures/scale.radialLinear/indexable-gridlines.png differ diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index 573c132ae07..20890bb7720 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -29,7 +29,7 @@ describe('Core.scale', function() { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: false, offset: true, - expected: [51.5, 154.5, 256.5, 358.5, 461.5] + expected: [51.5, 153.5, 256.5, 358.5, 460.5] }, { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: true, @@ -39,7 +39,7 @@ describe('Core.scale', function() { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: true, offset: true, - expected: [0, 103, 205.5, 307.5, 410] + expected: [-0.5, 102.5, 204.5, 307.5, 409.5] }, { labels: ['tick1'], offsetGridLines: false, @@ -146,7 +146,7 @@ describe('Core.scale', function() { chart.draw(); expect(yScale.ctx.getCalls().filter(function(x) { - return x.name === 'moveTo' && x.args[0] === 0; + return x.name === 'moveTo' && x.args[0] === 1; }).map(function(x) { return x.args[1]; })).toEqual(test.expected); diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index c342fe64618..5ed898a0386 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -145,7 +145,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(266); + expect(tooltip._view.x).toBeCloseToPixel(267); expect(tooltip._view.y).toBeCloseToPixel(155); }); @@ -343,7 +343,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(266); + expect(tooltip._view.x).toBeCloseToPixel(267); expect(tooltip._view.y).toBeCloseToPixel(312); }); @@ -576,7 +576,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(266); + expect(tooltip._view.x).toBeCloseToPixel(267); expect(tooltip._view.y).toBeCloseToPixel(155); }); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 6992f23cf1e..eeda99f4b0a 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -24,6 +24,13 @@ describe('Deprecations', function() { }); }); }); + + describe('Chart.helpers.aliasPixel', function() { + it('should be defined and a function', function() { + expect(Chart.helpers.aliasPixel).toBeDefined(); + expect(typeof Chart.helpers.aliasPixel).toBe('function'); + }); + }); }); describe('Version 2.7.3', function() { diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 1665235ae8d..c0280accd27 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -328,19 +328,19 @@ describe('Category scale tests', function() { }); var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 0, 0)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); expect(yScale.getValueForPixel(32)).toBe(0); - expect(yScale.getPixelForValue(0, 4, 0)).toBe(484); + expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(484); expect(yScale.getValueForPixel(484)).toBe(4); yScale.options.offset = true; chart.update(); - expect(yScale.getPixelForValue(0, 0, 0)).toBe(77); + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(77); expect(yScale.getValueForPixel(77)).toBe(0); - expect(yScale.getPixelForValue(0, 4, 0)).toBe(439); + expect(yScale.getPixelForValue(0, 4, 0)).toBeCloseToPixel(439); expect(yScale.getValueForPixel(439)).toBe(4); }); @@ -378,13 +378,13 @@ describe('Category scale tests', function() { var yScale = chart.scales.yScale0; - expect(yScale.getPixelForValue(0, 1, 0)).toBe(32); - expect(yScale.getPixelForValue(0, 3, 0)).toBe(484); + expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(32); + expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(484); yScale.options.offset = true; chart.update(); - expect(yScale.getPixelForValue(0, 1, 0)).toBe(107); - expect(yScale.getPixelForValue(0, 3, 0)).toBe(409); + expect(yScale.getPixelForValue(0, 1, 0)).toBeCloseToPixel(107); + expect(yScale.getPixelForValue(0, 3, 0)).toBeCloseToPixel(409); }); }); diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index da19f9e000d..5459eac311f 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -398,10 +398,10 @@ describe('Test the radial linear scale', function() { expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(232); - expect(chart.scale.getPointPositionForValue(1, 5)).toEqual({ - x: 270, - y: 275, - }); + + var position = chart.scale.getPointPositionForValue(1, 5); + expect(position.x).toBeCloseToPixel(270); + expect(position.y).toBeCloseToPixel(275); chart.scale.options.ticks.reverse = true; chart.update();