Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the rounding issue of floating point numbers in category scale #5880

Merged
merged 1 commit into from Dec 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/core/core.helpers.js
Expand Up @@ -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
Expand Down
147 changes: 79 additions & 68 deletions src/core/core.scale.js
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -683,21 +683,26 @@ 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();
var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor);
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;

Expand All @@ -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.

Expand All @@ -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({
Expand Down Expand Up @@ -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..
Expand All @@ -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;
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/core/core.tooltip.js
Expand Up @@ -124,8 +124,8 @@ var positioners = {
}

return {
x: Math.round(x / count),
y: Math.round(y / count)
x: x / count,
y: y / count
};
},

Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/scales/scale.category.js
Expand Up @@ -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));
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/scales/scale.radialLinear.js
Expand Up @@ -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) {
Expand Down
Binary file modified test/fixtures/controller.line/point-style.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/controller.radar/point-style.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/core.scale/tick-drawing.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/core.tooltip/opacity.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.radialLinear/border-dash.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.radialLinear/indexable-gridlines.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions test/specs/core.scale.tests.js
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions test/specs/core.tooltip.tests.js
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down
7 changes: 7 additions & 0 deletions test/specs/global.deprecations.tests.js
Expand Up @@ -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() {
Expand Down