#14486 Legend: Never use zero as lower limit for a logarithmic color range

The lower limit of a logarithmic color range was computed from the value closest to zero, which is not always available. When this value was zero, log10() returned -inf and the lower limit ended up as zero. Use the closest power of ten below the lowest value instead, and use the value closest to zero only when the lowest value is zero or negative. Guard computeTenExponentCeil() and computeTenExponentFloor() against zero, and make sure the range always spans at least one decade.
This commit is contained in:
Magne Sjaastad
2026-08-10 12:38:21 +02:00
parent 2237f578c8
commit 6bdccdf074
3 changed files with 85 additions and 3 deletions
@@ -46,7 +46,8 @@ double RiaNumericalTools::roundToClosestPowerOfTenFloor( double value )
//--------------------------------------------------------------------------------------------------
double RiaNumericalTools::computeTenExponentCeil( double value )
{
if ( value < 0.0 ) return 0.0;
// log10() is not defined for zero and negative values
if ( value <= 0.0 ) return 0.0;
double logDecValueMax = log10( value );
logDecValueMax = cvf::Math::ceil( logDecValueMax );
@@ -59,7 +60,8 @@ double RiaNumericalTools::computeTenExponentCeil( double value )
//--------------------------------------------------------------------------------------------------
double RiaNumericalTools::computeTenExponentFloor( double value )
{
if ( value < 0.0 ) return 0.0;
// log10() is not defined for zero and negative values
if ( value <= 0.0 ) return 0.0;
double logDecValueMin = log10( value );
logDecValueMin = cvf::Math::floor( logDecValueMin );
@@ -380,6 +380,21 @@ auto computeAdjustedMinMax = []( double minimum, double maximum, double precisio
return std::make_pair( adjustedMin, adjustedMax );
};
//--------------------------------------------------------------------------------------------------
/// A logarithmic scale is not defined for zero, so the lower limit must always be larger than zero.
/// Return the first candidate value above zero, and use one decade below the maximum value if no
/// candidate value is above zero. The candidate values are given in prioritized order.
//--------------------------------------------------------------------------------------------------
auto lowerLimitForLogarithmicScale = []( const std::vector<double>& candidateValues, double maximumValue ) -> double
{
for ( auto candidateValue : candidateValues )
{
if ( candidateValue > 0.0 ) return candidateValue;
}
return RiaNumericalTools::roundToClosestPowerOfTenFloor( maximumValue ) / 10.0;
};
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -467,6 +482,12 @@ void RimRegularLegendConfig::updateLegend()
adjustedMin = negClosestToZero;
}
}
// The values closest to zero are not always available, make sure the lower limit is never zero
if ( adjustedMin <= 0.0 && adjustedMax > 0.0 )
{
adjustedMin = lowerLimitForLogarithmicScale( { posClosestToZero, m_globalAutoMin, m_localAutoMin }, adjustedMax );
}
}
m_logDiscreteScalarMapper->setRange( adjustedMin, adjustedMax );
@@ -816,8 +837,15 @@ void RimRegularLegendConfig::updateTickCountAndUserDefinedRange()
{
if ( m_mappingMode() == MappingType::LOG10_CONTINUOUS || m_mappingMode() == MappingType::LOG10_DISCRETE )
{
// Use the closest power of ten below the lowest value. The value closest to zero is used when the lowest
// value is zero or negative, as a logarithmic scale is not defined for these values.
double minimumValue = lowerLimitForLogarithmicScale( { m_globalAutoMin, m_globalAutoPosClosestToZero }, m_globalAutoMax );
double exponentMax = RiaNumericalTools::computeTenExponentCeil( m_globalAutoMax );
double exponentMin = RiaNumericalTools::computeTenExponentFloor( m_globalAutoPosClosestToZero );
double exponentMin = RiaNumericalTools::computeTenExponentFloor( minimumValue );
// Make sure the range spans at least one decade
if ( exponentMin >= exponentMax ) exponentMin = exponentMax - 1.0;
m_userDefinedMaxValue = pow( 10, exponentMax );
m_userDefinedMinValue = pow( 10, exponentMin );
@@ -2,6 +2,8 @@
#include "RiaNumericalTools.h"
#include <cmath>
TEST( RiaNumericalTools, LogTenFunctions )
{
{
@@ -15,6 +17,17 @@ TEST( RiaNumericalTools, LogTenFunctions )
EXPECT_EQ( 0.0f, exponentFloor );
}
{
// Zero will return zero, as log10() is not defined for zero
double value = 0.0;
auto exponentCeil = RiaNumericalTools::computeTenExponentCeil( value );
EXPECT_EQ( 0.0f, exponentCeil );
auto exponentFloor = RiaNumericalTools::computeTenExponentFloor( value );
EXPECT_EQ( 0.0f, exponentFloor );
}
{
double value = 0.15;
@@ -46,6 +59,45 @@ TEST( RiaNumericalTools, LogTenFunctions )
}
}
TEST( RiaNumericalTools, ComputeTenExponentFloor )
{
struct TestValues
{
double value;
double expectedExponent;
};
TestValues testValues[] = {
// Zero and negative values are not defined for log10(), and return zero
{ -1150.0, 0.0 },
{ -0.5, 0.0 },
{ 0.0, 0.0 },
// Values below one give a negative exponent
{ 0.0005, -4.0 },
{ 0.05, -2.0 },
{ 0.1, -1.0 },
{ 0.5, -1.0 },
// Values above one give a positive exponent
{ 1.0, 0.0 },
{ 9.99, 0.0 },
{ 10.0, 1.0 },
{ 1150.0, 3.0 },
};
for ( const auto& testValue : testValues )
{
auto exponentFloor = RiaNumericalTools::computeTenExponentFloor( testValue.value );
EXPECT_EQ( testValue.expectedExponent, exponentFloor ) << "Failed for value " << testValue.value;
// The lower limit of a logarithmic range is the closest power of ten below the value
if ( testValue.value > 0.0 )
{
EXPECT_EQ( pow( 10.0, testValue.expectedExponent ), RiaNumericalTools::roundToClosestPowerOfTenFloor( testValue.value ) )
<< "Failed for value " << testValue.value;
}
}
}
TEST( RiaNumericalTools, RoundToSignificant )
{
struct TestValues