module Rubyvis # Represents an abstract quantitative scale; a function that performs a # numeric transformation. This class is typically not used directly; see one of # the quantitative scale implementations (linear, log, root, etc.) # instead. A quantitative # scale represents a 1-dimensional transformation from a numeric domain of # input data [d0, d1] to a numeric range of # pixels [r0, r1]. In addition to # readability, scales offer several useful features: # #
1. The range can be expressed in colors, rather than pixels. For example: # # .fill_style(Scale.linear(0, 100).range("red", "green")) # # will fill the marks "red" on an input value of 0, "green" on an input value # of 100, and some color in-between for intermediate values. # #
2. The domain and range can be subdivided for a non-uniform # transformation. For example, you may want a diverging color scale that is # increasingly red for negative values, and increasingly green for positive # values: # # .fill_style(Scale.linear(-1, 0, 1).range("red", "white", "green")) # # The domain can be specified as a series of n monotonically-increasing # values; the range must also be specified as n values, resulting in # n - 1 contiguous linear scales. # #
3. Quantitative scales can be inverted for interaction. The # invert() method takes a value in the output range, and returns the # corresponding value in the input domain. This is frequently used to convert # the mouse location (see Mark#mouse) to a value in the input # domain. Note that inversion is only supported for numeric ranges, and not # colors. # #
4. A scale can be queried for reasonable "tick" values. The ticks() # method provides a convenient way to get a series of evenly-spaced rounded # values in the input domain. Frequently these are used in conjunction with # Rule to display tick marks or grid lines. # #
5. A scale can be "niced" to extend the domain to suitable rounded # numbers. If the minimum and maximum of the domain are messy because they are # derived from data, you can use nice() to round these values down and # up to even numbers. # # @see Scale.linear # @see Scale.log # @see Scale.root class Scale::Quantitative include Rubyvis::Scale attr_reader :l # Returns a default quantitative, linear, scale for the specified domain. The # arguments to this constructor are optional, and equivalent to calling # domain. The default domain and range are [0,1]. # # This constructor is typically not used directly; see one of the # quantitative scale implementations instead. # @param {number...} domain... optional domain values. def initialize(*args) @d=[0,1] # domain @l=[0,1] # transformed domain @r=[0,1] # default range @i=[Rubyvis.identity] # default interpolator @type=:to_f # default type @n=false @f=Rubyvis.identity # default forward transformation @g=Rubyvis.identity @tick_format=lambda {|x| if x.is_a? Numeric ((x.to_f-x.to_i==0) ? x.to_i : x.to_f).to_s else "" end } domain(*args) end # Deprecated def new_date(x=nil) # :nodoc: x.nil? ? Time.new() : Time.at(x) end # Return # lambda {|d| scale_object.scale(d)} # Useful as value on dynamic properties # scale=Rubyvis.linear(0,1000) # bar.width(scale) # is the same as # bar.width(lambda {|x| scale.scale(x)}) def to_proc that=self lambda {|*args| args[0] ? that.scale(args[0]) : nil } end # Transform value +x+ according to domain and range def scale(x) return nil if x.nil? x=x.to_f j=Rubyvis.search(@d, x) j=-j-2 if (j<0) j=[0,[@i.size-1,j].min].max # p @l # puts "Primero #{j}: #{@f.call(x) - @l[j]}" # puts "Segundo #{(@l[j + 1] - @l[j])}" @i[j].call((@f.call(x) - @l[j]) .quo(@l[j + 1] - @l[j])); end alias :[] :scale def transform(forward, inverse) @f=lambda {|x| @n ? -forward.call(-x) : forward.call(x); } @g=lambda {|y| @n ? -inverse.call(-y) : inverse.call(y); } @l=@d.map{|v| @f.call(v)} self end private :transform # Sets or gets the input domain. This method can be invoked several ways: # #
1. domain(min, ..., max) # #
Specifying the domain as a series of numbers is the most explicit and # recommended approach. Most commonly, two numbers are specified: the minimum # and maximum value. However, for a diverging scale, or other subdivided # non-uniform scales, multiple values can be specified. Values can be derived # from data using Rubyvis.min and Rubyvis.max. For example: # # .domain(0, Rubyvis.max(array)) # # An alternative method for deriving minimum and maximum values from data # follows. # #
2. domain(array, minf, maxf) # #
When both the minimum and maximum value are derived from data, the # arguments to the domain method can be specified as the array of # data, followed by zero, one or two accessor functions. For example, if the # array of data is just an array of numbers: # # .domain(array) # # On the other hand, if the array elements are objects representing stock # values per day, and the domain should consider the stock's daily low and # daily high: # # .domain(array, lambda {|d| d.low}, lambda {|d| d.high}) # # The first method of setting the domain is preferred because it is more # explicit; setting the domain using this second method should be used only # if brevity is required. # #
3. domain() # #
Invoking the domain method with no arguments returns the # current domain as an array of numbers. def domain(*arguments) array,min,max=arguments o=nil if (arguments.size>0) if array.is_a? Array min = Rubyvis.identity if (arguments.size < 2) max = min if (arguments.size < 3) o = [array[0]].min if array.size>0 @d = array.size>0 ? [Rubyvis.min(array, min), Rubyvis.max(array, max)] : [] else o = array @d = arguments.map {|i| i.to_f} end if !@d.size @d = [-Infinity, Infinity]; elsif (@d.size == 1) @d = [@d.first, @d.first] end @n = (@d.first.to_f<0 or @d.last.to_f<0) @l=@d.map{|v| @f.call(v)} @type = (o.is_a? Time) ? :time : :number; return self end # TODO: Fix this. @d.map{|v| case @type when :number v.to_f when :time Time.at(v) else v end } end # Sets or gets the output range. This method can be invoked several ways: # #
1. range(min, ..., max) # #
The range may be specified as a series of numbers or colors. Most # commonly, two numbers are specified: the minimum and maximum pixel values. # For a color scale, values may be specified as {@link Rubyvis.Color}s or # equivalent strings. For a diverging scale, or other subdivided non-uniform # scales, multiple values can be specified. For example: # # .range("red", "white", "green") # #
Currently, only numbers and colors are supported as range values. The # number of range values must exactly match the number of domain values, or # the behavior of the scale is undefined. # #
2. range() # #
Invoking the range method with no arguments returns the current
# range as an array of numbers or colors.
# :call-seq:
# range(min,...,max)
# range()
def range(*arguments)
if (arguments.size>0)
@r = arguments.dup
if (@r.size==0)
@r = [-Infinity, Infinity];
elsif (@r.size == 1)
@r = [@r[0], @r[0]]
end
@i=(@r.size-1).times.map do |j|
Rubyvis::Scale.interpolator(@r[j], @r[j + 1]);
end
return self
end
@r
end
def invert(y)
j=Rubyvis.search(@r, y)
j=-j-2 if j<0
j = [0, [@i.size - 1, j].min].max
val=@g.call(@l[j] + (y - @r[j]).quo(@r[j + 1] - @r[j]) * (@l[j + 1] - @l[j]))
@type==:time ? Time.at(val) : val
end
def type(v=nil)
return @type if v.nil?
case @type
when Numeric
v.to_f
when Date
raise "Not implemented yet"
end
end
def ticks_floor(d,prec) # :nodoc:
ar=d.to_a
#p ar
# [ sec, min, hour, day, month, year, wday, yday, isdst, zone ]
case(prec)
when 31536e6
ar[4]=1
when 2592e6
ar[3]=1
when 6048e5
ar[3]=ar[3]-ar[6] if (prec == 6048e5)
when 864e5
ar[2]=0
when 36e5
ar[1]=0
when 6e4
ar[0]=0
when 1e3
# do nothing
end
to_date(ar)
end
private :ticks_floor
def to_date(d) # :nodoc:
Time.utc(*d)
end
# Returns an array of evenly-spaced, suitably-rounded values in the input
# domain. This method attempts to return between 5 and 10 tick values. These
# values are frequently used in conjunction with Rule to display
# tick marks or grid lines.
#
# @todo: fix for dates and n>10
def ticks(*arguments) # :args: (number_of_ticks=nil)
m = arguments[0]
start = @d.first
_end = @d.last
reverse = _end < start
min = reverse ? _end : start
max = reverse ? start : _end
span = max - min
# Special case: empty, invalid or infinite span.
if (!span or (span.is_a? Float and span.infinite?))
@tick_format= Rubyvis.Format.date("%x") if (@type == newDate)
return [type(min)];
end
#/* Special case: dates. */
if (@type == :time)
#/* Floor the date d given the precision p. */
precision, format, increment, step = 1,1,1,1
if (span >= 3 * 31536e6 / 1000.0)
precision = 31536e6
format = "%Y"
increment = lambda {|d| Time.at(d.to_f+(step*365*24*60*60)) }
elsif (span >= 3 * 2592e6 / 1000.0)
precision = 2592e6;
format = "%m/%Y";
increment = lambda {|d| Time.at(d.to_f+(step*30*24*60*60)) }
elsif (span >= 3 * 6048e5 / 1000.0)
precision = 6048e5;
format = "%m/%d";
increment = lambda {|d| Time.at(d.to_f+(step*7*24*60*60)) }
elsif (span >= 3 * 864e5 / 1000.0)
precision = 864e5;
format = "%m/%d";
increment = lambda {|d| Time.at(d.to_f+(step*24*60*60)) }
elsif (span >= 3 * 36e5 / 1000.0)
precision = 36e5;
format = "%I:%M %p";
increment = lambda {|d| Time.at(d.to_f+(step*60*60)) }
elsif (span >= 3 * 6e4 / 1000.0 )
precision = 6e4;
format = "%I:%M %p";
increment = lambda {|d| Time.at(d.to_f+(step*60)) }
elsif (span >= 3 * 1e3 / 1000.0)
precision = 1e3;
format = "%I:%M:%S";
increment = lambda {|d| Time.at(d.to_f+(step)) }
else
precision = 1;
format = "%S.%Qs";
increment = lambda {|d| Time.at(d.to_f+(step/1000.0)) }
end
@tick_format = Rubyvis.Format.date(format);
date = Time.at(min.to_f)
dates = []
date = ticks_floor(date,precision)
# If we'd generate too many ticks, skip some!.
n = span / (precision/1000.0)
# FIX FROM HERE
if (n > 10)
case (precision)
when 36e5
step = (n > 20) ? 6 : 3;
date.setHours(Math.floor(date.getHours() / step) * step);
when 2592e6
step = 3; # seasons
ar=date.to_a
ar[4]=(date.month/step.to_f).floor*step
date=to_date(ar)
when 6e4
step = (n > 30) ? 15 : ((n > 15) ? 10 : 5);
date.setMinutes(Math.floor(date.getMinutes() / step) * step);
when 1e3
step = (n > 90) ? 15 : ((n > 60) ? 10 : 5);
date.setSeconds(Math.floor(date.getSeconds() / step) * step);
when 1
step = (n > 1000) ? 250 : ((n > 200) ? 100 : ((n > 100) ? 50 : ((n > 50) ? 25 : 5)));
date.setMilliseconds(Math.floor(date.getMilliseconds() / step) * step);
else
step = Rubyvis.logCeil(n / 15, 10);
if (n / step < 2)
step =step.quo(5)
elsif (n / step < 5)
step = step.quo(2)
end
date.setFullYear((date.getFullYear().quo(step)).floor * step);
end
end
# END FIX
while (true)
date=increment.call(date)
break if (date.to_f > max.to_f)
dates.push(date)
end
return reverse ? dates.reverse() : dates;
end
# Normal case: numbers.
m = 10 if (arguments.size==0)
step = Rubyvis.log_floor(span.quo(m), 10)
err = m.quo(span.quo(step))
if (err <= 0.15)
step = step*10
elsif (err <= 0.35)
step = step*5
elsif (err <= 0.75)
step = step*2
end
start = (min.quo(step)).ceil * step
_end = (max.quo(step)).floor * step
@tick_format= Rubyvis.Format.number.fraction_digits([0, -(Rubyvis.log(step, 10) + 0.01).floor].max).to_proc
ticks = Rubyvis.range(start, _end + step, step);
return reverse ? ticks.reverse() : ticks;
end
# Returns a Proc that formats the specified tick value using the appropriate precision, based on
# the step interval between tick marks. If ticks() has not been called,
# the argument is converted to a string, but no formatting is applied.
# scale.tick_format.call(value)
#
def tick_format
@tick_format
end
# "Nices" this scale, extending the bounds of the input domain to
# evenly-rounded values. Nicing is useful if the domain is computed
# dynamically from data, and may be irregular. For example, given a domain of
# [0.20147987687960267, 0.996679553296417], a call to nice() might
# extend the domain to [0.2, 1].
#
# This method must be invoked each time after setting the domain.
def nice
return self if @d.size!=2
start=@d.first
_end=@d[@d.size-1]
reverse=_end