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.
- checksums.yaml +7 -0
- data/GPL.txt +340 -0
- data/History.txt +97 -0
- data/LICENSE.txt +57 -0
- data/README.md +132 -0
- data/README.txt +89 -0
- data/Rakefile +42 -0
- data/lib/SVG/Graph/Bar.rb +154 -0
- data/lib/SVG/Graph/BarBase.rb +149 -0
- data/lib/SVG/Graph/BarHorizontal.rb +155 -0
- data/lib/SVG/Graph/C3js.rb +274 -0
- data/lib/SVG/Graph/DataPoint.rb +86 -0
- data/lib/SVG/Graph/ErrBar.rb +198 -0
- data/lib/SVG/Graph/Graph.rb +1338 -0
- data/lib/SVG/Graph/Line.rb +467 -0
- data/lib/SVG/Graph/Pie.rb +442 -0
- data/lib/SVG/Graph/Plot.rb +636 -0
- data/lib/SVG/Graph/Schedule.rb +386 -0
- data/lib/SVG/Graph/TimeSeries.rb +263 -0
- data/lib/svggraph.rb +18 -0
- data/test/test_svg_graph.rb +68 -0
- metadata +79 -0
@@ -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
|