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,1338 @@
1
+ begin
2
+ require 'zlib'
3
+ rescue
4
+ # No Zlib.
5
+ end
6
+
7
+ require 'rexml/document'
8
+
9
+ module SVG
10
+ module Graph
11
+
12
+ # === Base object for generating SVG Graphs
13
+ #
14
+ # == Synopsis
15
+ #
16
+ # This class is only used as a superclass of specialized charts. Do not
17
+ # attempt to use this class directly, unless creating a new chart type.
18
+ #
19
+ # For examples of how to subclass this class, see the existing specific
20
+ # subclasses, such as SVG::Graph::Pie.
21
+ #
22
+ # == Examples
23
+ #
24
+ # For examples of how to use this package, see either the test files, or
25
+ # the documentation for the specific class you want to use.
26
+ #
27
+ # * file:test/plot.rb
28
+ # * file:test/single.rb
29
+ # * file:test/test.rb
30
+ # * file:test/timeseries.rb
31
+ #
32
+ # == Description
33
+ #
34
+ # This package should be used as a base for creating SVG graphs.
35
+ #
36
+ # == Acknowledgements
37
+ #
38
+ # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
39
+ # port is based on.
40
+ #
41
+ # Stephen Morgan for creating the TT template and SVG.
42
+ #
43
+ # == See
44
+ #
45
+ # * SVG::Graph::BarHorizontal
46
+ # * SVG::Graph::Bar
47
+ # * SVG::Graph::Line
48
+ # * SVG::Graph::Pie
49
+ # * SVG::Graph::Plot
50
+ # * SVG::Graph::TimeSeries
51
+ #
52
+ # == Author
53
+ #
54
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
55
+ #
56
+ # Copyright 2004 Sean E. Russell
57
+ # This software is available under the Ruby license[LICENSE.txt]
58
+ #
59
+ class Graph
60
+ include REXML
61
+
62
+ # Initialize the graph object with the graph settings. You won't
63
+ # instantiate this class directly; see the subclass for options.
64
+ # [width] 500
65
+ # [height] 300
66
+ # [x_axis_position] nil
67
+ # [y_axis_position] nil
68
+ # [show_x_guidelines] false
69
+ # [show_y_guidelines] true
70
+ # [show_data_values] true
71
+ # [min_scale_value] 0
72
+ # [show_x_labels] true
73
+ # [stagger_x_labels] false
74
+ # [rotate_x_labels] false
75
+ # [step_x_labels] 1
76
+ # [step_include_first_x_label] true
77
+ # [show_y_labels] true
78
+ # [rotate_y_labels] false
79
+ # [scale_integers] false
80
+ # [show_x_title] false
81
+ # [x_title] 'X Field names'
82
+ # [x_title_location] :middle | :end
83
+ # [show_y_title] false
84
+ # [y_title_text_direction] :bt | :tb
85
+ # [y_title] 'Y Scale'
86
+ # [y_title_location] :middle | :end
87
+ # [show_graph_title] false
88
+ # [graph_title] 'Graph Title'
89
+ # [show_graph_subtitle] false
90
+ # [graph_subtitle] 'Graph Sub Title'
91
+ # [key] true,
92
+ # [key_position] :right, # bottom or righ
93
+ # [font_size] 12
94
+ # [title_font_size] 16
95
+ # [subtitle_font_size] 14
96
+ # [x_label_font_size] 12
97
+ # [x_title_font_size] 14
98
+ # [y_label_font_size] 12
99
+ # [y_title_font_size] 14
100
+ # [key_font_size] 10
101
+ # [no_css] false
102
+ # [add_popups] false
103
+ # [number_format] '%.2f'
104
+ def initialize( config )
105
+ @config = config
106
+ # array of Hash
107
+ @data = []
108
+ #self.top_align = self.top_font = 0
109
+ #self.right_align = self.right_font = 0
110
+
111
+ init_with({
112
+ :width => 500,
113
+ :height => 300,
114
+ :show_x_guidelines => false,
115
+ :show_y_guidelines => true,
116
+ :show_data_values => true,
117
+
118
+ :x_axis_position => nil,
119
+ :y_axis_position => nil,
120
+
121
+ :min_scale_value => nil,
122
+
123
+ :show_x_labels => true,
124
+ :stagger_x_labels => false,
125
+ :rotate_x_labels => false,
126
+ :step_x_labels => 1,
127
+ :step_include_first_x_label => true,
128
+
129
+ :show_y_labels => true,
130
+ :rotate_y_labels => false,
131
+ :stagger_y_labels => false,
132
+ :scale_integers => false,
133
+
134
+ :data_lines => nil,
135
+
136
+ :show_x_title => false,
137
+ :x_title => 'X Field names',
138
+ :x_title_location => :middle, # or :end
139
+
140
+ :show_y_title => false,
141
+ :y_title_text_direction => :bt, # other option is :tb
142
+ :y_title => 'Y Scale',
143
+ :y_title_location => :middle, # or :end
144
+
145
+ :show_graph_title => false,
146
+ :graph_title => 'Graph Title',
147
+ :show_graph_subtitle => false,
148
+ :graph_subtitle => 'Graph Sub Title',
149
+ :key => true,
150
+ :key_width => nil,
151
+ :key_position => :right, # bottom or right
152
+
153
+ :font_size => 12,
154
+ :title_font_size => 16,
155
+ :subtitle_font_size => 14,
156
+ :x_label_font_size => 12,
157
+ :y_label_font_size => 12,
158
+ :x_title_font_size => 14,
159
+ :y_title_font_size => 14,
160
+ :key_font_size => 10,
161
+ :key_box_size => 12,
162
+ :key_spacing => 5,
163
+
164
+ :no_css => false,
165
+ :add_popups => false,
166
+ :popup_radius => 10,
167
+ :number_format => '%.2f',
168
+ :style_sheet => '',
169
+ :inline_style_sheet => ''
170
+ })
171
+ set_defaults if self.respond_to? :set_defaults
172
+ # override default values with user supplied values
173
+ init_with config
174
+ end
175
+
176
+
177
+ # This method allows you do add data to the graph object.
178
+ # It can be called several times to add more data sets in.
179
+ #
180
+ # data_sales_02 = [12, 45, 21];
181
+ #
182
+ # graph.add_data({
183
+ # :data => data_sales_02,
184
+ # :title => 'Sales 2002'
185
+ # })
186
+ # @param conf [Hash] with the following keys:
187
+ # :data [Array] mandatory
188
+ # :title [String] mandatory name of data series for legend of graph
189
+ # :description [Array<String>] (optional) if given, description for each datapoint (shown in popups)
190
+ # :shape [Array<String>] (optional) if given, DataPoint shape is chosen based on this string instead of description
191
+ # :url [Array<String>] (optional) if given, link will be added to each datapoint
192
+ def add_data(conf)
193
+ @data ||= []
194
+ raise "No data provided by #{conf.inspect}" unless conf[:data].is_a?(Array)
195
+
196
+ add_data_init_or_check_optional_keys(conf, conf[:data].size)
197
+ @data << conf
198
+ end
199
+
200
+ # Checks all optional keys of the add_data method
201
+ def add_data_init_or_check_optional_keys(conf, datasize)
202
+ conf[:description] ||= Array.new(datasize)
203
+ conf[:shape] ||= Array.new(datasize)
204
+ conf[:url] ||= Array.new(datasize)
205
+
206
+ if conf[:description].size != datasize
207
+ raise "Description for popups does not have same size as provided data: #{conf[:description].size} vs #{conf[:data].size/2}"
208
+ end
209
+
210
+ if conf[:shape].size != datasize
211
+ raise "Shapes for points do not have same size as provided data: #{conf[:shape].size} vs #{conf[:data].size/2}"
212
+ end
213
+
214
+ if conf[:url].size != datasize
215
+ raise "URLs for points do not have same size as provided data: #{conf[:url].size} vs #{conf[:data].size/2}"
216
+ end
217
+ end
218
+
219
+ # This method removes all data from the object so that you can
220
+ # reuse it to create a new graph but with the same config options.
221
+ #
222
+ # graph.clear_data
223
+ def clear_data
224
+ @data = []
225
+ end
226
+
227
+
228
+ # This method processes the template with the data and
229
+ # config which has been set and returns the resulting SVG.
230
+ #
231
+ # This method will croak unless at least one data set has
232
+ # been added to the graph object.
233
+ #
234
+ # print graph.burn
235
+ #
236
+ def burn
237
+ raise "No data available" unless @data.size > 0
238
+
239
+ start_svg
240
+ calculate_graph_dimensions
241
+ @foreground = Element.new( "g" )
242
+ draw_graph
243
+ draw_titles
244
+ draw_legend
245
+ draw_data # this method needs to be implemented by child classes
246
+ @graph.add_element( @foreground )
247
+ style
248
+
249
+ data = ""
250
+ @doc.write( data, 0 )
251
+
252
+ if @config[:compress]
253
+ if defined?(Zlib)
254
+ inp, out = IO.pipe
255
+ gz = Zlib::GzipWriter.new( out )
256
+ gz.write data
257
+ gz.close
258
+ data = inp.read
259
+ else
260
+ data << "<!-- Ruby Zlib not available for SVGZ -->";
261
+ end
262
+ end
263
+
264
+ return data
265
+ end
266
+
267
+ # Burns the graph but returns only the <svg> node as String without the
268
+ # Doctype and XML Declaration. This allows easy integration into
269
+ # existing xml documents.
270
+ #
271
+ # @return [String] the SVG node which represents the Graph
272
+ def burn_svg_only
273
+ # initialize all instance variables by burning the graph
274
+ burn
275
+ f = REXML::Formatters::Pretty.new(0)
276
+ f.compact = true
277
+ out = ''
278
+ f.write(@root, out)
279
+ return out
280
+ end
281
+
282
+ # Burns the graph to an SVG string and returns it with a text/html mime type to be
283
+ # displayed in IRuby.
284
+ #
285
+ # @return [Array] A 2-dimension array containing the SVg string and a mime-type. This is the format expected by IRuby.
286
+ def to_iruby
287
+ ["text/html", burn_svg_only]
288
+ end
289
+
290
+
291
+ # Set the height of the graph box, this is the total height
292
+ # of the SVG box created - not the graph it self which auto
293
+ # scales to fix the space.
294
+ attr_accessor :height
295
+ # Set the width of the graph box, this is the total width
296
+ # of the SVG box created - not the graph it self which auto
297
+ # scales to fix the space.
298
+ attr_accessor :width
299
+ # Set the path/url to an external stylesheet, set to '' if
300
+ # you want to revert back to using the defaut internal version.
301
+ #
302
+ # To create an external stylesheet create a graph using the
303
+ # default internal version and copy the stylesheet section to
304
+ # an external file and edit from there.
305
+ attr_accessor :style_sheet
306
+ # Define as String the stylesheet contents to be inlined, set to '' to disable.
307
+ # This can be used, when referring to a url via :style_sheet is not suitable.
308
+ # E.g. in situations where there will be no internet access or the graph must
309
+ # consist of only one file.
310
+ #
311
+ # If not empty, the :style_sheet parameter (url) above will be ignored and is
312
+ # not written to the file
313
+ # see also https://github.com/erullmann/svg-graph2/commit/55eb6e983f6fcc69cc5a110d0ee6e05f906f639a
314
+ # Default: ''
315
+ attr_accessor :inline_style_sheet
316
+ # (Bool) Show the value of each element of data on the graph
317
+ attr_accessor :show_data_values
318
+ # By default (nil/undefined) the x-axis is at the bottom of the graph.
319
+ # With this property a custom position for the x-axis can be defined.
320
+ # Valid values are between :min_scale_value and maximum value of the
321
+ # data.
322
+ # Default: nil
323
+ attr_accessor :x_axis_position
324
+ # By default (nil/undefined) the y-axis is the left border of the graph.
325
+ # With this property a custom position for the y-axis can be defined.
326
+ # Valid values are any values in the range of x-values (in case of a
327
+ # Plot) or any of the :fields values (in case of Line/Bar Graphs, note
328
+ # the '==' operator is used to find at which value to draw the axis).
329
+ # Default: nil
330
+ attr_accessor :y_axis_position
331
+ # The point at which the Y axis starts, defaults to nil,
332
+ # if set to nil it will default to the minimum data value.
333
+ attr_accessor :min_scale_value
334
+ # Whether to show labels on the X axis or not, defaults
335
+ # to true, set to false if you want to turn them off.
336
+ attr_accessor :show_x_labels
337
+ # This puts the X labels at alternative levels so if they
338
+ # are long field names they will not overlap so easily.
339
+ # Default is false, to turn on set to true.
340
+ attr_accessor :stagger_x_labels
341
+ # This puts the Y labels at alternative levels so if they
342
+ # are long field names they will not overlap so easily.
343
+ # Default is false, to turn on set to true.
344
+ attr_accessor :stagger_y_labels
345
+ # This turns the X axis labels by 90 degrees when true or by a custom
346
+ # amount when a numeric value is given.
347
+ # Default is false, to turn on set to true.
348
+ attr_accessor :rotate_x_labels
349
+ # This turns the Y axis labels by 90 degrees when true or by a custom
350
+ # amount when a numeric value is given.
351
+ # Default is false, to turn on set to true or numeric value.
352
+ attr_accessor :rotate_y_labels
353
+ # How many "steps" to use between displayed X axis labels,
354
+ # a step of one means display every label, a step of two results
355
+ # in every other label being displayed (label <gap> label <gap> label),
356
+ # a step of three results in every third label being displayed
357
+ # (label <gap> <gap> label <gap> <gap> label) and so on.
358
+ attr_accessor :step_x_labels
359
+ # Whether to (when taking "steps" between X axis labels) step from
360
+ # the first label (i.e. always include the first label) or step from
361
+ # the X axis origin (i.e. start with a gap if step_x_labels is greater
362
+ # than one).
363
+ attr_accessor :step_include_first_x_label
364
+ # Whether to show labels on the Y axis or not, defaults
365
+ # to true, set to false if you want to turn them off.
366
+ attr_accessor :show_y_labels
367
+ # Ensures only whole numbers are used as the scale divisions.
368
+ # Default is false, to turn on set to true. This has no effect if
369
+ # scale divisions are less than 1.
370
+ attr_accessor :scale_integers
371
+ # This defines the gap between markers on the Y axis,
372
+ # default is a 10th of the max_value, e.g. you will have
373
+ # 10 markers on the Y axis. NOTE: do not set this too
374
+ # low - you are limited to 999 markers, after that the
375
+ # graph won't generate.
376
+ attr_accessor :scale_divisions
377
+ # Whether to show the title under the X axis labels,
378
+ # default is false, set to true to show.
379
+ attr_accessor :show_x_title
380
+ # What the title under X axis should be, e.g. 'Months'.
381
+ attr_accessor :x_title
382
+ # Where the x_title should be positioned, either in the :middle of the axis or
383
+ # at the :end of the axis. Defaults to :middle
384
+ attr_accessor :x_title_location
385
+ # Whether to show the title under the Y axis labels,
386
+ # default is false, set to true to show.
387
+ attr_accessor :show_y_title
388
+ # Aligns writing mode for Y axis label.
389
+ # Defaults to :bt (Bottom to Top).
390
+ # Change to :tb (Top to Bottom) to reverse.
391
+ attr_accessor :y_title_text_direction
392
+ # What the title under Y axis should be, e.g. 'Sales in thousands'.
393
+ attr_accessor :y_title
394
+ # Where the y_title should be positioned, either in the :middle of the axis or
395
+ # at the :end of the axis. Defaults to :middle
396
+ attr_accessor :y_title_location
397
+ # Whether to show a title on the graph, defaults
398
+ # to false, set to true to show.
399
+ attr_accessor :show_graph_title
400
+ # What the title on the graph should be.
401
+ attr_accessor :graph_title
402
+ # Whether to show a subtitle on the graph, defaults
403
+ # to false, set to true to show.
404
+ attr_accessor :show_graph_subtitle
405
+ # What the subtitle on the graph should be.
406
+ attr_accessor :graph_subtitle
407
+ # Whether to show a key (legend), defaults to true, set to
408
+ # false if you want to hide it.
409
+ attr_accessor :key
410
+ # Where the key should be positioned, defaults to
411
+ # :right, set to :bottom if you want to move it.
412
+ attr_accessor :key_position
413
+
414
+ attr_accessor :key_box_size
415
+
416
+ attr_accessor :key_spacing
417
+
418
+ attr_accessor :key_width
419
+
420
+ attr_accessor :data_lines
421
+
422
+ # Set the font size (in points) of the data point labels.
423
+ # Defaults to 12.
424
+ attr_accessor :font_size
425
+ # Set the font size of the X axis labels.
426
+ # Defaults to 12.
427
+ attr_accessor :x_label_font_size
428
+ # Set the font size of the X axis title.
429
+ # Defaults to 14.
430
+ attr_accessor :x_title_font_size
431
+ # Set the font size of the Y axis labels.
432
+ # Defaults to 12.
433
+ attr_accessor :y_label_font_size
434
+ # Set the font size of the Y axis title.
435
+ # Defaults to 14.
436
+ attr_accessor :y_title_font_size
437
+ # Set the title font size.
438
+ # Defaults to 16.
439
+ attr_accessor :title_font_size
440
+ # Set the subtitle font size.
441
+ # Defaults to 14.
442
+ attr_accessor :subtitle_font_size
443
+ # Set the key font size.
444
+ # Defaults to 10.
445
+ attr_accessor :key_font_size
446
+ # Show guidelines for the X axis, default is false
447
+ attr_accessor :show_x_guidelines
448
+ # Show guidelines for the Y axis, default is true
449
+ attr_accessor :show_y_guidelines
450
+ # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
451
+ # not using CSS can result in larger SVGs as well as making it impossible to
452
+ # change colors after the chart is generated. Defaults to false.
453
+ attr_accessor :no_css
454
+ # Add popups for the data points on some graphs, default is false.
455
+ attr_accessor :add_popups
456
+ # Customize popup radius
457
+ attr_accessor :popup_radius
458
+ # Number format values and Y axis representation like 1.2345667 represent as 1.23,
459
+ # Any valid format accepted by sprintf can be specified.
460
+ # If you don't want to change the format in any way you can use "%s". Defaults to "%.2f"
461
+ attr_accessor :number_format
462
+
463
+
464
+ protected
465
+
466
+ # implementation of a multiple array sort used for Schedule and Plot
467
+ def sort( *arrys )
468
+ new_arrys = arrys.transpose.sort_by(&:first).transpose
469
+ new_arrys.each_index { |k| arrys[k].replace(new_arrys[k]) }
470
+ end
471
+
472
+ # Overwrite configuration options with supplied options. Used
473
+ # by subclasses.
474
+ def init_with config
475
+ config.each { |key, value|
476
+ self.send( key.to_s+"=", value ) if self.respond_to? key
477
+ }
478
+ end
479
+
480
+ # Override this (and call super) to change the margin to the left
481
+ # of the plot area. Results in @border_left being set.
482
+ #
483
+ # By default it is 7 + max label height(font size or string length, depending on rotate) + title height
484
+ def calculate_left_margin
485
+ @border_left = 7
486
+ # Check size of Y labels
487
+ @border_left += max_y_label_width_px
488
+ if (show_y_title && (y_title_location ==:middle))
489
+ @border_left += y_title_font_size + 5
490
+ end
491
+ end
492
+
493
+ # Calculates the width of the widest Y label. This will be the
494
+ # character height if the Y labels are rotated. Returns 0 if labels
495
+ # are not shown
496
+ def max_y_label_width_px
497
+ return 0 if !show_y_labels
498
+ base_width = y_label_font_size + 3
499
+ if rotate_y_labels == true
500
+ self.rotate_y_labels = 90
501
+ end
502
+ if rotate_y_labels == false
503
+ self.rotate_y_labels = 0
504
+ end
505
+ # don't change rotate_y_label, if neither true nor false
506
+ label_width = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.5
507
+ rotated_width = label_width * Math.cos( rotate_y_labels * Math::PI / 180).abs()
508
+ max_width = base_width + rotated_width
509
+ if stagger_y_labels
510
+ max_width += 5 + y_label_font_size
511
+ end
512
+ return max_width
513
+ end
514
+
515
+
516
+ # Override this (and call super) to change the margin to the right
517
+ # of the plot area. Results in @border_right being set.
518
+ #
519
+ # By default it is 7 + width of the key if it is placed on the right
520
+ # or the maximum of this value or the tilte length (if title is placed at :end)
521
+ def calculate_right_margin
522
+ @border_right = 7
523
+ if key and key_position == :right
524
+ val = keys.max { |a,b| a.length <=> b.length }
525
+ @border_right += val.length * key_font_size * 0.6
526
+ @border_right += key_box_size
527
+ @border_right += 10 # Some padding around the box
528
+
529
+ if key_width.nil?
530
+ @border_right
531
+ else
532
+ @border_right = [key_width, @border_right].min
533
+ end
534
+ end
535
+ if (x_title_location == :end)
536
+ @border_right = [@border_right, x_title.length * x_title_font_size * 0.6].max
537
+ end
538
+ end
539
+
540
+
541
+ # Override this (and call super) to change the margin to the top
542
+ # of the plot area. Results in @border_top being set.
543
+ #
544
+ # This is 5 + the Title size + 5 + subTitle size
545
+ def calculate_top_margin
546
+ @border_top = 5
547
+ @border_top += [title_font_size, y_title_font_size].max if (show_graph_title || (y_title_location ==:end))
548
+ @border_top += 5
549
+ @border_top += subtitle_font_size if show_graph_subtitle
550
+ end
551
+
552
+ def add_datapoint_text_and_popup( x, y, label )
553
+ add_popup( x, y, label )
554
+ make_datapoint_text( x, y, label )
555
+ end
556
+
557
+ # Adds pop-up point information to a graph only if the config option is set.
558
+ def add_popup( x, y, label, style="", url="" )
559
+ if add_popups
560
+ if( numeric?(label) )
561
+ label = @number_format % label
562
+ end
563
+ txt_width = label.length * font_size * 0.6 + 10
564
+ tx = (x+txt_width > @graph_width ? x-5 : x+5)
565
+ g = Element.new( "g" )
566
+ g.attributes["id"] = g.object_id.to_s
567
+ g.attributes["visibility"] = "hidden"
568
+
569
+ # First add the mask
570
+ t = g.add_element( "text", {
571
+ "x" => tx.to_s,
572
+ "y" => (y - font_size).to_s,
573
+ "class" => "dataPointPopupMask"
574
+ })
575
+ t.attributes["style"] = style +
576
+ (x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
577
+ t.text = label.to_s
578
+
579
+ # Then add the text
580
+ t = g.add_element( "text", {
581
+ "x" => tx.to_s,
582
+ "y" => (y - font_size).to_s,
583
+ "class" => "dataPointPopup"
584
+ })
585
+ t.attributes["style"] = style +
586
+ (x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
587
+ t.text = label.to_s
588
+
589
+ @foreground.add_element( g )
590
+
591
+ # add a circle to catch the mouseover
592
+ mouseover = Element.new( "circle" )
593
+ mouseover.add_attributes({
594
+ "cx" => x.to_s,
595
+ "cy" => y.to_s,
596
+ "r" => "#{popup_radius}",
597
+ "style" => "opacity: 0",
598
+ "onmouseover" =>
599
+ "document.getElementById(#{g.object_id.to_s}).style.visibility ='visible'",
600
+ "onmouseout" =>
601
+ "document.getElementById(#{g.object_id.to_s}).style.visibility = 'hidden'",
602
+ })
603
+ if !url.nil?
604
+ href = Element.new("a")
605
+ href.add_attribute("xlink:href", url)
606
+ href.add_element(mouseover)
607
+ @foreground.add_element(href)
608
+ else
609
+ @foreground.add_element(mouseover)
610
+ end
611
+ elsif !url.nil?
612
+ # add a circle to catch the mouseover
613
+ mouseover = Element.new( "circle" )
614
+ mouseover.add_attributes({
615
+ "cx" => x.to_s,
616
+ "cy" => y.to_s,
617
+ "r" => "#{popup_radius}",
618
+ "style" => "opacity: 0",
619
+ })
620
+ href = Element.new("a")
621
+ href.add_attribute("xlink:href", url)
622
+ href.add_element(mouseover)
623
+ @foreground.add_element(href)
624
+ end # if add_popups
625
+ end # def add_popup
626
+
627
+ # returns the longest label from an array of labels as string
628
+ # each object in the array must support .to_s
629
+ def get_longest_label(arry)
630
+ longest_label = arry.max{|a,b|
631
+ # respect number_format
632
+ a = @number_format % a if numeric?(a)
633
+ b = @number_format % b if numeric?(b)
634
+ a.to_s.length <=> b.to_s.length
635
+ }
636
+ longest_label = @number_format % longest_label if numeric?(longest_label)
637
+ return longest_label
638
+ end
639
+
640
+ # Override this (and call super) to change the margin to the bottom
641
+ # of the plot area. Results in @border_bottom being set.
642
+ #
643
+ # 7 + max label height(font size or string length, depending on rotate) + title height
644
+ def calculate_bottom_margin
645
+ @border_bottom = 7
646
+ if key and key_position == :bottom
647
+ @border_bottom += @data.size * (font_size + 5)
648
+ @border_bottom += 10
649
+ end
650
+ @border_bottom += max_x_label_height_px
651
+ if (show_x_title && (x_title_location ==:middle))
652
+ @border_bottom += x_title_font_size + 5
653
+ end
654
+ end
655
+
656
+ # returns the maximum height of the labels respect the rotation or 0 if
657
+ # the labels are not shown
658
+ def max_x_label_height_px
659
+ return 0 if !show_x_labels
660
+
661
+ if rotate_x_labels
662
+ max_height = get_longest_label(get_x_labels).to_s.length * x_label_font_size * 0.6
663
+ else
664
+ max_height = x_label_font_size + 3
665
+ end
666
+ max_height += 5 + x_label_font_size if stagger_x_labels
667
+ return max_height
668
+ end
669
+
670
+
671
+ # Draws the background, axis, and labels.
672
+ def draw_graph
673
+ @graph = @root.add_element( "g", {
674
+ "transform" => "translate( #@border_left #@border_top )"
675
+ })
676
+
677
+ # Background
678
+ @graph.add_element( "rect", {
679
+ "x" => "0",
680
+ "y" => "0",
681
+ "width" => @graph_width.to_s,
682
+ "height" => @graph_height.to_s,
683
+ "class" => "graphBackground"
684
+ })
685
+
686
+ draw_x_axis
687
+ draw_y_axis
688
+
689
+ draw_x_labels
690
+ draw_y_labels
691
+
692
+ draw_data_lines
693
+ end
694
+
695
+ def draw_data_lines
696
+ if data_lines
697
+ data_lines.each do |line|
698
+ mycount = (line / @y_scale_division)
699
+ y = mycount * field_height
700
+ draw_y_data_lines(y)
701
+ end
702
+ end
703
+ end
704
+
705
+ # draws the x-axis; can be overridden by child classes
706
+ def draw_x_axis
707
+ # relative position on y-axis (hence @graph_height is our axis length)
708
+ relative_position = calculate_rel_position(get_y_labels, field_height, @x_axis_position, @graph_height)
709
+ # X-Axis
710
+ y_offset = (1 - relative_position) * @graph_height
711
+ @graph.add_element( "path", {
712
+ "d" => "M 0 #{y_offset} h#@graph_width",
713
+ "class" => "axis",
714
+ "id" => "yAxis"
715
+ })
716
+ end
717
+
718
+ # draws the y-axis; can be overridden by child classes
719
+ def draw_y_axis
720
+ # relative position on x-axis (hence @graph_width is our axis length)
721
+ relative_position = calculate_rel_position(get_x_labels, field_width, @y_axis_position, @graph_width)
722
+ # Y-Axis
723
+ x_offset = relative_position * @graph_width
724
+ @graph.add_element( "path", {
725
+ "d" => "M #{x_offset} 0 v#@graph_height",
726
+ "class" => "axis",
727
+ "id" => "xAxis"
728
+ })
729
+ end
730
+
731
+ # calculates the relative position betewen 0 and 1 of a value on the axis
732
+ # can be multiplied with either @graph_height or @graph_width to get the
733
+ # absolute position in pixels.
734
+ # If labels are strings, checks if one of label matches with the value
735
+ # and returns this position.
736
+ # If labels are numeric, compute relative position between first and last value
737
+ # If nothing else applies or the value is nil, the relative position is 0
738
+ # @param labels [Array] the array of x or y labels, see {#get_x_labels} or {#get_y_labels}
739
+ # @param segment_px [Float] number of pixels per label, see {#field_width} or {#field_height}
740
+ # @param value [Numeric, String] the value for which the relative position is computed
741
+ # @param axis_length [Numeric] either @graph_width or @graph_height
742
+ # @return [Float] relative position between 0 and 1, returns 0
743
+ def calculate_rel_position(labels, segment_px, value, axis_length)
744
+ # default value, y-axis on the left side, or x-axis at bottom
745
+ # puts "calculate_rel_position:"
746
+ # p labels
747
+ # p segment_px
748
+ # p value
749
+ # p axis_length
750
+ relative_position = 0
751
+ if !value.nil? # only
752
+ if (labels[0].is_a? Numeric) && (labels[-1].is_a? Numeric) && (value.is_a? Numeric)
753
+ # labels are numeric, compute relative position between first and last value
754
+ range = labels[-1] - labels[0]
755
+ position = value - labels[0]
756
+ # compute how many segments long the offset is
757
+ relative_to_segemts = position/range * (labels.size - 1)
758
+ # convert from segments to relative position on the axis axis,
759
+ # the number of segments (i.e. relative_to_segemts >= 1)
760
+ relative_position = relative_to_segemts * segment_px / axis_length
761
+ elsif labels[0].is_a? String
762
+ # labels are strings, see if one of label matches with the position
763
+ # and place the axis there
764
+ index = labels.index(value)
765
+ if !index.nil? # index would be nil if label is not found
766
+ offset_px = segment_px * index
767
+ relative_position = offset_px/axis_length # between 0 and 1
768
+ end
769
+ end
770
+ end # value.nil?
771
+ return relative_position
772
+ end
773
+
774
+ # Where in the X area the label is drawn
775
+ # Centered in the field, should be width/2. Start, 0.
776
+ def x_label_offset( width )
777
+ 0
778
+ end
779
+
780
+ # check if an object can be converted to float
781
+ def numeric?(object)
782
+ # true if Float(object) rescue false
783
+ object.is_a? Numeric
784
+ end
785
+
786
+ # adds the datapoint text to the graph only if the config option is set
787
+ def make_datapoint_text( x, y, value, style="" )
788
+ if show_data_values
789
+ textStr = value
790
+ if( numeric?(value) )
791
+ textStr = @number_format % value
792
+ end
793
+ # change anchor is label overlaps axis, normally anchor is middle (that's why we compute length/2)
794
+ if x < textStr.length/2 * font_size
795
+ style << "text-anchor: start;"
796
+ elsif x > @graph_width - textStr.length/2 * font_size
797
+ style << "text-anchor: end;"
798
+ end
799
+ # background for better readability
800
+ text = @foreground.add_element( "text", {
801
+ "x" => x.to_s,
802
+ "y" => y.to_s,
803
+ "class" => "dataPointLabelBackground",
804
+ })
805
+ text.text = textStr
806
+ text.attributes["style"] = style if style.length > 0
807
+ # actual label
808
+ text = @foreground.add_element( "text", {
809
+ "x" => x.to_s,
810
+ "y" => y.to_s,
811
+ "class" => "dataPointLabel"
812
+ })
813
+ text.text = textStr
814
+ text.attributes["style"] = style if style.length > 0
815
+ end
816
+ end
817
+
818
+
819
+ # Draws the X axis labels. The x-axis (@graph_width) is diveded into
820
+ # {#get_x_labels.length} equal sections. The (center) x-coordinate for a
821
+ # label hence is label_index * width_of_section
822
+ def draw_x_labels
823
+ stagger = x_label_font_size + 5
824
+ label_width = field_width
825
+ count = 0
826
+ x_axis_already_drawn = false
827
+ for label in get_x_labels
828
+ if step_include_first_x_label == true then
829
+ step = count % step_x_labels
830
+ else
831
+ step = (count + 1) % step_x_labels
832
+ end
833
+ # only draw every n-th label as defined by step_x_labels
834
+ if step == 0 && show_x_labels then
835
+ textStr = label.to_s
836
+ if( numeric?(label) )
837
+ textStr = @number_format % label
838
+ end
839
+ text = @graph.add_element( "text" )
840
+ text.attributes["class"] = "xAxisLabels"
841
+ text.text = textStr
842
+
843
+ x = count * label_width + x_label_offset( label_width )
844
+ y = @graph_height + x_label_font_size + 3
845
+ #t = 0 - (font_size / 2)
846
+
847
+ if stagger_x_labels and count % 2 == 1
848
+ y += stagger
849
+ @graph.add_element( "path", {
850
+ "d" => "M#{x} #@graph_height v#{stagger}",
851
+ "class" => "staggerGuideLine"
852
+ })
853
+ end
854
+
855
+ text.attributes["x"] = x.to_s
856
+ text.attributes["y"] = y.to_s
857
+ if rotate_x_labels
858
+ degrees = 90
859
+ if numeric? rotate_x_labels
860
+ degrees = rotate_x_labels
861
+ end
862
+ text.attributes["transform"] =
863
+ "rotate( #{degrees} #{x} #{y-x_label_font_size} )"+
864
+ " translate( 0 -#{x_label_font_size/4} )"
865
+ text.attributes["style"] = "text-anchor: start"
866
+ else
867
+ text.attributes["style"] = "text-anchor: middle"
868
+ end
869
+ end # if step == 0 && show_x_labels
870
+
871
+ draw_x_guidelines( label_width, count ) if show_x_guidelines
872
+ count += 1
873
+ end # for label in get_x_labels
874
+ end # draw_x_labels
875
+
876
+
877
+ # Where in the Y area the label is drawn
878
+ # Centered in the field, should be width/2. Start, 0.
879
+ def y_label_offset( height )
880
+ 0
881
+ end
882
+
883
+ # override this method in child class
884
+ # must return the array of labels for the x-axis
885
+ def get_x_labels
886
+ end
887
+
888
+ # override this method in child class
889
+ # must return the array of labels for the y-axis
890
+ # this method defines @y_scale_division
891
+ def get_y_labels
892
+ end
893
+
894
+ # space in px between x-labels
895
+ def field_width
896
+ # -1 is to use entire x-axis
897
+ # otherwise there is always 1 division unused
898
+ @graph_width.to_f / ( get_x_labels.length - 1 )
899
+ end
900
+
901
+ # space in px between the y-labels
902
+ def field_height
903
+ #(@graph_height.to_f - font_size*2*top_font) /
904
+ # (get_y_labels.length - top_align)
905
+ @graph_height.to_f / get_y_labels.length
906
+ end
907
+
908
+
909
+ # Draws the Y axis labels, the Y-Axis (@graph_height) is divided equally into #get_y_labels.lenght sections
910
+ # So the y coordinate for an arbitrary value is calculated as follows:
911
+ # y = @graph_height equals the min_value
912
+ # #normalize value of a single scale_division:
913
+ # count = value /(@y_scale_division)
914
+ # y = @graph_height - count * field_height
915
+ #
916
+ def draw_y_labels
917
+ stagger = y_label_font_size + 5
918
+ label_height = field_height
919
+ label_width = max_y_label_width_px
920
+ count = 0
921
+ y_offset = @graph_height + y_label_offset( label_height )
922
+ y_offset += font_size/3.0
923
+ for label in get_y_labels
924
+ if show_y_labels
925
+ # x = 0, y = 0 is top left right next to graph area
926
+ y = y_offset - (label_height * count)
927
+ # instead of calculating the middle anchor position, simply use
928
+ # static offset and anchor end to right-align the labels. See line :936 below.
929
+ #x = -label_width/2.0 + y_label_font_size/2.0
930
+ x = 3
931
+
932
+ if stagger_y_labels and count % 2 == 1
933
+ x -= stagger
934
+ @graph.add_element( "path", {
935
+ "d" => "M0 #{y} h#{-stagger}",
936
+ "class" => "staggerGuideLine"
937
+ })
938
+ end
939
+
940
+ text = @graph.add_element( "text", {
941
+ "x" => x.to_s,
942
+ "y" => y.to_s,
943
+ "class" => "yAxisLabels"
944
+ })
945
+ textStr = label.to_s
946
+ if( numeric?(label) )
947
+ textStr = @number_format % label
948
+ end
949
+ text.text = textStr
950
+ # note text-anchor is at bottom of textfield
951
+ #text.attributes["style"] = "text-anchor: middle"
952
+ text.attributes["style"] = "text-anchor: end"
953
+ degrees = rotate_y_labels
954
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) " +
955
+ "rotate( #{degrees} #{x} #{y} ) "
956
+ # text.attributes["y"] = (y - (y_label_font_size/2)).to_s
957
+
958
+ end # if show_y_labels
959
+ draw_y_guidelines( label_height, count ) if show_y_guidelines
960
+ count += 1
961
+ end # for label in get_y_labels
962
+ end # draw_y_labels
963
+
964
+
965
+ # Draws the X axis guidelines, parallel to the y-axis
966
+ def draw_x_guidelines( label_height, count )
967
+ if count != 0
968
+ @graph.add_element( "path", {
969
+ "d" => "M#{label_height*count} 0 v#@graph_height",
970
+ "class" => "guideLines"
971
+ })
972
+ end
973
+ end
974
+
975
+
976
+ # Draws the Y axis guidelines, parallel to the x-axis
977
+ def draw_y_guidelines( label_height, count )
978
+ if count != 0
979
+ @graph.add_element( "path", {
980
+ "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
981
+ "class" => "guideLines"
982
+ })
983
+ end
984
+ end
985
+
986
+ # Draws the Y axis guidelines, parallel to the x-axis
987
+ def draw_y_data_lines( height )
988
+ if height != 0
989
+ @graph.add_element( "path", {
990
+ "d" => "M0 #{@graph_height-(height)} h#@graph_width",
991
+ "class" => "dataLine"
992
+ })
993
+ end
994
+ end
995
+
996
+
997
+ # Draws the graph title and subtitle
998
+ def draw_titles
999
+ if show_graph_title
1000
+ @root.add_element( "text", {
1001
+ "x" => (width / 2).to_s,
1002
+ "y" => (title_font_size).to_s,
1003
+ "class" => "mainTitle"
1004
+ }).text = graph_title.to_s
1005
+ end
1006
+
1007
+ if show_graph_subtitle
1008
+ y_subtitle = show_graph_title ?
1009
+ title_font_size + subtitle_font_size + 5 :
1010
+ subtitle_font_size
1011
+ @root.add_element("text", {
1012
+ "x" => (width / 2).to_s,
1013
+ "y" => (y_subtitle).to_s,
1014
+ "class" => "subTitle"
1015
+ }).text = graph_subtitle.to_s
1016
+ end
1017
+
1018
+ if show_x_title
1019
+ if (x_title_location == :end)
1020
+ y = @graph_height + @border_top + x_title_font_size/2.0
1021
+ x = @border_left + @graph_width + x_title.length * x_title_font_size * 0.6/2.0
1022
+ else
1023
+ y = @graph_height + @border_top + x_title_font_size + max_x_label_height_px
1024
+ x = @border_left + @graph_width / 2
1025
+ end
1026
+
1027
+ @root.add_element("text", {
1028
+ "x" => x.to_s,
1029
+ "y" => y.to_s,
1030
+ "class" => "xAxisTitle",
1031
+ }).text = x_title.to_s
1032
+ end
1033
+
1034
+ if show_y_title
1035
+ if (y_title_location == :end)
1036
+ x = y_title.length * y_title_font_size * 0.6/2.0 # positioning is not optimal but ok for now
1037
+ y = @border_top - y_title_font_size/2.0
1038
+ else
1039
+ x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
1040
+ y = @border_top + @graph_height / 2
1041
+ end
1042
+ text = @root.add_element("text", {
1043
+ "x" => x.to_s,
1044
+ "y" => y.to_s,
1045
+ "class" => "yAxisTitle",
1046
+ })
1047
+ text.text = y_title.to_s
1048
+ # only rotate text if it is at the middle left of the y-axis
1049
+ # ignore the text_direction if y_title_location is set to :end
1050
+ if (y_title_location != :end)
1051
+ if y_title_text_direction == :bt
1052
+ text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
1053
+ else
1054
+ text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
1055
+ end
1056
+ end
1057
+ end
1058
+ end # draw_titles
1059
+
1060
+ def keys
1061
+ i = 0
1062
+ return @data.collect{ |d| i+=1; d[:title] || "Serie #{i}" }
1063
+ end
1064
+
1065
+ # Draws the legend on the graph
1066
+ def draw_legend
1067
+ if key
1068
+ group = @root.add_element( "g" )
1069
+
1070
+ key_count = 0
1071
+ for key_name in keys
1072
+ y_offset = (key_box_size * key_count) + (key_count * key_spacing)
1073
+ group.add_element( "rect", {
1074
+ "x" => 0.to_s,
1075
+ "y" => y_offset.to_s,
1076
+ "width" => key_box_size.to_s,
1077
+ "height" => key_box_size.to_s,
1078
+ "class" => "key#{key_count+1}"
1079
+ })
1080
+ group.add_element( "text", {
1081
+ "x" => (key_box_size + key_spacing).to_s,
1082
+ "y" => (y_offset + key_box_size).to_s,
1083
+ "class" => "keyText"
1084
+ }).text = key_name.to_s
1085
+ key_count += 1
1086
+ end
1087
+
1088
+ case key_position
1089
+ when :right
1090
+ x_offset = @graph_width + @border_left + (key_spacing * 2)
1091
+ y_offset = @border_top + (key_spacing * 2)
1092
+ when :bottom
1093
+ x_offset = @border_left + (key_spacing * 2)
1094
+ y_offset = @border_top + @graph_height + key_spacing
1095
+ if show_x_labels
1096
+ y_offset += max_x_label_height_px
1097
+ end
1098
+ y_offset += x_title_font_size + key_spacing if show_x_title
1099
+ end
1100
+ group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
1101
+ end
1102
+ end
1103
+
1104
+
1105
+ private
1106
+
1107
+ def style
1108
+ if no_css
1109
+ styles = parse_css
1110
+ @root.elements.each("//*[@class]") { |el|
1111
+ cl = el.attributes["class"]
1112
+ style = styles[cl]
1113
+ style += el.attributes["style"] if el.attributes["style"]
1114
+ el.attributes["style"] = style
1115
+ }
1116
+ end
1117
+ end
1118
+
1119
+ def parse_css
1120
+ css = get_style
1121
+ rv = {}
1122
+ while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
1123
+ names = $1
1124
+ css = $'
1125
+ css =~ /([^}]+)\}/m
1126
+ content = $1
1127
+ css = $'
1128
+
1129
+ nms = []
1130
+ while names =~ /^\s*,?\s*\.(\w+)/
1131
+ nms << $1
1132
+ names = $'
1133
+ end
1134
+
1135
+ content = content.tr( "\n\t", " ")
1136
+ for name in nms
1137
+ current = rv[name]
1138
+ current = current ? current+"; "+content : content
1139
+ rv[name] = current.strip.squeeze(" ")
1140
+ end
1141
+ end
1142
+ return rv
1143
+ end
1144
+
1145
+
1146
+ # Override and place code to add defs here
1147
+ # @param defs [REXML::Element]
1148
+ def add_defs defs
1149
+ end
1150
+
1151
+ # Creates the XML document and adds the root svg element with
1152
+ # the width, height and viewBox attributes already set.
1153
+ # The element is stored as @root.
1154
+ #
1155
+ # In addition a rectangle background of the same size as the
1156
+ # svg is added.
1157
+ #
1158
+ def start_svg
1159
+ # Base document
1160
+ @doc = Document.new
1161
+ @doc << XMLDecl.new
1162
+ @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
1163
+ %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
1164
+ if style_sheet && style_sheet != '' && inline_style_sheet.to_s.empty?
1165
+ # if inline_style_sheet is defined, url style sheet is ignored
1166
+ @doc << Instruction.new( "xml-stylesheet",
1167
+ %Q{href="#{style_sheet}" type="text/css"} )
1168
+ end
1169
+ @root = @doc.add_element( "svg", {
1170
+ "width" => width.to_s,
1171
+ "height" => height.to_s,
1172
+ "viewBox" => "0 0 #{width} #{height}",
1173
+ "xmlns" => "http://www.w3.org/2000/svg",
1174
+ "xmlns:xlink" => "http://www.w3.org/1999/xlink",
1175
+ "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
1176
+ "a3:scriptImplementation" => "Adobe"
1177
+ })
1178
+ @root << Comment.new( " "+"\\"*66 )
1179
+ @root << Comment.new( " Created with SVG::Graph " )
1180
+ @root << Comment.new( " SVG::Graph by Sean E. Russell " )
1181
+ @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
1182
+ " Leo Lapworth & Stephan Morgan " )
1183
+ @root << Comment.new( " "+"/"*66 )
1184
+
1185
+ defs = @root.add_element( "defs" )
1186
+ add_defs defs
1187
+ if !no_css
1188
+ if inline_style_sheet && inline_style_sheet != ''
1189
+ style = defs.add_element( "style", {"type"=>"text/css"} )
1190
+ style << CData.new( inline_style_sheet )
1191
+ else
1192
+ @root << Comment.new(" include default stylesheet if none specified ")
1193
+ style = defs.add_element( "style", {"type"=>"text/css"} )
1194
+ style << CData.new( get_style )
1195
+ end
1196
+ end
1197
+
1198
+ @root << Comment.new( "SVG Background" )
1199
+ @root.add_element( "rect", {
1200
+ "width" => width.to_s,
1201
+ "height" => height.to_s,
1202
+ "x" => "0",
1203
+ "y" => "0",
1204
+ "class" => "svgBackground"
1205
+ })
1206
+ end
1207
+
1208
+ #
1209
+ def calculate_graph_dimensions
1210
+ calculate_left_margin
1211
+ calculate_right_margin
1212
+ calculate_bottom_margin
1213
+ calculate_top_margin
1214
+ @graph_width = width - @border_left - @border_right
1215
+ @graph_height = height - @border_top - @border_bottom
1216
+ end
1217
+
1218
+ def get_style
1219
+ return <<EOL
1220
+ /* Copy from here for external style sheet */
1221
+ .svgBackground{
1222
+ fill:#ffffff;
1223
+ }
1224
+ .graphBackground{
1225
+ fill:#f0f0f0;
1226
+ }
1227
+
1228
+ /* graphs titles */
1229
+ .mainTitle{
1230
+ text-anchor: middle;
1231
+ fill: #000000;
1232
+ font-size: #{title_font_size}px;
1233
+ font-family: Arial, sans-serif;
1234
+ font-weight: normal;
1235
+ }
1236
+ .subTitle{
1237
+ text-anchor: middle;
1238
+ fill: #999999;
1239
+ font-size: #{subtitle_font_size}px;
1240
+ font-family: Arial, sans-serif;
1241
+ font-weight: normal;
1242
+ }
1243
+
1244
+ .axis{
1245
+ stroke: #000000;
1246
+ stroke-width: 1px;
1247
+ }
1248
+
1249
+ .guideLines{
1250
+ stroke: #666666;
1251
+ stroke-width: 1px;
1252
+ stroke-dasharray: 5 5;
1253
+ }
1254
+
1255
+ .xAxisLabels{
1256
+ text-anchor: middle;
1257
+ fill: #000000;
1258
+ font-size: #{x_label_font_size}px;
1259
+ font-family: Arial, sans-serif;
1260
+ font-weight: normal;
1261
+ }
1262
+
1263
+ .yAxisLabels{
1264
+ text-anchor: end;
1265
+ fill: #000000;
1266
+ font-size: #{y_label_font_size}px;
1267
+ font-family: Arial, sans-serif;
1268
+ font-weight: normal;
1269
+ }
1270
+
1271
+ .xAxisTitle{
1272
+ text-anchor: middle;
1273
+ fill: #ff0000;
1274
+ font-size: #{x_title_font_size}px;
1275
+ font-family: Arial, sans-serif;
1276
+ font-weight: normal;
1277
+ }
1278
+
1279
+ .yAxisTitle{
1280
+ fill: #ff0000;
1281
+ text-anchor: middle;
1282
+ font-size: #{y_title_font_size}px;
1283
+ font-family: Arial, sans-serif;
1284
+ font-weight: normal;
1285
+ }
1286
+
1287
+ .dataPointLabel, .dataPointLabelBackground, .dataPointPopup, .dataPointPopupMask{
1288
+ fill: #000000;
1289
+ text-anchor:middle;
1290
+ font-size: 10px;
1291
+ font-family: Arial, sans-serif;
1292
+ font-weight: normal;
1293
+ }
1294
+
1295
+ .dataPointLabelBackground{
1296
+ stroke: #ffffff;
1297
+ stroke-width: 2;
1298
+ }
1299
+
1300
+ .dataPointPopupMask{
1301
+ stroke: white;
1302
+ stroke-width: 7;
1303
+ }
1304
+
1305
+ .dataPointPopup{
1306
+ fill: black;
1307
+ stroke-width: 2;
1308
+ }
1309
+
1310
+ .staggerGuideLine{
1311
+ fill: none;
1312
+ stroke: #000000;
1313
+ stroke-width: 0.5px;
1314
+ }
1315
+
1316
+ .dataLine{
1317
+ fill: none;
1318
+ stroke: #075985;
1319
+ stroke-width: 1px;
1320
+ stroke-dasharray: 5 5;
1321
+ }
1322
+
1323
+ #{get_css}
1324
+
1325
+ .keyText{
1326
+ fill: #000000;
1327
+ text-anchor:start;
1328
+ font-size: #{key_font_size}px;
1329
+ font-family: Arial, sans-serif;
1330
+ font-weight: normal;
1331
+ }
1332
+ /* End copy for external style sheet */
1333
+ EOL
1334
+ end # get_style
1335
+
1336
+ end # class Graph
1337
+ end # module Graph
1338
+ end # module SVG