svg-graph 2.2.0 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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