annotate base/ScaleTickIntervals.h @ 1525:a92e94215863

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