Skip to content

Commit 3289f64

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 3289f64

File tree

5 files changed

+268
-31
lines changed

5 files changed

+268
-31
lines changed

src/scale/Log.ts

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,16 @@ 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 values to a logarithmic range.
44+
*
45+
* Support for negative values is implemented by inverting the extents and first handling values as absolute values.
46+
* Then in tick generation, the tick values are multiplied by -1 back to the original values and the normalize function
47+
* uses a reverse extent to get the correct negative values in plot with smaller values at the top of Y axis.
48+
*/
4249
class LogScale extends Scale {
4350
static type = 'log';
4451
readonly type = 'log';
@@ -47,6 +54,14 @@ class LogScale extends Scale {
4754

4855
private _originalScale: IntervalScale = new IntervalScale();
4956

57+
/**
58+
* Whether the original input values are negative.
59+
*
60+
* @type {boolean}
61+
* @private
62+
*/
63+
private _isNegative: boolean = false;
64+
5065
private _fixMin: boolean;
5166
private _fixMax: boolean;
5267

@@ -63,12 +78,13 @@ class LogScale extends Scale {
6378
const originalScale = this._originalScale;
6479
const extent = this._extent;
6580
const originalExtent = originalScale.getExtent();
81+
const negativeMultiplier = this._isNegative ? -1 : 1;
6682

6783
const ticks = intervalScaleProto.getTicks.call(this, expandToNicedExtent);
6884

6985
return zrUtil.map(ticks, function (tick) {
7086
const val = tick.value;
71-
let powVal = numberUtil.round(mathPow(this.base, val));
87+
let powVal = mathPow(this.base, val);
7288

7389
// Fix #4158
7490
powVal = (val === extent[0] && this._fixMin)
@@ -79,27 +95,31 @@ class LogScale extends Scale {
7995
: powVal;
8096

8197
return {
82-
value: powVal
98+
value: powVal * negativeMultiplier
8399
};
84100
}, this);
85101
}
86102

87103
setExtent(start: number, end: number): void {
88-
const base = mathLog(this.base);
104+
// Assume the start and end can be infinity
89105
// log(-Infinity) is NaN, so safe guard here
90-
start = mathLog(Math.max(0, start)) / base;
91-
end = mathLog(Math.max(0, end)) / base;
106+
if (start < Infinity) {
107+
start = scaleHelper.absMathLog(start, this.base);
108+
}
109+
if (end > -Infinity) {
110+
end = scaleHelper.absMathLog(end, this.base);
111+
}
112+
92113
intervalScaleProto.setExtent.call(this, start, end);
93114
}
94115

95116
/**
96117
* @return {number} end
97118
*/
98119
getExtent() {
99-
const base = this.base;
100120
const extent = scaleProto.getExtent.call(this);
101-
extent[0] = mathPow(base, extent[0]);
102-
extent[1] = mathPow(base, extent[1]);
121+
extent[0] = mathPow(this.base, extent[0]);
122+
extent[1] = mathPow(this.base, extent[1]);
103123

104124
// Fix #4158
105125
const originalScale = this._originalScale;
@@ -113,9 +133,17 @@ class LogScale extends Scale {
113133
unionExtent(extent: [number, number]): void {
114134
this._originalScale.unionExtent(extent);
115135

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

@@ -131,13 +159,18 @@ class LogScale extends Scale {
131159
*/
132160
calcNiceTicks(approxTickNum: number): void {
133161
approxTickNum = approxTickNum || 10;
134-
const extent = this._extent;
135-
const span = extent[1] - extent[0];
162+
163+
const span = this._extent[1] - this._extent[0];
164+
136165
if (span === Infinity || span <= 0) {
137166
return;
138167
}
139168

140-
let interval = numberUtil.quantity(span);
169+
let interval = mathMax(
170+
1,
171+
mathRound(span / approxTickNum)
172+
);
173+
141174
const err = approxTickNum / span * interval;
142175

143176
// Filter ticks to get closer to the desired count.
@@ -150,10 +183,10 @@ class LogScale extends Scale {
150183
interval *= 10;
151184
}
152185

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

158191
this._interval = interval;
159192
this._niceExtent = niceExtent;
@@ -177,13 +210,19 @@ class LogScale extends Scale {
177210
}
178211

179212
contain(val: number): boolean {
180-
val = mathLog(val) / mathLog(this.base);
213+
val = scaleHelper.absMathLog(val, this.base);
181214
return scaleHelper.contain(val, this._extent);
182215
}
183216

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

189228
scale(val: number): number {
@@ -193,6 +232,26 @@ class LogScale extends Scale {
193232

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

198257
const proto = LogScale.prototype;

src/scale/helper.ts

Lines changed: 19 additions & 0 deletions
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

Lines changed: 134 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)