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