File Coverage

blib/lib/App/Chart/Gtk2/View.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2012 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17             package App::Chart::Gtk2::View;
18 1     1   588 use 5.010;
  1         5  
19 1     1   7 use strict;
  1         3  
  1         32  
20 1     1   8 use warnings;
  1         2  
  1         35  
21 1     1   7 use Carp;
  1         3  
  1         72  
22 1     1   220 use Glib;
  0            
  0            
23             use Glib::Ex::ConnectProperties;
24             use Gtk2 1.220;
25             use Gtk2::Ex::AdjustmentBits 43; # v.43 for set_maybe()
26             use List::Util qw(min max);
27             use Locale::TextDomain ('App-Chart');
28              
29             use App::Chart::Glib::Ex::MoreUtils;
30             use App::Chart;
31             use App::Chart::Gtk2::GUI;
32              
33             # uncomment this to run the ### lines
34             #use Smart::Comments;
35              
36             use constant DEFAULT_TIMEBASE_CLASS => 'App::Chart::Timebase::Days';
37              
38             use Glib::Object::Subclass
39             'Gtk2::Table',
40             properties => [Glib::ParamSpec->string
41             ('symbol',
42             __('Symbol'),
43             'The stock or commodity symbol to display, or empty string for none.',
44             '', # default
45             Glib::G_PARAM_READWRITE),
46              
47             Glib::ParamSpec->string
48             ('timebase-class',
49             'timebase-class',
50             'Blurb.',
51             DEFAULT_TIMEBASE_CLASS,
52             Glib::G_PARAM_READWRITE),
53              
54             Glib::ParamSpec->object
55             ('statusbar',
56             'statusbar',
57             'Blurb.',
58             'Gtk2::Statusbar',
59             Glib::G_PARAM_READWRITE),
60              
61             Glib::ParamSpec->scalar
62             ('viewstyle',
63             'viewstyle',
64             'Blurb.',
65             Glib::G_PARAM_READWRITE),
66             ];
67              
68             # FIXME: adjust_splits breaks AnnDrag
69             use constant DEFAULT_VIEWSTYLE =>
70             { adjust_splits => 0,
71             adjust_dividends => 0,
72             adjust_imputation => 1,
73             adjust_rollovers => 0,
74             graphs => [ { size => 4,
75             linestyle => 'Candles',
76             indicators => [{ key => 'SMA', },
77             ],
78             },
79             { size => 1,
80             indicators => [{ key => 'Volume', }
81             ],
82             },
83             ],
84             };
85              
86             sub viewstyle_read {
87             require App::Chart::DBI;
88             my $str = App::Chart::DBI->read_single
89             ('SELECT value FROM preference WHERE key=\'viewstyle\'');
90             if (! defined $str) { return DEFAULT_VIEWSTYLE; }
91             my $viewstyle = eval $str;
92             if (! defined $viewstyle) {
93             print "chart: oops, bad viewstyle in database, using default: $@";
94             return DEFAULT_VIEWSTYLE;
95             }
96             return $viewstyle;
97             }
98             # viewstyle_write(DEFAULT_VIEWSTYLE);
99             # print viewstyle_read(DEFAULT_VIEWSTYLE);
100             # exit 0;
101             sub viewstyle_write {
102             my ($viewstyle) = @_;
103             require App::Chart::DBI;
104             require Data::Dumper;
105             my $str = Data::Dumper->new([$viewstyle],['viewstyle'])->Indent(1)->Terse(1)->Sortkeys(1)->Dump;
106             require App::Chart::DBI;
107             my $dbh = App::Chart::DBI->instance;
108             $dbh->do ('INSERT OR REPLACE INTO preference (key, value)
109             VALUES (\'viewstyle\',?)', {}, $str);
110             App::Chart::chart_dirbroadcast()->send ('viewstyle-changed');
111             }
112              
113             #------------------------------------------------------------------------------
114              
115             sub INIT_INSTANCE {
116             my ($self) = @_;
117             $self->{'symbol'} = ''; # per property default above
118             $self->{'series_list'} = [];
119             $self->{'timebase_class'} = DEFAULT_TIMEBASE_CLASS;
120             $self->{'graphs'} = [];
121             $self->set(n_rows => 9,
122             n_columns => 3);
123              
124             App::Chart::chart_dirbroadcast()->connect_for_object
125             ('data-changed', \&_do_data_changed, $self);
126              
127             # FIXME: this induces a rescale at a good time, but otherwise not wanted
128             App::Chart::chart_dirbroadcast()->connect_for_object
129             ('latest-changed', \&_do_data_changed, $self);
130              
131             App::Chart::Gtk2::GUI::chart_style_widget ('AppChartViewLabel');
132             my $ebox = $self->{'initial'} = Gtk2::EventBox->new;
133             $ebox->set_name ('AppChartViewLabel');
134             my $label = Gtk2::Label->new
135             (__('Use File/Open to open or add a symbol'));
136             $label->set_name ('AppChartViewLabel');
137             $ebox->add ($label);
138             $ebox->show_all;
139              
140             $self->attach ($ebox, 0,3, 0,9,
141             ['fill','shrink','expand'],
142             ['fill','shrink','expand'], 0,0);
143             }
144              
145             sub GET_PROPERTY {
146             my ($self, $pspec) = @_;
147             my $pname = $pspec->get_name;
148             if ($pname eq 'viewstyle') {
149             if (! $self->{'init_graphs'}) {
150             return viewstyle_read();
151             }
152             }
153             return $self->{$pname};
154             }
155              
156             sub SET_PROPERTY {
157             my ($self, $pspec, $newval) = @_;
158             my $pname = $pspec->get_name;
159             my $oldval = $self->{$pname};
160             ### View SET_PROPERTY: $pname
161             ### $newval
162              
163             if ($pname eq 'symbol') {
164             $self->set_symbol ($newval);
165             return;
166             }
167              
168             $self->{$pname} = $newval; # per default GET_PROPERTY
169              
170             if ($pname eq 'timebase_class') {
171             if ($oldval ne $newval) {
172             $self->set_symbol ($self->get('symbol'));
173             }
174              
175             } elsif ($pname eq 'statusbar') {
176             # lose old id
177             delete $self->{'crosshair_status_id'};
178              
179             } elsif ($pname eq 'viewstyle') {
180             if ($self->{'init_graphs'}) {
181             _update_attach ($self);
182             }
183             if ($self->{'symbol'}) {
184             _set_symbol ($self, $self->{'symbol'});
185             }
186             }
187             }
188              
189             #------------------------------------------------------------------------------
190             # Crosshair
191              
192             sub crosshair {
193             my ($self) = @_;
194             return ($self->{'crosshair_object'}
195             ||= do {
196             _init_graphs ($self);
197             require Gtk2::Ex::CrossHair;
198             my $ch = Gtk2::Ex::CrossHair->new (widgets => $self->{'graphs'},
199             foreground => 'orange');
200             $ch->signal_connect (moved => \&_do_crosshair_moved,
201             App::Chart::Glib::Ex::MoreUtils::ref_weak($self));
202             ### View created crosshair: "$ch"
203             $ch;
204             });
205             }
206              
207             sub _do_crosshair_moved {
208             my ($crosshair, $graph, $x, $y, $ref_weak_self) = @_;
209             my $self = $$ref_weak_self or return;
210             ### View _do_crosshair_moved()
211              
212             my $statusbar = $self->{'statusbar'} || return;
213             my $id = $statusbar->get_context_id (__PACKAGE__ . '.crosshair');
214             $statusbar->pop ($id);
215              
216             if (! defined $x) { return; }
217             my $series = $graph->get('series-list')->[0] || return;
218              
219             my $t = $graph->x_to_date ($x);
220             my $dstr = $series->timebase->strftime ($App::Chart::option{'d_fmt'}, $t);
221              
222             my $value = $graph->y_to_value ($y);
223             my $nf = App::Chart::number_formatter();
224             my $pstr = $nf->format_number ($value, $series->decimals, 0);
225              
226             my $status = $dstr . ' ' . $pstr;
227             ### $id
228             ### $status
229             $statusbar->push ($id, $status);
230             }
231              
232              
233             sub _do_lasso_ended {
234             }
235              
236             sub _do_graph_button_press {
237             my ($graph, $event) = @_;
238             my $self = $graph->get_ancestor (__PACKAGE__);
239              
240             if ($event->button == 3) {
241             $self->crosshair->start ($event);
242             }
243             return Gtk2::EVENT_PROPAGATE;
244             }
245              
246             #------------------------------------------------------------------------------
247              
248             sub set_symbol {
249             my ($self, $symbol) = @_;
250             $self->{'symbol'} = $symbol;
251             if ($self->realized) {
252             _set_symbol ($self, $symbol);
253             } else {
254              
255             # a nasty hack to get initial pages for scaling after windows realized
256             $self->{'realize_set_symbol_id'} ||=
257             $self->signal_connect (realize => sub {
258             my ($self) = @_;
259             # once only
260             my $id = delete $self->{'realize_set_symbol_id'};
261             $self->signal_handler_disconnect ($id);
262             _set_symbol ($self, $self->{'symbol'});
263             });
264             }
265             $self->notify ('symbol');
266             }
267              
268             sub _init_graphs {
269             my ($self) = @_;
270             if ($self->{'init_graphs'}) { return; }
271             ### View _init_graphs()
272             $self->{'init_graphs'} = 1;
273             $self->{'viewstyle'} = viewstyle_read();
274              
275             require App::Chart::Gtk2::Graph;
276             require App::Chart::Gtk2::HAxis;
277              
278             App::Chart::Gtk2::GUI::chart_style_class ('Gtk2::Ex::NumAxis');
279              
280             require App::Chart::Gtk2::Heading;
281             $self->{'heading'} = App::Chart::Gtk2::Heading->new;
282              
283             # initial horiz scale 4 pixels per date
284             require App::Chart::Gtk2::HScale;
285             my $hadj = $self->{'hadjustment'}
286             = App::Chart::Gtk2::HScale->new (pixel_per_value => 4);
287             $self->{'haxis'} = App::Chart::Gtk2::HAxis->new (adjustment => $hadj);
288             $self->{'hscroll'} = Gtk2::HScrollbar->new ($hadj);
289              
290             _update_attach ($self);
291             $self->show_all;
292              
293             # # this is a nasty hack to force the Gtk2::Table to set its childrens'
294             # # sizes now, instead of later under the queue_resize or whatever
295             # $self->size_allocate ($self->allocation);
296              
297             if (my $ebox = delete $self->{'initial'}) {
298             $self->remove ($ebox);
299             }
300             }
301              
302             # 0 1 2 3
303             # 0 +--------------+--+--+
304             # | heading |
305             # 1 +--------------+--+--+
306             # | |v |v |
307             # | upper |a |s |
308             # | |x |c |
309             # | |i |r |
310             # | |s |o |
311             # | | |l |
312             # | | |l |
313             # 5 +--------------+--+--+
314             # | gap |
315             # 6 +--------------+--+--+
316             # | |v |v |
317             # | lower |a |s |
318             # | |x |b |
319             # 7 +--------------+--+--+
320             # | haxis | |
321             # 8 +--------------+ |
322             # | hscroll | |
323             # 9 +--------------+--+--+
324             #
325             sub _update_attach {
326             my ($self) = @_;
327             require Gtk2::Ex::TableBits;
328              
329             my $y = 0;
330             Gtk2::Ex::TableBits::update_attach
331             ($self, $self->{'heading'}, 0,3, $y,$y+1,
332             ['fill','shrink','expand'], [], 0,0);
333             $y++;
334              
335             my $graphs = $self->{'graphs'};
336             my @graphstyles = @{$self->{'viewstyle'}->{'graphs'}};
337             while ($#$graphs > max (0, $#graphstyles)) {
338             my $graph = pop @$graphs;
339             $self->remove ($graph);
340             $self->remove ($graph->{'noshrink'});
341             $self->remove ($graph->{'vscroll'});
342             }
343             $graphs->[0] ||= do {
344             my $upper = _make_graph($self);
345             delete $upper->{'heading_in_graph'};
346             $self->{'hadjustment'}->set (widget => $upper);
347             Glib::Ex::ConnectProperties->new ([$upper,'series-list'],
348             [$self->{'heading'},'series-list']);
349             $upper;
350             };
351              
352             for (my $i = 0; $i < @graphstyles; $i++) {
353             my $graph = ($graphs->[$i] ||= _make_graph($self));
354             ### now graphs: "@$graphs"
355              
356             if ($i > 0) {
357             my $gap = ($graph->{'gap'} ||= Glib::Object::new ('Gtk2::DrawingArea',
358             height_request => 2));
359             Gtk2::Ex::TableBits::update_attach
360             ($self, $gap, 0,3, $y,$y+1,
361             [], [], 0,0);
362             $y++;
363             }
364              
365             my $graphstyle = $graphstyles[$i];
366             my $size = $graphstyle->{'size'};
367              
368             Gtk2::Ex::TableBits::update_attach
369             ($self, $graph, 0,1, $y,$y+$size,
370             ['fill','shrink','expand'],
371             ['fill','shrink','expand'], 0,0);
372             Gtk2::Ex::TableBits::update_attach
373             ($self, $graph->{'noshrink'},
374             1,2, $y,$y+$size,
375             ['fill','shrink'],
376             ['fill','shrink','expand'], 0,0);
377             Gtk2::Ex::TableBits::update_attach
378             ($self, $graph->{'vscroll'},
379             2,3, $y,$y+$size,
380             ['fill','shrink'],
381             ['fill','shrink','expand'], 0,0);
382             $y += $size;
383             }
384              
385             Gtk2::Ex::TableBits::update_attach
386             ($self, $self->{'haxis'}, 0,1, $y,$y+1,
387             ['fill','shrink','expand'],
388             ['fill','shrink'], 0,0);
389             $y++;
390             Gtk2::Ex::TableBits::update_attach
391             ($self, $self->{'hscroll'}, 0,1, $y,$y+1,
392             ['fill','shrink','expand'],
393             ['fill','shrink'], 0,0);
394             $y++;
395              
396             if (my $cross = $self->{'crosshair_object'}) {
397             ### _update_attach() cross widgets: "@$graphs"
398             $cross->set (widgets => $graphs);
399             }
400              
401             $self->resize (3, $y);
402             }
403              
404             sub _make_graph {
405             my ($self) = @_;
406              
407             require App::Chart::Gtk2::Graph;
408             require App::Chart::Gtk2::AdjScale;
409             my $vadj = App::Chart::Gtk2::AdjScale->new (orientation => 'vertical',
410             inverted => 1);
411             my $graph = App::Chart::Gtk2::Graph->new (hadjustment => $self->{'hadjustment'},
412             vadjustment => $vadj);
413             $graph->{'heading_in_graph'} = 1;
414             $vadj->set (widget => $graph);
415             $graph->signal_connect (button_press_event => \&_do_graph_button_press);
416              
417             require Gtk2::Ex::NumAxis;
418             my $vaxis = $graph->{'vaxis'}
419             = Gtk2::Ex::NumAxis->new (adjustment => $vadj,
420             inverted => 1);
421             $vaxis->signal_connect (number_to_text => \&_vaxis_number_to_text);
422              
423             require Gtk2::Ex::NoShrink;
424             $graph->{'noshrink'} = Gtk2::Ex::NoShrink->new (child => $vaxis);
425             my $vscroll = $graph->{'vscroll'} = Gtk2::VScrollbar->new ($vadj);
426             $vscroll->set_inverted (1);
427              
428             $vaxis->add_events (['button-press-mask',
429             'button-motion-mask',
430             'button-release-mask']);
431             $vaxis->signal_connect (button_press_event => \&_do_vaxis_button_press);
432             $graph->show_all;
433             return $graph;
434             }
435              
436             sub _vaxis_number_to_text {
437             my ($axis, $number, $decimals) = @_;
438             return App::Chart::number_formatter()->format_number ($number, $decimals, 1);
439             }
440              
441              
442             sub _do_vaxis_button_press {
443             my ($vaxis, $event) = @_;
444             if ($event->button == 1) {
445             require Gtk2::Ex::Dragger;
446             my $dragger = ($vaxis->{'dragger'} ||=
447             Gtk2::Ex::Dragger->new
448             (widget => $vaxis,
449             vadjustment => $vaxis->get('adjustment'),
450             vinverted => 1,
451             cursor => 'sb-v-double-arrow',
452             confine => 1));
453             $dragger->start ($event);
454             }
455             return Gtk2::EVENT_PROPAGATE;
456             }
457              
458             sub _set_symbol {
459             my ($self, $symbol) = @_;
460             if (! $symbol) { return; }
461              
462             _init_graphs ($self);
463              
464             my $hadj = $self->{'hadjustment'};
465             my $haxis = $self->{'haxis'};
466              
467             if (! $symbol) {
468             foreach my $graph (@{$self->{'graphs'}}) {
469             $graph->set('series_list', []);
470             $graph->get('vadjustment')->empty
471             }
472             $hadj->empty;
473             return;
474             }
475              
476             require App::Chart::Series::Database;
477             my $series = App::Chart::Series::Database->new ($symbol);
478              
479             my $viewstyle = $self->{'viewstyle'};
480             if ($viewstyle->{'adjust_splits'}
481             || $viewstyle->{'adjust_dividends'}
482             || $viewstyle->{'adjust_rollovers'}) {
483             require App::Chart::Series::Derived::Adjust;
484             $series = App::Chart::Series::Derived::Adjust->derive
485             ($series,
486             adjust_splits => $viewstyle->{'adjust_splits'},
487             adjust_dividends => $viewstyle->{'adjust_dividends'},
488             adjust_imputation => $viewstyle->{'adjust_imputation'},
489             adjust_rollovers => $viewstyle->{'adjust_rollovers'});
490             }
491              
492             my $timebase_class = $self->{'timebase_class'};
493             if (! $series->timebase->isa ($timebase_class)) {
494             ### collapse to: $timebase_class
495             $series = $series->collapse ($timebase_class);
496             }
497              
498             my $timebase = $series->timebase;
499             $haxis->set(timebase => $timebase);
500              
501             my $graphstyles = $viewstyle->{'graphs'} || [];
502             my $graphs = $self->{'graphs'};
503              
504             require App::Chart::Gtk2::Graph::Plugin::Latest;
505             require App::Chart::Gtk2::Graph::Plugin::Today;
506             require App::Chart::Gtk2::Graph::Plugin::Text;
507             require App::Chart::Gtk2::Graph::Plugin::AnnLines;
508             my @hrange = (0, $series->hi);
509             my @today_hrange;
510              
511             for (my $i = 0; $i < @$graphstyles; $i++) {
512             my $graphstyle = $graphstyles->[$i];
513             my $graph = $graphs->[$i] || die;
514             my $series_list = graphstyle_to_series_list ($graphstyle, $series);
515              
516             # date range for series, latest, and perhaps today
517             if ($i == 0) {
518             push @hrange,
519             (@today_hrange = App::Chart::Gtk2::Graph::Plugin::Today->hrange ($graph, $series_list));
520             push @hrange,
521             (App::Chart::Gtk2::Graph::Plugin::Latest->hrange ($graph, $series_list),
522             App::Chart::Gtk2::Graph::Plugin::Text->hrange ($graph, $series_list),
523             App::Chart::Gtk2::Graph::Plugin::AnnLines->hrange ($graph, $series_list));
524             }
525              
526             $graph->set('series_list', []);
527             $graph->set('series_list', $series_list);
528             my $decimals = max (0, map {$_->decimals} @$series_list);
529             $graph->{'vaxis'}->set (min_decimals => $decimals);
530             ### graph: "$i decimals $decimals"
531             }
532              
533             require List::MoreUtils;
534             my ($lower, $upper) = List::MoreUtils::minmax (@hrange);
535             $upper += 2; # +1 for inclusive, +1 for bit of margin
536              
537             # rightmost edge
538             my $value = $upper;
539             my $today = $today_hrange[0];
540             if (defined $today) {
541             if ($upper > $today + 10) {
542             $value = $today + 4;
543             }
544             }
545             $value -= $hadj->page_size;
546             $lower = min ($lower, $value);
547              
548             ### View decide hadj: "$lower to $upper, value=$value"
549             Gtk2::Ex::AdjustmentBits::set_maybe ($hadj,
550             lower => $lower,
551             upper => $upper,
552             value => $value);
553             ### View hadj: $hadj->lower." to $upper =",$timebase->to_iso($upper)
554             my ($lo, $hi) = $hadj->value_range_inc;
555              
556             foreach my $graph (@$graphs) {
557             $graph->update_v_range;
558              
559             # my $series_list = $graph->{'series_list'};
560             # my $this_series = $series_list->[0] || next;
561             #
562             # my ($p_lo, $p_hi) = $this_series->range ($lo, $hi);
563             # if (! defined $p_lo) {
564             # $p_hi = $p_lo = 0;
565             # }
566             # ### View graph vrange: "$p_lo $p_hi"
567             # Gtk2::Ex::AdjustmentBits::set_maybe ($graph->get('vadjustment'),
568             # lower => $p_lo,
569             # upper => $p_hi);
570             }
571             }
572              
573             sub graphstyle_to_series_list {
574             my ($graphstyle, $series) = @_;
575             ### View graphstyle_to_series_list()
576             my @series_list;
577              
578             # top-level series goes into upper graph only
579             if (exists $graphstyle->{'linestyle'}
580             && ($graphstyle->{'linestyle'}||'') ne 'None') {
581             $series->linestyle($graphstyle->{'linestyle'});
582             push @series_list, $series;
583             }
584              
585             foreach my $indicatorstyle (@{$graphstyle->{'indicators'}}) {
586             my $key = $indicatorstyle->{'key'} || next;
587             if ($key eq 'None') { next; }
588             ### $indicatorstyle
589              
590             if (! $series->can($key)) {
591             warn "Ignoring unknown indicator '$key'";
592             next;
593             }
594              
595             require App::Chart::IndicatorInfo;
596             my $info = App::Chart::IndicatorInfo->new ($key);
597             my @params;
598             foreach my $paraminfo (@{$info->parameter_info}) {
599             my $paramkey = $paraminfo->{'key'};
600             push @params, ($indicatorstyle->{$paramkey}
601             // $paraminfo->{'default'});
602             }
603             ### @params
604             my $derived = $series->$key (@params);
605             push @series_list, $derived;
606             }
607             return \@series_list;
608             }
609              
610             # 'data-changed'
611             sub _do_data_changed {
612             my ($self, $symbol_hash) = @_;
613             my $symbol = $self->{'symbol'} // return;
614             if (exists $symbol_hash->{$symbol}) {
615             ### "View _do_data_changed() displayed symbol: $symbol
616             _set_symbol ($self, $symbol);
617             }
618             }
619              
620             sub centre {
621             my ($self) = @_;
622             $self->{'graphs'}->[0]->centre;
623             # $self->{'lower'}->centre;
624             }
625              
626             sub zoom {
627             my ($self, $xfactor, $yfactor) = @_;
628             if ($xfactor != 1) {
629             my $hadj = $self->{'hadjustment'};
630             my $ppv = $hadj->get_pixel_per_value;
631             my $new_ppv = POSIX::ceil ($xfactor * $ppv);
632             if ($ppv == $new_ppv) {
633             if ($xfactor < 1) {
634             $new_ppv = max (1, $new_ppv-1);
635             } else {
636             $new_ppv = $new_ppv+1;
637             }
638             }
639             if ($ppv != $new_ppv) {
640             $hadj->set_pixel_per_value ($new_ppv);
641             }
642             }
643             if ($yfactor != 1) {
644             foreach my $graph (@{$self->{'graphs'}}) {
645             my $vadj = $graph->get('vadjustment');
646             $vadj->set_pixel_per_value ($yfactor * $vadj->get_pixel_per_value);
647             }
648             }
649             }
650              
651             1;
652             __END__
653              
654             =for stopwords viewstyle
655              
656             =head1 NAME
657              
658             App::Chart::Gtk2::View -- view widget of heading, graphs, axes, scrollbars, etc
659              
660             =head1 SYNOPSIS
661              
662             my $view = App::Chart::Gtk2::View->new;
663              
664             =head1 DESCRIPTION
665              
666             A C<App::Chart::Gtk2::View> widget displays graphs of the data from a given stock
667             symbol, with a "viewstyle" controlling what graphs and indicators are
668             presented.
669              
670             =head1 FUNCTIONS
671              
672             =over 4
673              
674             =item C<< $view->symbol >>
675              
676             Return the currently displayed stock symbol (a string), or C<undef> if none.
677              
678             =item C<< $view->centre >>
679              
680             Centre the displayed data within the graph windows. This is the
681             "View/Centre" menu item in the main GUI.
682              
683             =back
684              
685             =head1 PROPERTIES
686              
687             =over 4
688              
689             =item C<symbol> (string, default none)
690              
691             =back
692              
693             =cut