annotate base/ScaleTickIntervals.h @ 1440:04caefd35391 streaming-csv-writer

Add failing test for non zero selection start
author Lucas Thompson <dev@lucas.im>
date Tue, 17 Apr 2018 10:03:50 +0100
parents 04ce84f21af3
children 48e9f538e6e9
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@1407 33 double min; // start of value range
Chris@1407 34 double max; // end of value range
Chris@1412 35 int n; // number of divisions (approximate only)
Chris@1407 36 };
Chris@1407 37
Chris@1407 38 struct Tick {
Chris@1407 39 double value; // value this tick represents
Chris@1407 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@1417 82 double initial; // value of first tick
Chris@1417 83 double limit; // max from original range
Chris@1417 84 double spacing; // increment between ticks
Chris@1417 85 double roundTo; // what all displayed values should be rounded to
Chris@1418 86 Display display; // whether to use fixed precision (%e, %f, or %g)
Chris@1417 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@1407 95 if (r.max < r.min) {
Chris@1417 96 return linearInstruction({ r.max, r.min, r.n });
Chris@1407 97 }
Chris@1418 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@1407 101
Chris@1407 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@1418 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@1418 185 if (r.n < 1) {
Chris@1418 186 return {};
Chris@1418 187 }
Chris@1418 188 if (r.max < r.min) {
Chris@1418 189 return logInstruction({ r.max, r.min, r.n });
Chris@1418 190 }
Chris@1418 191 if (r.max == r.min) {
Chris@1418 192 return { r.min, r.max, 1.0, r.min, display, 1, true };
Chris@1418 193 }
Chris@1418 194
Chris@1418 195 double inc = (r.max - r.min) / r.n;
Chris@1418 196
Chris@1418 197 double digInc = log10(inc);
Chris@1418 198 int precInc = int(floor(digInc));
Chris@1418 199 double roundTo = pow(10.0, precInc);
Chris@1418 200
Chris@1418 201 if (roundTo != 0.0) {
Chris@1418 202 inc = round(inc / roundTo) * roundTo;
Chris@1418 203 if (inc < roundTo) inc = roundTo;
Chris@1418 204 }
Chris@1418 205
Chris@1418 206 // if inc is close to giving us powers of two, nudge it
Chris@1418 207 if (fabs(inc - 0.301) < 0.01) {
Chris@1418 208 inc = log10(2.0);
Chris@1418 209 }
Chris@1418 210
Chris@1418 211 // smallest increment as displayed
Chris@1418 212 double minDispInc =
Chris@1418 213 LogRange::unmap(r.min + inc) - LogRange::unmap(r.min);
Chris@1418 214
Chris@1418 215 int prec = 1;
Chris@1418 216
Chris@1418 217 if (minDispInc > 0.0) {
Chris@1418 218 prec = int(floor(log10(minDispInc)));
Chris@1418 219 if (prec < 0) prec = -prec;
Chris@1418 220 }
Chris@1418 221
Chris@1418 222 if (r.max >= -2.0 && r.max <= 3.0 &&
Chris@1418 223 r.min >= -3.0 && r.min <= 3.0) {
Chris@1418 224 display = Fixed;
Chris@1418 225 if (prec == 0) prec = 1;
Chris@1418 226 }
Chris@1419 227
Chris@1418 228 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419 229 SVDEBUG << "ScaleTickIntervals: calculating logInstruction" << endl
Chris@1419 230 << "ScaleTickIntervals: min = " << r.min << ", max = " << r.max
Chris@1419 231 << ", n = " << r.n << ", inc = " << inc
Chris@1419 232 << ", minDispInc = " << minDispInc << ", digInc = " << digInc
Chris@1419 233 << endl;
Chris@1419 234 SVDEBUG << "ScaleTickIntervals: display = " << display
Chris@1419 235 << ", inc = " << inc << ", precInc = " << precInc
Chris@1419 236 << ", prec = " << prec << endl;
Chris@1419 237 SVDEBUG << "ScaleTickIntervals: roundTo = " << roundTo << endl;
Chris@1418 238 #endif
Chris@1418 239
Chris@1418 240 double min = r.min;
Chris@1418 241 if (inc != 0.0) {
Chris@1418 242 min = ceil(r.min / inc) * inc;
Chris@1418 243 if (min > r.max) min = r.max;
Chris@1418 244 }
Chris@1418 245
Chris@1418 246 return { min, r.max, inc, 0.0, display, prec, true };
Chris@1407 247 }
Chris@1407 248
Chris@1414 249 static Ticks linearTicks(Range r) {
Chris@1417 250 Instruction instruction = linearInstruction(r);
Chris@1417 251 Ticks ticks = explode(instruction);
Chris@1417 252 return ticks;
Chris@1414 253 }
Chris@1414 254
Chris@1414 255 static Ticks logTicks(Range r) {
Chris@1418 256 Instruction instruction = logInstruction(r);
Chris@1417 257 Ticks ticks = explode(instruction);
Chris@1417 258 return ticks;
Chris@1414 259 }
Chris@1418 260
Chris@1418 261 static Tick makeTick(Display display, int precision, double value) {
Chris@1422 262 if (value == -0.0) {
Chris@1422 263 value = 0.0;
Chris@1422 264 }
Chris@1414 265 const int buflen = 40;
Chris@1414 266 char buffer[buflen];
Chris@1414 267 snprintf(buffer, buflen,
Chris@1418 268 display == Auto ? "%.*g" :
Chris@1418 269 display == Fixed ? "%.*f" :
Chris@1418 270 "%.*e",
Chris@1414 271 precision, value);
Chris@1414 272 return Tick({ value, std::string(buffer) });
Chris@1414 273 }
Chris@1414 274
Chris@1417 275 static Ticks explode(Instruction instruction) {
Chris@1417 276
Chris@1411 277 #ifdef DEBUG_SCALE_TICK_INTERVALS
Chris@1419 278 SVDEBUG << "ScaleTickIntervals::explode:" << endl
Chris@1419 279 << "initial = " << instruction.initial
Chris@1419 280 << ", limit = " << instruction.limit
Chris@1419 281 << ", spacing = " << instruction.spacing
Chris@1419 282 << ", roundTo = " << instruction.roundTo
Chris@1419 283 << ", display = " << instruction.display
Chris@1419 284 << ", precision = " << instruction.precision
Chris@1419 285 << ", logUnmap = " << instruction.logUnmap
Chris@1419 286 << endl;
Chris@1411 287 #endif
Chris@1417 288
Chris@1417 289 if (instruction.spacing == 0.0) {
Chris@1417 290 return {};
Chris@1414 291 }
Chris@1417 292
Chris@1411 293 double eps = 1e-7;
Chris@1417 294 if (instruction.spacing < eps * 10.0) {
Chris@1417 295 eps = instruction.spacing / 10.0;
Chris@1411 296 }
Chris@1417 297
Chris@1417 298 double max = instruction.limit;
Chris@1412 299 int n = 0;
Chris@1417 300
Chris@1417 301 Ticks ticks;
Chris@1417 302
Chris@1412 303 while (true) {
Chris@1417 304 double value = instruction.initial + n * instruction.spacing;
Chris@1414 305 if (value >= max + eps) {
Chris@1412 306 break;
Chris@1412 307 }
Chris@1417 308 if (instruction.logUnmap) {
Chris@1414 309 value = pow(10.0, value);
Chris@1414 310 }
Chris@1418 311 if (instruction.roundTo != 0.0) {
Chris@1418 312 value = instruction.roundTo * round(value / instruction.roundTo);
Chris@1418 313 }
Chris@1418 314 ticks.push_back(makeTick(instruction.display,
Chris@1417 315 instruction.precision,
Chris@1417 316 value));
Chris@1412 317 ++n;
Chris@1407 318 }
Chris@1417 319
Chris@1417 320 return ticks;
Chris@1407 321 }
Chris@1407 322 };
Chris@1407 323
Chris@1407 324 #endif