TAChart: finite precision of floating point operations makes pointers/marks invisible
Original Reporter info from Mantis: Marcin Wiazowski
-
Reporter name:
Original Reporter info from Mantis: Marcin Wiazowski
- Reporter name:
Description:
TAChart: finite precision of floating point operations makes pointers/marks invisible
In TChart.CalculateTransformationCoeffs procedure, basing on the current extent (i.e. FCurrentExtent), FScale and FOffset factors are calculated; then they are used back, to recalculate the extent (i.e. results are written back to FCurrentExtent). So, ideally, after the calculations, FCurrentExtent should still hold exactly same values (with the exception of case when Proportional = True - in this case either horizontal, or vertical range becomes even larger).
But, due to finite precision of floating point operations and the Double type, FScale may be calculated in such way, that new FCurrentExtent's bounds are more tight than in the original FCurrentExtent - for example:
original FCurrentExtent.a.Y = -10
original FCurrentExtent.b.Y = 21
new FCurrentExtent.a.Y = -10
new FCurrentExtent.b.Y = 20.999999999999996
Initially, with default axes' settings, FCurrentExtent is initialized in such way, that all series' data points are visible. Then the extent may become even larger, to make an additional space for margins, marks, etc. - but it may never become smaller. But, due to lack of floating point precision, in practice it can become smaller - so, as in the example above, data point having its Y = 21 will be excluded from the chart's viewport and will not be presented to the user (or, in case of bar series, data point's mark will not be presented).
To solve the problem, FScale must be adjusted, and then the extent must be calculated again. Since the Double variable can hold up to 15-16 significant decimal digits, it's enough to adjust the FScale variable by the magnitude of one or two least significant digits - this can be done by dividing it by 1.000000000000001. But multiplying is faster than dividing, so it's even better to multiply the variable by 1 / 1.000000000000001 = 0.999999999999999 instead (please note, that this is a greatest value less than 1, that can be represented by using 15 significant decimal digits).
At the attached animation it can be observed that, for some FClipRect's heights (for example 202), lower FCurrentExtent's bound is -9.999999999999998 instead of -10, so first bar's mark is invisible (because TChart.IsPointInViewPort function tells that -10 is outside the extent). For some other FClipRect's heights (for example 204), higher FCurrentExtent's bound is 20.999999999999996 instead of 21, so second bar's mark is invisible (because TChart.IsPointInViewPort function tells that 21 is outside the extent).
The attached patch adjusts the FScale, recalculates the FOffset, and finally recalculates the extent and validates it again.
Some notes: currently (i.e. without a patch), at the end of the TChart.CalculateTransformationCoeffs function, FCurrentExtent is overwritten with new values - this is done inside of the UpdateMinMax calls. But, since new values must NOT be written to FCurrentExtent before validation, I edited the UpdateMinMax procedure to write its results not directly to FCurrentExtent, but to a temporary UpdatedExtent variable. For this reason, I renamed "UpdateMinMax" to "CalcUpdatedMinMax" (I thought that it would be more adequate not to use the "update" term). Due to this change in the UpdateMinMax procedure, the TAxisCoeffHelper's FMin and FMax variables no longer need to be pointers, so I changed them just to hold normal values. I also needed to perform same boolean comparison, that is currently used in the TAxisCoeffHelper.CalcScale function, so I introduced a FInputRangeIsInvalid variable, that is initialized in TAxisCoeffHelper.Init and used both by the TAxisCoeffHelper.CalcScale function and my code.
Regards