File Coverage

blib/lib/App/Chart/Gtk2/HAxis.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             # Copyright 2007, 2008, 2009, 2010, 2011, 2014 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::HAxis;
18 1     1   398 use 5.008;
  1         3  
19 1     1   4 use strict;
  1         1  
  1         18  
20 1     1   4 use warnings;
  1         1  
  1         22  
21 1     1   263 use Glib::Ex::SignalIds;
  0            
  0            
22             use Gtk2 1.220;
23             use POSIX ();
24             use POSIX::Wide;
25             use List::Util qw(min max);
26             use List::MoreUtils;
27             # use Locale::TextDomain ('App-Chart');
28              
29             use App::Chart::Glib::Ex::MoreUtils;
30             use Gtk2::Ex::Units;
31             use App::Chart;
32             use App::Chart::Gtk2::GUI;
33             use App::Chart::Series;
34              
35             # set this to 1 for some diagnostic prints
36             use constant DEBUG => 0;
37              
38             use Glib::Object::Subclass
39             'Gtk2::DrawingArea',
40             signals => { expose_event => \&_do_expose_event,
41             button_press_event => \&_do_button_press_event,
42             size_request => \&_do_size_request,
43             configure_event => \&_do_configure_event,
44             style_set => \&_do_style_set,
45             set_scroll_adjustments =>
46             { param_types => ['Gtk2::Adjustment',
47             'Gtk2::Adjustment'],
48             return_type => undef,
49             class_closure => \&_do_set_scroll_adjustments },
50             start_drag =>
51             { param_types => ['Glib::Int'],
52             return_type => undef,
53             class_closure => \&_do_start_drag,
54             flags => ['run-first','action'] },
55             },
56             properties => [Glib::ParamSpec->object
57             ('adjustment',
58             'adjustment',
59             '',
60             'Gtk2::Adjustment',
61             Glib::G_PARAM_READWRITE),
62              
63             Glib::ParamSpec->scalar
64             ('timebase',
65             'timebase',
66             'A perl App::Chart::Timebase object.',
67             Glib::G_PARAM_READWRITE),
68             ];
69             App::Chart::Gtk2::GUI::chart_style_class (__PACKAGE__);
70              
71             use constant {
72             # height of small and medium tick marks, as a fraction of line_height, and
73             # then the gap between the medium tick and the labels similarly
74             SMALL_TICK_LINE_FRAC => 0.2,
75             MEDIUM_TICK_LINE_FRAC => 0.4,
76             MEDIUM_GAP_LINE_FRAC => 0.1,
77              
78             # space at the left and right of the labels, in "em"s
79             LABEL_LEFT_SPACE_EMS => 0.7,
80             LABEL_RIGHT_SPACE_EMS => 1.2,
81             };
82              
83             # strftime() is a bit slow, so do this with some private functions just
84             # concatenating bits. Each function is called like B_Y($year,$month,$day)
85             # and returns a string.
86             #
87             my @fullmonth;
88             my @shortmonth;
89             sub B_Y { return $fullmonth[$_[1]] . ' ' . $_[0]; }
90             sub b_Y { return $shortmonth[$_[1]] . ' ' . $_[0]; }
91             sub b_y { return sprintf '%s %02d', $shortmonth[$_[1]], $_[0] % 100; }
92             sub by { return sprintf '%s%02d', $shortmonth[$_[1]], $_[0] % 100; }
93             sub Y { return $_[0]; }
94             sub y { return sprintf '%02d', $_[0] % 100; }
95              
96             # Possible format string func and timebase interval. The first one whose
97             # string output fits in the display in those timebase interval steps is
98             # used.
99             my @format_list = ([ \&B_Y, 'App::Chart::Timebase::Months' ], # "March 2007"
100             [ \&b_Y, 'App::Chart::Timebase::Months' ], # "Mar 2007"
101             [ \&b_y, 'App::Chart::Timebase::Months' ], # "Mar 07"
102             [ \&by, 'App::Chart::Timebase::Months' ], # "Mar07"
103             [ \&B_Y, 'App::Chart::Timebase::Quarters' ], # "March 2007"
104             [ \&b_Y, 'App::Chart::Timebase::Quarters' ], # "Mar 2007"
105             [ \&b_y, 'App::Chart::Timebase::Quarters' ], # "Mar 07"
106             [ \&by, 'App::Chart::Timebase::Quarters' ], # "Mar07"
107             [ \&Y, 'App::Chart::Timebase::Years' ], # "2007"
108             [ \&y, 'App::Chart::Timebase::Years' ], # "07"
109             [ \&Y, 'App::Chart::Timebase::Decades' ], # "2007"
110             [ \&y, 'App::Chart::Timebase::Decades' ]);# "07"
111              
112             # possible timebases to use for drawing the marks
113             # the base $timebase is expected to be among these, then the medium marks
114             # are drawn in the one after that
115             my @marks_timebase_list = qw(App::Chart::Timebase::Days
116             App::Chart::Timebase::Weeks
117             App::Chart::Timebase::Months
118             App::Chart::Timebase::Quarters
119             App::Chart::Timebase::Years
120             App::Chart::Timebase::Decades);
121              
122             Gtk2::Rc->parse_string (<<'HERE');
123             binding "App__Chart__Gtk2__HAxis_keys" {
124             bind "Pointer_Button1" { "start_drag" (1) }
125             }
126             # priority level "gtk" treating this as widget level default, for overriding
127             # by application or user RC
128             class "App__Chart__Gtk2__HAxis" binding:gtk "App__Chart__Gtk2__HAxis_keys"
129             HERE
130              
131             sub INIT_INSTANCE {
132             my ($self) = @_;
133             $self->add_events (['button-press-mask',
134             'button-motion-mask',
135             'button-release-mask']);
136             $self->{'layout'} = $self->create_pango_layout ('');
137              
138             if (! @fullmonth) {
139             my $fill = sub {
140             my ($fmt, $aref) = @_;
141             foreach my $mon (0 .. 11) {
142             $aref->[$mon+1]
143             = POSIX::Wide::strftime ($fmt,
144             0,0,0, # midnight
145             1,$mon,80, # 1st $mon 1980
146             0,0,0);
147             }
148             };
149             $fill->('%B', \@fullmonth);
150             $fill->('%b', \@shortmonth);
151             }
152             }
153              
154             sub SET_PROPERTY {
155             my ($self, $pspec, $newval) = @_;
156             my $pname = $pspec->get_name;
157             $self->{$pname} = $newval; # per default GET_PROPERTY
158              
159             if ($pname eq 'adjustment') {
160             my $adj = $newval;
161             my $ref_weak_self = App::Chart::Glib::Ex::MoreUtils::ref_weak ($self);
162             $self->{'adjustment_ids'} = $adj && Glib::Ex::SignalIds->new
163             ($adj,
164             $adj->signal_connect (value_changed => \&_do_adj_value_changed,
165             $ref_weak_self),
166             $adj->signal_connect (changed => \&_do_adj_other_changed,
167             $ref_weak_self));
168             }
169             _reset_scale ($self);
170             $self->queue_draw;
171             }
172              
173             # 'set-scroll-adjustments' class closure
174             sub _do_set_scroll_adjustments {
175             my ($self, $hadj, $vadj) = @_;
176             $self->set (adjustment => $hadj);
177             }
178              
179             # mark that the scale has somehow changed, so the label_timebase and format
180             # should be recalculated (on the next draw)
181             sub _reset_scale {
182             my ($self) = @_;
183             $self->{'sizes'} = undef;
184             }
185              
186             sub _establish_bases {
187             my ($self) = @_;
188             $self->{'sizes'} = undef; # if no adj etc
189             my $adj = $self->{'adjustment'} || return;
190             my $timebase = $self->{'timebase'} || return;
191             # my $page_size = $adj->page_size || return;
192             # my $win = $self->window || return;
193              
194             my $sizes = $self->{'sizes'} = {};
195             my $layout = $self->{'layout'};
196             my $em = Gtk2::Ex::Units::em($layout);
197             my $line_height = Gtk2::Ex::Units::line_height ($layout);
198             # my ($win_width, $win_height) = $win->get_size;
199             my $x_step = $adj->get_pixel_per_value;
200              
201             $sizes->{'small_y'} = POSIX::ceil (SMALL_TICK_LINE_FRAC * $line_height);
202             $sizes->{'medium_y'} = POSIX::ceil (MEDIUM_TICK_LINE_FRAC * $line_height);
203             $sizes->{'text_y'}
204             = $sizes->{'medium_y'} + POSIX::ceil (MEDIUM_GAP_LINE_FRAC * $line_height);
205              
206             # establish $medium_timebase as the entry in @marks_timebase_list which
207             # is one above $timebase
208             {
209             my @a = List::MoreUtils::after {$timebase->isa($_)}
210             @marks_timebase_list;
211             if (@a) {
212             my $medium_timebase_class = $a[0];
213             require Module::Load;
214             Module::Load::load ($medium_timebase_class);
215              
216             # medium_timebase from the class name, with an arbitrary start base
217             $sizes->{'medium_timebase'}
218             = $medium_timebase_class->new_from_ymd (1970,1,5);
219             }
220             }
221              
222             my $label_x_offset = $sizes->{'label_x_offset'}
223             = POSIX::ceil ($em * LABEL_LEFT_SPACE_EMS);
224             foreach my $elem (@format_list) {
225             my ($format_func, $timebase_class) = @$elem;
226              
227             require Module::Load;
228             Module::Load::load ($timebase_class);
229              
230             # label_timebase from the class name, with an arbitrary start base
231             my $label_timebase = $timebase_class->new_from_ymd (1970,1,5);
232             my $t_width = $x_step
233             * ($timebase->convert_from_floor ($label_timebase, 1)
234             - $timebase->convert_from_floor ($label_timebase, 0));
235             my $label_width = $label_x_offset
236             + format_func_width ($layout, $format_func)
237             + POSIX::ceil ($em * LABEL_RIGHT_SPACE_EMS);
238             if (DEBUG) {
239             print "$label_timebase label $label_width versus $t_width\n"; }
240              
241             if ($label_width < $t_width || $elem == $format_list[-1]) {
242             $sizes->{'label_timebase'} = $label_timebase;
243             $sizes->{'format_func'} = $format_func;
244             last;
245             }
246             }
247              
248             if (DEBUG) { require Data::Dumper;
249             print "HAxis sizes ", Data::Dumper::Dumper($sizes);
250             print " em=$em\n";
251             print " x_step=$x_step\n";
252             }
253             return $sizes;
254             }
255              
256             # 'expose-event' class closure
257             sub _do_expose_event {
258             my ($self, $event) = @_;
259             if (DEBUG >= 2) { print "HAxis expose\n"; }
260             my $sizes = ($self->{'sizes'} || _establish_bases($self));
261              
262             # must have an adjustment and timebase set and a non-empty page to draw
263             # anything
264             my $adj = $self->{'adjustment'} || return Gtk2::EVENT_PROPAGATE;
265             my $timebase = $self->{'timebase'} || return Gtk2::EVENT_PROPAGATE;
266             # my $page_size = $adj->page_size || return Gtk2::EVENT_PROPAGATE;
267              
268             my $layout = $self->{'layout'};
269             my $style = $self->style;
270             my $state = $self->state;
271             my $gc = $style->fg_gc($state);
272             my $win = $self->window;
273             my $value_to_pixel_proc = $adj->value_to_pixel_proc;
274              
275             my $t_lo = POSIX::floor ($adj->value);
276             my $t_hi = POSIX::ceil ($adj->value + $adj->page_size);
277             if (DEBUG >= 2) { print " values $t_lo to $t_hi ",
278             $timebase->to_iso($t_lo), " to ",
279             $timebase->to_iso($t_hi), "\n"; }
280              
281             # small tick marks at every $timebase increment, unless that's every pixel
282             #
283             my $x_step = $adj->get_pixel_per_value;
284             if ($x_step > 1) {
285             my $small_y = $sizes->{'small_y'};
286             $win->draw_segments ($gc, map { my $x = $value_to_pixel_proc->($_);
287             ($x,0, $x,$small_y) }
288             ($t_lo .. $t_hi));
289             }
290              
291             # medium tick marks at every $medium_timebase increment, if such a timebase
292             #
293             if (my $medium_timebase = $sizes->{'medium_timebase'}) {
294             my $medium_y = $sizes->{'medium_y'};
295             my $medium_t_lo
296             = $medium_timebase->convert_from_floor ($timebase, $t_lo) - 1;
297             my $medium_t_hi
298             = $medium_timebase->convert_from_floor ($timebase, $t_hi) + 1;
299             if (DEBUG >= 2) { print " medium $medium_t_lo to $medium_t_hi ",
300             $medium_timebase->to_iso($medium_t_lo), " to ",
301             $medium_timebase->to_iso($medium_t_hi), "\n"; }
302              
303             $win->draw_segments ($gc, map {
304             my $t = $timebase->convert_from_floor ($medium_timebase, $_);
305             my $x = $value_to_pixel_proc->($t);
306             ($x,0, $x,$medium_y) }
307             ($medium_t_lo .. $medium_t_hi));
308             }
309              
310             # label and full length tick mark at every $label_timebase increment, if
311             # such a timebase
312             #
313             if (my $label_timebase = $sizes->{'label_timebase'}) {
314             my $label_x_offset = $sizes->{'label_x_offset'};
315             my $text_y = $sizes->{'text_y'};
316             my $format_func = $sizes->{'format_func'};
317             my $label_t_lo
318             = $label_timebase->convert_from_floor ($timebase, $t_lo) - 1;
319             my $label_t_hi
320             = $label_timebase->convert_from_ceil ($timebase, $t_hi) + 1;
321             if (DEBUG >= 2) { print " label $label_t_lo to $label_t_hi ",
322             $label_timebase->to_iso($label_t_lo), " to ",
323             $label_timebase->to_iso($label_t_hi), "\n"; }
324             my ($win_width, $win_height) = $win->get_size;
325              
326             foreach my $label_t ($label_t_lo .. $label_t_hi) {
327             my $t = $timebase->convert_from_floor ($label_timebase, $label_t);
328             my $x = $value_to_pixel_proc->($t);
329             $win->draw_line ($gc, $x, 0, $x, $win_height);
330              
331             my $str = $format_func->($label_timebase->to_ymd ($label_t));
332             $layout->set_text ($str);
333             $style->paint_layout ($win, # window
334             $state,
335             1, # use_text, for text gc instead of the fg one
336             $event->area,
337             $self, # widget
338             __PACKAGE__, # style detail string
339             $x + $label_x_offset,
340             $text_y,
341             $layout);
342             }
343             }
344             return Gtk2::EVENT_PROPAGATE;
345             }
346              
347             sub _do_adj_value_changed {
348             my ($adj, $ref_weak_self) = @_;
349             my $self = $$ref_weak_self || return;
350             $self->queue_draw;
351             }
352              
353             sub _do_adj_other_changed {
354             my ($adj, $ref_weak_self) = @_;
355             my $self = $$ref_weak_self || return;
356             _reset_scale ($self); # for new page_size
357             $self->queue_draw; # for new page_size
358             }
359              
360             # 'button-press-event' class closure
361             sub _do_button_press_event {
362             my ($self, $event) = @_;
363             require App::Chart::Gtk2::Ex::BindingBits;
364             App::Chart::Gtk2::Ex::BindingBits::activate_button_event
365             ('App__Chart__Gtk2__HAxis_keys', $event, $self);
366             return shift->signal_chain_from_overridden(@_);
367             }
368              
369             sub _do_start_drag {
370             my ($self, $button) = @_;
371             if (DEBUG) { print "HAxis _do_start_drag\n"; }
372             my $adj = $self->{'adjustment'} || return; # only when an adj set
373             require Gtk2::Ex::Dragger;
374             my $dragger = ($self->{'dragger'} ||= Gtk2::Ex::Dragger->new
375             (widget => $self,
376             hadjustment => $adj,
377             cursor => 'sb-h-double-arrow',
378             confine => 1));
379             $dragger->start (Gtk2->get_current_event);
380             }
381              
382             # 'size_request' class closure
383             sub _do_size_request {
384             my ($self, $req) = @_;
385              
386             my $layout = $self->{'layout'};
387             my $line_height = Gtk2::Ex::Units::line_height($layout);
388              
389             $req->width (0);
390             $req->height ($line_height
391             + POSIX::ceil ($line_height * MEDIUM_TICK_LINE_FRAC)
392             + POSIX::ceil ($line_height * MEDIUM_GAP_LINE_FRAC));
393             }
394              
395             # 'style-set' class closure
396             sub _do_style_set {
397             my ($self, $prev_style) = @_;
398              
399             # update as advised by gtk_widget_create_pango_layout()
400             $self->{'layout'}->context_changed;
401              
402             _reset_scale ($self); # new font perhaps
403             $self->queue_resize;
404             $self->queue_draw;
405             return shift->signal_chain_from_overridden(@_);
406             }
407              
408             # 'configure-event' class closure
409             sub _do_configure_event {
410             my ($self, $event) = @_;
411             $self->queue_draw;
412             return shift->signal_chain_from_overridden(@_);
413             }
414              
415             # Return the width in pixels needed to draw the given $format_func string in
416             # $layout.
417             #
418             # This is only geared towards the month/year formats above. It goes through
419             # all the months Jan to Dec to see how they come out, but assumes year 2000
420             # is as wide as any year will be.
421             #
422             sub format_func_width {
423             my ($layout, $format_func) = @_;
424             return max (map { my $str = $format_func->(2000, $_);
425             $layout->set_text ($str);
426             my ($str_width, $str_height) = $layout->get_pixel_size;
427             $str_width;
428             } 1 .. 12);
429             }
430              
431             1;
432             __END__
433              
434             =for stopwords undef HAxis
435              
436             =head1 NAME
437              
438             App::Chart::Gtk2::HAxis -- horizontal timebase axis display widget
439              
440             =head1 SYNOPSIS
441              
442             my $hscale = App::Chart::Gtk2::HAxis->new();
443              
444             =head1 WIDGET HIERARCHY
445              
446             C<App::Chart::Gtk2::HAxis> is a subclass of C<Gtk2::DrawingArea>.
447              
448             Gtk2::Widget
449             Gtk2::DrawingArea
450             App::Chart::Gtk2::HAxis
451              
452             =head1 DESCRIPTION
453              
454             A C<App::Chart::Gtk2::HAxis> widget displays tick marks and dates on a
455             horizontal axis, for use below a C<App::Chart::Gtk2::Graph>.
456              
457             =head1 PROPERTIES
458              
459             =over 4
460              
461             =item C<adjustment> (C<Gtk2::Adjustment>, default undef)
462              
463             An adjustment object giving the range of dates to display. The HAxis
464             display updates as the adjustment moves.
465              
466             =item C<timebase> (C<App::Chart::Timebase> object, default undef)
467              
468             The timebase for the dates displayed. This is used to get the actual
469             calendar date represented by integer values from the C<adjustment>.
470              
471             =back
472              
473             =head1 SEE ALSO
474              
475             L<App::Chart::Gtk2::Graph>
476              
477             =head1 HOME PAGE
478              
479             L<http://user42.tuxfamily.org/chart/index.html>
480              
481             =head1 LICENCE
482              
483             Copyright 2007, 2008, 2009, 2010, 2011, 2014 Kevin Ryde
484              
485             Chart is free software; you can redistribute it and/or modify it under the
486             terms of the GNU General Public License as published by the Free Software
487             Foundation; either version 3, or (at your option) any later version.
488              
489             Chart is distributed in the hope that it will be useful, but WITHOUT ANY
490             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
491             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
492             details.
493              
494             You should have received a copy of the GNU General Public License along with
495             Chart; see the file F<COPYING>. Failing that, see
496             L<http://www.gnu.org/licenses/>.
497              
498             =cut