Skip to content

Commit c67121f

Browse files
committed
Support negative values for log Scale.
Support implemented by inverting the extents at input and handling the input as absolute value and finally mapping everything back to negative axes in inverted order. Added test cases for passing through 0 power and new a new file ogScale-negative.html.
1 parent 2fab291 commit c67121f

File tree

5 files changed

+271
-31
lines changed

5 files changed

+271
-31
lines changed

src/scale/Log.ts

+82-25
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ const roundingErrorFix = numberUtil.round;
3636
const mathFloor = Math.floor;
3737
const mathCeil = Math.ceil;
3838
const mathPow = Math.pow;
39-
40-
const mathLog = Math.log;
41-
39+
const mathMax = Math.max;
40+
const mathRound = Math.round;
41+
42+
/**
43+
* LogScale is a scale that maps a linear values to a logarithmic range.
44+
*
45+
* Support for negative values is implemented by inverting the extents and mapping the values as they were positive.
46+
*/
4247
class LogScale extends Scale {
4348
static type = 'log';
4449
readonly type = 'log';
@@ -47,6 +52,14 @@ class LogScale extends Scale {
4752

4853
private _originalScale: IntervalScale = new IntervalScale();
4954

55+
/**
56+
* Whether the original input values are negative.
57+
*
58+
* @type {boolean}
59+
* @private
60+
*/
61+
private _isNegative: boolean = false;
62+
5063
private _fixMin: boolean;
5164
private _fixMax: boolean;
5265

@@ -63,12 +76,13 @@ class LogScale extends Scale {
6376
const originalScale = this._originalScale;
6477
const extent = this._extent;
6578
const originalExtent = originalScale.getExtent();
79+
const negativeMultiplier = this._isNegative ? -1 : 1;
6680

6781
const ticks = intervalScaleProto.getTicks.call(this, expandToNicedExtent);
6882

6983
return zrUtil.map(ticks, function (tick) {
7084
const val = tick.value;
71-
let powVal = numberUtil.round(mathPow(this.base, val));
85+
let powVal = mathPow(this.base, val);
7286

7387
// Fix #4158
7488
powVal = (val === extent[0] && this._fixMin)
@@ -79,27 +93,31 @@ class LogScale extends Scale {
7993
: powVal;
8094

8195
return {
82-
value: powVal
96+
value: powVal * negativeMultiplier
8397
};
8498
}, this);
8599
}
86100

87101
setExtent(start: number, end: number): void {
88-
const base = mathLog(this.base);
102+
// Assume the start and end can be infinity
89103
// log(-Infinity) is NaN, so safe guard here
90-
start = mathLog(Math.max(0, start)) / base;
91-
end = mathLog(Math.max(0, end)) / base;
104+
if (start < Infinity) {
105+
start = scaleHelper.absMathLog(start, this.base);
106+
}
107+
if (end > -Infinity) {
108+
end = scaleHelper.absMathLog(end, this.base);
109+
}
110+
92111
intervalScaleProto.setExtent.call(this, start, end);
93112
}
94113

95114
/**
96115
* @return {number} end
97116
*/
98117
getExtent() {
99-
const base = this.base;
100118
const extent = scaleProto.getExtent.call(this);
101-
extent[0] = mathPow(base, extent[0]);
102-
extent[1] = mathPow(base, extent[1]);
119+
extent[0] = mathPow(this.base, extent[0]);
120+
extent[1] = mathPow(this.base, extent[1]);
103121

104122
// Fix #4158
105123
const originalScale = this._originalScale;
@@ -113,9 +131,17 @@ class LogScale extends Scale {
113131
unionExtent(extent: [number, number]): void {
114132
this._originalScale.unionExtent(extent);
115133

116-
const base = this.base;
117-
extent[0] = mathLog(extent[0]) / mathLog(base);
118-
extent[1] = mathLog(extent[1]) / mathLog(base);
134+
if (extent[0] < 0 && extent[1] < 0) {
135+
// If both extent are negative, switch to plotting negative values.
136+
// If there are only some negative values, they will be plotted incorrectly as positive values.
137+
this._isNegative = true;
138+
}
139+
140+
const [logStart, logEnd] = this.getLogExtent(extent[0], extent[1]);
141+
142+
extent[0] = logStart;
143+
extent[1] = logEnd;
144+
119145
scaleProto.unionExtent.call(this, extent);
120146
}
121147

@@ -131,13 +157,18 @@ class LogScale extends Scale {
131157
*/
132158
calcNiceTicks(approxTickNum: number): void {
133159
approxTickNum = approxTickNum || 10;
134-
const extent = this._extent;
135-
const span = extent[1] - extent[0];
160+
161+
const span = this._extent[1] - this._extent[0];
162+
136163
if (span === Infinity || span <= 0) {
137164
return;
138165
}
139166

140-
let interval = numberUtil.quantity(span);
167+
let interval = mathMax(
168+
1,
169+
mathRound(span / approxTickNum)
170+
);
171+
141172
const err = approxTickNum / span * interval;
142173

143174
// Filter ticks to get closer to the desired count.
@@ -150,10 +181,10 @@ class LogScale extends Scale {
150181
interval *= 10;
151182
}
152183

153-
const niceExtent = [
154-
numberUtil.round(mathCeil(extent[0] / interval) * interval),
155-
numberUtil.round(mathFloor(extent[1] / interval) * interval)
156-
] as [number, number];
184+
const niceExtent: [number, number] = [
185+
mathFloor(this._extent[0] / interval) * interval,
186+
mathCeil(this._extent[1] / interval) * interval
187+
];
157188

158189
this._interval = interval;
159190
this._niceExtent = niceExtent;
@@ -177,13 +208,19 @@ class LogScale extends Scale {
177208
}
178209

179210
contain(val: number): boolean {
180-
val = mathLog(val) / mathLog(this.base);
211+
val = scaleHelper.absMathLog(val, this.base);
181212
return scaleHelper.contain(val, this._extent);
182213
}
183214

184-
normalize(val: number): number {
185-
val = mathLog(val) / mathLog(this.base);
186-
return scaleHelper.normalize(val, this._extent);
215+
normalize(inputVal: number): number {
216+
const val = scaleHelper.absMathLog(inputVal, this.base);
217+
let ex: [number, number] = [this._extent[0], this._extent[1]];
218+
219+
if (this._isNegative) {
220+
// Invert the extent for normalize calculations as the extent is inverted for negative values.
221+
ex = [this._extent[1], this._extent[0]];
222+
}
223+
return scaleHelper.normalize(val, ex);
187224
}
188225

189226
scale(val: number): number {
@@ -193,6 +230,26 @@ class LogScale extends Scale {
193230

194231
getMinorTicks: IntervalScale['getMinorTicks'];
195232
getLabel: IntervalScale['getLabel'];
233+
234+
/**
235+
* Get the extent of the log scale.
236+
* @param start - The start value of the extent.
237+
* @param end - The end value of the extent.
238+
* @returns The extent of the log scale. The extent is reversed for negative values.
239+
*/
240+
getLogExtent(start: number, end: number): [number, number] {
241+
// Invert the extent but use absolute values
242+
if (this._isNegative) {
243+
const logStart = scaleHelper.absMathLog(Math.abs(end), this.base);
244+
const logEnd = scaleHelper.absMathLog(Math.abs(start), this.base);
245+
return [logStart, logEnd];
246+
}
247+
else {
248+
const logStart = scaleHelper.absMathLog(start, this.base);
249+
const logEnd = scaleHelper.absMathLog(end, this.base);
250+
return [logStart, logEnd];
251+
}
252+
}
196253
}
197254

198255
const proto = LogScale.prototype;

src/scale/helper.ts

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type intervalScaleNiceTicksResult = {
2828
niceTickExtent: [number, number]
2929
};
3030

31+
const mathLog = Math.log;
32+
3133
export function isValueNice(val: number) {
3234
const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
3335
const f = Math.abs(val / exp10);
@@ -136,3 +138,20 @@ export function normalize(val: number, extent: [number, number]): number {
136138
export function scale(val: number, extent: [number, number]): number {
137139
return val * (extent[1] - extent[0]) + extent[0];
138140
}
141+
142+
/**
143+
* Calculates the absolute logarithm of a number with a specified base.
144+
* Handles edge cases by:
145+
* - Returning 0 for values very close to 0 (within Number.EPSILON)
146+
* - Taking the absolute value of the input to handle negative numbers
147+
*
148+
* @param x - The number to calculate the logarithm of
149+
* @param base - The base of the logarithm (defaults to 10)
150+
* @returns The absolute logarithm value, or 0 if x is very close to 0
151+
*/
152+
export function absMathLog(x: number, base = 10): number {
153+
if (Math.abs(x) < Number.EPSILON) {
154+
return 0;
155+
}
156+
return mathLog(Math.abs(x)) / mathLog(base);
157+
}

test/logScale-negative.html

+134
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)