Module: CTioga::Utils
- Included in:
- EdgesAndAxes::Axis, PlotMaker
- Defined in:
- lib/CTioga/utils.rb,
lib/CTioga/partition.rb,
lib/CTioga/boundaries.rb
Overview
A module for small convenience functions.
Defined Under Namespace
Classes: Boundaries, NaturalDistance
Constant Summary collapse
- Locations =
Converts a location into the index of the corresponding coordinates…
{ Tioga::FigureConstants::LEFT => 0, Tioga::FigureConstants::RIGHT => 1, Tioga::FigureConstants::TOP => 2, Tioga::FigureConstants::BOTTOM => 3, }
- FrameNames =
{ :left => 0, :right => 1, :bottom => 2, :top => 3 }
- NaturalDistances =
Our natural way to split decades
Dobjects::Dvector[1, 2, 2.5, 5, 10]
- NaturalDistancesNonLinear =
Our natural way to split decades - except that all successive element now divide each other.
Class Method Summary collapse
-
.apply_margin_to_frame(frame, margin) ⇒ Object
A function that ‘applies’ a margin specification (like for show_plot_with_legend) to a frame spec (left,right,top,bottom).
-
.compose_margins(m1, m2) ⇒ Object
Compose two margins (in the form of arrays): you get the m2 expressed in the same frame as m1, but taken relative to m1.
-
.frame_to_array(hash, format = '%s') ⇒ Object
Converting a boundary hash to an array.
- .frames_str(t) ⇒ Object
-
.framespec_str(a) ⇒ Object
Returns a frame specification from a classical array.
-
.inset_margins(spec) ⇒ Object
A function that transforms an inset/legend specification into margins.
-
.interpret_arg(arg, hash) ⇒ Object
A function that interprets a string according to a hash, and returns either what was found in the hash, or the result of the given block if that was not found – or just the original string itself when no block was given and no corresponding hash element was found.
- .location_index(d) ⇒ Object
- .margin_hash(a) ⇒ Object
-
.partition_nonlinear(to, from, x1, x2, nb) ⇒ Object
Attempts to partition the segment image of min, max by the Proc object to into at most nb elements.
-
.partition_segment(min, max, nb) ⇒ Object
Attempts to partition the given segment in at most nb segments of equal size.
-
.side(spec) ⇒ Object
Returns the position corresponding to the side:.
-
.side?(spec) ⇒ Boolean
Returns true if the spec is left, right, top or bottom.
Instance Method Summary collapse
-
#safe_float(a) ⇒ Object
A function that takes care of converting the given parameter to a float, while ensuring that it was decently taken care of.
Class Method Details
.apply_margin_to_frame(frame, margin) ⇒ Object
A function that ‘applies’ a margin specification (like for show_plot_with_legend) to a frame spec (left,right,top,bottom).
The margin spec has to contain all plot_*_margin things.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/CTioga/utils.rb', line 68 def self.apply_margin_to_frame(frame, margin) width = frame[1] - frame[0] height = frame[2] - frame[3] dest = [ frame[0] + width * (margin['plot_left_margin'] || margin['left']), frame[1] - width * (margin['plot_right_margin']|| margin['right']), frame[2] - height * (margin['plot_top_margin'] || margin['top']), frame[3] + height * (margin['plot_bottom_margin'] || margin['bottom']) ] return dest end |
.compose_margins(m1, m2) ⇒ Object
Compose two margins (in the form of arrays): you get the m2 expressed in the same frame as m1, but taken relative to m1.
49 50 51 52 53 54 55 56 57 58 |
# File 'lib/CTioga/boundaries.rb', line 49 def self.compose_margins(m1, m2) width = 1 - m1[0] - m1[1] height = 1 - m1[2] - m1[3] return [ m1[0] + m2[0] * width, m1[1] + m2[1] * width, m1[2] + m2[2] * height, m1[3] + m2[3] * height ] end |
.frame_to_array(hash, format = '%s') ⇒ Object
Converting a boundary hash to an array
41 42 43 44 45 |
# File 'lib/CTioga/boundaries.rb', line 41 def self.frame_to_array(hash, format = '%s') return %w(left right top bottom).map {|x| sprintf(format,x) }.map do |f| hash[f] end end |
.frames_str(t) ⇒ Object
47 48 49 50 |
# File 'lib/CTioga/utils.rb', line 47 def self.frames_str(t) return framespec_str([t.frame_left, t.frame_right, t.frame_top, t.frame_bottom]) end |
.framespec_str(a) ⇒ Object
Returns a frame specification from a classical array
42 43 44 45 |
# File 'lib/CTioga/utils.rb', line 42 def self.framespec_str(a) return "top : #{a[2]} bottom: #{a[3]} " + "left : #{a[0]} right: #{a[1]}" end |
.inset_margins(spec) ⇒ Object
A function that transforms an inset/legend specification into margins. Specifications understood are:
-
x,y:w(xh) : box centered on x,y of size w x h (or w x w if h is omitted)
-
x1,y1;x2,y2 : the exact box
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/CTioga/boundaries.rb', line 185 def self.inset_margins(spec) case spec when /(.*),(.*):([^x]*)(?:x(.*))?/ x = $1.to_f; y = $2.to_f; w = $3.to_f h = ($4 || $3).to_f margins = [x - w/2, 1 - (x + w/2), 1 - (y+h/2), y - h/2] when /(.*),(.*);(.*),(.*)/ x1 = $1.to_f; y1 = $2.to_f; x2 = $3.to_f; y2 = $4.to_f; left = [x1, x2].min right = [x1, x2].max top = [y1, y2].max bottom = [y1, y2].min margins = [left, 1 - right, 1 - top, bottom] when /(.*)x(.*)([+-])(.*)([+-])(.*)/ # X geometry-like specification w = $1.to_f; h = $2.to_f if $3 == '+' # Left left = $4.to_f right = left + w else # Right right = $4.to_f left = right - w end if $5 == '+' # Top top = $6.to_f bottom = top - h else # Bottom bottom = $6.to_f top = bottom + h end margins = [left, 1 - right, 1 - top, bottom] else raise "Incorrect inset specification #{spec}" end return margins end |
.interpret_arg(arg, hash) ⇒ Object
A function that interprets a string according to a hash, and returns either what was found in the hash, or the result of the given block if that was not found – or just the original string itself when no block was given and no corresponding hash element was found. Correspondance is done with the === operator.
Actually, the first argument doesn’t need to be a string.
28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/CTioga/utils.rb', line 28 def self.interpret_arg(arg, hash) for key, val in hash if key === arg return val end end if block_given? return yield(arg) else return arg end end |
.location_index(d) ⇒ Object
108 109 110 |
# File 'lib/CTioga/utils.rb', line 108 def self.location_index(d) return Locations[d] end |
.margin_hash(a) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/CTioga/utils.rb', line 85 def self.margin_hash(a) if Hash === a return a else return { 'left' => a[0], 'right' => a[1], 'top' => a[2], 'bottom' => a[3] } end end |
.partition_nonlinear(to, from, x1, x2, nb) ⇒ Object
Attempts to partition the segment image of min, max by the Proc object to into at most nb elements. The reverse of the transformation, from, has to be provided.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/CTioga/partition.rb', line 177 def self.partition_nonlinear(to, from, x1, x2, nb) x1, x2 = x2, x1 if x1 > x2 if x1.nan? or x2.nan? return [0] * nb elsif x1 == x2 # Nothing to do return self.partition_segment(x1 * 0.7, x2 * 1.3, nb) end xdist = x2 - x1 xdist_min = xdist/(nb + 1.0) # Why + 1.0 ? To account # for the space that could be left on the side. y1 = to.call(x1) y2 = to.call(x2) # Make sure y1 < y2 y1, y2 = y2, y1 if y1 > y2 # We first need to check if the linear partitioning of # the target segment could be enough: candidate = self.partition_segment(y1, y2, nb) candidate_real = candidate.map(&from) # We inspect the segment: if one of the length deviates from the # average expected by more than 25%, we drop it length = [] p candidate_real, xdist_min 0.upto(candidate.size - 2) do |i| length << (candidate_real[i+1] - candidate_real[i]).abs/(xdist_min) end p length # If everything stays within 25% off, we keep that if length.min > 0.75 and length.max < 1.7 return candidate end # We start with a geometric measure of the distance, that # will most likely scale better: ydist = y1 * (y2/y1).abs ** (1/(nb + 1.0)) cur_dist = NaturalDistance.new(ydist) retval = [] cur_y = y1 # This flag is necessary to avoid infinite loops last_was_decrease = false distance_unchanged = 0 last_real_distance = false while cur_y < y2 candidates = cur_dist.to_next_decade(cur_y) # We now evaluate the distance in real real_distance = (from.call(cur_y) - from.call(candidates.last)).abs/ candidates.size if last_real_distance && (real_distance == last_real_distance) distance_unchanged += 1 else distance_unchanged = 0 end # p [:cur_y=, cur_y, :y2=, y2, :real_distance, real_distance, # :distance=, cur_dist, :xdist_min, xdist_min, # :candidates=, *candidates] if (real_distance > 1.25 * xdist_min) && (distance_unchanged < 3) cur_dist.decrease last_was_decrease = true elsif real_distance < 0.75 * xdist_min && !last_was_decrease && (distance_unchanged < 3) && candidates.last <= 10 * y2 cur_dist.increase last_was_decrease = false else retval += candidates cur_y = candidates.last last_was_decrease = false end last_real_distance = real_distance end # We need to select them so return retval.select do |y| y >= y1 and y <= y2 end end |
.partition_segment(min, max, nb) ⇒ Object
Attempts to partition the given segment in at most nb segments of equal size. The segments don’t necessarily start on the edge of the original segment
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/CTioga/partition.rb', line 36 def self.partition_segment(min, max, nb) if min > max return partition_segment(max, min, nb) elsif min.nan? or max.nan? return [0] * nb elsif min == max return self.partition_segment(min * 0.7, min * 1.3, nb) end distance = max - min min_distance = distance/(nb + 1.0) # Why + 1.0 ? To account # for the space that could be left on the side. # The order of magnitude of the distance: order = min_distance.log10.floor # A distance which is within [1, 10 [ (but the latter is never reached. normalized_distance = min_distance * 10**(-order) final_distance = NaturalDistances.min_gt(normalized_distance) * 10**(order) # puts "Distance: #{distance} in #{nb} : #{normalized_distance} #{final_distance}" # We're getting closer now: we found the natural distance between # ticks. start = (min/final_distance).ceil * final_distance retval = [] val = start while val <= max retval << val # I use this to avoid potential cumulative addition # rounding errors val = start + final_distance * retval.size end return retval end |
.side(spec) ⇒ Object
Returns the position corresponding to the side:
127 128 129 |
# File 'lib/CTioga/utils.rb', line 127 def self.side(spec) return FrameNames[spec.to_sym] end |
.side?(spec) ⇒ Boolean
Returns true if the spec is left, right, top or bottom
122 123 124 |
# File 'lib/CTioga/utils.rb', line 122 def self.side?(spec) return FrameNames.key?(spec.to_sym) end |
Instance Method Details
#safe_float(a) ⇒ Object
A function that takes care of converting the given parameter to a float, while ensuring that it was decently taken care of. Returns false if that happens not to be a float.
55 56 57 58 59 60 61 62 |
# File 'lib/CTioga/utils.rb', line 55 def safe_float(a) return begin Float(a) rescue warn "Expected a float, but got '#{a}' instead" false end end |