##// END OF EJS Templates
Update SVG library to latest stable (0.6.1) (#3056)....
Jean-Philippe Lang -
r2553:c9c269abf7e9
parent child
Show More
@@ -1,137 +1,148
1 require 'rexml/document'
1 require 'rexml/document'
2 require 'SVG/Graph/Graph'
2 require 'SVG/Graph/Graph'
3 require 'SVG/Graph/BarBase'
3 require 'SVG/Graph/BarBase'
4
4
5 module SVG
5 module SVG
6 module Graph
6 module Graph
7 # === Create presentation quality SVG bar graphs easily
7 # === Create presentation quality SVG bar graphs easily
8 #
8 #
9 # = Synopsis
9 # = Synopsis
10 #
10 #
11 # require 'SVG/Graph/Bar'
11 # require 'SVG/Graph/Bar'
12 #
12 #
13 # fields = %w(Jan Feb Mar);
13 # fields = %w(Jan Feb Mar);
14 # data_sales_02 = [12, 45, 21]
14 # data_sales_02 = [12, 45, 21]
15 #
15 #
16 # graph = SVG::Graph::Bar.new(
16 # graph = SVG::Graph::Bar.new(
17 # :height => 500,
17 # :height => 500,
18 # :width => 300,
18 # :width => 300,
19 # :fields => fields
19 # :fields => fields
20 # )
20 # )
21 #
21 #
22 # graph.add_data(
22 # graph.add_data(
23 # :data => data_sales_02,
23 # :data => data_sales_02,
24 # :title => 'Sales 2002'
24 # :title => 'Sales 2002'
25 # )
25 # )
26 #
26 #
27 # print "Content-type: image/svg+xml\r\n\r\n"
27 # print "Content-type: image/svg+xml\r\n\r\n"
28 # print graph.burn
28 # print graph.burn
29 #
29 #
30 # = Description
30 # = Description
31 #
31 #
32 # This object aims to allow you to easily create high quality
32 # This object aims to allow you to easily create high quality
33 # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
33 # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
34 # style sheet or supply your own. Either way there are many options which
34 # style sheet or supply your own. Either way there are many options which
35 # can be configured to give you control over how the graph is generated -
35 # can be configured to give you control over how the graph is generated -
36 # with or without a key, data elements at each point, title, subtitle etc.
36 # with or without a key, data elements at each point, title, subtitle etc.
37 #
37 #
38 # = Notes
38 # = Notes
39 #
39 #
40 # The default stylesheet handles upto 12 data sets, if you
40 # The default stylesheet handles upto 12 data sets, if you
41 # use more you must create your own stylesheet and add the
41 # use more you must create your own stylesheet and add the
42 # additional settings for the extra data sets. You will know
42 # additional settings for the extra data sets. You will know
43 # if you go over 12 data sets as they will have no style and
43 # if you go over 12 data sets as they will have no style and
44 # be in black.
44 # be in black.
45 #
45 #
46 # = Examples
46 # = Examples
47 #
47 #
48 # * http://germane-software.com/repositories/public/SVG/test/test.rb
48 # * http://germane-software.com/repositories/public/SVG/test/test.rb
49 #
49 #
50 # = See also
50 # = See also
51 #
51 #
52 # * SVG::Graph::Graph
52 # * SVG::Graph::Graph
53 # * SVG::Graph::BarHorizontal
53 # * SVG::Graph::BarHorizontal
54 # * SVG::Graph::Line
54 # * SVG::Graph::Line
55 # * SVG::Graph::Pie
55 # * SVG::Graph::Pie
56 # * SVG::Graph::Plot
56 # * SVG::Graph::Plot
57 # * SVG::Graph::TimeSeries
57 # * SVG::Graph::TimeSeries
58 class Bar < BarBase
58 class Bar < BarBase
59 include REXML
59 include REXML
60
60
61 # See Graph::initialize and BarBase::set_defaults
61 # See Graph::initialize and BarBase::set_defaults
62 def set_defaults
62 def set_defaults
63 super
63 super
64 self.top_align = self.top_font = 1
64 self.top_align = self.top_font = 1
65 end
65 end
66
66
67 protected
67 protected
68
68
69 def get_x_labels
69 def get_x_labels
70 @config[:fields]
70 @config[:fields]
71 end
71 end
72
72
73 def get_y_labels
73 def get_y_labels
74 maxvalue = max_value
74 maxvalue = max_value
75 minvalue = min_value
75 minvalue = min_value
76 range = maxvalue - minvalue
76 range = maxvalue - minvalue
77
77
78 top_pad = range == 0 ? 10 : range / 20.0
78 top_pad = range == 0 ? 10 : range / 20.0
79 scale_range = (maxvalue + top_pad) - minvalue
79 scale_range = (maxvalue + top_pad) - minvalue
80
80
81 scale_division = scale_divisions || (scale_range / 10.0)
81 scale_division = scale_divisions || (scale_range / 10.0)
82
82
83 if scale_integers
83 if scale_integers
84 scale_division = scale_division < 1 ? 1 : scale_division.round
84 scale_division = scale_division < 1 ? 1 : scale_division.round
85 end
85 end
86
86
87 rv = []
87 rv = []
88 maxvalue = maxvalue%scale_division == 0 ?
88 maxvalue = maxvalue%scale_division == 0 ?
89 maxvalue : maxvalue + scale_division
89 maxvalue : maxvalue + scale_division
90 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
90 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
91 return rv
91 return rv
92 end
92 end
93
93
94 def x_label_offset( width )
94 def x_label_offset( width )
95 width / 2.0
95 width / 2.0
96 end
96 end
97
97
98 def draw_data
98 def draw_data
99 fieldwidth = field_width
99 minvalue = min_value
100 maxvalue = max_value
100 fieldwidth = field_width
101 minvalue = min_value
101
102
102 unit_size = (@graph_height.to_f - font_size*2*top_font) /
103 fieldheight = (@graph_height.to_f - font_size*2*top_font) /
103 (get_y_labels.max - get_y_labels.min)
104 (get_y_labels.max - get_y_labels.min)
104 bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
105 bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
105
106
106 bar_width = fieldwidth - bargap
107 subbar_width = fieldwidth - bargap
107 bar_width /= @data.length if stack == :side
108 subbar_width /= @data.length if stack == :side
108 x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
109 x_mod = (@graph_width-bargap)/2 - (stack==:side ? subbar_width/2 : 0)
109
110 # Y1
110 bottom = @graph_height
111 p2 = @graph_height
111
112 # to X2
112 field_count = 0
113 field_count = 0
113 @config[:fields].each_index { |i|
114 @config[:fields].each_index { |i|
114 dataset_count = 0
115 dataset_count = 0
115 for dataset in @data
116 for dataset in @data
116
117 # X1
117 # cases (assume 0 = +ve):
118 p1 = (fieldwidth * field_count)
118 # value min length
119 # to Y2
119 # +ve +ve value - min
120 p3 = @graph_height - ((dataset[:data][i] - minvalue) * fieldheight)
120 # +ve -ve value - 0
121 p1 += subbar_width * dataset_count if stack == :side
121 # -ve -ve value.abs - 0
122 @graph.add_element( "path", {
122
123 "class" => "fill#{dataset_count+1}",
123 value = dataset[:data][i]
124 "d" => "M#{p1} #{p2} V#{p3} h#{subbar_width} V#{p2} Z"
124
125 })
125 left = (fieldwidth * field_count)
126 make_datapoint_text(
126
127 p1 + subbar_width/2.0,
127 length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
128 p3 - 6,
128 # top is 0 if value is negative
129 dataset[:data][i].to_s)
129 top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
130 dataset_count += 1
130 left += bar_width * dataset_count if stack == :side
131 end
131
132 field_count += 1
132 @graph.add_element( "rect", {
133 }
133 "x" => left.to_s,
134 end
134 "y" => top.to_s,
135 end
135 "width" => bar_width.to_s,
136 end
136 "height" => length.to_s,
137 end
137 "class" => "fill#{dataset_count+1}"
138 })
139
140 make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
141 dataset_count += 1
142 end
143 field_count += 1
144 }
145 end
146 end
147 end
148 end
@@ -1,140 +1,139
1 require 'rexml/document'
1 require 'rexml/document'
2 require 'SVG/Graph/Graph'
2 require 'SVG/Graph/Graph'
3
3
4 module SVG
4 module SVG
5 module Graph
5 module Graph
6 # = Synopsis
6 # = Synopsis
7 #
7 #
8 # A superclass for bar-style graphs. Do not attempt to instantiate
8 # A superclass for bar-style graphs. Do not attempt to instantiate
9 # directly; use one of the subclasses instead.
9 # directly; use one of the subclasses instead.
10 #
10 #
11 # = Author
11 # = Author
12 #
12 #
13 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
13 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
14 #
14 #
15 # Copyright 2004 Sean E. Russell
15 # Copyright 2004 Sean E. Russell
16 # This software is available under the Ruby license[LICENSE.txt]
16 # This software is available under the Ruby license[LICENSE.txt]
17 #
17 #
18 class BarBase < SVG::Graph::Graph
18 class BarBase < SVG::Graph::Graph
19 # Ensures that :fields are provided in the configuration.
19 # Ensures that :fields are provided in the configuration.
20 def initialize config
20 def initialize config
21 raise "fields was not supplied or is empty" unless config[:fields] &&
21 raise "fields was not supplied or is empty" unless config[:fields] &&
22 config[:fields].kind_of?(Array) &&
22 config[:fields].kind_of?(Array) &&
23 config[:fields].length > 0
23 config[:fields].length > 0
24 super
24 super
25 end
25 end
26
26
27 # In addition to the defaults set in Graph::initialize, sets
27 # In addition to the defaults set in Graph::initialize, sets
28 # [bar_gap] true
28 # [bar_gap] true
29 # [stack] :overlap
29 # [stack] :overlap
30 def set_defaults
30 def set_defaults
31 init_with( :bar_gap => true, :stack => :overlap )
31 init_with( :bar_gap => true, :stack => :overlap )
32 end
32 end
33
33
34 # Whether to have a gap between the bars or not, default
34 # Whether to have a gap between the bars or not, default
35 # is true, set to false if you don't want gaps.
35 # is true, set to false if you don't want gaps.
36 attr_accessor :bar_gap
36 attr_accessor :bar_gap
37 # How to stack data sets. :overlap overlaps bars with
37 # How to stack data sets. :overlap overlaps bars with
38 # transparent colors, :top stacks bars on top of one another,
38 # transparent colors, :top stacks bars on top of one another,
39 # :side stacks the bars side-by-side. Defaults to :overlap.
39 # :side stacks the bars side-by-side. Defaults to :overlap.
40 attr_accessor :stack
40 attr_accessor :stack
41
41
42
42
43 protected
43 protected
44
44
45 def max_value
45 def max_value
46 return @data.collect{|x| x[:data].max}.max
46 @data.collect{|x| x[:data].max}.max
47 end
47 end
48
48
49 def min_value
49 def min_value
50 min = 0
50 min = 0
51
51 if min_scale_value.nil?
52 if (min_scale_value.nil? == false) then
52 min = @data.collect{|x| x[:data].min}.min
53 min = min_scale_value
53 min = min > 0 ? 0 : min
54 else
54 else
55 min = @data.collect{|x| x[:data].min}.min
55 min = min_scale_value
56 end
56 end
57
57 return min
58 return min
58 end
59 end
59
60
60 def get_css
61 def get_css
61 return <<EOL
62 return <<EOL
62 /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
63 /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
63 .key1,.fill1{
64 .key1,.fill1{
64 fill: #ff0000;
65 fill: #ff0000;
65 fill-opacity: 0.5;
66 fill-opacity: 0.5;
66 stroke: none;
67 stroke: none;
67 stroke-width: 0.5px;
68 stroke-width: 0.5px;
68 }
69 }
69 .key2,.fill2{
70 .key2,.fill2{
70 fill: #0000ff;
71 fill: #0000ff;
71 fill-opacity: 0.5;
72 fill-opacity: 0.5;
72 stroke: none;
73 stroke: none;
73 stroke-width: 1px;
74 stroke-width: 1px;
74 }
75 }
75 .key3,.fill3{
76 .key3,.fill3{
76 fill: #00ff00;
77 fill: #00ff00;
77 fill-opacity: 0.5;
78 fill-opacity: 0.5;
78 stroke: none;
79 stroke: none;
79 stroke-width: 1px;
80 stroke-width: 1px;
80 }
81 }
81 .key4,.fill4{
82 .key4,.fill4{
82 fill: #ffcc00;
83 fill: #ffcc00;
83 fill-opacity: 0.5;
84 fill-opacity: 0.5;
84 stroke: none;
85 stroke: none;
85 stroke-width: 1px;
86 stroke-width: 1px;
86 }
87 }
87 .key5,.fill5{
88 .key5,.fill5{
88 fill: #00ccff;
89 fill: #00ccff;
89 fill-opacity: 0.5;
90 fill-opacity: 0.5;
90 stroke: none;
91 stroke: none;
91 stroke-width: 1px;
92 stroke-width: 1px;
92 }
93 }
93 .key6,.fill6{
94 .key6,.fill6{
94 fill: #ff00ff;
95 fill: #ff00ff;
95 fill-opacity: 0.5;
96 fill-opacity: 0.5;
96 stroke: none;
97 stroke: none;
97 stroke-width: 1px;
98 stroke-width: 1px;
98 }
99 }
99 .key7,.fill7{
100 .key7,.fill7{
100 fill: #00ffff;
101 fill: #00ffff;
101 fill-opacity: 0.5;
102 fill-opacity: 0.5;
102 stroke: none;
103 stroke: none;
103 stroke-width: 1px;
104 stroke-width: 1px;
104 }
105 }
105 .key8,.fill8{
106 .key8,.fill8{
106 fill: #ffff00;
107 fill: #ffff00;
107 fill-opacity: 0.5;
108 fill-opacity: 0.5;
108 stroke: none;
109 stroke: none;
109 stroke-width: 1px;
110 stroke-width: 1px;
110 }
111 }
111 .key9,.fill9{
112 .key9,.fill9{
112 fill: #cc6666;
113 fill: #cc6666;
113 fill-opacity: 0.5;
114 fill-opacity: 0.5;
114 stroke: none;
115 stroke: none;
115 stroke-width: 1px;
116 stroke-width: 1px;
116 }
117 }
117 .key10,.fill10{
118 .key10,.fill10{
118 fill: #663399;
119 fill: #663399;
119 fill-opacity: 0.5;
120 fill-opacity: 0.5;
120 stroke: none;
121 stroke: none;
121 stroke-width: 1px;
122 stroke-width: 1px;
122 }
123 }
123 .key11,.fill11{
124 .key11,.fill11{
124 fill: #339900;
125 fill: #339900;
125 fill-opacity: 0.5;
126 fill-opacity: 0.5;
126 stroke: none;
127 stroke: none;
127 stroke-width: 1px;
128 stroke-width: 1px;
128 }
129 }
129 .key12,.fill12{
130 .key12,.fill12{
130 fill: #9966FF;
131 fill: #9966FF;
131 fill-opacity: 0.5;
132 fill-opacity: 0.5;
132 stroke: none;
133 stroke: none;
133 stroke-width: 1px;
134 stroke-width: 1px;
134 }
135 }
135 EOL
136 EOL
136 end
137 end
137 end
138 end
138 end
139 end
139 end
140 end
@@ -1,136 +1,149
1 require 'rexml/document'
1 require 'rexml/document'
2 require 'SVG/Graph/BarBase'
2 require 'SVG/Graph/BarBase'
3
3
4 module SVG
4 module SVG
5 module Graph
5 module Graph
6 # === Create presentation quality SVG horitonzal bar graphs easily
6 # === Create presentation quality SVG horitonzal bar graphs easily
7 #
7 #
8 # = Synopsis
8 # = Synopsis
9 #
9 #
10 # require 'SVG/Graph/BarHorizontal'
10 # require 'SVG/Graph/BarHorizontal'
11 #
11 #
12 # fields = %w(Jan Feb Mar)
12 # fields = %w(Jan Feb Mar)
13 # data_sales_02 = [12, 45, 21]
13 # data_sales_02 = [12, 45, 21]
14 #
14 #
15 # graph = SVG::Graph::BarHorizontal.new({
15 # graph = SVG::Graph::BarHorizontal.new({
16 # :height => 500,
16 # :height => 500,
17 # :width => 300,
17 # :width => 300,
18 # :fields => fields,
18 # :fields => fields,
19 # })
19 # })
20 #
20 #
21 # graph.add_data({
21 # graph.add_data({
22 # :data => data_sales_02,
22 # :data => data_sales_02,
23 # :title => 'Sales 2002',
23 # :title => 'Sales 2002',
24 # })
24 # })
25 #
25 #
26 # print "Content-type: image/svg+xml\r\n\r\n"
26 # print "Content-type: image/svg+xml\r\n\r\n"
27 # print graph.burn
27 # print graph.burn
28 #
28 #
29 # = Description
29 # = Description
30 #
30 #
31 # This object aims to allow you to easily create high quality
31 # This object aims to allow you to easily create high quality
32 # SVG horitonzal bar graphs. You can either use the default style sheet
32 # SVG horitonzal bar graphs. You can either use the default style sheet
33 # or supply your own. Either way there are many options which can
33 # or supply your own. Either way there are many options which can
34 # be configured to give you control over how the graph is
34 # be configured to give you control over how the graph is
35 # generated - with or without a key, data elements at each point,
35 # generated - with or without a key, data elements at each point,
36 # title, subtitle etc.
36 # title, subtitle etc.
37 #
37 #
38 # = Examples
38 # = Examples
39 #
39 #
40 # * http://germane-software.com/repositories/public/SVG/test/test.rb
40 # * http://germane-software.com/repositories/public/SVG/test/test.rb
41 #
41 #
42 # = See also
42 # = See also
43 #
43 #
44 # * SVG::Graph::Graph
44 # * SVG::Graph::Graph
45 # * SVG::Graph::Bar
45 # * SVG::Graph::Bar
46 # * SVG::Graph::Line
46 # * SVG::Graph::Line
47 # * SVG::Graph::Pie
47 # * SVG::Graph::Pie
48 # * SVG::Graph::Plot
48 # * SVG::Graph::Plot
49 # * SVG::Graph::TimeSeries
49 # * SVG::Graph::TimeSeries
50 #
50 #
51 # == Author
51 # == Author
52 #
52 #
53 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
53 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
54 #
54 #
55 # Copyright 2004 Sean E. Russell
55 # Copyright 2004 Sean E. Russell
56 # This software is available under the Ruby license[LICENSE.txt]
56 # This software is available under the Ruby license[LICENSE.txt]
57 #
57 #
58 class BarHorizontal < BarBase
58 class BarHorizontal < BarBase
59 # In addition to the defaults set in BarBase::set_defaults, sets
59 # In addition to the defaults set in BarBase::set_defaults, sets
60 # [rotate_y_labels] true
60 # [rotate_y_labels] true
61 # [show_x_guidelines] true
61 # [show_x_guidelines] true
62 # [show_y_guidelines] false
62 # [show_y_guidelines] false
63 def set_defaults
63 def set_defaults
64 super
64 super
65 init_with(
65 init_with(
66 :rotate_y_labels => true,
66 :rotate_y_labels => true,
67 :show_x_guidelines => true,
67 :show_x_guidelines => true,
68 :show_y_guidelines => false
68 :show_y_guidelines => false
69 )
69 )
70 self.right_align = self.right_font = 1
70 self.right_align = self.right_font = 1
71 end
71 end
72
72
73 protected
73 protected
74
74
75 def get_x_labels
75 def get_x_labels
76 maxvalue = max_value
76 maxvalue = max_value
77 minvalue = min_value
77 minvalue = min_value
78 range = maxvalue - minvalue
78 range = maxvalue - minvalue
79 top_pad = range == 0 ? 10 : range / 20.0
79 top_pad = range == 0 ? 10 : range / 20.0
80 scale_range = (maxvalue + top_pad) - minvalue
80 scale_range = (maxvalue + top_pad) - minvalue
81
81
82 scale_division = scale_divisions || (scale_range / 10.0)
82 scale_division = scale_divisions || (scale_range / 10.0)
83
83
84 if scale_integers
84 if scale_integers
85 scale_division = scale_division < 1 ? 1 : scale_division.round
85 scale_division = scale_division < 1 ? 1 : scale_division.round
86 end
86 end
87
87
88 rv = []
88 rv = []
89 maxvalue = maxvalue%scale_division == 0 ?
89 maxvalue = maxvalue%scale_division == 0 ?
90 maxvalue : maxvalue + scale_division
90 maxvalue : maxvalue + scale_division
91 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
91 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
92 return rv
92 return rv
93 end
93 end
94
94
95 def get_y_labels
95 def get_y_labels
96 @config[:fields]
96 @config[:fields]
97 end
97 end
98
98
99 def y_label_offset( height )
99 def y_label_offset( height )
100 height / -2.0
100 height / -2.0
101 end
101 end
102
102
103 def draw_data
103 def draw_data
104 minvalue = min_value
104 minvalue = min_value
105 fieldheight = field_height
105 fieldheight = field_height
106 fieldwidth = (@graph_width.to_f - font_size*2*right_font ) /
106
107 (get_x_labels.max - get_x_labels.min )
107 unit_size = (@graph_width.to_f - font_size*2*right_font ) /
108 bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
108 (get_x_labels.max - get_x_labels.min )
109
109 bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
110 subbar_height = fieldheight - bargap
110
111 subbar_height /= @data.length if stack == :side
111 bar_height = fieldheight - bargap
112
112 bar_height /= @data.length if stack == :side
113 field_count = 1
113 y_mod = (bar_height / 2) + (font_size / 2)
114 y_mod = (subbar_height / 2) + (font_size / 2)
114
115 @config[:fields].each_index { |i|
115 field_count = 1
116 dataset_count = 0
116 @config[:fields].each_index { |i|
117 for dataset in @data
117 dataset_count = 0
118 y = @graph_height - (fieldheight * field_count)
118 for dataset in @data
119 y += (subbar_height * dataset_count) if stack == :side
119 value = dataset[:data][i]
120 x = (dataset[:data][i] - minvalue) * fieldwidth
120
121
121 top = @graph_height - (fieldheight * field_count)
122 @graph.add_element( "path", {
122 top += (bar_height * dataset_count) if stack == :side
123 "d" => "M0 #{y} H#{x} v#{subbar_height} H0 Z",
123 # cases (assume 0 = +ve):
124 "class" => "fill#{dataset_count+1}"
124 # value min length left
125 })
125 # +ve +ve value.abs - min minvalue.abs
126 make_datapoint_text(
126 # +ve -ve value.abs - 0 minvalue.abs
127 x+5, y+y_mod, dataset[:data][i], "text-anchor: start; "
127 # -ve -ve value.abs - 0 minvalue.abs + value
128 )
128 length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
129 dataset_count += 1
129 left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
130 end
130
131 field_count += 1
131 @graph.add_element( "rect", {
132 }
132 "x" => left.to_s,
133 end
133 "y" => top.to_s,
134 end
134 "width" => length.to_s,
135 end
135 "height" => bar_height.to_s,
136 end
136 "class" => "fill#{dataset_count+1}"
137 })
138
139 make_datapoint_text(
140 left+length+5, top+y_mod, value, "text-anchor: start; "
141 )
142 dataset_count += 1
143 end
144 field_count += 1
145 }
146 end
147 end
148 end
149 end
This diff has been collapsed as it changes many lines, (1955 lines changed) Show them Hide them
@@ -1,977 +1,978
1 begin
1 begin
2 require 'zlib'
2 require 'zlib'
3 @@__have_zlib = true
3 @@__have_zlib = true
4 rescue
4 rescue
5 @@__have_zlib = false
5 @@__have_zlib = false
6 end
6 end
7
7
8 require 'rexml/document'
8 require 'rexml/document'
9
9
10 module SVG
10 module SVG
11 module Graph
11 module Graph
12 VERSION = '@ANT_VERSION@'
12 VERSION = '@ANT_VERSION@'
13
13
14 # === Base object for generating SVG Graphs
14 # === Base object for generating SVG Graphs
15 #
15 #
16 # == Synopsis
16 # == Synopsis
17 #
17 #
18 # This class is only used as a superclass of specialized charts. Do not
18 # This class is only used as a superclass of specialized charts. Do not
19 # attempt to use this class directly, unless creating a new chart type.
19 # attempt to use this class directly, unless creating a new chart type.
20 #
20 #
21 # For examples of how to subclass this class, see the existing specific
21 # For examples of how to subclass this class, see the existing specific
22 # subclasses, such as SVG::Graph::Pie.
22 # subclasses, such as SVG::Graph::Pie.
23 #
23 #
24 # == Examples
24 # == Examples
25 #
25 #
26 # For examples of how to use this package, see either the test files, or
26 # For examples of how to use this package, see either the test files, or
27 # the documentation for the specific class you want to use.
27 # the documentation for the specific class you want to use.
28 #
28 #
29 # * file:test/plot.rb
29 # * file:test/plot.rb
30 # * file:test/single.rb
30 # * file:test/single.rb
31 # * file:test/test.rb
31 # * file:test/test.rb
32 # * file:test/timeseries.rb
32 # * file:test/timeseries.rb
33 #
33 #
34 # == Description
34 # == Description
35 #
35 #
36 # This package should be used as a base for creating SVG graphs.
36 # This package should be used as a base for creating SVG graphs.
37 #
37 #
38 # == Acknowledgements
38 # == Acknowledgements
39 #
39 #
40 # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
40 # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
41 # port is based on.
41 # port is based on.
42 #
42 #
43 # Stephen Morgan for creating the TT template and SVG.
43 # Stephen Morgan for creating the TT template and SVG.
44 #
44 #
45 # == See
45 # == See
46 #
46 #
47 # * SVG::Graph::BarHorizontal
47 # * SVG::Graph::BarHorizontal
48 # * SVG::Graph::Bar
48 # * SVG::Graph::Bar
49 # * SVG::Graph::Line
49 # * SVG::Graph::Line
50 # * SVG::Graph::Pie
50 # * SVG::Graph::Pie
51 # * SVG::Graph::Plot
51 # * SVG::Graph::Plot
52 # * SVG::Graph::TimeSeries
52 # * SVG::Graph::TimeSeries
53 #
53 #
54 # == Author
54 # == Author
55 #
55 #
56 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
56 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
57 #
57 #
58 # Copyright 2004 Sean E. Russell
58 # Copyright 2004 Sean E. Russell
59 # This software is available under the Ruby license[LICENSE.txt]
59 # This software is available under the Ruby license[LICENSE.txt]
60 #
60 #
61 class Graph
61 class Graph
62 include REXML
62 include REXML
63
63
64 # Initialize the graph object with the graph settings. You won't
64 # Initialize the graph object with the graph settings. You won't
65 # instantiate this class directly; see the subclass for options.
65 # instantiate this class directly; see the subclass for options.
66 # [width] 500
66 # [width] 500
67 # [height] 300
67 # [height] 300
68 # [show_x_guidelines] false
68 # [show_x_guidelines] false
69 # [show_y_guidelines] true
69 # [show_y_guidelines] true
70 # [show_data_values] true
70 # [show_data_values] true
71 # [min_scale_value] 0
71 # [min_scale_value] 0
72 # [show_x_labels] true
72 # [show_x_labels] true
73 # [stagger_x_labels] false
73 # [stagger_x_labels] false
74 # [rotate_x_labels] false
74 # [rotate_x_labels] false
75 # [step_x_labels] 1
75 # [step_x_labels] 1
76 # [step_include_first_x_label] true
76 # [step_include_first_x_label] true
77 # [show_y_labels] true
77 # [show_y_labels] true
78 # [rotate_y_labels] false
78 # [rotate_y_labels] false
79 # [scale_integers] false
79 # [scale_integers] false
80 # [show_x_title] false
80 # [show_x_title] false
81 # [x_title] 'X Field names'
81 # [x_title] 'X Field names'
82 # [show_y_title] false
82 # [show_y_title] false
83 # [y_title_text_direction] :bt
83 # [y_title_text_direction] :bt
84 # [y_title] 'Y Scale'
84 # [y_title] 'Y Scale'
85 # [show_graph_title] false
85 # [show_graph_title] false
86 # [graph_title] 'Graph Title'
86 # [graph_title] 'Graph Title'
87 # [show_graph_subtitle] false
87 # [show_graph_subtitle] false
88 # [graph_subtitle] 'Graph Sub Title'
88 # [graph_subtitle] 'Graph Sub Title'
89 # [key] true,
89 # [key] true,
90 # [key_position] :right, # bottom or righ
90 # [key_position] :right, # bottom or righ
91 # [font_size] 12
91 # [font_size] 12
92 # [title_font_size] 16
92 # [title_font_size] 16
93 # [subtitle_font_size] 14
93 # [subtitle_font_size] 14
94 # [x_label_font_size] 12
94 # [x_label_font_size] 12
95 # [x_title_font_size] 14
95 # [x_title_font_size] 14
96 # [y_label_font_size] 12
96 # [y_label_font_size] 12
97 # [y_title_font_size] 14
97 # [y_title_font_size] 14
98 # [key_font_size] 10
98 # [key_font_size] 10
99 # [no_css] false
99 # [no_css] false
100 # [add_popups] false
100 # [add_popups] false
101 def initialize( config )
101 def initialize( config )
102 @config = config
102 @config = config
103
103
104 self.top_align = self.top_font = self.right_align = self.right_font = 0
104 self.top_align = self.top_font = self.right_align = self.right_font = 0
105
105
106 init_with({
106 init_with({
107 :width => 500,
107 :width => 500,
108 :height => 300,
108 :height => 300,
109 :show_x_guidelines => false,
109 :show_x_guidelines => false,
110 :show_y_guidelines => true,
110 :show_y_guidelines => true,
111 :show_data_values => true,
111 :show_data_values => true,
112
112
113 :min_scale_value => 0,
113 # :min_scale_value => 0,
114
114
115 :show_x_labels => true,
115 :show_x_labels => true,
116 :stagger_x_labels => false,
116 :stagger_x_labels => false,
117 :rotate_x_labels => false,
117 :rotate_x_labels => false,
118 :step_x_labels => 1,
118 :step_x_labels => 1,
119 :step_include_first_x_label => true,
119 :step_include_first_x_label => true,
120
120
121 :show_y_labels => true,
121 :show_y_labels => true,
122 :rotate_y_labels => false,
122 :rotate_y_labels => false,
123 :stagger_y_labels => false,
123 :stagger_y_labels => false,
124 :scale_integers => false,
124 :scale_integers => false,
125
125
126 :show_x_title => false,
126 :show_x_title => false,
127 :x_title => 'X Field names',
127 :x_title => 'X Field names',
128
128
129 :show_y_title => false,
129 :show_y_title => false,
130 :y_title_text_direction => :bt,
130 :y_title_text_direction => :bt,
131 :y_title => 'Y Scale',
131 :y_title => 'Y Scale',
132
132
133 :show_graph_title => false,
133 :show_graph_title => false,
134 :graph_title => 'Graph Title',
134 :graph_title => 'Graph Title',
135 :show_graph_subtitle => false,
135 :show_graph_subtitle => false,
136 :graph_subtitle => 'Graph Sub Title',
136 :graph_subtitle => 'Graph Sub Title',
137 :key => true,
137 :key => true,
138 :key_position => :right, # bottom or right
138 :key_position => :right, # bottom or right
139
139
140 :font_size =>10,
140 :font_size =>12,
141 :title_font_size =>12,
141 :title_font_size =>16,
142 :subtitle_font_size =>14,
142 :subtitle_font_size =>14,
143 :x_label_font_size =>11,
143 :x_label_font_size =>12,
144 :x_title_font_size =>14,
144 :x_title_font_size =>14,
145 :y_label_font_size =>11,
145 :y_label_font_size =>12,
146 :y_title_font_size =>14,
146 :y_title_font_size =>14,
147 :key_font_size => 9,
147 :key_font_size =>10,
148
148
149 :no_css =>false,
149 :no_css =>false,
150 :add_popups =>false,
150 :add_popups =>false,
151 })
151 })
152
152
153 set_defaults if methods.include? "set_defaults"
153 set_defaults if methods.include? "set_defaults"
154
154
155 init_with config
155 init_with config
156 end
156 end
157
157
158
158
159 # This method allows you do add data to the graph object.
159 # This method allows you do add data to the graph object.
160 # It can be called several times to add more data sets in.
160 # It can be called several times to add more data sets in.
161 #
161 #
162 # data_sales_02 = [12, 45, 21];
162 # data_sales_02 = [12, 45, 21];
163 #
163 #
164 # graph.add_data({
164 # graph.add_data({
165 # :data => data_sales_02,
165 # :data => data_sales_02,
166 # :title => 'Sales 2002'
166 # :title => 'Sales 2002'
167 # })
167 # })
168 def add_data conf
168 def add_data conf
169 @data = [] unless defined? @data
169 @data = [] unless defined? @data
170
170
171 if conf[:data] and conf[:data].kind_of? Array
171 if conf[:data] and conf[:data].kind_of? Array
172 @data << conf
172 @data << conf
173 else
173 else
174 raise "No data provided by #{conf.inspect}"
174 raise "No data provided by #{conf.inspect}"
175 end
175 end
176 end
176 end
177
177
178
178
179 # This method removes all data from the object so that you can
179 # This method removes all data from the object so that you can
180 # reuse it to create a new graph but with the same config options.
180 # reuse it to create a new graph but with the same config options.
181 #
181 #
182 # graph.clear_data
182 # graph.clear_data
183 def clear_data
183 def clear_data
184 @data = []
184 @data = []
185 end
185 end
186
186
187
187
188 # This method processes the template with the data and
188 # This method processes the template with the data and
189 # config which has been set and returns the resulting SVG.
189 # config which has been set and returns the resulting SVG.
190 #
190 #
191 # This method will croak unless at least one data set has
191 # This method will croak unless at least one data set has
192 # been added to the graph object.
192 # been added to the graph object.
193 #
193 #
194 # print graph.burn
194 # print graph.burn
195 def burn
195 def burn
196 raise "No data available" unless @data.size > 0
196 raise "No data available" unless @data.size > 0
197
197
198 calculations if methods.include? 'calculations'
198 calculations if methods.include? 'calculations'
199
199
200 start_svg
200 start_svg
201 calculate_graph_dimensions
201 calculate_graph_dimensions
202 @foreground = Element.new( "g" )
202 @foreground = Element.new( "g" )
203 draw_graph
203 draw_graph
204 draw_titles
204 draw_titles
205 draw_legend
205 draw_legend
206 draw_data
206 draw_data
207 @graph.add_element( @foreground )
207 @graph.add_element( @foreground )
208 style
208 style
209
209
210 data = ""
210 data = ""
211 @doc.write( data, 0 )
211 @doc.write( data, 0 )
212
212
213 if @config[:compress]
213 if @config[:compress]
214 if @@__have_zlib
214 if @@__have_zlib
215 inp, out = IO.pipe
215 inp, out = IO.pipe
216 gz = Zlib::GzipWriter.new( out )
216 gz = Zlib::GzipWriter.new( out )
217 gz.write data
217 gz.write data
218 gz.close
218 gz.close
219 data = inp.read
219 data = inp.read
220 else
220 else
221 data << "<!-- Ruby Zlib not available for SVGZ -->";
221 data << "<!-- Ruby Zlib not available for SVGZ -->";
222 end
222 end
223 end
223 end
224
224
225 return data
225 return data
226 end
226 end
227
227
228
228
229 # Set the height of the graph box, this is the total height
229 # Set the height of the graph box, this is the total height
230 # of the SVG box created - not the graph it self which auto
230 # of the SVG box created - not the graph it self which auto
231 # scales to fix the space.
231 # scales to fix the space.
232 attr_accessor :height
232 attr_accessor :height
233 # Set the width of the graph box, this is the total width
233 # Set the width of the graph box, this is the total width
234 # of the SVG box created - not the graph it self which auto
234 # of the SVG box created - not the graph it self which auto
235 # scales to fix the space.
235 # scales to fix the space.
236 attr_accessor :width
236 attr_accessor :width
237 # Set the path to an external stylesheet, set to '' if
237 # Set the path to an external stylesheet, set to '' if
238 # you want to revert back to using the defaut internal version.
238 # you want to revert back to using the defaut internal version.
239 #
239 #
240 # To create an external stylesheet create a graph using the
240 # To create an external stylesheet create a graph using the
241 # default internal version and copy the stylesheet section to
241 # default internal version and copy the stylesheet section to
242 # an external file and edit from there.
242 # an external file and edit from there.
243 attr_accessor :style_sheet
243 attr_accessor :style_sheet
244 # (Bool) Show the value of each element of data on the graph
244 # (Bool) Show the value of each element of data on the graph
245 attr_accessor :show_data_values
245 attr_accessor :show_data_values
246 # The point at which the Y axis starts, defaults to '0',
246 # The point at which the Y axis starts, defaults to '0',
247 # if set to nil it will default to the minimum data value.
247 # if set to nil it will default to the minimum data value.
248 attr_accessor :min_scale_value
248 attr_accessor :min_scale_value
249 # Whether to show labels on the X axis or not, defaults
249 # Whether to show labels on the X axis or not, defaults
250 # to true, set to false if you want to turn them off.
250 # to true, set to false if you want to turn them off.
251 attr_accessor :show_x_labels
251 attr_accessor :show_x_labels
252 # This puts the X labels at alternative levels so if they
252 # This puts the X labels at alternative levels so if they
253 # are long field names they will not overlap so easily.
253 # are long field names they will not overlap so easily.
254 # Default it false, to turn on set to true.
254 # Default it false, to turn on set to true.
255 attr_accessor :stagger_x_labels
255 attr_accessor :stagger_x_labels
256 # This puts the Y labels at alternative levels so if they
256 # This puts the Y labels at alternative levels so if they
257 # are long field names they will not overlap so easily.
257 # are long field names they will not overlap so easily.
258 # Default it false, to turn on set to true.
258 # Default it false, to turn on set to true.
259 attr_accessor :stagger_y_labels
259 attr_accessor :stagger_y_labels
260 # This turns the X axis labels by 90 degrees.
260 # This turns the X axis labels by 90 degrees.
261 # Default it false, to turn on set to true.
261 # Default it false, to turn on set to true.
262 attr_accessor :rotate_x_labels
262 attr_accessor :rotate_x_labels
263 # This turns the Y axis labels by 90 degrees.
263 # This turns the Y axis labels by 90 degrees.
264 # Default it false, to turn on set to true.
264 # Default it false, to turn on set to true.
265 attr_accessor :rotate_y_labels
265 attr_accessor :rotate_y_labels
266 # How many "steps" to use between displayed X axis labels,
266 # How many "steps" to use between displayed X axis labels,
267 # a step of one means display every label, a step of two results
267 # a step of one means display every label, a step of two results
268 # in every other label being displayed (label <gap> label <gap> label),
268 # in every other label being displayed (label <gap> label <gap> label),
269 # a step of three results in every third label being displayed
269 # a step of three results in every third label being displayed
270 # (label <gap> <gap> label <gap> <gap> label) and so on.
270 # (label <gap> <gap> label <gap> <gap> label) and so on.
271 attr_accessor :step_x_labels
271 attr_accessor :step_x_labels
272 # Whether to (when taking "steps" between X axis labels) step from
272 # Whether to (when taking "steps" between X axis labels) step from
273 # the first label (i.e. always include the first label) or step from
273 # the first label (i.e. always include the first label) or step from
274 # the X axis origin (i.e. start with a gap if step_x_labels is greater
274 # the X axis origin (i.e. start with a gap if step_x_labels is greater
275 # than one).
275 # than one).
276 attr_accessor :step_include_first_x_label
276 attr_accessor :step_include_first_x_label
277 # Whether to show labels on the Y axis or not, defaults
277 # Whether to show labels on the Y axis or not, defaults
278 # to true, set to false if you want to turn them off.
278 # to true, set to false if you want to turn them off.
279 attr_accessor :show_y_labels
279 attr_accessor :show_y_labels
280 # Ensures only whole numbers are used as the scale divisions.
280 # Ensures only whole numbers are used as the scale divisions.
281 # Default it false, to turn on set to true. This has no effect if
281 # Default it false, to turn on set to true. This has no effect if
282 # scale divisions are less than 1.
282 # scale divisions are less than 1.
283 attr_accessor :scale_integers
283 attr_accessor :scale_integers
284 # This defines the gap between markers on the Y axis,
284 # This defines the gap between markers on the Y axis,
285 # default is a 10th of the max_value, e.g. you will have
285 # default is a 10th of the max_value, e.g. you will have
286 # 10 markers on the Y axis. NOTE: do not set this too
286 # 10 markers on the Y axis. NOTE: do not set this too
287 # low - you are limited to 999 markers, after that the
287 # low - you are limited to 999 markers, after that the
288 # graph won't generate.
288 # graph won't generate.
289 attr_accessor :scale_divisions
289 attr_accessor :scale_divisions
290 # Whether to show the title under the X axis labels,
290 # Whether to show the title under the X axis labels,
291 # default is false, set to true to show.
291 # default is false, set to true to show.
292 attr_accessor :show_x_title
292 attr_accessor :show_x_title
293 # What the title under X axis should be, e.g. 'Months'.
293 # What the title under X axis should be, e.g. 'Months'.
294 attr_accessor :x_title
294 attr_accessor :x_title
295 # Whether to show the title under the Y axis labels,
295 # Whether to show the title under the Y axis labels,
296 # default is false, set to true to show.
296 # default is false, set to true to show.
297 attr_accessor :show_y_title
297 attr_accessor :show_y_title
298 # Aligns writing mode for Y axis label.
298 # Aligns writing mode for Y axis label.
299 # Defaults to :bt (Bottom to Top).
299 # Defaults to :bt (Bottom to Top).
300 # Change to :tb (Top to Bottom) to reverse.
300 # Change to :tb (Top to Bottom) to reverse.
301 attr_accessor :y_title_text_direction
301 attr_accessor :y_title_text_direction
302 # What the title under Y axis should be, e.g. 'Sales in thousands'.
302 # What the title under Y axis should be, e.g. 'Sales in thousands'.
303 attr_accessor :y_title
303 attr_accessor :y_title
304 # Whether to show a title on the graph, defaults
304 # Whether to show a title on the graph, defaults
305 # to false, set to true to show.
305 # to false, set to true to show.
306 attr_accessor :show_graph_title
306 attr_accessor :show_graph_title
307 # What the title on the graph should be.
307 # What the title on the graph should be.
308 attr_accessor :graph_title
308 attr_accessor :graph_title
309 # Whether to show a subtitle on the graph, defaults
309 # Whether to show a subtitle on the graph, defaults
310 # to false, set to true to show.
310 # to false, set to true to show.
311 attr_accessor :show_graph_subtitle
311 attr_accessor :show_graph_subtitle
312 # What the subtitle on the graph should be.
312 # What the subtitle on the graph should be.
313 attr_accessor :graph_subtitle
313 attr_accessor :graph_subtitle
314 # Whether to show a key, defaults to false, set to
314 # Whether to show a key, defaults to false, set to
315 # true if you want to show it.
315 # true if you want to show it.
316 attr_accessor :key
316 attr_accessor :key
317 # Where the key should be positioned, defaults to
317 # Where the key should be positioned, defaults to
318 # :right, set to :bottom if you want to move it.
318 # :right, set to :bottom if you want to move it.
319 attr_accessor :key_position
319 attr_accessor :key_position
320 # Set the font size (in points) of the data point labels
320 # Set the font size (in points) of the data point labels
321 attr_accessor :font_size
321 attr_accessor :font_size
322 # Set the font size of the X axis labels
322 # Set the font size of the X axis labels
323 attr_accessor :x_label_font_size
323 attr_accessor :x_label_font_size
324 # Set the font size of the X axis title
324 # Set the font size of the X axis title
325 attr_accessor :x_title_font_size
325 attr_accessor :x_title_font_size
326 # Set the font size of the Y axis labels
326 # Set the font size of the Y axis labels
327 attr_accessor :y_label_font_size
327 attr_accessor :y_label_font_size
328 # Set the font size of the Y axis title
328 # Set the font size of the Y axis title
329 attr_accessor :y_title_font_size
329 attr_accessor :y_title_font_size
330 # Set the title font size
330 # Set the title font size
331 attr_accessor :title_font_size
331 attr_accessor :title_font_size
332 # Set the subtitle font size
332 # Set the subtitle font size
333 attr_accessor :subtitle_font_size
333 attr_accessor :subtitle_font_size
334 # Set the key font size
334 # Set the key font size
335 attr_accessor :key_font_size
335 attr_accessor :key_font_size
336 # Show guidelines for the X axis
336 # Show guidelines for the X axis
337 attr_accessor :show_x_guidelines
337 attr_accessor :show_x_guidelines
338 # Show guidelines for the Y axis
338 # Show guidelines for the Y axis
339 attr_accessor :show_y_guidelines
339 attr_accessor :show_y_guidelines
340 # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
340 # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
341 # not using CSS can result in larger SVGs as well as making it impossible to
341 # not using CSS can result in larger SVGs as well as making it impossible to
342 # change colors after the chart is generated. Defaults to false.
342 # change colors after the chart is generated. Defaults to false.
343 attr_accessor :no_css
343 attr_accessor :no_css
344 # Add popups for the data points on some graphs
344 # Add popups for the data points on some graphs
345 attr_accessor :add_popups
345 attr_accessor :add_popups
346
346
347
347
348 protected
348 protected
349
349
350 def sort( *arrys )
350 def sort( *arrys )
351 sort_multiple( arrys )
351 sort_multiple( arrys )
352 end
352 end
353
353
354 # Overwrite configuration options with supplied options. Used
354 # Overwrite configuration options with supplied options. Used
355 # by subclasses.
355 # by subclasses.
356 def init_with config
356 def init_with config
357 config.each { |key, value|
357 config.each { |key, value|
358 self.send( key.to_s+"=", value ) if methods.include? key.to_s
358 self.send( key.to_s+"=", value ) if methods.include? key.to_s
359 }
359 }
360 end
360 end
361
361
362 attr_accessor :top_align, :top_font, :right_align, :right_font
362 attr_accessor :top_align, :top_font, :right_align, :right_font
363
363
364 KEY_BOX_SIZE = 12
364 KEY_BOX_SIZE = 12
365
365
366 # Override this (and call super) to change the margin to the left
366 # Override this (and call super) to change the margin to the left
367 # of the plot area. Results in @border_left being set.
367 # of the plot area. Results in @border_left being set.
368 def calculate_left_margin
368 def calculate_left_margin
369 @border_left = 7
369 @border_left = 7
370 # Check for Y labels
370 # Check for Y labels
371 max_y_label_height_px = rotate_y_labels ?
371 max_y_label_height_px = rotate_y_labels ?
372 y_label_font_size :
372 y_label_font_size :
373 get_y_labels.max{|a,b|
373 get_y_labels.max{|a,b|
374 a.to_s.length<=>b.to_s.length
374 a.to_s.length<=>b.to_s.length
375 }.to_s.length * y_label_font_size * 0.6
375 }.to_s.length * y_label_font_size * 0.6
376 @border_left += max_y_label_height_px if show_y_labels
376 @border_left += max_y_label_height_px if show_y_labels
377 @border_left += max_y_label_height_px + 10 if stagger_y_labels
377 @border_left += max_y_label_height_px + 10 if stagger_y_labels
378 @border_left += y_title_font_size + 5 if show_y_title
378 @border_left += y_title_font_size + 5 if show_y_title
379 end
379 end
380
380
381
381
382 # Calculates the width of the widest Y label. This will be the
382 # Calculates the width of the widest Y label. This will be the
383 # character height if the Y labels are rotated
383 # character height if the Y labels are rotated
384 def max_y_label_width_px
384 def max_y_label_width_px
385 return font_size if rotate_y_labels
385 return font_size if rotate_y_labels
386 end
386 end
387
387
388
388
389 # Override this (and call super) to change the margin to the right
389 # Override this (and call super) to change the margin to the right
390 # of the plot area. Results in @border_right being set.
390 # of the plot area. Results in @border_right being set.
391 def calculate_right_margin
391 def calculate_right_margin
392 @border_right = 7
392 @border_right = 7
393 if key and key_position == :right
393 if key and key_position == :right
394 val = keys.max { |a,b| a.length <=> b.length }
394 val = keys.max { |a,b| a.length <=> b.length }
395 @border_right += val.length * key_font_size * 0.7
395 @border_right += val.length * key_font_size * 0.6
396 @border_right += KEY_BOX_SIZE
396 @border_right += KEY_BOX_SIZE
397 @border_right += 10 # Some padding around the box
397 @border_right += 10 # Some padding around the box
398 end
398 end
399 end
399 end
400
400
401
401
402 # Override this (and call super) to change the margin to the top
402 # Override this (and call super) to change the margin to the top
403 # of the plot area. Results in @border_top being set.
403 # of the plot area. Results in @border_top being set.
404 def calculate_top_margin
404 def calculate_top_margin
405 @border_top = 5
405 @border_top = 5
406 @border_top += title_font_size if show_graph_title
406 @border_top += title_font_size if show_graph_title
407 @border_top += 5
407 @border_top += 5
408 @border_top += subtitle_font_size if show_graph_subtitle
408 @border_top += subtitle_font_size if show_graph_subtitle
409 end
409 end
410
410
411
411
412 # Adds pop-up point information to a graph.
412 # Adds pop-up point information to a graph.
413 def add_popup( x, y, label )
413 def add_popup( x, y, label )
414 txt_width = label.length * font_size * 0.6 + 10
414 txt_width = label.length * font_size * 0.6 + 10
415 tx = (x+txt_width > width ? x-5 : x+5)
415 tx = (x+txt_width > width ? x-5 : x+5)
416 t = @foreground.add_element( "text", {
416 t = @foreground.add_element( "text", {
417 "x" => tx.to_s,
417 "x" => tx.to_s,
418 "y" => (y - font_size).to_s,
418 "y" => (y - font_size).to_s,
419 "visibility" => "hidden",
419 "visibility" => "hidden",
420 })
420 })
421 t.attributes["style"] = "fill: #000; "+
421 t.attributes["style"] = "fill: #000; "+
422 (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
422 (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
423 t.text = label.to_s
423 t.text = label.to_s
424 t.attributes["id"] = t.id.to_s
424 t.attributes["id"] = t.object_id.to_s
425
425
426 @foreground.add_element( "circle", {
426 @foreground.add_element( "circle", {
427 "cx" => x.to_s,
427 "cx" => x.to_s,
428 "cy" => y.to_s,
428 "cy" => y.to_s,
429 "r" => "10",
429 "r" => "10",
430 "style" => "opacity: 0",
430 "style" => "opacity: 0",
431 "onmouseover" =>
431 "onmouseover" =>
432 "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
432 "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
433 "onmouseout" =>
433 "onmouseout" =>
434 "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
434 "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
435 })
435 })
436
436
437 end
437 end
438
438
439
439
440 # Override this (and call super) to change the margin to the bottom
440 # Override this (and call super) to change the margin to the bottom
441 # of the plot area. Results in @border_bottom being set.
441 # of the plot area. Results in @border_bottom being set.
442 def calculate_bottom_margin
442 def calculate_bottom_margin
443 @border_bottom = 7
443 @border_bottom = 7
444 if key and key_position == :bottom
444 if key and key_position == :bottom
445 @border_bottom += @data.size * (font_size + 5)
445 @border_bottom += @data.size * (font_size + 5)
446 @border_bottom += 10
446 @border_bottom += 10
447 end
447 end
448 if show_x_labels
448 if show_x_labels
449 max_x_label_height_px = rotate_x_labels ?
449 max_x_label_height_px = (not rotate_x_labels) ?
450 get_x_labels.max{|a,b|
450 x_label_font_size :
451 a.length<=>b.length
451 get_x_labels.max{|a,b|
452 }.length * x_label_font_size * 0.6 :
452 a.to_s.length<=>b.to_s.length
453 x_label_font_size
453 }.to_s.length * x_label_font_size * 0.6
454 @border_bottom += max_x_label_height_px
454 @border_bottom += max_x_label_height_px
455 @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
455 @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
456 end
456 end
457 @border_bottom += x_title_font_size + 5 if show_x_title
457 @border_bottom += x_title_font_size + 5 if show_x_title
458 end
458 end
459
459
460
460
461 # Draws the background, axis, and labels.
461 # Draws the background, axis, and labels.
462 def draw_graph
462 def draw_graph
463 @graph = @root.add_element( "g", {
463 @graph = @root.add_element( "g", {
464 "transform" => "translate( #@border_left #@border_top )"
464 "transform" => "translate( #@border_left #@border_top )"
465 })
465 })
466
466
467 # Background
467 # Background
468 @graph.add_element( "rect", {
468 @graph.add_element( "rect", {
469 "x" => "0",
469 "x" => "0",
470 "y" => "0",
470 "y" => "0",
471 "width" => @graph_width.to_s,
471 "width" => @graph_width.to_s,
472 "height" => @graph_height.to_s,
472 "height" => @graph_height.to_s,
473 "class" => "graphBackground"
473 "class" => "graphBackground"
474 })
474 })
475
475
476 # Axis
476 # Axis
477 @graph.add_element( "path", {
477 @graph.add_element( "path", {
478 "d" => "M 0 0 v#@graph_height",
478 "d" => "M 0 0 v#@graph_height",
479 "class" => "axis",
479 "class" => "axis",
480 "id" => "xAxis"
480 "id" => "xAxis"
481 })
481 })
482 @graph.add_element( "path", {
482 @graph.add_element( "path", {
483 "d" => "M 0 #@graph_height h#@graph_width",
483 "d" => "M 0 #@graph_height h#@graph_width",
484 "class" => "axis",
484 "class" => "axis",
485 "id" => "yAxis"
485 "id" => "yAxis"
486 })
486 })
487
487
488 draw_x_labels
488 draw_x_labels
489 draw_y_labels
489 draw_y_labels
490 end
490 end
491
491
492
492
493 # Where in the X area the label is drawn
493 # Where in the X area the label is drawn
494 # Centered in the field, should be width/2. Start, 0.
494 # Centered in the field, should be width/2. Start, 0.
495 def x_label_offset( width )
495 def x_label_offset( width )
496 0
496 0
497 end
497 end
498
498
499 def make_datapoint_text( x, y, value, style="" )
499 def make_datapoint_text( x, y, value, style="" )
500 if show_data_values
500 if show_data_values
501 @foreground.add_element( "text", {
501 @foreground.add_element( "text", {
502 "x" => x.to_s,
502 "x" => x.to_s,
503 "y" => y.to_s,
503 "y" => y.to_s,
504 "class" => "dataPointLabel",
504 "class" => "dataPointLabel",
505 "style" => "#{style} stroke: #fff; stroke-width: 2;"
505 "style" => "#{style} stroke: #fff; stroke-width: 2;"
506 }).text = value.to_s
506 }).text = value.to_s
507 text = @foreground.add_element( "text", {
507 text = @foreground.add_element( "text", {
508 "x" => x.to_s,
508 "x" => x.to_s,
509 "y" => y.to_s,
509 "y" => y.to_s,
510 "class" => "dataPointLabel"
510 "class" => "dataPointLabel"
511 })
511 })
512 text.text = value.to_s
512 text.text = value.to_s
513 text.attributes["style"] = style if style.length > 0
513 text.attributes["style"] = style if style.length > 0
514 end
514 end
515 end
515 end
516
516
517
517
518 # Draws the X axis labels
518 # Draws the X axis labels
519 def draw_x_labels
519 def draw_x_labels
520 stagger = x_label_font_size + 5
520 stagger = x_label_font_size + 5
521 if show_x_labels
521 if show_x_labels
522 label_width = field_width
522 label_width = field_width
523
523
524 count = 0
524 count = 0
525 for label in get_x_labels
525 for label in get_x_labels
526 if step_include_first_x_label == true then
526 if step_include_first_x_label == true then
527 step = count % step_x_labels
527 step = count % step_x_labels
528 else
528 else
529 step = (count + 1) % step_x_labels
529 step = (count + 1) % step_x_labels
530 end
530 end
531
531
532 if step == 0 then
532 if step == 0 then
533 text = @graph.add_element( "text" )
533 text = @graph.add_element( "text" )
534 text.attributes["class"] = "xAxisLabels"
534 text.attributes["class"] = "xAxisLabels"
535 text.text = label.to_s
535 text.text = label.to_s
536
536
537 x = count * label_width + x_label_offset( label_width )
537 x = count * label_width + x_label_offset( label_width )
538 y = @graph_height + x_label_font_size + 3
538 y = @graph_height + x_label_font_size + 3
539 t = 0 - (font_size / 2)
539 t = 0 - (font_size / 2)
540
540
541 if stagger_x_labels and count % 2 == 1
541 if stagger_x_labels and count % 2 == 1
542 y += stagger
542 y += stagger
543 @graph.add_element( "path", {
543 @graph.add_element( "path", {
544 "d" => "M#{x} #@graph_height v#{stagger}",
544 "d" => "M#{x} #@graph_height v#{stagger}",
545 "class" => "staggerGuideLine"
545 "class" => "staggerGuideLine"
546 })
546 })
547 end
547 end
548
548
549 text.attributes["x"] = x.to_s
549 text.attributes["x"] = x.to_s
550 text.attributes["y"] = y.to_s
550 text.attributes["y"] = y.to_s
551 if rotate_x_labels
551 if rotate_x_labels
552 text.attributes["transform"] =
552 text.attributes["transform"] =
553 "rotate( 90 #{x} #{y-x_label_font_size} )"+
553 "rotate( 90 #{x} #{y-x_label_font_size} )"+
554 " translate( 0 -#{x_label_font_size/4} )"
554 " translate( 0 -#{x_label_font_size/4} )"
555 text.attributes["style"] = "text-anchor: start"
555 text.attributes["style"] = "text-anchor: start"
556 else
556 else
557 text.attributes["style"] = "text-anchor: middle"
557 text.attributes["style"] = "text-anchor: middle"
558 end
558 end
559 end
559 end
560
560
561 draw_x_guidelines( label_width, count ) if show_x_guidelines
561 draw_x_guidelines( label_width, count ) if show_x_guidelines
562 count += 1
562 count += 1
563 end
563 end
564 end
564 end
565 end
565 end
566
566
567
567
568 # Where in the Y area the label is drawn
568 # Where in the Y area the label is drawn
569 # Centered in the field, should be width/2. Start, 0.
569 # Centered in the field, should be width/2. Start, 0.
570 def y_label_offset( height )
570 def y_label_offset( height )
571 0
571 0
572 end
572 end
573
573
574
574
575 def field_width
575 def field_width
576 (@graph_width.to_f - font_size*2*right_font) /
576 (@graph_width.to_f - font_size*2*right_font) /
577 (get_x_labels.length - right_align)
577 (get_x_labels.length - right_align)
578 end
578 end
579
579
580
580
581 def field_height
581 def field_height
582 (@graph_height.to_f - font_size*2*top_font) /
582 (@graph_height.to_f - font_size*2*top_font) /
583 (get_y_labels.length - top_align)
583 (get_y_labels.length - top_align)
584 end
584 end
585
585
586
586
587 # Draws the Y axis labels
587 # Draws the Y axis labels
588 def draw_y_labels
588 def draw_y_labels
589 stagger = y_label_font_size + 5
589 stagger = y_label_font_size + 5
590 if show_y_labels
590 if show_y_labels
591 label_height = field_height
591 label_height = field_height
592
592
593 count = 0
593 count = 0
594 y_offset = @graph_height + y_label_offset( label_height )
594 y_offset = @graph_height + y_label_offset( label_height )
595 y_offset += font_size/1.2 unless rotate_y_labels
595 y_offset += font_size/1.2 unless rotate_y_labels
596 for label in get_y_labels
596 for label in get_y_labels
597 y = y_offset - (label_height * count)
597 y = y_offset - (label_height * count)
598 x = rotate_y_labels ? 0 : -3
598 x = rotate_y_labels ? 0 : -3
599
599
600 if stagger_y_labels and count % 2 == 1
600 if stagger_y_labels and count % 2 == 1
601 x -= stagger
601 x -= stagger
602 @graph.add_element( "path", {
602 @graph.add_element( "path", {
603 "d" => "M#{x} #{y} h#{stagger}",
603 "d" => "M#{x} #{y} h#{stagger}",
604 "class" => "staggerGuideLine"
604 "class" => "staggerGuideLine"
605 })
605 })
606 end
606 end
607
607
608 text = @graph.add_element( "text", {
608 text = @graph.add_element( "text", {
609 "x" => x.to_s,
609 "x" => x.to_s,
610 "y" => y.to_s,
610 "y" => y.to_s,
611 "class" => "yAxisLabels"
611 "class" => "yAxisLabels"
612 })
612 })
613 text.text = label.to_s
613 text.text = label.to_s
614 if rotate_y_labels
614 if rotate_y_labels
615 text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
615 text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
616 "rotate( 90 #{x} #{y} ) "
616 "rotate( 90 #{x} #{y} ) "
617 text.attributes["style"] = "text-anchor: middle"
617 text.attributes["style"] = "text-anchor: middle"
618 else
618 else
619 text.attributes["y"] = (y - (y_label_font_size/2)).to_s
619 text.attributes["y"] = (y - (y_label_font_size/2)).to_s
620 text.attributes["style"] = "text-anchor: end"
620 text.attributes["style"] = "text-anchor: end"
621 end
621 end
622 draw_y_guidelines( label_height, count ) if show_y_guidelines
622 draw_y_guidelines( label_height, count ) if show_y_guidelines
623 count += 1
623 count += 1
624 end
624 end
625 end
625 end
626 end
626 end
627
627
628
628
629 # Draws the X axis guidelines
629 # Draws the X axis guidelines
630 def draw_x_guidelines( label_height, count )
630 def draw_x_guidelines( label_height, count )
631 if count != 0
631 if count != 0
632 @graph.add_element( "path", {
632 @graph.add_element( "path", {
633 "d" => "M#{label_height*count} 0 v#@graph_height",
633 "d" => "M#{label_height*count} 0 v#@graph_height",
634 "class" => "guideLines"
634 "class" => "guideLines"
635 })
635 })
636 end
636 end
637 end
637 end
638
638
639
639
640 # Draws the Y axis guidelines
640 # Draws the Y axis guidelines
641 def draw_y_guidelines( label_height, count )
641 def draw_y_guidelines( label_height, count )
642 if count != 0
642 if count != 0
643 @graph.add_element( "path", {
643 @graph.add_element( "path", {
644 "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
644 "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
645 "class" => "guideLines"
645 "class" => "guideLines"
646 })
646 })
647 end
647 end
648 end
648 end
649
649
650
650
651 # Draws the graph title and subtitle
651 # Draws the graph title and subtitle
652 def draw_titles
652 def draw_titles
653 if show_graph_title
653 if show_graph_title
654 @root.add_element( "text", {
654 @root.add_element( "text", {
655 "x" => (width / 2).to_s,
655 "x" => (width / 2).to_s,
656 "y" => (title_font_size).to_s,
656 "y" => (title_font_size).to_s,
657 "class" => "mainTitle"
657 "class" => "mainTitle"
658 }).text = graph_title.to_s
658 }).text = graph_title.to_s
659 end
659 end
660
660
661 if show_graph_subtitle
661 if show_graph_subtitle
662 y_subtitle = show_graph_title ?
662 y_subtitle = show_graph_title ?
663 title_font_size + 10 :
663 title_font_size + 10 :
664 subtitle_font_size
664 subtitle_font_size
665 @root.add_element("text", {
665 @root.add_element("text", {
666 "x" => (width / 2).to_s,
666 "x" => (width / 2).to_s,
667 "y" => (y_subtitle).to_s,
667 "y" => (y_subtitle).to_s,
668 "class" => "subTitle"
668 "class" => "subTitle"
669 }).text = graph_subtitle.to_s
669 }).text = graph_subtitle.to_s
670 end
670 end
671
671
672 if show_x_title
672 if show_x_title
673 y = @graph_height + @border_top + x_title_font_size
673 y = @graph_height + @border_top + x_title_font_size
674 if show_x_labels
674 if show_x_labels
675 y += x_label_font_size + 5 if stagger_x_labels
675 y += x_label_font_size + 5 if stagger_x_labels
676 y += x_label_font_size + 5
676 y += x_label_font_size + 5
677 end
677 end
678 x = width / 2
678 x = width / 2
679
679
680 @root.add_element("text", {
680 @root.add_element("text", {
681 "x" => x.to_s,
681 "x" => x.to_s,
682 "y" => y.to_s,
682 "y" => y.to_s,
683 "class" => "xAxisTitle",
683 "class" => "xAxisTitle",
684 }).text = x_title.to_s
684 }).text = x_title.to_s
685 end
685 end
686
686
687 if show_y_title
687 if show_y_title
688 x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
688 x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
689 y = height / 2
689 y = height / 2
690
690
691 text = @root.add_element("text", {
691 text = @root.add_element("text", {
692 "x" => x.to_s,
692 "x" => x.to_s,
693 "y" => y.to_s,
693 "y" => y.to_s,
694 "class" => "yAxisTitle",
694 "class" => "yAxisTitle",
695 })
695 })
696 text.text = y_title.to_s
696 text.text = y_title.to_s
697 if y_title_text_direction == :bt
697 if y_title_text_direction == :bt
698 text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
698 text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
699 else
699 else
700 text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
700 text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
701 end
701 end
702 end
702 end
703 end
703 end
704
704
705 def keys
705 def keys
706 return @data.collect{ |d| d[:title] }
706 return @data.collect{ |d| d[:title] }
707 end
707 end
708
708
709 # Draws the legend on the graph
709 # Draws the legend on the graph
710 def draw_legend
710 def draw_legend
711 if key
711 if key
712 group = @root.add_element( "g" )
712 group = @root.add_element( "g" )
713
713
714 key_count = 0
714 key_count = 0
715 for key_name in keys
715 for key_name in keys
716 y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
716 y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
717 group.add_element( "rect", {
717 group.add_element( "rect", {
718 "x" => 0.to_s,
718 "x" => 0.to_s,
719 "y" => y_offset.to_s,
719 "y" => y_offset.to_s,
720 "width" => KEY_BOX_SIZE.to_s,
720 "width" => KEY_BOX_SIZE.to_s,
721 "height" => KEY_BOX_SIZE.to_s,
721 "height" => KEY_BOX_SIZE.to_s,
722 "class" => "key#{key_count+1}"
722 "class" => "key#{key_count+1}"
723 })
723 })
724 group.add_element( "text", {
724 group.add_element( "text", {
725 "x" => (KEY_BOX_SIZE + 5).to_s,
725 "x" => (KEY_BOX_SIZE + 5).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE - 2).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE).to_s,
727 "class" => "keyText"
727 "class" => "keyText"
728 }).text = key_name.to_s
728 }).text = key_name.to_s
729 key_count += 1
729 key_count += 1
730 end
730 end
731
731
732 case key_position
732 case key_position
733 when :right
733 when :right
734 x_offset = @graph_width + @border_left + 10
734 x_offset = @graph_width + @border_left + 10
735 y_offset = @border_top + 20
735 y_offset = @border_top + 20
736 when :bottom
736 when :bottom
737 x_offset = @border_left + 20
737 x_offset = @border_left + 20
738 y_offset = @border_top + @graph_height + 5
738 y_offset = @border_top + @graph_height + 5
739 if show_x_labels
739 if show_x_labels
740 max_x_label_height_px = rotate_x_labels ?
740 max_x_label_height_px = (not rotate_x_labels) ?
741 get_x_labels.max{|a,b|
741 x_label_font_size :
742 a.length<=>b.length
742 get_x_labels.max{|a,b|
743 }.length * x_label_font_size :
743 a.to_s.length<=>b.to_s.length
744 x_label_font_size
744 }.to_s.length * x_label_font_size * 0.6
745 y_offset += max_x_label_height_px
745 x_label_font_size
746 y_offset += max_x_label_height_px + 5 if stagger_x_labels
746 y_offset += max_x_label_height_px
747 end
747 y_offset += max_x_label_height_px + 5 if stagger_x_labels
748 y_offset += x_title_font_size + 5 if show_x_title
748 end
749 end
749 y_offset += x_title_font_size + 5 if show_x_title
750 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
750 end
751 end
751 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
752 end
752 end
753
753 end
754
754
755 private
755
756
756 private
757 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
757
758 if lo < hi
758 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
759 p = partition(arrys,lo,hi)
759 if lo < hi
760 sort_multiple(arrys, lo, p-1)
760 p = partition(arrys,lo,hi)
761 sort_multiple(arrys, p+1, hi)
761 sort_multiple(arrys, lo, p-1)
762 end
762 sort_multiple(arrys, p+1, hi)
763 arrys
763 end
764 end
764 arrys
765
765 end
766 def partition( arrys, lo, hi )
766
767 p = arrys[0][lo]
767 def partition( arrys, lo, hi )
768 l = lo
768 p = arrys[0][lo]
769 z = lo+1
769 l = lo
770 while z <= hi
770 z = lo+1
771 if arrys[0][z] < p
771 while z <= hi
772 l += 1
772 if arrys[0][z] < p
773 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
773 l += 1
774 end
774 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
775 z += 1
775 end
776 end
776 z += 1
777 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
777 end
778 l
778 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
779 end
779 l
780
780 end
781 def style
781
782 if no_css
782 def style
783 styles = parse_css
783 if no_css
784 @root.elements.each("//*[@class]") { |el|
784 styles = parse_css
785 cl = el.attributes["class"]
785 @root.elements.each("//*[@class]") { |el|
786 style = styles[cl]
786 cl = el.attributes["class"]
787 style += el.attributes["style"] if el.attributes["style"]
787 style = styles[cl]
788 el.attributes["style"] = style
788 style += el.attributes["style"] if el.attributes["style"]
789 }
789 el.attributes["style"] = style
790 end
790 }
791 end
791 end
792
792 end
793 def parse_css
793
794 css = get_style
794 def parse_css
795 rv = {}
795 css = get_style
796 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
796 rv = {}
797 names_orig = names = $1
797 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
798 css = $'
798 names_orig = names = $1
799 css =~ /([^}]+)\}/m
799 css = $'
800 content = $1
800 css =~ /([^}]+)\}/m
801 css = $'
801 content = $1
802
802 css = $'
803 nms = []
803
804 while names =~ /^\s*,?\s*\.(\w+)/
804 nms = []
805 nms << $1
805 while names =~ /^\s*,?\s*\.(\w+)/
806 names = $'
806 nms << $1
807 end
807 names = $'
808
808 end
809 content = content.tr( "\n\t", " ")
809
810 for name in nms
810 content = content.tr( "\n\t", " ")
811 current = rv[name]
811 for name in nms
812 current = current ? current+"; "+content : content
812 current = rv[name]
813 rv[name] = current.strip.squeeze(" ")
813 current = current ? current+"; "+content : content
814 end
814 rv[name] = current.strip.squeeze(" ")
815 end
815 end
816 return rv
816 end
817 end
817 return rv
818
818 end
819
819
820 # Override and place code to add defs here
820
821 def add_defs defs
821 # Override and place code to add defs here
822 end
822 def add_defs defs
823
823 end
824
824
825 def start_svg
825
826 # Base document
826 def start_svg
827 @doc = Document.new
827 # Base document
828 @doc << XMLDecl.new
828 @doc = Document.new
829 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
829 @doc << XMLDecl.new
830 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
830 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
831 if style_sheet && style_sheet != ''
831 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
832 @doc << Instruction.new( "xml-stylesheet",
832 if style_sheet && style_sheet != ''
833 %Q{href="#{style_sheet}" type="text/css"} )
833 @doc << Instruction.new( "xml-stylesheet",
834 end
834 %Q{href="#{style_sheet}" type="text/css"} )
835 @root = @doc.add_element( "svg", {
835 end
836 "width" => width.to_s,
836 @root = @doc.add_element( "svg", {
837 "height" => height.to_s,
837 "width" => width.to_s,
838 "viewBox" => "0 0 #{width} #{height}",
838 "height" => height.to_s,
839 "xmlns" => "http://www.w3.org/2000/svg",
839 "viewBox" => "0 0 #{width} #{height}",
840 "xmlns:xlink" => "http://www.w3.org/1999/xlink",
840 "xmlns" => "http://www.w3.org/2000/svg",
841 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
841 "xmlns:xlink" => "http://www.w3.org/1999/xlink",
842 "a3:scriptImplementation" => "Adobe"
842 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
843 })
843 "a3:scriptImplementation" => "Adobe"
844 @root << Comment.new( " "+"\\"*66 )
844 })
845 @root << Comment.new( " Created with SVG::Graph " )
845 @root << Comment.new( " "+"\\"*66 )
846 @root << Comment.new( " SVG::Graph by Sean E. Russell " )
846 @root << Comment.new( " Created with SVG::Graph " )
847 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
847 @root << Comment.new( " SVG::Graph by Sean E. Russell " )
848 " Leo Lapworth & Stephan Morgan " )
848 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
849 @root << Comment.new( " "+"/"*66 )
849 " Leo Lapworth & Stephan Morgan " )
850
850 @root << Comment.new( " "+"/"*66 )
851 defs = @root.add_element( "defs" )
851
852 add_defs defs
852 defs = @root.add_element( "defs" )
853 if not(style_sheet && style_sheet != '') and !no_css
853 add_defs defs
854 @root << Comment.new(" include default stylesheet if none specified ")
854 if not(style_sheet && style_sheet != '') and !no_css
855 style = defs.add_element( "style", {"type"=>"text/css"} )
855 @root << Comment.new(" include default stylesheet if none specified ")
856 style << CData.new( get_style )
856 style = defs.add_element( "style", {"type"=>"text/css"} )
857 end
857 style << CData.new( get_style )
858
858 end
859 @root << Comment.new( "SVG Background" )
859
860 @root.add_element( "rect", {
860 @root << Comment.new( "SVG Background" )
861 "width" => width.to_s,
861 @root.add_element( "rect", {
862 "height" => height.to_s,
862 "width" => width.to_s,
863 "x" => "0",
863 "height" => height.to_s,
864 "y" => "0",
864 "x" => "0",
865 "class" => "svgBackground"
865 "y" => "0",
866 })
866 "class" => "svgBackground"
867 end
867 })
868
868 end
869
869
870 def calculate_graph_dimensions
870
871 calculate_left_margin
871 def calculate_graph_dimensions
872 calculate_right_margin
872 calculate_left_margin
873 calculate_bottom_margin
873 calculate_right_margin
874 calculate_top_margin
874 calculate_bottom_margin
875 @graph_width = width - @border_left - @border_right
875 calculate_top_margin
876 @graph_height = height - @border_top - @border_bottom
876 @graph_width = width - @border_left - @border_right
877 end
877 @graph_height = height - @border_top - @border_bottom
878
878 end
879 def get_style
879
880 return <<EOL
880 def get_style
881 /* Copy from here for external style sheet */
881 return <<EOL
882 .svgBackground{
882 /* Copy from here for external style sheet */
883 fill:#ffffff;
883 .svgBackground{
884 }
884 fill:#ffffff;
885 .graphBackground{
885 }
886 fill:#f5f5f5;
886 .graphBackground{
887 }
887 fill:#f0f0f0;
888
888 }
889 /* graphs titles */
889
890 .mainTitle{
890 /* graphs titles */
891 text-anchor: middle;
891 .mainTitle{
892 fill: #555555;
892 text-anchor: middle;
893 font-size: #{title_font_size}px;
893 fill: #000000;
894 font-family: "Verdana", sans-serif;
894 font-size: #{title_font_size}px;
895 font-weight: bold;
895 font-family: "Arial", sans-serif;
896 }
896 font-weight: normal;
897 .subTitle{
897 }
898 text-anchor: middle;
898 .subTitle{
899 fill: #999999;
899 text-anchor: middle;
900 font-size: #{subtitle_font_size}px;
900 fill: #999999;
901 font-family: "Verdana", sans-serif;
901 font-size: #{subtitle_font_size}px;
902 font-weight: normal;
902 font-family: "Arial", sans-serif;
903 }
903 font-weight: normal;
904
904 }
905 .axis{
905
906 stroke: #666666;
906 .axis{
907 stroke-width: 1px;
907 stroke: #000000;
908 }
908 stroke-width: 1px;
909
909 }
910 .guideLines{
910
911 stroke: #666666;
911 .guideLines{
912 stroke-width: 1px;
912 stroke: #666666;
913 stroke-dasharray:2,2,2;
913 stroke-width: 1px;
914 }
914 stroke-dasharray: 5 5;
915
915 }
916 .xAxisLabels{
916
917 text-anchor: middle;
917 .xAxisLabels{
918 fill: #000000;
918 text-anchor: middle;
919 font-size: #{x_label_font_size}px;
919 fill: #000000;
920 font-family: "Verdana", sans-serif;
920 font-size: #{x_label_font_size}px;
921 font-weight: normal;
921 font-family: "Arial", sans-serif;
922 }
922 font-weight: normal;
923
923 }
924 .yAxisLabels{
924
925 text-anchor: end;
925 .yAxisLabels{
926 fill: #000000;
926 text-anchor: end;
927 font-size: #{y_label_font_size}px;
927 fill: #000000;
928 font-family: "Verdana", sans-serif;
928 font-size: #{y_label_font_size}px;
929 font-weight: normal;
929 font-family: "Arial", sans-serif;
930 }
930 font-weight: normal;
931
931 }
932 .xAxisTitle{
932
933 text-anchor: middle;
933 .xAxisTitle{
934 fill: #ff0000;
934 text-anchor: middle;
935 font-size: #{x_title_font_size}px;
935 fill: #ff0000;
936 font-family: "Verdana", sans-serif;
936 font-size: #{x_title_font_size}px;
937 font-weight: normal;
937 font-family: "Arial", sans-serif;
938 }
938 font-weight: normal;
939
939 }
940 .yAxisTitle{
940
941 fill: #ff0000;
941 .yAxisTitle{
942 text-anchor: middle;
942 fill: #ff0000;
943 font-size: #{y_title_font_size}px;
943 text-anchor: middle;
944 font-family: "Verdana", sans-serif;
944 font-size: #{y_title_font_size}px;
945 font-weight: normal;
945 font-family: "Arial", sans-serif;
946 }
946 font-weight: normal;
947
947 }
948 .dataPointLabel{
948
949 fill: #000000;
949 .dataPointLabel{
950 text-anchor:middle;
950 fill: #000000;
951 font-size: 10px;
951 text-anchor:middle;
952 font-family: "Verdana", sans-serif;
952 font-size: 10px;
953 font-weight: normal;
953 font-family: "Arial", sans-serif;
954 }
954 font-weight: normal;
955
955 }
956 .staggerGuideLine{
956
957 fill: none;
957 .staggerGuideLine{
958 stroke: #000000;
958 fill: none;
959 stroke-width: 0.5px;
959 stroke: #000000;
960 }
960 stroke-width: 0.5px;
961
961 }
962 #{get_css}
962
963
963 #{get_css}
964 .keyText{
964
965 fill: #000000;
965 .keyText{
966 text-anchor:start;
966 fill: #000000;
967 font-size: #{key_font_size}px;
967 text-anchor:start;
968 font-family: "Verdana", sans-serif;
968 font-size: #{key_font_size}px;
969 font-weight: normal;
969 font-family: "Arial", sans-serif;
970 }
970 font-weight: normal;
971 /* End copy for external style sheet */
971 }
972 EOL
972 /* End copy for external style sheet */
973 end
973 EOL
974
974 end
975 end
975
976 end
976 end
977 end
977 end
978 end
This diff has been collapsed as it changes many lines, (789 lines changed) Show them Hide them
@@ -1,394 +1,395
1 require 'SVG/Graph/Graph'
1 require 'SVG/Graph/Graph'
2
2
3 module SVG
3 module SVG
4 module Graph
4 module Graph
5 # === Create presentation quality SVG pie graphs easily
5 # === Create presentation quality SVG pie graphs easily
6 #
6 #
7 # == Synopsis
7 # == Synopsis
8 #
8 #
9 # require 'SVG/Graph/Pie'
9 # require 'SVG/Graph/Pie'
10 #
10 #
11 # fields = %w(Jan Feb Mar)
11 # fields = %w(Jan Feb Mar)
12 # data_sales_02 = [12, 45, 21]
12 # data_sales_02 = [12, 45, 21]
13 #
13 #
14 # graph = SVG::Graph::Pie.new({
14 # graph = SVG::Graph::Pie.new({
15 # :height => 500,
15 # :height => 500,
16 # :width => 300,
16 # :width => 300,
17 # :fields => fields,
17 # :fields => fields,
18 # })
18 # })
19 #
19 #
20 # graph.add_data({
20 # graph.add_data({
21 # :data => data_sales_02,
21 # :data => data_sales_02,
22 # :title => 'Sales 2002',
22 # :title => 'Sales 2002',
23 # })
23 # })
24 #
24 #
25 # print "Content-type: image/svg+xml\r\n\r\n"
25 # print "Content-type: image/svg+xml\r\n\r\n"
26 # print graph.burn();
26 # print graph.burn();
27 #
27 #
28 # == Description
28 # == Description
29 #
29 #
30 # This object aims to allow you to easily create high quality
30 # This object aims to allow you to easily create high quality
31 # SVG pie graphs. You can either use the default style sheet
31 # SVG pie graphs. You can either use the default style sheet
32 # or supply your own. Either way there are many options which can
32 # or supply your own. Either way there are many options which can
33 # be configured to give you control over how the graph is
33 # be configured to give you control over how the graph is
34 # generated - with or without a key, display percent on pie chart,
34 # generated - with or without a key, display percent on pie chart,
35 # title, subtitle etc.
35 # title, subtitle etc.
36 #
36 #
37 # = Examples
37 # = Examples
38 #
38 #
39 # http://www.germane-software/repositories/public/SVG/test/single.rb
39 # http://www.germane-software/repositories/public/SVG/test/single.rb
40 #
40 #
41 # == See also
41 # == See also
42 #
42 #
43 # * SVG::Graph::Graph
43 # * SVG::Graph::Graph
44 # * SVG::Graph::BarHorizontal
44 # * SVG::Graph::BarHorizontal
45 # * SVG::Graph::Bar
45 # * SVG::Graph::Bar
46 # * SVG::Graph::Line
46 # * SVG::Graph::Line
47 # * SVG::Graph::Plot
47 # * SVG::Graph::Plot
48 # * SVG::Graph::TimeSeries
48 # * SVG::Graph::TimeSeries
49 #
49 #
50 # == Author
50 # == Author
51 #
51 #
52 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
52 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
53 #
53 #
54 # Copyright 2004 Sean E. Russell
54 # Copyright 2004 Sean E. Russell
55 # This software is available under the Ruby license[LICENSE.txt]
55 # This software is available under the Ruby license[LICENSE.txt]
56 #
56 #
57 class Pie < Graph
57 class Pie < Graph
58 # Defaults are those set by Graph::initialize, and
58 # Defaults are those set by Graph::initialize, and
59 # [show_shadow] true
59 # [show_shadow] true
60 # [shadow_offset] 10
60 # [shadow_offset] 10
61 # [show_data_labels] false
61 # [show_data_labels] false
62 # [show_actual_values] false
62 # [show_actual_values] false
63 # [show_percent] true
63 # [show_percent] true
64 # [show_key_data_labels] true
64 # [show_key_data_labels] true
65 # [show_key_actual_values] true
65 # [show_key_actual_values] true
66 # [show_key_percent] false
66 # [show_key_percent] false
67 # [expanded] false
67 # [expanded] false
68 # [expand_greatest] false
68 # [expand_greatest] false
69 # [expand_gap] 10
69 # [expand_gap] 10
70 # [show_x_labels] false
70 # [show_x_labels] false
71 # [show_y_labels] false
71 # [show_y_labels] false
72 # [datapoint_font_size] 12
72 # [datapoint_font_size] 12
73 def set_defaults
73 def set_defaults
74 init_with(
74 init_with(
75 :show_shadow => true,
75 :show_shadow => true,
76 :shadow_offset => 10,
76 :shadow_offset => 10,
77
77
78 :show_data_labels => false,
78 :show_data_labels => false,
79 :show_actual_values => false,
79 :show_actual_values => false,
80 :show_percent => true,
80 :show_percent => true,
81
81
82 :show_key_data_labels => true,
82 :show_key_data_labels => true,
83 :show_key_actual_values => true,
83 :show_key_actual_values => true,
84 :show_key_percent => false,
84 :show_key_percent => false,
85
85
86 :expanded => false,
86 :expanded => false,
87 :expand_greatest => false,
87 :expand_greatest => false,
88 :expand_gap => 10,
88 :expand_gap => 10,
89
89
90 :show_x_labels => false,
90 :show_x_labels => false,
91 :show_y_labels => false,
91 :show_y_labels => false,
92 :datapoint_font_size => 12
92 :datapoint_font_size => 12
93 )
93 )
94 @data = []
94 @data = []
95 end
95 end
96
96
97 # Adds a data set to the graph.
97 # Adds a data set to the graph.
98 #
98 #
99 # graph.add_data( { :data => [1,2,3,4] } )
99 # graph.add_data( { :data => [1,2,3,4] } )
100 #
100 #
101 # Note that the :title is not necessary. If multiple
101 # Note that the :title is not necessary. If multiple
102 # data sets are added to the graph, the pie chart will
102 # data sets are added to the graph, the pie chart will
103 # display the +sums+ of the data. EG:
103 # display the +sums+ of the data. EG:
104 #
104 #
105 # graph.add_data( { :data => [1,2,3,4] } )
105 # graph.add_data( { :data => [1,2,3,4] } )
106 # graph.add_data( { :data => [2,3,5,9] } )
106 # graph.add_data( { :data => [2,3,5,9] } )
107 #
107 #
108 # is the same as:
108 # is the same as:
109 #
109 #
110 # graph.add_data( { :data => [3,5,8,13] } )
110 # graph.add_data( { :data => [3,5,8,13] } )
111 def add_data arg
111 def add_data arg
112 arg[:data].each_index {|idx|
112 arg[:data].each_index {|idx|
113 @data[idx] = 0 unless @data[idx]
113 @data[idx] = 0 unless @data[idx]
114 @data[idx] += arg[:data][idx]
114 @data[idx] += arg[:data][idx]
115 }
115 }
116 end
116 end
117
117
118 # If true, displays a drop shadow for the chart
118 # If true, displays a drop shadow for the chart
119 attr_accessor :show_shadow
119 attr_accessor :show_shadow
120 # Sets the offset of the shadow from the pie chart
120 # Sets the offset of the shadow from the pie chart
121 attr_accessor :shadow_offset
121 attr_accessor :shadow_offset
122 # If true, display the data labels on the chart
122 # If true, display the data labels on the chart
123 attr_accessor :show_data_labels
123 attr_accessor :show_data_labels
124 # If true, display the actual field values in the data labels
124 # If true, display the actual field values in the data labels
125 attr_accessor :show_actual_values
125 attr_accessor :show_actual_values
126 # If true, display the percentage value of each pie wedge in the data
126 # If true, display the percentage value of each pie wedge in the data
127 # labels
127 # labels
128 attr_accessor :show_percent
128 attr_accessor :show_percent
129 # If true, display the labels in the key
129 # If true, display the labels in the key
130 attr_accessor :show_key_data_labels
130 attr_accessor :show_key_data_labels
131 # If true, display the actual value of the field in the key
131 # If true, display the actual value of the field in the key
132 attr_accessor :show_key_actual_values
132 attr_accessor :show_key_actual_values
133 # If true, display the percentage value of the wedges in the key
133 # If true, display the percentage value of the wedges in the key
134 attr_accessor :show_key_percent
134 attr_accessor :show_key_percent
135 # If true, "explode" the pie (put space between the wedges)
135 # If true, "explode" the pie (put space between the wedges)
136 attr_accessor :expanded
136 attr_accessor :expanded
137 # If true, expand the largest pie wedge
137 # If true, expand the largest pie wedge
138 attr_accessor :expand_greatest
138 attr_accessor :expand_greatest
139 # The amount of space between expanded wedges
139 # The amount of space between expanded wedges
140 attr_accessor :expand_gap
140 attr_accessor :expand_gap
141 # The font size of the data point labels
141 # The font size of the data point labels
142 attr_accessor :datapoint_font_size
142 attr_accessor :datapoint_font_size
143
143
144
144
145 protected
145 protected
146
146
147 def add_defs defs
147 def add_defs defs
148 gradient = defs.add_element( "filter", {
148 gradient = defs.add_element( "filter", {
149 "id"=>"dropshadow",
149 "id"=>"dropshadow",
150 "width" => "1.2",
150 "width" => "1.2",
151 "height" => "1.2",
151 "height" => "1.2",
152 } )
152 } )
153 gradient.add_element( "feGaussianBlur", {
153 gradient.add_element( "feGaussianBlur", {
154 "stdDeviation" => "4",
154 "stdDeviation" => "4",
155 "result" => "blur"
155 "result" => "blur"
156 })
156 })
157 end
157 end
158
158
159 # We don't need the graph
159 # We don't need the graph
160 def draw_graph
160 def draw_graph
161 end
161 end
162
162
163 def get_y_labels
163 def get_y_labels
164 [""]
164 [""]
165 end
165 end
166
166
167 def get_x_labels
167 def get_x_labels
168 [""]
168 [""]
169 end
169 end
170
170
171 def keys
171 def keys
172 total = 0
172 total = 0
173 max_value = 0
173 max_value = 0
174 @data.each {|x| total += x }
174 @data.each {|x| total += x }
175 percent_scale = 100.0 / total
175 percent_scale = 100.0 / total
176 count = -1
176 count = -1
177 a = @config[:fields].collect{ |x|
177 a = @config[:fields].collect{ |x|
178 count += 1
178 count += 1
179 v = @data[count]
179 v = @data[count]
180 perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
180 perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
181 x + " [" + v.to_s + "]" + perc
181 x + " [" + v.to_s + "]" + perc
182 }
182 }
183 end
183 end
184
184
185 RADIANS = Math::PI/180
185 RADIANS = Math::PI/180
186
186
187 def draw_data
187 def draw_data
188 @graph = @root.add_element( "g" )
188 @graph = @root.add_element( "g" )
189 background = @graph.add_element("g")
189 background = @graph.add_element("g")
190 midground = @graph.add_element("g")
190 midground = @graph.add_element("g")
191
191
192 diameter = @graph_height > @graph_width ? @graph_width : @graph_height
192 diameter = @graph_height > @graph_width ? @graph_width : @graph_height
193 diameter -= expand_gap if expanded or expand_greatest
193 diameter -= expand_gap if expanded or expand_greatest
194 diameter -= datapoint_font_size if show_data_labels
194 diameter -= datapoint_font_size if show_data_labels
195 diameter -= 10 if show_shadow
195 diameter -= 10 if show_shadow
196 radius = diameter / 2.0
196 radius = diameter / 2.0
197
197
198 xoff = (width - diameter) / 2
198 xoff = (width - diameter) / 2
199 yoff = (height - @border_bottom - diameter)
199 yoff = (height - @border_bottom - diameter)
200 yoff -= 10 if show_shadow
200 yoff -= 10 if show_shadow
201 @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"
201 @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"
202
202
203 wedge_text_pad = 5
203 wedge_text_pad = 5
204 wedge_text_pad = 20 if show_percent and show_data_labels
204 wedge_text_pad = 20 if show_percent and show_data_labels
205
205
206 total = 0
206 total = 0
207 max_value = 0
207 max_value = 0
208 @data.each {|x|
208 @data.each {|x|
209 max_value = max_value < x ? x : max_value
209 max_value = max_value < x ? x : max_value
210 total += x
210 total += x
211 }
211 }
212 percent_scale = 100.0 / total
212 percent_scale = 100.0 / total
213
213
214 prev_percent = 0
214 prev_percent = 0
215 rad_mult = 3.6 * RADIANS
215 rad_mult = 3.6 * RADIANS
216 @config[:fields].each_index { |count|
216 @config[:fields].each_index { |count|
217 value = @data[count]
217 value = @data[count]
218 percent = percent_scale * value
218 percent = percent_scale * value
219
219
220 radians = prev_percent * rad_mult
220 radians = prev_percent * rad_mult
221 x_start = radius+(Math.sin(radians) * radius)
221 x_start = radius+(Math.sin(radians) * radius)
222 y_start = radius-(Math.cos(radians) * radius)
222 y_start = radius-(Math.cos(radians) * radius)
223 radians = (prev_percent+percent) * rad_mult
223 radians = (prev_percent+percent) * rad_mult
224 x_end = radius+(Math.sin(radians) * radius)
224 x_end = radius+(Math.sin(radians) * radius)
225 y_end = radius-(Math.cos(radians) * radius)
225 x_end -= 0.00001 if @data.length == 1
226 path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
226 y_end = radius-(Math.cos(radians) * radius)
227 "A#{radius},#{radius} "+
227 path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
228 "0, #{percent >= 50 ? '1' : '0'},1, "+
228 "A#{radius},#{radius} "+
229 "#{x_end} #{y_end} Z"
229 "0, #{percent >= 50 ? '1' : '0'},1, "+
230
230 "#{x_end} #{y_end} Z"
231
231
232 wedge = @foreground.add_element( "path", {
232
233 "d" => path,
233 wedge = @foreground.add_element( "path", {
234 "class" => "fill#{count+1}"
234 "d" => path,
235 })
235 "class" => "fill#{count+1}"
236
236 })
237 translate = nil
237
238 tx = 0
238 translate = nil
239 ty = 0
239 tx = 0
240 half_percent = prev_percent + percent / 2
240 ty = 0
241 radians = half_percent * rad_mult
241 half_percent = prev_percent + percent / 2
242
242 radians = half_percent * rad_mult
243 if show_shadow
243
244 shadow = background.add_element( "path", {
244 if show_shadow
245 "d" => path,
245 shadow = background.add_element( "path", {
246 "filter" => "url(#dropshadow)",
246 "d" => path,
247 "style" => "fill: #ccc; stroke: none;"
247 "filter" => "url(#dropshadow)",
248 })
248 "style" => "fill: #ccc; stroke: none;"
249 clear = midground.add_element( "path", {
249 })
250 "d" => path,
250 clear = midground.add_element( "path", {
251 "style" => "fill: #fff; stroke: none;"
251 "d" => path,
252 })
252 "style" => "fill: #fff; stroke: none;"
253 end
253 })
254
254 end
255 if expanded or (expand_greatest && value == max_value)
255
256 tx = (Math.sin(radians) * expand_gap)
256 if expanded or (expand_greatest && value == max_value)
257 ty = -(Math.cos(radians) * expand_gap)
257 tx = (Math.sin(radians) * expand_gap)
258 translate = "translate( #{tx} #{ty} )"
258 ty = -(Math.cos(radians) * expand_gap)
259 wedge.attributes["transform"] = translate
259 translate = "translate( #{tx} #{ty} )"
260 clear.attributes["transform"] = translate
260 wedge.attributes["transform"] = translate
261 end
261 clear.attributes["transform"] = translate if clear
262
262 end
263 if show_shadow
263
264 shadow.attributes["transform"] =
264 if show_shadow
265 "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
265 shadow.attributes["transform"] =
266 end
266 "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
267
267 end
268 if show_data_labels and value != 0
268
269 label = ""
269 if show_data_labels and value != 0
270 label += @config[:fields][count] if show_key_data_labels
270 label = ""
271 label += " ["+value.to_s+"]" if show_actual_values
271 label += @config[:fields][count] if show_key_data_labels
272 label += " "+percent.round.to_s+"%" if show_percent
272 label += " ["+value.to_s+"]" if show_actual_values
273
273 label += " "+percent.round.to_s+"%" if show_percent
274 msr = Math.sin(radians)
274
275 mcr = Math.cos(radians)
275 msr = Math.sin(radians)
276 tx = radius + (msr * radius)
276 mcr = Math.cos(radians)
277 ty = radius -(mcr * radius)
277 tx = radius + (msr * radius)
278
278 ty = radius -(mcr * radius)
279 if expanded or (expand_greatest && value == max_value)
279
280 tx += (msr * expand_gap)
280 if expanded or (expand_greatest && value == max_value)
281 ty -= (mcr * expand_gap)
281 tx += (msr * expand_gap)
282 end
282 ty -= (mcr * expand_gap)
283 @foreground.add_element( "text", {
283 end
284 "x" => tx.to_s,
284 @foreground.add_element( "text", {
285 "y" => ty.to_s,
285 "x" => tx.to_s,
286 "class" => "dataPointLabel",
286 "y" => ty.to_s,
287 "style" => "stroke: #fff; stroke-width: 2;"
287 "class" => "dataPointLabel",
288 }).text = label.to_s
288 "style" => "stroke: #fff; stroke-width: 2;"
289 @foreground.add_element( "text", {
289 }).text = label.to_s
290 "x" => tx.to_s,
290 @foreground.add_element( "text", {
291 "y" => ty.to_s,
291 "x" => tx.to_s,
292 "class" => "dataPointLabel",
292 "y" => ty.to_s,
293 }).text = label.to_s
293 "class" => "dataPointLabel",
294 end
294 }).text = label.to_s
295
295 end
296 prev_percent += percent
296
297 }
297 prev_percent += percent
298 end
298 }
299
299 end
300
300
301 def round val, to
301
302 up = 10**to.to_f
302 def round val, to
303 (val * up).to_i / up
303 up = 10**to.to_f
304 end
304 (val * up).to_i / up
305
305 end
306
306
307 def get_css
307
308 return <<EOL
308 def get_css
309 .dataPointLabel{
309 return <<EOL
310 fill: #000000;
310 .dataPointLabel{
311 text-anchor:middle;
311 fill: #000000;
312 font-size: #{datapoint_font_size}px;
312 text-anchor:middle;
313 font-family: "Arial", sans-serif;
313 font-size: #{datapoint_font_size}px;
314 font-weight: normal;
314 font-family: "Arial", sans-serif;
315 }
315 font-weight: normal;
316
316 }
317 /* key - MUST match fill styles */
317
318 .key1,.fill1{
318 /* key - MUST match fill styles */
319 fill: #ff0000;
319 .key1,.fill1{
320 fill-opacity: 0.7;
320 fill: #ff0000;
321 stroke: none;
321 fill-opacity: 0.7;
322 stroke-width: 1px;
322 stroke: none;
323 }
323 stroke-width: 1px;
324 .key2,.fill2{
324 }
325 fill: #0000ff;
325 .key2,.fill2{
326 fill-opacity: 0.7;
326 fill: #0000ff;
327 stroke: none;
327 fill-opacity: 0.7;
328 stroke-width: 1px;
328 stroke: none;
329 }
329 stroke-width: 1px;
330 .key3,.fill3{
330 }
331 fill-opacity: 0.7;
331 .key3,.fill3{
332 fill: #00ff00;
332 fill-opacity: 0.7;
333 stroke: none;
333 fill: #00ff00;
334 stroke-width: 1px;
334 stroke: none;
335 }
335 stroke-width: 1px;
336 .key4,.fill4{
336 }
337 fill-opacity: 0.7;
337 .key4,.fill4{
338 fill: #ffcc00;
338 fill-opacity: 0.7;
339 stroke: none;
339 fill: #ffcc00;
340 stroke-width: 1px;
340 stroke: none;
341 }
341 stroke-width: 1px;
342 .key5,.fill5{
342 }
343 fill-opacity: 0.7;
343 .key5,.fill5{
344 fill: #00ccff;
344 fill-opacity: 0.7;
345 stroke: none;
345 fill: #00ccff;
346 stroke-width: 1px;
346 stroke: none;
347 }
347 stroke-width: 1px;
348 .key6,.fill6{
348 }
349 fill-opacity: 0.7;
349 .key6,.fill6{
350 fill: #ff00ff;
350 fill-opacity: 0.7;
351 stroke: none;
351 fill: #ff00ff;
352 stroke-width: 1px;
352 stroke: none;
353 }
353 stroke-width: 1px;
354 .key7,.fill7{
354 }
355 fill-opacity: 0.7;
355 .key7,.fill7{
356 fill: #00ff99;
356 fill-opacity: 0.7;
357 stroke: none;
357 fill: #00ff99;
358 stroke-width: 1px;
358 stroke: none;
359 }
359 stroke-width: 1px;
360 .key8,.fill8{
360 }
361 fill-opacity: 0.7;
361 .key8,.fill8{
362 fill: #ffff00;
362 fill-opacity: 0.7;
363 stroke: none;
363 fill: #ffff00;
364 stroke-width: 1px;
364 stroke: none;
365 }
365 stroke-width: 1px;
366 .key9,.fill9{
366 }
367 fill-opacity: 0.7;
367 .key9,.fill9{
368 fill: #cc6666;
368 fill-opacity: 0.7;
369 stroke: none;
369 fill: #cc6666;
370 stroke-width: 1px;
370 stroke: none;
371 }
371 stroke-width: 1px;
372 .key10,.fill10{
372 }
373 fill-opacity: 0.7;
373 .key10,.fill10{
374 fill: #663399;
374 fill-opacity: 0.7;
375 stroke: none;
375 fill: #663399;
376 stroke-width: 1px;
376 stroke: none;
377 }
377 stroke-width: 1px;
378 .key11,.fill11{
378 }
379 fill-opacity: 0.7;
379 .key11,.fill11{
380 fill: #339900;
380 fill-opacity: 0.7;
381 stroke: none;
381 fill: #339900;
382 stroke-width: 1px;
382 stroke: none;
383 }
383 stroke-width: 1px;
384 .key12,.fill12{
384 }
385 fill-opacity: 0.7;
385 .key12,.fill12{
386 fill: #9966FF;
386 fill-opacity: 0.7;
387 stroke: none;
387 fill: #9966FF;
388 stroke-width: 1px;
388 stroke: none;
389 }
389 stroke-width: 1px;
390 EOL
390 }
391 end
391 EOL
392 end
392 end
393 end
393 end
394 end
394 end
395 end
This diff has been collapsed as it changes many lines, (994 lines changed) Show them Hide them
@@ -1,494 +1,500
1 require 'SVG/Graph/Graph'
1 require 'SVG/Graph/Graph'
2
2
3 module SVG
3 module SVG
4 module Graph
4 module Graph
5 # === For creating SVG plots of scalar data
5 # === For creating SVG plots of scalar data
6 #
6 #
7 # = Synopsis
7 # = Synopsis
8 #
8 #
9 # require 'SVG/Graph/Plot'
9 # require 'SVG/Graph/Plot'
10 #
10 #
11 # # Data sets are x,y pairs
11 # # Data sets are x,y pairs
12 # # Note that multiple data sets can differ in length, and that the
12 # # Note that multiple data sets can differ in length, and that the
13 # # data in the datasets needn't be in order; they will be ordered
13 # # data in the datasets needn't be in order; they will be ordered
14 # # by the plot along the X-axis.
14 # # by the plot along the X-axis.
15 # projection = [
15 # projection = [
16 # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
16 # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
17 # 7, 9
17 # 7, 9
18 # ]
18 # ]
19 # actual = [
19 # actual = [
20 # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
20 # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
21 # 15, 6, 4, 17, 2, 12
21 # 15, 6, 4, 17, 2, 12
22 # ]
22 # ]
23 #
23 #
24 # graph = SVG::Graph::Plot.new({
24 # graph = SVG::Graph::Plot.new({
25 # :height => 500,
25 # :height => 500,
26 # :width => 300,
26 # :width => 300,
27 # :key => true,
27 # :key => true,
28 # :scale_x_integers => true,
28 # :scale_x_integers => true,
29 # :scale_y_integerrs => true,
29 # :scale_y_integerrs => true,
30 # })
30 # })
31 #
31 #
32 # graph.add_data({
32 # graph.add_data({
33 # :data => projection
33 # :data => projection
34 # :title => 'Projected',
34 # :title => 'Projected',
35 # })
35 # })
36 #
36 #
37 # graph.add_data({
37 # graph.add_data({
38 # :data => actual,
38 # :data => actual,
39 # :title => 'Actual',
39 # :title => 'Actual',
40 # })
40 # })
41 #
41 #
42 # print graph.burn()
42 # print graph.burn()
43 #
43 #
44 # = Description
44 # = Description
45 #
45 #
46 # Produces a graph of scalar data.
46 # Produces a graph of scalar data.
47 #
47 #
48 # This object aims to allow you to easily create high quality
48 # This object aims to allow you to easily create high quality
49 # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
49 # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
50 # default style sheet or supply your own. Either way there are many options
50 # default style sheet or supply your own. Either way there are many options
51 # which can be configured to give you control over how the graph is
51 # which can be configured to give you control over how the graph is
52 # generated - with or without a key, data elements at each point, title,
52 # generated - with or without a key, data elements at each point, title,
53 # subtitle etc.
53 # subtitle etc.
54 #
54 #
55 # = Examples
55 # = Examples
56 #
56 #
57 # http://www.germane-software/repositories/public/SVG/test/plot.rb
57 # http://www.germane-software/repositories/public/SVG/test/plot.rb
58 #
58 #
59 # = Notes
59 # = Notes
60 #
60 #
61 # The default stylesheet handles upto 10 data sets, if you
61 # The default stylesheet handles upto 10 data sets, if you
62 # use more you must create your own stylesheet and add the
62 # use more you must create your own stylesheet and add the
63 # additional settings for the extra data sets. You will know
63 # additional settings for the extra data sets. You will know
64 # if you go over 10 data sets as they will have no style and
64 # if you go over 10 data sets as they will have no style and
65 # be in black.
65 # be in black.
66 #
66 #
67 # Unlike the other types of charts, data sets must contain x,y pairs:
67 # Unlike the other types of charts, data sets must contain x,y pairs:
68 #
68 #
69 # [ 1, 2 ] # A data set with 1 point: (1,2)
69 # [ 1, 2 ] # A data set with 1 point: (1,2)
70 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
70 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
71 #
71 #
72 # = See also
72 # = See also
73 #
73 #
74 # * SVG::Graph::Graph
74 # * SVG::Graph::Graph
75 # * SVG::Graph::BarHorizontal
75 # * SVG::Graph::BarHorizontal
76 # * SVG::Graph::Bar
76 # * SVG::Graph::Bar
77 # * SVG::Graph::Line
77 # * SVG::Graph::Line
78 # * SVG::Graph::Pie
78 # * SVG::Graph::Pie
79 # * SVG::Graph::TimeSeries
79 # * SVG::Graph::TimeSeries
80 #
80 #
81 # == Author
81 # == Author
82 #
82 #
83 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
83 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
84 #
84 #
85 # Copyright 2004 Sean E. Russell
85 # Copyright 2004 Sean E. Russell
86 # This software is available under the Ruby license[LICENSE.txt]
86 # This software is available under the Ruby license[LICENSE.txt]
87 #
87 #
88 class Plot < Graph
88 class Plot < Graph
89
89
90 # In addition to the defaults set by Graph::initialize, sets
90 # In addition to the defaults set by Graph::initialize, sets
91 # [show_data_points] true
91 # [show_data_values] true
92 # [area_fill] false
92 # [show_data_points] true
93 # [stacked] false
93 # [area_fill] false
94 def set_defaults
94 # [stacked] false
95 init_with(
95 def set_defaults
96 :show_data_points => true,
96 init_with(
97 :area_fill => false,
97 :show_data_values => true,
98 :stacked => false
98 :show_data_points => true,
99 )
99 :area_fill => false,
100 self.top_align = self.right_align = self.top_font = self.right_font = 1
100 :stacked => false
101 end
101 )
102
102 self.top_align = self.right_align = self.top_font = self.right_font = 1
103 # Determines the scaling for the X axis divisions.
103 end
104 #
104
105 # graph.scale_x_divisions = 2
105 # Determines the scaling for the X axis divisions.
106 #
106 #
107 # would cause the graph to attempt to generate labels stepped by 2; EG:
107 # graph.scale_x_divisions = 2
108 # 0,2,4,6,8...
108 #
109 attr_accessor :scale_x_divisions
109 # would cause the graph to attempt to generate labels stepped by 2; EG:
110 # Determines the scaling for the Y axis divisions.
110 # 0,2,4,6,8...
111 #
111 attr_accessor :scale_x_divisions
112 # graph.scale_y_divisions = 0.5
112 # Determines the scaling for the Y axis divisions.
113 #
113 #
114 # would cause the graph to attempt to generate labels stepped by 0.5; EG:
114 # graph.scale_y_divisions = 0.5
115 # 0, 0.5, 1, 1.5, 2, ...
115 #
116 attr_accessor :scale_y_divisions
116 # would cause the graph to attempt to generate labels stepped by 0.5; EG:
117 # Make the X axis labels integers
117 # 0, 0.5, 1, 1.5, 2, ...
118 attr_accessor :scale_x_integers
118 attr_accessor :scale_y_divisions
119 # Make the Y axis labels integers
119 # Make the X axis labels integers
120 attr_accessor :scale_y_integers
120 attr_accessor :scale_x_integers
121 # Fill the area under the line
121 # Make the Y axis labels integers
122 attr_accessor :area_fill
122 attr_accessor :scale_y_integers
123 # Show a small circle on the graph where the line
123 # Fill the area under the line
124 # goes from one point to the next.
124 attr_accessor :area_fill
125 attr_accessor :show_data_points
125 # Show a small circle on the graph where the line
126 # Set the minimum value of the X axis
126 # goes from one point to the next.
127 attr_accessor :min_x_value
127 attr_accessor :show_data_points
128 # Set the minimum value of the Y axis
128 # Set the minimum value of the X axis
129 attr_accessor :min_y_value
129 attr_accessor :min_x_value
130
130 # Set the minimum value of the Y axis
131
131 attr_accessor :min_y_value
132 # Adds data to the plot. The data must be in X,Y pairs; EG
132
133 # [ 1, 2 ] # A data set with 1 point: (1,2)
133
134 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
134 # Adds data to the plot. The data must be in X,Y pairs; EG
135 def add_data data
135 # [ 1, 2 ] # A data set with 1 point: (1,2)
136 @data = [] unless @data
136 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
137
137 def add_data data
138 raise "No data provided by #{conf.inspect}" unless data[:data] and
138 @data = [] unless @data
139 data[:data].kind_of? Array
139
140 raise "Data supplied must be x,y pairs! "+
140 raise "No data provided by #{conf.inspect}" unless data[:data] and
141 "The data provided contained an odd set of "+
141 data[:data].kind_of? Array
142 "data points" unless data[:data].length % 2 == 0
142 raise "Data supplied must be x,y pairs! "+
143 return if data[:data].length == 0
143 "The data provided contained an odd set of "+
144
144 "data points" unless data[:data].length % 2 == 0
145 x = []
145 return if data[:data].length == 0
146 y = []
146
147 data[:data].each_index {|i|
147 x = []
148 (i%2 == 0 ? x : y) << data[:data][i]
148 y = []
149 }
149 data[:data].each_index {|i|
150 sort( x, y )
150 (i%2 == 0 ? x : y) << data[:data][i]
151 data[:data] = [x,y]
151 }
152 @data << data
152 sort( x, y )
153 end
153 data[:data] = [x,y]
154
154 @data << data
155
155 end
156 protected
156
157
157
158 def keys
158 protected
159 @data.collect{ |x| x[:title] }
159
160 end
160 def keys
161
161 @data.collect{ |x| x[:title] }
162 def calculate_left_margin
162 end
163 super
163
164 label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
164 def calculate_left_margin
165 @border_left = label_left if label_left > @border_left
165 super
166 end
166 label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
167
167 @border_left = label_left if label_left > @border_left
168 def calculate_right_margin
168 end
169 super
169
170 label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
170 def calculate_right_margin
171 @border_right = label_right if label_right > @border_right
171 super
172 end
172 label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
173
173 @border_right = label_right if label_right > @border_right
174
174 end
175 X = 0
175
176 Y = 1
176
177 def x_range
177 X = 0
178 max_value = @data.collect{|x| x[:data][X][-1] }.max
178 Y = 1
179 min_value = @data.collect{|x| x[:data][X][0] }.min
179 def x_range
180 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
180 max_value = @data.collect{|x| x[:data][X][-1] }.max
181
181 min_value = @data.collect{|x| x[:data][X][0] }.min
182 range = max_value - min_value
182 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
183 right_pad = range == 0 ? 10 : range / 20.0
183
184 scale_range = (max_value + right_pad) - min_value
184 range = max_value - min_value
185
185 right_pad = range == 0 ? 10 : range / 20.0
186 scale_division = scale_x_divisions || (scale_range / 10.0)
186 scale_range = (max_value + right_pad) - min_value
187
187
188 if scale_x_integers
188 scale_division = scale_x_divisions || (scale_range / 10.0)
189 scale_division = scale_division < 1 ? 1 : scale_division.round
189
190 end
190 if scale_x_integers
191
191 scale_division = scale_division < 1 ? 1 : scale_division.round
192 [min_value, max_value, scale_division]
192 end
193 end
193
194
194 [min_value, max_value, scale_division]
195 def get_x_values
195 end
196 min_value, max_value, scale_division = x_range
196
197 rv = []
197 def get_x_values
198 min_value.step( max_value, scale_division ) {|v| rv << v}
198 min_value, max_value, scale_division = x_range
199 return rv
199 rv = []
200 end
200 min_value.step( max_value, scale_division ) {|v| rv << v}
201 alias :get_x_labels :get_x_values
201 return rv
202
202 end
203 def field_width
203 alias :get_x_labels :get_x_values
204 values = get_x_values
204
205 max = @data.collect{|x| x[:data][X][-1]}.max
205 def field_width
206 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
206 values = get_x_values
207 (@graph_width.to_f - font_size*2*right_font) /
207 max = @data.collect{|x| x[:data][X][-1]}.max
208 (values.length + dx - right_align)
208 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
209 end
209 (@graph_width.to_f - font_size*2*right_font) /
210
210 (values.length + dx - right_align)
211
211 end
212 def y_range
212
213 max_value = @data.collect{|x| x[:data][Y].max }.max
213
214 min_value = @data.collect{|x| x[:data][Y].min }.min
214 def y_range
215 min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
215 max_value = @data.collect{|x| x[:data][Y].max }.max
216
216 min_value = @data.collect{|x| x[:data][Y].min }.min
217 range = max_value - min_value
217 min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
218 top_pad = range == 0 ? 10 : range / 20.0
218
219 scale_range = (max_value + top_pad) - min_value
219 range = max_value - min_value
220
220 top_pad = range == 0 ? 10 : range / 20.0
221 scale_division = scale_y_divisions || (scale_range / 10.0)
221 scale_range = (max_value + top_pad) - min_value
222
222
223 if scale_y_integers
223 scale_division = scale_y_divisions || (scale_range / 10.0)
224 scale_division = scale_division < 1 ? 1 : scale_division.round
224
225 end
225 if scale_y_integers
226
226 scale_division = scale_division < 1 ? 1 : scale_division.round
227 return [min_value, max_value, scale_division]
227 end
228 end
228
229
229 return [min_value, max_value, scale_division]
230 def get_y_values
230 end
231 min_value, max_value, scale_division = y_range
231
232 rv = []
232 def get_y_values
233 min_value.step( max_value, scale_division ) {|v| rv << v}
233 min_value, max_value, scale_division = y_range
234 return rv
234 rv = []
235 end
235 min_value.step( max_value, scale_division ) {|v| rv << v}
236 alias :get_y_labels :get_y_values
236 return rv
237
237 end
238 def field_height
238 alias :get_y_labels :get_y_values
239 values = get_y_values
239
240 max = @data.collect{|x| x[:data][Y].max }.max
240 def field_height
241 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
241 values = get_y_values
242 (@graph_height.to_f - font_size*2*top_font) /
242 max = @data.collect{|x| x[:data][Y].max }.max
243 (values.length + dx - top_align)
243 if values.length == 1
244 end
244 dx = values[-1]
245
245 else
246 def draw_data
246 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
247 line = 1
247 end
248
248 (@graph_height.to_f - font_size*2*top_font) /
249 x_min, x_max, x_div = x_range
249 (values.length + dx - top_align)
250 y_min, y_max, y_div = y_range
250 end
251 x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
251
252 y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
252 def draw_data
253
253 line = 1
254 for data in @data
254
255 x_points = data[:data][X]
255 x_min, x_max, x_div = x_range
256 y_points = data[:data][Y]
256 y_min, y_max, y_div = y_range
257
257 x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
258 lpath = "L"
258 y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
259 x_start = 0
259
260 y_start = 0
260 for data in @data
261 x_points.each_index { |idx|
261 x_points = data[:data][X]
262 x = (x_points[idx] - x_min) * x_step
262 y_points = data[:data][Y]
263 y = @graph_height - (y_points[idx] - y_min) * y_step
263
264 x_start, y_start = x,y if idx == 0
264 lpath = "L"
265 lpath << "#{x} #{y} "
265 x_start = 0
266 }
266 y_start = 0
267
267 x_points.each_index { |idx|
268 if area_fill
268 x = (x_points[idx] - x_min) * x_step
269 @graph.add_element( "path", {
269 y = @graph_height - (y_points[idx] - y_min) * y_step
270 "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
270 x_start, y_start = x,y if idx == 0
271 "class" => "fill#{line}"
271 lpath << "#{x} #{y} "
272 })
272 }
273 end
273
274
274 if area_fill
275 @graph.add_element( "path", {
275 @graph.add_element( "path", {
276 "d" => "M#{x_start} #{y_start} #{lpath}",
276 "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
277 "class" => "line#{line}"
277 "class" => "fill#{line}"
278 })
278 })
279
279 end
280 if show_data_points || show_data_values
280
281 x_points.each_index { |idx|
281 @graph.add_element( "path", {
282 x = (x_points[idx] - x_min) * x_step
282 "d" => "M#{x_start} #{y_start} #{lpath}",
283 y = @graph_height - (y_points[idx] - y_min) * y_step
283 "class" => "line#{line}"
284 if show_data_points
284 })
285 @graph.add_element( "circle", {
285
286 "cx" => x.to_s,
286 if show_data_points || show_data_values
287 "cy" => y.to_s,
287 x_points.each_index { |idx|
288 "r" => "2.5",
288 x = (x_points[idx] - x_min) * x_step
289 "class" => "dataPoint#{line}"
289 y = @graph_height - (y_points[idx] - y_min) * y_step
290 })
290 if show_data_points
291 add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
291 @graph.add_element( "circle", {
292 end
292 "cx" => x.to_s,
293 make_datapoint_text( x, y-6, y_points[idx] )
293 "cy" => y.to_s,
294 }
294 "r" => "2.5",
295 end
295 "class" => "dataPoint#{line}"
296 line += 1
296 })
297 end
297 add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
298 end
298 end
299
299 make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
300 def format x, y
300 }
301 "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
301 end
302 end
302 line += 1
303
303 end
304 def get_css
304 end
305 return <<EOL
305
306 /* default line styles */
306 def format x, y
307 .line1{
307 "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
308 fill: none;
308 end
309 stroke: #ff0000;
309
310 stroke-width: 1px;
310 def get_css
311 }
311 return <<EOL
312 .line2{
312 /* default line styles */
313 fill: none;
313 .line1{
314 stroke: #0000ff;
314 fill: none;
315 stroke-width: 1px;
315 stroke: #ff0000;
316 }
316 stroke-width: 1px;
317 .line3{
317 }
318 fill: none;
318 .line2{
319 stroke: #00ff00;
319 fill: none;
320 stroke-width: 1px;
320 stroke: #0000ff;
321 }
321 stroke-width: 1px;
322 .line4{
322 }
323 fill: none;
323 .line3{
324 stroke: #ffcc00;
324 fill: none;
325 stroke-width: 1px;
325 stroke: #00ff00;
326 }
326 stroke-width: 1px;
327 .line5{
327 }
328 fill: none;
328 .line4{
329 stroke: #00ccff;
329 fill: none;
330 stroke-width: 1px;
330 stroke: #ffcc00;
331 }
331 stroke-width: 1px;
332 .line6{
332 }
333 fill: none;
333 .line5{
334 stroke: #ff00ff;
334 fill: none;
335 stroke-width: 1px;
335 stroke: #00ccff;
336 }
336 stroke-width: 1px;
337 .line7{
337 }
338 fill: none;
338 .line6{
339 stroke: #00ffff;
339 fill: none;
340 stroke-width: 1px;
340 stroke: #ff00ff;
341 }
341 stroke-width: 1px;
342 .line8{
342 }
343 fill: none;
343 .line7{
344 stroke: #ffff00;
344 fill: none;
345 stroke-width: 1px;
345 stroke: #00ffff;
346 }
346 stroke-width: 1px;
347 .line9{
347 }
348 fill: none;
348 .line8{
349 stroke: #ccc6666;
349 fill: none;
350 stroke-width: 1px;
350 stroke: #ffff00;
351 }
351 stroke-width: 1px;
352 .line10{
352 }
353 fill: none;
353 .line9{
354 stroke: #663399;
354 fill: none;
355 stroke-width: 1px;
355 stroke: #ccc6666;
356 }
356 stroke-width: 1px;
357 .line11{
357 }
358 fill: none;
358 .line10{
359 stroke: #339900;
359 fill: none;
360 stroke-width: 1px;
360 stroke: #663399;
361 }
361 stroke-width: 1px;
362 .line12{
362 }
363 fill: none;
363 .line11{
364 stroke: #9966FF;
364 fill: none;
365 stroke-width: 1px;
365 stroke: #339900;
366 }
366 stroke-width: 1px;
367 /* default fill styles */
367 }
368 .fill1{
368 .line12{
369 fill: #cc0000;
369 fill: none;
370 fill-opacity: 0.2;
370 stroke: #9966FF;
371 stroke: none;
371 stroke-width: 1px;
372 }
372 }
373 .fill2{
373 /* default fill styles */
374 fill: #0000cc;
374 .fill1{
375 fill-opacity: 0.2;
375 fill: #cc0000;
376 stroke: none;
376 fill-opacity: 0.2;
377 }
377 stroke: none;
378 .fill3{
378 }
379 fill: #00cc00;
379 .fill2{
380 fill-opacity: 0.2;
380 fill: #0000cc;
381 stroke: none;
381 fill-opacity: 0.2;
382 }
382 stroke: none;
383 .fill4{
383 }
384 fill: #ffcc00;
384 .fill3{
385 fill-opacity: 0.2;
385 fill: #00cc00;
386 stroke: none;
386 fill-opacity: 0.2;
387 }
387 stroke: none;
388 .fill5{
388 }
389 fill: #00ccff;
389 .fill4{
390 fill-opacity: 0.2;
390 fill: #ffcc00;
391 stroke: none;
391 fill-opacity: 0.2;
392 }
392 stroke: none;
393 .fill6{
393 }
394 fill: #ff00ff;
394 .fill5{
395 fill-opacity: 0.2;
395 fill: #00ccff;
396 stroke: none;
396 fill-opacity: 0.2;
397 }
397 stroke: none;
398 .fill7{
398 }
399 fill: #00ffff;
399 .fill6{
400 fill-opacity: 0.2;
400 fill: #ff00ff;
401 stroke: none;
401 fill-opacity: 0.2;
402 }
402 stroke: none;
403 .fill8{
403 }
404 fill: #ffff00;
404 .fill7{
405 fill-opacity: 0.2;
405 fill: #00ffff;
406 stroke: none;
406 fill-opacity: 0.2;
407 }
407 stroke: none;
408 .fill9{
408 }
409 fill: #cc6666;
409 .fill8{
410 fill-opacity: 0.2;
410 fill: #ffff00;
411 stroke: none;
411 fill-opacity: 0.2;
412 }
412 stroke: none;
413 .fill10{
413 }
414 fill: #663399;
414 .fill9{
415 fill-opacity: 0.2;
415 fill: #cc6666;
416 stroke: none;
416 fill-opacity: 0.2;
417 }
417 stroke: none;
418 .fill11{
418 }
419 fill: #339900;
419 .fill10{
420 fill-opacity: 0.2;
420 fill: #663399;
421 stroke: none;
421 fill-opacity: 0.2;
422 }
422 stroke: none;
423 .fill12{
423 }
424 fill: #9966FF;
424 .fill11{
425 fill-opacity: 0.2;
425 fill: #339900;
426 stroke: none;
426 fill-opacity: 0.2;
427 }
427 stroke: none;
428 /* default line styles */
428 }
429 .key1,.dataPoint1{
429 .fill12{
430 fill: #ff0000;
430 fill: #9966FF;
431 stroke: none;
431 fill-opacity: 0.2;
432 stroke-width: 1px;
432 stroke: none;
433 }
433 }
434 .key2,.dataPoint2{
434 /* default line styles */
435 fill: #0000ff;
435 .key1,.dataPoint1{
436 stroke: none;
436 fill: #ff0000;
437 stroke-width: 1px;
437 stroke: none;
438 }
438 stroke-width: 1px;
439 .key3,.dataPoint3{
439 }
440 fill: #00ff00;
440 .key2,.dataPoint2{
441 stroke: none;
441 fill: #0000ff;
442 stroke-width: 1px;
442 stroke: none;
443 }
443 stroke-width: 1px;
444 .key4,.dataPoint4{
444 }
445 fill: #ffcc00;
445 .key3,.dataPoint3{
446 stroke: none;
446 fill: #00ff00;
447 stroke-width: 1px;
447 stroke: none;
448 }
448 stroke-width: 1px;
449 .key5,.dataPoint5{
449 }
450 fill: #00ccff;
450 .key4,.dataPoint4{
451 stroke: none;
451 fill: #ffcc00;
452 stroke-width: 1px;
452 stroke: none;
453 }
453 stroke-width: 1px;
454 .key6,.dataPoint6{
454 }
455 fill: #ff00ff;
455 .key5,.dataPoint5{
456 stroke: none;
456 fill: #00ccff;
457 stroke-width: 1px;
457 stroke: none;
458 }
458 stroke-width: 1px;
459 .key7,.dataPoint7{
459 }
460 fill: #00ffff;
460 .key6,.dataPoint6{
461 stroke: none;
461 fill: #ff00ff;
462 stroke-width: 1px;
462 stroke: none;
463 }
463 stroke-width: 1px;
464 .key8,.dataPoint8{
464 }
465 fill: #ffff00;
465 .key7,.dataPoint7{
466 stroke: none;
466 fill: #00ffff;
467 stroke-width: 1px;
467 stroke: none;
468 }
468 stroke-width: 1px;
469 .key9,.dataPoint9{
469 }
470 fill: #cc6666;
470 .key8,.dataPoint8{
471 stroke: none;
471 fill: #ffff00;
472 stroke-width: 1px;
472 stroke: none;
473 }
473 stroke-width: 1px;
474 .key10,.dataPoint10{
474 }
475 fill: #663399;
475 .key9,.dataPoint9{
476 stroke: none;
476 fill: #cc6666;
477 stroke-width: 1px;
477 stroke: none;
478 }
478 stroke-width: 1px;
479 .key11,.dataPoint11{
479 }
480 fill: #339900;
480 .key10,.dataPoint10{
481 stroke: none;
481 fill: #663399;
482 stroke-width: 1px;
482 stroke: none;
483 }
483 stroke-width: 1px;
484 .key12,.dataPoint12{
484 }
485 fill: #9966FF;
485 .key11,.dataPoint11{
486 stroke: none;
486 fill: #339900;
487 stroke-width: 1px;
487 stroke: none;
488 }
488 stroke-width: 1px;
489 EOL
489 }
490 end
490 .key12,.dataPoint12{
491
491 fill: #9966FF;
492 end
492 stroke: none;
493 end
493 stroke-width: 1px;
494 end
494 }
495 EOL
496 end
497
498 end
499 end
500 end
@@ -1,241 +1,241
1 require 'SVG/Graph/Plot'
1 require 'SVG/Graph/Plot'
2 require 'parsedate'
2 require 'parsedate'
3
3
4 module SVG
4 module SVG
5 module Graph
5 module Graph
6 # === For creating SVG plots of scalar temporal data
6 # === For creating SVG plots of scalar temporal data
7 #
7 #
8 # = Synopsis
8 # = Synopsis
9 #
9 #
10 # require 'SVG/Graph/TimeSeriess'
10 # require 'SVG/Graph/TimeSeriess'
11 #
11 #
12 # # Data sets are x,y pairs
12 # # Data sets are x,y pairs
13 # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
13 # 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]
14 # "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,
15 # 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,
16 # "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]
17 # "5/1/84", 17, "10/1/80", 12]
18 #
18 #
19 # graph = SVG::Graph::TimeSeries.new( {
19 # graph = SVG::Graph::TimeSeries.new( {
20 # :width => 640,
20 # :width => 640,
21 # :height => 480,
21 # :height => 480,
22 # :graph_title => title,
22 # :graph_title => title,
23 # :show_graph_title => true,
23 # :show_graph_title => true,
24 # :no_css => true,
24 # :no_css => true,
25 # :key => true,
25 # :key => true,
26 # :scale_x_integers => true,
26 # :scale_x_integers => true,
27 # :scale_y_integers => true,
27 # :scale_y_integers => true,
28 # :min_x_value => 0,
28 # :min_x_value => 0,
29 # :min_y_value => 0,
29 # :min_y_value => 0,
30 # :show_data_labels => true,
30 # :show_data_labels => true,
31 # :show_x_guidelines => true,
31 # :show_x_guidelines => true,
32 # :show_x_title => true,
32 # :show_x_title => true,
33 # :x_title => "Time",
33 # :x_title => "Time",
34 # :show_y_title => true,
34 # :show_y_title => true,
35 # :y_title => "Ice Cream Cones",
35 # :y_title => "Ice Cream Cones",
36 # :y_title_text_direction => :bt,
36 # :y_title_text_direction => :bt,
37 # :stagger_x_labels => true,
37 # :stagger_x_labels => true,
38 # :x_label_format => "%m/%d/%y",
38 # :x_label_format => "%m/%d/%y",
39 # })
39 # })
40 #
40 #
41 # graph.add_data({
41 # graph.add_data({
42 # :data => projection
42 # :data => projection
43 # :title => 'Projected',
43 # :title => 'Projected',
44 # })
44 # })
45 #
45 #
46 # graph.add_data({
46 # graph.add_data({
47 # :data => actual,
47 # :data => actual,
48 # :title => 'Actual',
48 # :title => 'Actual',
49 # })
49 # })
50 #
50 #
51 # print graph.burn()
51 # print graph.burn()
52 #
52 #
53 # = Description
53 # = Description
54 #
54 #
55 # Produces a graph of temporal scalar data.
55 # Produces a graph of temporal scalar data.
56 #
56 #
57 # = Examples
57 # = Examples
58 #
58 #
59 # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
59 # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
60 #
60 #
61 # = Notes
61 # = Notes
62 #
62 #
63 # The default stylesheet handles upto 10 data sets, if you
63 # The default stylesheet handles upto 10 data sets, if you
64 # use more you must create your own stylesheet and add the
64 # use more you must create your own stylesheet and add the
65 # additional settings for the extra data sets. You will know
65 # 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
66 # if you go over 10 data sets as they will have no style and
67 # be in black.
67 # be in black.
68 #
68 #
69 # Unlike the other types of charts, data sets must contain x,y pairs:
69 # Unlike the other types of charts, data sets must contain x,y pairs:
70 #
70 #
71 # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
71 # [ "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
72 # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
73 # # ("14:20",6)
73 # # ("14:20",6)
74 #
74 #
75 # Note that multiple data sets within the same chart can differ in length,
75 # 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
76 # and that the data in the datasets needn't be in order; they will be ordered
77 # by the plot along the X-axis.
77 # by the plot along the X-axis.
78 #
78 #
79 # The dates must be parseable by ParseDate, but otherwise can be
79 # The dates must be parseable by ParseDate, but otherwise can be
80 # any order of magnitude (seconds within the hour, or years)
80 # any order of magnitude (seconds within the hour, or years)
81 #
81 #
82 # = See also
82 # = See also
83 #
83 #
84 # * SVG::Graph::Graph
84 # * SVG::Graph::Graph
85 # * SVG::Graph::BarHorizontal
85 # * SVG::Graph::BarHorizontal
86 # * SVG::Graph::Bar
86 # * SVG::Graph::Bar
87 # * SVG::Graph::Line
87 # * SVG::Graph::Line
88 # * SVG::Graph::Pie
88 # * SVG::Graph::Pie
89 # * SVG::Graph::Plot
89 # * SVG::Graph::Plot
90 #
90 #
91 # == Author
91 # == Author
92 #
92 #
93 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
93 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
94 #
94 #
95 # Copyright 2004 Sean E. Russell
95 # Copyright 2004 Sean E. Russell
96 # This software is available under the Ruby license[LICENSE.txt]
96 # This software is available under the Ruby license[LICENSE.txt]
97 #
97 #
98 class TimeSeries < Plot
98 class TimeSeries < Plot
99 # In addition to the defaults set by Graph::initialize and
99 # In addition to the defaults set by Graph::initialize and
100 # Plot::set_defaults, sets:
100 # Plot::set_defaults, sets:
101 # [x_label_format] '%Y-%m-%d %H:%M:%S'
101 # [x_label_format] '%Y-%m-%d %H:%M:%S'
102 # [popup_format] '%Y-%m-%d %H:%M:%S'
102 # [popup_format] '%Y-%m-%d %H:%M:%S'
103 def set_defaults
103 def set_defaults
104 super
104 super
105 init_with(
105 init_with(
106 #:max_time_span => '',
106 #:max_time_span => '',
107 :x_label_format => '%Y-%m-%d %H:%M:%S',
107 :x_label_format => '%Y-%m-%d %H:%M:%S',
108 :popup_format => '%Y-%m-%d %H:%M:%S'
108 :popup_format => '%Y-%m-%d %H:%M:%S'
109 )
109 )
110 end
110 end
111
111
112 # The format string use do format the X axis labels.
112 # The format string use do format the X axis labels.
113 # See Time::strformat
113 # See Time::strformat
114 attr_accessor :x_label_format
114 attr_accessor :x_label_format
115 # Use this to set the spacing between dates on the axis. The value
115 # Use this to set the spacing between dates on the axis. The value
116 # must be of the form
116 # must be of the form
117 # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
117 # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
118 #
118 #
119 # EG:
119 # EG:
120 #
120 #
121 # graph.timescale_divisions = "2 weeks"
121 # graph.timescale_divisions = "2 weeks"
122 #
122 #
123 # will cause the chart to try to divide the X axis up into segments of
123 # will cause the chart to try to divide the X axis up into segments of
124 # two week periods.
124 # two week periods.
125 attr_accessor :timescale_divisions
125 attr_accessor :timescale_divisions
126 # The formatting used for the popups. See x_label_format
126 # The formatting used for the popups. See x_label_format
127 attr_accessor :popup_format
127 attr_accessor :popup_format
128
128
129 # Add data to the plot.
129 # Add data to the plot.
130 #
130 #
131 # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
131 # 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
132 # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
133 # # ("14:20",6)
133 # # ("14:20",6)
134 # graph.add_data(
134 # graph.add_data(
135 # :data => d1,
135 # :data => d1,
136 # :title => 'One'
136 # :title => 'One'
137 # )
137 # )
138 # graph.add_data(
138 # graph.add_data(
139 # :data => d2,
139 # :data => d2,
140 # :title => 'Two'
140 # :title => 'Two'
141 # )
141 # )
142 #
142 #
143 # Note that the data must be in time,value pairs, and that the date format
143 # 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.
144 # may be any date that is parseable by ParseDate.
145 def add_data data
145 def add_data data
146 @data = [] unless @data
146 @data = [] unless @data
147
147
148 raise "No data provided by #{conf.inspect}" unless data[:data] and
148 raise "No data provided by #{@data.inspect}" unless data[:data] and
149 data[:data].kind_of? Array
149 data[:data].kind_of? Array
150 raise "Data supplied must be x,y pairs! "+
150 raise "Data supplied must be x,y pairs! "+
151 "The data provided contained an odd set of "+
151 "The data provided contained an odd set of "+
152 "data points" unless data[:data].length % 2 == 0
152 "data points" unless data[:data].length % 2 == 0
153 return if data[:data].length == 0
153 return if data[:data].length == 0
154
154
155
155
156 x = []
156 x = []
157 y = []
157 y = []
158 data[:data].each_index {|i|
158 data[:data].each_index {|i|
159 if i%2 == 0
159 if i%2 == 0
160 arr = ParseDate.parsedate( data[:data][i] )
160 arr = ParseDate.parsedate( data[:data][i] )
161 t = Time.local( *arr[0,6].compact )
161 t = Time.local( *arr[0,6].compact )
162 x << t.to_i
162 x << t.to_i
163 else
163 else
164 y << data[:data][i]
164 y << data[:data][i]
165 end
165 end
166 }
166 }
167 sort( x, y )
167 sort( x, y )
168 data[:data] = [x,y]
168 data[:data] = [x,y]
169 @data << data
169 @data << data
170 end
170 end
171
171
172
172
173 protected
173 protected
174
174
175 def min_x_value=(value)
175 def min_x_value=(value)
176 arr = ParseDate.parsedate( value )
176 arr = ParseDate.parsedate( value )
177 @min_x_value = Time.local( *arr[0,6].compact ).to_i
177 @min_x_value = Time.local( *arr[0,6].compact ).to_i
178 end
178 end
179
179
180
180
181 def format x, y
181 def format x, y
182 Time.at( x ).strftime( popup_format )
182 Time.at( x ).strftime( popup_format )
183 end
183 end
184
184
185 def get_x_labels
185 def get_x_labels
186 get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
186 get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
187 end
187 end
188
188
189 private
189 private
190 def get_x_values
190 def get_x_values
191 rv = []
191 rv = []
192 min, max, scale_division = x_range
192 min, max, scale_division = x_range
193 if timescale_divisions
193 if timescale_divisions
194 timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
194 timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/
195 division_units = $2 ? $2 : "days"
195 division_units = $2 ? $2 : "day"
196 amount = $1.to_i
196 amount = $1.to_i
197 if amount
197 if amount
198 step = nil
198 step = nil
199 case division_units
199 case division_units
200 when "months"
200 when "month"
201 cur = min
201 cur = min
202 while cur < max
202 while cur < max
203 rv << cur
203 rv << cur
204 arr = Time.at( cur ).to_a
204 arr = Time.at( cur ).to_a
205 arr[4] += amount
205 arr[4] += amount
206 if arr[4] > 12
206 if arr[4] > 12
207 arr[5] += (arr[4] / 12).to_i
207 arr[5] += (arr[4] / 12).to_i
208 arr[4] = (arr[4] % 12)
208 arr[4] = (arr[4] % 12)
209 end
209 end
210 cur = Time.local(*arr).to_i
210 cur = Time.local(*arr).to_i
211 end
211 end
212 when "years"
212 when "year"
213 cur = min
213 cur = min
214 while cur < max
214 while cur < max
215 rv << cur
215 rv << cur
216 arr = Time.at( cur ).to_a
216 arr = Time.at( cur ).to_a
217 arr[5] += amount
217 arr[5] += amount
218 cur = Time.local(*arr).to_i
218 cur = Time.local(*arr).to_i
219 end
219 end
220 when "weeks"
220 when "week"
221 step = 7 * 24 * 60 * 60 * amount
221 step = 7 * 24 * 60 * 60 * amount
222 when "days"
222 when "day"
223 step = 24 * 60 * 60 * amount
223 step = 24 * 60 * 60 * amount
224 when "hours"
224 when "hour"
225 step = 60 * 60 * amount
225 step = 60 * 60 * amount
226 when "minutes"
226 when "minute"
227 step = 60 * amount
227 step = 60 * amount
228 when "seconds"
228 when "second"
229 step = amount
229 step = amount
230 end
230 end
231 min.step( max, step ) {|v| rv << v} if step
231 min.step( max, step ) {|v| rv << v} if step
232
232
233 return rv
233 return rv
234 end
234 end
235 end
235 end
236 min.step( max, scale_division ) {|v| rv << v}
236 min.step( max, scale_division ) {|v| rv << v}
237 return rv
237 return rv
238 end
238 end
239 end
239 end
240 end
240 end
241 end
241 end
General Comments 0
You need to be logged in to leave comments. Login now