OSDN Git Service

add Redmine trunk rev 3089
[redminele/redminele.git] / redmine / lib / SVG / Graph / Line.rb
1 require 'SVG/Graph/Graph'
2
3 module SVG
4   module Graph
5     # === Create presentation quality SVG line graphs easily
6     # 
7     # = Synopsis
8     # 
9     #   require 'SVG/Graph/Line'
10     # 
11     #   fields = %w(Jan Feb Mar);
12     #   data_sales_02 = [12, 45, 21]
13     #   data_sales_03 = [15, 30, 40]
14     #   
15     #   graph = SVG::Graph::Line.new({
16     #           :height => 500,
17     #           :width => 300,
18     #     :fields => fields,
19     #   })
20     #   
21     #   graph.add_data({
22     #           :data => data_sales_02,
23     #     :title => 'Sales 2002',
24     #   })
25     # 
26     #   graph.add_data({
27     #           :data => data_sales_03,
28     #     :title => 'Sales 2003',
29     #   })
30     #   
31     #   print "Content-type: image/svg+xml\r\n\r\n";
32     #   print graph.burn();
33     # 
34     # = Description
35     # 
36     # This object aims to allow you to easily create high quality
37     # SVG line graphs. You can either use the default style sheet
38     # or supply your own. Either way there are many options which can
39     # be configured to give you control over how the graph is
40     # generated - with or without a key, data elements at each point,
41     # title, subtitle etc.
42     # 
43     # = Examples
44     # 
45     # http://www.germane-software/repositories/public/SVG/test/single.rb
46     # 
47     # = Notes
48     # 
49     # The default stylesheet handles upto 10 data sets, if you
50     # use more you must create your own stylesheet and add the
51     # additional settings for the extra data sets. You will know
52     # if you go over 10 data sets as they will have no style and
53     # be in black.
54     # 
55     # = See also
56     # 
57     # * SVG::Graph::Graph
58     # * SVG::Graph::BarHorizontal
59     # * SVG::Graph::Bar
60     # * SVG::Graph::Pie
61     # * SVG::Graph::Plot
62     # * SVG::Graph::TimeSeries
63     #
64     # == Author
65     #
66     # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
67     #
68     # Copyright 2004 Sean E. Russell
69     # This software is available under the Ruby license[LICENSE.txt]
70     #
71     class Line < SVG::Graph::Graph
72       #    Show a small circle on the graph where the line
73       #    goes from one point to the next.
74       attr_accessor :show_data_points
75       #    Accumulates each data set. (i.e. Each point increased by sum of 
76       #   all previous series at same point). Default is 0, set to '1' to show.
77       attr_accessor :stacked
78       # Fill in the area under the plot if true
79       attr_accessor :area_fill
80
81       # The constructor takes a hash reference, fields (the names for each
82       # field on the X axis) MUST be set, all other values are defaulted to 
83       # those shown above - with the exception of style_sheet which defaults
84       # to using the internal style sheet.
85       def initialize config
86         raise "fields was not supplied or is empty" unless config[:fields] &&
87         config[:fields].kind_of?(Array) &&
88         config[:fields].length > 0
89                                 super
90                         end
91
92       # In addition to the defaults set in Graph::initialize, sets
93       # [show_data_points] true
94       # [show_data_values] true
95       # [stacked] false
96       # [area_fill] false
97                         def set_defaults
98         init_with(
99           :show_data_points   => true,
100           :show_data_values   => true,
101           :stacked            => false,
102           :area_fill          => false
103         )
104
105         self.top_align = self.top_font = self.right_align = self.right_font = 1
106       end
107
108       protected
109
110       def max_value
111         max = 0
112         
113         if (stacked == true) then
114           sums = Array.new(@config[:fields].length).fill(0)
115
116           @data.each do |data|
117             sums.each_index do |i|
118               sums[i] += data[:data][i].to_f
119             end
120           end
121           
122           max = sums.max
123         else
124           max = @data.collect{|x| x[:data].max}.max
125         end
126
127         return max
128       end
129
130       def min_value
131         min = 0
132         
133         if (min_scale_value.nil? == false) then
134           min = min_scale_value
135         elsif (stacked == true) then
136           min = @data[-1][:data].min
137         else
138           min = @data.collect{|x| x[:data].min}.min
139         end
140
141         return min
142       end
143
144       def get_x_labels
145         @config[:fields]
146       end
147
148       def calculate_left_margin
149         super
150         label_left = @config[:fields][0].length / 2 * font_size * 0.6
151         @border_left = label_left if label_left > @border_left
152       end
153
154       def get_y_labels
155         maxvalue = max_value
156         minvalue = min_value
157         range = maxvalue - minvalue
158         top_pad = range == 0 ? 10 : range / 20.0
159         scale_range = (maxvalue + top_pad) - minvalue
160
161         scale_division = scale_divisions || (scale_range / 10.0)
162
163         if scale_integers
164           scale_division = scale_division < 1 ? 1 : scale_division.round
165         end
166
167         rv = []
168         maxvalue = maxvalue%scale_division == 0 ? 
169           maxvalue : maxvalue + scale_division
170         minvalue.step( maxvalue, scale_division ) {|v| rv << v}
171         return rv
172       end
173
174       def calc_coords(field, value, width = field_width, height = field_height)
175         coords = {:x => 0, :y => 0}
176         coords[:x] = width * field
177         coords[:y] = @graph_height - value * height
178       
179         return coords
180       end
181
182       def draw_data
183         minvalue = min_value
184         fieldheight = (@graph_height.to_f - font_size*2*top_font) / 
185                          (get_y_labels.max - get_y_labels.min)
186         fieldwidth = field_width
187         line = @data.length
188
189         prev_sum = Array.new(@config[:fields].length).fill(0)
190         cum_sum = Array.new(@config[:fields].length).fill(-minvalue)
191
192         for data in @data.reverse
193           lpath = ""
194           apath = ""
195
196           if not stacked then cum_sum.fill(-minvalue) end
197           
198           data[:data].each_index do |i|
199             cum_sum[i] += data[:data][i]
200             
201             c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
202             
203             lpath << "#{c[:x]} #{c[:y]} "
204           end
205         
206           if area_fill
207             if stacked then
208               (prev_sum.length - 1).downto 0 do |i|
209                 c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
210                 
211                 apath << "#{c[:x]} #{c[:y]} "
212               end
213           
214               c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
215             else
216               apath = "V#@graph_height"
217               c = calc_coords(0, 0, fieldwidth, fieldheight)
218             end
219               
220             @graph.add_element("path", {
221               "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
222               "class" => "fill#{line}"
223             })
224           end
225         
226           @graph.add_element("path", {
227             "d" => "M0 #@graph_height L" + lpath,
228             "class" => "line#{line}"
229           })
230           
231           if show_data_points || show_data_values
232             cum_sum.each_index do |i|
233               if show_data_points
234                 @graph.add_element( "circle", {
235                   "cx" => (fieldwidth * i).to_s,
236                   "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
237                   "r" => "2.5",
238                   "class" => "dataPoint#{line}"
239                 })
240               end
241               make_datapoint_text( 
242                 fieldwidth * i, 
243                 @graph_height - cum_sum[i] * fieldheight - 6,
244                 cum_sum[i] + minvalue
245               )
246             end
247           end
248
249           prev_sum = cum_sum.dup
250           line -= 1
251         end
252       end
253
254
255       def get_css
256         return <<EOL
257 /* default line styles */
258 .line1{
259         fill: none;
260         stroke: #ff0000;
261         stroke-width: 1px;      
262 }
263 .line2{
264         fill: none;
265         stroke: #0000ff;
266         stroke-width: 1px;      
267 }
268 .line3{
269         fill: none;
270         stroke: #00ff00;
271         stroke-width: 1px;      
272 }
273 .line4{
274         fill: none;
275         stroke: #ffcc00;
276         stroke-width: 1px;      
277 }
278 .line5{
279         fill: none;
280         stroke: #00ccff;
281         stroke-width: 1px;      
282 }
283 .line6{
284         fill: none;
285         stroke: #ff00ff;
286         stroke-width: 1px;      
287 }
288 .line7{
289         fill: none;
290         stroke: #00ffff;
291         stroke-width: 1px;      
292 }
293 .line8{
294         fill: none;
295         stroke: #ffff00;
296         stroke-width: 1px;      
297 }
298 .line9{
299         fill: none;
300         stroke: #ccc6666;
301         stroke-width: 1px;      
302 }
303 .line10{
304         fill: none;
305         stroke: #663399;
306         stroke-width: 1px;      
307 }
308 .line11{
309         fill: none;
310         stroke: #339900;
311         stroke-width: 1px;      
312 }
313 .line12{
314         fill: none;
315         stroke: #9966FF;
316         stroke-width: 1px;      
317 }
318 /* default fill styles */
319 .fill1{
320         fill: #cc0000;
321         fill-opacity: 0.2;
322         stroke: none;
323 }
324 .fill2{
325         fill: #0000cc;
326         fill-opacity: 0.2;
327         stroke: none;
328 }
329 .fill3{
330         fill: #00cc00;
331         fill-opacity: 0.2;
332         stroke: none;
333 }
334 .fill4{
335         fill: #ffcc00;
336         fill-opacity: 0.2;
337         stroke: none;
338 }
339 .fill5{
340         fill: #00ccff;
341         fill-opacity: 0.2;
342         stroke: none;
343 }
344 .fill6{
345         fill: #ff00ff;
346         fill-opacity: 0.2;
347         stroke: none;
348 }
349 .fill7{
350         fill: #00ffff;
351         fill-opacity: 0.2;
352         stroke: none;
353 }
354 .fill8{
355         fill: #ffff00;
356         fill-opacity: 0.2;
357         stroke: none;
358 }
359 .fill9{
360         fill: #cc6666;
361         fill-opacity: 0.2;
362         stroke: none;
363 }
364 .fill10{
365         fill: #663399;
366         fill-opacity: 0.2;
367         stroke: none;
368 }
369 .fill11{
370         fill: #339900;
371         fill-opacity: 0.2;
372         stroke: none;
373 }
374 .fill12{
375         fill: #9966FF;
376         fill-opacity: 0.2;
377         stroke: none;
378 }
379 /* default line styles */
380 .key1,.dataPoint1{
381         fill: #ff0000;
382         stroke: none;
383         stroke-width: 1px;      
384 }
385 .key2,.dataPoint2{
386         fill: #0000ff;
387         stroke: none;
388         stroke-width: 1px;      
389 }
390 .key3,.dataPoint3{
391         fill: #00ff00;
392         stroke: none;
393         stroke-width: 1px;      
394 }
395 .key4,.dataPoint4{
396         fill: #ffcc00;
397         stroke: none;
398         stroke-width: 1px;      
399 }
400 .key5,.dataPoint5{
401         fill: #00ccff;
402         stroke: none;
403         stroke-width: 1px;      
404 }
405 .key6,.dataPoint6{
406         fill: #ff00ff;
407         stroke: none;
408         stroke-width: 1px;      
409 }
410 .key7,.dataPoint7{
411         fill: #00ffff;
412         stroke: none;
413         stroke-width: 1px;      
414 }
415 .key8,.dataPoint8{
416         fill: #ffff00;
417         stroke: none;
418         stroke-width: 1px;      
419 }
420 .key9,.dataPoint9{
421         fill: #cc6666;
422         stroke: none;
423         stroke-width: 1px;      
424 }
425 .key10,.dataPoint10{
426         fill: #663399;
427         stroke: none;
428         stroke-width: 1px;      
429 }
430 .key11,.dataPoint11{
431         fill: #339900;
432         stroke: none;
433         stroke-width: 1px;      
434 }
435 .key12,.dataPoint12{
436         fill: #9966FF;
437         stroke: none;
438         stroke-width: 1px;      
439 }
440 EOL
441       end
442     end
443   end
444 end