svg-graph 2.2.0 → 2.2.1

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
  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.