File Coverage

blib/lib/Chart/Clicker/Axis.pm
Criterion Covered Total %
statement 53 68 77.9
branch 4 20 20.0
condition 0 2 0.0
subroutine 15 16 93.7
pod 3 4 75.0
total 75 110 68.1


line stmt bran cond sub pod time code
1             package Chart::Clicker::Axis;
2             $Chart::Clicker::Axis::VERSION = '2.88';
3 11     11   69089 use Moose;
  11         1047721  
  11         89  
4 11     11   82612 use Moose::Util;
  11         31  
  11         110  
5              
6             extends 'Chart::Clicker::Container';
7             with 'Chart::Clicker::Positioned';
8              
9             # ABSTRACT: An X or Y Axis
10              
11 11     11   2245 use Class::Load;
  11         37  
  11         574  
12              
13 11     11   4805 use Chart::Clicker::Data::Range;
  11         40  
  11         525  
14              
15 11     11   11757 use English qw(-no_match_vars);
  11         55948  
  11         77  
16              
17 11     11   8588 use Graphics::Color::RGB;
  11         2933936  
  11         377  
18              
19 11     11   10511 use Graphics::Primitive::Font;
  11         1458171  
  11         547  
20              
21 11     11   18251 use Layout::Manager::Absolute;
  11         215117  
  11         517  
22              
23 11     11   7675 use Math::Trig ':pi';
  11         144103  
  11         18750  
24              
25              
26             has 'tick_label_angle' => (
27             is => 'rw',
28             isa => 'Num'
29             );
30              
31              
32             has 'tick_division_type' => ( is => 'rw', isa => 'Str', default => 'Exact' );
33              
34             # The above tick division type is loaded on the first call to divvy()
35             has '_tick_division_type_loaded' => ( is => 'ro', isa => 'Bool', lazy_build => 1 );
36              
37              
38             has 'baseline' => (
39             is => 'rw',
40             isa => 'Num',
41             );
42              
43              
44             has 'brush' => (
45             is => 'rw',
46             isa => 'Graphics::Primitive::Brush',
47             default => sub {
48             Graphics::Primitive::Brush->new(
49             color => Graphics::Color::RGB->new(
50             red => 1, green => 0, blue => 1, alpha => 1
51             ),
52             width => 1
53             )
54             }
55             );
56              
57              
58             has '+color' => (
59             default => sub {
60             Graphics::Color::RGB->new({
61             red => 0, green => 0, blue => 0, alpha => 1
62             })
63             }
64             );
65              
66              
67             has 'format' => ( is => 'rw', isa => 'Str|CodeRef', default => '%s' );
68              
69              
70             has 'fudge_amount' => ( is => 'rw', isa => 'Num', default => 0 );
71              
72              
73             has 'hidden' => ( is => 'rw', isa => 'Bool', default => 0 );
74              
75              
76             has 'label' => ( is => 'rw', isa => 'Str' );
77              
78              
79             has 'label_color' => (
80             is => 'rw',
81             isa => 'Graphics::Color',
82             default => sub {
83             Graphics::Color::RGB->new({
84             red => 0, green => 0, blue => 0, alpha => 1
85             })
86             }
87             );
88              
89              
90             has 'label_font' => (
91             is => 'rw',
92             isa => 'Graphics::Primitive::Font',
93             default => sub { Graphics::Primitive::Font->new }
94             );
95              
96              
97             has '+layout_manager' => ( default => sub { Layout::Manager::Absolute->new });
98              
99              
100             has '+orientation' => (
101             required => 1
102             );
103              
104              
105             has '+position' => (
106             required => 1
107             );
108              
109              
110             has 'range' => (
111             is => 'rw',
112             isa => 'Chart::Clicker::Data::Range',
113             default => sub { Chart::Clicker::Data::Range->new }
114             );
115              
116              
117             has 'show_ticks' => ( is => 'rw', isa => 'Bool', default => 1 );
118              
119              
120             has 'staggered' => ( is => 'rw', isa => 'Bool', default => 0 );
121              
122              
123             has 'skip_range' => (
124             is => 'rw',
125             isa => 'Chart::Clicker::Data::Range',
126             predicate => 'has_skip_range'
127             );
128              
129              
130             has 'tick_font' => (
131             is => 'rw',
132             isa => 'Graphics::Primitive::Font',
133             default => sub { Graphics::Primitive::Font->new }
134             );
135              
136              
137             has 'tick_label_color' => (
138             is => 'rw',
139             isa => 'Graphics::Color',
140             default => sub {
141             Graphics::Color::RGB->new({
142             red => 0, green => 0, blue => 0, alpha => 1
143             })
144             }
145             );
146              
147              
148             has 'tick_labels' => (
149             is => 'rw',
150             isa => 'ArrayRef',
151             );
152              
153              
154             has 'tick_values' => (
155             traits => [ 'Array' ],
156             is => 'rw',
157             isa => 'ArrayRef',
158             default => sub { [] },
159             handles => {
160             'add_to_tick_values' => 'push',
161             'clear_tick_values' => 'clear',
162             'tick_value_count' => 'count'
163             }
164             );
165              
166              
167             has 'ticks' => ( is => 'rw', isa => 'Int', default => 6 );
168              
169             sub BUILD {
170 27     27 0 57 my ($self) = @_;
171              
172 27         1294 $self->padding(3);
173             }
174              
175             sub _build__tick_division_type_loaded {
176 5     5   10 my $self = shift;
177              
178             # User modules are prefixed with a +.
179 5         6 my $divisionTypeModule;
180 5         9 my $extensionOf = 'Chart::Clicker::Axis::DivisionType';
181 5 50       200 if ( $self->tick_division_type =~ m/^\+(.*)$/xmisg ) {
182 0         0 $divisionTypeModule = $1;
183             }
184             else {
185 5         188 $divisionTypeModule = sprintf( '%s::%s', $extensionOf, $self->tick_division_type );
186             }
187              
188             # Try to load the DivisionType module. An error is thrown when the class is
189             # not available or cannot be loaded
190 5         29 Class::Load::load_class($divisionTypeModule);
191              
192             # Apply the newly loaded role to this class.
193 5         250 Moose::Util::apply_all_roles( $self => $divisionTypeModule );
194 5 50       122901 if ( not $self->does($extensionOf) ) {
195 0         0 die("Module $divisionTypeModule does not extend $extensionOf");
196             }
197              
198 5         1664 return 1;
199             }
200              
201             override('prepare', sub {
202             my ($self, $driver) = @_;
203              
204             $self->clear_components;
205              
206             # The BUILD method above establishes a 5px padding at instantiation time.
207             # That saves us setting it elsewhere, but if 'hidden' feature is used,
208             # then we have to unset the padding. hidden (as explained below) is
209             # basically a hack as far as G:P is concerned to render an empty Axis.
210             # We want it to render because we need it prepared for other elements
211             # of the chart to work.
212             if($self->hidden) {
213             $self->padding(0);
214             # Hide the border too, jic
215             $self->border->width(0);
216             }
217              
218             super;
219              
220             if($self->range->span == 0) {
221             die('This axis has a span of 0, that\'s fatal!');
222             }
223              
224             if(defined($self->baseline)) {
225             if($self->range->lower > $self->baseline) {
226             $self->range->lower($self->baseline);
227             }
228             } else {
229             $self->baseline($self->range->lower);
230             }
231              
232             if($self->fudge_amount) {
233             my $span = $self->range->span;
234             my $lower = $self->range->lower;
235             $self->range->lower($lower - abs($span * $self->fudge_amount));
236             my $upper = $self->range->upper;
237             $self->range->upper($upper + ($span * $self->fudge_amount));
238             }
239              
240             if($self->show_ticks && !scalar(@{ $self->tick_values })) {
241             $self->tick_values($self->divvy);
242             }
243              
244             # Return now without setting a min height or width and allow
245             # Layout::Manager to to set it for us, this is how we 'hide'
246             return if $self->hidden;
247              
248             my $tfont = $self->tick_font;
249              
250             my $bheight = 0;
251             my $bwidth = 0;
252              
253             # Determine all this once... much faster.
254             my $i = 0;
255             foreach my $val (@{ $self->tick_values }) {
256             if($self->has_skip_range && $self->skip_range->contains($val)) {
257             # If the label falls in inside the skip range, it's not to be
258             # used at all.
259             next;
260             }
261              
262             my $label = $val;
263             if(defined($self->tick_labels)) {
264             if (defined $self->tick_labels->[$i]) {
265             $label = $self->tick_labels->[$i];
266             }
267             else {
268             $label = "";
269             }
270             } else {
271             $label = $self->format_value($val);
272             }
273              
274             my $tlabel = Graphics::Primitive::TextBox->new(
275             text => $label,
276             font => $tfont,
277             color => $self->tick_label_color,
278             );
279             if($self->tick_label_angle) {
280             $tlabel->angle($self->tick_label_angle);
281             }
282             my $lay = $driver->get_textbox_layout($tlabel);
283              
284             $tlabel->prepare($driver);
285              
286             $tlabel->width($lay->width);
287             $tlabel->height($lay->height);
288              
289             $bwidth = $tlabel->width if($tlabel->width > $bwidth);
290             $bheight = $tlabel->height if($tlabel->height > $bheight);
291              
292             $self->add_component($tlabel);
293             $i++;
294             }
295              
296             my $big = $bheight;
297             if($self->is_vertical) {
298             $big = $bwidth;
299             }
300              
301             my $label_width = 0;
302             my $label_height = 0;
303              
304             if ($self->label) {
305             my $label = Graphics::Primitive::TextBox->new(
306             # angle => $angle,
307             color => $self->label_color,
308             name => 'label',
309             font => $self->label_font,
310             text => $self->label,
311             width => $self->height,
312             horizontal_align => 'center'
313             );
314             $label->name('label');
315 11     11   124 use Graphics::Color::RGB;
  11         28  
  11         18440  
316             $label->border->color(Graphics::Color::RGB->new(r => 0, g => 0, b => 0));
317            
318              
319             if($self->is_vertical) {
320             if ($self->is_left) {
321             $label->angle(-&pip2);
322             } else {
323             $label->angle(&pip2)
324             }
325             }
326              
327              
328             $label->prepare($driver);
329              
330             $label->width($label->minimum_width);
331             $label->height($label->minimum_height);
332              
333             $label_width = $label->width;
334             $label_height = $label->height;
335             $self->add_component($label);
336             }
337              
338             if($self->is_vertical) {
339             my $new_min_width = $self->outside_width + $big + $label_width;
340             $self->minimum_width($new_min_width) if $new_min_width > $self->minimum_width;
341             my $new_min_height = $self->outside_height + $big;
342             $self->minimum_height($new_min_height) if $new_min_height > $self->minimum_height;
343             } else {
344             my $new_min_height = $self->outside_height + $big + $label_height;
345             my $new_min_width = $self->outside_width + $big;
346             $self->minimum_height($new_min_height) if $new_min_height > $self->minimum_height;
347             $self->minimum_width($new_min_width) if $new_min_width > $self->minimum_width;
348             if($self->staggered) {
349             $self->minimum_height($self->minimum_height * 2);
350             }
351             }
352              
353             return 1;
354             });
355              
356              
357             sub mark {
358 0     0 1 0 my ($self, $span, $value) = @_;
359              
360 0 0       0 return undef if not defined $value;
361 0 0       0 if($self->has_skip_range) {
362             # We must completely ignore values that fall inside the skip range,
363             # so we return an undef.
364 0 0       0 return undef if $self->skip_range->contains($value);
365 0 0       0 if($value > $self->skip_range->upper) {
366             # If the value was outside the range, but above it then we must
367             # be sure and substract the range we are skipping so that the
368             # value will still fall on the chart.
369 0         0 $value = $value - $self->skip_range->span;
370             }
371             }
372              
373             # 'caching' this here speeds things up. Calling after changing the
374             # range would result in a messed up chart anyway...
375 0 0       0 if(!defined($self->{LOWER})) {
376 0         0 $self->{LOWER} = $self->range->lower;
377 0 0       0 if($self->has_skip_range) {
378             # If we have a skip range then the RSPAN is less the skip range's
379             # span.
380 0         0 $self->{RSPAN} = $self->range->span - $self->skip_range->span;
381             } else {
382 0         0 $self->{RSPAN} = $self->range->span;
383             }
384             }
385              
386 0   0     0 return ($span / $self->{RSPAN}) * ($value - $self->{LOWER} || 0);
387             }
388              
389             override('finalize', sub {
390             my ($self) = @_;
391              
392             if($self->hidden) {
393             # Call the callback, just in case it matters.
394             super;
395             return;
396             }
397              
398             my $x = 0;
399             my $y = 0;
400              
401             my $width = $self->width;
402             my $height = $self->height;
403             my $ibb = $self->inside_bounding_box;
404             my $iox = $ibb->origin->x;
405             my $ioy = $ibb->origin->y;
406              
407             my $iwidth = $ibb->width;
408             my $iheight = $ibb->height;
409              
410             if($self->is_left) {
411             $x += $width;
412             } elsif($self->is_right) {
413             # nuffin
414             } elsif($self->is_top) {
415             $y += $height;
416             } else {
417             # nuffin
418             }
419              
420             my $lower = $self->range->lower;
421             my $upper = $self->range->upper;
422              
423             my @values = @{ $self->tick_values };
424              
425             if($self->is_vertical) {
426              
427             my $comp_count = 0;
428             for(0..$#values) {
429             my $val = $values[$_];
430             my $mark = $self->mark($height, $val);
431             next unless defined($mark);
432             my $iy = $height - $mark;
433             my $label = $self->get_component($comp_count);
434              
435             # Adjust text on the Y axis to fit when it is
436             # too close to the edges
437             my $standardYOrigin = $iy - ($label->height / 2);
438             #my $lowerYOrigin = $iy - $label->height;
439             my $lowerYOrigin = $ioy + $iheight - $label->height;
440             my $upperYOrigin = $ioy;
441             if($standardYOrigin >= $lowerYOrigin) {
442             # The first label (being at the bottom) needs to be
443             # skooched up a bit to fit.
444             $label->origin->y($lowerYOrigin);
445             } elsif($standardYOrigin <= $upperYOrigin) {
446             # The last label (being at the top) can be positioned
447             # exactly at the mark.
448             $label->origin->y($upperYOrigin);
449             } else {
450             $label->origin->y($standardYOrigin);
451             }
452              
453             if($self->is_left) {
454             $label->origin->x($iox + $iwidth - $label->width);
455             } else {
456             $label->origin->x($iox);
457             }
458             # Keep track of how many components we've actually grabbed, since
459             # we could be skipping any that are in a skip range.
460             $comp_count++;
461             }
462              
463             # Draw the label
464             # FIXME Not working, rotated text labels...
465             if($self->label) {
466             my $label = $self->get_component($self->find_component('label'));
467              
468             if($self->is_left) {
469              
470             $label->origin->x($iox);
471             $label->origin->y(($height - $label->height) / 2);
472             } else {
473              
474             $label->origin->x($iox + $iwidth - $label->width);
475             $label->origin->y(($height - $label->height) / 2);
476             }
477             }
478             } else {
479             # Draw a tick for each value.
480             my $comp_count = 0;
481             for(0..$#values) {
482             my $val = $values[$_];
483             my $ix = $self->mark($width, $val);
484             next unless defined($ix);
485              
486             my $label = $self->get_component($comp_count);
487              
488             my $bump = 0;
489             if($self->staggered) {
490             if($_ % 2) {
491             $bump = $iheight / 2;
492             }
493             }
494              
495             # Adjust the X-origin value to ensure it is within the graph
496             # and does not get snipped when too close to the edge.
497             my $standardXOrigin = $ix - ($label->width / 1.8);
498             my $lowerXOrigin = $iox;
499             my $upperXOrigin = $iox + $iwidth - $label->width;
500             if($standardXOrigin <= $lowerXOrigin) {
501             $label->origin->x($lowerXOrigin);
502             } elsif($standardXOrigin >= $upperXOrigin) {
503             $label->origin->x($upperXOrigin);
504             } else {
505             $label->origin->x($standardXOrigin);
506             }
507              
508             if($self->is_top) {
509             $label->origin->y($ioy + $iheight - $label->height - $bump);
510             } else {
511             $label->origin->y($ioy + $bump);
512             }
513             # Keep track of how many components we've actually grabbed, since
514             # we could be skipping any that are in a skip range.
515             $comp_count++;
516             }
517              
518             # Draw the label
519             if($self->label) {
520             my $label = $self->get_component($self->find_component('label'));
521              
522             my $ext = $self->{'label_extents_cache'};
523             if ($self->is_bottom) {
524             $label->origin->x(($width - $label->width) / 2);
525             $label->origin->y($height - $label->height
526             - ($self->padding->bottom + $self->margins->bottom
527             + $self->border->top->width
528             )
529             );
530             } else {
531             $label->origin->x(($width - $label->width) / 2);
532             }
533             }
534             }
535              
536             super;
537             });
538              
539              
540             sub format_value {
541 1     1 1 7 my $self = shift;
542 1         1 my $value = shift;
543              
544 1         42 my $format = $self->format;
545 1 50       5 if($format) {
546 1 50       3 if(ref($format) eq 'CODE') {
547 0         0 return &$format($value);
548             } else {
549 1         9 return sprintf($format, $value);
550             }
551              
552             }
553             }
554              
555              
556             sub divvy {
557 5     5 1 14 my $self = shift;
558              
559             # Loads the divvy module once and only once
560             # which implements _real_divvy()
561 5         1282 $self->_tick_division_type_loaded;
562 5         33 return $self->_real_divvy();
563             }
564              
565             __PACKAGE__->meta->make_immutable;
566              
567 11     11   91 no Moose;
  11         21  
  11         115  
568              
569             1;
570              
571             __END__
572              
573             =pod
574              
575             =head1 NAME
576              
577             Chart::Clicker::Axis - An X or Y Axis
578              
579             =head1 VERSION
580              
581             version 2.88
582              
583             =head1 SYNOPSIS
584              
585             use Chart::Clicker::Axis;
586             use Graphics::Primitive::Font;
587             use Graphics::Primitive::Brush;
588              
589             my $axis = Chart::Clicker::Axis->new({
590             label_font => Graphics::Primitive::Font->new,
591             orientation => 'vertical',
592             position => 'left',
593             brush => Graphics::Primitive::Brush->new,
594             visible => 1,
595             });
596              
597             =head1 DESCRIPTION
598              
599             Chart::Clicker::Axis represents the plot of the chart.
600              
601             =head1 ATTRIBUTES
602              
603             =head2 height
604              
605             The height of the axis.
606              
607             =head2 width
608              
609             This axis' width.
610              
611             =head2 tick_label_angle
612              
613             The angle (in radians) to rotate the tick's labels.
614              
615             =head2 tick_division_type
616              
617             Selects the algorithm for dividing the graph axis into labelled ticks.
618              
619             The currently included algorithms are:
620             L<Chart::Clicker::Data::DivisionType::Exact/Exact>,
621             L<Chart::Clicker::Data::DivisionType::LinearRounded/LinearRounded>.
622              
623             You may write your own by providing a Moose Role which includes Role
624             L<Chart::Clicker::Data::DivisionType> and prefixing the module name
625             with + when setting tick_division_type.
626              
627             Chart::Clicker::Axis->new(tick_division_type => '+MyApp::TickDivision');
628              
629             This value should only be set once per axis.
630              
631             =head2 baseline
632              
633             The 'baseline' value of this axis. This is used by some renderers to change
634             the way a value is marked. The Bar render, for instance, considers values
635             below the base to be 'negative'.
636              
637             =head2 brush
638              
639             The brush for this axis. Expects a L<Graphics::Primitve::Brush>.
640              
641             =head2 color
642              
643             The color of the axis' border. Expects a L<Graphics::Color::RGB> object.
644             Defaults to black.
645              
646             =head2 format
647              
648             The format to use for the axis values.
649              
650             If the format is a string then format is applied to each value 'tick' via
651             sprintf. See sprintf perldoc for details! This is useful for situations
652             where the values end up with repeating decimals.
653              
654             If the format is a coderef then that coderef will be executed and the value
655             passed to it as an argument.
656              
657             my $nf = Number::Format->new;
658             $default->domain_axis->format(sub { return $nf->format_number(shift); });
659              
660             =head2 fudge_amount
661              
662             The amount to 'fudge' the span of this axis. You should supply a
663             percentage (in decimal form) and the axis will grow at both ends by the
664             supplied amount. This is useful when you want a bit of padding above and
665             below the dataset.
666              
667             As an example, a fugdge_amount of .10 on an axis with a span of 10 to 50
668             would add 5 to the top and bottom of the axis.
669              
670             =head2 hidden
671              
672             This axis' hidden flag. If this is true then the Axis will not be drawn.
673              
674             =head2 label
675              
676             The label of the axis.
677              
678             =head2 label_color
679              
680             The color of the Axis' labels. Expects a L<Graphics::Color::RGB> object.
681              
682             =head2 label_font
683              
684             The font used for the axis' label. Expects a L<Graphics::Primitive::Font> object.
685              
686             =head2 layout_manager
687              
688             Set/Get the Layout Manager. Defaults to L<Layout::Manager::Absolute>.
689              
690             =head2 orientation
691              
692             The orientation of this axis. See L<Graphics::Primitive::Oriented>.
693              
694             =head2 position
695              
696             The position of the axis on the chart.
697              
698             =head2 range
699              
700             The Range for this axis. Expects a L<Chart::Clicker::Data::Range> object.
701             You may use this to explicitly set an upper and lower bound for the chart:
702              
703             $axis->range->max(1000);
704             $axis->range->min(1);
705              
706             =head2 show_ticks
707              
708             If this is value is false then 'ticks' and their labels will not drawn for
709             this axis.
710              
711             =head2 staggered
712              
713             If true, causes horizontally labeled axes to 'stagger' the labels so that half
714             are at the top of the box and the other half are at the bottom. This makes
715             long, overlapping labels less likely to overlap. It only does something
716             useful with B<horizontal> labels.
717              
718             =head2 skip_range
719              
720             Allows you to specify a range of values that will be skipped completely on
721             this axis. This is often used to trim a large, unremarkable section of data.
722             If, for example, 50% of your values fall below 10 and 50% fall above 100 it
723             is useless to bother charting the 10 to 100 range. Skipping it with this
724             attribute will make for a much more useful chart, albeit somewhat visually
725             skewed.
726              
727             $axis->skip_range(Chart::Clicker::Data::Range->new(lower => 10, upper => 100));
728              
729             Note that B<any> data points, including ticks, that fall inside the range
730             specified will be completely ignored.
731              
732             =head2 tick_font
733              
734             The font used for the axis' ticks. Expects a L<Graphics::Primitive::Font> object.
735              
736             =head2 tick_label_color
737              
738             The color of the tick labels. Expects a L<Graphics::Color::RGB> object.
739              
740             =head2 tick_labels
741              
742             The arrayref of labels to show for ticks on this Axis. This arrayref is
743             consulted for every tick, in order. So placing a string at the zeroeth index
744             will result in it being displayed on the zeroeth tick, etc, etc.
745              
746             =head2 tick_values
747              
748             The arrayref of values show as ticks on this Axis.
749              
750             =head2 ticks
751              
752             The number of 'ticks' to show. Setting this will divide the range on this
753             axis by the specified value to establish tick values. This will have no
754             effect if you specify tick_values.
755              
756             =head1 METHODS
757              
758             =head2 add_to_tick_values
759              
760             Add a value to the list of tick values.
761              
762             =head2 clear_tick_values
763              
764             Clear all tick values.
765              
766             =head2 tick_value_count
767              
768             Get a count of tick values.
769              
770             =head2 mark
771              
772             Given a span and a value, returns it's pixel position on this Axis.
773              
774             =head2 format_value
775              
776             Given a value, returns it formatted using this Axis' formatter.
777              
778             =head2 divvy
779              
780             Retrieves the divisions or ticks for the axis.
781              
782             =head1 AUTHOR
783              
784             Cory G Watson <gphat@cpan.org>
785              
786             =head1 COPYRIGHT AND LICENSE
787              
788             This software is copyright (c) 2014 by Cold Hard Code, LLC.
789              
790             This is free software; you can redistribute it and/or modify it under
791             the same terms as the Perl 5 programming language system itself.
792              
793             =cut