annotate base/ScaleTickIntervals.h @ 1459:3a128665fa6f horizontal-scale

Fixes to logarithmic scale tick intervals. The approach here is not right, though -- and I've left in a failing test or two to remind me of that
author Chris Cannam
date Wed, 02 May 2018 14:17:10 +0100
parents 48e9f538e6e9
children 9528c73aa98c
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@1419 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@1429 86 Display display; // whether to use fixed precision (%e, %f, or %g)
Chris@1429 87 int precision; // number of dp (%f) or sf (%e)
Chris@1417 88 bool logUnmap; // true if values represent logs of display values
Chris@1417 89 };
Chris@1417 90
Chris@1417 91 static Instruction linearInstruction(Range r)
Chris@1414 92 {
Chris@1418 93 Display display = Auto;
Chris@1418 94
Chris@1429 95 if (r.max < r.min) {
Chris@1429 96 return linearInstruction({ r.max, r.min, r.n });
Chris@1429 97 }
Chris@1429 98 if (r.n < 1 || r.max == r.min) {
Chris@1418 99 return { r.min, r.min, 1.0, r.min, display, 1, false };
Chris@1418 100 }
Chris@1429 101
Chris@1429 102 double inc = (r.max - r.min) / r.n;
Chris@1408 103
Chris@1408 104 double digInc = log10(inc);
Chris@1408 105 double digMax = log10(fabs(r.max));
Chris@1408 106 double digMin = log10(fabs(r.min));
Chris@1408 107
Chris@1418 108 int precInc = int(floor(digInc));
Chris@1429 109 double roundTo = pow(10.0, precInc);
Chris@1408 110
Chris@1408 111 if (precInc > -4 && precInc < 4) {
Chris@1418 112 display = Fixed;
Chris@1418 113 } else if ((digMax >= -2.0 && digMax <= 3.0) &&
Chris@1408 114 (digMin >= -3.0 && digMin <= 3.0)) {
Chris@1418 115 display = Fixed;
Chris@1418 116 } else {
Chris@1418 117 display = Scientific;
Chris@1408 118 }
Chris@1408 119
Chris@1408 120 int precRange = int(ceil(digMax - digInc));
Chris@1408 121
Chris@1408 122 int prec = 1;
Chris@1408 123
Chris@1418 124 if (display == Fixed) {
Chris@1415 125 if (digInc < 0) {
Chris@1410 126 prec = -precInc;
Chris@1415 127 } else {
Chris@1410 128 prec = 0;
Chris@1408 129 }
Chris@1408 130 } else {
Chris@1408 131 prec = precRange;
Chris@1408 132 }
Chris@1408 133
Chris@1411 134 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419 135 SVDEBUG << "ScaleTickIntervals: calculating linearInstruction" << endl
Chris@1419 136 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
Chris@1419 137 << ", n = " << r.n << ", inc = " << inc << endl;
Chris@1419 138 SVDEBUG << "ScaleTickIntervals: digMax = " << digMax
Chris@1419 139 << ", digInc = " << digInc << endl;
Chris@1419 140 SVDEBUG << "ScaleTickIntervals: display = " << display
Chris@1419 141 << ", inc = " << inc << ", precInc = " << precInc
Chris@1419 142 << ", precRange = " << precRange
Chris@1419 143 << ", prec = " << prec << ", roundTo = " << roundTo
Chris@1419 144 << endl;
Chris@1411 145 #endif
Chris@1418 146
Chris@1418 147 double min = r.min;
Chris@1408 148
Chris@1418 149 if (roundTo != 0.0) {
Chris@1421 150 // Round inc to the nearest multiple of roundTo, and min
Chris@1421 151 // to the next multiple of roundTo up. The small offset of
Chris@1421 152 // eps is included to avoid inc of 2.49999999999 rounding
Chris@1421 153 // to 2 or a min of -0.9999999999 rounding to 0, both of
Chris@1421 154 // which would prevent some of our test cases from getting
Chris@1421 155 // the most natural results.
Chris@1421 156 double eps = 1e-8;
Chris@1421 157 inc = round(inc / roundTo + eps) * roundTo;
Chris@1418 158 if (inc < roundTo) inc = roundTo;
Chris@1421 159 min = ceil(min / roundTo - eps) * roundTo;
Chris@1418 160 if (min > r.max) min = r.max;
Chris@1421 161 if (min == -0.0) min = 0.0;
Chris@1421 162 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1421 163 SVDEBUG << "ScaleTickIntervals: rounded inc to " << inc
Chris@1421 164 << " and min to " << min << endl;
Chris@1421 165 #endif
Chris@1418 166 }
Chris@1407 167
Chris@1418 168 if (display == Scientific && min != 0.0) {
Chris@1413 169 double digNewMin = log10(fabs(min));
Chris@1413 170 if (digNewMin < digInc) {
Chris@1413 171 prec = int(ceil(digMax - digNewMin));
Chris@1413 172 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419 173 SVDEBUG << "ScaleTickIntervals: min is smaller than increment, adjusting prec to " << prec << endl;
Chris@1413 174 #endif
Chris@1413 175 }
Chris@1413 176 }
Chris@1413 177
Chris@1418 178 return { min, r.max, inc, roundTo, display, prec, false };
Chris@1418 179 }
Chris@1418 180
Chris@1418 181 static Instruction logInstruction(Range r)
Chris@1418 182 {
Chris@1418 183 Display display = Auto;
Chris@1418 184
Chris@1459 185 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 186 SVDEBUG << "ScaleTickIntervals::logInstruction: Range is "
Chris@1459 187 << r.min << " to " << r.max << endl;
Chris@1459 188 #endif
Chris@1459 189
Chris@1429 190 if (r.n < 1) {
Chris@1429 191 return {};
Chris@1429 192 }
Chris@1429 193 if (r.max < r.min) {
Chris@1429 194 return logInstruction({ r.max, r.min, r.n });
Chris@1429 195 }
Chris@1418 196 if (r.max == r.min) {
Chris@1418 197 return { r.min, r.max, 1.0, r.min, display, 1, true };
Chris@1418 198 }
Chris@1429 199
Chris@1429 200 double inc = (r.max - r.min) / r.n;
Chris@1418 201
Chris@1418 202 double digInc = log10(inc);
Chris@1418 203 int precInc = int(floor(digInc));
Chris@1459 204
Chris@1429 205 double roundTo = pow(10.0, precInc);
Chris@1418 206
Chris@1459 207 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 208 SVDEBUG << "ScaleTickIntervals::logInstruction: Naive increment is "
Chris@1459 209 << inc << ", of " << digInc << "-digit length" << endl;
Chris@1459 210 SVDEBUG << "ScaleTickIntervals::logInstruction: "
Chris@1459 211 << "So increment is precision " << precInc
Chris@1459 212 << ", yielding rounding for increment of "
Chris@1459 213 << roundTo << endl;
Chris@1459 214 #endif
Chris@1459 215
Chris@1418 216 if (roundTo != 0.0) {
Chris@1418 217 inc = round(inc / roundTo) * roundTo;
Chris@1418 218 if (inc < roundTo) inc = roundTo;
Chris@1459 219
Chris@1459 220 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 221 SVDEBUG << "ScaleTickIntervals::logInstruction: "
Chris@1459 222 << "Rounded increment to " << inc << endl;
Chris@1459 223 #endif
Chris@1418 224 }
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@1459 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@1418 237 // smallest increment as displayed
Chris@1418 238 double minDispInc =
Chris@1418 239 LogRange::unmap(r.min + inc) - LogRange::unmap(r.min);
Chris@1418 240
Chris@1459 241 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 242 SVDEBUG << "ScaleTickIntervals::logInstruction: "
Chris@1459 243 << "Smallest displayed increment is " << minDispInc << endl;
Chris@1459 244 #endif
Chris@1459 245
Chris@1418 246 int prec = 1;
Chris@1418 247
Chris@1418 248 if (minDispInc > 0.0) {
Chris@1459 249 prec = int(ceil(log10(minDispInc))) - 1;
Chris@1459 250 if (prec == 0) prec = 1;
Chris@1418 251 if (prec < 0) prec = -prec;
Chris@1459 252
Chris@1459 253 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 254 SVDEBUG << "ScaleTickIntervals::logInstruction: "
Chris@1459 255 << "Precision therefrom is " << prec << endl;
Chris@1459 256 #endif
Chris@1418 257 }
Chris@1418 258
Chris@1418 259 if (r.max >= -2.0 && r.max <= 3.0 &&
Chris@1418 260 r.min >= -3.0 && r.min <= 3.0) {
Chris@1418 261 display = Fixed;
Chris@1418 262 if (prec == 0) prec = 1;
Chris@1459 263
Chris@1459 264 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 265 SVDEBUG << "ScaleTickIntervals::logInstruction: "
Chris@1459 266 << "Min and max within modest range, adjusted precision to "
Chris@1459 267 << prec << " and display to Fixed" << endl;
Chris@1459 268 #endif
Chris@1418 269 }
Chris@1419 270
Chris@1418 271 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419 272 SVDEBUG << "ScaleTickIntervals: calculating logInstruction" << endl
Chris@1419 273 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
Chris@1419 274 << ", n = " << r.n << ", inc = " << inc
Chris@1419 275 << ", minDispInc = " << minDispInc << ", digInc = " << digInc
Chris@1419 276 << endl;
Chris@1419 277 SVDEBUG << "ScaleTickIntervals: display = " << display
Chris@1419 278 << ", inc = " << inc << ", precInc = " << precInc
Chris@1419 279 << ", prec = " << prec << endl;
Chris@1419 280 SVDEBUG << "ScaleTickIntervals: roundTo = " << roundTo << endl;
Chris@1418 281 #endif
Chris@1418 282
Chris@1429 283 double min = r.min;
Chris@1418 284 if (inc != 0.0) {
Chris@1418 285 min = ceil(r.min / inc) * inc;
Chris@1418 286 if (min > r.max) min = r.max;
Chris@1418 287 }
Chris@1418 288
Chris@1418 289 return { min, r.max, inc, 0.0, display, prec, true };
Chris@1407 290 }
Chris@1407 291
Chris@1414 292 static Ticks linearTicks(Range r) {
Chris@1417 293 Instruction instruction = linearInstruction(r);
Chris@1417 294 Ticks ticks = explode(instruction);
Chris@1417 295 return ticks;
Chris@1414 296 }
Chris@1414 297
Chris@1414 298 static Ticks logTicks(Range r) {
Chris@1418 299 Instruction instruction = logInstruction(r);
Chris@1417 300 Ticks ticks = explode(instruction);
Chris@1417 301 return ticks;
Chris@1414 302 }
Chris@1418 303
Chris@1418 304 static Tick makeTick(Display display, int precision, double value) {
Chris@1459 305
Chris@1422 306 if (value == -0.0) {
Chris@1422 307 value = 0.0;
Chris@1422 308 }
Chris@1459 309
Chris@1414 310 const int buflen = 40;
Chris@1414 311 char buffer[buflen];
Chris@1459 312
Chris@1459 313 if (display == Auto) {
Chris@1459 314
Chris@1459 315 int digits = (value != 0.0 ? int(ceil(log10(abs(value)))) : 0);
Chris@1459 316
Chris@1459 317 // This is not the same logic as %g uses for determining
Chris@1459 318 // whether to delegate to use scientific or fixed notation
Chris@1459 319
Chris@1459 320 if (digits < -3 || digits > 4) {
Chris@1459 321
Chris@1459 322 display = Auto; // delegate planning to %g
Chris@1459 323
Chris@1459 324 } else {
Chris@1459 325
Chris@1459 326 display = Fixed;
Chris@1459 327
Chris@1459 328 // in %.*f, the * indicates decimal places, not sig figs
Chris@1459 329 if (precision > digits) {
Chris@1459 330 precision -= digits;
Chris@1459 331 } else if (precision == digits) {
Chris@1459 332 precision = 1;
Chris@1459 333 } else if (precision + 1 < digits) {
Chris@1459 334 double r = pow(10, digits - precision - 1);
Chris@1459 335 value = r * round(value / r);
Chris@1459 336 precision = 0;
Chris@1459 337 } else {
Chris@1459 338 precision = 0;
Chris@1459 339 }
Chris@1459 340 }
Chris@1459 341 }
Chris@1459 342
Chris@1459 343 const char *spec = (display == Auto ? "%.*g" :
Chris@1459 344 display == Scientific ? "%.*e" :
Chris@1459 345 "%.*f");
Chris@1459 346
Chris@1459 347 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
Chris@1459 348
Chris@1459 349 snprintf(buffer, buflen, spec, precision, value);
Chris@1459 350
Chris@1459 351 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1459 352 SVDEBUG << "makeTick: spec = \"" << spec
Chris@1459 353 << "\", prec = " << precision << ", value = " << value
Chris@1459 354 << ", label = \"" << buffer << "\"" << endl;
Chris@1459 355 #endif
Chris@1459 356
Chris@1414 357 return Tick({ value, std::string(buffer) });
Chris@1414 358 }
Chris@1414 359
Chris@1417 360 static Ticks explode(Instruction instruction) {
Chris@1417 361
Chris@1411 362 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1429 363 SVDEBUG << "ScaleTickIntervals::explode:" << endl
Chris@1419 364 << "initial = " << instruction.initial
Chris@1419 365 << ", limit = " << instruction.limit
Chris@1419 366 << ", spacing = " << instruction.spacing
Chris@1419 367 << ", roundTo = " << instruction.roundTo
Chris@1419 368 << ", display = " << instruction.display
Chris@1419 369 << ", precision = " << instruction.precision
Chris@1419 370 << ", logUnmap = " << instruction.logUnmap
Chris@1419 371 << endl;
Chris@1411 372 #endif
Chris@1417 373
Chris@1417 374 if (instruction.spacing == 0.0) {
Chris@1417 375 return {};
Chris@1414 376 }
Chris@1417 377
Chris@1411 378 double eps = 1e-7;
Chris@1417 379 if (instruction.spacing < eps * 10.0) {
Chris@1417 380 eps = instruction.spacing / 10.0;
Chris@1411 381 }
Chris@1417 382
Chris@1417 383 double max = instruction.limit;
Chris@1412 384 int n = 0;
Chris@1417 385
Chris@1417 386 Ticks ticks;
Chris@1417 387
Chris@1412 388 while (true) {
Chris@1417 389 double value = instruction.initial + n * instruction.spacing;
Chris@1414 390 if (value >= max + eps) {
Chris@1412 391 break;
Chris@1412 392 }
Chris@1417 393 if (instruction.logUnmap) {
Chris@1414 394 value = pow(10.0, value);
Chris@1414 395 }
Chris@1418 396 if (instruction.roundTo != 0.0) {
Chris@1418 397 value = instruction.roundTo * round(value / instruction.roundTo);
Chris@1418 398 }
Chris@1429 399 ticks.push_back(makeTick(instruction.display,
Chris@1417 400 instruction.precision,
Chris@1417 401 value));
Chris@1412 402 ++n;
Chris@1429 403 }
Chris@1417 404
Chris@1417 405 return ticks;
Chris@1407 406 }
Chris@1407 407 };
Chris@1407 408
Chris@1407 409 #endif