@@ -36,9 +36,16 @@ const roundingErrorFix = numberUtil.round;
36
36
const mathFloor = Math . floor ;
37
37
const mathCeil = Math . ceil ;
38
38
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
+ */
42
49
class LogScale extends Scale {
43
50
static type = 'log' ;
44
51
readonly type = 'log' ;
@@ -47,6 +54,14 @@ class LogScale extends Scale {
47
54
48
55
private _originalScale : IntervalScale = new IntervalScale ( ) ;
49
56
57
+ /**
58
+ * Whether the original input values are negative.
59
+ *
60
+ * @type {boolean }
61
+ * @private
62
+ */
63
+ private _isNegative : boolean = false ;
64
+
50
65
private _fixMin : boolean ;
51
66
private _fixMax : boolean ;
52
67
@@ -63,12 +78,13 @@ class LogScale extends Scale {
63
78
const originalScale = this . _originalScale ;
64
79
const extent = this . _extent ;
65
80
const originalExtent = originalScale . getExtent ( ) ;
81
+ const negativeMultiplier = this . _isNegative ? - 1 : 1 ;
66
82
67
83
const ticks = intervalScaleProto . getTicks . call ( this , expandToNicedExtent ) ;
68
84
69
85
return zrUtil . map ( ticks , function ( tick ) {
70
86
const val = tick . value ;
71
- let powVal = numberUtil . round ( mathPow ( this . base , val ) ) ;
87
+ let powVal = mathPow ( this . base , val ) ;
72
88
73
89
// Fix #4158
74
90
powVal = ( val === extent [ 0 ] && this . _fixMin )
@@ -79,27 +95,31 @@ class LogScale extends Scale {
79
95
: powVal ;
80
96
81
97
return {
82
- value : powVal
98
+ value : powVal * negativeMultiplier
83
99
} ;
84
100
} , this ) ;
85
101
}
86
102
87
103
setExtent ( start : number , end : number ) : void {
88
- const base = mathLog ( this . base ) ;
104
+ // Assume the start and end can be infinity
89
105
// 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
+
92
113
intervalScaleProto . setExtent . call ( this , start , end ) ;
93
114
}
94
115
95
116
/**
96
117
* @return {number } end
97
118
*/
98
119
getExtent ( ) {
99
- const base = this . base ;
100
120
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 ] ) ;
103
123
104
124
// Fix #4158
105
125
const originalScale = this . _originalScale ;
@@ -113,9 +133,17 @@ class LogScale extends Scale {
113
133
unionExtent ( extent : [ number , number ] ) : void {
114
134
this . _originalScale . unionExtent ( extent ) ;
115
135
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
+
119
147
scaleProto . unionExtent . call ( this , extent ) ;
120
148
}
121
149
@@ -131,13 +159,18 @@ class LogScale extends Scale {
131
159
*/
132
160
calcNiceTicks ( approxTickNum : number ) : void {
133
161
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
+
136
165
if ( span === Infinity || span <= 0 ) {
137
166
return ;
138
167
}
139
168
140
- let interval = numberUtil . quantity ( span ) ;
169
+ let interval = mathMax (
170
+ 1 ,
171
+ mathRound ( span / approxTickNum )
172
+ ) ;
173
+
141
174
const err = approxTickNum / span * interval ;
142
175
143
176
// Filter ticks to get closer to the desired count.
@@ -150,10 +183,10 @@ class LogScale extends Scale {
150
183
interval *= 10 ;
151
184
}
152
185
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
+ ] ;
157
190
158
191
this . _interval = interval ;
159
192
this . _niceExtent = niceExtent ;
@@ -177,13 +210,19 @@ class LogScale extends Scale {
177
210
}
178
211
179
212
contain ( val : number ) : boolean {
180
- val = mathLog ( val ) / mathLog ( this . base ) ;
213
+ val = scaleHelper . absMathLog ( val , this . base ) ;
181
214
return scaleHelper . contain ( val , this . _extent ) ;
182
215
}
183
216
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 ) ;
187
226
}
188
227
189
228
scale ( val : number ) : number {
@@ -193,6 +232,26 @@ class LogScale extends Scale {
193
232
194
233
getMinorTicks : IntervalScale [ 'getMinorTicks' ] ;
195
234
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
+ }
196
255
}
197
256
198
257
const proto = LogScale . prototype ;
0 commit comments