svg-graph 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/History.txt +8 -0
- data/README.md +18 -5
- data/Rakefile +16 -6
- data/lib/SVG/Graph/DataPoint.rb +28 -16
- data/lib/SVG/Graph/Graph.rb +128 -71
- data/lib/SVG/Graph/Line.rb +12 -7
- data/lib/SVG/Graph/Pie.rb +12 -1
- data/lib/SVG/Graph/Plot.rb +40 -29
- data/lib/SVG/Graph/TimeSeries.rb +27 -27
- data/lib/svggraph.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6eaa56f94856a998ecd2f0befef3a38c5529da886266cd15150cf4e9758168f
|
4
|
+
data.tar.gz: f9eb26d8c758153505d95d450e57c3fa0dfbfcdd9018d89bb6bcee3b656cebff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c77c61810a9d855bc469ac4672d9917f0bbb345b5464235c0a06e9d901aa796c8cba67c12e181200ca3865ea2ce9629d8ae39065aec831b77c707fdc46720d4
|
7
|
+
data.tar.gz: d270039fc1ea0340a515f554c4ff1f6437d0138c62ff09daafb5c1599df819d096ab04ffa23fe8862cac16b4bd6fcc71efea973e001fa97cfd1e5058e7444a82
|
data/History.txt
CHANGED
@@ -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
|
+
[](https://travis-ci.com/lumean/svg-graph2)
|
5
|
+
[](https://codeclimate.com/github/lumean/svg-graph2/maintainability)
|
6
|
+
[](https://codeclimate.com/github/lumean/svg-graph2/test_coverage)
|
7
|
+
[](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
|
-
|
122
|
+
|
123
|
+
`gem build svg-graph.gemspec`
|
124
|
+
|
118
125
|
* Install:
|
119
|
-
|
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:
|
24
|
+
# by default run all unit tests with 'rake test'
|
25
|
+
task default: [:test]
|
26
26
|
|
27
27
|
task :test do
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
data/lib/SVG/Graph/DataPoint.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Allows to customize datapoint shapes
|
2
2
|
class DataPoint
|
3
|
-
|
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,
|
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|
|
23
|
-
#
|
24
|
-
#
|
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(
|
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
|
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
|
81
|
+
proc.call(@x, @y, @line) if datapoint_description =~ regexp
|
70
82
|
}.compact
|
71
83
|
|
72
84
|
return shapes + overlays
|
data/lib/SVG/Graph/Graph.rb
CHANGED
@@ -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
|
-
|
184
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
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
|
440
|
-
# used for Schedule and Plot
|
462
|
+
# implementation of a multiple array sort used for Schedule and Plot
|
441
463
|
def sort( *arrys )
|
442
|
-
|
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
|
-
|
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
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
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
|
-
|
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" => "
|
534
|
-
"visibility" => "hidden",
|
569
|
+
"class" => "dataPointPopupMask"
|
535
570
|
})
|
536
|
-
t.attributes["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
|
-
|
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
|
-
|
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(#{
|
595
|
+
"document.getElementById(#{g.object_id.to_s}).style.visibility ='visible'",
|
549
596
|
"onmouseout" =>
|
550
|
-
"document.getElementById(#{
|
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
|
-
#
|
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" => "
|
720
|
-
|
721
|
-
|
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/
|
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 =
|
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" => "
|
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
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
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
|
-
|
868
|
-
|
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;
|
data/lib/SVG/Graph/Line.rb
CHANGED
@@ -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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
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
|
|
data/lib/SVG/Graph/Pie.rb
CHANGED
@@ -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;
|
data/lib/SVG/Graph/Plot.rb
CHANGED
@@ -59,10 +59,10 @@ module SVG
|
|
59
59
|
#
|
60
60
|
# = Notes
|
61
61
|
#
|
62
|
-
# The default stylesheet handles upto
|
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
|
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
|
-
|
169
|
-
raise "No data provided by #{conf.inspect}" unless conf[:data]
|
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
|
186
|
+
return if conf[:data].length.zero?
|
182
187
|
|
183
|
-
conf
|
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(
|
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 /
|
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.
|
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 /
|
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.
|
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 /=
|
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
|
-
|
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
|
|
data/lib/SVG/Graph/TimeSeries.rb
CHANGED
@@ -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
|
data/lib/svggraph.rb
CHANGED
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.
|
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:
|
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.
|
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.
|