You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

图表Y轴显示值与步长计算逻辑技术咨询

问题描述

我有一组数据对:[(0,15.0),(1,17.0),(2,20.0),(3,28.5),(4,29),(5,32.0),(6,33.5),(7,33.6)],需要创建散点图,其中第一个数为X值(X轴标签),第二个数为Y轴值。

要求实现通用的Y轴标签计算逻辑,满足以下条件:

  • 参考MS Excel散点图Y轴标签生成逻辑(示例数据应生成0,5,10,...,35),但需支持Y值范围极大的场景(含正负,覆盖Double类型极值),能生成如10,20,...,50、10,100,...,10000等多种易读的标签形式。
  • Y轴标签数量动态调整:数据范围大时不超过8个,范围小时至少5个。
  • 可调整小数位数,步长随小数位数变化(如设置0位小数时步长为1,标签为0,1,2...)。
  • 确保Y轴最小标签≤数据最小Y值、最大标签≥数据最大Y值。

目前尝试的Kotlin代码存在标签数量过多(可达15-20个)的问题,求不依赖特定库、可跨语言实现的通用逻辑,优先提供Chart.js、React、Kotlin、Swift方案。


现有尝试的Kotlin代码
private fun calculateStepSize(range: Double, preferredLabelCount: Int): Double {
    // 计算初始步长
    val rawStep = range / preferredLabelCount

    // 计算步长的数量级
    val magnitude = floor(log10(rawStep)).toInt()

    // 计算步长的最高有效位
    var mostSignificantDigit = (rawStep / 10.0.pow(magnitude.toDouble())).roundToInt()

    // 将最高有效位调整为1、2或5(保证标签易读)
    mostSignificantDigit = if (mostSignificantDigit > 5) {
        10
    } else if (mostSignificantDigit > 2) {
        5
    } else {
        1
    }

    // 生成最终步长
    return mostSignificantDigit * 10.0.pow(magnitude.toDouble())
}

// 初始设置显示7个Y轴标签
var yAxisLabelCount = 7
var yAxisRange = maxYValue - minYValue
var yAxisValueStep = calculateStepSize(yAxisRange, yAxisLabelCount - 1)

var yAxisRoundedMin = floor(minYValue / yAxisValueStep) * yAxisValueStep
var yAxisMax = yAxisRoundedMin + (yAxisLabelCount - 1) * yAxisValueStep

// 如果当前最大标签小于数据最大值,就增加标签数量直到满足条件
while (yAxisMax < maxYValue) {
    yAxisLabelCount++
    yAxisRange = maxYValue - minYValue
    yAxisValueStep = calculateStepSize(yAxisRange, yAxisLabelCount - 1)
    yAxisRoundedMin = floor(minYValue / yAxisValueStep) * yAxisValueStep
    yAxisMax = yAxisRoundedMin + (yAxisLabelCount - 1) * yAxisValueStep
}

通用逻辑解决方案

核心思路是先基于数据范围和目标标签数量计算合适的步长,再调整标签数量确保在[5,8]区间内,同时保证覆盖全部数据范围:

  1. 确定数据范围:计算minY(数据最小Y值)和maxY(数据最大Y值),得到range = maxY - minY。
  2. 约束标签数量范围:设置标签数量的上下限minLabelCount=5、maxLabelCount=8。
  3. 计算候选步长:针对标签数量从minLabelCount到maxLabelCount的每个值,计算对应的步长(基于1、2、5的有效位规则)。
  4. 筛选最优步长:选择能覆盖数据范围且标签数量在约束内的最小步长(保证标签数量不超标),或者调整步长后匹配合适的标签数量。
  5. 生成标签序列:从向下取整到步长倍数的minY开始,以步长递增,直到超过maxY,确保最后一个标签≥maxY。
  6. 适配小数位数:根据指定的小数位数,对步长和标签进行格式化,若小数位数为0,步长取整为整数。

分语言实现

1. Kotlin 优化实现

import kotlin.math.*

data class YAxisConfig(
    val labels: List<Double>,
    val step: Double,
    val minLabel: Double,
    val maxLabel: Double
)

fun calculateYAxisLabels(
    minY: Double,
    maxY: Double,
    decimalPlaces: Int = 2,
    minLabelCount: Int = 5,
    maxLabelCount: Int = 8
): YAxisConfig {
    val range = maxY - minY
    if (range == 0.0) {
        // 所有Y值相同的情况,生成对称标签
        val mid = minY
        val step = if (mid == 0.0) 1.0 else abs(mid) / (minLabelCount - 1)
        val adjustedStep = roundToDecimalPlaces(step, decimalPlaces)
        val labels = mutableListOf<Double>()
        repeat(minLabelCount) { i ->
            labels.add(mid - adjustedStep * (minLabelCount/2 - i))
        }
        return YAxisConfig(labels, adjustedStep, labels.first(), labels.last())
    }

    var bestStep = Double.MAX_VALUE
    var bestLabelCount = minLabelCount

    // 遍历所有可能的标签数量,找到最优步长
    for (labelCount in minLabelCount..maxLabelCount) {
        val rawStep = range / (labelCount - 1)
        val magnitude = floor(log10(rawStep)).toInt()
        val msd = (rawStep / 10.0.pow(magnitude)).roundToInt()

        val adjustedMsd = when {
            msd > 5 -> 10
            msd > 2 -> 5
            else -> 1
        }

        var step = adjustedMsd * 10.0.pow(magnitude)
        // 适配小数位数
        step = roundToDecimalPlaces(step, decimalPlaces)

        // 检查是否能覆盖数据范围
        val roundedMin = floor(minY / step) * step
        val roundedMax = roundedMin + (labelCount - 1) * step

        if (roundedMax >= maxY && roundedMin <= minY) {
            if (step < bestStep) {
                bestStep = step
                bestLabelCount = labelCount
            }
        }
    }

    // 如果没有找到合适的,扩大步长直到覆盖
    if (bestStep == Double.MAX_VALUE) {
        var labelCount = maxLabelCount
        while (true) {
            val rawStep = range / (labelCount - 1)
            val magnitude = floor(log10(rawStep)).toInt()
            val msd = (rawStep / 10.0.pow(magnitude)).roundToInt()
            val adjustedMsd = when {
                msd > 5 -> 10
                msd > 2 -> 5
                else -> 1
            }
            bestStep = adjustedMsd * 10.0.pow(magnitude)
            bestStep = roundToDecimalPlaces(bestStep, decimalPlaces)
            val roundedMin = floor(minY / bestStep) * bestStep
            val roundedMax = roundedMin + (labelCount - 1) * bestStep
            if (roundedMax >= maxY && roundedMin <= minY) {
                bestLabelCount = labelCount
                break
            }
            labelCount++
        }
    }

    // 生成标签序列
    val roundedMin = floor(minY / bestStep) * bestStep
    val labels = mutableListOf<Double>()
    var current = roundedMin
    while (current <= maxY + bestStep * 0.1) { // 加个小偏移避免精度问题
        labels.add(roundToDecimalPlaces(current, decimalPlaces))
        current += bestStep
    }

    return YAxisConfig(labels, bestStep, labels.first(), labels.last())
}

private fun roundToDecimalPlaces(value: Double, decimalPlaces: Int): Double {
    val factor = 10.0.pow(decimalPlaces)
    return round(value * factor) / factor
}

// 使用示例
fun main() {
    val data = listOf(0 to 15.0, 1 to 17.0, 2 to 20.0, 3 to 28.5, 4 to 29.0, 5 to 32.0, 6 to 33.5, 7 to 33.6)
    val minY = data.minOf { it.second }
    val maxY = data.maxOf { it.second }
    val config = calculateYAxisLabels(minY, maxY, decimalPlaces = 0)
    println("Y轴标签: ${config.labels}") // 输出: Y轴标签: [0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0]
}

2. Swift 实现

import Foundation

struct YAxisConfig {
    let labels: [Double]
    let step: Double
    let minLabel: Double
    let maxLabel: Double
}

func calculateYAxisLabels(
    minY: Double,
    maxY: Double,
    decimalPlaces: Int = 2,
    minLabelCount: Int = 5,
    maxLabelCount: Int = 8
) -> YAxisConfig {
    let range = maxY - minY
    if range == 0 {
        let mid = minY
        let step = mid == 0 ? 1.0 : abs(mid) / Double(minLabelCount - 1)
        let adjustedStep = roundToDecimalPlaces(value: step, decimalPlaces: decimalPlaces)
        var labels = [Double]()
        for i in 0..<minLabelCount {
            labels.append(mid - adjustedStep * Double(minLabelCount/2 - i))
        }
        return YAxisConfig(labels: labels, step: adjustedStep, minLabel: labels.first!, maxLabel: labels.last!)
    }
    
    var bestStep = Double.greatestFiniteMagnitude
    var bestLabelCount = minLabelCount
    
    for labelCount in minLabelCount...maxLabelCount {
        let rawStep = range / Double(labelCount - 1)
        let magnitude = floor(log10(rawStep))
        let msd = Int(round(rawStep / pow(10, magnitude)))
        
        let adjustedMsd: Int
        if msd > 5 {
            adjustedMsd = 10
        } else if msd > 2 {
            adjustedMsd = 5
        } else {
            adjustedMsd = 1
        }
        
        var step = Double(adjustedMsd) * pow(10, magnitude)
        step = roundToDecimalPlaces(value: step, decimalPlaces: decimalPlaces)
        
        let roundedMin = floor(minY / step) * step
        let roundedMax = roundedMin + Double(labelCount - 1) * step
        
        if roundedMax >= maxY && roundedMin <= minY {
            if step < bestStep {
                bestStep = step
                bestLabelCount = labelCount
            }
        }
    }
    
    if bestStep == Double.greatestFiniteMagnitude {
        var labelCount = maxLabelCount
        while true {
            let rawStep = range / Double(labelCount - 1)
            let magnitude = floor(log10(rawStep))
            let msd = Int(round(rawStep / pow(10, magnitude)))
            
            let adjustedMsd: Int
            if msd > 5 {
                adjustedMsd = 10
            } else if msd > 2 {
                adjustedMsd = 5
            } else {
                adjustedMsd = 1
            }
            
            bestStep = Double(adjustedMsd) * pow(10, magnitude)
            bestStep = roundToDecimalPlaces(value: bestStep, decimalPlaces: decimalPlaces)
            
            let roundedMin = floor(minY / bestStep) * bestStep
            let roundedMax = roundedMin + Double(labelCount - 1) * bestStep
            
            if roundedMax >= maxY && roundedMin <= minY {
                bestLabelCount = labelCount
                break
            }
            labelCount += 1
        }
    }
    
    let roundedMin = floor(minY / bestStep) * bestStep
    var labels = [Double]()
    var current = roundedMin
    while current <= maxY + bestStep * 0.1 {
        labels.append(roundToDecimalPlaces(value: current, decimalPlaces: decimalPlaces))
        current += bestStep
    }
    
    return YAxisConfig(labels: labels, step: bestStep, minLabel: labels.first!, maxLabel: labels.last!)
}

private func roundToDecimalPlaces(value: Double, decimalPlaces: Int) -> Double {
    let factor = pow(10, Double(decimalPlaces))
    return round(value * factor) / factor
}

// 使用示例
let data = [(0,15.0),(1,17.0),(2,20.0),(3,28.5),(4,29),(5,32.0),(6,33.5),(7,33.6)]
let minY = data.map { $0.1 }.min()!
let maxY = data.map { $0.1 }.max()!
let config = calculateYAxisLabels(minY: minY, maxY: maxY, decimalPlaces: 0)
print("Y轴标签: \(config.labels)")

3. Chart.js 实现

function calculateYAxisLabels(minY, maxY, decimalPlaces = 2, minLabelCount = 5, maxLabelCount = 8) {
    const range = maxY - minY;
    if (range === 0) {
        const mid = minY;
        const step = mid === 0 ? 1 : Math.abs(mid) / (minLabelCount - 1);
        const adjustedStep = roundToDecimalPlaces(step, decimalPlaces);
        const labels = [];
        for (let i = 0; i < minLabelCount; i++) {
            labels.push(mid - adjustedStep * (Math.floor(minLabelCount/2) - i));
        }
        return { labels, step: adjustedStep, minLabel: labels[0], maxLabel: labels[labels.length-1] };
    }

    let bestStep = Infinity;
    let bestLabelCount = minLabelCount;

    for (let labelCount = minLabelCount; labelCount <= maxLabelCount; labelCount++) {
        const rawStep = range / (labelCount - 1);
        const magnitude = Math.floor(Math.log10(rawStep));
        const msd = Math.round(rawStep / Math.pow(10, magnitude));

        let adjustedMsd;
        if (msd > 5) {
            adjustedMsd = 10;
        } else if (msd > 2) {
            adjustedMsd = 5;
        } else {
            adjustedMsd = 1;
        }

        let step = adjustedMsd * Math.pow(10, magnitude);
        step = roundToDecimalPlaces(step, decimalPlaces);

        const roundedMin = Math.floor(minY / step) * step;
        const roundedMax = roundedMin + (labelCount - 1) * step;

        if (roundedMax >= maxY && roundedMin <= minY) {
            if (step < bestStep) {
                bestStep = step;
                bestLabelCount = labelCount;
            }
        }
    }

    if (bestStep === Infinity) {
        let labelCount = maxLabelCount;
        while (true) {
            const rawStep = range / (labelCount - 1);
            const magnitude = Math.floor(Math.log10(rawStep));
            const msd = Math.round(rawStep / Math.pow(10, magnitude));

            let adjustedMsd;
            if (msd > 5) {
                adjustedMsd = 10;
            } else if (msd > 2) {
                adjustedMsd = 5;
            } else {
                adjustedMsd = 1;
            }

            bestStep = adjustedMsd * Math.pow(10, magnitude);
            bestStep = roundToDecimalPlaces(bestStep, decimalPlaces);

            const roundedMin = Math.floor(minY / bestStep) * bestStep;
            const roundedMax = roundedMin + (labelCount - 1) * bestStep;

            if (roundedMax >= maxY && roundedMin <= minY) {
                bestLabelCount = labelCount;
                break;
            }
            labelCount++;
        }
    }

    const roundedMin = Math.floor(minY / bestStep) * bestStep;
    const labels = [];
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 23:56:51