svg-graph 2.0.2.beta → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d856850b800bff5e1688e0ad450ccbcc21bdd21d
4
- data.tar.gz: 095e8398439866cc6cd52439d60d036ef960ac8a
3
+ metadata.gz: 7101e9e0d576c758450907b8f9d30bf2b8c787ee
4
+ data.tar.gz: c370f0c3b179587d0e4bf34d4862f4c1a0366dce
5
5
  SHA512:
6
- metadata.gz: 69d1f607ba2edec2452d14e39fdb18079bc59a76c52898e101d7adff4c383d8ae40c589e45a001eb3b50912bd2d46af6ac89e4d1b9a9a68f87968a8cf2382877
7
- data.tar.gz: 2cb188581fe18553a4c1a2527168f4d4988cf2b47dd6577dadf248b4d93fe09d26608f164bdca8fddb9d97ee9feece347b1287ee8fef12e325245a11d63b05d6
6
+ metadata.gz: da39453fcbd961e3b8758da1b7a5ed49f9fa6142f598151de6625e7294ca3ffc2689aca19110841a6f598bf23305ab85daaf2f5b2d59a03f29b3204d96c2ecc2
7
+ data.tar.gz: 58a9de11f45ce90ffbf7600b4d82a0b26b27f4b6598e016221bc838fa074cd7aaaecad537aa7cd69fbcae33d03f4c6da432883b890775dfd9db24900c1c39f3e
data/History.txt CHANGED
@@ -1,16 +1,18 @@
1
- TODO
2
- * fix axis titles positioning for staggered axis (prevent overlap)
3
- * fix scale_integer
4
- * fix sorting and connected lines for Plot
1
+ TODO / Backlog
2
+ * add unit tests for all types of graphs
3
+ * refactor various hardcoded constant pixel offsets in Graph class to use named constants or variables
5
4
 
6
5
 
7
- === 2.0.2 work in progress
6
+ === 2.0.2 / 2016-10-24 [lumean]
8
7
  * fix axis-title positioning
9
8
  * fix/add support for popups of values line, plot, bar graph
10
9
  * Line,Plot,Pie graph support nil values (or pairs of nil for plot) for non-present datapoints
11
- * changed text anchor of first datapoint label to avoid overlap with axis
12
- * fix pie graph key overlapping the graph
13
- *
10
+ * fix show datapoint labels: changed text anchor of first datapoint label to avoid overlap with axis
11
+ and text anchor of last datapoint label
12
+ * fix pie graph key overlapping the graph [thanks adamalbrecht for reporting]
13
+ * fix axis titles positioning for staggered axis (prevent overlap)
14
+ * fix axis labels and positioning when scale_integers is true and min value is not integer.
15
+ * added various comments in the code to be better understandable (for me)
14
16
 
15
17
  === 2.0.1 / 2016-08-09
16
18
  * dropped support for ruby 1.8.7 [lumean]
data/README.markdown CHANGED
@@ -78,18 +78,27 @@ File.open('bar.svg', 'w') {|f| f.write(g.burn_svg_only)}
78
78
 
79
79
  ### ErrBar
80
80
 
81
+ ![example err_bar graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/err_bar.svg)
82
+
81
83
  ### Line
82
84
 
83
85
  ![example line graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/line.svg)
84
86
 
85
87
  ### Pie
86
88
 
89
+ ![example pie graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/pie.svg)
90
+
87
91
  ### Plot
88
92
 
93
+ ![example plot graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/plot.svg)
94
+
89
95
  ### Schedule
90
96
 
97
+ ![example schedule graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/schedule.svg)
98
+
91
99
  ### TimeSeries
92
100
 
101
+ ![example timeseries graph](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/timeseries.svg)
93
102
 
94
103
  Also have a look at the original [SVG::Graph web page](http://www.germane-software.com/software/SVG/SVG::Graph/), but note that this repository has already added some additional functionality, not available with the original.
95
104
 
data/lib/SVG/Graph/Bar.rb CHANGED
@@ -58,12 +58,6 @@ module SVG
58
58
  class Bar < BarBase
59
59
  include REXML
60
60
 
61
- # See Graph::initialize and BarBase::set_defaults
62
- def set_defaults
63
- super
64
- # self.top_align = self.top_font = 1
65
- end
66
-
67
61
  protected
68
62
 
69
63
  def get_x_labels
@@ -74,9 +68,13 @@ module SVG
74
68
  maxvalue = max_value
75
69
  minvalue = min_value
76
70
  range = maxvalue - minvalue
77
-
78
- top_pad = range == 0 ? 10 : range / 20.0
79
- scale_range = (maxvalue + top_pad) - minvalue
71
+ # add some padding on top of the graph
72
+ if range == 0
73
+ maxvalue += 10
74
+ else
75
+ maxvalue += range / 20.0
76
+ end
77
+ scale_range = maxvalue - minvalue
80
78
 
81
79
  @y_scale_division = scale_divisions || (scale_range / 10.0)
82
80
 
@@ -100,14 +98,11 @@ module SVG
100
98
  minvalue = min_value
101
99
  fieldwidth = field_width
102
100
 
103
- # unit_size = (@graph_height.to_f - font_size*2*top_font) /
104
- # (get_y_labels.max - get_y_labels.min)
105
101
  unit_size = field_height
106
102
  bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
107
103
 
108
104
  bar_width = fieldwidth - bargap
109
105
  bar_width /= @data.length if stack == :side
110
- #x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
111
106
 
112
107
  bottom = @graph_height
113
108
 
@@ -40,6 +40,13 @@ module SVG
40
40
  attr_accessor :stack
41
41
  protected
42
42
 
43
+ # space in px between x-labels, we override the Graph version because
44
+ # we need the extra space (i.e. don't subtract 1 from get_x_labels.length)
45
+ def field_width
46
+ # don't use -1 otherwise bar is out of bounds
47
+ @graph_width.to_f / ( get_x_labels.length )
48
+ end
49
+
43
50
  def max_value
44
51
  @data.collect{|x| x[:data].max}.max
45
52
  end
@@ -86,9 +86,9 @@ module SVG
86
86
  end
87
87
 
88
88
  rv = []
89
- if maxvalue%@x_scale_division != 0
90
- maxvalue = maxvalue + @x_scale_division
91
- end
89
+ #if maxvalue%@x_scale_division != 0
90
+ # maxvalue = maxvalue + @x_scale_division
91
+ #end
92
92
  minvalue.step( maxvalue, @x_scale_division ) {|v| rv << v}
93
93
  return rv
94
94
  end
@@ -104,10 +104,7 @@ module SVG
104
104
  def draw_data
105
105
  minvalue = min_value
106
106
  fieldheight = field_height
107
- # number of steps in px between x-labels
108
- # unit_size = (@graph_width.to_f - font_size*2*right_font ) /
109
- # (get_x_labels.max - get_x_labels.min )
110
- unit_size = field_width
107
+
111
108
  bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
112
109
 
113
110
  bar_height = fieldheight - bargap
@@ -118,7 +115,7 @@ module SVG
118
115
  @config[:fields].each_index { |i|
119
116
  dataset_count = 0
120
117
  for dataset in @data
121
- value = dataset[:data][i]/@x_scale_division
118
+ value = dataset[:data][i]
122
119
 
123
120
  top = @graph_height - (fieldheight * field_count)
124
121
  top += (bar_height * dataset_count) if stack == :side
@@ -127,8 +124,8 @@ module SVG
127
124
  # +ve +ve value.abs - min minvalue.abs
128
125
  # +ve -ve value.abs - 0 minvalue.abs
129
126
  # -ve -ve value.abs - 0 minvalue.abs + value
130
- length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
131
- left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
127
+ length = (value.abs - (minvalue > 0 ? minvalue : 0))/@x_scale_division.to_f * field_width
128
+ left = (minvalue.abs + (value < 0 ? value : 0))/@x_scale_division.to_f * field_width
132
129
 
133
130
  @graph.add_element( "rect", {
134
131
  "x" => left.to_s,
@@ -1,3 +1,4 @@
1
+ # Allows to customize datapoint shapes
1
2
  class DataPoint
2
3
  OVERLAY = "OVERLAY" unless defined?(OVERLAY)
3
4
  DEFAULT_SHAPE = lambda{|x,y,line| ["circle", {
@@ -8,24 +9,58 @@ class DataPoint
8
9
  }]
9
10
  } unless defined? DEFAULT_SHAPE
10
11
  CRITERIA = [] unless defined? CRITERIA
12
+
13
+ # matchers are class scope. Once configured, each DataPoint instance will have
14
+ # access to the same matchers
15
+ # @param matchers [Array] multiple arrays of the following form:
16
+ # [ regex ,
17
+ # lambda taking three arguments (x,y, line_number for css)
18
+ # -> return value of the lambda must be an array: [svg tag name, Hash with attributes for the svg tag, e.g. "points" and "class"]
19
+ # ]
20
+ # @example
21
+ # DataPoint.configure_shape_criteria(
22
+ # [/.*/, lambda{|x,y,line| ['polygon', {
23
+ # "points" => "#{x-1.5},#{y+2.5} #{x+1.5},#{y+2.5} #{x+1.5},#{y-2.5} #{x-1.5},#{y-2.5}",
24
+ # "class" => "dataPoint#{line}"
25
+ # }]
26
+ # }]
27
+ # )
11
28
  def DataPoint.configure_shape_criteria(*matchers)
12
29
  CRITERIA.push(*matchers)
13
30
  end
31
+
32
+ #
14
33
  def DataPoint.reset_shape_criteria
15
34
  CRITERIA.clear
16
35
  end
17
36
 
37
+ # creates a new DataPoint
38
+ # @param x [Numeric] x coordinates of the point
39
+ # @param y [Numeric] y coordinates of the point
40
+ # @param line [Fixnum] line index of the current dataset (e.g. when multiple times Graph.add_data()), can be used to reference to the correct css class
18
41
  def initialize(x, y, line)
19
42
  @x = x
20
43
  @y = y
21
44
  @line = line
22
45
  end
46
+
47
+ # @return [Array<Array>] see example
48
+ # @example Return value
49
+ # # two dimensional array, the splatted (*) inner array can be used as argument to REXML::add_element
50
+ # [["svgtag", {"points" => "", "class"=> "dataPoint#{line}" } ], ["svgtag", {"points"=>"", "class"=> ""}], ...]
51
+ # @exmple Usage
52
+ # dp = DataPoint.new(x, y, line).shape(data[:description])
53
+ # # for each svg we insert it to the graph
54
+ # dp.each {|s| @graph.add_element( *s )}
55
+ #
23
56
  def shape(description=nil)
57
+ # select all criteria with size 2, and collect rendered lambdas in an array
24
58
  shapes = CRITERIA.select {|criteria|
25
59
  criteria.size == 2
26
60
  }.collect {|regexp, proc|
27
61
  proc.call(@x, @y, @line) if description =~ regexp
28
62
  }.compact
63
+ # if above did not render anything use the defalt shape
29
64
  shapes = [DEFAULT_SHAPE.call(@x, @y, @line)] if shapes.empty?
30
65
 
31
66
  overlays = CRITERIA.select { |criteria|
@@ -8,19 +8,29 @@ module SVG
8
8
  #
9
9
  # = Synopsis
10
10
  #
11
- # require 'SVG/Graph/Bar'
11
+ # require 'SVG/Graph/ErrBar'
12
12
  #
13
- # fields = %w(Jan Feb Mar);
14
- # data_sales_02 = [12, 45, 21]
15
- #
16
- # graph = SVG::Graph::Bar.new(
13
+ # fields = %w(Jan Feb);
14
+ # myarr1_mean = 10
15
+ # myarr1_confidence = 1
16
+ #
17
+ # myarr2_mean = 20
18
+ # myarr2_confidence = 2
19
+ #
20
+ # data= [myarr1_mean, myarr2_mean]
21
+ #
22
+ # err_mesure = [myarr1_confidence, myarr2_confidence]
23
+ #
24
+ # graph = SVG::Graph::ErrBar.new(
17
25
  # :height => 500,
18
- # :width => 300,
19
- # :fields => fields
26
+ # :width => 600,
27
+ # :fields => fields,
28
+ # :errorBars => err_mesure,
29
+ # :scale_integers => true,
20
30
  # )
21
- #
31
+ #
22
32
  # graph.add_data(
23
- # :data => data_sales_02,
33
+ # :data => data,
24
34
  # :title => 'Sales 2002'
25
35
  # )
26
36
  #
@@ -58,19 +68,15 @@ module SVG
58
68
  class ErrBar < BarBase
59
69
  include REXML
60
70
 
61
- def initialize config
62
- raise "fields was not supplied or is empty" unless config[:errorBars] &&
63
- config[:errorBars].kind_of?(Array) &&
64
- config[:errorBars].length > 0
65
- super
66
- end
67
-
68
-
69
- # See Graph::initialize and BarBase::set_defaults
70
- def set_defaults
71
- super
72
- # self.top_align = self.top_font = 1
73
- end
71
+ def initialize config
72
+ raise "fields was not supplied or is empty" unless config[:errorBars] &&
73
+ config[:errorBars].kind_of?(Array) &&
74
+ config[:errorBars].length > 0
75
+ super
76
+ end
77
+ # Array of confidence values for each item in :fields. A range from
78
+ # value[i]-errorBars[i] to value[i]+errorBars[i] is drawn into the graph.
79
+ attr_accessor :errorBars
74
80
 
75
81
  protected
76
82
 
@@ -82,20 +88,24 @@ module SVG
82
88
  maxvalue = max_value
83
89
  minvalue = min_value
84
90
  range = maxvalue - minvalue
91
+ # add some padding on top of the graph
92
+ if range == 0
93
+ maxvalue += 10
94
+ else
95
+ maxvalue += range / 20.0
96
+ end
97
+ scale_range = maxvalue - minvalue
85
98
 
86
- top_pad = range == 0 ? 10 : range / 20.0
87
- scale_range = (maxvalue + top_pad) - minvalue
88
-
89
- scale_division = scale_divisions || (scale_range / 10.0)
99
+ @y_scale_division = scale_divisions || (scale_range / 10.0)
90
100
 
91
101
  if scale_integers
92
- scale_division = scale_division < 1 ? 1 : scale_division.round
102
+ @y_scale_division = @y_scale_division < 1 ? 1 : @y_scale_division.round
93
103
  end
94
104
 
95
105
  rv = []
96
- maxvalue = maxvalue%scale_division == 0 ?
97
- maxvalue : maxvalue + scale_division
98
- minvalue.step( maxvalue, scale_division ) {|v| rv << v}
106
+ maxvalue = maxvalue%@y_scale_division == 0 ?
107
+ maxvalue : maxvalue + @y_scale_division
108
+ minvalue.step( maxvalue, @y_scale_division ) {|v| rv << v}
99
109
  return rv
100
110
  end
101
111
 
@@ -107,13 +117,11 @@ module SVG
107
117
  minvalue = min_value
108
118
  fieldwidth = field_width
109
119
 
110
- unit_size = (@graph_height.to_f - font_size*2*top_font) /
111
- (get_y_labels.max - get_y_labels.min)
120
+ unit_size = field_height
112
121
  bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
113
122
 
114
123
  bar_width = fieldwidth - (bargap *2)
115
124
  bar_width /= @data.length if stack == :side
116
- #x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
117
125
 
118
126
  bottom = @graph_height
119
127
 
@@ -128,7 +136,7 @@ module SVG
128
136
  # +ve -ve value - 0
129
137
  # -ve -ve value.abs - 0
130
138
 
131
- value = dataset[:data][i]
139
+ value = dataset[:data][i].to_f/@y_scale_division
132
140
 
133
141
  left = (fieldwidth * field_count)
134
142
  left += bargap
@@ -147,13 +155,11 @@ module SVG
147
155
  "class" => "fill#{dataset_count+1}"
148
156
  })
149
157
 
150
-
151
-
152
- threshold = @config[:errorBars][i] * unit_size
153
- middlePointErr = left+bar_width/2
154
- upperErr = top+threshold
155
- bottomErr = top-threshold
156
- withthErr = bar_width/4
158
+ threshold = @config[:errorBars][i].to_f/@y_scale_division * unit_size
159
+ middlePointErr = left+bar_width/2
160
+ upperErr = top+threshold
161
+ bottomErr = top-threshold
162
+ withthErr = bar_width/4
157
163
 
158
164
  @graph.add_element( "line", {
159
165
  "x1" => middlePointErr.to_s,
@@ -178,12 +184,14 @@ module SVG
178
184
  "style" => "stroke:rgb(0,0,0);stroke-width:1"
179
185
  })
180
186
 
181
- make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
187
+ make_datapoint_text(left + bar_width/2.0, top - 6, dataset[:data][i].to_s)
188
+ add_popup(left + bar_width/2.0, top , dataset[:data][i].to_s)
182
189
  dataset_count += 1
183
190
  end
184
191
  field_count += 1
185
- }
186
- end
187
- end
192
+ } # config[:fields].each_index
193
+ end # draw_data
194
+
195
+ end # ErrBar
188
196
  end
189
197
  end
@@ -198,11 +198,7 @@ module SVG
198
198
  #
199
199
  def burn
200
200
  raise "No data available" unless @data.size > 0
201
-
202
- # undocumented and not used in any sublass
203
- # to be removed
204
- #calculations if methods.include? 'calculations'
205
-
201
+
206
202
  start_svg
207
203
  calculate_graph_dimensions
208
204
  @foreground = Element.new( "g" )
@@ -381,7 +377,8 @@ module SVG
381
377
  # Customize popup radius
382
378
  attr_accessor :popup_radius
383
379
  # Number format values and Y axis representation like 1.2345667 represent as 1.23,
384
- # defaults to '%.2f'
380
+ # Any valid format accepted by sprintf can be specified.
381
+ # If you don't want to change the format in any way you can use "%s". Defaults to "%.2f"
385
382
  attr_accessor :number_format
386
383
 
387
384
 
@@ -402,9 +399,6 @@ module SVG
402
399
  @popup_radius ||= 10
403
400
  end
404
401
 
405
- # unknown why needed
406
- # attr_accessor :top_align, :top_font, :right_align, :right_font
407
-
408
402
  # size of the square box in the legend which indicates the colors
409
403
  KEY_BOX_SIZE = 12
410
404
 
@@ -431,7 +425,7 @@ module SVG
431
425
  else
432
426
  max_width = y_label_font_size + 3
433
427
  end
434
- max_width += 10 if stagger_y_labels
428
+ max_width += 5 + y_label_font_size if stagger_y_labels
435
429
  return max_width
436
430
  end
437
431
 
@@ -501,8 +495,8 @@ module SVG
501
495
  "onmouseout" =>
502
496
  "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
503
497
  })
504
- end # add_popups
505
- end
498
+ end # if add_popups
499
+ end # add_popup
506
500
 
507
501
  # returns the longest label from an array of labels as string
508
502
  # each object in the array must support .to_s
@@ -543,7 +537,7 @@ module SVG
543
537
  else
544
538
  max_height = x_label_font_size + 3
545
539
  end
546
- max_height += 10 if stagger_x_labels
540
+ max_height += 5 + x_label_font_size if stagger_x_labels
547
541
  return max_height
548
542
  end
549
543
 
@@ -598,9 +592,11 @@ module SVG
598
592
  if( numeric?(value) )
599
593
  textStr = @number_format % value
600
594
  end
601
- # change anchor is label overlaps axis
602
- if x < textStr.length/2 * font_size * 0.6
595
+ # change anchor is label overlaps axis, normally anchor is middle (that's why we compute length/2)
596
+ if x < textStr.length/2 * font_size
603
597
  style << "text-anchor: start;"
598
+ elsif x > @graph_width - textStr.length/2 * font_size
599
+ style << "text-anchor: end;"
604
600
  end
605
601
  # white background for better readability
606
602
  @foreground.add_element( "text", {
@@ -694,9 +690,9 @@ module SVG
694
690
 
695
691
  # space in px between x-labels
696
692
  def field_width
697
- #(@graph_width.to_f - font_size*2*right_font) /
698
- # (get_x_labels.length - right_align)
699
- @graph_width.to_f / get_x_labels.length
693
+ # -1 is to use entire x-axis
694
+ # otherwise there is always 1 division unused
695
+ @graph_width.to_f / ( get_x_labels.length - 1 )
700
696
  end
701
697
 
702
698
  # space in px between the y-labels
@@ -880,14 +876,7 @@ module SVG
880
876
  x_offset = @border_left + 20
881
877
  y_offset = @border_top + @graph_height + 5
882
878
  if show_x_labels
883
- # max_x_label_height_px = (not rotate_x_labels) ?
884
- # x_label_font_size :
885
- # get_x_labels.max{|a,b|
886
- # a.to_s.length<=>b.to_s.length
887
- # }.to_s.length * x_label_font_size * 0.6
888
- # x_label_font_size
889
879
  y_offset += max_x_label_height_px
890
- y_offset += max_x_label_height_px + 5 if stagger_x_labels
891
880
  end
892
881
  y_offset += x_title_font_size + 5 if show_x_title
893
882
  end
@@ -104,7 +104,6 @@ module SVG
104
104
  :stacked => false,
105
105
  :area_fill => false
106
106
  )
107
- # self.top_align = self.top_font = self.right_align = self.right_font = 1
108
107
  end
109
108
 
110
109
  protected
@@ -157,29 +156,26 @@ module SVG
157
156
  def get_y_labels
158
157
  maxvalue = max_value
159
158
  minvalue = min_value
160
- #
161
159
  range = maxvalue - minvalue
160
+ # add some padding on top of the graph
162
161
  if range == 0
163
- top_pad = 10
162
+ maxvalue += 10
164
163
  else
165
- top_pad = range / 20.0
164
+ maxvalue += range / 20.0
166
165
  end
167
- scale_range = (maxvalue + top_pad) - minvalue
166
+ scale_range = maxvalue - minvalue
168
167
 
169
168
  @y_scale_division = scale_divisions || (scale_range / 10.0)
170
-
169
+ @y_offset = 0
170
+
171
171
  if scale_integers
172
- # only use integers if there will be at least 3 labels and division is > 0.5
173
- if maxvalue/@y_scale_division >= 3 && @y_scale_division > 0.5
174
- @y_scale_division = @y_scale_division.round
175
- end
172
+ @y_scale_division = @y_scale_division < 1 ? 1 : @y_scale_division.round
173
+ @y_offset = (minvalue.to_f - minvalue.floor).to_f
174
+ minvalue = minvalue.floor
176
175
  end
177
176
 
178
177
  rv = []
179
- # make sure we have at least one label higher than the max_value
180
- if maxvalue%@y_scale_division != 0
181
- maxvalue = maxvalue + @y_scale_division
182
- end
178
+
183
179
  minvalue.step( maxvalue, @y_scale_division ) {|v| rv << v}
184
180
  return rv
185
181
  end
@@ -187,20 +183,19 @@ module SVG
187
183
  def calc_coords(field, value, width = field_width, height = field_height)
188
184
  coords = {:x => 0, :y => 0}
189
185
  coords[:x] = width * field
190
- coords[:y] = @graph_height - value/@y_scale_division * height
191
-
186
+ # make sure we do float division, otherwise coords get messed up
187
+ coords[:y] = @graph_height - (value + @y_offset)/@y_scale_division.to_f * height
192
188
  return coords
193
189
  end
194
190
 
195
191
  def draw_data
196
192
  minvalue = min_value
197
- #fieldheight = (@graph_height.to_f - font_size*2*top_font) /
198
- # (get_y_labels.max - get_y_labels.min)
199
193
  fieldheight = field_height
200
194
  fieldwidth = field_width
201
195
  line = @data.length
202
-
203
- prev_sum = Array.new(@config[:fields].length).fill(0)
196
+ # always zero for filling areas
197
+ prev_sum = Array.new(@config[:fields].length).fill(-@y_offset)
198
+ # cumulated sum (used for stacked graphs)
204
199
  cum_sum = Array.new(@config[:fields].length).fill(nil)
205
200
 
206
201
  for data in @data.reverse
@@ -234,7 +229,7 @@ module SVG
234
229
  c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
235
230
  else
236
231
  apath = "V#@graph_height"
237
- c = calc_coords(0, 0, fieldwidth, fieldheight)
232
+ c = calc_coords(0, -@y_offset, fieldwidth, fieldheight)
238
233
  end
239
234
 
240
235
  @graph.add_element("path", {
@@ -255,18 +250,13 @@ module SVG
255
250
  c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
256
251
  if show_data_points
257
252
  @graph.add_element( "circle", {
258
- # "cx" => (fieldwidth * i).to_s,
259
- # "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
260
253
  "cx" => c[:x].to_s,
261
254
  "cy" => c[:y].to_s,
262
255
  "r" => "2.5",
263
256
  "class" => "dataPoint#{line}"
264
257
  })
265
258
  end
266
- #x = fieldwidth * i
267
- #y = @graph_height - cum_sum[i] * fieldheight
268
- #make_datapoint_text( x, y - font_size/2, cum_sum[i] + minvalue)
269
- #add_popup(x, y, cum_sum[i] + minvalue)
259
+
270
260
  make_datapoint_text( c[:x], c[:y] - font_size/2, cum_sum[i] + minvalue)
271
261
  add_popup(c[:x], c[:y], cum_sum[i] + minvalue)
272
262
  end
data/lib/SVG/Graph/Pie.rb CHANGED
@@ -204,7 +204,6 @@ module SVG
204
204
  diameter -= 10 if show_shadow
205
205
  radius = diameter / 2.0
206
206
 
207
- #xoff = (width - diameter) / 2
208
207
  xoff = (@graph_width - diameter) / 2
209
208
  yoff = (height - @border_bottom - diameter)
210
209
  yoff -= 10 if show_shadow
@@ -224,7 +223,7 @@ module SVG
224
223
  prev_percent = 0
225
224
  rad_mult = 3.6 * RADIANS
226
225
  @config[:fields].each_index { |count|
227
- value = @data[count]
226
+ value = @data[count].to_f
228
227
  percent = percent_scale * value
229
228
  radians = prev_percent * rad_mult
230
229
 
@@ -67,8 +67,11 @@ module SVG
67
67
  #
68
68
  # Unlike the other types of charts, data sets must contain x,y pairs:
69
69
  #
70
- # [ 1, 2 ] # A data set with 1 point: (1,2)
71
- # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
70
+ # [ 1,2 ] # A data set with 1 point: (1,2)
71
+ # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
72
+ # Additional possible notation
73
+ # [ [1,2], 5,6] # A data set with 2 points: (1,2) and (5,6), mixed notation
74
+ # [ [1,2], [5,6]] # A data set with 2 points: (1,2) and (5,6), nested array
72
75
  #
73
76
  # = See also
74
77
  #
@@ -89,20 +92,21 @@ module SVG
89
92
  class Plot < Graph
90
93
 
91
94
  # In addition to the defaults set by Graph::initialize, sets
92
- # [show_data_values] true
93
95
  # [show_data_points] true
94
96
  # [area_fill] false
95
- # [stacked] false
97
+ # [stacked] false, will not have any effect if true
98
+ # [show_lines] true
99
+ # [round_popups] true
96
100
  def set_defaults
97
101
  init_with(
98
- :show_data_values => true,
99
- :show_data_points => true,
100
- :area_fill => false,
101
- :stacked => false,
102
- :show_lines => true,
103
- :round_popups => true
104
- )
105
- # self.top_align = self.right_align = self.top_font = self.right_font = 1
102
+ :show_data_points => true,
103
+ :area_fill => false,
104
+ :stacked => false,
105
+ :show_lines => true,
106
+ :round_popups => true,
107
+ :scale_x_integers => false,
108
+ :scale_y_integerrs => false,
109
+ )
106
110
  end
107
111
 
108
112
  # Determines the scaling for the X axis divisions.
@@ -111,6 +115,7 @@ module SVG
111
115
  #
112
116
  # would cause the graph to attempt to generate labels stepped by 2; EG:
113
117
  # 0,2,4,6,8...
118
+ # default is automatic such that there are 10 labels
114
119
  attr_accessor :scale_x_divisions
115
120
  # Determines the scaling for the Y axis divisions.
116
121
  #
@@ -118,33 +123,38 @@ module SVG
118
123
  #
119
124
  # would cause the graph to attempt to generate labels stepped by 0.5; EG:
120
125
  # 0, 0.5, 1, 1.5, 2, ...
126
+ # default is automatic such that there are 10 labels
121
127
  attr_accessor :scale_y_divisions
122
- # Make the X axis labels integers
128
+ # Make the X axis labels integers, default: false
123
129
  attr_accessor :scale_x_integers
124
- # Make the Y axis labels integers
130
+ # Make the Y axis labels integers, default: false
125
131
  attr_accessor :scale_y_integers
126
- # Fill the area under the line
132
+ # Fill the area under the line, default: false
127
133
  attr_accessor :area_fill
128
134
  # Show a small circle on the graph where the line
129
- # goes from one point to the next.
135
+ # goes from one point to the next. default: true
130
136
  attr_accessor :show_data_points
131
- # Set the minimum value of the X axis
137
+ # Set the minimum value of the X axis, if nil the minimum from data is chosen, default: nil
132
138
  attr_accessor :min_x_value
133
- # Set the maximum value of the X axis
139
+ # Set the maximum value of the X axis, if nil the maximum from data is chosen, default: nil
134
140
  attr_accessor :max_x_value
135
- # Set the minimum value of the Y axis
141
+ # Set the minimum value of the Y axis, if nil the minimum from data is chosen, default: nil
136
142
  attr_accessor :min_y_value
137
- # Set the maximum value of the Y axis
143
+ # Set the maximum value of the Y axis, if nil the maximum from data is chosen, default: nil
138
144
  attr_accessor :max_y_value
139
- # Show lines connecting data points
145
+ # Show lines connecting data points, default: true
140
146
  attr_accessor :show_lines
141
- # Round value of data points in popups to integer
147
+ # Round value of data points in popups to integer, default: true
142
148
  attr_accessor :round_popups
143
149
 
144
150
 
145
151
  # Adds data to the plot. The data must be in X,Y pairs; EG
146
152
  # data_set1 = [ 1, 2 ] # A data set with 1 point: (1,2)
147
- # data_set2 = [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
153
+ # data_set2 = [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
154
+ # It's also supported to supply nested array or a mix (flatten is applied to the array); EG
155
+ # data_set2 = [[1,2], 5,6]
156
+ # or
157
+ # data_set2 = [[1,2], [5,6]]
148
158
  #
149
159
  # graph.add_data({
150
160
  # :data => data_set1,
@@ -156,13 +166,18 @@ module SVG
156
166
  # })
157
167
  def add_data(conf)
158
168
  @data ||= []
159
- # remove nil values
160
- conf[:data] = conf[:data].compact
161
169
  raise "No data provided by #{conf.inspect}" unless conf[:data] and
162
170
  conf[:data].kind_of? Array
171
+ # support array of arrays and flatten it
172
+ conf[:data] = conf[:data].flatten
173
+ # check that we have pairs of values
163
174
  raise "Data supplied must be x,y pairs! "+
164
175
  "The data provided contained an odd set of "+
165
176
  "data points" unless conf[:data].length % 2 == 0
177
+
178
+ # remove nil values
179
+ conf[:data] = conf[:data].compact
180
+
166
181
  return if conf[:data].length == 0
167
182
 
168
183
  conf[:description] ||= Array.new(conf[:data].size/2)
@@ -177,6 +192,11 @@ module SVG
177
192
  }
178
193
  sort( x, y, conf[:description] )
179
194
  conf[:data] = [x,y]
195
+ # at the end data looks like:
196
+ # [
197
+ # [all x values],
198
+ # [all y values]
199
+ # ]
180
200
  @data << conf
181
201
  end
182
202
 
@@ -214,27 +234,34 @@ module SVG
214
234
  min_value
215
235
  end
216
236
 
217
- def x_range
237
+ def x_label_range
218
238
  max_value = max_x_range
219
239
  min_value = min_x_range
220
-
221
240
  range = max_value - min_value
222
- right_pad = range == 0 ? 10 : range / 20.0
223
- scale_range = (max_value + right_pad) - min_value
241
+ # add some padding on right
242
+ if range == 0
243
+ max_value += 10
244
+ else
245
+ max_value += range / 20.0
246
+ end
247
+ scale_range = max_value - min_value
224
248
 
225
249
  scale_division = scale_x_divisions || (scale_range / 10.0)
226
-
250
+ @x_offset = 0
251
+
227
252
  if scale_x_integers
228
253
  scale_division = scale_division < 1 ? 1 : scale_division.round
254
+ @x_offset = min_value.to_f - min_value.floor
255
+ min_value = min_value.floor
229
256
  end
230
257
 
231
258
  [min_value, max_value, scale_division]
232
259
  end
233
260
 
234
261
  def get_x_values
235
- min_value, max_value, scale_division = x_range
262
+ min_value, max_value, @x_scale_division = x_label_range
236
263
  rv = []
237
- min_value.step( max_value, scale_division ) {|v| rv << v}
264
+ min_value.step( max_value, @x_scale_division ) {|v| rv << v}
238
265
  return rv
239
266
  end
240
267
  alias :get_x_labels :get_x_values
@@ -242,11 +269,8 @@ module SVG
242
269
  def field_width
243
270
  # exclude values which are outside max_x_range
244
271
  values = get_x_values
245
- max = max_x_range
246
- dx = (max - values[-1]).to_f / (values[-1] - values[-2])
247
- #(@graph_width.to_f - font_size*2*right_font) /
248
- # (values.length + dx - right_align)
249
- @graph_width.to_f / values.length
272
+ @graph_width.to_f / (values.length - 1 ) # -1 is to use entire x-axis
273
+ # otherwise there is always 1 division unused
250
274
  end
251
275
 
252
276
 
@@ -262,32 +286,39 @@ module SVG
262
286
  min_value
263
287
  end
264
288
 
265
- def y_range
289
+ def y_label_range
266
290
  max_value = max_y_range
267
291
  min_value = min_y_range
268
-
269
292
  range = max_value - min_value
270
- top_pad = range == 0 ? 10 : range / 20.0
271
- scale_range = (max_value + top_pad) - min_value
293
+ # add some padding on top
294
+ if range == 0
295
+ max_value += 10
296
+ else
297
+ max_value += range / 20.0
298
+ end
299
+ scale_range = max_value - min_value
272
300
 
273
301
  scale_division = scale_y_divisions || (scale_range / 10.0)
274
-
302
+ @y_offset = 0
303
+
275
304
  if scale_y_integers
276
305
  scale_division = scale_division < 1 ? 1 : scale_division.round
306
+ @y_offset = (min_value.to_f - min_value.floor).to_f
307
+ min_value = min_value.floor
277
308
  end
278
309
 
279
310
  return [min_value, max_value, scale_division]
280
311
  end
281
312
 
282
313
  def get_y_values
283
- min_value, max_value, scale_division = y_range
314
+ min_value, max_value, @y_scale_division = y_label_range
284
315
  if max_value != min_value
285
- while (max_value - min_value) < scale_division
286
- scale_division /= 10.0
316
+ while (max_value - min_value) < @y_scale_division
317
+ @y_scale_division /= 10.0
287
318
  end
288
319
  end
289
320
  rv = []
290
- min_value.step( max_value, scale_division ) {|v| rv << v}
321
+ min_value.step( max_value, @y_scale_division ) {|v| rv << v}
291
322
  rv << rv[0] + 1 if rv.length == 1
292
323
  return rv
293
324
  end
@@ -302,18 +333,25 @@ module SVG
302
333
  else
303
334
  dx = (max - values[-1]).to_f / (values[-1] - values[-2])
304
335
  end
305
- #(@graph_height.to_f - font_size*2*top_font) /
306
- # (values.length + dx - top_align)
307
336
  @graph_height.to_f / values.length
308
337
  end
338
+
339
+ def calc_coords(x, y)
340
+ coords = {:x => 0, :y => 0}
341
+ # scale the coordinates, use float division / multiplication
342
+ # otherwise the point will be place inaccurate
343
+ coords[:x] = (x + @x_offset)/@x_scale_division.to_f * field_width
344
+ coords[:y] = @graph_height - (y + @y_offset)/@y_scale_division.to_f * field_height
345
+ return coords
346
+ end
309
347
 
310
348
  def draw_data
311
349
  line = 1
312
350
 
313
- x_min, x_max = x_range
314
- y_min, y_max = y_range
315
- x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
316
- y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
351
+ x_min = min_x_range
352
+ x_max = max_x_range
353
+ y_min = min_y_range
354
+ y_max = max_y_range
317
355
 
318
356
  for data in @data
319
357
  x_points = data[:data][X]
@@ -323,10 +361,9 @@ module SVG
323
361
  x_start = 0
324
362
  y_start = 0
325
363
  x_points.each_index { |idx|
326
- x = (x_points[idx] - x_min) * x_step
327
- y = @graph_height - (y_points[idx] - y_min) * y_step
328
- x_start, y_start = x,y if idx == 0
329
- lpath << "#{x} #{y} "
364
+ c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min)
365
+ x_start, y_start = c[:x],c[:y] if idx == 0
366
+ lpath << "#{c[:x]} #{c[:y]} "
330
367
  }
331
368
 
332
369
  if area_fill
@@ -345,25 +382,25 @@ module SVG
345
382
 
346
383
  if show_data_points || show_data_values || add_popups
347
384
  x_points.each_index { |idx|
348
- x = (x_points[idx] - x_min) * x_step
349
- y = @graph_height - (y_points[idx] - y_min) * y_step
385
+ c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min)
350
386
  if show_data_points
351
- DataPoint.new(x, y, line).shape(data[:description][idx]).each{|s|
387
+ DataPoint.new(c[:x], c[:y], line).shape(data[:description][idx]).each{|s|
352
388
  @graph.add_element( *s )
353
389
  }
354
390
  end
355
- make_datapoint_text( x, y-6, y_points[idx] )
356
- add_popup(x, y, format( x_points[idx], y_points[idx], data[:description][idx]))
391
+ make_datapoint_text( c[:x], c[:y]-6, y_points[idx] )
392
+ add_popup(c[:x], c[:y], format( x_points[idx], y_points[idx], data[:description][idx]))
357
393
  }
358
394
  end
359
395
  line += 1
360
396
  end
361
397
  end
362
-
398
+
399
+ # returns the formatted string which is added as popup information
363
400
  def format x, y, desc
364
401
  info = []
365
- info << (round_popups ? (x * 100).to_i / 100 : x)
366
- info << (round_popups ? (y * 100).to_i / 100 : y)
402
+ info << (round_popups ? x.round : @number_format % x )
403
+ info << (round_popups ? y.round : @number_format % y )
367
404
  info << desc
368
405
  "(#{info.compact.join(', ')})"
369
406
  end
@@ -140,7 +140,7 @@ module SVG
140
140
  # Note that the data must be in time,value pairs, and that the date format
141
141
  # may be any date that is parseable by DateTime#parse, DateTime#strptime.
142
142
  # The :template argument is optional. By default DateTime#parse will be used.
143
- # Checkout the ruby doc for valit template notation:
143
+ # Checkout the ruby doc for valid template notation:
144
144
  # http://ruby-doc.org/stdlib-2.3.1/libdoc/date/rdoc/Date.html#method-i-strftime
145
145
  # http://ruby-doc.org/stdlib-2.3.1/libdoc/date/rdoc/DateTime.html#method-c-parse
146
146
  # http://ruby-doc.org/stdlib-2.3.1/libdoc/date/rdoc/DateTime.html#method-c-strptime.
@@ -109,12 +109,12 @@ module SVG
109
109
  init_with(
110
110
  #:max_time_span => '',
111
111
  :x_label_format => '%Y-%m-%d %H:%M:%S',
112
- :popup_format => '%Y-%m-%d %H:%M:%S'
112
+ :popup_format => '%Y-%m-%d %H:%M:%S',
113
113
  )
114
114
  end
115
115
 
116
- # The format string use do format the X axis labels.
117
- # See Time::strformat
116
+ # The format string used to format the X axis labels.
117
+ # See Time::strformat, default: '%Y-%m-%d %H:%M:%S'
118
118
  attr_accessor :x_label_format
119
119
  # Use this to set the spacing between dates on the axis. The value
120
120
  # must be of the form
@@ -138,7 +138,7 @@ module SVG
138
138
  # graph.add_data(
139
139
  # :data => d1,
140
140
  # :title => 'One',
141
- # :template => '%H:%M'
141
+ # :template => '%H:%M' #template is optional
142
142
  # )
143
143
  # graph.add_data(
144
144
  # :data => d2,
@@ -150,26 +150,24 @@ module SVG
150
150
  # a format that is parseable by ParseDate, a Time object, or a number of seconds
151
151
  # after the unix epoch.
152
152
  def add_data data
153
- @time_template = data[:template]
154
153
  data[:data].each_index do |i|
155
- data[:data][i] = parse_time(data[:data][i]).to_i if i % 2 == 0
154
+ data[:data][i] = parse_time(data[:data][i], data[:template]).to_i if i % 2 == 0 # only even indices are time, odd indices are values
156
155
  end
157
- @time_template = nil # reset for the next data series
158
156
  super(data)
159
157
  end
160
158
 
161
159
 
162
160
  protected
163
161
 
164
-
162
+ # value must be Integer, Time, or parseable by DateTime#parse
165
163
  def min_x_value=(value)
166
- t = parse_time(value)
164
+ t = parse_time(value, nil)
167
165
  @min_x_value = t.to_i
168
166
  end
169
167
 
170
- # value should be
168
+ # value must be Integer, Time, or parseable by DateTime#parse
171
169
  def max_x_value=(value)
172
- t = parse_time(value)
170
+ t = parse_time(value, nil)
173
171
  @max_x_value = t.to_i
174
172
  end
175
173
 
@@ -190,13 +188,13 @@ module SVG
190
188
  # Accepts date time as a string, number of seconds since the epoch, or Time
191
189
  # object and returns a Time object. Raises an error if not a valid date time
192
190
  # representation.
193
- def parse_time(time)
191
+ def parse_time(time, template)
194
192
  case time
195
193
  when Time
196
194
  return time
197
195
  when String
198
- if @time_template.kind_of? String
199
- return DateTime.strptime(time, @time_template).to_time
196
+ if template.kind_of? String
197
+ return DateTime.strptime(time, template).to_time
200
198
  else
201
199
  return DateTime.parse(time).to_time
202
200
  end
@@ -209,7 +207,7 @@ module SVG
209
207
 
210
208
  def get_x_values
211
209
  rv = []
212
- min, max, scale_division = x_range
210
+ min, max, @x_scale_division = x_label_range
213
211
  if timescale_divisions
214
212
  timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/
215
213
  division_units = $2 ? $2 : "day"
@@ -219,6 +217,7 @@ module SVG
219
217
  case division_units
220
218
  when "month"
221
219
  cur = min
220
+ @x_scale_division = 365.25/12 * 24 * 60 * 60 * amount
222
221
  while cur < max
223
222
  rv << cur
224
223
  arr = Time.at( cur ).to_a
@@ -231,6 +230,7 @@ module SVG
231
230
  end
232
231
  when "year"
233
232
  cur = min
233
+ @x_scale_division = 365.25 * 24 * 60 * 60 * amount
234
234
  while cur < max
235
235
  rv << cur
236
236
  arr = Time.at( cur ).to_a
@@ -248,12 +248,13 @@ module SVG
248
248
  when "second"
249
249
  step = amount
250
250
  end
251
+ # only do this if division_units is not year or month. Those are done already above in the cases.
251
252
  min.step( max, step ) {|v| rv << v} if step
252
-
253
+ @x_scale_division = step if step
253
254
  return rv
254
255
  end
255
256
  end
256
- min.step( max, scale_division ) {|v| rv << v}
257
+ min.step( max, @x_scale_division ) {|v| rv << v}
257
258
  return rv
258
259
  end # get_x_values
259
260
 
data/lib/svggraph.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module SVG
2
2
  module Graph
3
- VERSION = '2.0.2.beta'
3
+ VERSION = '2.0.2'
4
4
  end
5
5
  end
6
6
  require_relative 'SVG/Graph/DataPoint'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svg-graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2.beta
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Russell
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2016-08-08 00:00:00.000000000 Z
15
+ date: 2016-10-26 00:00:00.000000000 Z
16
16
  dependencies: []
17
17
  description: "Gem version of SVG:::Graph. SVG:::Graph is a pure Ruby library for generating
18
18
  charts, \nwhich are a type of graph where the values of one axis are not scalar.
@@ -67,9 +67,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
67
  version: '0'
68
68
  required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  requirements:
70
- - - ">"
70
+ - - ">="
71
71
  - !ruby/object:Gem::Version
72
- version: 1.3.1
72
+ version: '0'
73
73
  requirements: []
74
74
  rubyforge_project:
75
75
  rubygems_version: 2.6.7