implot icon indicating copy to clipboard operation
implot copied to clipboard

Log-Scale Labels/Ticks are broken

Open DerAlbiG opened this issue 10 months ago • 1 comments

Hi, i was using a log-scale in my project and when zooming in, the automatic labeling / automatic ticks are really unusable. For example, having a log-axis zoomed into the range from 99k to 101k produces the single 100k label. Zooming into the range from 101k to 102k generate no labels what so ever. I consider this broken behavior.

I have no idea how to fix this in the code base, but ImPlot allows you to set your manual labels, therefore i want to publish an algorithm i wrote, with the help of NiceNum (that already exist in the code-base) that generates a sane list of labels for any set of valid log-axis-limits:

double niceNum(double range, bool round)
{
    const double exponent = std::floor(std::log10(range));
    const double fraction = range / std::pow(10.0, exponent);
    double niceFraction;

    if (round)
    {
        if (fraction < 1.5)
            niceFraction = 1;
        else if (fraction < 3)
            niceFraction = 2;
        else if (fraction < 7)
            niceFraction = 5;
        else
            niceFraction = 10;
    }
    else
    {
        if (fraction <= 1)
            niceFraction = 1;
        else if (fraction <= 2)
            niceFraction = 2;
        else if (fraction <= 5)
            niceFraction = 5;
        else
            niceFraction = 10;
    }

    return niceFraction * std::pow(10.0, exponent);
}

std::vector<double> compute_log_labels(double min_val, double max_val, int n)
{
    std::vector<double> labels;
    labels.reserve(n*2);
    const double logStepSize = (std::log10(max_val) - std::log10(min_val)) / double(n);
    const double firstNiceDiff = niceNum(std::pow(10.0, std::log10(min_val) + logStepSize) - min_val, true);
    const double lastNiceDiff = niceNum(max_val - std::pow(10.0, std::log10(max_val) - logStepSize), true);

    const double niceStart = std::max(std::floor(min_val / firstNiceDiff), 1.0) * firstNiceDiff;
    const double niceEnd = std::ceil(max_val / lastNiceDiff) * lastNiceDiff;

    for (double val = niceStart; val <= niceEnd; )
    {
        labels.push_back(val);
        const double niceDiff = niceNum(std::pow(10.0, std::log10(val) + logStepSize) - val, true);
        const auto newVal = val+niceDiff;
        const auto valFloor = std::floor(newVal / niceDiff) * niceDiff;
        val = valFloor > val ? valFloor : std::ceil(newVal / niceDiff) * niceDiff;
    }

    return labels;
}

This will generate a list of labels for the range [min_val, max_val] with approximately n entries. It can be more, it can be less. I have used this successfully:

const auto numLabels = static_cast<int>(ImPlot::GetCurrentPlot()->FrameRect.GetHeight() / (ImGui::GetFrameHeight() * 2.1));
auto ticks = compute_log_labels(axisMin, axisMax, numLabels);
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), static_cast<int>(ticks.size()), nullptr, false);

I hope this helps someone or it could even somehow merged into the project so this algorithm is the default behavior. The existing ImPlot::NiceNum function does some shenanigans by casting the floor() result to an integer, all while that integer is never used as an integer but immediately casted back to double. I therefore included a corrected version that does not do useless overhead.

DerAlbiG avatar Apr 15 '25 09:04 DerAlbiG

I consider this broken behavior.

The implementation isn't broken just because it doesn't behave how you want.

RobertAlbus avatar Jun 21 '25 04:06 RobertAlbus