1 /* Pretty handling of log axes. |
|
2 |
|
3 Copyright (c) 2007-2014 IOLA and Ole Laursen. |
|
4 Copyright (c) 2015 Ciprian Ceteras cipix2000@gmail.com. |
|
5 Copyright (c) 2017 Raluca Portase |
|
6 Licensed under the MIT license. |
|
7 |
|
8 Set axis.mode to "log" to enable. |
|
9 */ |
|
10 |
|
11 /* global jQuery*/ |
|
12 |
|
13 /** |
|
14 ## jquery.flot.logaxis |
|
15 This plugin is used to create logarithmic axis. This includes tick generation, |
|
16 formatters and transformers to and from logarithmic representation. |
|
17 |
|
18 ### Methods and hooks |
|
19 */ |
|
20 |
|
21 (function ($) { |
|
22 'use strict'; |
|
23 |
|
24 var options = { |
|
25 xaxis: {} |
|
26 }; |
|
27 |
|
28 /*tick generators and formatters*/ |
|
29 var PREFERRED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 10), |
|
30 EXTENDED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 4); |
|
31 |
|
32 function computePreferedLogTickValues(endLimit, rangeStep) { |
|
33 var log10End = Math.floor(Math.log(endLimit) * Math.LOG10E) - 1, |
|
34 log10Start = -log10End, |
|
35 val, range, vals = []; |
|
36 |
|
37 for (var power = log10Start; power <= log10End; power++) { |
|
38 range = parseFloat('1e' + power); |
|
39 for (var mult = 1; mult < 9; mult += rangeStep) { |
|
40 val = range * mult; |
|
41 vals.push(val); |
|
42 } |
|
43 } |
|
44 return vals; |
|
45 } |
|
46 |
|
47 /** |
|
48 - logTickGenerator(plot, axis, noTicks) |
|
49 |
|
50 Generates logarithmic ticks, depending on axis range. |
|
51 In case the number of ticks that can be generated is less than the expected noTicks/4, |
|
52 a linear tick generation is used. |
|
53 */ |
|
54 var logTickGenerator = function (plot, axis, noTicks) { |
|
55 var ticks = [], |
|
56 minIdx = -1, |
|
57 maxIdx = -1, |
|
58 surface = plot.getCanvas(), |
|
59 logTickValues = PREFERRED_LOG_TICK_VALUES, |
|
60 min = clampAxis(axis, plot), |
|
61 max = axis.max; |
|
62 |
|
63 if (!noTicks) { |
|
64 noTicks = 0.3 * Math.sqrt(axis.direction === "x" ? surface.width : surface.height); |
|
65 } |
|
66 |
|
67 PREFERRED_LOG_TICK_VALUES.some(function (val, i) { |
|
68 if (val >= min) { |
|
69 minIdx = i; |
|
70 return true; |
|
71 } else { |
|
72 return false; |
|
73 } |
|
74 }); |
|
75 |
|
76 PREFERRED_LOG_TICK_VALUES.some(function (val, i) { |
|
77 if (val >= max) { |
|
78 maxIdx = i; |
|
79 return true; |
|
80 } else { |
|
81 return false; |
|
82 } |
|
83 }); |
|
84 |
|
85 if (maxIdx === -1) { |
|
86 maxIdx = PREFERRED_LOG_TICK_VALUES.length - 1; |
|
87 } |
|
88 |
|
89 if (maxIdx - minIdx <= noTicks / 4 && logTickValues.length !== EXTENDED_LOG_TICK_VALUES.length) { |
|
90 //try with multiple of 5 for tick values |
|
91 logTickValues = EXTENDED_LOG_TICK_VALUES; |
|
92 minIdx *= 2; |
|
93 maxIdx *= 2; |
|
94 } |
|
95 |
|
96 var lastDisplayed = null, |
|
97 inverseNoTicks = 1 / noTicks, |
|
98 tickValue, pixelCoord, tick; |
|
99 |
|
100 // Count the number of tick values would appear, if we can get at least |
|
101 // nTicks / 4 accept them. |
|
102 if (maxIdx - minIdx >= noTicks / 4) { |
|
103 for (var idx = maxIdx; idx >= minIdx; idx--) { |
|
104 tickValue = logTickValues[idx]; |
|
105 pixelCoord = (Math.log(tickValue) - Math.log(min)) / (Math.log(max) - Math.log(min)); |
|
106 tick = tickValue; |
|
107 |
|
108 if (lastDisplayed === null) { |
|
109 lastDisplayed = { |
|
110 pixelCoord: pixelCoord, |
|
111 idealPixelCoord: pixelCoord |
|
112 }; |
|
113 } else { |
|
114 if (Math.abs(pixelCoord - lastDisplayed.pixelCoord) >= inverseNoTicks) { |
|
115 lastDisplayed = { |
|
116 pixelCoord: pixelCoord, |
|
117 idealPixelCoord: lastDisplayed.idealPixelCoord - inverseNoTicks |
|
118 }; |
|
119 } else { |
|
120 tick = null; |
|
121 } |
|
122 } |
|
123 |
|
124 if (tick) { |
|
125 ticks.push(tick); |
|
126 } |
|
127 } |
|
128 // Since we went in backwards order. |
|
129 ticks.reverse(); |
|
130 } else { |
|
131 var tickSize = plot.computeTickSize(min, max, noTicks), |
|
132 customAxis = {min: min, max: max, tickSize: tickSize}; |
|
133 ticks = $.plot.linearTickGenerator(customAxis); |
|
134 } |
|
135 |
|
136 return ticks; |
|
137 }; |
|
138 |
|
139 var clampAxis = function (axis, plot) { |
|
140 var min = axis.min, |
|
141 max = axis.max; |
|
142 |
|
143 if (min <= 0) { |
|
144 //for empty graph if axis.min is not strictly positive make it 0.1 |
|
145 if (axis.datamin === null) { |
|
146 min = axis.min = 0.1; |
|
147 } else { |
|
148 min = processAxisOffset(plot, axis); |
|
149 } |
|
150 |
|
151 if (max < min) { |
|
152 axis.max = axis.datamax !== null ? axis.datamax : axis.options.max; |
|
153 axis.options.offset.below = 0; |
|
154 axis.options.offset.above = 0; |
|
155 } |
|
156 } |
|
157 |
|
158 return min; |
|
159 } |
|
160 |
|
161 /** |
|
162 - logTickFormatter(value, axis, precision) |
|
163 |
|
164 This is the corresponding tickFormatter of the logaxis. |
|
165 For a number greater that 10^6 or smaller than 10^(-3), this will be drawn |
|
166 with e representation |
|
167 */ |
|
168 var logTickFormatter = function (value, axis, precision) { |
|
169 var tenExponent = value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0; |
|
170 |
|
171 if (precision) { |
|
172 if ((tenExponent >= -4) && (tenExponent <= 7)) { |
|
173 return $.plot.defaultTickFormatter(value, axis, precision); |
|
174 } else { |
|
175 return $.plot.expRepTickFormatter(value, axis, precision); |
|
176 } |
|
177 } |
|
178 if ((tenExponent >= -4) && (tenExponent <= 7)) { |
|
179 //if we have float numbers, return a limited length string(ex: 0.0009 is represented as 0.000900001) |
|
180 var formattedValue = tenExponent < 0 ? value.toFixed(-tenExponent) : value.toFixed(tenExponent + 2); |
|
181 if (formattedValue.indexOf('.') !== -1) { |
|
182 var lastZero = formattedValue.lastIndexOf('0'); |
|
183 |
|
184 while (lastZero === formattedValue.length - 1) { |
|
185 formattedValue = formattedValue.slice(0, -1); |
|
186 lastZero = formattedValue.lastIndexOf('0'); |
|
187 } |
|
188 |
|
189 //delete the dot if is last |
|
190 if (formattedValue.indexOf('.') === formattedValue.length - 1) { |
|
191 formattedValue = formattedValue.slice(0, -1); |
|
192 } |
|
193 } |
|
194 return formattedValue; |
|
195 } else { |
|
196 return $.plot.expRepTickFormatter(value, axis); |
|
197 } |
|
198 }; |
|
199 |
|
200 /*logaxis caracteristic functions*/ |
|
201 var logTransform = function (v) { |
|
202 if (v < PREFERRED_LOG_TICK_VALUES[0]) { |
|
203 v = PREFERRED_LOG_TICK_VALUES[0]; |
|
204 } |
|
205 |
|
206 return Math.log(v); |
|
207 }; |
|
208 |
|
209 var logInverseTransform = function (v) { |
|
210 return Math.exp(v); |
|
211 }; |
|
212 |
|
213 var invertedTransform = function (v) { |
|
214 return -v; |
|
215 } |
|
216 |
|
217 var invertedLogTransform = function (v) { |
|
218 return -logTransform(v); |
|
219 } |
|
220 |
|
221 var invertedLogInverseTransform = function (v) { |
|
222 return logInverseTransform(-v); |
|
223 } |
|
224 |
|
225 /** |
|
226 - setDataminRange(plot, axis) |
|
227 |
|
228 It is used for clamping the starting point of a logarithmic axis. |
|
229 This will set the axis datamin range to 0.1 or to the first datapoint greater then 0. |
|
230 The function is usefull since the logarithmic representation can not show |
|
231 values less than or equal to 0. |
|
232 */ |
|
233 function setDataminRange(plot, axis) { |
|
234 if (axis.options.mode === 'log' && axis.datamin <= 0) { |
|
235 if (axis.datamin === null) { |
|
236 axis.datamin = 0.1; |
|
237 } else { |
|
238 axis.datamin = processAxisOffset(plot, axis); |
|
239 } |
|
240 } |
|
241 } |
|
242 |
|
243 function processAxisOffset(plot, axis) { |
|
244 var series = plot.getData(), |
|
245 range = series |
|
246 .filter(function(series) { |
|
247 return series.xaxis === axis || series.yaxis === axis; |
|
248 }) |
|
249 .map(function(series) { |
|
250 return plot.computeRangeForDataSeries(series, null, isValid); |
|
251 }), |
|
252 min = axis.direction === 'x' |
|
253 ? Math.min(0.1, range && range[0] ? range[0].xmin : 0.1) |
|
254 : Math.min(0.1, range && range[0] ? range[0].ymin : 0.1); |
|
255 |
|
256 axis.min = min; |
|
257 |
|
258 return min; |
|
259 } |
|
260 |
|
261 function isValid(a) { |
|
262 return a > 0; |
|
263 } |
|
264 |
|
265 function init(plot) { |
|
266 plot.hooks.processOptions.push(function (plot) { |
|
267 $.each(plot.getAxes(), function (axisName, axis) { |
|
268 var opts = axis.options; |
|
269 if (opts.mode === 'log') { |
|
270 axis.tickGenerator = function (axis) { |
|
271 var noTicks = 11; |
|
272 return logTickGenerator(plot, axis, noTicks); |
|
273 }; |
|
274 if (typeof axis.options.tickFormatter !== 'function') { |
|
275 axis.options.tickFormatter = logTickFormatter; |
|
276 } |
|
277 axis.options.transform = opts.inverted ? invertedLogTransform : logTransform; |
|
278 axis.options.inverseTransform = opts.inverted ? invertedLogInverseTransform : logInverseTransform; |
|
279 axis.options.autoScaleMargin = 0; |
|
280 plot.hooks.setRange.push(setDataminRange); |
|
281 } else if (opts.inverted) { |
|
282 axis.options.transform = invertedTransform; |
|
283 axis.options.inverseTransform = invertedTransform; |
|
284 } |
|
285 }); |
|
286 }); |
|
287 } |
|
288 |
|
289 $.plot.plugins.push({ |
|
290 init: init, |
|
291 options: options, |
|
292 name: 'log', |
|
293 version: '0.1' |
|
294 }); |
|
295 |
|
296 $.plot.logTicksGenerator = logTickGenerator; |
|
297 $.plot.logTickFormatter = logTickFormatter; |
|
298 })(jQuery); |
|