Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
Run results for:
双轴图刻度对齐算法性能对比
Go to the benchmark
Embed
Embed Benchmark Result
Run details:
User agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Browser:
Chrome 135
Operating system:
Mac OS X 10.15.7
Device Platform:
Desktop
Date tested:
one year ago
Test name
Executions per second
双轴共同最优解
5890.4 Ops/sec
线性插值
446425.2 Ops/sec
HTML Preparation code:
<!--your preparation HTML code goes here-->
Script Preparation code:
"use strict"; const isArrayLike = function (value) { return value !== null && typeof value !== 'function' && isFinite(value.length); }; function last(o) { if (isArrayLike(o)) { const arr = o; return arr[arr.length - 1]; } return undefined; } // isFinite, const isNil = function (value) { /** * isNil(null) => true * isNil() => true */ return value === null || value === undefined; }; function size(o) { if (isNil(o)) { return 0; } if (isArrayLike(o)) { return o.length; } return Object.keys(o).length; } function head(o) { if (isArrayLike(o)) { return o[0]; } return undefined; } const indexOf = function (arr, obj) { if (!isArrayLike(arr)) { return -1; } const m = Array.prototype.indexOf; if (m) { return m.call(arr, obj); } let index = -1; for (let i = 0; i < arr.length; i++) { if (arr[i] === obj) { index = i; break; } } return index; }; const DEFAULT_Q = [1, 5, 2, 2.5, 4, 3]; const ALL_Q = [1, 5, 2, 2.5, 4, 3, 1.5, 7, 6, 8, 9]; const eps = Number.EPSILON * 100; function mod(n, m) { return ((n % m) + m) % m; } function round(n) { return Math.round(n * 1e12) / 1e12; } function simplicity(q, Q, j, lmin, lmax, lstep) { const n = size(Q); const i = indexOf(Q, q); let v = 0; const m = mod(lmin, lstep); if ((m < eps || lstep - m < eps) && lmin <= 0 && lmax >= 0) { v = 1; } return 1 - i / (n - 1) - j + v; } function simplicityMax(q, Q, j) { const n = size(Q); const i = indexOf(Q, q); const v = 1; return 1 - i / (n - 1) - j + v; } function density(k, m, dMin, dMax, lMin, lMax) { const r = (k - 1) / (lMax - lMin); const rt = (m - 1) / (Math.max(lMax, dMax) - Math.min(dMin, lMin)); return 2 - Math.max(r / rt, rt / r); } function densityMax(k, m) { if (k >= m) { return 2 - (k - 1) / (m - 1); } return 1; } function coverage(dMin, dMax, lMin, lMax) { const range = dMax - dMin; return 1 - (0.5 * ((dMax - lMax) ** 2 + (dMin - lMin) ** 2)) / (0.1 * range) ** 2; } function coverageMax(dMin, dMax, span) { const range = dMax - dMin; if (span > range) { const half = (span - range) / 2; return 1 - half ** 2 / (0.1 * range) ** 2; } return 1; } function legibility() { return 1; } function extended(dMin, dMax, n = 5, onlyLoose = true, Q = DEFAULT_Q, w = [0.25, 0.2, 0.5, 0.05]) { // 处理小于 0 和小数的 tickCount const m = n < 0 ? 0 : Math.round(n); // nan 也会导致异常 if (Number.isNaN(dMin) || Number.isNaN(dMax) || typeof dMin !== 'number' || typeof dMax !== 'number' || !m) { return { min: 0, max: 0, ticks: [], }; } // js 极大值极小值问题,差值小于 1e-15 会导致计算出错 if (dMax - dMin < 1e-15 || m === 1) { return { min: dMin, max: dMax, ticks: [dMin], }; } // js 超大值问题 if (dMax - dMin > 1e148) { const count = n || 5; const step = (dMax - dMin) / count; return { min: dMin, max: dMax, ticks: Array(count) .fill(null) .map((_, idx) => { return prettyNumber(dMin + step * idx); }), }; } const best = { score: -2, lmin: 0, lmax: 0, lstep: 0, }; let j = 1; while (j < Infinity) { for (let i = 0; i < Q.length; i += 1) { const q = Q[i]; const sm = simplicityMax(q, Q, j); if (w[0] * sm + w[1] + w[2] + w[3] < best.score) { j = Infinity; break; } let k = 2; while (k < Infinity) { const dm = densityMax(k, m); if (w[0] * sm + w[1] + w[2] * dm + w[3] < best.score) { break; } const delta = (dMax - dMin) / (k + 1) / j / q; let z = Math.ceil(Math.log10(delta)); while (z < Infinity) { const step = j * q * 10 ** z; const cm = coverageMax(dMin, dMax, step * (k - 1)); if (w[0] * sm + w[1] * cm + w[2] * dm + w[3] < best.score) { break; } const minStart = Math.floor(dMax / step) * j - (k - 1) * j; const maxStart = Math.ceil(dMin / step) * j; if (minStart <= maxStart) { const count = maxStart - minStart; for (let i = 0; i <= count; i += 1) { const start = minStart + i; const lMin = start * (step / j); const lMax = lMin + step * (k - 1); const lStep = step; const s = simplicity(q, Q, j, lMin, lMax, lStep); const c = coverage(dMin, dMax, lMin, lMax); const g = density(k, m, dMin, dMax, lMin, lMax); const l = legibility(); const score = w[0] * s + w[1] * c + w[2] * g + w[3] * l; if (score > best.score && (!onlyLoose || (lMin <= dMin && lMax >= dMax))) { best.lmin = lMin; best.lmax = lMax; best.lstep = lStep; best.score = score; } } } z += 1; } k += 1; } } j += 1; } // 处理精度问题,保证这三个数没有精度问题 const lmax = prettyNumber(best.lmax); const lmin = prettyNumber(best.lmin); const lstep = prettyNumber(best.lstep); // 加 round 是为处理 extended(0.94, 1, 5) // 保证生成的 tickCount 没有精度问题 const tickCount = Math.floor(round((lmax - lmin) / lstep)) + 1; const ticks = new Array(tickCount); // 少用乘法:防止出现 -1.2 + 1.2 * 3 = 2.3999999999999995 的情况 ticks[0] = prettyNumber(lmin); for (let i = 1; i < tickCount; i++) { ticks[i] = prettyNumber(ticks[i - 1] + lstep); } return { min: Math.min(dMin, head(ticks)), max: Math.max(dMax, last(ticks)), ticks, }; } function prettyNumber(n) { if (Math.abs(n) < 1e-15) return 0; // 处理接近整数的值 const rounded = Math.round(n); if (Math.abs(n - rounded) < 1e-10) return rounded; // 基于数值大小动态调整精度 const magnitude = Math.abs(n); if (magnitude >= 1000) { return Number(n.toFixed(2)); } return parseFloat(n.toFixed(15)); } function dualAxisNice(primaryRange, secondaryRange, minTickCount = 4, maxTickCount = 7) { // 生成刻度时的权重 - 提高density权重以确保刻度数量更接近目标值 const generationWeights = [0.1, 0.1, 0.75, 0.05]; // 为每个轴生成多种候选方案 const primaryCandidates = []; const secondaryCandidates = []; // 生成不同刻度数量的候选方案 for (let tickCount = minTickCount; tickCount <= maxTickCount; tickCount++) { // 为每个刻度数量使用不同的Q值集合 const primaryQs = [ DEFAULT_Q, // 默认优雅数字集合[1,5,2,2.5,4,3] ALL_Q, // 扩展集合包含更多倍数(如0.1,10等) generateSmartQ(primaryRange[0], primaryRange[1], tickCount), // 根据数据范围和目标刻度数量生成智能Q值 ]; const secondaryQs = [ DEFAULT_Q, // 默认优雅数字集合[1,5,2,2.5,4,3] ALL_Q, // 扩展集合包含更多倍数(如0.1,10等) generateSmartQ(secondaryRange[0], secondaryRange[1], tickCount), // 根据数据范围和目标刻度数量生成智能Q值 ]; // 为主轴生成候选方案 - 使用增强density权重的参数 for (const Q of primaryQs) { primaryCandidates.push({ result: extended(primaryRange[0], primaryRange[1], tickCount, true, Q, generationWeights), targetCount: tickCount }); } // 为次轴生成候选方案 - 使用增强density权重的参数 for (const Q of secondaryQs) { secondaryCandidates.push({ result: extended(secondaryRange[0], secondaryRange[1], tickCount, true, Q, generationWeights), targetCount: tickCount }); } } // 按刻度数量分组 const groupedByCount = new Map(); // 收集所有出现的刻度数量 const allTickCounts = new Set(); // 对主轴候选方案按刻度数量分组 primaryCandidates.forEach(candidate => { const tickCount = candidate.result.ticks.length; allTickCounts.add(tickCount); if (!groupedByCount.has(tickCount)) { groupedByCount.set(tickCount, { primary: [], secondary: [] }); } groupedByCount.get(tickCount).primary.push(candidate); }); // 对次轴候选方案按刻度数量分组 secondaryCandidates.forEach(candidate => { const tickCount = candidate.result.ticks.length; allTickCounts.add(tickCount); if (!groupedByCount.has(tickCount)) { groupedByCount.set(tickCount, { primary: [], secondary: [] }); } groupedByCount.get(tickCount).secondary.push(candidate); }); // 找到最佳匹配的刻度组合 let bestScore = -Infinity; let bestResult = null; // 对每个刻度数量分别评估 allTickCounts.forEach(tickCount => { const group = groupedByCount.get(tickCount); // 确保这个刻度数量对于两个轴都有候选方案 if (group.primary.length > 0 && group.secondary.length > 0) { for (const primary of group.primary) { for (const secondary of group.secondary) { // 使用评估函数评估当前组合 const score = evaluateDualAxisMatch(primary.result, secondary.result, primaryRange, secondaryRange, primary.targetCount, secondary.targetCount); if (score > bestScore) { bestScore = score; bestResult = { primary: primary.result, secondary: secondary.result }; } } } } }); return bestResult; } function generateSmartQ(min, max, n) { const span = max - min; if (span === 0) return [1]; const power = 10 ** Math.floor(Math.log10(span)); const baseQ = [1, 2, 5, 2.5, 4, 3]; let Q = baseQ.map((q) => q * power); // 根据基准步长扩展候选 const step = span / (n - 1); Q.push(step); // 加入基准步长 Q.push(step * 2); // 加入倍数 // 去重、排序、截断 Q = Array.from(new Set(Q)) .sort((a, b) => a - b) .slice(0, 10) // 修复小数精度问题: 根据数值大小智能限制小数位数 .map((i) => { // 对大数值取整,对中等数值保留1位小数,对小数值保留2位小数 if (i >= 100) { return Math.round(i); } else if (i >= 10) { return Number(i.toFixed(1)); } else { return Number(i.toFixed(2)); } }); return Q; } // 评估双轴刻度匹配度 - 不考虑density,提高simplicity和coverage的权重 function evaluateDualAxisMatch(primary, secondary, primaryRange, secondaryRange, primaryTargetCount = 0, secondaryTargetCount = 0) { // 确保两个轴有相同数量的刻度 if (primary.ticks.length !== secondary.ticks.length) { return -Infinity; } // 评估权重 - 提高simplicity和coverage权重,不考虑density const evaluationWeights = [0.45, 0.45, 0, 0.1]; // 计算主轴和次轴的刻度美观度 - 使用调整后的评估权重 const primaryScore = calculateAxisScore(primary.ticks, primaryRange, primaryTargetCount, evaluationWeights); const secondaryScore = calculateAxisScore(secondary.ticks, secondaryRange, secondaryTargetCount, evaluationWeights); // 计算映射一致性 - 理想情况下,相对位置应该均匀对应 const mappingConsistency = calculateMappingConsistency(primary.ticks, primaryRange, secondary.ticks, secondaryRange); // 计算总分 - 权重可以根据需要调整 return 0.3 * primaryScore + 0.3 * secondaryScore + 0.4 * mappingConsistency; } // 计算单个轴的评分 function calculateAxisScore(ticks, dataRange, targetCount = 0, weights = [0.45, 0.45, 0, 0.1]) { if (ticks.length < 2) return 0; // 从ticks中提取关键信息 const lmin = ticks[0]; const lmax = ticks[ticks.length - 1]; const lstep = ticks[1] - ticks[0]; // 确保步长一致性 const isConsistentStep = ticks.every((t, i) => i === 0 || Math.abs((t - ticks[i - 1]) - lstep) < 1e-10); if (!isConsistentStep) return 0; // 如果步长不一致,评分为0 // 提取数据范围 const [dMin, dMax] = dataRange; const m = targetCount || ticks.length; // 计算q值(从step反推) const magnitude = Math.pow(10, Math.floor(Math.log10(lstep))); const q = lstep / magnitude; const j = 1; // 简化处理 // 复用现有函数计算各项评分 const s = simplicity(q, DEFAULT_Q, j, lmin, lmax, lstep); const c = coverage(dMin, dMax, lmin, lmax); const g = density(ticks.length, m, dMin, dMax, lmin, lmax); const l = legibility(); // 使用提供的权重计算总分 return weights[0] * s + weights[1] * c + weights[2] * g + weights[3] * l; } // 计算映射一致性 - 检查相对位置的线性关系 function calculateMappingConsistency(primaryTicks, primaryRange, secondaryTicks, secondaryRange) { // 针对不同长度的刻度数组处理映射一致性 // 计算归一化位置 const primaryNormalized = primaryTicks.map(t => (t - primaryRange[0]) / (primaryRange[1] - primaryRange[0])); const secondaryNormalized = secondaryTicks.map(t => (t - secondaryRange[0]) / (secondaryRange[1] - secondaryRange[0])); // 如果两个轴的刻度数不同,我们需要通过插值来比较它们的映射一致性 if (primaryNormalized.length !== secondaryNormalized.length) { // 获取较短的数组长度和较长的数组 const minLength = Math.min(primaryNormalized.length, secondaryNormalized.length); const maxLength = Math.max(primaryNormalized.length, secondaryNormalized.length); const shorterArray = primaryNormalized.length < secondaryNormalized.length ? primaryNormalized : secondaryNormalized; const longerArray = primaryNormalized.length < secondaryNormalized.length ? secondaryNormalized : primaryNormalized; // 计算长度比例惩罚 - 长度差距越大,惩罚越大 const lengthPenalty = 1 - Math.min(0.5, (maxLength - minLength) / maxLength); // 计算关键点的映射差异 let sumSquaredDiff = 0; // 对较短数组中的每个点,在较长数组中找到对应位置的插值 for (let i = 0; i < minLength; i++) { // 根据位置比例在较长数组中查找对应点 const position = i / (minLength - 1); const interpolatedIndex = position * (maxLength - 1); const lowerIndex = Math.floor(interpolatedIndex); const upperIndex = Math.ceil(interpolatedIndex); // 如果索引相同,直接取值 let interpolatedValue; if (lowerIndex === upperIndex) { interpolatedValue = longerArray[lowerIndex]; } else { // 否则进行线性插值 const weight = interpolatedIndex - lowerIndex; interpolatedValue = longerArray[lowerIndex] * (1 - weight) + longerArray[upperIndex] * weight; } // 计算差异 const diff = shorterArray[i] - interpolatedValue; sumSquaredDiff += diff * diff; } const rmsDiff = Math.sqrt(sumSquaredDiff / minLength); // 返回考虑长度惩罚的评分 return lengthPenalty * (1 - Math.min(1, rmsDiff * 10)); } // 如果两个轴的刻度数相同,使用原来的逻辑 let sumSquaredDiff = 0; for (let i = 0; i < primaryNormalized.length; i++) { const diff = primaryNormalized[i] - secondaryNormalized[i]; sumSquaredDiff += diff * diff; } const rmsDiff = Math.sqrt(sumSquaredDiff / primaryNormalized.length); // 返回[0,1]范围的评分,0表示完全不匹配,1表示完美匹配 return 1 - Math.min(1, rmsDiff * 10); } function linearInterpolation(range, proportions, decimals) { const { min, max } = range; const propMin = Math.min(...proportions); const propMax = Math.max(...proportions); const propGap = propMax - propMin; const rangeGap = max - min; const interpolate = (value) => { const result = min + ((value - propMin) * rangeGap) / propGap; return decimals == null ? result : Number(result.toFixed(decimals)); }; const ticks = proportions.map(interpolate); // 若有NaN则表示失败(比如除数为0),返回空 return ticks.some(isNaN) ? undefined : ticks; } const testArray = [ [10, 50], [200, 800], [0.06, 0.09], [400, 800], [0.273, 0.894], [-800, -200], [0.01, 0.05], [1000, 5000], [0.1, 0.8], [10000, 90000], [2.71, 3.14], [100, 900], [0.001, 0.009], [100, 900], [1, 2], [0, 1000], [0.1, 0.5], [1.5, 3.5], [0.999, 1.001], [9999, 10001], ]
Tests:
双轴共同最优解
testArray.forEach(item => { dualAxisNice(item[0], item[1]); })
线性插值
testArray.forEach(item => { linearInterpolation({ min: item[0][0], max: item[0][1] }, extended(item[1][0], item[1][1]).ticks); })