@@ -1,241 +1,238 | |||||
1 | require 'SVG/Graph/Plot' |
|
1 | require 'SVG/Graph/Plot' | |
2 | require 'parsedate' |
|
|||
3 |
|
2 | |||
4 | module SVG |
|
3 | module SVG | |
5 | module Graph |
|
4 | module Graph | |
6 | # === For creating SVG plots of scalar temporal data |
|
5 | # === For creating SVG plots of scalar temporal data | |
7 | # |
|
6 | # | |
8 | # = Synopsis |
|
7 | # = Synopsis | |
9 | # |
|
8 | # | |
10 | # require 'SVG/Graph/TimeSeriess' |
|
9 | # require 'SVG/Graph/TimeSeriess' | |
11 | # |
|
10 | # | |
12 | # # Data sets are x,y pairs |
|
11 | # # Data sets are x,y pairs | |
13 | # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, |
|
12 | # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, | |
14 | # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] |
|
13 | # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] | |
15 | # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, |
|
14 | # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, | |
16 | # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, |
|
15 | # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, | |
17 | # "5/1/84", 17, "10/1/80", 12] |
|
16 | # "5/1/84", 17, "10/1/80", 12] | |
18 | # |
|
17 | # | |
19 | # graph = SVG::Graph::TimeSeries.new( { |
|
18 | # graph = SVG::Graph::TimeSeries.new( { | |
20 | # :width => 640, |
|
19 | # :width => 640, | |
21 | # :height => 480, |
|
20 | # :height => 480, | |
22 | # :graph_title => title, |
|
21 | # :graph_title => title, | |
23 | # :show_graph_title => true, |
|
22 | # :show_graph_title => true, | |
24 | # :no_css => true, |
|
23 | # :no_css => true, | |
25 | # :key => true, |
|
24 | # :key => true, | |
26 | # :scale_x_integers => true, |
|
25 | # :scale_x_integers => true, | |
27 | # :scale_y_integers => true, |
|
26 | # :scale_y_integers => true, | |
28 | # :min_x_value => 0, |
|
27 | # :min_x_value => 0, | |
29 | # :min_y_value => 0, |
|
28 | # :min_y_value => 0, | |
30 | # :show_data_labels => true, |
|
29 | # :show_data_labels => true, | |
31 | # :show_x_guidelines => true, |
|
30 | # :show_x_guidelines => true, | |
32 | # :show_x_title => true, |
|
31 | # :show_x_title => true, | |
33 | # :x_title => "Time", |
|
32 | # :x_title => "Time", | |
34 | # :show_y_title => true, |
|
33 | # :show_y_title => true, | |
35 | # :y_title => "Ice Cream Cones", |
|
34 | # :y_title => "Ice Cream Cones", | |
36 | # :y_title_text_direction => :bt, |
|
35 | # :y_title_text_direction => :bt, | |
37 | # :stagger_x_labels => true, |
|
36 | # :stagger_x_labels => true, | |
38 | # :x_label_format => "%m/%d/%y", |
|
37 | # :x_label_format => "%m/%d/%y", | |
39 | # }) |
|
38 | # }) | |
40 | # |
|
39 | # | |
41 | # graph.add_data({ |
|
40 | # graph.add_data({ | |
42 | # :data => projection |
|
41 | # :data => projection | |
43 | # :title => 'Projected', |
|
42 | # :title => 'Projected', | |
44 | # }) |
|
43 | # }) | |
45 | # |
|
44 | # | |
46 | # graph.add_data({ |
|
45 | # graph.add_data({ | |
47 | # :data => actual, |
|
46 | # :data => actual, | |
48 | # :title => 'Actual', |
|
47 | # :title => 'Actual', | |
49 | # }) |
|
48 | # }) | |
50 | # |
|
49 | # | |
51 | # print graph.burn() |
|
50 | # print graph.burn() | |
52 | # |
|
51 | # | |
53 | # = Description |
|
52 | # = Description | |
54 | # |
|
53 | # | |
55 | # Produces a graph of temporal scalar data. |
|
54 | # Produces a graph of temporal scalar data. | |
56 | # |
|
55 | # | |
57 | # = Examples |
|
56 | # = Examples | |
58 | # |
|
57 | # | |
59 | # http://www.germane-software/repositories/public/SVG/test/timeseries.rb |
|
58 | # http://www.germane-software/repositories/public/SVG/test/timeseries.rb | |
60 | # |
|
59 | # | |
61 | # = Notes |
|
60 | # = Notes | |
62 | # |
|
61 | # | |
63 | # The default stylesheet handles upto 10 data sets, if you |
|
62 | # The default stylesheet handles upto 10 data sets, if you | |
64 | # use more you must create your own stylesheet and add the |
|
63 | # use more you must create your own stylesheet and add the | |
65 | # additional settings for the extra data sets. You will know |
|
64 | # additional settings for the extra data sets. You will know | |
66 | # if you go over 10 data sets as they will have no style and |
|
65 | # if you go over 10 data sets as they will have no style and | |
67 | # be in black. |
|
66 | # be in black. | |
68 | # |
|
67 | # | |
69 | # Unlike the other types of charts, data sets must contain x,y pairs: |
|
68 | # Unlike the other types of charts, data sets must contain x,y pairs: | |
70 | # |
|
69 | # | |
71 | # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) |
|
70 | # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) | |
72 | # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
|
71 | # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and | |
73 | # # ("14:20",6) |
|
72 | # # ("14:20",6) | |
74 | # |
|
73 | # | |
75 | # Note that multiple data sets within the same chart can differ in length, |
|
74 | # Note that multiple data sets within the same chart can differ in length, | |
76 | # and that the data in the datasets needn't be in order; they will be ordered |
|
75 | # and that the data in the datasets needn't be in order; they will be ordered | |
77 | # by the plot along the X-axis. |
|
76 | # by the plot along the X-axis. | |
78 | # |
|
77 | # | |
79 | # The dates must be parseable by ParseDate, but otherwise can be |
|
78 | # The dates must be parseable by ParseDate, but otherwise can be | |
80 | # any order of magnitude (seconds within the hour, or years) |
|
79 | # any order of magnitude (seconds within the hour, or years) | |
81 | # |
|
80 | # | |
82 | # = See also |
|
81 | # = See also | |
83 | # |
|
82 | # | |
84 | # * SVG::Graph::Graph |
|
83 | # * SVG::Graph::Graph | |
85 | # * SVG::Graph::BarHorizontal |
|
84 | # * SVG::Graph::BarHorizontal | |
86 | # * SVG::Graph::Bar |
|
85 | # * SVG::Graph::Bar | |
87 | # * SVG::Graph::Line |
|
86 | # * SVG::Graph::Line | |
88 | # * SVG::Graph::Pie |
|
87 | # * SVG::Graph::Pie | |
89 | # * SVG::Graph::Plot |
|
88 | # * SVG::Graph::Plot | |
90 | # |
|
89 | # | |
91 | # == Author |
|
90 | # == Author | |
92 | # |
|
91 | # | |
93 | # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
|
92 | # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> | |
94 | # |
|
93 | # | |
95 | # Copyright 2004 Sean E. Russell |
|
94 | # Copyright 2004 Sean E. Russell | |
96 | # This software is available under the Ruby license[LICENSE.txt] |
|
95 | # This software is available under the Ruby license[LICENSE.txt] | |
97 | # |
|
96 | # | |
98 | class TimeSeries < Plot |
|
97 | class TimeSeries < Plot | |
99 | # In addition to the defaults set by Graph::initialize and |
|
98 | # In addition to the defaults set by Graph::initialize and | |
100 | # Plot::set_defaults, sets: |
|
99 | # Plot::set_defaults, sets: | |
101 | # [x_label_format] '%Y-%m-%d %H:%M:%S' |
|
100 | # [x_label_format] '%Y-%m-%d %H:%M:%S' | |
102 | # [popup_format] '%Y-%m-%d %H:%M:%S' |
|
101 | # [popup_format] '%Y-%m-%d %H:%M:%S' | |
103 | def set_defaults |
|
102 | def set_defaults | |
104 | super |
|
103 | super | |
105 | init_with( |
|
104 | init_with( | |
106 | #:max_time_span => '', |
|
105 | #:max_time_span => '', | |
107 | :x_label_format => '%Y-%m-%d %H:%M:%S', |
|
106 | :x_label_format => '%Y-%m-%d %H:%M:%S', | |
108 | :popup_format => '%Y-%m-%d %H:%M:%S' |
|
107 | :popup_format => '%Y-%m-%d %H:%M:%S' | |
109 | ) |
|
108 | ) | |
110 | end |
|
109 | end | |
111 |
|
110 | |||
112 | # The format string use do format the X axis labels. |
|
111 | # The format string use do format the X axis labels. | |
113 | # See Time::strformat |
|
112 | # See Time::strformat | |
114 | attr_accessor :x_label_format |
|
113 | attr_accessor :x_label_format | |
115 | # Use this to set the spacing between dates on the axis. The value |
|
114 | # Use this to set the spacing between dates on the axis. The value | |
116 | # must be of the form |
|
115 | # must be of the form | |
117 | # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" |
|
116 | # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" | |
118 | # |
|
117 | # | |
119 | # EG: |
|
118 | # EG: | |
120 | # |
|
119 | # | |
121 | # graph.timescale_divisions = "2 weeks" |
|
120 | # graph.timescale_divisions = "2 weeks" | |
122 | # |
|
121 | # | |
123 | # will cause the chart to try to divide the X axis up into segments of |
|
122 | # will cause the chart to try to divide the X axis up into segments of | |
124 | # two week periods. |
|
123 | # two week periods. | |
125 | attr_accessor :timescale_divisions |
|
124 | attr_accessor :timescale_divisions | |
126 | # The formatting used for the popups. See x_label_format |
|
125 | # The formatting used for the popups. See x_label_format | |
127 | attr_accessor :popup_format |
|
126 | attr_accessor :popup_format | |
128 |
|
127 | |||
129 | # Add data to the plot. |
|
128 | # Add data to the plot. | |
130 | # |
|
129 | # | |
131 | # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) |
|
130 | # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) | |
132 | # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
|
131 | # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and | |
133 | # # ("14:20",6) |
|
132 | # # ("14:20",6) | |
134 | # graph.add_data( |
|
133 | # graph.add_data( | |
135 | # :data => d1, |
|
134 | # :data => d1, | |
136 | # :title => 'One' |
|
135 | # :title => 'One' | |
137 | # ) |
|
136 | # ) | |
138 | # graph.add_data( |
|
137 | # graph.add_data( | |
139 | # :data => d2, |
|
138 | # :data => d2, | |
140 | # :title => 'Two' |
|
139 | # :title => 'Two' | |
141 | # ) |
|
140 | # ) | |
142 | # |
|
141 | # | |
143 | # Note that the data must be in time,value pairs, and that the date format |
|
142 | # Note that the data must be in time,value pairs, and that the date format | |
144 | # may be any date that is parseable by ParseDate. |
|
143 | # may be any date that is parseable by ParseDate. | |
145 | def add_data data |
|
144 | def add_data data | |
146 | @data = [] unless @data |
|
145 | @data = [] unless @data | |
147 |
|
146 | |||
148 | raise "No data provided by #{@data.inspect}" unless data[:data] and |
|
147 | raise "No data provided by #{@data.inspect}" unless data[:data] and | |
149 | data[:data].kind_of? Array |
|
148 | data[:data].kind_of? Array | |
150 | raise "Data supplied must be x,y pairs! "+ |
|
149 | raise "Data supplied must be x,y pairs! "+ | |
151 | "The data provided contained an odd set of "+ |
|
150 | "The data provided contained an odd set of "+ | |
152 | "data points" unless data[:data].length % 2 == 0 |
|
151 | "data points" unless data[:data].length % 2 == 0 | |
153 | return if data[:data].length == 0 |
|
152 | return if data[:data].length == 0 | |
154 |
|
153 | |||
155 |
|
154 | |||
156 | x = [] |
|
155 | x = [] | |
157 | y = [] |
|
156 | y = [] | |
158 | data[:data].each_index {|i| |
|
157 | data[:data].each_index {|i| | |
159 | if i%2 == 0 |
|
158 | if i%2 == 0 | |
160 |
|
|
159 | t = DateTime.parse( data[:data][i] ).to_time | |
161 | t = Time.local( *arr[0,6].compact ) |
|
|||
162 | x << t.to_i |
|
160 | x << t.to_i | |
163 | else |
|
161 | else | |
164 | y << data[:data][i] |
|
162 | y << data[:data][i] | |
165 | end |
|
163 | end | |
166 | } |
|
164 | } | |
167 | sort( x, y ) |
|
165 | sort( x, y ) | |
168 | data[:data] = [x,y] |
|
166 | data[:data] = [x,y] | |
169 | @data << data |
|
167 | @data << data | |
170 | end |
|
168 | end | |
171 |
|
169 | |||
172 |
|
170 | |||
173 | protected |
|
171 | protected | |
174 |
|
172 | |||
175 | def min_x_value=(value) |
|
173 | def min_x_value=(value) | |
176 | arr = ParseDate.parsedate( value ) |
|
174 | @min_x_value = DateTime.parse( data[:data][i] ).to_time | |
177 | @min_x_value = Time.local( *arr[0,6].compact ).to_i |
|
|||
178 | end |
|
175 | end | |
179 |
|
176 | |||
180 |
|
177 | |||
181 | def format x, y |
|
178 | def format x, y | |
182 | Time.at( x ).strftime( popup_format ) |
|
179 | Time.at( x ).strftime( popup_format ) | |
183 | end |
|
180 | end | |
184 |
|
181 | |||
185 | def get_x_labels |
|
182 | def get_x_labels | |
186 | get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } |
|
183 | get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } | |
187 | end |
|
184 | end | |
188 |
|
185 | |||
189 | private |
|
186 | private | |
190 | def get_x_values |
|
187 | def get_x_values | |
191 | rv = [] |
|
188 | rv = [] | |
192 | min, max, scale_division = x_range |
|
189 | min, max, scale_division = x_range | |
193 | if timescale_divisions |
|
190 | if timescale_divisions | |
194 | timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/ |
|
191 | timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/ | |
195 | division_units = $2 ? $2 : "day" |
|
192 | division_units = $2 ? $2 : "day" | |
196 | amount = $1.to_i |
|
193 | amount = $1.to_i | |
197 | if amount |
|
194 | if amount | |
198 | step = nil |
|
195 | step = nil | |
199 | case division_units |
|
196 | case division_units | |
200 | when "month" |
|
197 | when "month" | |
201 | cur = min |
|
198 | cur = min | |
202 | while cur < max |
|
199 | while cur < max | |
203 | rv << cur |
|
200 | rv << cur | |
204 | arr = Time.at( cur ).to_a |
|
201 | arr = Time.at( cur ).to_a | |
205 | arr[4] += amount |
|
202 | arr[4] += amount | |
206 | if arr[4] > 12 |
|
203 | if arr[4] > 12 | |
207 | arr[5] += (arr[4] / 12).to_i |
|
204 | arr[5] += (arr[4] / 12).to_i | |
208 | arr[4] = (arr[4] % 12) |
|
205 | arr[4] = (arr[4] % 12) | |
209 | end |
|
206 | end | |
210 | cur = Time.local(*arr).to_i |
|
207 | cur = Time.local(*arr).to_i | |
211 | end |
|
208 | end | |
212 | when "year" |
|
209 | when "year" | |
213 | cur = min |
|
210 | cur = min | |
214 | while cur < max |
|
211 | while cur < max | |
215 | rv << cur |
|
212 | rv << cur | |
216 | arr = Time.at( cur ).to_a |
|
213 | arr = Time.at( cur ).to_a | |
217 | arr[5] += amount |
|
214 | arr[5] += amount | |
218 | cur = Time.local(*arr).to_i |
|
215 | cur = Time.local(*arr).to_i | |
219 | end |
|
216 | end | |
220 | when "week" |
|
217 | when "week" | |
221 | step = 7 * 24 * 60 * 60 * amount |
|
218 | step = 7 * 24 * 60 * 60 * amount | |
222 | when "day" |
|
219 | when "day" | |
223 | step = 24 * 60 * 60 * amount |
|
220 | step = 24 * 60 * 60 * amount | |
224 | when "hour" |
|
221 | when "hour" | |
225 | step = 60 * 60 * amount |
|
222 | step = 60 * 60 * amount | |
226 | when "minute" |
|
223 | when "minute" | |
227 | step = 60 * amount |
|
224 | step = 60 * amount | |
228 | when "second" |
|
225 | when "second" | |
229 | step = amount |
|
226 | step = amount | |
230 | end |
|
227 | end | |
231 | min.step( max, step ) {|v| rv << v} if step |
|
228 | min.step( max, step ) {|v| rv << v} if step | |
232 |
|
229 | |||
233 | return rv |
|
230 | return rv | |
234 | end |
|
231 | end | |
235 | end |
|
232 | end | |
236 | min.step( max, scale_division ) {|v| rv << v} |
|
233 | min.step( max, scale_division ) {|v| rv << v} | |
237 | return rv |
|
234 | return rv | |
238 | end |
|
235 | end | |
239 | end |
|
236 | end | |
240 | end |
|
237 | end | |
241 | end |
|
238 | end |
General Comments 0
You need to be logged in to leave comments.
Login now