A library for working with phylogenetic and population genetic data.
v0.32.0
tickmarks.cpp
Go to the documentation of this file.
1 /*
2  Genesis - A toolkit for working with phylogenetic data.
3  Copyright (C) 2014-2024 Lucas Czech
4 
5  This program is free software: you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation, either version 3 of the License, or
8  (at your option) any later version.
9 
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  GNU General Public License for more details.
14 
15  You should have received a copy of the GNU General Public License
16  along with this program. If not, see <http://www.gnu.org/licenses/>.
17 
18  Contact:
19  Lucas Czech <lucas.czech@sund.ku.dk>
20  University of Copenhagen, Globe Institute, Section for GeoGenetics
21  Oster Voldgade 5-7, 1350 Copenhagen K, Denmark
22 */
23 
32 
34 
35 #include <algorithm>
36 #include <cassert>
37 #include <cmath>
38 #include <stdexcept>
39 
40 namespace genesis {
41 namespace utils {
42 
43 // ================================================================================================
44 // Tickmarks
45 // ================================================================================================
46 
47 double Tickmarks::step_size( double interval_size, size_t target_steps )
48 {
49  // Adapted from
50  // http://stackoverflow.com/questions/361681/algorithm-for-nice-grid-line-intervals-on-a-graph
51 
52  if( target_steps == 0 ) {
53  throw std::invalid_argument( "Cannot calculate tickmark step size for 0 steps." );
54  }
55  if( interval_size <= 0.0 ) {
56  throw std::invalid_argument( "Cannot calculate tickmark step size for negative intervals." );
57  }
58 
59  // Calculate an initial guess at step size.
60  auto const step_guess = interval_size / static_cast<double>( target_steps );
61 
62  // Get the magnitude of the step size.
63  auto const mag = std::floor( std::log10( step_guess ));
64  auto const mag_pow = std::pow( 10, mag );
65 
66  // Calculate most significant digit (MSD) of the new step size.
67  auto mag_msd = static_cast<int>( step_guess / mag_pow + 0.5 );
68 
69  // Promote the MSD to either 1, 2, 5 or 10.
70  if( mag_msd > 5 ) {
71  mag_msd = 10;
72  } else if( mag_msd > 2 ) {
73  mag_msd = 5;
74  } else if( mag_msd > 1 ) {
75  mag_msd = 2;
76  } else {
77  assert( mag_msd == 1 );
78  }
79 
80  return static_cast<double>( mag_msd ) * mag_pow;
81 }
82 
83 std::vector<double> Tickmarks::linear_ticks( double min, double max, size_t target_steps )
84 {
85  // Prepare result.
86  auto res = std::vector<double>();
87 
88  if( max < min ) {
89  throw std::runtime_error( "Cannot calculate scale with max < min." );
90  }
91  auto const interval_size = max - min;
92 
93  // We want the ticks to be somewhat rounded, within some epsilon of the nice values that
94  // we are trying to create here, in order to avoid artifacts such as 1e-18 instead of 0.
95  // As the epsilon of double values is in the order of 1e-18 itself, we do e-15 here,
96  // which is large enough to avoid these trailing rounding errors, but small enough to not
97  // affect the numerical results too much, which is important for linear_labels() to work.
98  auto const rounding_accuracy = 15;
99 
100  // The case of 0 target steps can happen for example in SvgPalette.
101  // In that case, we only output min and max if needed, but not any inner tickmarks.
102  if( target_steps > 0 ) {
103  // Get step size.
104  auto const step_sz = step_size( interval_size, target_steps );
105 
106  // Calculate first tick position, so that it is the largest multiple of the step size
107  // that is below the min.
108  auto tick = step_sz * std::floor( min / step_sz );
109 
110  // Determine whether we want to start before or after the min.
111  if( ! undershoot_at_min ) {
112  tick += step_sz;
113  }
114 
115  // Add ticks to the list.
116  while( tick <= max ) {
117  res.push_back( round_to( tick, rounding_accuracy ));
118  tick += step_sz;
119  }
120 
121  // Determine whether we want to stop before or after the max.
122  if( overshoot_at_max ) {
123  res.push_back( round_to( tick, rounding_accuracy ));
124  }
125  }
126 
127  // Add min and max if needed.
128  if( include_min ) {
129  res.push_back( round_to( min, rounding_accuracy ));
130  }
131  if( include_max ) {
132  res.push_back( round_to( max, rounding_accuracy ));
133  }
134 
135  // Cleanup duplicate entries and those that are close by. We do not need ticks that are
136  // too close to each other.
137  // It is easier to do this than to check for duplicate entries in each addition step...
138  std::sort( res.begin(), res.end() );
139  auto uniq_end = std::unique( res.begin(), res.end(), [&]( double lhs, double rhs ){
140  return almost_equal_relative( lhs, rhs, relative_epsilon );
141  });
142  res.erase( uniq_end, res.end() );
143 
144  return res;
145 }
146 
147 std::vector<Tickmarks::LabeledTick> Tickmarks::linear_labels(
148  double min, double max, size_t target_steps
149 ) {
150  auto res = std::vector<LabeledTick>();
151 
152  auto ticks = linear_ticks( min, max, target_steps );
153  auto range = max - min;
154 
155  for( auto const& tick : ticks ) {
156  auto rel_pos = ( tick - min ) / range;
157  res.emplace_back( rel_pos, tick );
158  }
159  return res;
160 }
161 
162 std::vector<Tickmarks::LabeledTick> Tickmarks::logarithmic_labels( double min, double max, double base )
163 {
164  auto res = std::vector<LabeledTick>();
165 
166  if( min <= 0.0 ) {
167  throw std::runtime_error( "Cannot calculate logarithmic scale for negative values." );
168  }
169  if( min >= max ) {
170  throw std::runtime_error( "Cannot calculate scale with min >= max." );
171  }
172  if( base < 0.0 ) {
173  throw std::runtime_error( "Cannot calculate logarithmic scale with negative base." );
174  }
175 
176  // Start at a power below min.
177  double exp_i = std::floor( std::log( min ) / std::log( base ) );
178 
179  // Determine whether we want to start before or after the min.
180  if( ! undershoot_at_min ) {
181  exp_i += 1.0;
182  }
183 
184  // Constants.
185  auto const lg_min = std::log( min ) / std::log( base );
186  auto const lg_max = std::log( max ) / std::log( base );
187 
188  // Either stop at max or do one more loop if we want to overshoot.
189  auto const lim = lg_max + ( overshoot_at_max ? 1.0 : 0.0 );
190 
191  while( exp_i <= lim ) {
192  auto const rel_pos = ( exp_i - lg_min ) / ( lg_max - lg_min );
193  res.emplace_back( rel_pos, std::pow( base, exp_i ) );
194 
195  // Next order of magnitude.
196  exp_i += 1.0;
197  }
198 
199  // Add min and max if needed.
200  if( include_min ) {
201  res.emplace_back( 0.0, min );
202  }
203  if( include_max ) {
204  res.emplace_back( 1.0, max );
205  }
206 
207  // Cleanup duplicate entries and those that are close by. We do not need ticks that are
208  // too close to each other.
209  // It is easier to do this than to check for duplicate entries in each addition step...
210  std::sort( res.begin(), res.end(), []( LabeledTick const& lhs, LabeledTick const& rhs ){
211  return lhs.relative_position < rhs.relative_position;
212  });
213  auto uniq_end = std::unique( res.begin(), res.end(),
214  [&]( LabeledTick const& lhs, LabeledTick const& rhs ){
215  return almost_equal_relative(
216  lhs.relative_position,
217  rhs.relative_position,
218  relative_epsilon
219  );
220  }
221  );
222  res.erase( uniq_end, res.end() );
223 
224  return res;
225 }
226 
227 } // namespace utils
228 } // namespace genesis
common.hpp
genesis::utils::Tickmarks::linear_labels
std::vector< LabeledTick > linear_labels(double min, double max, size_t target_steps)
Return a set of labels with relative positions between min and max, where the labels correspond to th...
Definition: tickmarks.cpp:147
genesis::utils::Tickmarks::undershoot_at_min
bool undershoot_at_min
Should the lowest value in the resulting list of ticks be below the provided min value (true) or not ...
Definition: tickmarks.hpp:147
genesis::utils::Tickmarks::linear_ticks
std::vector< double > linear_ticks(double min, double max, size_t target_steps)
Calculate a set of ticks that linearily span from min to max in approximately target_steps many steps...
Definition: tickmarks.cpp:83
genesis::utils::Tickmarks::logarithmic_labels
std::vector< LabeledTick > logarithmic_labels(double min, double max, double base=10.0)
Return a set of labels with relative positions between min and max, where the labels correspond to lo...
Definition: tickmarks.cpp:162
genesis
Container namespace for all symbols of genesis in order to keep them separate when used as a library.
Definition: placement/formats/edge_color.cpp:42
genesis::utils::Tickmarks::include_min
bool include_min
Should the provided min value be included in the resulting list of ticks or not.
Definition: tickmarks.hpp:136
tickmarks.hpp
genesis::utils::Tickmarks::overshoot_at_max
bool overshoot_at_max
Should the highest value in the resulting list of ticks be above the provided max value (true) or not...
Definition: tickmarks.hpp:153
genesis::utils::Tickmarks::LabeledTick
Definition: tickmarks.hpp:56
genesis::utils::Tickmarks::include_max
bool include_max
Should the provided max value be included in the resulting list of ticks or not.
Definition: tickmarks.hpp:141
genesis::utils::Tickmarks::step_size
static double step_size(double interval_size, size_t target_steps)
Calculate a step size that fills the interval_size in approximately target_steps many steps.
Definition: tickmarks.cpp:47
genesis::utils::round_to
double round_to(double x, size_t accuracy_order)
Retun the value of x, rounded to the decimal digit given by accuracy_order.
Definition: common.hpp:175