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

Instance Method Summary collapse

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

Returns:

  • (Boolean)


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