TAChart: series' shadow is sometimes displaced
Original Reporter info from Mantis: Marcin Wiazowski
-
Reporter name:
Original Reporter info from Mantis: Marcin Wiazowski
- Reporter name:
Description:
Please take a look at the attached animation, and the Reproduce application. To make things clearly visible, colors are quite unusual: series is red, and series shadow is light green.
Shadow settings are OffsetX = OffsetY = 1. But - as can be seen especially on "ddd" label - shadow is sometimes displaced by +1 or -1 pixel.
Explanation:
Calculations are performed by using floating point numbers, but then they are finally rounded to integer pixel coordinates.
Series and shadow coordinates are shifted by 1 pixel in our case, so they should remain shifted by 1 pixel also after rounding - but they aren't.
The problem is in ImgRoundChecked():
function ImgRoundChecked(A: Double): Integer;
begin
Result := Round(EnsureRange(A, -MAX_COORD, MAX_COORD));
end;
As can be seen, Round() function is used to transform floating point numbers into integer values. Round() uses the current FPU's rounding mode. Most of the processors - including x86 - implement four rounding modes:
- to nearest, ties to even,
- toward -INF,
- toward +INF,
- toward 0.
These modes can be chosen by using SetRoundMode() function:
- SetRoundMode(rmNearest),
- SetRoundMode(rmDown),
- SetRoundMode(rmUp),
- SetRoundMode(rmTruncate).
The chosen rounding mode is used:
- when converting floating point values to integers (this is the case, in which we are interested in),
- also when performing floating point calculations - rounding is needed due to finite precision of floating point numbers (for Double values, rounding will be performed on 15th or 16th significant decimal digit).
The default rounding mode is 1), i.e. rmNearest. In this mode, floating point values, having exactly .5 at the end of their decimal representation, are rounded to the nearest EVEN integer value:
Round(-4.5) = -4
Round(-3.5) = -4
Round(-2.5) = -2
Round(-1.5) = -2
Round(-0.5) = 0
Round(0.5) = 0
Round(1.5) = 2
Round(2.5) = 2
Round(3.5) = 4
Round(4.5) = 4
Now let's imagine the following series/shadow positions before and after rounding:
series: 101.2 => 101
shadow: 102.2 => 102
DISTANCE: 1 pixel => 1 pixel
series: 102.2 => 102
shadow: 103.2 => 103
DISTANCE: 1 pixel => 1 pixel
series: 103.2 => 103
shadow: 104.2 => 104
DISTANCE: 1 pixel => 1 pixel
series: 104.2 => 104
shadow: 105.2 => 105
DISTANCE: 1 pixel => 1 pixel
but:
series: 101.5 => 102
shadow: 102.5 => 102
DISTANCE: 1 pixel => 0 pixels
series: 102.5 => 102
shadow: 103.5 => 104
DISTANCE: 1 pixel => 2 pixels
series: 103.5 => 104
shadow: 104.5 => 104
DISTANCE: 1 pixel => 0 pixels
series: 104.5 => 104
shadow: 105.5 => 106
DISTANCE: 1 pixel => 2 pixels
So, as we can see, rounding may change distance between two values, even when initial distance is given by an integer value.
The solution is: use rmDown or rmUp rounding mode (the choice is arbitrary):
function ImgRoundChecked(A: Double): Integer;
var
SaveMode: TFPURoundingMode;
begin
SaveMode := SetRoundMode(rmDown);
OR
SaveMode := SetRoundMode(rmUp);
try
Result := Round(EnsureRange(A, -MAX_COORD, MAX_COORD));
finally
SetRoundMode(SaveMode);
end;
end;
But calling SetRoundMode() can be avoided, by making some observation about rounding modes:
- to nearest, ties to even,
- toward -INF - this is equivalent to Floor(),
- toward +INF - this is equivalent to Ceil(),
- toward 0 - this is equivalent to Trunc().
Although, internally, Floor() and Ceil() don't use SetRoundMode() calls, they use some other, equivalent calculations. So, finally, we can use:
function ImgRoundChecked(A: Double): Integer;
begin
Result := Floor(EnsureRange(A, -MAX_COORD, MAX_COORD));
OR
Result := Ceil(EnsureRange(A, -MAX_COORD, MAX_COORD));
end;
Although it's not required to work properly, we can make results more close to the results returned in the rmNearest mode, by adding or subtracting 0.5:
function ImgRoundChecked(A: Double): Integer;
begin
Result := Floor(EnsureRange(A, -MAX_COORD, MAX_COORD) + 0.5);
OR
Result := Ceil(EnsureRange(A, -MAX_COORD, MAX_COORD) - 0.5);
end;
In this case - when comparing to the original implementation - returned values differ only for input values ending with .5 - they are always rounded in the +INF direction for the Floor() variant, or in the -INF direction for the Ceil() variant (it's not a mistake: Floor(100.5 + 0.5) = 101, which rounds 100.5 up, not down).
The attached patch solves the described problem.
ADDITIONAL NOTE: It may be possible, that similar problem occurs also with a RoundPoint() function. I tested RoundPoint() calls only in the TSeriesPointer.DrawSize() implementation, but - due to specificity of that code - values ending with .5 are never passed to RoundPoint() there, so I wasn't able to verify if the problem occurs. I couldn't find a way of calling RoundPoint() from other places.
Mantis conversion info:
- Mantis ID: 35640
- Build: 61296
- Version: 2.1 (SVN)
- Fixed in revision: 61304 (#c754e91a)