svg-graph-test 0.0.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.
@@ -0,0 +1,274 @@
1
+ require 'rexml/document'
2
+ require 'json'
3
+
4
+ module SVG
5
+ module Graph
6
+
7
+ # This class provides a lightweight generator for html code indluding c3js based
8
+ # graphs specified as javascript.
9
+ class C3js
10
+
11
+ # By default, the generated html code links the javascript and css dependencies
12
+ # to the d3 and c3 libraries in the <head> element. The latest versions of d3 and c3 available
13
+ # at the time of gem release are used through cdnjs.
14
+ # Custom versions of d3 and c3 can easily be used by specifying the corresponding keys
15
+ # in the optional Hash argument.
16
+ #
17
+ # If the dependencies are http(s) urls a simple href / src link is inserted in the
18
+ # html header.
19
+ # If you want to create a fully offline capable html file, you can do this by
20
+ # downloading the (minified) versions of d3.js, c3.css, c3.js to disk and then
21
+ # point to the files instead of http links. This will then inline the complete
22
+ # script and css payload directly into the generated html page.
23
+ #
24
+ #
25
+ # @option opts [String] "inline_dependencies" if true will inline the script and css
26
+ # parts of d3 and c3 directly into html, otherwise they are referred
27
+ # as external dependencies. default: false
28
+ # @option opts [String] "d3_js" url or path to local files. default: d3.js url via cdnjs
29
+ # @option opts [String] "c3_css" url or path to local files. default: c3.css url via cdnjs
30
+ # @option opts [String] "c3_js" url or path to local files. default: c3.js url via cdnjs
31
+ # @example create a simple graph
32
+ # my_graph = SVG::Graph::C3js.new("my_funny_chart_var")
33
+ # @example create a graph with custom version of C3 and D3
34
+ # # use external dependencies
35
+ # opts = {
36
+ # "d3_js" => "https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js",
37
+ # "c3_css" => "https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.8/c3.min.css",
38
+ # "c3_js" => "https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.8/c3.min.js"
39
+ # }
40
+ # # or inline dependencies into generated html
41
+ # opts = {
42
+ # "inline_dependencies" => true,
43
+ # "d3_js" => "/path/to/local/copy/of/d3.min.js",
44
+ # "c3_css" => "/path/to/local/copy/of/c3.min.css",
45
+ # "c3_js" => "/path/to/local/copy/of/c3.min.js"
46
+ # }
47
+ # my_graph = SVG::Graph::C3js.new("my_funny_chart_var", opts)
48
+ def initialize(opts = {})
49
+ default_opts = {
50
+ "inline_dependencies" => false,
51
+ "d3_js" => "https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js",
52
+ "c3_css" => "https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.css",
53
+ "c3_js" => "https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.js"
54
+ }
55
+ @opts = default_opts.merge(opts)
56
+ if @opts["inline_dependencies"]
57
+ # we replace the values in the opts Hash by the referred file contents
58
+ ["d3_js", "c3_css", "c3_js"].each do |key|
59
+ if !File.file?(@opts[key])
60
+ raise "opts[\"#{key}\"]: No such file - #{File.expand_path(@opts[key])}"
61
+ end
62
+ @opts[key] = File.read(@opts[key])
63
+ end # ["d3_js", "c3_css", "c3_js"].each
64
+ end # if @opts["inline_dependencies"]
65
+ start_document()
66
+ end # def initialize
67
+
68
+ # Adds a javascript/json C3js chart definition into the div tag
69
+ # @param javascript [String, Hash] see example
70
+ # @param js_chart_variable_name [String] only needed if the `javascript` parameter is a Hash.
71
+ # unique variable name representing the chart in javascript scope.
72
+ # Note this is a global javascript "var" so make sure to avoid name clashes
73
+ # with other javascript us might use on the same page.
74
+ #
75
+ # @raise
76
+ # @example
77
+ # # see http://c3js.org/examples.html
78
+ # # since ruby 2.3 you can use string symbol keys:
79
+ # chart_spec = {
80
+ # # bindto is mandatory
81
+ # "bindto": "#this_is_my_awesom_graph",
82
+ # "data": {
83
+ # "columns": [
84
+ # ['data1', 30, 200, 100, 400, 150, 250],
85
+ # ['data2', 50, 20, 10, 40, 15, 25]
86
+ # ]
87
+ # }
88
+ # # otherwise simply write plain javascript into a heredoc string:
89
+ # # make sure to include the var <chartname> = c3.generate() if using heredoc
90
+ # chart_spec_string =<<-HEREDOC
91
+ # var mychart1 = c3.generate({
92
+ # // bindto is mandatory
93
+ # "bindto": "#this_is_my_awesom_graph",
94
+ # "data": {
95
+ # "columns": [
96
+ # ['data1', 30, 200, 100, 400, 150, 250],
97
+ # ['data2', 50, 20, 10, 40, 15, 25]
98
+ # ]
99
+ # });
100
+ # HEREDOC
101
+ # graph.add_chart_spec(chart_spec, "my_chart1")
102
+ # # or
103
+ # graph.add_chart_spec(chart_spec_string)
104
+ def add_chart_spec(javascript, js_chart_variable_name = "")
105
+ if javascript.kind_of?(Hash)
106
+ if js_chart_variable_name.to_s.empty? || js_chart_variable_name.to_s.match(/\s/)
107
+ raise "js_chart_variable_name ('#{js_chart_variable_name.to_s}') cannot be empty or contain spaces, " +
108
+ "a valid javascript variable name is needed."
109
+ end
110
+ chart_spec = JSON(javascript)
111
+ inline_script = "var #{js_chart_variable_name} = c3.generate(#{chart_spec});"
112
+ elsif javascript.kind_of?(String)
113
+ inline_script = javascript
114
+ if !inline_script.match(/c3\.generate/)
115
+ raise "var <chartname> = c3.generate({...}) statement is missing in javascript string"
116
+ end
117
+ else
118
+ raise "Unsupported argument type: #{javascript.class}"
119
+ end
120
+ # (.+?)" means non-greedy match up to next double quote
121
+ if m = inline_script.match(/"bindto":\s*"#(.+?)"/)
122
+ @bindto = m[1]
123
+ else
124
+ raise "Missing chart specification is missing the mandatory \"bindto\" key/value pair."
125
+ end
126
+ add_div_element_for_graph()
127
+ add_javascript() {inline_script}
128
+ end # def add_chart_spec
129
+
130
+ # Appends a <script> element to the <div> element, this can be used to add additional animations
131
+ # but any script can also directly be part of the js_chart_specification in the #add_chart_spec
132
+ # method when you use a HEREDOC string as input.
133
+ # @param attrs [Hash] attributes for the <script> element. The following attribute
134
+ # is added by default: type="text/javascript"
135
+ # @yieldreturn [String] the actual javascript code to be added to the <script> element
136
+ # @return [REXML::Element] the Element which was just added
137
+ def add_javascript(attrs={}, &block)
138
+ default_attrs = {"type" => "text/javascript"}
139
+ attrs = default_attrs.merge(attrs)
140
+ temp = REXML::Element.new("script")
141
+ temp.add_attributes(attrs)
142
+ @svg.add_element(temp)
143
+ raise "Block argument is mandatory" unless block_given?
144
+ script_content = block.call()
145
+ cdata(script_content, temp)
146
+ end # def add_javascript
147
+
148
+
149
+ # @return [String] the complete html file
150
+ def burn
151
+ f = REXML::Formatters::Pretty.new(0)
152
+ out = ''
153
+ f.write(@doc, out)
154
+ out
155
+ end # def burn
156
+
157
+ # Burns the graph but returns only the <div> node as String without the
158
+ # Doctype and XML / HTML Declaration. This allows easy integration into
159
+ # existing xml/html documents. The Javascript to create the C3js graph
160
+ # is inlined into the div tag.
161
+ #
162
+ # You have to take care to refer the proper C3 and D3 dependencies in your
163
+ # html page.
164
+ #
165
+ # @return [String] the div element into which the graph will be rendered
166
+ # by C3.js
167
+ def burn_svg_only
168
+ # initialize all instance variables by burning the graph
169
+ burn
170
+ f = REXML::Formatters::Pretty.new(0)
171
+ f.compact = true
172
+ out = ''
173
+ f.write(@svg, out)
174
+ return out
175
+ end # def burn_svg_only
176
+
177
+ private
178
+
179
+ # Appends a <style> element to the <div> element, this can be used to add additional animations
180
+ # but any script can also directly be part of the js_chart_specification in the #add_chart_spec
181
+ # method when you use a HEREDOC string as input.
182
+ # @yieldreturn [String] the actual javascript code to be added to the <script> element
183
+ # @return [REXML::Element] the Element which was just added
184
+ def add_css_to_head(&block)
185
+ raise "Block argument is mandatory" unless block_given?
186
+ css_content_or_url = block.call()
187
+ if @opts["inline_dependencies"]
188
+ # for inline css use "style"
189
+ temp = REXML::Element.new("style")
190
+ attrs = {
191
+ "type" => "text/css"
192
+ }
193
+ cdata(css_content_or_url, temp)
194
+ else
195
+ # for external css use "link"
196
+ temp = REXML::Element.new("link")
197
+ attrs = {
198
+ "href" => @opts["c3_css"],
199
+ "rel" => "stylesheet"
200
+ }
201
+ end
202
+ temp.add_attributes(attrs)
203
+ @head.add_element(temp)
204
+ end # def add_css_to_head
205
+
206
+ # Appends a <script> element to the <head> element, this can be used to add
207
+ # the dependencies/libraries.
208
+ # @yieldreturn [String] the actual javascript code to be added to the <script> element
209
+ # @return [REXML::Element] the Element which was just added
210
+ def add_js_to_head(&block)
211
+ raise "Block argument is mandatory" unless block_given?
212
+ script_content_or_url = block.call()
213
+ attrs = {"type" => "text/javascript"}
214
+ temp = REXML::Element.new("script")
215
+ if @opts["inline_dependencies"]
216
+ cdata(script_content_or_url, temp)
217
+ else
218
+ attrs["src"] = script_content_or_url
219
+ # note: self-closing xml script tags are not allowed in html. Only for xhtml this is ok.
220
+ # Thus add a space textnode to enforce closing tags.
221
+ temp.add_text(" ")
222
+ end
223
+ temp.add_attributes(attrs)
224
+ @head.add_element(temp)
225
+ end # def add_js_to_head
226
+
227
+ def start_document
228
+ # Base document
229
+ @doc = REXML::Document.new
230
+ @doc << REXML::XMLDecl.new("1.0", "UTF-8")
231
+ @doc << REXML::DocType.new("html")
232
+ # attribute xmlns is needed, otherwise the browser will only display raw xml
233
+ # instead of rendering the page
234
+ @html = @doc.add_element("html", {"xmlns" => 'http://www.w3.org/1999/xhtml'})
235
+ @html << REXML::Comment.new( " "+"\\"*66 )
236
+ @html << REXML::Comment.new( " Created with SVG::Graph - https://github.com/lumean/svg-graph2" )
237
+ @head = @html.add_element("head")
238
+ @body = @html.add_element("body")
239
+ @head.add_element("meta", {"charset" => "utf-8"})
240
+ add_js_to_head() {@opts["d3_js"]}
241
+ add_css_to_head() {@opts["c3_css"]}
242
+ add_js_to_head() {@opts["c3_js"]}
243
+ end # def start_svg
244
+
245
+ # @param attrs [Hash] html attributes for the <div> tag to which svg graph
246
+ # is bound to by C3js. The "id" attribute
247
+ # is filled automatically by this method. default: an empty hash {}
248
+ def add_div_element_for_graph(attrs={})
249
+ if @bindto.to_s.empty?
250
+ raise "#add_chart_spec needs to be called before the svg can be added"
251
+ end
252
+ attrs["id"] = @bindto
253
+ @svg = @body.add_element("div", attrs)
254
+ end
255
+
256
+ # Surrounds CData tag with c-style comments to remain compatible with normal html.
257
+ # This can be used to inline arbitrary javascript code and is compatible with many browsers.
258
+ # Example /*<![CDATA[*/\n ...content ... \n/*]]>*/
259
+ # @param str [String] the string to be enclosed in cdata
260
+ # @param parent_element [REXML::Element] the element to which cdata should be added
261
+ # @return [REXML::Element] parent_element
262
+ def cdata(str, parent_element)
263
+ # somehow there is a problem with CDATA, any text added after will automatically go into the CDATA
264
+ # so we have do add a dummy node after the CDATA and then add the text.
265
+ parent_element.add_text("/*")
266
+ parent_element.add(REXML::CData.new("*/\n"+str+"\n/*"))
267
+ parent_element.add(REXML::Comment.new("dummy comment to make c-style comments for cdata work"))
268
+ parent_element.add_text("*/")
269
+ end # def cdata
270
+
271
+ end # class C3js
272
+
273
+ end # module Graph
274
+ end # module SVG
@@ -0,0 +1,86 @@
1
+ # Allows to customize datapoint shapes
2
+ class DataPoint
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"
6
+ DEFAULT_SHAPE = lambda{|x,y,line| ["circle", {
7
+ "cx" => x,
8
+ "cy" => y,
9
+ "r" => "2.5",
10
+ "class" => "dataPoint#{line}"
11
+ }]
12
+ } unless defined? DEFAULT_SHAPE
13
+ CRITERIA = [] unless defined? CRITERIA
14
+
15
+ # matchers are class scope. Once configured, each DataPoint instance will have
16
+ # access to the same matchers
17
+ # @param matchers [Array] multiple arrays of the following form 2 or 3 elements:
18
+ # [ regex ,
19
+ # lambda taking three arguments (x,y, line_number for css)
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)
24
+ # ]
25
+ # @example
26
+ # DataPoint.configure_shape_criteria(
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
+ # ]
34
+ # }]
35
+ # )
36
+ def DataPoint.configure_shape_criteria(*matchers)
37
+ CRITERIA.push(*matchers)
38
+ end
39
+
40
+ #
41
+ def DataPoint.reset_shape_criteria
42
+ CRITERIA.clear
43
+ end
44
+
45
+ # creates a new DataPoint
46
+ # @param x [Numeric] x coordinates of the point
47
+ # @param y [Numeric] y coordinates of the point
48
+ # @param line [Fixnum] line index of the current dataset (e.g. when multiple times Graph.add_data()), can be used to reference to the correct css class
49
+ def initialize(x, y, line)
50
+ @x = x
51
+ @y = y
52
+ @line = line
53
+ end
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
59
+ # @return [Array<Array>] see example
60
+ # @example Return value
61
+ # # two dimensional array, the splatted (*) inner array can be used as argument to REXML::add_element
62
+ # [["svgtag", {"points" => "", "class"=> "dataPoint#{line}" } ], ["svgtag", {"points"=>"", "class"=> ""}], ...]
63
+ # @exmple Usage
64
+ # dp = DataPoint.new(x, y, line).shape(data[:description])
65
+ # # for each svg we insert it to the graph
66
+ # dp.each {|s| @graph.add_element( *s )}
67
+ #
68
+ def shape(datapoint_description=nil)
69
+ # select all criteria with size 2, and collect rendered lambdas in an array
70
+ shapes = CRITERIA.select {|criteria|
71
+ criteria.size == 2
72
+ }.collect {|regexp, proc|
73
+ proc.call(@x, @y, @line) if datapoint_description =~ regexp
74
+ }.compact
75
+ # if above did not render anything use the defalt shape
76
+ shapes = [DEFAULT_SHAPE.call(@x, @y, @line)] if shapes.empty?
77
+
78
+ overlays = CRITERIA.select { |criteria|
79
+ criteria.last == OVERLAY
80
+ }.collect { |regexp, proc|
81
+ proc.call(@x, @y, @line) if datapoint_description =~ regexp
82
+ }.compact
83
+
84
+ return shapes + overlays
85
+ end
86
+ end
@@ -0,0 +1,198 @@
1
+ require 'rexml/document'
2
+ require_relative 'Graph'
3
+ require_relative 'BarBase'
4
+
5
+ module SVG
6
+ module Graph
7
+ # === Create presentation quality SVG bar graphs easily
8
+ #
9
+ # = Synopsis
10
+ #
11
+ # require 'SVG/Graph/ErrBar'
12
+ #
13
+ # fields = %w(Jan Feb);
14
+ # myarr1_mean = 10
15
+ # myarr1_confidence = 1
16
+ #
17
+ # myarr2_mean = 20
18
+ # myarr2_confidence = 2
19
+ #
20
+ # data= [myarr1_mean, myarr2_mean]
21
+ #
22
+ # err_mesure = [myarr1_confidence, myarr2_confidence]
23
+ #
24
+ # graph = SVG::Graph::ErrBar.new(
25
+ # :height => 500,
26
+ # :width => 600,
27
+ # :fields => fields,
28
+ # :errorBars => err_mesure,
29
+ # :scale_integers => true,
30
+ # )
31
+ #
32
+ # graph.add_data(
33
+ # :data => data,
34
+ # :title => 'Sales 2002'
35
+ # )
36
+ #
37
+ # print "Content-type: image/svg+xml\r\n\r\n"
38
+ # print graph.burn
39
+ #
40
+ # = Description
41
+ #
42
+ # This object aims to allow you to easily create high quality
43
+ # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
44
+ # style sheet or supply your own. Either way there are many options which
45
+ # can be configured to give you control over how the graph is generated -
46
+ # with or without a key, data elements at each point, title, subtitle etc.
47
+ #
48
+ # = Notes
49
+ #
50
+ # The default stylesheet handles upto 12 data sets, if you
51
+ # use more you must create your own stylesheet and add the
52
+ # additional settings for the extra data sets. You will know
53
+ # if you go over 12 data sets as they will have no style and
54
+ # be in black.
55
+ #
56
+ # = Examples
57
+ #
58
+ # * http://germane-software.com/repositories/public/SVG/test/test.rb
59
+ #
60
+ # = See also
61
+ #
62
+ # * SVG::Graph::Graph
63
+ # * SVG::Graph::BarHorizontal
64
+ # * SVG::Graph::Line
65
+ # * SVG::Graph::Pie
66
+ # * SVG::Graph::Plot
67
+ # * SVG::Graph::TimeSeries
68
+ class ErrBar < BarBase
69
+ include REXML
70
+
71
+ def initialize config
72
+ raise "fields was not supplied or is empty" unless config[:errorBars] &&
73
+ config[:errorBars].kind_of?(Array) &&
74
+ config[:errorBars].length > 0
75
+ super
76
+ end
77
+ # Array of confidence values for each item in :fields. A range from
78
+ # value[i]-errorBars[i] to value[i]+errorBars[i] is drawn into the graph.
79
+ attr_accessor :errorBars
80
+
81
+ protected
82
+
83
+ def get_x_labels
84
+ @config[:fields]
85
+ end
86
+
87
+ def get_y_labels
88
+ maxvalue = max_value
89
+ minvalue = min_value
90
+ range = maxvalue - minvalue
91
+ # add some padding on top of the graph
92
+ if range == 0
93
+ maxvalue += 10
94
+ else
95
+ maxvalue += range / 20.0
96
+ end
97
+ scale_range = maxvalue - minvalue
98
+
99
+ @y_scale_division = scale_divisions || (scale_range / 10.0)
100
+
101
+ if scale_integers
102
+ @y_scale_division = @y_scale_division < 1 ? 1 : @y_scale_division.round
103
+ end
104
+
105
+ rv = []
106
+ maxvalue = maxvalue%@y_scale_division == 0 ?
107
+ maxvalue : maxvalue + @y_scale_division
108
+ minvalue.step( maxvalue, @y_scale_division ) {|v| rv << v}
109
+ return rv
110
+ end
111
+
112
+ def x_label_offset( width )
113
+ width / 2.0
114
+ end
115
+
116
+ def draw_data
117
+ minvalue = min_value
118
+ fieldwidth = field_width
119
+
120
+ unit_size = field_height
121
+ bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
122
+
123
+ bar_width = fieldwidth - (bargap *2)
124
+ bar_width /= @data.length if stack == :side
125
+
126
+ bottom = @graph_height
127
+
128
+ field_count = 0
129
+ @config[:fields].each_index { |i|
130
+ dataset_count = 0
131
+ for dataset in @data
132
+
133
+ # cases (assume 0 = +ve):
134
+ # value min length
135
+ # +ve +ve value - min
136
+ # +ve -ve value - 0
137
+ # -ve -ve value.abs - 0
138
+
139
+ value = dataset[:data][i].to_f/@y_scale_division
140
+
141
+ left = (fieldwidth * field_count)
142
+ left += bargap
143
+
144
+
145
+ length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
146
+ # top is 0 if value is negative
147
+ top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
148
+ left += bar_width * dataset_count if stack == :side
149
+
150
+ @graph.add_element( "rect", {
151
+ "x" => left.to_s,
152
+ "y" => top.to_s,
153
+ "width" => bar_width.to_s,
154
+ "height" => length.to_s,
155
+ "class" => "fill#{dataset_count+1}"
156
+ })
157
+
158
+ threshold = @config[:errorBars][i].to_f/@y_scale_division * unit_size
159
+ middlePointErr = left+bar_width/2
160
+ upperErr = top+threshold
161
+ bottomErr = top-threshold
162
+ withthErr = bar_width/4
163
+
164
+ @graph.add_element( "line", {
165
+ "x1" => middlePointErr.to_s,
166
+ "y1" => upperErr.to_s,
167
+ "x2" => middlePointErr.to_s,
168
+ "y2" => bottomErr.to_s,
169
+ "style" => "stroke:rgb(0,0,0);stroke-width:1"
170
+ })
171
+
172
+ @graph.add_element( "line", {
173
+ "x1" => (middlePointErr-withthErr).to_s,
174
+ "y1" => upperErr.to_s,
175
+ "x2" => (middlePointErr+withthErr).to_s,
176
+ "y2" => upperErr.to_s,
177
+ "style" => "stroke:rgb(0,0,0);stroke-width:1"
178
+ })
179
+ @graph.add_element( "line", {
180
+ "x1" => (middlePointErr-withthErr).to_s,
181
+ "y1" => bottomErr.to_s,
182
+ "x2" => (middlePointErr+withthErr).to_s,
183
+ "y2" => bottomErr.to_s,
184
+ "style" => "stroke:rgb(0,0,0);stroke-width:1"
185
+ })
186
+
187
+ make_datapoint_text(left + bar_width/2.0, top - 6, dataset[:data][i])
188
+ # number format shall not apply to popup (use .to_s conversion)
189
+ add_popup(left + bar_width/2.0, top , dataset[:data][i].to_s)
190
+ dataset_count += 1
191
+ end
192
+ field_count += 1
193
+ } # config[:fields].each_index
194
+ end # draw_data
195
+
196
+ end # ErrBar
197
+ end
198
+ end