File Coverage

blib/lib/App/Chart/Gtk2/Graph.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


line stmt bran cond sub pod time code
1             # Graph widget.
2              
3             # Copyright 2007, 2008, 2009, 2010, 2011, 2013 Kevin Ryde
4              
5             # This file is part of Chart.
6             #
7             # Chart is free software; you can redistribute it and/or modify it under the
8             # terms of the GNU General Public License as published by the Free Software
9             # Foundation; either version 3, or (at your option) any later version.
10             #
11             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
12             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14             # details.
15             #
16             # You should have received a copy of the GNU General Public License along
17             # with Chart. If not, see <http://www.gnu.org/licenses/>.
18              
19             package App::Chart::Gtk2::Graph;
20 1     1   383 use 5.010;
  1         3  
21 1     1   5 use strict;
  1         2  
  1         16  
22 1     1   4 use warnings;
  1         2  
  1         23  
23 1     1   138 use Gtk2 1.220;
  0            
  0            
24             use List::Util qw(min max);
25             use List::MoreUtils 0.24; # version 0.24 for bug fixes
26             use Module::Load;
27             use POSIX ();
28             use Set::IntSpan::Fast 1.10; # 1.10 for contains_all_range()
29             use Locale::Messages;
30             use Locale::TextDomain ('App-Chart');
31              
32             # alerts and lines last since they're xor based, umm, maybe
33             use Module::Pluggable require => 1;
34             my @plugins = sort __PACKAGE__->plugins;
35             ### Graph plugins: @plugins
36              
37             use Glib::Ex::SignalIds;
38             use Gtk2::Ex::AdjustmentBits 43; # v.43 for set_maybe()
39             use Gtk2::Ex::GdkBits 23; # v.23 for window_clear_region()
40              
41             use App::Chart::Glib::Ex::MoreUtils;
42             use App::Chart::Gtk2::GUI;
43             use App::Chart::Series;
44             use App::Chart::Gtk2::Graph::Plugin::Latest;
45              
46             # uncomment this to run the ### lines
47             #use Devel::Comments;
48              
49             use Glib::Object::Subclass
50             'Gtk2::DrawingArea',
51             signals => { button_press_event => \&_do_button_press,
52             expose_event => \&_do_expose_event,
53             # size_allocate => \&_do_size_allocate,
54             scroll_event => \&_do_scroll_event,
55              
56             # GtkToolbar in gtk 2.14.7 has a bug in its finalize
57             # provoking a ref_count>0 log error if anyone is hooked onto
58             # 'parent-set', even if in an unrelated class, so use notify
59             # instead
60             #
61             # parent_set => \&_do_parent_set,
62             #
63             # notify => \&_do_notify,
64              
65             set_scroll_adjustments =>
66             { param_types => ['Gtk2::Adjustment',
67             'Gtk2::Adjustment'],
68             return_type => undef,
69             class_closure => \&_do_set_scroll_adjustments },
70              
71             start_drag => { param_types => ['Glib::Int'],
72             return_type => undef,
73             class_closure => \&_do_start_drag,
74             flags => ['run-first','action'] },
75             start_lasso => { param_types => ['Glib::Int'],
76             return_type => undef,
77             class_closure => \&_do_start_lasso,
78             flags => ['run-first','action'] },
79             start_annotation_drag =>
80             { param_types => ['Glib::Int'],
81             return_type => undef,
82             class_closure => \&_do_start_annotation_drag,
83             flags => ['run-first','action'] },
84              
85             },
86             properties => [Glib::ParamSpec->object
87             ('hadjustment',
88             Locale::Messages::dgettext('gtk20-properties',
89             'Horizontal adjustment'), # per Gtk2::ScrolledWindow
90             'Blurb',
91             'Gtk2::Adjustment',
92             Glib::G_PARAM_READWRITE),
93              
94             Glib::ParamSpec->object
95             ('vadjustment',
96             Locale::Messages::dgettext('gtk20-properties',
97             'Vertical adjustment'), # per Gtk2::ScrolledWindow
98             'Blurb',
99             'Gtk2::Adjustment',
100             Glib::G_PARAM_READWRITE),
101              
102             Glib::ParamSpec->scalar
103             ('series-list',
104             'Series list',
105             'Arrayref of perl App::Chart::Series objects.',
106             Glib::G_PARAM_READWRITE),
107             ];
108             App::Chart::Gtk2::GUI::chart_style_class (__PACKAGE__);
109              
110             # priority level "gtk" treating this as widget level default, for overriding
111             # by application or user RC
112             Gtk2::Rc->parse_string (<<'HERE');
113             binding "App__Chart__Gtk2__Graph_keys" {
114             bind "<Shift>Pointer_Button1" { "start_lasso" (1) }
115             bind "Pointer_Button1" { "start_drag" (1) }
116             bind "Pointer_Button2" { "start_annotation_drag" (2) }
117             }
118             class "App__Chart__Gtk2__Graph" binding:gtk "App__Chart__Gtk2__Graph_keys"
119             HERE
120              
121              
122             #------------------------------------------------------------------------------
123              
124             sub INIT_INSTANCE {
125             my ($self) = @_;
126             $self->{'series_list'} = [];
127             $self->set_double_buffered (0);
128             $self->add_events (['button-press-mask',
129             'button-motion-mask',
130             'button-release-mask']);
131             $self->{'waiting_initial_allocate'} = 1;
132             }
133              
134             sub SET_PROPERTY {
135             my ($self, $pspec, $newval) = @_;
136             my $pname = $pspec->get_name;
137             ### Graph SET_PROPERTY(): $pname
138             my $oldval = $self->{$pname};
139             $self->{$pname} = $newval; # per default GET_PROPERTY
140              
141             if ($pname eq 'hadjustment') {
142             my $hadj = $newval;
143             my $ref_weak_self = App::Chart::Glib::Ex::MoreUtils::ref_weak($self);
144             $self->{'hadjustment_ids'} = $hadj && Glib::Ex::SignalIds->new
145             ($hadj,
146             $hadj->signal_connect(value_changed => \&_do_hadj_value_changed,
147             $ref_weak_self),
148             $hadj->signal_connect(changed => \&_do_hadj_other_changed,
149             $ref_weak_self));
150              
151             } elsif ($pname eq 'vadjustment') {
152             my $vadj = $newval;
153             my $ref_weak_self = App::Chart::Glib::Ex::MoreUtils::ref_weak($self);
154             $self->{'vadjustment_ids'} = $vadj && Glib::Ex::SignalIds->new
155             ($vadj,
156             $vadj->signal_connect (value_changed => \&_do_vadj_changed,
157             $ref_weak_self),
158             $vadj->signal_connect (changed => \&_do_vadj_changed,
159             $ref_weak_self));
160              
161             } elsif ($pname eq 'series_list') {
162             # initial page size and position when going from empty to non-empty
163             ### Graph set series_list, count: scalar(@$newval)
164             if (@$newval && ! @$oldval) {
165             $self->initial_scale;
166             }
167             $self->queue_draw;
168             }
169             }
170              
171             # sub _do_parent_set {
172             # my ($self, $parent) = @_;
173             # if (! $parent) {
174             # $self->{'waiting_initial_allocate'} = 1;
175             # }
176             # $self->signal_chain_from_overridden ($parent);
177             # }
178              
179             # # 'size-allocate' class closure
180             # sub _do_size_allocate {
181             # my ($self, $alloc) = @_;
182             # ### Graph _do_size_allocate(): $alloc->width."x".$alloc->height
183             # $self->signal_chain_from_overridden($alloc);
184             #
185             # # after superclass has set $alloc into $self->allocation
186             # if ($alloc->width != 1 && $alloc->height != 1) {
187             # if (delete $self->{'waiting_initial_allocate'}) {
188             # _initial_scale ($self);
189             # }
190             # }
191             # }
192             #
193             # sub _do_notify {
194             # my ($self, $pspec) = @_;
195             # if ($pspec->get_name eq 'parent') {
196             # if (! $self->get_parent) {
197             # $self->{'waiting_initial_allocate'} = 1;
198             # }
199             # }
200             # return shift->signal_chain_from_overridden(@_);
201             # }
202              
203             sub initial_scale {
204             my ($self) = @_;
205             $self->{'initial_scale'} = 1;
206             }
207              
208             sub _initial_scale {
209             my ($self) = @_;
210             ### Graph _initial_scale(): "$self"
211              
212             # {
213             # my $alloc = $self->allocation;
214             # ### Graph _initial_scale() size: $alloc->width."x".$alloc->height
215             # return if ($alloc->width == 1 || $alloc->height == 1);
216             # }
217              
218             my $series_list = $self->{'series_list'};
219             my $series = $series_list->[0] || do {
220             ### no series...
221             return;
222             };
223              
224             my $hadj = $self->{'hadjustment'};
225             my $vadj = $self->{'vadjustment'};
226             my ($lo, $hi) = $hadj->value_range_inc;
227             ### hadj: "$lo $hi, on ".ref($series)
228              
229             if (my ($p_lo, $p_hi) = $series->initial_range ($lo, $hi)) {
230             ### series initial_range(): ($p_lo//'undef')." to ".($p_hi//'undef')." from $lo to $hi on ".ref($series)
231             ($p_lo, $p_hi) = stretch_range ($p_lo, $p_hi);
232             ### stretched to: ($p_lo//'undef')." to ".($p_hi//'undef')
233             if (defined $p_lo) {
234             $vadj->set_page_range ($p_lo, $p_hi);
235             }
236             $self->queue_draw;
237             }
238             # expanding on initial ...
239             $self->{'vrange_span'} = undef;
240             update_v_range ($self);
241             }
242              
243             # 'set-scroll-adjustments' class closure
244             sub _do_set_scroll_adjustments {
245             my ($self, $hadj, $vadj) = @_;
246             $self->set (hadjustment => $hadj,
247             vadjustment => $vadj);
248             }
249              
250             sub scale_x_step {
251             my ($self) = @_;
252             return $self->{'hadjustment'}->get_pixel_per_value;
253             }
254              
255             sub scale_x {
256             my ($self, $t) = @_;
257             return $self->{'hadjustment'}->value_to_pixel ($t);
258             }
259              
260             sub scale_x_proc {
261             my ($self) = @_;
262             return $self->{'hadjustment'}->value_to_pixel_proc;
263             }
264              
265             sub x_to_date {
266             my ($self, $x) = @_;
267             return POSIX::floor ($self->{'hadjustment'}->pixel_to_value ($x));
268             }
269              
270             sub scale_y {
271             my ($self, $y) = @_;
272             return $self->{'vadjustment'}->value_to_pixel ($y);
273             }
274              
275             sub scale_y_proc {
276             my ($self) = @_;
277             return $self->{'vadjustment'}->value_to_pixel_proc;
278              
279             # my $price_lo = $self->{'vadjustment'}->get_value;
280             # my $price_height = $self->{'vadjustment'}->page_size;
281             # if ($price_height == 0) { $price_height = 1; }
282             # my ($win_width, $win_height) = $self->window->get_size();
283             # my $factor = $win_height / $price_height;
284             #
285             # return sub {
286             # my ($price) = @_;
287             # return $win_height - $factor * ($price - $price_lo);
288             # };
289             }
290              
291             sub y_to_value {
292             my ($self, $y) = @_;
293             return $self->{'vadjustment'}->pixel_to_value ($y);
294              
295             # my $win_height = $self->allocation->height;
296             # my $vadj = $self->{'vadjustment'};
297             # my $factor = $vadj->value
298             # + ($win_height - $y) * $vadj->page_size / $win_height;
299             }
300              
301             sub draw_t_range {
302             my ($self) = @_;
303             my $hadj = $self->{'hadjustment'} || return (0, -1);
304             my ($lo, $hi) = $hadj->value_range_inc;
305             $lo = max (0, $lo);
306             return ($lo, $hi);
307             }
308              
309             # 'expose' class closure
310             sub _do_expose_event {
311             my ($self, $event) = @_;
312             ### Graph _do_expose_event()
313              
314             if (delete $self->{'initial_scale'}) {
315             _initial_scale ($self);
316             $self->queue_draw;
317             return Gtk2::EVENT_PROPAGATE;
318             }
319              
320             my $series_list = $self->get('series_list');
321             if (! $self->{'vadjustment'}) { return Gtk2::EVENT_PROPAGATE; }
322              
323             my $region = $event->region;
324             Gtk2::Ex::GdkBits::window_clear_region ($self->window, $region);
325              
326             if (! @$series_list) {
327             App::Chart::Gtk2::GUI::draw_text_centred
328             ($self, $event, __('Use File/Open to open or add a symbol'));
329              
330             } else {
331             _draw_region ($self, $region);
332             }
333             return Gtk2::EVENT_PROPAGATE;
334             }
335              
336             sub _draw_region {
337             my ($self, $region) = @_;
338              
339             my $series_list = $self->get('series_list');
340             my $any = 0;
341             foreach my $series (@$series_list) {
342             ### Graph draw_region linestyle: $series->linestyle_class
343             my $class = $series->linestyle_class // next;
344             Module::Load::load ($class);
345             $any |= $class->draw ($self, $series, $region);
346             }
347             if (! $any) {
348             App::Chart::Gtk2::GUI::draw_text_centred ($self, $region, __('no data'));
349             }
350              
351             foreach my $class (@plugins) {
352             $class->draw ($self, $region);
353             }
354             }
355              
356             # 'changed' and 'value-changed' signals on vadjustment
357             sub _do_vadj_changed {
358             my ($adj, $ref_weak_self) = @_;
359             my $self = $$ref_weak_self || return;
360             ### Graph vadj changed, redraw: "value=@{[$adj->value]} upper=@{[$adj->upper]} lower=@{[$adj->lower]}"
361             # if ($adj->value < 0) {
362             # require Devel::StackTrace;
363             # my $trace = Devel::StackTrace->new;
364             # print $trace->as_string; # like carp
365             # }
366              
367             $self->queue_draw;
368             }
369              
370             # Expand $adj so its lower/upper covers all of @values.
371             #
372             # If lower==upper==page_size==0 in the existing $adj settings it's treated
373             # as uninitialized and that 0 lower/upper is ignored, just @values is used.
374             #
375             # undefs in @values are ignored, and if all are undef then $adj is not
376             # changed.
377             #
378             sub adjustment_expand {
379             my ($adj, @values) = @_;
380             @values = grep {defined} @values;
381             ### Graph adjustment_expand(): join(' ',@values)
382             if (! @values) { return; }
383              
384             my ($new_lower, $new_upper) = List::MoreUtils::minmax (@values);
385             ### base: " new_lower $new_lower new_upper $new_upper"
386             ($new_lower, $new_upper) = stretch_range ($new_lower, $new_upper);
387             ### stretch: "new_lower $new_lower new_upper $new_upper"
388              
389             my $old_lower = $adj->lower;
390             my $old_upper = $adj->upper;
391             ### old: " old_lower $old_lower old_upper $old_upper old_page ".$adj->page_size
392             if (! ($old_lower == 0 && $old_upper == 0 && $adj->page_size == 0)) {
393             ($new_lower, $new_upper) = List::MoreUtils::minmax
394             ($new_lower, $new_upper, $old_lower, $old_upper);
395             }
396              
397             ### new: " new_lower $new_lower new_upper $new_upper"
398             Gtk2::Ex::AdjustmentBits::set_maybe
399             ($adj,
400             lower => $new_lower,
401             upper => $new_upper);
402             }
403              
404             sub stretch_range {
405             my ($lo, $hi) = @_;
406             my $extra = ($hi - $lo) * 0.1;
407             if ($lo < 0) {
408             $lo -= $extra;
409             } else {
410             $lo = max ($lo - $extra, $lo * 0.5);
411             }
412             $hi += $extra;
413             return ($lo, $hi);
414             }
415              
416             sub update_v_range {
417             my ($self) = @_;
418             my $vadj = $self->{'vadjustment'} || return;
419              
420             my ($lo, $hi) = $self->draw_t_range;
421             my $vrange_span = ($self->{'vrange_span'} ||= do {
422             require Set::IntSpan::Fast;
423             Set::IntSpan::Fast->new
424             });
425             if ($vrange_span->contains_all_range ($lo, $hi)) { return; }
426              
427             ### Graph update_v_range for: "$lo to $hi"
428             my $series_list = $self->{'series_list'};
429             adjustment_expand ($vadj,
430             (map {
431             $_->vrange ($self, $series_list);
432             } @plugins),
433             (map {
434             _series_v_range($_, $lo, $hi)
435             } @$series_list));
436             $vrange_span->add_range ($lo, $hi);
437             }
438             sub _series_v_range {
439             my ($series, $lo, $hi) = @_;
440             my @ret;
441             my $min = $series->minimum; push @ret, $min;
442             my $max = $series->maximum; push @ret, $max;
443             $series->fill ($lo, $hi);
444              
445             foreach my $p ($series->filled_low, $series->filled_high) {
446             $p // next;
447             push @ret, $p;
448             # foreach my $w ($p * 1.1, $p / 1.1) {
449             # if (defined $min) { $w = max ($w, $min); }
450             # if (defined $max) { $w = min ($w, $max); }
451             # push @ret, $w;
452             # }
453             }
454             return @ret;
455             }
456              
457             # 'value-changed' signal on hadjustment
458             sub _do_hadj_value_changed {
459             my ($adj, $ref_weak_self) = @_;
460             my $self = $$ref_weak_self || return;
461             ### Graph hadj changed, v_range and redraw: "value=@{[$adj->value]} upper=@{[$adj->upper]} lower=@{[$adj->lower]}"
462              
463             update_v_range ($self);
464             $self->queue_draw;
465             }
466              
467             # 'changed' signal on hadjustment
468             *_do_hadj_other_changed = \&_do_hadj_value_changed;
469             # my ($adj, $ref_weak_self) = @_;
470             # my $self = $$ref_weak_self || return;
471             # update_v_range ($self);
472             # $self->queue_draw;
473             # }
474              
475             # 'button-press-event' class closure
476             sub _do_button_press {
477             my ($self, $event) = @_;
478             ### Graph _do_button_press(): $event->button
479             require App::Chart::Gtk2::Ex::BindingBits;
480             App::Chart::Gtk2::Ex::BindingBits::activate_button_event
481             ('App__Chart__Gtk2__Graph_keys', $event, $self);
482             return shift->signal_chain_from_overridden(@_);
483             }
484              
485             sub centre {
486             my ($self) = @_;
487             ### Graph centre()
488             my $vadj = $self->{'vadjustment'};
489             my $page = $vadj->page_size * 0.9; # gap at ends
490             my $series_list = $self->{'series_list'};
491              
492             my ($lo, $hi) = $self->draw_t_range;
493             ### Graph centre() on drawn: "$lo $hi (of ".$self->{'hadjustment'}->lower." ".$self->{'hadjustment'}->upper.")"
494              
495             my ($l, $h);
496             my $accumulate = sub {
497             my ($value) = @_;
498             ### accumulate: $value
499             if (! defined $value) { return 1; }
500             if (! defined $l) { $l = $h = $value; return 1; }
501              
502             my $new_l = min ($l, $value);
503             my $new_h = max ($h, $value);
504             my $new_page = $new_h - $new_l;
505             if ($new_page <= $page) {
506             $l = $new_l;
507             $h = $new_h;
508             return 1;
509             }
510              
511             if ($new_l < $l) {
512             $l = $h - $page;
513             } else {
514             $h = $l + $page;
515             }
516             return 0;
517             };
518              
519             if (my $series = $series_list->[0]) {
520             if (defined (my $symbol = $series->symbol)) {
521             my ($latest_lo,$latest_hi)
522             = App::Chart::Gtk2::Graph::Plugin::Latest->hrange ($self, $series_list);
523             ### latest hrange: $latest_lo,$latest_hi
524             if (defined $lo
525             && App::Chart::overlap_inclusive_p ($lo,$hi,
526             $latest_lo,$latest_hi)) {
527             my $latest = App::Chart::Latest->get($symbol);
528             if ($series->isa('App::Chart::Series::Derived::Volume')) {
529             $accumulate->($latest->{'volume'});
530             } else {
531             $accumulate->($latest->{'last'})
532             and $accumulate->($latest->{'bid'})
533             and $accumulate->($latest->{'offer'})
534             and $accumulate->($latest->{'high'})
535             and $accumulate->($latest->{'low'});
536             }
537             }
538             }
539             }
540              
541             my @arrays;
542             foreach my $series (@$series_list) {
543             $series->fill ($lo, $hi);
544             my $values = $series->values_array;
545             push @arrays, $values;
546             if (my $highs = $series->array('highs')) {
547             if ($highs != $values) { push @arrays, $highs; }
548             }
549             if (my $lows = $series->array('lows')) {
550             if ($lows != $values) { push @arrays, $lows; }
551             }
552             }
553              
554             OUTER: for (my $i = $hi; $i >= $lo; $i--) {
555             foreach my $array (@arrays) {
556             $accumulate->($array->[$i])
557             or last OUTER;
558             }
559             }
560             if (! defined $l) { return; }
561             ### decided: "$l to $h"
562              
563             my $extra = $page - ($h - $l);
564             $l -= $extra / 2;
565             ### expand to: "low $l on page $page"
566             $vadj->set_value ($l);
567             }
568              
569              
570             #------------------------------------------------------------------------------
571             # scrolling
572              
573             # 'scroll-event' class closure
574             sub _do_scroll_event {
575             my ($self, $event) = @_;
576             ### Graph _do_scroll_event(): "$self->{'hadjustment'}, $self->{'vadjustment'}"
577              
578             my $direction = $event->direction;
579             if ($direction eq 'up') { $self->{'vadjustment'}->scroll_step(1); }
580             elsif ($direction eq 'down') { $self->{'vadjustment'}->scroll_step(-1); }
581             elsif ($direction eq 'left') { $self->{'hadjustment'}->scroll_step(1); }
582             elsif ($direction eq 'right') { $self->{'hadjustment'}->scroll_step(-1); }
583              
584             return $self->signal_chain_from_overridden ($event);
585             }
586              
587              
588             #------------------------------------------------------------------------------
589             # action signal handlers
590              
591             sub _do_start_drag {
592             my ($self, $button) = @_;
593             my $hadj = $self->{'hadjustment'} || return; # only when adj set
594             my $vadj = $self->{'vadjustment'} || return; # only when adj set
595             my $dragger = ($self->{'dragger'} ||= do {
596             require Gtk2::Ex::Dragger;
597             Gtk2::Ex::Dragger->new (widget => $self,
598             hadjustment => $hadj,
599             vadjustment => $vadj,
600             vinverted => 1,
601             confine => 1,
602             cursor => 'fleur')
603             });
604             $dragger->start (Gtk2->get_current_event);
605             }
606              
607             sub _do_start_lasso {
608             my ($self, $button) = @_;
609             my $lasso = ($self->{'lasso'} ||= do {
610             require Gtk2::Ex::Lasso;
611             my $l = Gtk2::Ex::Lasso->new (widget => $self);
612             $l->signal_connect (ended => \&_do_lasso_ended);
613             $l
614             });
615             $lasso->start (Gtk2->get_current_event);
616             }
617             sub _do_lasso_ended {
618             my ($lasso, $x1,$y1, $x2,$y2) = @_;
619             my $self = $lasso->get('widget') || return;
620              
621             my $hadj = $self->{'hadjustment'};
622             my $t1 = $self->x_to_date ($x1);
623             my $t2 = $self->x_to_date ($x2);
624             $hadj->set_value_range (min($t1,$t2), max($t1,$t2));
625              
626             my $vadj = $self->{'vadjustment'};
627             my $p1 = $self->y_to_value ($y1);
628             my $p2 = $self->y_to_value ($y2);
629             $vadj->set_value_range (min($p1,$p2), max($p1,$p2));
630             }
631              
632             sub _do_start_annotation_drag {
633             my ($self, $button) = @_;
634             require App::Chart::Gtk2::AnnDrag;
635             App::Chart::Gtk2::AnnDrag::start ($self, Gtk2->get_current_event);
636             }
637              
638              
639             1;
640             __END__
641              
642             =head1 NAME
643              
644             App::Chart::Gtk2::Graph -- graph widget
645              
646             =for test_synopsis my ($series1, $series2)
647              
648             =head1 SYNOPSIS
649              
650             use App::Chart::Gtk2::Graph;
651             my $image = App::Chart::Gtk2::Graph->new();
652             $image->set('series_list', [ $series1, $series2 ]);
653              
654             =head1 DESCRIPTION
655              
656             A App::Chart::Gtk2::Graph widget displays a graph of a set of
657             L<App::Chart::Series> objects.
658              
659             =head1 FUNCTIONS
660              
661             =over 4
662              
663             =item C<< $graph->centre() >>
664              
665             ...
666              
667             =back
668              
669             =head1 PROPERTIES
670              
671             =over 4
672              
673             =item C<series_list> (arrayref)
674              
675             A reference to an array of C<App::Chart::Series> objects to display.
676              
677             =back
678              
679             =head1 SEE ALSO
680              
681             L<App::Chart::Series>
682              
683             =head1 HOME PAGE
684              
685             L<http://user42.tuxfamily.org/chart/index.html>
686              
687             =head1 LICENCE
688              
689             Copyright 2007, 2008, 2009, 2010, 2011, 2013 Kevin Ryde
690              
691             Chart is free software; you can redistribute it and/or modify it under the
692             terms of the GNU General Public License as published by the Free Software
693             Foundation; either version 3, or (at your option) any later version.
694              
695             Chart is distributed in the hope that it will be useful, but WITHOUT ANY
696             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
697             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
698             details.
699              
700             You should have received a copy of the GNU General Public License along with
701             Chart; see the file F<COPYING>. Failing that, see
702             L<http://www.gnu.org/licenses/>.
703              
704             =cut