Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
双轴图对齐算法性能对比(version2)
(version: 1)
Comparing performance of:
线性插值 vs 双轴共同最优解
Created:
one year ago
by:
Guest
Jump to the latest result
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 => { extended([0][0],[0][1]) linearInterpolation({ min: item[0][0], max: item[0][1] }, extended(item[1][0], item[1][1]).ticks); })
双轴共同最优解
testArray.forEach(item => { dualAxisNice(item[0], item[1]); })
Rendered benchmark preparation results:
Suite status:
<idle, ready to run>
Run tests (2)
Previous results
Fork
Test case name
Result
线性插值
双轴共同最优解
Fastest:
N/A
Slowest:
N/A
Latest run results:
Run details:
(Test run date:
one year ago
)
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/OS:
Chrome 135 on Mac OS X 10.15.7
View result in a separate tab
Embed
Embed Benchmark Result
Test name
Executions per second
线性插值
436014.9 Ops/sec
双轴共同最优解
6150.8 Ops/sec
Related benchmarks:
Multiple Nil checks 0.5
Multiple Nil checks 0.5 2
Multiple Nil checks 0.6
Multiple Nil checks 0.7
Multiple Nil checks 0.8
Find last element of an array
Find last element of an array no empty array
双轴图刻度对齐算法性能对比
双轴图对齐算法性能对比
Comments
Confirm delete:
Do you really want to delete benchmark?