File Coverage

blib/lib/Chart/Clicker/Axis.pm
Criterion Covered Total %
statement 50 65 76.9
branch 4 20 20.0
condition 0 2 0.0
subroutine 14 15 93.3
pod 3 4 75.0
total 71 106 66.9


line stmt bran cond sub pod time code
1             package Chart::Clicker::Axis;
2             $Chart::Clicker::Axis::VERSION = '2.90';
3 11     11   182309 use Moose;
  11         1053875  
  11         79  
4 11     11   73697 use Moose::Util;
  11         26  
  11         106  
5              
6             extends 'Chart::Clicker::Container';
7             with 'Chart::Clicker::Positioned';
8              
9             # ABSTRACT: An X or Y Axis
10              
11 11     11   2259 use Class::Load;
  11         32  
  11         730  
12              
13 11     11   4928 use Chart::Clicker::Data::Range;
  11         31  
  11         563  
14              
15 11     11   8093 use English qw(-no_match_vars);
  11         41199  
  11         81  
16              
17 11     11   6029 use Graphics::Color::RGB;
  11         2544075  
  11         435  
18              
19 11     11   7090 use Graphics::Primitive::Font;
  11         1159870  
  11         611  
20              
21 11     11   8893 use Layout::Manager::Absolute;
  11         176339  
  11         605  
22              
23 11     11   5589 use Math::Trig ':pi';
  11         95508  
  11         27958  
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 54 my ($self) = @_;
171              
172 27         1230 $self->padding(3);
173             }
174              
175             sub _build__tick_division_type_loaded {
176 5     5   13 my $self = shift;
177              
178             # User modules are prefixed with a +.
179 5         10 my $divisionTypeModule;
180 5         44 my $extensionOf = 'Chart::Clicker::Axis::DivisionType';
181 5 50       313 if ( $self->tick_division_type =~ m/^\+(.*)$/xmisg ) {
182 0         0 $divisionTypeModule = $1;
183             }
184             else {
185 5         367 $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         45 Class::Load::load_class($divisionTypeModule);
191              
192             # Apply the newly loaded role to this class.
193 5         351 Moose::Util::apply_all_roles( $self => $divisionTypeModule );
194 5 50       157789 if ( not $self->does($extensionOf) ) {
195 0         0 die("Module $divisionTypeModule does not extend $extensionOf");
196             }
197              
198 5         2599 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             $label->border->color(Graphics::Color::RGB->new(r => 0, g => 0, b => 0));
316              
317              
318             if($self->is_vertical) {
319             if ($self->is_left) {
320             $label->angle(-&pip2);
321             } else {
322             $label->angle(&pip2)
323             }
324             }
325              
326              
327             $label->prepare($driver);
328              
329             $label->width($label->minimum_width);
330             $label->height($label->minimum_height);
331              
332             $label_width = $label->width;
333             $label_height = $label->height;
334             $self->add_component($label);
335             }
336              
337             if($self->is_vertical) {
338             my $new_min_width = $self->outside_width + $big + $label_width;
339             $self->minimum_width($new_min_width) if $new_min_width > $self->minimum_width;
340             my $new_min_height = $self->outside_height + $big;
341             $self->minimum_height($new_min_height) if $new_min_height > $self->minimum_height;
342             } else {
343             my $new_min_height = $self->outside_height + $big + $label_height;
344             my $new_min_width = $self->outside_width + $big;
345             $self->minimum_height($new_min_height) if $new_min_height > $self->minimum_height;
346             $self->minimum_width($new_min_width) if $new_min_width > $self->minimum_width;
347             if($self->staggered) {
348             $self->minimum_height($self->minimum_height * 2);
349             }
350             }
351              
352             return 1;
353             });
354              
355              
356             sub mark {
357 0     0 1 0 my ($self, $span, $value) = @_;
358              
359 0 0       0 return undef if not defined $value;
360 0 0       0 if($self->has_skip_range) {
361             # We must completely ignore values that fall inside the skip range,
362             # so we return an undef.
363 0 0       0 return undef if $self->skip_range->contains($value);
364 0 0       0 if($value > $self->skip_range->upper) {
365             # If the value was outside the range, but above it then we must
366             # be sure and substract the range we are skipping so that the
367             # value will still fall on the chart.
368 0         0 $value = $value - $self->skip_range->span;
369             }
370             }
371              
372             # 'caching' this here speeds things up. Calling after changing the
373             # range would result in a messed up chart anyway...
374 0 0       0 if(!defined($self->{LOWER})) {
375 0         0 $self->{LOWER} = $self->range->lower;
376 0 0       0 if($self->has_skip_range) {
377             # If we have a skip range then the RSPAN is less the skip range's
378             # span.
379 0         0 $self->{RSPAN} = $self->range->span - $self->skip_range->span;
380             } else {
381 0         0 $self->{RSPAN} = $self->range->span;
382             }
383             }
384              
385 0   0     0 return ($span / $self->{RSPAN}) * ($value - $self->{LOWER} || 0);
386             }
387              
388             override('finalize', sub {
389             my ($self) = @_;
390              
391             if($self->hidden) {
392             # Call the callback, just in case it matters.
393             super;
394             return;
395             }
396              
397             my $x = 0;
398             my $y = 0;
399              
400             my $width = $self->width;
401             my $height = $self->height;
402             my $ibb = $self->inside_bounding_box;
403             my $iox = $ibb->origin->x;
404             my $ioy = $ibb->origin->y;
405              
406             my $iwidth = $ibb->width;
407             my $iheight = $ibb->height;
408              
409             if($self->is_left) {
410             $x += $width;
411             } elsif($self->is_right) {
412             # nuffin
413             } elsif($self->is_top) {
414             $y += $height;
415             } else {
416             # nuffin
417             }
418              
419             my $lower = $self->range->lower;
420             my $upper = $self->range->upper;
421              
422             my @values = @{ $self->tick_values };
423              
424             if($self->is_vertical) {
425              
426             my $comp_count = 0;
427             for(0..$#values) {
428             my $val = $values[$_];
429             my $mark = $self->mark($height, $val);
430             next unless defined($mark);
431             my $iy = $height - $mark;
432             my $label = $self->get_component($comp_count);
433              
434             # Adjust text on the Y axis to fit when it is
435             # too close to the edges
436             my $standardYOrigin = $iy - ($label->height / 2);
437             #my $lowerYOrigin = $iy - $label->height;
438             my $lowerYOrigin = $ioy + $iheight - $label->height;
439             my $upperYOrigin = $ioy;
440             if($standardYOrigin >= $lowerYOrigin) {
441             # The first label (being at the bottom) needs to be
442             # skooched up a bit to fit.
443             $label->origin->y($lowerYOrigin);
444             } elsif($standardYOrigin <= $upperYOrigin) {
445             # The last label (being at the top) can be positioned
446             # exactly at the mark.
447             $label->origin->y($upperYOrigin);
448             } else {
449             $label->origin->y($standardYOrigin);
450             }
451              
452             if($self->is_left) {
453             $label->origin->x($iox + $iwidth - $label->width);
454             } else {
455             $label->origin->x($iox);
456             }
457             # Keep track of how many components we've actually grabbed, since
458             # we could be skipping any that are in a skip range.
459             $comp_count++;
460             }
461              
462             # Draw the label
463             # FIXME Not working, rotated text labels...
464             if($self->label) {
465             my $label = $self->get_component($self->find_component('label'));
466              
467             if($self->is_left) {
468              
469             $label->origin->x($iox);
470             $label->origin->y(($height - $label->height) / 2);
471             } else {
472              
473             $label->origin->x($iox + $iwidth - $label->width);
474             $label->origin->y(($height - $label->height) / 2);
475             }
476             }
477             } else {
478             # Draw a tick for each value.
479             my $comp_count = 0;
480             for(0..$#values) {
481             my $val = $values[$_];
482             my $ix = $self->mark($width, $val);
483             next unless defined($ix);
484              
485             my $label = $self->get_component($comp_count);
486              
487             my $bump = 0;
488             if($self->staggered) {
489             if($_ % 2) {
490             $bump = $iheight / 2;
491             }
492             }
493              
494             # Adjust the X-origin value to ensure it is within the graph
495             # and does not get snipped when too close to the edge.
496             my $standardXOrigin = $ix - ($label->width / 1.8);
497             my $lowerXOrigin = $iox;
498             my $upperXOrigin = $iox + $iwidth - $label->width;
499             if($standardXOrigin <= $lowerXOrigin) {
500             $label->origin->x($lowerXOrigin);
501             } elsif($standardXOrigin >= $upperXOrigin) {
502             $label->origin->x($upperXOrigin);
503             } else {
504             $label->origin->x($standardXOrigin);
505             }
506              
507             if($self->is_top) {
508             $label->origin->y($ioy + $iheight - $label->height - $bump);
509             } else {
510             $label->origin->y($ioy + $bump);
511             }
512             # Keep track of how many components we've actually grabbed, since
513             # we could be skipping any that are in a skip range.
514             $comp_count++;
515             }
516              
517             # Draw the label
518             if($self->label) {
519             my $label = $self->get_component($self->find_component('label'));
520              
521             my $ext = $self->{'label_extents_cache'};
522             if ($self->is_bottom) {
523             $label->origin->x(($width - $label->width) / 2);
524             $label->origin->y($height - $label->height
525             - ($self->padding->bottom + $self->margins->bottom
526             + $self->border->top->width
527             )
528             );
529             } else {
530             $label->origin->x(($width - $label->width) / 2);
531             }
532             }
533             }
534              
535             super;
536             });
537              
538              
539             sub format_value {
540 1     1 1 6 my $self = shift;
541 1         2 my $value = shift;
542              
543 1         30 my $format = $self->format;
544 1 50       4 if($format) {
545 1 50       4 if(ref($format) eq 'CODE') {
546 0         0 return &$format($value);
547             } else {
548 1         9 return sprintf($format, $value);
549             }
550              
551             }
552             }
553              
554              
555             sub divvy {
556 5     5 1 14 my $self = shift;
557              
558             # Loads the divvy module once and only once
559             # which implements _real_divvy()
560 5         287 $self->_tick_division_type_loaded;
561 5         43 return $self->_real_divvy();
562             }
563              
564             __PACKAGE__->meta->make_immutable;
565              
566 11     11   105 no Moose;
  11         25  
  11         157  
567              
568             1;
569              
570             __END__
571              
572             =pod
573              
574             =head1 NAME
575              
576             Chart::Clicker::Axis - An X or Y Axis
577              
578             =head1 VERSION
579              
580             version 2.90
581              
582             =head1 SYNOPSIS
583              
584             use Chart::Clicker::Axis;
585             use Graphics::Primitive::Font;
586             use Graphics::Primitive::Brush;
587              
588             my $axis = Chart::Clicker::Axis->new({
589             label_font => Graphics::Primitive::Font->new,
590             orientation => 'vertical',
591             position => 'left',
592             brush => Graphics::Primitive::Brush->new,
593             visible => 1,
594             });
595              
596             =head1 DESCRIPTION
597              
598             Chart::Clicker::Axis represents the plot of the chart.
599              
600             =head1 ATTRIBUTES
601              
602             =head2 height
603              
604             The height of the axis.
605              
606             =head2 width
607              
608             This axis' width.
609              
610             =head2 tick_label_angle
611              
612             The angle (in radians) to rotate the tick's labels.
613              
614             =head2 tick_division_type
615              
616             Selects the algorithm for dividing the graph axis into labelled ticks.
617              
618             The currently included algorithms are:
619             L<Chart::Clicker::Data::DivisionType::Exact/Exact>,
620             L<Chart::Clicker::Data::DivisionType::LinearRounded/LinearRounded>.
621              
622             You may write your own by providing a Moose Role which includes Role
623             L<Chart::Clicker::Data::DivisionType> and prefixing the module name
624             with + when setting tick_division_type.
625              
626             Chart::Clicker::Axis->new(tick_division_type => '+MyApp::TickDivision');
627              
628             This value should only be set once per axis.
629              
630             =head2 baseline
631              
632             The 'baseline' value of this axis. This is used by some renderers to change
633             the way a value is marked. The Bar render, for instance, considers values
634             below the base to be 'negative'.
635              
636             =head2 brush
637              
638             The brush for this axis. Expects a L<Graphics::Primitve::Brush>.
639              
640             =head2 color
641              
642             The color of the axis' border. Expects a L<Graphics::Color::RGB> object.
643             Defaults to black.
644              
645             =head2 format
646              
647             The format to use for the axis values.
648              
649             If the format is a string then format is applied to each value 'tick' via
650             sprintf. See sprintf perldoc for details! This is useful for situations
651             where the values end up with repeating decimals.
652              
653             If the format is a coderef then that coderef will be executed and the value
654             passed to it as an argument.
655              
656             my $nf = Number::Format->new;
657             $default->domain_axis->format(sub { return $nf->format_number(shift); });
658              
659             =head2 fudge_amount
660              
661             The amount to 'fudge' the span of this axis. You should supply a
662             percentage (in decimal form) and the axis will grow at both ends by the
663             supplied amount. This is useful when you want a bit of padding above and
664             below the dataset.
665              
666             As an example, a fugdge_amount of .10 on an axis with a span of 10 to 50
667             would add 5 to the top and bottom of the axis.
668              
669             =head2 hidden
670              
671             This axis' hidden flag. If this is true then the Axis will not be drawn.
672              
673             =head2 label
674              
675             The label of the axis.
676              
677             =head2 label_color
678              
679             The color of the Axis' labels. Expects a L<Graphics::Color::RGB> object.
680              
681             =head2 label_font
682              
683             The font used for the axis' label. Expects a L<Graphics::Primitive::Font> object.
684              
685             =head2 layout_manager
686              
687             Set/Get the Layout Manager. Defaults to L<Layout::Manager::Absolute>.
688              
689             =head2 orientation
690              
691             The orientation of this axis. See L<Graphics::Primitive::Oriented>.
692              
693             =head2 position
694              
695             The position of the axis on the chart.
696              
697             =head2 range
698              
699             The Range for this axis. Expects a L<Chart::Clicker::Data::Range> object.
700             You may use this to explicitly set an upper and lower bound for the chart:
701              
702             $axis->range->max(1000);
703             $axis->range->min(1);
704              
705             =head2 show_ticks
706              
707             If this is value is false then 'ticks' and their labels will not drawn for
708             this axis.
709              
710             =head2 staggered
711              
712             If true, causes horizontally labeled axes to 'stagger' the labels so that half
713             are at the top of the box and the other half are at the bottom. This makes
714             long, overlapping labels less likely to overlap. It only does something
715             useful with B<horizontal> labels.
716              
717             =head2 skip_range
718              
719             Allows you to specify a range of values that will be skipped completely on
720             this axis. This is often used to trim a large, unremarkable section of data.
721             If, for example, 50% of your values fall below 10 and 50% fall above 100 it
722             is useless to bother charting the 10 to 100 range. Skipping it with this
723             attribute will make for a much more useful chart, albeit somewhat visually
724             skewed.
725              
726             $axis->skip_range(Chart::Clicker::Data::Range->new(lower => 10, upper => 100));
727              
728             Note that B<any> data points, including ticks, that fall inside the range
729             specified will be completely ignored.
730              
731             =head2 tick_font
732              
733             The font used for the axis' ticks. Expects a L<Graphics::Primitive::Font> object.
734              
735             =head2 tick_label_color
736              
737             The color of the tick labels. Expects a L<Graphics::Color::RGB> object.
738              
739             =head2 tick_labels
740              
741             The arrayref of labels to show for ticks on this Axis. This arrayref is
742             consulted for every tick, in order. So placing a string at the zeroeth index
743             will result in it being displayed on the zeroeth tick, etc, etc.
744              
745             =head2 tick_values
746              
747             The arrayref of values show as ticks on this Axis.
748              
749             =head2 ticks
750              
751             The number of 'ticks' to show. Setting this will divide the range on this
752             axis by the specified value to establish tick values. This will have no
753             effect if you specify tick_values.
754              
755             =head1 METHODS
756              
757             =head2 add_to_tick_values
758              
759             Add a value to the list of tick values.
760              
761             =head2 clear_tick_values
762              
763             Clear all tick values.
764              
765             =head2 tick_value_count
766              
767             Get a count of tick values.
768              
769             =head2 mark
770              
771             Given a span and a value, returns it's pixel position on this Axis.
772              
773             =head2 format_value
774              
775             Given a value, returns it formatted using this Axis' formatter.
776              
777             =head2 divvy
778              
779             Retrieves the divisions or ticks for the axis.
780              
781             =head1 AUTHOR
782              
783             Cory G Watson <gphat@cpan.org>
784              
785             =head1 COPYRIGHT AND LICENSE
786              
787             This software is copyright (c) 2016 by Cory G Watson.
788              
789             This is free software; you can redistribute it and/or modify it under
790             the same terms as the Perl 5 programming language system itself.
791              
792             =cut