ScaleTickIntervals.h
Go to the documentation of this file.
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4  Sonic Visualiser
5  An audio file viewer and annotation editor.
6  Centre for Digital Music, Queen Mary, University of London.
7  This file copyright 2006-2017 Chris Cannam and QMUL.
8 
9  This program is free software; you can redistribute it and/or
10  modify it under the terms of the GNU General Public License as
11  published by the Free Software Foundation; either version 2 of the
12  License, or (at your option) any later version. See the file
13  COPYING included with this distribution for more information.
14 */
15 
16 #ifndef SV_SCALE_TICK_INTERVALS_H
17 #define SV_SCALE_TICK_INTERVALS_H
18 
19 #include <string>
20 #include <vector>
21 #include <cmath>
22 
23 #include "LogRange.h"
24 #include "Debug.h"
25 
26 // Can't have this on by default, as we're called on every refresh
27 //#define DEBUG_SCALE_TICK_INTERVALS 1
28 
30 {
31 public:
32  struct Range {
33  double min; // start of value range
34  double max; // end of value range
35  int n; // number of divisions (approximate only)
36  };
37 
38  struct Tick {
39  double value; // value this tick represents
40  std::string label; // value as written
41  };
42 
43  typedef std::vector<Tick> Ticks;
44 
50  static Ticks linear(Range r) {
51  return linearTicks(r);
52  }
53 
59  static Ticks logarithmic(Range r) {
61  return logarithmicAlready(r);
62  }
63 
70  static Ticks logarithmicAlready(Range r) {
71  return logTicks(r);
72  }
73 
74 private:
75  enum Display {
79  };
80 
81  struct Instruction {
82  double initial; // value of first tick
83  double limit; // max from original range
84  double spacing; // increment between ticks
85  double roundTo; // what all displayed values should be rounded to
86  // (if 0.0, then calculate based on precision)
87  Display display; // whether to use fixed precision (%e, %f, or %g)
88  int precision; // number of dp (%f) or sf (%e)
89  bool logUnmap; // true if values represent logs of display values
90  };
91 
93  {
94  Display display = Auto;
95 
96  if (r.max < r.min) {
97  return linearInstruction({ r.max, r.min, r.n });
98  }
99  if (r.n < 1 || r.max == r.min) {
100  return { r.min, r.min, 1.0, r.min, display, 1, false };
101  }
102 
103  double inc = (r.max - r.min) / r.n;
104 
105  double digInc = log10(inc);
106  double digMax = log10(fabs(r.max));
107  double digMin = log10(fabs(r.min));
108 
109  int precInc = int(floor(digInc));
110  double roundTo = pow(10.0, precInc);
111 
112  if (precInc > -4 && precInc < 4) {
113  display = Fixed;
114  } else if ((digMax >= -2.0 && digMax <= 3.0) &&
115  (digMin >= -3.0 && digMin <= 3.0)) {
116  display = Fixed;
117  } else {
118  display = Scientific;
119  }
120 
121  int precRange = int(ceil(digMax - digInc));
122 
123  int prec = 1;
124 
125  if (display == Fixed) {
126  if (digInc < 0) {
127  prec = -precInc;
128  } else {
129  prec = 0;
130  }
131  } else {
132  prec = precRange;
133  }
134 
135 #ifdef DEBUG_SCALE_TICK_INTERVALS
136  SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl
137  << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
138  << ", n = " << r.n << ", inc = " << inc << endl;
139  SVDEBUG << "ScaleTickIntervals: digMax = " << digMax
140  << ", digInc = " << digInc << endl;
141  SVDEBUG << "ScaleTickIntervals: display = " << display
142  << ", inc = " << inc << ", precInc = " << precInc
143  << ", precRange = " << precRange
144  << ", prec = " << prec << ", roundTo = " << roundTo
145  << endl;
146 #endif
147 
148  double min = r.min;
149 
150  if (roundTo != 0.0) {
151  // Round inc to the nearest multiple of roundTo, and min
152  // to the next multiple of roundTo up. The small offset of
153  // eps is included to avoid inc of 2.49999999999 rounding
154  // to 2 or a min of -0.9999999999 rounding to 0, both of
155  // which would prevent some of our test cases from getting
156  // the most natural results.
157  double eps = 1e-7;
158  inc = round(inc / roundTo + eps) * roundTo;
159  if (inc < roundTo) inc = roundTo;
160  min = ceil(min / roundTo - eps) * roundTo;
161  if (min > r.max) min = r.max;
162  if (min == -0.0) min = 0.0;
163 #ifdef DEBUG_SCALE_TICK_INTERVALS
164  SVDEBUG << "ScaleTickIntervals: rounded inc to " << inc
165  << " and min to " << min << endl;
166 #endif
167  }
168 
169  if (display == Scientific && min != 0.0) {
170  double digNewMin = log10(fabs(min));
171  if (digNewMin < digInc) {
172  prec = int(ceil(digMax - digNewMin));
173 #ifdef DEBUG_SCALE_TICK_INTERVALS
174  SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl;
175 #endif
176  }
177  }
178 
179  return { min, r.max, inc, roundTo, display, prec, false };
180  }
181 
183  {
184  Display display = Auto;
185 
186 #ifdef DEBUG_SCALE_TICK_INTERVALS
187  SVDEBUG << "ScaleTickIntervals::logInstruction: Range is "
188  << r.min << " to " << r.max << endl;
189 #endif
190 
191  if (r.n < 1) {
192  return {};
193  }
194  if (r.max < r.min) {
195  return logInstruction({ r.max, r.min, r.n });
196  }
197  if (r.max == r.min) {
198  return { r.min, r.max, 1.0, r.min, display, 1, true };
199  }
200 
201  double inc = (r.max - r.min) / r.n;
202 
203 #ifdef DEBUG_SCALE_TICK_INTERVALS
204  SVDEBUG << "ScaleTickIntervals::logInstruction: "
205  << "Naive increment is " << inc << endl;
206 #endif
207 
208  int precision = 1;
209 
210  if (inc < 1.0) {
211  precision = int(ceil(1.0 - inc)) + 1;
212  }
213 
214  double digInc = log10(inc);
215  int precInc = int(floor(digInc));
216  double roundIncTo = pow(10.0, precInc);
217 
218  inc = round(inc / roundIncTo) * roundIncTo;
219  if (inc < roundIncTo) inc = roundIncTo;
220 
221 #ifdef DEBUG_SCALE_TICK_INTERVALS
222  SVDEBUG << "ScaleTickIntervals::logInstruction: "
223  << "Rounded increment to " << inc << endl;
224 #endif
225 
226  // if inc is close to giving us powers of two, nudge it
227  if (fabs(inc - 0.301) < 0.01) {
228  inc = log10(2.0);
229 
230 #ifdef DEBUG_SCALE_TICK_INTERVALS
231  SVDEBUG << "ScaleTickIntervals::logInstruction: "
232  << "Nudged increment to " << inc << " to get powers of two"
233  << endl;
234 #endif
235  }
236 
237  double min = r.min;
238  if (inc != 0.0) {
239  min = ceil(r.min / inc) * inc;
240  if (min > r.max) min = r.max;
241  }
242 
243  return { min, r.max, inc, 0.0, display, precision, true };
244  }
245 
246  static Ticks linearTicks(Range r) {
247  Instruction instruction = linearInstruction(r);
248  Ticks ticks = explode(instruction);
249  return ticks;
250  }
251 
252  static Ticks logTicks(Range r) {
253  Instruction instruction = logInstruction(r);
254  Ticks ticks = explode(instruction);
255  return ticks;
256  }
257 
258  static Tick makeTick(Display display, int precision, double value) {
259 
260  if (value == -0.0) {
261  value = 0.0;
262  }
263 
264  const int buflen = 40;
265  char buffer[buflen];
266 
267  if (display == Auto) {
268 
269  double eps = 1e-7;
270 
271  int digits = (value != 0.0 ?
272  1 + int(floor(eps + log10(fabs(value)))) :
273  0);
274 
275 #ifdef DEBUG_SCALE_TICK_INTERVALS
276  SVDEBUG << "makeTick: display = Auto, precision = "
277  << precision << ", value = " << value
278  << ", resulting digits = " << digits << endl;
279 #endif
280 
281  // This is not the same logic as %g uses for determining
282  // whether to delegate to use scientific or fixed notation
283 
284  if (digits < -3 || digits > 4) {
285 
286  display = Auto; // delegate planning to %g
287 
288  } else {
289 
290  display = Fixed;
291 
292  // in %.*f, the * indicates decimal places, not sig figs
293  if (precision >= digits) {
294  precision -= digits;
295  } else {
296  precision = 0;
297  }
298  }
299  }
300 
301  const char *spec = (display == Auto ? "%.*g" :
302  display == Scientific ? "%.*e" :
303  "%.*f");
304 
305 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
306 
307  snprintf(buffer, buflen, spec, precision, value);
308 
309 #ifdef DEBUG_SCALE_TICK_INTERVALS
310  SVDEBUG << "makeTick: spec = \"" << spec
311  << "\", prec = " << precision << ", value = " << value
312  << ", label = \"" << buffer << "\"" << endl;
313 #endif
314 
315  return Tick({ value, std::string(buffer) });
316  }
317 
318  static Ticks explode(Instruction instruction) {
319 
320 #ifdef DEBUG_SCALE_TICK_INTERVALS
321  SVDEBUG << "ScaleTickIntervals::explode:" << endl
322  << "initial = " << instruction.initial
323  << ", limit = " << instruction.limit
324  << ", spacing = " << instruction.spacing
325  << ", roundTo = " << instruction.roundTo
326  << ", display = " << instruction.display
327  << ", precision = " << instruction.precision
328  << ", logUnmap = " << instruction.logUnmap
329  << endl;
330 #endif
331 
332  if (instruction.spacing == 0.0) {
333  return {};
334  }
335 
336  double eps = 1e-7;
337  if (instruction.spacing < eps * 10.0) {
338  eps = instruction.spacing / 10.0;
339  }
340 
341  double max = instruction.limit;
342  int n = 0;
343 
344  Ticks ticks;
345 
346  while (true) {
347 
348  double value = instruction.initial + n * instruction.spacing;
349 
350  if (value >= max + eps) {
351  break;
352  }
353 
354  if (instruction.logUnmap) {
355  value = pow(10.0, value);
356  }
357 
358  double roundTo = instruction.roundTo;
359 
360  if (roundTo == 0.0 && value != 0.0) {
361  // We don't want the internal value secretly not
362  // matching the displayed one
363  roundTo =
364  pow(10, ceil(log10(fabs(value))) - instruction.precision);
365  }
366 
367  if (roundTo != 0.0) {
368  value = roundTo * round(value / roundTo);
369  }
370 
371  if (fabs(value) < eps) {
372  value = 0.0;
373  }
374 
375  ticks.push_back(makeTick(instruction.display,
376  instruction.precision,
377  value));
378  ++n;
379  }
380 
381  return ticks;
382  }
383 };
384 
385 #endif
static void mapRange(double &min, double &max, double thresh=-10)
Map a linear range onto a logarithmic range.
Definition: LogRange.cpp:24
static Ticks linear(Range r)
Return a set of ticks that divide the range r linearly into roughly r.n equal divisions, in such a way as to yield reasonably human-readable labels.
static Ticks logarithmic(Range r)
Return a set of ticks that divide the range r into roughly r.n logarithmic divisions, in such a way as to yield reasonably human-readable labels.
static Instruction linearInstruction(Range r)
static Tick makeTick(Display display, int precision, double value)
static Ticks explode(Instruction instruction)
#define SVDEBUG
Definition: Debug.h:106
static Instruction logInstruction(Range r)
static Ticks logTicks(Range r)
static Ticks logarithmicAlready(Range r)
Return a set of ticks that divide the range r into roughly r.n logarithmic divisions, on the asssumption that r.min and r.max already represent the logarithms of the boundary values rather than the values themselves.
std::vector< Tick > Ticks
static Ticks linearTicks(Range r)