svg-graph 2.0.2.beta → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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