svg-graph-test 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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