From f6a06eb827cca08fd697b3f1989450d4bb373d07 Mon Sep 17 00:00:00 2001 From: Brandon Gonzalez Date: Tue, 2 Sep 2025 13:27:30 -0400 Subject: [PATCH] Adds tick interval base --- src/coord/axisCommonTypes.ts | 6 +++++ src/coord/axisHelper.ts | 3 ++- src/scale/Interval.ts | 7 +++--- src/scale/Scale.ts | 3 ++- src/scale/helper.ts | 18 +++++++++++--- src/util/number.ts | 46 ++++++++++++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index eee3ab74e3..821dc69171 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -163,6 +163,12 @@ export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { * Will be ignored if interval is set. */ alignTicks?: boolean + + /** + * Configures the base of the interval to be used, e.g. use 2 for binary-based nice intervals (1, 2, 4, 8 × 2^n) + * or 10 for decimal-based intervals (1, 2, 5 × 10^n) (default). + */ + tickBase?: number; } export interface CategoryAxisBaseOption extends AxisBaseOptionCommon { diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 24cb773ee3..cb78261e16 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -178,7 +178,8 @@ export function niceScaleExtent( fixMin: extentInfo.fixMin, fixMax: extentInfo.fixMax, minInterval: isIntervalOrTime ? model.get('minInterval') : null, - maxInterval: isIntervalOrTime ? model.get('maxInterval') : null + maxInterval: isIntervalOrTime ? model.get('maxInterval') : null, + tickBase: isIntervalOrTime ? model.get('tickBase') : undefined }); // If some one specified the min, max. And the default calculated interval diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts index 917aa3f531..92ac5a1121 100644 --- a/src/scale/Interval.ts +++ b/src/scale/Interval.ts @@ -285,7 +285,7 @@ class IntervalScale e * * @param splitNumber By default `5`. */ - calcNiceTicks(splitNumber?: number, minInterval?: number, maxInterval?: number): void { + calcNiceTicks(splitNumber?: number, minInterval?: number, maxInterval?: number, tickBase?: number): void { splitNumber = splitNumber || 5; let extent = this._extent.slice() as [number, number]; let span = this._getExtentSpanWithBreaks(); @@ -302,7 +302,7 @@ class IntervalScale e } const result = helper.intervalScaleNiceTicks( - extent, span, splitNumber, minInterval, maxInterval + extent, span, splitNumber, minInterval, maxInterval, tickBase, ); this._intervalPrecision = result.intervalPrecision; @@ -316,6 +316,7 @@ class IntervalScale e fixMax?: boolean, minInterval?: number, maxInterval?: number + tickBase?: number, }): void { let extent = this._extent.slice() as [number, number]; // If extent start and end are same, expand them @@ -350,7 +351,7 @@ class IntervalScale e this._innerSetExtent(extent[0], extent[1]); extent = this._extent.slice() as [number, number]; - this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); + this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval, opt.tickBase); const interval = this._interval; const intervalPrecition = this._intervalPrecision; diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index 1f1fbdecf2..814ec25428 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -250,6 +250,7 @@ abstract class Scale fixMax?: boolean, minInterval?: number, maxInterval?: number + tickBase?: number, } ): void; @@ -270,4 +271,4 @@ abstract class Scale type ScaleConstructor = typeof Scale & clazzUtil.ClassManager; clazzUtil.enableClassManagement(Scale as ScaleConstructor); -export default Scale; \ No newline at end of file +export default Scale; diff --git a/src/scale/helper.ts b/src/scale/helper.ts index 31f41945ec..43c71d3149 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -17,7 +17,7 @@ * under the License. */ -import {getPrecision, round, nice, quantityExponent} from '../util/number'; +import {getPrecision, round, nice, niceBinary, quantityExponent} from '../util/number'; import IntervalScale from './Interval'; import LogScale from './Log'; import type Scale from './Scale'; @@ -53,12 +53,13 @@ export function intervalScaleNiceTicks( spanWithBreaks: number, splitNumber: number, minInterval?: number, - maxInterval?: number + maxInterval?: number, + tickBase?: number ): intervalScaleNiceTicksResult { const result = {} as intervalScaleNiceTicksResult; - let interval = result.interval = nice(spanWithBreaks / splitNumber, true); + let interval = result.interval = getNiceInterval(spanWithBreaks / splitNumber, true, tickBase); if (minInterval != null && interval < minInterval) { interval = result.interval = minInterval; } @@ -173,3 +174,14 @@ export function logTransform(base: number, extent: number[], noClampNegative?: b Math.log(noClampNegative ? extent[1] : Math.max(0, extent[1])) / loggedBase ]; } + +function getNiceInterval(val: number, round?: boolean, base?: number): number { + if (base === 2) { + return niceBinary(val, round); + } + else if (base === 10) { + return nice(val, round); + } + // default to base 10 + return nice(val, round); +} diff --git a/src/util/number.ts b/src/util/number.ts index f156e6d3de..5152d068d9 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -521,6 +521,52 @@ export function nice(val: number, round?: boolean): number { return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val; } +/** + * Binary-based nice function for bytes, memory, etc. + * Produces intervals like 1, 2, 4, 8 × 2^n + */ +export function niceBinary(val: number, round?: boolean): number { + const exp2 = Math.pow(2, Math.floor(Math.log2(val))); + const f = val / exp2; // 1 <= f < 2 + + let nf: number; + if (round) { + if (f < 1.5) { + nf = 1; + } + else if (f < 2.5) { + nf = 2; + } + else if (f < 4) { + nf = 4; + } + else if (f < 7) { + nf = 8; + } + else { + nf = 16; + } + } + else { + if (f < 1) { + nf = 1; + } + else if (f < 2) { + nf = 2; + } + else if (f < 3) { + nf = 4; + } + else if (f < 5) { + nf = 8; + } + else { + nf = 16; + } + } + return nf * exp2; +} + /** * This code was copied from "d3.js" * .