Skip to content

Log-Scale Labels/Ticks are broken #617

@DerAlbiG

Description

@DerAlbiG

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions