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