annotate base/ScaleTickIntervals.h @ 1833:21c792334c2e sensible-delimited-data-strings

Rewrite all the DelimitedDataString stuff so as to return vectors of individual cell strings rather than having the classes add the delimiters themselves. Rename accordingly to names based on StringExport. Take advantage of this in the CSV writer code so as to properly quote cells that contain delimiter characters.
author Chris Cannam
date Fri, 03 Apr 2020 17:11:05 +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