svg-graph 2.2.0 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +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
|
+
[![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
|
-
|
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.
|