svg-graph 2.2.0 → 2.2.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
  SHA256:
3
- metadata.gz: c448a6423accd8618fa44587eeca0dc4f7b21ea99508c21dd95e5329cef59a5c
4
- data.tar.gz: 4775e687abb3ebe350754e3287b925ab6b5d5624158664aacecfffc6cbcff680
3
+ metadata.gz: 9d2a4756bdc712df308bb715f9076bac952fbf64dd8d8b2900d9c424b30026b7
4
+ data.tar.gz: 5c3b12638b3eb65936f32a84e50f222113b2c7c4c0e6f69bbbd9c594c99bd6eb
5
5
  SHA512:
6
- metadata.gz: 3ba65a950336a3718b7c0fd01ddcd55261911ac00542518914165e22388bdbafd834ed928980af8aa4ad75523ad8572110fb82ef5b933897243b2153d52aed38
7
- data.tar.gz: 19166679a0a198b667c240162aeb2438f77662130a03cbf430b289804b286b0fcd0c7e743b245eaebe2e982110946482ec6da94a02e29cc15d633808b74d952d
6
+ metadata.gz: fd35db671d05e9f8c2559508a4ab7bf7bfd7d4582f6433fe4b93c2cd4f182c0692100f806cf3d72704ff4ec74a267ec2b319100df7704972c2ad46530ddc1978
7
+ data.tar.gz: 5216c72900a99522c7f49ac48166f85167bfecaf087c2d2e3891abfeaad44adbe378e1230cc87569d7e99ce9f1f4bd318de708eeff615218c23449c48f345dfd
data/History.txt CHANGED
@@ -2,6 +2,21 @@ 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.3 /
6
+
7
+ === 2.2.2 / 2023-04-30
8
+ * fix line9 color typo in Line.rb [thanks, akostadinov #42]
9
+ * anchor right y labels [thanks, akostadinov #44]
10
+ * start line from first point, not 0 0 [thanks, akostadinov #43]
11
+
12
+ === 2.2.1 / 2020-12-25
13
+ * Remove inline styling for data point labels and popups [thanks marnen, PR #23]
14
+ * fix #29 text background not aligned close to axis due to missing anchors
15
+ * add & document :shape and :url hash keys for `add_data` [thanks t12nslookup, PR #32]
16
+ * add custom datapoint support to Line graphs
17
+ * Add white text behind popup text so that the black text is more readable [thanks t12nslookup, PR #30]
18
+ * fix #19 rotated lables should no longer be cropped or overlapping
19
+
5
20
  === 2.2.0 / 2019-11-26
6
21
  * on top of 2.2.0.beta adds the following
7
22
  * 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,23 @@ 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
+ # instead of calculating the middle anchor position, simply use
912
+ # static offset and anchor end to right-align the labels. See line :936 below.
913
+ #x = -label_width/2.0 + y_label_font_size/2.0
914
+ x = 3
841
915
 
842
916
  if stagger_y_labels and count % 2 == 1
843
917
  x -= stagger
844
918
  @graph.add_element( "path", {
845
- "d" => "M#{x} #{y} h#{stagger}",
919
+ "d" => "M0 #{y} h#{-stagger}",
846
920
  "class" => "staggerGuideLine"
847
921
  })
848
922
  end
@@ -857,18 +931,14 @@ module SVG
857
931
  textStr = @number_format % label
858
932
  end
859
933
  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 ) "+
934
+ # note text-anchor is at bottom of textfield
935
+ #text.attributes["style"] = "text-anchor: middle"
936
+ text.attributes["style"] = "text-anchor: end"
937
+ degrees = rotate_y_labels
938
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) " +
866
939
  "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
940
+ # text.attributes["y"] = (y - (y_label_font_size/2)).to_s
941
+
872
942
  end # if show_y_labels
873
943
  draw_y_guidelines( label_height, count ) if show_y_guidelines
874
944
  count += 1
@@ -1008,30 +1078,6 @@ module SVG
1008
1078
 
1009
1079
  private
1010
1080
 
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
1081
  def style
1036
1082
  if no_css
1037
1083
  styles = parse_css
@@ -1212,7 +1258,7 @@ module SVG
1212
1258
  font-weight: normal;
1213
1259
  }
1214
1260
 
1215
- .dataPointLabel{
1261
+ .dataPointLabel, .dataPointLabelBackground, .dataPointPopup, .dataPointPopupMask{
1216
1262
  fill: #000000;
1217
1263
  text-anchor:middle;
1218
1264
  font-size: 10px;
@@ -1220,6 +1266,21 @@ module SVG
1220
1266
  font-weight: normal;
1221
1267
  }
1222
1268
 
1269
+ .dataPointLabelBackground{
1270
+ stroke: #ffffff;
1271
+ stroke-width: 2;
1272
+ }
1273
+
1274
+ .dataPointPopupMask{
1275
+ stroke: white;
1276
+ stroke-width: 7;
1277
+ }
1278
+
1279
+ .dataPointPopup{
1280
+ fill: black;
1281
+ stroke-width: 2;
1282
+ }
1283
+
1223
1284
  .staggerGuideLine{
1224
1285
  fill: none;
1225
1286
  stroke: #000000;
@@ -42,7 +42,7 @@ module SVG
42
42
  #
43
43
  # = Examples
44
44
  #
45
- # http://www.germane-software/repositories/public/SVG/test/single.rb
45
+ # https://github.com/lumean/svg-graph2/blob/master/examples/line.rb
46
46
  #
47
47
  # = Notes
48
48
  # Only number of fileds datapoints will be drawn, additional data values
@@ -238,8 +238,9 @@ module SVG
238
238
  })
239
239
  end
240
240
 
241
+ matcher = /^(\S+ \S+) (.*)/.match lpath
241
242
  @graph.add_element("path", {
242
- "d" => "M0 #@graph_height L" + lpath,
243
+ "d" => "M#{matcher[1]} L#{matcher[2]}",
243
244
  "class" => "line#{line}"
244
245
  })
245
246
 
@@ -249,17 +250,22 @@ module SVG
249
250
  next if cum_sum[i].nil?
250
251
  c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
251
252
  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
- })
253
+ shape_selection_string = data[:description][i].to_s
254
+ if !data[:shape][i].nil?
255
+ shape_selection_string = data[:shape][i].to_s
256
+ end
257
+ DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s|
258
+ @graph.add_element( *s )
259
+ }
258
260
  end
259
261
 
260
262
  make_datapoint_text( c[:x], c[:y] - font_size/2, cum_sum[i] + minvalue)
261
263
  # number format shall not apply to popup (use .to_s conversion)
262
- add_popup(c[:x], c[:y], (cum_sum[i] + minvalue).to_s)
264
+ descr = ""
265
+ if !data[:description][i].to_s.empty?
266
+ descr = ", #{data[:description][i].to_s}"
267
+ end
268
+ add_popup(c[:x], c[:y], (cum_sum[i] + minvalue).to_s + descr, "", data[:url][i].to_s)
263
269
  end
264
270
  end
265
271
 
@@ -314,7 +320,7 @@ module SVG
314
320
  }
315
321
  .line9{
316
322
  fill: none;
317
- stroke: #ccc6666;
323
+ stroke: #cc6666;
318
324
  stroke-width: 1px;
319
325
  }
320
326
  .line10{
data/lib/SVG/Graph/Pie.rb CHANGED
@@ -36,7 +36,7 @@ module SVG
36
36
  #
37
37
  # = Examples
38
38
  #
39
- # http://www.germane-software/repositories/public/SVG/test/single.rb
39
+ # https://github.com/lumean/svg-graph2/blob/master/examples/pie.rb
40
40
  #
41
41
  # == See also
42
42
  #
@@ -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;
@@ -55,14 +55,14 @@ module SVG
55
55
  #
56
56
  # = Examples
57
57
  #
58
- # http://www.germane-software/repositories/public/SVG/test/plot.rb
58
+ # https://github.com/lumean/svg-graph2/blob/master/examples/plot.rb
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
@@ -175,23 +180,23 @@ module SVG
175
180
  "The data provided contained an odd set of "+
176
181
  "data points" unless conf[:data].length % 2 == 0
177
182
 
183
+ # clear the min/max x/y range caches
184
+ clear_cache
185
+
178
186
  # remove nil values
179
187
  conf[:data] = conf[:data].compact
180
188
 
181
- return if conf[:data].length == 0
189
+ return if conf[:data].length.zero?
182
190
 
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
191
+ add_data_init_or_check_optional_keys(conf, conf[:data].size / 2)
187
192
 
188
193
  x = []
189
194
  y = []
190
195
  conf[:data].each_index {|i|
191
196
  (i%2 == 0 ? x : y) << conf[:data][i]
192
197
  }
193
- sort( x, y, conf[:description] )
194
- conf[:data] = [x,y]
198
+ sort(x, y, conf[:description], conf[:shape], conf[:url])
199
+ conf[:data] = [x, y]
195
200
  # at the end data looks like:
196
201
  # [
197
202
  # [all x values],
@@ -218,22 +223,40 @@ module SVG
218
223
  @border_right = label_right if label_right > @border_right
219
224
  end
220
225
 
221
-
222
226
  X = 0
223
227
  Y = 1
224
228
 
229
+ # procedure to clear all the cached variables used in working out the
230
+ # max and min ranges for the chart
231
+ def clear_cache
232
+ @max_x_cache = @min_x_cache = @max_y_cache = @min_y_cache = nil
233
+ end
234
+
225
235
  def max_x_range
236
+ return @max_x_cache unless @max_x_cache.nil?
237
+
238
+ # needs to be computed fresh when called, to cover the use-case:
239
+ # add_data -> burn -> add_data -> burn
240
+ # when values would be cached, the graph is not updated for second burning
226
241
  max_value = @data.collect{|x| x[:data][X][-1] }.max
227
242
  max_value = max_value > max_x_value ? max_value : max_x_value if max_x_value
228
- max_value
243
+ @max_x_cache = max_value
244
+ @max_x_cache
229
245
  end
230
246
 
231
247
  def min_x_range
248
+ return @min_x_cache unless @min_x_cache.nil?
249
+
250
+ # needs to be computed fresh when called, to cover the use-case:
251
+ # add_data -> burn -> add_data -> burn
252
+ # when values would be cached, the graph is not updated for second burning
232
253
  min_value = @data.collect{|x| x[:data][X][0] }.min
233
254
  min_value = min_value < min_x_value ? min_value : min_x_value if min_x_value
234
- min_value
255
+ @min_x_cache = min_value
256
+ @min_x_cache
235
257
  end
236
258
 
259
+ # calculate the min and max x value as well as the scale division used for the x-axis
237
260
  def x_label_range
238
261
  max_value = max_x_range
239
262
  min_value = min_x_range
@@ -246,11 +269,12 @@ module SVG
246
269
  end
247
270
  scale_range = max_value - min_value
248
271
 
249
- scale_division = scale_x_divisions || (scale_range / 10.0)
272
+ # either use the given step size or by default do 9 divisions.
273
+ scale_division = scale_x_divisions || (scale_range / 9.0)
250
274
  @x_offset = 0
251
275
 
252
276
  if scale_x_integers
253
- scale_division = scale_division < 1 ? 1 : scale_division.round
277
+ scale_division = scale_division < 1 ? 1 : scale_division.ceil
254
278
  @x_offset = min_value.to_f - min_value.floor
255
279
  min_value = min_value.floor
256
280
  end
@@ -258,10 +282,13 @@ module SVG
258
282
  [min_value, max_value, scale_division]
259
283
  end
260
284
 
285
+ # get array of values for the x axis divisions, assuming left-most value starts
286
+ # exactly where the graph starts.
261
287
  def get_x_values
262
288
  min_value, max_value, @x_scale_division = x_label_range
289
+ x_times = ((max_value-min_value)/@x_scale_division).round + 1
263
290
  rv = []
264
- min_value.step( max_value + @x_scale_division , @x_scale_division ) {|v| rv << v}
291
+ x_times.times{|v| rv << (min_value + (v * @x_scale_division))}
265
292
  return rv
266
293
  end
267
294
  alias :get_x_labels :get_x_values
@@ -273,17 +300,25 @@ module SVG
273
300
  # otherwise there is always 1 division unused
274
301
  end
275
302
 
276
-
277
303
  def max_y_range
304
+ return @max_y_cache unless @max_y_cache.nil?
305
+
278
306
  max_value = @data.collect{|x| x[:data][Y].max }.max
279
307
  max_value = max_value > max_y_value ? max_value : max_y_value if max_y_value
280
- max_value
308
+ @max_y_cache = max_value
309
+ @max_y_cache
281
310
  end
282
311
 
283
312
  def min_y_range
313
+ return @min_y_cache unless @min_y_cache.nil?
314
+
315
+ # needs to be computed fresh when called, to cover the use-case:
316
+ # add_data -> burn -> add_data -> burn
317
+ # when values would be cached, the graph is not updated for second burning
284
318
  min_value = @data.collect{|x| x[:data][Y].min }.min
285
319
  min_value = min_value < min_y_value ? min_value : min_y_value if min_y_value
286
- min_value
320
+ @min_y_cache = min_value
321
+ @min_y_cache
287
322
  end
288
323
 
289
324
  def y_label_range
@@ -298,11 +333,11 @@ module SVG
298
333
  end
299
334
  scale_range = max_value - min_value
300
335
 
301
- scale_division = scale_y_divisions || (scale_range / 10.0)
336
+ scale_division = scale_y_divisions || (scale_range / 9.0)
302
337
  @y_offset = 0
303
338
 
304
339
  if scale_y_integers
305
- scale_division = scale_division < 1 ? 1 : scale_division.round
340
+ scale_division = scale_division < 1 ? 1 : scale_division.ceil
306
341
  @y_offset = (min_value.to_f - min_value.floor).to_f
307
342
  min_value = min_value.floor
308
343
  end
@@ -314,7 +349,7 @@ module SVG
314
349
  min_value, max_value, @y_scale_division = y_label_range
315
350
  if max_value != min_value
316
351
  while (max_value - min_value) < @y_scale_division
317
- @y_scale_division /= 10.0
352
+ @y_scale_division /= 9.0
318
353
  end
319
354
  end
320
355
  rv = []
@@ -333,9 +368,10 @@ module SVG
333
368
  else
334
369
  dx = (max - values[-1]).to_f / (values[-1] - values[-2])
335
370
  end
336
- @graph_height.to_f / values.length
371
+ @graph_height.to_f / (values.length - 1)
337
372
  end
338
373
 
374
+ # calculates the x,y coordinates of a datapoint in the plot area
339
375
  def calc_coords(x, y)
340
376
  coords = {:x => 0, :y => 0}
341
377
  # scale the coordinates, use float division / multiplication
@@ -349,9 +385,7 @@ module SVG
349
385
  line = 1
350
386
 
351
387
  x_min = min_x_range
352
- x_max = max_x_range
353
388
  y_min = min_y_range
354
- y_max = max_y_range
355
389
 
356
390
  for data in @data
357
391
  x_points = data[:data][X]
@@ -384,12 +418,16 @@ module SVG
384
418
  x_points.each_index { |idx|
385
419
  c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min)
386
420
  if show_data_points
387
- DataPoint.new(c[:x], c[:y], line).shape(data[:description][idx]).each{|s|
421
+ shape_selection_string = data[:description][idx].to_s
422
+ if !data[:shape][idx].nil?
423
+ shape_selection_string = data[:shape][idx].to_s
424
+ end
425
+ DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s|
388
426
  @graph.add_element( *s )
389
427
  }
390
428
  end
391
429
  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]))
430
+ add_popup(c[:x], c[:y], format( x_points[idx], y_points[idx], data[:description][idx].to_s), "", data[:url][idx].to_s)
393
431
  }
394
432
  end
395
433
  line += 1
@@ -401,7 +439,7 @@ module SVG
401
439
  info = []
402
440
  info << (round_popups ? x.round : @number_format % x )
403
441
  info << (round_popups ? y.round : @number_format % y )
404
- info << desc
442
+ info << desc if !desc.empty?
405
443
  "(#{info.compact.join(', ')})"
406
444
  end
407
445
 
@@ -47,7 +47,7 @@ module SVG
47
47
  #
48
48
  # = Examples
49
49
  #
50
- # http://www.germane-software/repositories/public/SVG/test/schedule.rb
50
+ # https://github.com/lumean/svg-graph2/blob/master/examples/schedule.rb
51
51
  #
52
52
  # = Notes
53
53
  #
@@ -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
- # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
64
- #
63
+ # https://github.com/lumean/svg-graph2/blob/master/examples/timeseries.rb
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)
@@ -249,15 +249,15 @@ module SVG
249
249
  step = amount
250
250
  end
251
251
  # only do this if division_units is not year or month. Those are done already above in the cases.
252
- min.step( max, step ) {|v| rv << v} if step
252
+ min.step( max + (step/10), step ) {|v| rv << v} if step
253
253
  @x_scale_division = step if step
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/10), @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
data/lib/svggraph.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module SVG
2
2
  module Graph
3
- VERSION = '2.2.0'
3
+ VERSION = '2.2.2'
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.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: 2019-11-26 00:00:00.000000000 Z
15
+ date: 2023-04-30 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.
@@ -72,7 +72,7 @@ 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
75
+ rubygems_version: 3.2.3
76
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