From a4380ab0b27f6878935c20b5ad455be923a133b4 Mon Sep 17 00:00:00 2001 From: lxfu1 <954055752@qq.com> Date: Thu, 28 Aug 2025 15:11:02 +0800 Subject: [PATCH] feat: limit the compression range --- .eslintrc.js | 1 + src/scales/linear.ts | 71 +++++++++++++++++++++++++++++++++++++++----- src/types.ts | 3 +- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 3b6ddb2..90d3ece 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,7 @@ module.exports = { 'func-names': ['error', 'never'], 'no-else-return': 'off', 'no-restricted-syntax': 'off', + 'prefer-destructuring': 'off', }, settings: { 'import/parsers': { diff --git a/src/scales/linear.ts b/src/scales/linear.ts index 99a1744..fda97b6 100644 --- a/src/scales/linear.ts +++ b/src/scales/linear.ts @@ -25,8 +25,40 @@ export class Linear extends Continuous { }; } + protected removeUnsortedValues(breaksDomain: number[], breaksRange: number[], reverse: boolean) { + let pre = -Infinity; + const deleteIndices = breaksRange.reduce((acc, current, i) => { + if (i === 0) return acc; + const value = pre > 0 ? pre : current; + if (pre > 0 && (reverse ? current > pre : current < pre)) { + acc.push(i); + } else { + const diff = (value - breaksRange[i - 1]) * (reverse ? -1 : 1); + if (diff < 0) { + if (pre < 0) pre = breaksRange[i - 1]; + acc.push(i); + } else { + pre = -Infinity; + } + } + return acc; + }, [] as number[]); + + deleteIndices + .slice() + .reverse() + .forEach((index) => { + breaksDomain.splice(index, 1); + breaksRange.splice(index, 1); + }); + + return { breaksDomain, breaksRange }; + } + protected transformDomain(options: LinearOptions): { breaksDomain: number[]; breaksRange: number[] } { - const { domain, range = [1, 0], breaks = [], tickCount = 5 } = options; + const RANGE_LIMIT = [0.2, 0.8]; + const DEFAULT_GAP = 0.03; + const { domain = [], range = [1, 0], breaks = [], tickCount = 5 } = options; const [domainMin, domainMax] = [Math.min(...domain), Math.max(...domain)]; const sortedBreaks = breaks.filter(({ end }) => end < domainMax).sort((a, b) => a.start - b.start); const breaksDomain = d3Ticks(domainMin, domainMax, tickCount, sortedBreaks); @@ -43,24 +75,47 @@ export class Linear extends Continuous { return reverse ? r0 - ratio * diffRange : r0 + ratio * diffRange; }); // Compress the range scale according to breaks. - sortedBreaks.forEach(({ start, end, gap = 0.05 }) => { + const [MIN, MAX] = RANGE_LIMIT; + sortedBreaks.forEach(({ start, end, gap = DEFAULT_GAP, compress = 'middle' }) => { const startIndex = breaksDomain.indexOf(start); const endIndex = breaksDomain.indexOf(end); - const center = (breaksRange[startIndex] + breaksRange[endIndex]) / 2; - const scaledSpan = gap * diffRange; + let value = (breaksRange[startIndex] + breaksRange[endIndex]) / 2; + if (compress === 'start') value = breaksRange[startIndex]; + if (compress === 'end') value = breaksRange[endIndex]; + const halfSpan = (gap * diffRange) / 2; // Calculate the new start and end values based on the center and scaled span. - const startValue = reverse ? center + scaledSpan / 2 : center - scaledSpan / 2; - const endValue = reverse ? center - scaledSpan / 2 : center + scaledSpan / 2; + let startValue = reverse ? value + halfSpan : value - halfSpan; + let endValue = reverse ? value - halfSpan : value + halfSpan; + + // Ensure the new start and end values are within the defined limits. + if (startValue < MIN) { + endValue += MIN - startValue; + startValue = MIN; + } + if (endValue > MAX) { + startValue -= endValue - MAX; + endValue = MAX; + } + if (startValue > MAX) { + endValue -= startValue - MAX; + startValue = MAX; + } + if (endValue < MIN) { + startValue += MIN - endValue; + endValue = MIN; + } + breaksRange[startIndex] = startValue; breaksRange[endIndex] = endValue; }); - return { breaksDomain, breaksRange }; + + return this.removeUnsortedValues(breaksDomain, breaksRange, reverse); } protected transformBreaks(options: LinearOptions): LinearOptions { const { domain, breaks = [] } = options; if (!isArray(options.breaks)) return options; - const domainMax = Math.max(...domain); + const domainMax = Math.max(...(domain as number[])); const filteredBreaks = breaks.filter(({ end }) => end < domainMax); const optWithFilteredBreaks = { ...options, breaks: filteredBreaks }; const { breaksDomain, breaksRange } = this.transformDomain(optWithFilteredBreaks); diff --git a/src/types.ts b/src/types.ts index 88b08c8..0f44ab3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,7 +28,8 @@ export type CreateTransform = (...args: any[]) => Transform; export interface BreakOptions { start: number; // 断轴开始 end: number; // 断轴结束 - gap: number; // 在可视 range 中保留的间隔长度(0 ~ 1),默认为 0.05,表示 5% 的间隔 + gap: number; // 在可视 range 中保留的间隔长度(0 ~ 1),默认为 0.03,表示 3% 的间隔 + compress?: 'middle' | 'start' | 'end'; // 压缩方式,默认 middle } /** 通用的配置 */