File Coverage

blib/lib/SVG/TT/Graph/XY.pm
Criterion Covered Total %
statement 82 95 86.3
branch 26 44 59.0
condition 21 30 70.0
subroutine 10 10 100.0
pod 1 2 50.0
total 140 181 77.3


line stmt bran cond sub pod time code
1             package SVG::TT::Graph::XY;
2              
3 3     3   3714 use strict;
  3         5  
  3         87  
4 3     3   14 use Carp;
  3         4  
  3         149  
5 3     3   14 use SVG::TT::Graph;
  3         5  
  3         61  
6 3     3   11 use base qw(SVG::TT::Graph);
  3         3  
  3         3303  
7              
8             our $VERSION = $SVG::TT::Graph::VERSION;
9             our $TEMPLATE_FH = \*DATA;
10              
11              
12             =head1 NAME
13              
14             SVG::TT::Graph::XY - Create presentation quality SVG line graphs of XY data points easily
15              
16             =head1 SYNOPSIS
17              
18             use SVG::TT::Graph::XY;
19              
20             my @data_cpu = (0.3, 23, 0.5, 54, 1.0, 67, 1.8, 12);
21             my @data_disk = (0.45, 12, 0.51, 26, 0.53, 23);
22              
23             my $graph = SVG::TT::Graph::XY->new({
24             'height' => '500',
25             'width' => '300',
26             });
27              
28             $graph->add_data({
29             'data' => \@data_cpu,
30             'title' => 'CPU',
31             });
32              
33             $graph->add_data({
34             'data' => \@data_disk,
35             'title' => 'Disk',
36             });
37              
38             print "Content-type: image/svg+xml\n\n";
39             print $graph->burn();
40              
41             =head1 DESCRIPTION
42              
43             This object aims to allow you to easily create high quality
44             SVG line graphs of XY data. You can either use the default style sheet
45             or supply your own. Either way there are many options which can
46             be configured to give you control over how the graph is
47             generated - with or without a key, data elements at each point,
48             title, subtitle etc.
49              
50             =head1 METHODS
51              
52             =head2 new()
53              
54             use SVG::TT::Graph::XY;
55              
56             my $graph = SVG::TT::Graph::XY->new({
57              
58             # Optional - defaults shown
59             'height' => 500,
60             'width' => 300,
61              
62             'show_y_labels' => 1,
63             'yscale_divisions' => '',
64             'min_yscale_value' => 0,
65             'max_yscale_value' => '',
66              
67             'show_x_labels' => 1,
68             'xscale_divisions' => '',
69             'min_xscale_value' => '',
70             'max_xscale_value' => '',
71             'stagger_x_labels' => 0,
72             'rotate_x_labels' => 0,
73             'y_label_formatter' => sub { return @_ },
74             'x_label_formatter' => sub { return @_ },
75              
76             'show_data_points' => 1,
77             'show_data_values' => 1,
78             'rollover_values' => 0,
79              
80             'area_fill' => 0,
81              
82             'show_x_title' => 0,
83             'x_title' => 'X Field names',
84              
85             'show_y_title' => 0,
86             'y_title' => 'Y Scale',
87              
88             'show_graph_title' => 0,
89             'graph_title' => 'Graph Title',
90             'show_graph_subtitle' => 0,
91             'graph_subtitle' => 'Graph Sub Title',
92             'key' => 0,
93             'key_position' => 'right',
94              
95             # Stylesheet defaults
96             'style_sheet' => '/includes/graph.css', # internal stylesheet
97             'random_colors' => 0,
98             });
99              
100             The constructor takes a hash reference with values defaulted to those
101             shown above - with the exception of style_sheet which defaults
102             to using the internal style sheet.
103              
104             =head2 add_data()
105              
106             my @data_cpu = (0.3, 23, 0.5, 54, 1.0, 67, 1.8, 12);
107             or
108             my @data_cpu = ([0.3,23], [0.5,54], [1.0,67], [1.8,12]);
109             or
110             my @data_cpu = ([0.3,23,'23%'], [0.5,54,'54%'], [1.0,67,'67%'], [1.8,12,'12%']);
111              
112             $graph->add_data({
113             'data' => \@data_cpu,
114             'title' => 'CPU',
115             });
116              
117             This method allows you to add data to the graph object. The
118             data are expected to be either a list of scalars (in which
119             case pairs of elements are taken to be X,Y pairs) or a list
120             of array references. In the latter case, the first two
121             elements in each referenced array are taken to be X and Y,
122             and the optional third element (if present) is used as the
123             text to display for that point for show_data_values and
124             rollover_values; otherwise the Y value itself is displayed.
125             It can be called several times to add more data sets in.
126              
127             =head2 clear_data()
128              
129             my $graph->clear_data();
130              
131             This method removes all data from the object so that you can
132             reuse it to create a new graph but with the same config options.
133              
134             =head2 burn()
135              
136             print $graph->burn();
137              
138             This method processes the template with the data and
139             config which has been set and returns the resulting SVG.
140              
141             This method will croak unless at least one data set has
142             been added to the graph object.
143              
144             =head2 config methods
145              
146             my $value = $graph->method();
147             my $confirmed_new_value = $graph->method($value);
148              
149             The following is a list of the methods which are available
150             to change the config of the graph object after it has been
151             created.
152              
153             =over 4
154              
155             =item height()
156              
157             Set the height of the graph box, this is the total height
158             of the SVG box created - not the graph it self which auto
159             scales to fix the space.
160              
161             =item width()
162              
163             Set the width of the graph box, this is the total width
164             of the SVG box created - not the graph it self which auto
165             scales to fix the space.
166              
167             =item compress()
168              
169             Whether or not to compress the content of the SVG file (Compress::Zlib required).
170              
171             =item tidy()
172              
173             Whether or not to tidy the content of the SVG file (XML::Tidy required).
174              
175             =item style_sheet()
176              
177             Set the path to an external stylesheet, set to '' if
178             you want to revert back to using the default internal version.
179              
180             Set to "inline:<style>...</style>" with your CSS in between the tags.
181             You can thus override the default style without requireing an external URL.
182              
183             The default stylesheet handles up to 12 data sets. All data series over
184             the 12th will have no style and be in black. If you have over 12 data
185             sets you can assign them all random colors (see the random_color()
186             method) or create your own stylesheet and add the additional settings
187             for the extra data sets.
188              
189             To create an external stylesheet create a graph using the
190             default internal version and copy the stylesheet section to
191             an external file and edit from there.
192              
193             =item random_colors()
194              
195             Use random colors in the internal stylesheet.
196              
197             =item show_data_values()
198              
199             Show the value of each element of data on the graph (or
200             optionally a user-defined label; see add_data).
201              
202             =item show_data_points()
203              
204             Show a small circle on the graph where the line
205             goes from one point to the next.
206              
207             =item rollover_values()
208              
209             Shows data values and data points when the mouse is over the point.
210             Used in combination with show_data_values and/or show_data_points.
211              
212             =item data_value_format()
213              
214             Format specifier to for data values (as per printf).
215              
216             =item max_x_span()
217              
218             Maximum span for a line between data points on the X-axis. If this span is
219             exceeded, the points are not connected. This is useful for skipping missing data
220             sections. If you set this value to something smaller than 0 (e.g. -1), you will
221             get an XY scatter plot with no line joining the data points.
222              
223             =item stacked()
224              
225             Accumulates each data set. (i.e. Each point increased by
226             sum of all previous series at same point). Default is 0,
227             set to '1' to show.
228              
229             =item min_yscale_value()
230              
231             The point at which the Y axis starts, defaults to '0',
232             if set to '' it will default to the minimum data value.
233              
234             =item max_yscale_value()
235              
236             The point at which the Y axis ends,
237             if set to '' it will default to the maximum data value.
238              
239             =item yscale_divisions()
240              
241             This defines the gap between markers on the Y axis,
242             default is a 10th of the range, e.g. you will have
243             10 markers on the Y axis. NOTE: do not set this too
244             low - you are limited to 999 markers, after that the
245             graph won't generate.
246              
247             =item show_x_labels()
248              
249             Whether to show labels on the X axis or not, defaults
250             to 1, set to '0' if you want to turn them off.
251              
252             =item show_y_labels()
253              
254             Whether to show labels on the Y axis or not, defaults
255             to 1, set to '0' if you want to turn them off.
256              
257             =item y_label_format()
258              
259             Format string for presenting the Y axis labels (as per printf).
260              
261             =item xscale_divisions()
262              
263             This defines the gap between markers on the X axis.
264             Default is the entire range (only start and end axis
265             labels).
266              
267             =item stagger_x_labels()
268              
269             This puts the labels at alternative levels so if they
270             are long field names they will not overlap so easily.
271             Default it '0', to turn on set to '1'.
272              
273             =item rotate_x_labels()
274              
275             This turns the X axis labels by 90 degrees.
276             Default it '0', to turn on set to '1'.
277              
278             =item min_xscale_value()
279              
280             This sets the minimum X value. Any data points before this value will not be
281             shown.
282              
283             =item max_xscale_value()
284              
285             This sets the maximum X value. Any data points after this value will not be
286             shown.
287              
288             =item show_x_title()
289              
290             Whether to show the title under the X axis labels,
291             default is 0, set to '1' to show.
292              
293             =item x_title()
294              
295             What the title under X axis should be, e.g. 'Parameter X'.
296              
297             =item show_y_title()
298              
299             Whether to show the title under the Y axis labels,
300             default is 0, set to '1' to show.
301              
302             =item y_title()
303              
304             What the title under Y axis should be, e.g. 'Sales in thousands'.
305              
306             =item show_graph_title()
307              
308             Whether to show a title on the graph,
309             default is 0, set to '1' to show.
310              
311             =item graph_title()
312              
313             What the title on the graph should be.
314              
315             =item show_graph_subtitle()
316              
317             Whether to show a subtitle on the graph,
318             default is 0, set to '1' to show.
319              
320             =item graph_subtitle()
321              
322             What the subtitle on the graph should be.
323              
324             =item key()
325              
326             Whether to show a key, defaults to 0, set to
327             '1' if you want to show it.
328              
329             =item key_position()
330              
331             Where the key should be positioned, defaults to
332             'right', set to 'bottom' if you want to move it.
333              
334             =item x_label_formatter ()
335              
336             A callback subroutine which will format a label on the x axis. For example:
337              
338             $graph->x_label_formatter( sub { return '$' . $_[0] } );
339              
340             =item y_label_formatter()
341              
342             A callback subroutine which will format a label on the y axis. For example:
343              
344             $graph->y_label_formatter( sub { return '$' . $_[0] } );
345              
346             =back
347              
348             =head1 EXAMPLES
349              
350             For examples look at the project home page
351             http://leo.cuckoo.org/projects/SVG-TT-Graph/
352              
353             =head1 EXPORT
354              
355             None by default.
356              
357             =head1 SEE ALSO
358              
359             L<SVG::TT::Graph>,
360             L<SVG::TT::Graph::Line>,
361             L<SVG::TT::Graph::Bar>,
362             L<SVG::TT::Graph::BarHorizontal>,
363             L<SVG::TT::Graph::BarLine>,
364             L<SVG::TT::Graph::Pie>,
365             L<Compress::Zlib>,
366             L<XML::Tidy>
367              
368             =cut
369              
370             sub _init {
371 3     3   6 my $self = shift;
372             }
373              
374             sub _set_defaults {
375 3     3   7 my $self = shift;
376              
377 3         3 my @fields = ();
378              
379             my %default = (
380             'fields' => \@fields,
381              
382             'width' => '500',
383             'height' => '300',
384              
385             'style_sheet' => '',
386             'random_colors' => 0,
387              
388             'show_data_points' => 1,
389             'show_data_values' => 1,
390             'rollover_values' => 0,
391              
392             'max_x_span' => '',
393              
394             'area_fill' => 0,
395              
396             'show_y_labels' => 1,
397             'yscale_divisions' => '',
398             'min_yscale_value' => '0',
399              
400             'stacked' => 0,
401              
402             'show_x_labels' => 1,
403             'stagger_x_labels' => 0,
404             'rotate_x_labels' => 0,
405             'xscale_divisions' => '',
406 2     2   540 'x_label_formatter' => sub { return @_ },
407 8     8   210 'y_label_formatter' => sub { return @_ },
408              
409 3         63 'show_x_title' => 0,
410             'x_title' => 'X Field names',
411              
412             'show_y_title' => 0,
413             'y_title' => 'Y Scale',
414              
415             'show_graph_title' => 0,
416             'graph_title' => 'Graph Title',
417             'show_graph_subtitle' => 0,
418             'graph_subtitle' => 'Graph Sub Title',
419              
420             'key' => 0,
421             'key_position' => 'right', # bottom or right
422             );
423              
424 3         15 while( my ($key,$value) = each %default ) {
425 90         177 $self->{config}->{$key} = $value;
426             }
427             }
428              
429             # override this so we can pre-manipulate the data
430             sub add_data {
431 2     2 1 1599 my ($self, $conf) = @_;
432              
433             croak 'no data provided'
434 2 50 33     13 unless (defined $conf->{'data'} && ref($conf->{'data'}) eq 'ARRAY');
435              
436             # create an array
437 2 100       5 unless(defined $self->{'data'}) {
438 1         2 my @data;
439 1         2 $self->{'data'} = \@data;
440             }
441              
442             # convert to sorted (by ascending numeric value) array of [ x, y ]
443 2         3 my @new_data = ();
444 2         3 my ($i,$x,@pair);
445              
446 2         61 $i = 0;
447 2         3 while ($i < @{$conf->{'data'}}) {
  8         14  
448 6         7 @pair = ();
449 6 100       10 if (ref($conf->{'data'}->[$i]) eq 'ARRAY') {
450 4         5 push @pair,@{$conf->{'data'}->[$i]};
  4         7  
451 4         5 $i++;
452             }
453             else {
454 2         4 $pair[0] = $conf->{'data'}->[$i++];
455 2         3 $pair[1] = $conf->{'data'}->[$i++];
456             }
457 6         8 push @new_data, [ @pair ];
458             }
459              
460 2         8 my @sorted = sort {@{$a}[0] <=> @{$b}[0]} @new_data;
  6         5  
  6         9  
  6         11  
461              
462             # if stacked, we accumulate the
463 2 50 33     7 if (($self->{config}->{stacked}) && (@{$self->{'data'}})) {
  0         0  
464 0         0 my $prev = $self->{'data'}->[@{$self->{'data'}} - 1]->{pairs};
  0         0  
465              
466             # check our length matches previous
467             croak sprintf("Series %d can not be stacked on previous series. Mismatched length.",
468 0 0       0 scalar(@{$self->{'data'}}))
  0         0  
469             unless (scalar(@sorted) == scalar(@$prev));
470              
471 0         0 for (my $i = 0; $i < @sorted; $i++) {
472             # check the x value matches
473             croak sprintf("Series %d can not be stacked on previous series. Mismatched x value at sample %d (x %s).",
474 0 0       0 scalar(@{$self->{'data'}}),
  0         0  
475             $i,
476             $sorted[$i][0])
477             unless ($sorted[$i][0] == $prev->[$i][0]);
478              
479 0         0 $sorted[$i][1] += $prev->[$i][1];
480             }
481             }
482              
483 2         5 my %store = (
484             'pairs' => \@sorted,
485             );
486              
487 2 50       5 $store{'title'} = $conf->{'title'} if defined $conf->{'title'};
488 2         3 push (@{$self->{'data'}},\%store);
  2         37  
489              
490 2         7 return 1;
491             }
492              
493             # override calculations to set a few calculated values, mainly for scaling
494             sub calculations {
495 1     1 0 2 my $self = shift;
496              
497             # run through the data and calculate maximum and minimum values
498 1         2 my ($max_key_size, $max_x, $min_x, $max_y, $min_y, $max_x_label_length, $x_label);
499              
500 1         2 foreach my $dataset (@{$self->{data}}) {
  1         3  
501 2 100 66     8 $max_key_size = length($dataset->{title}) if ((!defined $max_key_size) || ($max_key_size < length($dataset->{title})));
502              
503 2         2 foreach my $pair (@{$dataset->{pairs}}) {
  2         3  
504 6 50 66     20 $max_x = $pair->[0] if ((!defined $max_x) || ($max_x < $pair->[0]));
505 6 100 66     15 $min_x = $pair->[0] if ((!defined $min_x) || ($min_x > $pair->[0]));
506 6 100 100     27 $max_y = $pair->[1] if (($pair->[1] ne '') && ((!defined $max_y) || ($max_y < $pair->[1])));
      66        
507 6 100 100     22 $min_y = $pair->[1] if (($pair->[1] ne '') && ((!defined $min_y) || ($min_y > $pair->[1])));
      66        
508              
509 6         5 $x_label = $pair->[0];
510 6 100 100     25 $max_x_label_length = length($x_label) if ((!defined $max_x_label_length) || ($max_x_label_length < length($x_label)));
511             }
512             }
513 1         3 $self->{calc}->{max_key_size} = $max_key_size;
514 1         3 $self->{calc}->{max_x} = $max_x;
515 1         2 $self->{calc}->{min_x} = $min_x;
516 1         2 $self->{calc}->{max_y} = $max_y;
517 1         2 $self->{calc}->{min_y} = $min_y;
518 1         2 $self->{calc}->{max_x_label_length} = $max_x_label_length;
519              
520             # Calc the x axis scale values
521 1 50       8 $self->{calc}->{min_xscale_value} = ($self->_is_valid_config('min_xscale_value')) ? $self->{config}->{min_xscale_value} : $min_x;
522 1 50       4 $self->{calc}->{max_xscale_value} = ($self->_is_valid_config('max_xscale_value')) ? $self->{config}->{max_xscale_value} : $max_x;
523 1         4 $self->{calc}->{xscale_range} = $self->{calc}->{max_xscale_value} - $self->{calc}->{min_xscale_value};
524              
525             # Calc the y axis scale values
526 1 50       3 $self->{calc}->{min_yscale_value} = ($self->_is_valid_config('min_yscale_value')) ? $self->{config}->{min_yscale_value} : $min_y;
527 1 50       4 $self->{calc}->{max_yscale_value} = ($self->_is_valid_config('max_yscale_value')) ? $self->{config}->{max_yscale_value} : $max_y;
528 1         4 $self->{calc}->{yscale_range} = $self->{calc}->{max_yscale_value} - $self->{calc}->{min_yscale_value};
529              
530 1         2 my ($range,$division,$precision);
531              
532 1 50       2 if ($self->_is_valid_config('yscale_divisions')) {
533 0         0 $division = $self->{config}->{yscale_divisions};
534              
535 0 0       0 if ($division >= 1) {
536 0         0 $precision = 0;
537             }
538             else {
539 0         0 $precision = length($division) - 2;
540             }
541             }
542             else {
543             # Find divisions, format and range
544 1         6 ($range, $division, $precision) = $self->_range_calc($self->{calc}->{yscale_range});
545              
546             # If a max value hasn't been set we can set a revised range and max value
547 1 50       4 if (! $self->_is_valid_config('max_yscale_value')) {
548 1         3 $self->{calc}->{max_yscale_value} = $self->{calc}->{min_yscale_value} + $range;
549 1         3 $self->{calc}->{yscale_range} = $self->{calc}->{max_yscale_value} - $self->{calc}->{min_yscale_value};
550             }
551             }
552 1         2 $self->{calc}->{yscale_division} = $division;
553              
554 1 50       3 $self->{calc}->{y_label_format} = ($self->_is_valid_config('y_label_format')) ? $self->{config}->{y_label_format} : "%.${precision}f";
555 1 50       3 $self->{calc}->{data_value_format} = ($self->_is_valid_config('data_value_format')) ? $self->{config}->{data_value_format} : "%.${precision}f";
556             }
557              
558             1;
559             __DATA__
560             <?xml version="1.0"?>
561             <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"
562             "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
563              
564             [% stylesheet = 'included' %]
565              
566             [% IF config.style_sheet && config.style_sheet != '' && config.style_sheet.substr(0,7) != 'inline:' %]
567             <?xml-stylesheet href="[% config.style_sheet %]" type="text/css"?>
568             [% ELSIF config.style_sheet && config.style_sheet.substr(0,7) == 'inline:'%]
569             [% stylesheet = 'inline'
570             style_inline = config.style_sheet.substr(7) %]
571             [% ELSE %]
572             [% stylesheet = 'excluded' %]
573             [% END %]
574              
575             <svg width="[% config.width %]" height="[% config.height %]" viewBox="0 0 [% config.width %] [% config.height %]" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
576              
577             <!-- \\\\\\\\\\\\\\\\\\\\\\\\\\\\ -->
578             <!-- Created with SVG::TT::Graph -->
579             <!-- Dave Meibusch -->
580             <!-- //////////////////////////// -->
581              
582             [% IF stylesheet == 'inline' %]
583             [% style_inline %]
584             [% ELSIF stylesheet == 'excluded' %]
585             [%# include default stylesheet if none specified %]
586             <defs>
587             <style type="text/css">
588             <![CDATA[
589             /* Copy from here for external style sheet */
590             .svgBackground{
591             fill:#ffffff;
592             }
593             .graphBackground{
594             fill:#f0f0f0;
595             }
596              
597             /* graphs titles */
598             .mainTitle{
599             text-anchor: middle;
600             fill: #000000;
601             font-size: 14px;
602             font-family: "Arial", sans-serif;
603             font-weight: normal;
604             }
605             .subTitle{
606             text-anchor: middle;
607             fill: #999999;
608             font-size: 12px;
609             font-family: "Arial", sans-serif;
610             font-weight: normal;
611             }
612              
613             .axis{
614             stroke: #000000;
615             stroke-width: 1px;
616             }
617              
618             .guideLines{
619             stroke: #666666;
620             stroke-width: 1px;
621             stroke-dasharray: 5 5;
622             }
623              
624             .xAxisLabels{
625             text-anchor: middle;
626             fill: #000000;
627             font-size: 12px;
628             font-family: "Arial", sans-serif;
629             font-weight: normal;
630             }
631              
632             .yAxisLabels{
633             text-anchor: end;
634             fill: #000000;
635             font-size: 12px;
636             font-family: "Arial", sans-serif;
637             font-weight: normal;
638             }
639              
640             .xAxisTitle{
641             text-anchor: middle;
642             fill: #ff0000;
643             font-size: 14px;
644             font-family: "Arial", sans-serif;
645             font-weight: normal;
646             }
647              
648             .yAxisTitle{
649             fill: #ff0000;
650             text-anchor: middle;
651             font-size: 14px;
652             font-family: "Arial", sans-serif;
653             font-weight: normal;
654             }
655              
656             .dataPointLabel{
657             fill: #000000;
658             text-anchor:middle;
659             font-size: 10px;
660             font-family: "Arial", sans-serif;
661             font-weight: normal;
662             }
663             .staggerGuideLine{
664             fill: none;
665             stroke: #000000;
666             stroke-width: 0.5px;
667             }
668              
669             [% FOREACH dataset = data %]
670             [% color = '' %]
671             [% IF config.random_colors %]
672             [% color = random_color() %]
673             [% ELSE %]
674             [% color = predefined_color(loop.count) %]
675             [% END %]
676              
677             .fill[% loop.count %]{
678             fill: [% color %];
679             fill-opacity: 0.2;
680             stroke: none;
681             }
682              
683             .line[% loop.count %]{
684             fill: none;
685             stroke: [% color %];
686             stroke-width: 1px;
687             }
688              
689             .key[% loop.count %],.fill[% loop.count %]{
690             fill: [% color %];
691             stroke: none;
692             stroke-width: 1px;
693             }
694              
695             [% LAST IF (config.random_colors == 0 && loop.count == 12) %]
696             [% END %]
697              
698             .keyText{
699             fill: #000000;
700             text-anchor:start;
701             font-size: 10px;
702             font-family: "Arial", sans-serif;
703             font-weight: normal;
704             }
705             /* End copy for external style sheet */
706             ]]>
707             </style>
708             </defs>
709             [% END %]
710              
711             [% IF config.key %]
712             <!-- Script to toggle paths when their key is clicked on -->
713             <script language="JavaScript"><![CDATA[
714             function togglePath( series ) {
715             var path = document.getElementById('groupDataSeries' + series);
716             var points = document.getElementById('groupDataLabels' + series);
717             var current = path.getAttribute('opacity');
718             if ( path.getAttribute('opacity') == 0 ) {
719             path.setAttribute('opacity',1);
720             points.setAttribute('opacity',1);
721             } else {
722             path.setAttribute('opacity',0);
723             points.setAttribute('opacity',0);
724             }
725             }
726             ]]></script>
727             [% END %]
728              
729             <!-- svg bg -->
730             <rect x="0" y="0" width="[% config.width %]" height="[% config.height %]" class="svgBackground"/>
731              
732             <!-- ///////////////// CALCULATE GRAPH AREA AND BOUNDARIES //////////////// -->
733             [%# get dimensions of actual graph area (NOT SVG area) %]
734             [% w = config.width %]
735             [% h = config.height %]
736              
737             [%# set start/default coords of graph %]
738             [% x = 0 %]
739             [% y = 0 %]
740              
741             [% char_width = 8 %]
742             [% half_char_height = 2.5 %]
743              
744             <!-- min_y [% calc.min_y %] max_y [% calc.max_y %] min_x [% calc.min_x %] max_x [% calc.max_x %] -->
745              
746             <!-- CALC HEIGHT AND Y COORD DIMENSIONS -->
747             [%# reduce height of graph area if there is labelling on x axis %]
748             [% IF config.show_x_labels %][% h = h - 20 %][% END %]
749              
750             [%# reduce height if x labels are rotated %]
751             [% x_label_allowance = 0 %]
752             [% IF config.rotate_x_labels %]
753             [% x_label_allowance = (calc.max_x_label_length * char_width) - 20 %]
754             [% h = h - x_label_allowance %]
755             [% END %]
756              
757             [%# stagger x labels if overlapping occurs %]
758             [% stagger = 0 %]
759             [% IF config.show_x_labels && config.stagger_x_labels %]
760             [% stagger = 17 %]
761             [% h = h - stagger %]
762             [% END %]
763              
764             [% IF config.show_x_title %][% h = h - 25 - stagger %][% END %]
765              
766             [%# pad top of graph if y axis has data labels so labels do not get chopped off %]
767             [% IF config.show_y_labels %][% h = h - 10 %][% y = y + 10 %][% END %]
768              
769             [%# reduce height if graph has title or subtitle %]
770             [% IF config.show_graph_title %][% h = h - 25 %][% y = y + 25 %][% END %]
771             [% IF config.show_graph_subtitle %][% h = h - 10 %][% y = y + 10 %][% END %]
772              
773             [%# reduce graph dimensions if there is a KEY %]
774             [% key_box_size = 12 %]
775             [% key_padding = 5 %]
776              
777             [% IF config.key && config.key_position == 'right' %]
778             [% w = w - (calc.max_key_size * (char_width - 1)) - (key_box_size * 3 ) %]
779             [% ELSIF config.key && config.key_position == 'bottom' %]
780             [% IF data.size < 4 %]
781             [% h = h - ((data.size + 1) * (key_box_size + key_padding))%]
782             [% ELSE %]
783             [% h = h - (4 * (key_box_size + key_padding))%]
784             [% END %]
785             [% END %]
786              
787             <!-- min_yscale_value [% calc.min_yscale_value %] max_yscale_value [% calc.max_yscale_value %] -->
788              
789             [%# base line %]
790             [% base_line = h + y %]
791              
792             [%# find the string length of max value %]
793             [% max_y_length = calc.max_yscale_value.length %]
794              
795             [%# label width in pixels %]
796             [% max_y_length_px = max_y_length * char_width %]
797             [%# If the y labels are shown but the size of the x labels are small, pad for y labels %]
798              
799             <!-- CALC WIDTH AND X COORD DIMENSIONS -->
800             [%# reduce width of graph area if there is large labelling on x axis %]
801             [% space_b4_y_axis = (calc.min_xscale_value.length / 2) * char_width %]
802              
803             [% IF config.show_x_labels %]
804             [% IF config.key && config.key_position == 'right' %]
805             [% w = w - space_b4_y_axis %]
806             [% ELSE %]
807             <!-- pad both sides -->
808             [% w = w - (space_b4_y_axis * 2) %]
809             [% END %]
810             [% x = x + space_b4_y_axis %]
811             [% ELSIF config.show_data_values %]
812             [% w = w - (max_y_length_px * 2) %]
813             [% x = x + max_y_length_px %]
814             [% END %]
815              
816             [% IF config.show_y_labels && space_b4_y_axis < max_y_length_px %]
817             <!-- allow slightly more padding if small labels -->
818             [% IF max_y_length < 2 %]
819             [% w = w - (max_y_length * (char_width * 2)) %]
820             [% x = x + (max_y_length * (char_width * 2)) %]
821             [% ELSE %]
822             [% w = w - max_y_length_px %]
823             [% x = x + max_y_length_px %]
824             [% END %]
825             [% ELSIF config.show_y_labels && !config.show_x_labels %]
826             [% w = w - max_y_length_px %]
827             [% x = x + max_y_length_px %]
828             [% END %]
829              
830             [% IF config.show_y_title %]
831             [% w = w - 25 %]
832             [% x = x + 25 %]
833             [% END %]
834              
835             <!-- min_xscale_value [% calc.min_xscale_value %] max_xscale_value [% calc.max_xscale_value %] -->
836              
837             [%# Missing data spans %]
838             [% max_x_span = 0 %]
839             [% IF config.max_x_span %]
840             [% max_x_span = config.max_x_span %]
841             <!-- max_x_span [% max_x_span %] -->
842             [% END %]
843              
844             <!-- ////////////////////////////// BUILD GRAPH AREA ////////////////////////////// -->
845             [%# graph bg and clipping regions for lines/fill and clip extended to included data labels %]
846             <rect x="[% x %]" y="[% y %]" width="[% w %]" height="[% h %]" class="graphBackground"/>
847             <clipPath id="clipGraphArea">
848             <rect x="[% x %]" y="[% y %]" width="[% w %]" height="[% h %]"/>
849             </clipPath>
850              
851             <!-- axis -->
852             <path d="M[% x %] [% base_line %] h[% w %]" class="axis" id="xAxis"/>
853             <path d="M[% x %] [% y %] v[% h %]" class="axis" id="yAxis"/>
854              
855             <!-- ////////////////////////////// AXIS DISTRIBUTIONS //////////////////////////// -->
856             <!-- x axis scaling -->
857             [% dx = calc.xscale_range %]
858             [% IF dx == 0 %]
859             [% dx = 1 %]
860             [% END %]
861             [% dw = w / dx %]
862             <!-- dx [% dx %] dw [% dw %] -->
863              
864             <!-- x axis labels -->
865             [% IF config.show_x_labels %]
866             [% x_value_txt = config.x_label_formatter(calc.min_xscale_value) %]
867             <text x="[% x %]" y="[% base_line + 15 %]" [% IF config.rotate_x_labels %] transform="rotate(90 [% x - half_char_height %] [% base_line + 15 %]) translate(-10,0)" style="text-anchor: start" [% END %] class="xAxisLabels">[% x_value_txt %]</text>
868             [% last_label = calc.min_xscale_value %]
869              
870             [% IF config.xscale_divisions %]
871              
872             [% xscale_division = config.xscale_divisions %]
873             [% x_value = calc.min_xscale_value + xscale_division %]
874              
875             [% count = 0 %]
876             [% WHILE ((x_value > calc.min_xscale_value) && ((x_value < calc.max_xscale_value))) %]
877             [% x_value_txt = config.x_label_formatter(x_value) %]
878             [% xpos = (dw * (x_value - calc.min_xscale_value)) + x %]
879             [% IF (config.stagger_x_labels && ((count % 2) == 0)) %]
880             <path d="M[% xpos %] [% base_line %] v[% stagger %]" class="staggerGuideLine" />
881             <text x="[% xpos %]" y="[% base_line + 15 + stagger %]" [% IF config.rotate_x_labels %] transform="rotate(90 [% xpos - half_char_height %] [% base_line + 15 + stagger %]) translate(-10,0)" style="text-anchor: start" [% END %] class="xAxisLabels">[% x_value_txt %]</text>
882             [% ELSE %]
883             <text x="[% xpos %]" y="[% base_line + 15 %]" [% IF config.rotate_x_labels %] transform="rotate(90 [% xpos - half_char_height %] [% base_line + 15 %]) translate(-10,0)" style="text-anchor: start" [% END %] class="xAxisLabels">[% x_value_txt %]</text>
884             [% END %]
885             [% last_label = x_value %]
886             [% x_value = x_value + xscale_division %]
887             [% count = count + 1 %]
888             [% LAST IF (count >= 999) %]
889             [% END %]
890              
891             [% END %]
892              
893             [% IF calc.max_xscale_value != last_label %]
894             [% x_value_txt = config.x_label_formatter(calc.max_xscale_value) %]
895             [% IF (config.stagger_x_labels && ((count % 2) == 0)) %]
896             <path d="M[% x + w %] [% base_line %] v[% stagger %]" class="staggerGuideLine" />
897             <text x="[% x + w %]" y="[% base_line + 15 + stagger %]" [% IF config.rotate_x_labels %] transform="rotate(90 [% x + w - half_char_height %] [% base_line + 15 + stagger %]) translate(-10,0)" style="text-anchor: start" [% END %] class="xAxisLabels">[% x_value_txt %]</text>
898             [% ELSE %]
899             <text x="[% x + w %]" y="[% base_line + 15 %]" [% IF config.rotate_x_labels %] transform="rotate(90 [% x + w - half_char_height %] [% base_line + 15 %]) translate(-10,0)" style="text-anchor: start" [% END %] class="xAxisLabels">[% x_value_txt %]</text>
900             [% END %]
901             [% END %]
902             [% END %]
903              
904             <!-- y axis scaling -->
905             [%# how much padding between largest bar and top of graph %]
906             [% top_pad = h / 40 %]
907              
908             [% dy = calc.yscale_range %]
909             [% IF dy == 0 %]
910             [% dy = 1 %]
911             [% END %]
912             [% dh = (h - top_pad) / dy %]
913             <!-- dy [% dy %] dh [% dh %] yscale_division [% calc.yscale_division %] max_yscale_value [% calc.max_yscale_value %]-->
914              
915             [% count = 0 %]
916             [% last_label = '' %]
917             [% IF (calc.min_yscale_value > calc.max_yscale_value) %]
918             <!-- Reversed y range -->
919             [% y_value = calc.max_yscale_value %]
920             [% reversed = 1 %]
921             [% ELSE %]
922             [% y_value = calc.min_yscale_value %]
923             [% reversed = 0 %]
924             [% END %]
925             [% IF config.show_y_labels %]
926             [% WHILE ((y_value == calc.min_yscale_value) || (y_value == calc.max_yscale_value) || ((y_value > calc.min_yscale_value) && (y_value < calc.max_yscale_value)) || ((y_value > calc.max_yscale_value) && (y_value < calc.min_yscale_value) && reversed )) %]
927             [%- next_label = y_value FILTER format(calc.y_label_format) -%]
928             [%- next_label = config.y_label_formatter(next_label) -%]
929             [%- IF ((count == 0) && (reversed == 0)) -%]
930             [%# no stroke for first line unless reversed %]
931             <text x="[% x - 5 %]" y="[% base_line - (dh * (y_value - calc.min_yscale_value)) %]" class="yAxisLabels">[% next_label %]</text>
932             [%- ELSE -%]
933             [% IF next_label != last_label %]
934             <text x="[% x - 5 %]" y="[% base_line - (dh * (y_value - calc.min_yscale_value)) %]" class="yAxisLabels">[% next_label %]</text>
935             <path d="M[% x %] [% base_line - (dh * (y_value - calc.min_yscale_value)) %] h[% w %]" class="guideLines"/>
936             [% END %]
937             [%- END -%]
938             [%- y_value = y_value + calc.yscale_division -%]
939             [%- last_label = next_label -%]
940             [%- count = count + 1 -%]
941             [%- LAST IF (count >= 999) -%]
942             [% END %]
943             [% END %]
944              
945             <!-- ////////////////////////////// AXIS TITLES ////////////////////////////// -->
946             <!-- x axis title -->
947             [% IF config.show_x_title %]
948             [% IF !config.show_x_labels %]
949             [% y_xtitle = 15 %]
950             [% ELSE %]
951             [% y_xtitle = 35 %]
952             [% END %]
953             <text x="[% (w / 2) + x %]" y="[% h + y + y_xtitle + stagger + x_label_allowance %]" class="xAxisTitle">[% config.x_title %]</text>
954             [% END %]
955              
956             <!-- y axis title -->
957             [% IF config.show_y_title %]
958             <text x="10" y="[% (h / 2) + y %]" transform="rotate(270,10,[% (h / 2) + y %])" class="yAxisTitle">[% config.y_title %]</text>
959             [% END %]
960              
961             <!-- ////////////////////////////// SHOW DATA ////////////////////////////// -->
962             [% line = data.size %]
963             <g id="groupData" class="data">
964             [% FOREACH dataset = data.reverse %]
965             <g id="groupDataSeries[% line %]" class="dataSeries[% line %]" clip-path="url(#clipGraphArea)">
966             [% IF config.area_fill %]
967             [%# create alternate fill first (so line can overwrite if necessary) %]
968             [% xcount = 0 %]
969             [% FOREACH pair = dataset.pairs %]
970             [%- IF ((pair.0 >= calc.min_xscale_value) && (pair.0 <= calc.max_xscale_value)) -%]
971             [%- IF xcount == 0 -%][% lastx = pair.0 %]<path d="M[% (dw * (pair.0 - calc.min_xscale_value)) + x %] [% base_line %][%- END -%]
972             [%- IF ((max_x_span) && (pair.0 > lastx + max_x_span)) -%]
973             V [% base_line %] H [% (dw * (pair.0 - calc.min_xscale_value)) + x %] V [% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]
974             [%- ELSE -%]
975             L [% (dw * (pair.0 - calc.min_xscale_value)) + x %] [% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]
976             [%- END -%]
977             [%- lastx = pair.0 -%][%- xcount = xcount + 1 -%]
978             [%- END -%]
979             [% END %]
980             [% IF xcount > 0 %] V [% base_line %] Z" class="fill[% line %]"/> [% END %]
981             [% END %]
982              
983             <!--- create line [% dataset.title %]-->
984             [% xcount = 0 %]
985             [% FOREACH pair = dataset.pairs %]
986             [% IF ((pair.0 >= calc.min_xscale_value) && (pair.0 <= calc.max_xscale_value)) %]
987             [%- IF xcount == 0 -%][%- lastx = pair.0 -%]<path d="M
988             [% (dw * (pair.0 - calc.min_xscale_value)) + x %] [% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]
989             [%- ELSE -%]
990             [%- IF ((max_x_span) && (pair.0 > lastx + max_x_span)) -%]
991             M [% (dw * (pair.0 - calc.min_xscale_value)) + x %] [% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]
992             [%- ELSE -%]
993             L [% (dw * (pair.0 - calc.min_xscale_value)) + x %] [% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]
994             [%- END -%]
995             [%- END -%]
996             [%- lastx = pair.0 -%][%- xcount = xcount + 1 -%]
997             [%- END -%]
998             [% END %]
999             [% IF xcount > 0 %] " class="line[% line %]"/> [% END %]
1000             </g>
1001             <g id="groupDataLabels[% line %]" class="dataLabels[% line %]">
1002             [% IF config.show_data_points || config.show_data_values %]
1003             [% FOREACH pair = dataset.pairs %]
1004             [% IF ((pair.0 >= calc.min_xscale_value) && (pair.0 <= calc.max_xscale_value)) %]
1005             <g class="dataLabel[% line %]" [% IF config.rollover_values %] opacity="0" [% END %]>
1006             [% IF config.show_data_points %]
1007             <circle cx="[% (dw * (pair.0 - calc.min_xscale_value)) + x %]" cy="[% base_line - (dh * (pair.1 - calc.min_yscale_value)) %]" r="2.5" class="dataPoint[% line %]"
1008             [% IF config.rollover_values %]
1009             onmouseover="evt.target.parentNode.setAttribute('opacity',1);"
1010             onmouseout="evt.target.parentNode.setAttribute('opacity',0);"
1011             [% END %]
1012             [% IF pair.3.defined %]
1013             onclick="[% pair.3 %]"
1014             [% END %]
1015             ></circle>
1016             [% END %]
1017             [% IF config.show_data_values %]
1018             [%# datavalue shown %]
1019             [% IF (pair.2.defined) && (pair.2 != '') %][% point_label = pair.2 %][% ELSE %][% point_label = pair.1 FILTER format(calc.data_value_format) %][% END %]
1020             <text x="[% (dw * (pair.0 - calc.min_xscale_value)) + x %]" y="[% base_line - (dh * (pair.1 - calc.min_yscale_value)) - 6 %]" class="dataPointLabel[% line %]"
1021             [% IF config.rollover_values %]
1022             onmouseover="evt.target.parentNode.setAttribute('opacity',1);"
1023             onmouseout="evt.target.parentNode.setAttribute('opacity',0);"
1024             [% END %]
1025             >[% point_label %]</text>
1026             [% END %]
1027             </g>
1028             [% END %]
1029             [% END %]
1030             [% END %]
1031             </g>
1032             [% line = line - 1 %]
1033             [% END %]
1034             </g>
1035              
1036             <!-- //////////////////////////////////// KEY ////////////////////////////// -->
1037             [% key_count = 1 %]
1038             [% IF config.key && config.key_position == 'right' %]
1039             [% FOREACH dataset = data %]
1040             <rect x="[% x + w + 20 %]" y="[% y + (key_box_size * key_count) + (key_count * key_padding) %]" width="[% key_box_size %]" height="[% key_box_size %]" class="key[% key_count %]" onclick="togglePath([% key_count %]);"/>
1041             <text x="[% x + w + 20 + key_box_size + key_padding %]" y="[% y + (key_box_size * key_count) + (key_count * key_padding) + key_box_size %]" class="keyText">[% dataset.title %]</text>
1042             [% key_count = key_count + 1 %]
1043             [% END %]
1044             [% ELSIF config.key && config.key_position == 'bottom' %]
1045             [%# calc y position of start of key %]
1046             [% y_key = base_line %]
1047             [%# consider x title %]
1048             [% IF config.show_x_title %][% y_key = base_line + 25 %][% END %]
1049             [%# consider x label rotation and stagger %]
1050             [% IF config.rotate_x_labels && config.show_x_labels %]
1051             [% y_key = y_key + x_label_allowance %]
1052             [% ELSIF config.show_x_labels && stagger < 1 %]
1053             [% y_key = y_key + 20 %]
1054             [% END %]
1055              
1056             [% y_key_start = y_key %]
1057             [% x_key = x %]
1058             [% FOREACH dataset = data %]
1059             [% IF key_count == 4 || key_count == 7 || key_count == 10 %]
1060             [%# wrap key every 3 entries %]
1061             [% x_key = x_key + 200 %]
1062             [% y_key = y_key - (key_box_size * 4) - 2 %]
1063             [% END %]
1064             <rect x="[% x_key %]" y="[% y_key + (key_box_size * key_count) + (key_count * key_padding) + stagger %]" width="[% key_box_size %]" height="[% key_box_size %]" class="key[% key_count %]" onclick="togglePath([% key_count %]);"/>
1065             <text x="[% x_key + key_box_size + key_padding %]" y="[% y_key + (key_box_size * key_count) + (key_count * key_padding) + key_box_size + stagger %]" class="keyText">[% dataset.title %]</text>
1066             [% key_count = key_count + 1 %]
1067             [% END %]
1068              
1069             [% END %]
1070              
1071             <!-- //////////////////////////////// MAIN TITLES ////////////////////////// -->
1072             <!-- main graph title -->
1073             [% IF config.show_graph_title %]
1074             <text x="[% config.width / 2 %]" y="15" class="mainTitle">[% config.graph_title %]</text>
1075             [% END %]
1076              
1077             <!-- graph sub title -->
1078             [% IF config.show_graph_subtitle %]
1079             [% IF config.show_graph_title %]
1080             [% y_subtitle = 30 %]
1081             [% ELSE %]
1082             [% y_subtitle = 15 %]
1083             [% END %]
1084             <text x="[% config.width / 2 %]" y="[% y_subtitle %]" class="subTitle">[% config.graph_subtitle %]</text>
1085             [% END %]
1086             </svg>