svg-graph 2.2.0 → 2.2.1

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
  SHA256:
3
- metadata.gz: c448a6423accd8618fa44587eeca0dc4f7b21ea99508c21dd95e5329cef59a5c
4
- data.tar.gz: 4775e687abb3ebe350754e3287b925ab6b5d5624158664aacecfffc6cbcff680
3
+ metadata.gz: c6eaa56f94856a998ecd2f0befef3a38c5529da886266cd15150cf4e9758168f
4
+ data.tar.gz: f9eb26d8c758153505d95d450e57c3fa0dfbfcdd9018d89bb6bcee3b656cebff
5
5
  SHA512:
6
- metadata.gz: 3ba65a950336a3718b7c0fd01ddcd55261911ac00542518914165e22388bdbafd834ed928980af8aa4ad75523ad8572110fb82ef5b933897243b2153d52aed38
7
- data.tar.gz: 19166679a0a198b667c240162aeb2438f77662130a03cbf430b289804b286b0fcd0c7e743b245eaebe2e982110946482ec6da94a02e29cc15d633808b74d952d
6
+ metadata.gz: 5c77c61810a9d855bc469ac4672d9917f0bbb345b5464235c0a06e9d901aa796c8cba67c12e181200ca3865ea2ce9629d8ae39065aec831b77c707fdc46720d4
7
+ data.tar.gz: d270039fc1ea0340a515f554c4ff1f6437d0138c62ff09daafb5c1599df819d096ab04ffa23fe8862cac16b4bd6fcc71efea973e001fa97cfd1e5058e7444a82
@@ -2,6 +2,14 @@ TODO / Backlog
2
2
  * refactor various hardcoded constant pixel offsets in Graph class to use named constants or variables
3
3
  * Fix bug in Plot where min/max_x/y_value are not respected, TODO
4
4
 
5
+ === 2.2.1 / 2020-12-25
6
+ * Remove inline styling for data point labels and popups [thanks marnen, PR #23]
7
+ * fix #29 text background not aligned close to axis due to missing anchors
8
+ * add & document :shape and :url hash keys for `add_data` [thanks t12nslookup, PR #32]
9
+ * add custom datapoint support to Line graphs
10
+ * Add white text behind popup text so that the black text is more readable [thanks t12nslookup, PR #30]
11
+ * fix #19 rotated lables should no longer be cropped or overlapping
12
+
5
13
  === 2.2.0 / 2019-11-26
6
14
  * on top of 2.2.0.beta adds the following
7
15
  * Fixed Divizion by zero when data is 0,0 in Pie [thanks chrismedrdz, PR #16]
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  SVG::Graph
2
2
  ============
3
3
 
4
+ [![Build Status](https://travis-ci.com/lumean/svg-graph2.svg?branch=master)](https://travis-ci.com/lumean/svg-graph2)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/0a2b2d977bb9a43f488a/maintainability)](https://codeclimate.com/github/lumean/svg-graph2/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/0a2b2d977bb9a43f488a/test_coverage)](https://codeclimate.com/github/lumean/svg-graph2/test_coverage)
7
+ [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/a5e00e98/svg-graph2)
8
+
4
9
  Description
5
10
  -----------
6
11
  This repo is the continuation of the original [SVG::Graph library](http://www.germane-software.com/software/SVG/SVG::Graph/) by Sean Russell. I'd like to thank Claudio Bustos for giving me permissions to continue publishing the gem under it's original name: [svg-graph](https://rubygems.org/gems/svg-graph)
@@ -106,14 +111,22 @@ Source: [C3js.rb](../master/examples/c3js.rb)
106
111
 
107
112
  [Link to Preview](https://cdn.rawgit.com/lumean/svg-graph2/master/examples/c3js.html)
108
113
 
109
- <iframe src="https://cdn.rawgit.com/lumean/svg-graph2/master/examples/c3js.html" width="600px"> </iframe>
110
-
111
- 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.
112
114
 
113
115
  Build
114
116
  -----
117
+ * Test
118
+
119
+ `bundle exec rake`
115
120
 
116
121
  * Build gem:
117
- * gem build svg-graph.gemspec
122
+
123
+ `gem build svg-graph.gemspec`
124
+
118
125
  * Install:
119
- * gem install svg-graph-\<version>.gem
126
+
127
+ `gem install svg-graph-\<version>.gem`
128
+
129
+ Percy.io integration
130
+ ---
131
+ https://docs.percy.io/docs/travis-ci
132
+ https://docs.percy.io/docs/snapshot-cli-command
data/Rakefile CHANGED
@@ -21,12 +21,22 @@
21
21
  # self.remote_rdoc_dir = 'svg-graph'
22
22
  #end
23
23
 
24
- # run all unit tests with 'rake test'
25
- task default: %w[test]
24
+ # by default run all unit tests with 'rake test'
25
+ task default: [:test]
26
26
 
27
27
  task :test do
28
- ruby "test/test_data_point.rb"
29
- ruby "test/test_plot.rb"
30
- ruby "test/test_svg_graph.rb"
31
- ruby "test/test_graph.rb"
28
+ [
29
+ "test/test_data_point.rb",
30
+ "test/test_plot.rb",
31
+ "test/test_svg_graph.rb",
32
+ "test/test_graph.rb",
33
+ "test/run_examples_and_percy.io.rb"
34
+ ].each do |file|
35
+ # exec all above scripts (with simplecov if env is set)
36
+ args = file
37
+ if ENV['COVERAGE']
38
+ args = '-r ./test/simplecov ' + file
39
+ end
40
+ ruby args
41
+ end
32
42
  end
@@ -1,6 +1,8 @@
1
1
  # Allows to customize datapoint shapes
2
2
  class DataPoint
3
- OVERLAY = "OVERLAY" unless defined?(OVERLAY)
3
+ # magic string that defines if a shape is intented to be overlayed to a default.
4
+ # this allowes to have strike through of a circle etc.
5
+ OVERLAY = "OVERLAY"
4
6
  DEFAULT_SHAPE = lambda{|x,y,line| ["circle", {
5
7
  "cx" => x,
6
8
  "cy" => y,
@@ -9,27 +11,33 @@ class DataPoint
9
11
  }]
10
12
  } unless defined? DEFAULT_SHAPE
11
13
  CRITERIA = [] unless defined? CRITERIA
12
-
14
+
13
15
  # matchers are class scope. Once configured, each DataPoint instance will have
14
16
  # access to the same matchers
15
- # @param matchers [Array] multiple arrays of the following form:
16
- # [ regex ,
17
+ # @param matchers [Array] multiple arrays of the following form 2 or 3 elements:
18
+ # [ regex ,
17
19
  # 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"]
20
+ # -> return value of the lambda must be an array: [svg tag name,
21
+ # Hash with attributes for the svg tag, e.g. "points" and "class",
22
+ # make sure to check source code of you graph type for valid css class.],
23
+ # "OVERLAY" (magic string, if specified, puts the shape on top of existing datapoint)
19
24
  # ]
20
25
  # @example
21
26
  # 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
- # }]
27
+ # [/.*/, lambda{|x,y,line|
28
+ # [ 'polygon',
29
+ # {
30
+ # "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}",
31
+ # "class" => "dataPoint#{line}"
32
+ # }
33
+ # ]
26
34
  # }]
27
35
  # )
28
36
  def DataPoint.configure_shape_criteria(*matchers)
29
37
  CRITERIA.push(*matchers)
30
38
  end
31
-
32
- #
39
+
40
+ #
33
41
  def DataPoint.reset_shape_criteria
34
42
  CRITERIA.clear
35
43
  end
@@ -43,7 +51,11 @@ class DataPoint
43
51
  @y = y
44
52
  @line = line
45
53
  end
46
-
54
+
55
+ # Returns different shapes depending on datapoint descriptions, if shape criteria have been configured.
56
+ # The definded criteria are evaluated in two stages, first the ones, which are note defined as overlay.
57
+ # then the "OVERLAY"
58
+ # @param datapoint_description [String] description or label of the current datapoint
47
59
  # @return [Array<Array>] see example
48
60
  # @example Return value
49
61
  # # two dimensional array, the splatted (*) inner array can be used as argument to REXML::add_element
@@ -53,12 +65,12 @@ class DataPoint
53
65
  # # for each svg we insert it to the graph
54
66
  # dp.each {|s| @graph.add_element( *s )}
55
67
  #
56
- def shape(description=nil)
57
- # select all criteria with size 2, and collect rendered lambdas in an array
68
+ def shape(datapoint_description=nil)
69
+ # select all criteria with size 2, and collect rendered lambdas in an array
58
70
  shapes = CRITERIA.select {|criteria|
59
71
  criteria.size == 2
60
72
  }.collect {|regexp, proc|
61
- proc.call(@x, @y, @line) if description =~ regexp
73
+ proc.call(@x, @y, @line) if datapoint_description =~ regexp
62
74
  }.compact
63
75
  # if above did not render anything use the defalt shape
64
76
  shapes = [DEFAULT_SHAPE.call(@x, @y, @line)] if shapes.empty?
@@ -66,7 +78,7 @@ class DataPoint
66
78
  overlays = CRITERIA.select { |criteria|
67
79
  criteria.last == OVERLAY
68
80
  }.collect { |regexp, proc|
69
- proc.call(@x, @y, @line) if description =~ regexp
81
+ proc.call(@x, @y, @line) if datapoint_description =~ regexp
70
82
  }.compact
71
83
 
72
84
  return shapes + overlays
@@ -103,6 +103,7 @@ module SVG
103
103
  # [number_format] '%.2f'
104
104
  def initialize( config )
105
105
  @config = config
106
+ # array of Hash
106
107
  @data = []
107
108
  #self.top_align = self.top_font = 0
108
109
  #self.right_align = self.right_font = 0
@@ -180,16 +181,38 @@ module SVG
180
181
  # :data => data_sales_02,
181
182
  # :title => 'Sales 2002'
182
183
  # })
183
- def add_data conf
184
- @data = [] unless (defined? @data and !@data.nil?)
184
+ # @param conf [Hash] with the following keys:
185
+ # :data [Array] mandatory
186
+ # :title [String] mandatory name of data series for legend of graph
187
+ # :description [Array<String>] (optional) if given, description for each datapoint (shown in popups)
188
+ # :shape [Array<String>] (optional) if given, DataPoint shape is chosen based on this string instead of description
189
+ # :url [Array<String>] (optional) if given, link will be added to each datapoint
190
+ def add_data(conf)
191
+ @data ||= []
192
+ raise "No data provided by #{conf.inspect}" unless conf[:data].is_a?(Array)
193
+
194
+ add_data_init_or_check_optional_keys(conf, conf[:data].size)
195
+ @data << conf
196
+ end
185
197
 
186
- if conf[:data] and conf[:data].kind_of? Array
187
- @data << conf
188
- else
189
- raise "No data provided by #{conf.inspect}"
198
+ # Checks all optional keys of the add_data method
199
+ def add_data_init_or_check_optional_keys(conf, datasize)
200
+ conf[:description] ||= Array.new(datasize)
201
+ conf[:shape] ||= Array.new(datasize)
202
+ conf[:url] ||= Array.new(datasize)
203
+
204
+ if conf[:description].size != datasize
205
+ raise "Description for popups does not have same size as provided data: #{conf[:description].size} vs #{conf[:data].size/2}"
190
206
  end
191
- end
192
207
 
208
+ if conf[:shape].size != datasize
209
+ raise "Shapes for points do not have same size as provided data: #{conf[:shape].size} vs #{conf[:data].size/2}"
210
+ end
211
+
212
+ if conf[:url].size != datasize
213
+ raise "URLs for points do not have same size as provided data: #{conf[:url].size} vs #{conf[:data].size/2}"
214
+ end
215
+ end
193
216
 
194
217
  # This method removes all data from the object so that you can
195
218
  # reuse it to create a new graph but with the same config options.
@@ -323,7 +346,7 @@ module SVG
323
346
  attr_accessor :rotate_x_labels
324
347
  # This turns the Y axis labels by 90 degrees when true or by a custom
325
348
  # amount when a numeric value is given.
326
- # Default is true, to turn on set to false.
349
+ # Default is false, to turn on set to true or numeric value.
327
350
  attr_accessor :rotate_y_labels
328
351
  # How many "steps" to use between displayed X axis labels,
329
352
  # a step of one means display every label, a step of two results
@@ -436,17 +459,17 @@ module SVG
436
459
 
437
460
  protected
438
461
 
439
- # implementation of quicksort
440
- # used for Schedule and Plot
462
+ # implementation of a multiple array sort used for Schedule and Plot
441
463
  def sort( *arrys )
442
- sort_multiple( arrys )
464
+ new_arrys = arrys.transpose.sort_by(&:first).transpose
465
+ new_arrys.each_index { |k| arrys[k].replace(new_arrys[k]) }
443
466
  end
444
467
 
445
468
  # Overwrite configuration options with supplied options. Used
446
469
  # by subclasses.
447
470
  def init_with config
448
471
  config.each { |key, value|
449
- self.send( key.to_s+"=", value ) if self.respond_to? key
472
+ self.send( key.to_s+"=", value ) if self.respond_to? key
450
473
  }
451
474
  end
452
475
 
@@ -468,12 +491,20 @@ module SVG
468
491
  # are not shown
469
492
  def max_y_label_width_px
470
493
  return 0 if !show_y_labels
471
- if !rotate_y_labels
472
- max_width = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.6
473
- else
474
- max_width = y_label_font_size + 3
494
+ base_width = y_label_font_size + 3
495
+ if rotate_y_labels == true
496
+ self.rotate_y_labels = 90
497
+ end
498
+ if rotate_y_labels == false
499
+ self.rotate_y_labels = 0
500
+ end
501
+ # don't change rotate_y_label, if neither true nor false
502
+ label_width = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.5
503
+ rotated_width = label_width * Math.cos( rotate_y_labels * Math::PI / 180).abs()
504
+ max_width = base_width + rotated_width
505
+ if stagger_y_labels
506
+ max_width += 5 + y_label_font_size
475
507
  end
476
- max_width += 5 + y_label_font_size if stagger_y_labels
477
508
  return max_width
478
509
  end
479
510
 
@@ -520,37 +551,74 @@ module SVG
520
551
  end
521
552
 
522
553
  # Adds pop-up point information to a graph only if the config option is set.
523
- def add_popup( x, y, label, style="" )
554
+ def add_popup( x, y, label, style="", url="" )
524
555
  if add_popups
525
556
  if( numeric?(label) )
526
557
  label = @number_format % label
527
558
  end
528
559
  txt_width = label.length * font_size * 0.6 + 10
529
560
  tx = (x+txt_width > @graph_width ? x-5 : x+5)
530
- t = @foreground.add_element( "text", {
561
+ g = Element.new( "g" )
562
+ g.attributes["id"] = g.object_id.to_s
563
+ g.attributes["visibility"] = "hidden"
564
+
565
+ # First add the mask
566
+ t = g.add_element( "text", {
531
567
  "x" => tx.to_s,
532
568
  "y" => (y - font_size).to_s,
533
- "class" => "dataPointLabel",
534
- "visibility" => "hidden",
569
+ "class" => "dataPointPopupMask"
535
570
  })
536
- t.attributes["style"] = "stroke-width: 2; fill: #000; #{style}"+
571
+ t.attributes["style"] = style +
537
572
  (x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
538
573
  t.text = label.to_s
539
- t.attributes["id"] = t.object_id.to_s
574
+
575
+ # Then add the text
576
+ t = g.add_element( "text", {
577
+ "x" => tx.to_s,
578
+ "y" => (y - font_size).to_s,
579
+ "class" => "dataPointPopup"
580
+ })
581
+ t.attributes["style"] = style +
582
+ (x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
583
+ t.text = label.to_s
584
+
585
+ @foreground.add_element( g )
540
586
 
541
587
  # add a circle to catch the mouseover
542
- @foreground.add_element( "circle", {
588
+ mouseover = Element.new( "circle" )
589
+ mouseover.add_attributes({
543
590
  "cx" => x.to_s,
544
591
  "cy" => y.to_s,
545
592
  "r" => "#{popup_radius}",
546
593
  "style" => "opacity: 0",
547
594
  "onmouseover" =>
548
- "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
595
+ "document.getElementById(#{g.object_id.to_s}).style.visibility ='visible'",
549
596
  "onmouseout" =>
550
- "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
597
+ "document.getElementById(#{g.object_id.to_s}).style.visibility = 'hidden'",
598
+ })
599
+ if !url.nil?
600
+ href = Element.new("a")
601
+ href.add_attribute("xlink:href", url)
602
+ href.add_element(mouseover)
603
+ @foreground.add_element(href)
604
+ else
605
+ @foreground.add_element(mouseover)
606
+ end
607
+ elsif !url.nil?
608
+ # add a circle to catch the mouseover
609
+ mouseover = Element.new( "circle" )
610
+ mouseover.add_attributes({
611
+ "cx" => x.to_s,
612
+ "cy" => y.to_s,
613
+ "r" => "#{popup_radius}",
614
+ "style" => "opacity: 0",
551
615
  })
616
+ href = Element.new("a")
617
+ href.add_attribute("xlink:href", url)
618
+ href.add_element(mouseover)
619
+ @foreground.add_element(href)
552
620
  end # if add_popups
553
- end # add_popup
621
+ end # def add_popup
554
622
 
555
623
  # returns the longest label from an array of labels as string
556
624
  # each object in the array must support .to_s
@@ -712,13 +780,14 @@ module SVG
712
780
  elsif x > @graph_width - textStr.length/2 * font_size
713
781
  style << "text-anchor: end;"
714
782
  end
715
- # white background for better readability
716
- @foreground.add_element( "text", {
783
+ # background for better readability
784
+ text = @foreground.add_element( "text", {
717
785
  "x" => x.to_s,
718
786
  "y" => y.to_s,
719
- "class" => "dataPointLabel",
720
- "style" => "#{style} stroke: #fff; stroke-width: 2;"
721
- }).text = textStr
787
+ "class" => "dataPointLabelBackground",
788
+ })
789
+ text.text = textStr
790
+ text.attributes["style"] = style if style.length > 0
722
791
  # actual label
723
792
  text = @foreground.add_element( "text", {
724
793
  "x" => x.to_s,
@@ -831,18 +900,20 @@ module SVG
831
900
  def draw_y_labels
832
901
  stagger = y_label_font_size + 5
833
902
  label_height = field_height
903
+ label_width = max_y_label_width_px
834
904
  count = 0
835
905
  y_offset = @graph_height + y_label_offset( label_height )
836
- y_offset += font_size/1.2 unless rotate_y_labels
906
+ y_offset += font_size/3.0
837
907
  for label in get_y_labels
838
908
  if show_y_labels
909
+ # x = 0, y = 0 is top left right next to graph area
839
910
  y = y_offset - (label_height * count)
840
- x = rotate_y_labels ? 0 : -3
911
+ x = -label_width/2.0 + y_label_font_size/2.0
841
912
 
842
913
  if stagger_y_labels and count % 2 == 1
843
914
  x -= stagger
844
915
  @graph.add_element( "path", {
845
- "d" => "M#{x} #{y} h#{stagger}",
916
+ "d" => "M0 #{y} h#{-stagger}",
846
917
  "class" => "staggerGuideLine"
847
918
  })
848
919
  end
@@ -857,18 +928,13 @@ module SVG
857
928
  textStr = @number_format % label
858
929
  end
859
930
  text.text = textStr
860
- if rotate_y_labels
861
- degrees = 90
862
- if numeric? rotate_y_labels
863
- degrees = rotate_y_labels
864
- end
865
- text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
931
+ # note text-anchor is at bottom of textfield
932
+ text.attributes["style"] = "text-anchor: middle"
933
+ degrees = rotate_y_labels
934
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) " +
866
935
  "rotate( #{degrees} #{x} #{y} ) "
867
- text.attributes["style"] = "text-anchor: middle"
868
- else
869
- text.attributes["y"] = (y - (y_label_font_size/2)).to_s
870
- text.attributes["style"] = "text-anchor: end"
871
- end
936
+ # text.attributes["y"] = (y - (y_label_font_size/2)).to_s
937
+
872
938
  end # if show_y_labels
873
939
  draw_y_guidelines( label_height, count ) if show_y_guidelines
874
940
  count += 1
@@ -1008,30 +1074,6 @@ module SVG
1008
1074
 
1009
1075
  private
1010
1076
 
1011
- def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
1012
- if lo < hi
1013
- p = partition(arrys,lo,hi)
1014
- sort_multiple(arrys, lo, p-1)
1015
- sort_multiple(arrys, p+1, hi)
1016
- end
1017
- arrys
1018
- end
1019
-
1020
- def partition( arrys, lo, hi )
1021
- p = arrys[0][lo]
1022
- l = lo
1023
- z = lo+1
1024
- while z <= hi
1025
- if arrys[0][z] < p
1026
- l += 1
1027
- arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
1028
- end
1029
- z += 1
1030
- end
1031
- arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
1032
- l
1033
- end
1034
-
1035
1077
  def style
1036
1078
  if no_css
1037
1079
  styles = parse_css
@@ -1212,7 +1254,7 @@ module SVG
1212
1254
  font-weight: normal;
1213
1255
  }
1214
1256
 
1215
- .dataPointLabel{
1257
+ .dataPointLabel, .dataPointLabelBackground, .dataPointPopup, .dataPointPopupMask{
1216
1258
  fill: #000000;
1217
1259
  text-anchor:middle;
1218
1260
  font-size: 10px;
@@ -1220,6 +1262,21 @@ module SVG
1220
1262
  font-weight: normal;
1221
1263
  }
1222
1264
 
1265
+ .dataPointLabelBackground{
1266
+ stroke: #ffffff;
1267
+ stroke-width: 2;
1268
+ }
1269
+
1270
+ .dataPointPopupMask{
1271
+ stroke: white;
1272
+ stroke-width: 7;
1273
+ }
1274
+
1275
+ .dataPointPopup{
1276
+ fill: black;
1277
+ stroke-width: 2;
1278
+ }
1279
+
1223
1280
  .staggerGuideLine{
1224
1281
  fill: none;
1225
1282
  stroke: #000000;
@@ -249,17 +249,22 @@ module SVG
249
249
  next if cum_sum[i].nil?
250
250
  c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
251
251
  if show_data_points
252
- @graph.add_element( "circle", {
253
- "cx" => c[:x].to_s,
254
- "cy" => c[:y].to_s,
255
- "r" => "2.5",
256
- "class" => "dataPoint#{line}"
257
- })
252
+ shape_selection_string = data[:description][i].to_s
253
+ if !data[:shape][i].nil?
254
+ shape_selection_string = data[:shape][i].to_s
255
+ end
256
+ DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s|
257
+ @graph.add_element( *s )
258
+ }
258
259
  end
259
260
 
260
261
  make_datapoint_text( c[:x], c[:y] - font_size/2, cum_sum[i] + minvalue)
261
262
  # number format shall not apply to popup (use .to_s conversion)
262
- add_popup(c[:x], c[:y], (cum_sum[i] + minvalue).to_s)
263
+ descr = ""
264
+ if !data[:description][i].to_s.empty?
265
+ descr = ", #{data[:description][i].to_s}"
266
+ end
267
+ add_popup(c[:x], c[:y], (cum_sum[i] + minvalue).to_s + descr, "", data[:url][i].to_s)
263
268
  end
264
269
  end
265
270
 
@@ -343,7 +343,7 @@ module SVG
343
343
 
344
344
  def get_css
345
345
  return <<EOL
346
- .dataPointLabel{
346
+ .dataPointLabel, .dataPointLabelBackground, .dataPointPopup{
347
347
  fill: #000000;
348
348
  text-anchor:middle;
349
349
  font-size: #{datapoint_font_size}px;
@@ -351,6 +351,17 @@ module SVG
351
351
  font-weight: normal;
352
352
  }
353
353
 
354
+ .dataPointLabelBackground{
355
+ stroke: #ffffff;
356
+ stroke-width: 2;
357
+ }
358
+
359
+ .dataPointPopup{
360
+ fill: #000000;
361
+ visibility: hidden;
362
+ stroke-width: 2;
363
+ }
364
+
354
365
  /* key - MUST match fill styles */
355
366
  .key1,.fill1{
356
367
  fill: #ff0000;
@@ -59,10 +59,10 @@ module SVG
59
59
  #
60
60
  # = Notes
61
61
  #
62
- # The default stylesheet handles upto 10 data sets, if you
62
+ # The default stylesheet handles upto 12 data sets, if you
63
63
  # use more you must create your own stylesheet and add the
64
64
  # additional settings for the extra data sets. You will know
65
- # if you go over 10 data sets as they will have no style and
65
+ # if you go over 12 data sets as they will have no style and
66
66
  # be in black.
67
67
  #
68
68
  # Unlike the other types of charts, data sets must contain x,y pairs:
@@ -164,10 +164,15 @@ module SVG
164
164
  # :data => data_set2,
165
165
  # :title => 'two points'
166
166
  # })
167
+ # @param conf [Hash] with keys
168
+ # :data [Array] of x,y pairs, one pair for each point
169
+ # :title [String] mandatory name of data series for legend of graph
170
+ # :description [Array<String>] (optional) if given, description for each datapoint (shown in popups)
171
+ # :shape [Array<String>] (optional) if given, DataPoint shape is chosen based on this string instead of description
172
+ # :url [Array<String>] (optional) if given, link will be added to each datapoint
167
173
  def add_data(conf)
168
- @data ||= []
169
- raise "No data provided by #{conf.inspect}" unless conf[:data] and
170
- conf[:data].kind_of? Array
174
+ @data ||= []
175
+ raise "No data provided by #{conf.inspect}" unless conf[:data].is_a?(Array)
171
176
  # support array of arrays and flatten it
172
177
  conf[:data] = conf[:data].flatten
173
178
  # check that we have pairs of values
@@ -178,20 +183,17 @@ module SVG
178
183
  # remove nil values
179
184
  conf[:data] = conf[:data].compact
180
185
 
181
- return if conf[:data].length == 0
186
+ return if conf[:data].length.zero?
182
187
 
183
- conf[:description] ||= Array.new(conf[:data].size/2)
184
- if conf[:description].size != conf[:data].size/2
185
- raise "Description for popups does not have same size as provided data: #{conf[:description].size} vs #{conf[:data].size/2}"
186
- end
188
+ add_data_init_or_check_optional_keys(conf, conf[:data].size / 2)
187
189
 
188
190
  x = []
189
191
  y = []
190
192
  conf[:data].each_index {|i|
191
193
  (i%2 == 0 ? x : y) << conf[:data][i]
192
194
  }
193
- sort( x, y, conf[:description] )
194
- conf[:data] = [x,y]
195
+ sort(x, y, conf[:description], conf[:shape], conf[:url])
196
+ conf[:data] = [x, y]
195
197
  # at the end data looks like:
196
198
  # [
197
199
  # [all x values],
@@ -218,20 +220,25 @@ module SVG
218
220
  @border_right = label_right if label_right > @border_right
219
221
  end
220
222
 
221
-
222
223
  X = 0
223
224
  Y = 1
224
225
 
225
226
  def max_x_range
227
+ # needs to be computed fresh when called, to cover the use-case:
228
+ # add_data -> burn -> add_data -> burn
229
+ # when values would be cached, the graph is not updated for second burning
226
230
  max_value = @data.collect{|x| x[:data][X][-1] }.max
227
231
  max_value = max_value > max_x_value ? max_value : max_x_value if max_x_value
228
- max_value
232
+ return max_value
229
233
  end
230
234
 
231
235
  def min_x_range
236
+ # needs to be computed fresh when called, to cover the use-case:
237
+ # add_data -> burn -> add_data -> burn
238
+ # when values would be cached, the graph is not updated for second burning
232
239
  min_value = @data.collect{|x| x[:data][X][0] }.min
233
240
  min_value = min_value < min_x_value ? min_value : min_x_value if min_x_value
234
- min_value
241
+ return min_value
235
242
  end
236
243
 
237
244
  def x_label_range
@@ -246,11 +253,11 @@ module SVG
246
253
  end
247
254
  scale_range = max_value - min_value
248
255
 
249
- scale_division = scale_x_divisions || (scale_range / 10.0)
256
+ scale_division = scale_x_divisions || (scale_range / 9.0)
250
257
  @x_offset = 0
251
258
 
252
259
  if scale_x_integers
253
- scale_division = scale_division < 1 ? 1 : scale_division.round
260
+ scale_division = scale_division < 1 ? 1 : scale_division.ceil
254
261
  @x_offset = min_value.to_f - min_value.floor
255
262
  min_value = min_value.floor
256
263
  end
@@ -273,17 +280,19 @@ module SVG
273
280
  # otherwise there is always 1 division unused
274
281
  end
275
282
 
276
-
277
283
  def max_y_range
278
284
  max_value = @data.collect{|x| x[:data][Y].max }.max
279
285
  max_value = max_value > max_y_value ? max_value : max_y_value if max_y_value
280
- max_value
286
+ return max_value
281
287
  end
282
288
 
283
289
  def min_y_range
290
+ # needs to be computed fresh when called, to cover the use-case:
291
+ # add_data -> burn -> add_data -> burn
292
+ # when values would be cached, the graph is not updated for second burning
284
293
  min_value = @data.collect{|x| x[:data][Y].min }.min
285
294
  min_value = min_value < min_y_value ? min_value : min_y_value if min_y_value
286
- min_value
295
+ return min_value
287
296
  end
288
297
 
289
298
  def y_label_range
@@ -298,11 +307,11 @@ module SVG
298
307
  end
299
308
  scale_range = max_value - min_value
300
309
 
301
- scale_division = scale_y_divisions || (scale_range / 10.0)
310
+ scale_division = scale_y_divisions || (scale_range / 9.0)
302
311
  @y_offset = 0
303
312
 
304
313
  if scale_y_integers
305
- scale_division = scale_division < 1 ? 1 : scale_division.round
314
+ scale_division = scale_division < 1 ? 1 : scale_division.ceil
306
315
  @y_offset = (min_value.to_f - min_value.floor).to_f
307
316
  min_value = min_value.floor
308
317
  end
@@ -314,7 +323,7 @@ module SVG
314
323
  min_value, max_value, @y_scale_division = y_label_range
315
324
  if max_value != min_value
316
325
  while (max_value - min_value) < @y_scale_division
317
- @y_scale_division /= 10.0
326
+ @y_scale_division /= 9.0
318
327
  end
319
328
  end
320
329
  rv = []
@@ -333,7 +342,7 @@ module SVG
333
342
  else
334
343
  dx = (max - values[-1]).to_f / (values[-1] - values[-2])
335
344
  end
336
- @graph_height.to_f / values.length
345
+ @graph_height.to_f / (values.length - 1)
337
346
  end
338
347
 
339
348
  def calc_coords(x, y)
@@ -349,9 +358,7 @@ module SVG
349
358
  line = 1
350
359
 
351
360
  x_min = min_x_range
352
- x_max = max_x_range
353
361
  y_min = min_y_range
354
- y_max = max_y_range
355
362
 
356
363
  for data in @data
357
364
  x_points = data[:data][X]
@@ -384,12 +391,16 @@ module SVG
384
391
  x_points.each_index { |idx|
385
392
  c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min)
386
393
  if show_data_points
387
- DataPoint.new(c[:x], c[:y], line).shape(data[:description][idx]).each{|s|
394
+ shape_selection_string = data[:description][idx].to_s
395
+ if !data[:shape][idx].nil?
396
+ shape_selection_string = data[:shape][idx].to_s
397
+ end
398
+ DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s|
388
399
  @graph.add_element( *s )
389
400
  }
390
401
  end
391
402
  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]))
403
+ add_popup(c[:x], c[:y], format( x_points[idx], y_points[idx], data[:description][idx].to_s), "", data[:url][idx].to_s)
393
404
  }
394
405
  end
395
406
  line += 1
@@ -401,7 +412,7 @@ module SVG
401
412
  info = []
402
413
  info << (round_popups ? x.round : @number_format % x )
403
414
  info << (round_popups ? y.round : @number_format % y )
404
- info << desc
415
+ info << desc if !desc.empty?
405
416
  "(#{info.compact.join(', ')})"
406
417
  end
407
418
 
@@ -4,20 +4,20 @@ require_relative 'Plot'
4
4
  module SVG
5
5
  module Graph
6
6
  # === For creating SVG plots of scalar temporal data
7
- #
7
+ #
8
8
  # = Synopsis
9
- #
9
+ #
10
10
  # require 'SVG/Graph/TimeSeries'
11
- #
11
+ #
12
12
  # # Data sets are x,y pairs
13
13
  # projection = ["6/17/72", 11, "1/11/72", 7, "4/13/04", 11,
14
14
  # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
15
15
  # actual = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
16
16
  # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
17
17
  # "5/1/84", 17, "10/1/80", 12]
18
- #
18
+ #
19
19
  # title = "Ice Cream Cone Consumption"
20
- #
20
+ #
21
21
  # graph = SVG::Graph::TimeSeries.new( {
22
22
  # :width => 640,
23
23
  # :height => 480,
@@ -39,31 +39,31 @@ module SVG
39
39
  # :stagger_x_labels => true,
40
40
  # :x_label_format => "%m/%d/%y",
41
41
  # })
42
- #
42
+ #
43
43
  # graph.add_data({
44
44
  # :data => projection,
45
45
  # :title => 'Projected',
46
46
  # :template => '%d/%m/%y'
47
47
  # })
48
- #
48
+ #
49
49
  # graph.add_data({
50
50
  # :data => actual,
51
51
  # :title => 'Actual',
52
52
  # :template => '%d/%m/%y'
53
53
  # })
54
- #
54
+ #
55
55
  # print graph.burn()
56
56
  #
57
57
  # = Description
58
- #
58
+ #
59
59
  # Produces a graph of temporal scalar data.
60
- #
60
+ #
61
61
  # = Examples
62
62
  #
63
63
  # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
64
- #
64
+ #
65
65
  # = Notes
66
- #
66
+ #
67
67
  # The default stylesheet handles upto 10 data sets, if you
68
68
  # use more you must create your own stylesheet and add the
69
69
  # additional settings for the extra data sets. You will know
@@ -73,18 +73,18 @@ module SVG
73
73
  # Unlike the other types of charts, data sets must contain x,y pairs:
74
74
  #
75
75
  # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
76
- # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
77
- # # ("14:20",6)
76
+ # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
77
+ # # ("14:20",6)
78
78
  #
79
- # Note that multiple data sets within the same chart can differ in length,
79
+ # Note that multiple data sets within the same chart can differ in length,
80
80
  # and that the data in the datasets needn't be in order; they will be ordered
81
81
  # by the plot along the X-axis.
82
- #
82
+ #
83
83
  # The dates must be parseable by DateTime#parse or DateTime#strptime, but otherwise can be
84
84
  # any order of magnitude (seconds within the hour, or years)
85
- #
85
+ #
86
86
  # = See also
87
- #
87
+ #
88
88
  # * SVG::Graph::Graph
89
89
  # * SVG::Graph::BarHorizontal
90
90
  # * SVG::Graph::Bar
@@ -117,9 +117,9 @@ module SVG
117
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
- # must be of the form
120
+ # must be of the form
121
121
  # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
122
- #
122
+ #
123
123
  # EG:
124
124
  #
125
125
  # graph.timescale_divisions = "2 weeks"
@@ -133,9 +133,9 @@ module SVG
133
133
  # Add data to the plot.
134
134
  #
135
135
  # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
136
- # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
137
- # # ("14:20",6)
138
- # graph.add_data(
136
+ # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
137
+ # # ("14:20",6)
138
+ # graph.add_data(
139
139
  # :data => d1,
140
140
  # :title => 'One',
141
141
  # :template => '%H:%M' #template is optional
@@ -182,10 +182,10 @@ module SVG
182
182
  def get_x_labels
183
183
  get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
184
184
  end
185
-
185
+
186
186
  private
187
187
 
188
- # Accepts date time as a string, number of seconds since the epoch, or Time
188
+ # Accepts date time as a string, number of seconds since the epoch, or Time
189
189
  # object and returns a Time object. Raises an error if not a valid date time
190
190
  # representation.
191
191
  def parse_time(time, template)
@@ -254,10 +254,10 @@ module SVG
254
254
  return rv
255
255
  end
256
256
  end
257
- min.step( max, @x_scale_division ) {|v| rv << v}
257
+ min.step( max , @x_scale_division ) {|v| rv << v}
258
258
  return rv
259
259
  end # get_x_values
260
-
260
+
261
261
  end # class TimeSeries
262
262
  end # module Graph
263
263
  end # module SVG
@@ -1,6 +1,6 @@
1
1
  module SVG
2
2
  module Graph
3
- VERSION = '2.2.0'
3
+ VERSION = '2.2.1'
4
4
 
5
5
  end
6
6
  end
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.2.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Russell
@@ -9,10 +9,10 @@ authors:
9
9
  - Liehann Loots
10
10
  - Piergiuliano Bossi
11
11
  - Manuel Widmer
12
- autorequire:
12
+ autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2019-11-26 00:00:00.000000000 Z
15
+ date: 2020-12-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.
@@ -57,7 +57,7 @@ homepage: https://github.com/lumean/svg-graph2
57
57
  licenses:
58
58
  - GPL-2.0
59
59
  metadata: {}
60
- post_install_message:
60
+ post_install_message:
61
61
  rdoc_options: []
62
62
  require_paths:
63
63
  - lib
@@ -72,8 +72,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  - !ruby/object:Gem::Version
73
73
  version: '0'
74
74
  requirements: []
75
- rubygems_version: 3.0.1
76
- signing_key:
75
+ rubygems_version: 3.1.4
76
+ signing_key:
77
77
  specification_version: 4
78
78
  summary: SVG:::Graph is a pure Ruby library for generating charts, which are a type
79
79
  of graph where the values of one axis are not scalar.