dyi 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/dyi/canvas.rb ADDED
@@ -0,0 +1,202 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ module DYI #:nodoc:
23
+
24
+ class Canvas < GraphicalElement
25
+ IMPLEMENT_ATTRIBUTES = [:view_box, :preserve_aspect_ratio]
26
+ attr_length :width, :height
27
+ attr_reader *IMPLEMENT_ATTRIBUTES
28
+ attr_reader :child_elements
29
+ # @since 1.0.0
30
+ attr_reader :event_listeners, :stylesheets
31
+
32
+ def initialize(width, height,
33
+ real_width = nil, real_height = nil,
34
+ preserve_aspect_ratio='none', options={})
35
+ self.width = width
36
+ self.height = height
37
+ @view_box = "0 0 #{width} #{height}"
38
+ @preserve_aspect_ratio = preserve_aspect_ratio
39
+ @child_elements = []
40
+ @scripts = []
41
+ @event_listeners = {}
42
+ @stylesheets = []
43
+ @seed_of_id = -1
44
+ @receive_event = false
45
+ self.css_class = options[:css_class]
46
+ self.real_width = real_width
47
+ self.real_height = real_height
48
+ end
49
+
50
+ def real_width
51
+ @real_width || width
52
+ end
53
+
54
+ def real_width=(width)
55
+ @real_width = Length.new_or_nil(width)
56
+ end
57
+
58
+ def real_height
59
+ @real_height || height
60
+ end
61
+
62
+ def real_height=(height)
63
+ @real_height = Length.new_or_nil(height)
64
+ end
65
+
66
+ # This method is depricated; use Canvas#root_element?
67
+ # @deprecated
68
+ def root_node?
69
+ msg = [__FILE__, __LINE__, ' waring']
70
+ msg << ' DYI::Canvas#root_node? is depricated; use DYI::Canvas#root_element?'
71
+ warn(msg.join(':'))
72
+ true
73
+ end
74
+
75
+ # @since 1.0.0
76
+ def root_element?
77
+ true
78
+ end
79
+
80
+ # Returns the canvas where the shape is drawn
81
+ # @return [Canvas] the canvas where the shape is drawn
82
+ # @since 1.0.0
83
+ def canvas
84
+ self
85
+ end
86
+
87
+ # @since 1.0.0
88
+ def event_listeners
89
+ @event_listeners ||= {}
90
+ end
91
+
92
+ def write_as(formatter, io=$>)
93
+ formatter.write_canvas(self, io)
94
+ end
95
+
96
+ def save(file_name, format=nil, options={})
97
+ get_formatter(format).save(file_name, options)
98
+ end
99
+
100
+ def puts_in_io(format=nil, io=$>)
101
+ get_formatter(format).puts(io)
102
+ end
103
+
104
+ def string(format=nil)
105
+ get_formatter(format).string
106
+ end
107
+
108
+ def attributes #:nodoc:
109
+ IMPLEMENT_ATTRIBUTES.inject({}) do |hash, attribute|
110
+ variable_name = '@' + attribute.to_s.split(/(?=[A-Z])/).map{|str| str.downcase}.join('_')
111
+ value = instance_variable_get(variable_name)
112
+ hash[attribute] = value.to_s if value
113
+ hash
114
+ end
115
+ end
116
+
117
+ # Create a new id for a descendant element
118
+ # @return [String] new id for a descendant element
119
+ # @since 1.0.0
120
+ def publish_shape_id
121
+ 'elm%04d' % (@seed_of_id += 1)
122
+ end
123
+
124
+ # @since 1.0.0
125
+ def set_event(event)
126
+ super
127
+ @receive_event = true
128
+ end
129
+
130
+ # @return [Boolean] whether event is set to the shape
131
+ # @since 1.0.0
132
+ def receive_event?
133
+ @receive_event
134
+ end
135
+
136
+ # @since 1.0.0
137
+ def scripts
138
+ @init_script ? [@init_script].push(*@scripts) : @scripts
139
+ end
140
+
141
+ # @since 1.0.0
142
+ def add_script(script_body, content_type = 'application/ecmascript')
143
+ @scripts << Script::SimpleScript.new(script_body, content_type)
144
+ end
145
+
146
+ # @since 1.0.0
147
+ def reference_script_file(reference_path, content_type = 'application/ecmascript')
148
+ @scripts << Script::ScriptReference.new(reference_path, content_type)
149
+ end
150
+
151
+ # @since 1.0.0
152
+ def add_stylesheet(style_body, content_type = 'text/css')
153
+ @stylesheets << Stylesheet::Style.new(style_body, content_type)
154
+ end
155
+
156
+ # @since 1.0.0
157
+ def reference_stylesheet_file(reference_path, content_type = 'text/css')
158
+ @stylesheets << Stylesheet::StyleReference.new(reference_path, content_type)
159
+ end
160
+
161
+ # @since 1.0.0
162
+ def add_initialize_script(script_body)
163
+ if @init_script
164
+ @init_script = Script::EcmaScript::EventListener.new(
165
+ @init_script.instance_variable_get(:@body) + script_body, 'init')
166
+ else
167
+ @init_script = Script::EcmaScript::EventListener.new(script_body, 'init')
168
+ add_event_listener(:load, @init_script)
169
+ end
170
+ end
171
+
172
+ # @since 1.0.0
173
+ def add_event_listener(event_name, event_listener)
174
+ if event_listeners.key?(event_name)
175
+ unless event_listeners[event_name].include?(event_listener)
176
+ event_listeners[event_name] << event_listener
177
+ end
178
+ else
179
+ event_listeners[event_name] = [event_listener]
180
+ end
181
+ end
182
+
183
+ # @since 1.0.0
184
+ def remove_event_listener(event_name, event_listener)
185
+ if event_listeners.key?(event_name)
186
+ event_listeners[event_name].delete(event_listener)
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def get_formatter(format=nil) #:nodoc:
193
+ case format
194
+ when :svg, nil then Formatter::SvgFormatter.new(self, 2)
195
+ when :xaml then Formatter::XamlFormatter.new(self, 2)
196
+ when :eps then Formatter::EpsFormatter.new(self)
197
+ when :png then Formatter::PngFormatter.new(self)
198
+ else raise ArgumentError, "`#{format}' is unknown format"
199
+ end
200
+ end
201
+ end
202
+ end
data/lib/dyi/chart.rb CHANGED
@@ -22,6 +22,8 @@
22
22
  %w(
23
23
 
24
24
  base
25
+ legend
26
+ axis_util
25
27
  line_chart
26
28
  pie_chart
27
29
  table
@@ -23,95 +23,94 @@ module DYI #:nodoc:
23
23
  module Chart #:nodoc:
24
24
 
25
25
  class ArrayReader
26
+ include Enumerable
26
27
 
27
28
  def [](i, j)
28
- @data[i][j]
29
+ @records[i].values[j]
29
30
  end
30
31
 
31
- def row_title(i)
32
- @row_titles[i]
32
+ # @return [Array] array of a struct
33
+ # @since 1.0.0
34
+ def records
35
+ @records.dup
33
36
  end
34
37
 
35
- def column_title(j)
36
- @col_titles[j]
38
+ # @return [Integer] number of the records
39
+ # @since 1.0.0
40
+ def records_size
41
+ @records.size
37
42
  end
38
43
 
39
- def row_values(i)
40
- @data[i].dup
44
+ # @return [Integer] number of the values
45
+ # @since 1.0.0
46
+ def values_size
47
+ @records.first.values.size rescue 0
41
48
  end
42
49
 
43
- def column_values(j)
44
- @data.map{|r| r[j]}
45
- end
46
-
47
- def row_count
48
- @data.size
49
- end
50
-
51
- def column_count
52
- @data.first.size
50
+ def clear_data
51
+ @records.clear
53
52
  end
54
53
 
55
- def clear_data
56
- @data.clear
57
- @col_titles.clear
58
- @row_titles.clear
54
+ # @return [void]
55
+ # @since 1.0.0
56
+ def values_each(&block)
57
+ @records.each do |record|
58
+ yield record.values
59
+ end
59
60
  end
60
61
 
61
- def values
62
- @data.dup
62
+ # @param [Integer] index index of series
63
+ # @return [Array]
64
+ # @since 1.0.0
65
+ def series(index)
66
+ @records.map do |record|
67
+ record.values[index]
68
+ end
63
69
  end
64
70
 
65
- def column_titles
66
- @col_titles.dup
71
+ # @since 1.0.0
72
+ def has_field?(field_name)
73
+ @schema.members.include?(field_name.to_s)
67
74
  end
68
75
 
69
- def row_titles
70
- @row_titles.dup
76
+ # @return [void]
77
+ # @since 1.0.0
78
+ def each(&block)
79
+ @records.each(&block)
71
80
  end
72
81
 
73
82
  def initialize
74
- @data = []
75
- @col_titles = []
76
- @row_titles = []
83
+ @records = []
77
84
  end
78
85
 
79
86
  def read(array_of_array, options={})
80
87
  clear_data
88
+ row_range = options[:row_range] || (0..-1)
89
+ col_range = options[:column_range] || (0..-1)
90
+ schema = options[:schema] || [:value]
81
91
  data_types = options[:data_types] || []
82
- row_skip = (options[:row_skip].to_i rescue 0)
83
- col_skip = (options[:column_skip].to_i rescue 0)
84
- title_row = ((options[:title_row] ? options[:title_row].to_i : nil) rescue nil)
85
- title_col = ((options[:title_column] ? options[:title_column].to_i : nil) rescue nil)
86
- row_limit = ((options[:row_limit] ? options[:row_limit].to_i : nil) rescue nil)
87
- row_proc = options[:row_proc]
88
- array_of_array.each_with_index do |row, i|
89
- unless options[:transposed]
90
- if i == title_row
91
- @col_titles.replace(row[col_skip..-1].map{|v| primitive_title_value(v)})
92
- end
93
- next if i < row_skip
94
- break if row_limit && row_limit + row_skip <= i
95
- next if row_proc.respond_to?(:call) && !row_proc.call(*row[col_skip..-1])
96
- @row_titles << primitive_title_value(row[title_col]) if title_col
97
- vals = []
98
- row[col_skip..-1].each_with_index do |value, j|
99
- vals << primitive_value(value, data_types[j])
100
- end
101
- @data << vals
102
- else
103
- row_limit_number = row_limit ? row_limit + row_skip - 1 : -1
104
- if i == title_col
105
- @row_titles.replace(row[row_skip..row_limit_number].map{|v| primitive_title_value(v)})
106
- end
107
- next if i < col_skip
108
- vals = row[row_skip..row_limit_number]
109
- @data.replace(vals.map{|value| []}) if @data.empty?
110
- @col_titles << primitive_title_value(row[title_row]) if title_row
111
- vals.each_with_index do |value, j|
112
- @data[j] << primitive_value(value, data_types[i])
92
+ # row_proc = options[:row_proc]
93
+ @schema = record_schema(schema)
94
+ array_of_array = transpose(array_of_array) if options[:transposed]
95
+
96
+ array_of_array[row_range].each do |row|
97
+ record_source = []
98
+ values = []
99
+ has_set_value = false
100
+ row[col_range].each_with_index do |cell, i|
101
+ cell = primitive_value(cell, data_types[i])
102
+ if schema[i].nil? || schema[i].to_sym == :value
103
+ unless has_set_value
104
+ record_source << cell
105
+ has_set_value = true
106
+ end
107
+ values << cell
108
+ else
109
+ record_source << cell
113
110
  end
114
111
  end
112
+ record_source << values
113
+ @records << @schema.new(*record_source)
115
114
  end
116
115
  self
117
116
  end
@@ -122,8 +121,58 @@ module DYI #:nodoc:
122
121
  value
123
122
  end
124
123
 
125
- def primitive_title_value(value, type=nil)
126
- primitive_value(value, type)
124
+ # Transposes row and column of array-of-array.
125
+ # @example
126
+ # transpose([[0,1,2],[3,4,5]]) => [[0,3],[1,4],[2,5]]
127
+ # @param [Array] array_of_array array of array
128
+ # @return [Array] transposed array
129
+ # @since 1.0.0
130
+ def transpose(array_of_array)
131
+ transposed_array = []
132
+ array_of_array.each_with_index do |row, i|
133
+ row.each_with_index do |cell, j|
134
+ transposed_array[j] ||= Array.new(i)
135
+ transposed_array[j] << cell
136
+ end
137
+ end
138
+ transposed_array
139
+ end
140
+
141
+ # @param [Array] schema of the record
142
+ # @return [Class] subclass of Struct class
143
+ # @since 1.0.0
144
+ def record_schema(schema)
145
+ struct_schema =
146
+ schema.inject([]) do |result, name|
147
+ if result.include?(name.to_sym)
148
+ if name.to_sym == :value
149
+ next result
150
+ else
151
+ raise ArgumentError, "schema option has a duplicate name: `#{name}'"
152
+ end
153
+ end
154
+ if name.to_sym == :values
155
+ raise ArgumentError, "schema option may not contain `:values'"
156
+ end
157
+ result << name.to_sym
158
+ end
159
+ struct_schema << :values
160
+ Struct.new(*struct_schema)
161
+ end
162
+
163
+ # Makes the instance respond to xxx_values method.
164
+ # @example
165
+ # data = ArrayReader.read([['Smith', 20, 3432], ['Thomas', 25, 9721]],
166
+ # :schema => [:name, :age, :value])
167
+ # data.name_vlaues # => ['Smith', 'Thomas']
168
+ # data.age_values # => [20, 25]
169
+ # @since 1.0.0
170
+ def method_missing(name, *args)
171
+ if args.size == 0 && name.to_s =~ /_values\z/ && @schema.members.include?($`)
172
+ @records.map{|r| r.__send__($`)}
173
+ else
174
+ super
175
+ end
127
176
  end
128
177
 
129
178
  class << self
@@ -0,0 +1,298 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ require 'csv'
23
+
24
+ module DYI #:nodoc:
25
+ module Chart #:nodoc:
26
+
27
+ module AxisUtil
28
+
29
+ private
30
+
31
+ def moderate_axis(data, axis_length, min=nil, max=nil, scale_count_limit=nil)
32
+ raise ArgumentError, 'no data' if (data = data.flatten.compact).empty?
33
+
34
+ axis_length = Length.new(axis_length)
35
+ data_min, data_max = chart_range(data, min, max)
36
+
37
+ base_value = base_value(data_min, data_max, min.nil?, max.nil?)
38
+ scale_count_limit ||= (axis_length / Length.new(30)).to_i
39
+ scale_count_limit = 10 if 10 < scale_count_limit
40
+ scale_count_limit = 2 if scale_count_limit < 2
41
+ scale_interval = scale_interval(base_value, data_min, data_max, scale_count_limit)
42
+ min_scale_value = nil
43
+ (base_value + scale_interval).step(data_min, -scale_interval) {|n| min_scale_value = n}
44
+ min ||= (min_scale_value.nil? ? base_value : min_scale_value - scale_interval)
45
+ min_scale_value ||= min + scale_interval
46
+ unless max
47
+ base_value.step(data_max, scale_interval) {|n| max = n}
48
+ max += scale_interval if max < data_max
49
+ end
50
+ {
51
+ :min => min || min_scale_value - scale_interval,
52
+ :max => max,
53
+ :axis_length => axis_length,
54
+ :min_scale_value => min_scale_value,
55
+ :scale_interval => scale_interval
56
+ }
57
+ end
58
+
59
+ def top_digit(num, n=1) #:nodoc:
60
+ num.div(10 ** (figures_count(num) - n + 1))
61
+ end
62
+
63
+ def suitable_1digit_value(a, b) #:nodoc:
64
+ return a if a == b
65
+ a, b = b, a if a > b
66
+ return 0 if a == 0
67
+ return 5 if a <= 5 && 5 <= b
68
+ return 2 if a <= 2 && 2 <= b
69
+ return 4 if a <= 4 && 4 <= b
70
+ return 6 if a <= 6 && 6 <= b
71
+ 8
72
+ end
73
+
74
+ def figures_count(num) #:nodoc:
75
+ Math.log10(num).floor
76
+ end
77
+
78
+ def base_value(a, b, allow_under=true, allow_over=true) #:nodoc:
79
+ return 0 if a * b <= 0 || a == b
80
+ a, b = -a, -b if negative = (a < 0)
81
+ a, b = b, a if a > b
82
+ return 0 if ((negative && allow_over) || (!negative && allow_under)) && a < b * 0.3
83
+ suitable_value_positive(a, b) * (negative ? -1 : 1)
84
+ end
85
+
86
+ def suitable_value_positive(a, b) #:nodoc:
87
+ if figures_count(a) != (dig = figures_count(b))
88
+ return 10 ** dig
89
+ end
90
+ n = 1
91
+ n += 1 while (dig_a = top_digit(a, n)) == (dig_b = top_digit(b, n))
92
+ (suitable_1digit_value(dig_a - dig_a.div(10) * 10 + (dig_a == dig_a.div(10) * 10 ? 0 : 1), dig_b - dig_b.div(10) * 10) + dig_a.div(10) * 10) * (10 ** (dig - figures_count(dig_a)))
93
+ end
94
+
95
+ def scale_interval(base_value, data_min, data_max, scale_count_limit) #:nodoc:
96
+ if base_value - data_min < data_max - base_value
97
+ allocate_scale_count = (data_max - base_value).div((data_max - data_min).quo(scale_count_limit))
98
+ scale_interval_base2edge(base_value, data_max, allocate_scale_count)
99
+ else
100
+ allocate_scale_count = (base_value - data_min).div((data_max - data_min).quo(scale_count_limit))
101
+ scale_interval_base2edge(base_value, data_min, allocate_scale_count)
102
+ end
103
+ end
104
+
105
+ def scale_interval_base2edge(base_value, edge_value, scale_count_limit) #:nodoc:
106
+ raise ArgumentError, 'base_value should not equal edge_value' if edge_value == base_value
107
+ range = (base_value - edge_value).abs
108
+
109
+ top_2_digits = top_digit(range, 2)
110
+ case scale_count_limit.to_i
111
+ when 1
112
+ case top_2_digits
113
+ when 10 then label_range = 10
114
+ when 11..20 then label_range = 20
115
+ when 21..40 then label_range = 40
116
+ when 41..50 then label_range = 50
117
+ when 51..99 then label_range = 100
118
+ end
119
+ when 2
120
+ case top_2_digits
121
+ when 10 then label_range = 5
122
+ when 11..20 then label_range = 10
123
+ when 21..40 then label_range = 20
124
+ when 41..50 then label_range = 25
125
+ when 51..99 then label_range = 50
126
+ end
127
+ when 3
128
+ case top_2_digits
129
+ when 10 then label_range = 4
130
+ when 11..15 then label_range = 5
131
+ when 16..30 then label_range = 10
132
+ when 31..60 then label_range = 20
133
+ when 61..75 then label_range = 25
134
+ when 76..99 then label_range = 40
135
+ end
136
+ when 4
137
+ case top_2_digits
138
+ when 10 then label_range = 2.5
139
+ when 11..15 then label_range = 4
140
+ when 16..20 then label_range = 5
141
+ when 21..40 then label_range = 10
142
+ when 41..80 then label_range = 20
143
+ when 81..99 then label_range = 25
144
+ end
145
+ when 5
146
+ case top_2_digits
147
+ when 10 then label_range = 2
148
+ when 11..12 then label_range = 2.5
149
+ when 13..15 then label_range = 4
150
+ when 16..25 then label_range = 5
151
+ when 26..50 then label_range = 10
152
+ when 51..99 then label_range = 20
153
+ end
154
+ when 6
155
+ case top_2_digits
156
+ when 10..12 then label_range = 2
157
+ when 13..15 then label_range = 2.5
158
+ when 16..20 then label_range = 4
159
+ when 21..30 then label_range = 5
160
+ when 31..60 then label_range = 10
161
+ when 61..99 then label_range = 20
162
+ end
163
+ when 7
164
+ case top_2_digits
165
+ when 10..14 then label_range = 2
166
+ when 15..17 then label_range = 2.5
167
+ when 18..20 then label_range = 4
168
+ when 21..35 then label_range = 5
169
+ when 35..70 then label_range = 10
170
+ when 71..99 then label_range = 20
171
+ end
172
+ when 8
173
+ case top_2_digits
174
+ when 10..16 then label_range = 2
175
+ when 17..20 then label_range = 2.5
176
+ when 21..25 then label_range = 4
177
+ when 26..40 then label_range = 5
178
+ when 41..80 then label_range = 10
179
+ when 81..99 then label_range = 20
180
+ end
181
+ when 9
182
+ case top_2_digits
183
+ when 10..18 then label_range = 2
184
+ when 19..22 then label_range = 2.5
185
+ when 23..30 then label_range = 4
186
+ when 31..45 then label_range = 5
187
+ when 46..90 then label_range = 10
188
+ when 91..99 then label_range = 20
189
+ end
190
+ else
191
+ case top_2_digits
192
+ when 10 then label_range = 1
193
+ when 11..20 then label_range = 2
194
+ when 21..25 then label_range = 2.5
195
+ when 26..30 then label_range = 4
196
+ when 31..50 then label_range = 5
197
+ when 51..99 then label_range = 10
198
+ end
199
+ end
200
+ label_range * (10 ** (figures_count(range) - 1))
201
+ end
202
+
203
+ def moderate_sub_axis(data, main_axis_settings, min=nil, max=nil)
204
+ if min && max
205
+ axis_ratio = (max - min).quo(main_axis_settings[:max] - main_axis_settings[:min])
206
+ return {
207
+ :max => max,
208
+ :min => min,
209
+ :min_scale_value => min + (main_axis_settings[:min_scale_value] - main_axis_settings[:min]) * axis_ratio,
210
+ :axis_length => main_axis_settings[:axis_length],
211
+ :scale_interval => main_axis_settings[:scale_interval] * axis_ratio
212
+ }
213
+ end
214
+ scale_count = (main_axis_settings[:max] - main_axis_settings[:min_scale_value]).div(main_axis_settings[:scale_interval]) + (main_axis_settings[:min_scale_value] == main_axis_settings[:min] ? 0 : 1)
215
+ data_min, data_max = chart_range(data, min, max)
216
+
217
+ base_value = base_value(data_min, data_max, min.nil?, max.nil?)
218
+
219
+ scale_interval = scale_interval(base_value, data_min, data_max, scale_count)
220
+ min_scale_value = nil
221
+ (base_value + scale_interval).step(data_min, -scale_interval) {|n| min_scale_value = n}
222
+ min ||= (min_scale_value.nil? ? base_value : min_scale_value - scale_interval)
223
+ min_scale_value ||= min + scale_interval
224
+ max = scale_interval * scale_count + min
225
+
226
+ {
227
+ :min => min || min_scale_value - scale_interval,
228
+ :max => max,
229
+ :axis_length => main_axis_settings[:axis_length],
230
+ :min_scale_value => min_scale_value,
231
+ :scale_interval => scale_interval
232
+ }
233
+ end
234
+
235
+ def chart_range(data, min=nil, max=nil)
236
+ data = data.compact.flatten
237
+ if min.nil? && max.nil?
238
+ data_min, data_max =
239
+ data.inject([nil, nil]) do |(_min, _max), value|
240
+ [value < (_min ||= value) ? value : _min, (_max ||= value) < value ? value : _max]
241
+ end
242
+ elsif min && max && max < min
243
+ data_min, data_max = max, min
244
+ else
245
+ data_min = min || [data.min, max].min
246
+ data_max = max || [data.max, min].max
247
+ end
248
+
249
+ if data_min == data_max
250
+ if data_min > 0
251
+ data_min = 0
252
+ elsif data_max < 0
253
+ data_max = 0
254
+ else
255
+ data_min = 0
256
+ data_max = 100
257
+ end
258
+ end
259
+ [data_min, data_max]
260
+ end
261
+
262
+ def value_position_on_chart(chart_margin, axis_settings, value, reverse_direction = false)
263
+ axis_settings[:axis_length] *
264
+ ((reverse_direction ? (axis_settings[:max] - value) : (value - axis_settings[:min])).to_f / (axis_settings[:max] - axis_settings[:min])) +
265
+ Length.new(chart_margin)
266
+ end
267
+
268
+ def order_position_on_chart(chart_margin, axis_length, count, index, type=:point, renge_width_ratio=0, reverse_direction=false)
269
+ chart_margin = Length.new(chart_margin)
270
+ pos =
271
+ case type
272
+ when :point then index.to_f / (count - 1)
273
+ when :range then (index + 0.5 - renge_width_ratio.to_f / 2) / count
274
+ else raise ArgumentError, "\"#{type}\" is invalid type"
275
+ end
276
+ axis_length * pos + chart_margin
277
+ end
278
+
279
+ def round_top_2_digit(max, min) #:nodoc:
280
+ digit = Math.log10([max.abs, min.abs].max).floor - 1
281
+ [max.quo(10 ** digit).ceil * (10 ** digit), min.quo(10 ** digit).floor * (10 ** digit)]
282
+ end
283
+
284
+ def min_scale_value(max, min, scale_interval) #:nodoc:
285
+ return scale_interval if min == 0
286
+ if (max_digit = Math.log10(max).to_i) != Math.log10(min).to_i
287
+ base_value = 10 ** max_digit
288
+ elsif max.div(10 ** max_digit) != min.div(10 ** max_digit)
289
+ base_value = 9 * 10 ** max_digit
290
+ else
291
+ range_digit = Math.log10(max - min).floor
292
+ base_value = max.div(10 ** range_digit) * (10 ** range_digit)
293
+ end
294
+ base_value - ((base_value - min).quo(scale_interval).ceil - 1) * scale_interval
295
+ end
296
+ end
297
+ end
298
+ end