1package Chart::Clicker::Renderer::Line;
2$Chart::Clicker::Renderer::Line::VERSION = '2.90';
3use Moose;
4
5# ABSTRACT: Line renderer
6
7extends 'Chart::Clicker::Renderer';
8
9use Geometry::Primitive::Point;
10use Graphics::Primitive::Brush;
11use Graphics::Primitive::Operation::Stroke;
12use Geometry::Primitive::Circle;
13
14#number of defined points we must have around another point
15#to render a line instead of a scatter
16#
17use constant MIN_DEFINED_SURROUNDING_POINTS => 5;
18
19
20has 'brush' => (
21    is => 'rw',
22    isa => 'Graphics::Primitive::Brush',
23    default => sub { Graphics::Primitive::Brush->new(width => 2) }
24);
25
26
27has 'shape' => (
28    is => 'rw',
29    isa => 'Geometry::Primitive::Shape',
30);
31
32
33has 'shape_brush' => (
34    is => 'rw',
35    isa => 'Graphics::Primitive::Brush',
36);
37# TODO Readd shapes
38
39sub finalize {
40    my ($self) = @_;
41
42    my $width = $self->width;
43    my $height = $self->height;
44
45    my $clicker = $self->clicker;
46
47    my $dses = $clicker->get_datasets_for_context($self->context);
48    my %accum;
49    foreach my $ds (@{ $dses }) {
50        foreach my $series (@{ $ds->series }) {
51
52            # TODO if undef...
53            my $ctx = $clicker->get_context($ds->context);
54            my $domain = $ctx->domain_axis;
55            my $range = $ctx->range_axis;
56
57            my $color = $clicker->color_allocator->next;
58
59            my @vals = @{ $series->values };
60            my @keys = @{ $series->keys };
61
62            my $kcount = $series->key_count - 1;
63
64            my $skip = 0;
65            my $previous_x = -1;
66            my $previous_y = -1;
67            my $min_y_delta_on_same_x = $height / 100;
68
69            for(0..$kcount) {
70
71                my $key = $keys[$_];
72
73                my $x = $domain->mark($width, $key);
74                next unless defined($x);
75                $skip = 1 unless defined $vals[$_];
76                my $ymark = $range->mark($height, $vals[$_]);
77                next unless defined($ymark);
78
79                if($self->additive) {
80                    if(exists($accum{$key})) {
81                        $accum{$key} += $ymark;
82                        $ymark = $accum{$key};
83                    } else {
84                        $accum{$key} = $ymark;
85                    }
86                }
87
88                my $y = $height - $ymark;
89                if( $_ == 0 || $skip ) {
90                    my $lineop = Graphics::Primitive::Operation::Stroke->new(
91                        brush => $self->brush->clone
92                    );
93                    $lineop->brush->color($color);
94                    $self->do($lineop);
95                    $self->move_to($x, $y);
96                    my $start_new_line = 1;
97                    foreach my $i ($_..($_ + MIN_DEFINED_SURROUNDING_POINTS)) {
98                        if ($i > 0 && $i < @vals && !defined($vals[$i])) {
99                            $start_new_line = 0;
100                        }
101                    }
102                    if ($start_new_line){
103                        $skip = 0;
104                    }
105                    else {
106                        my $shape = Geometry::Primitive::Circle->new(radius => 3);
107                        $shape->origin(Geometry::Primitive::Point->new(x => $x, y => $y));
108                        $self->path->add_primitive($shape);
109                        my $fill = Graphics::Primitive::Operation::Fill->new(
110                            paint => Graphics::Primitive::Paint::Solid->new(
111                                color => $color
112                            )
113                        );
114                        $self->do($fill);
115                    }
116                }
117                else {
118                    # when in fast mode, we plot only if we moved by more than
119                    # 1 of a pixel on the X axis or we moved by more than 1%
120                    # of the size of the Y axis.
121                    if( $clicker->plot_mode ne 'fast' ||
122                        $x - $previous_x > 1 ||
123                        abs($y - $previous_y) > $min_y_delta_on_same_x
124                      )
125                    {
126                        $self->line_to($x, $y);
127                        $previous_x = $x;
128                        $previous_y = $y;
129                    }
130                }
131
132            }
133            my $op = Graphics::Primitive::Operation::Stroke->new;
134            $op->brush($self->brush->clone);
135            $op->brush->color($color);
136            $self->do($op);
137
138            if(defined($self->shape)) {
139                for(0..$kcount) {
140                    my $key = $keys[$_];
141                    my $x = $domain->mark($width, $key);
142                    next unless defined($x);
143                    my $ymark = $range->mark($height, $vals[$_]);
144                    next unless defined($ymark);
145
146                    if($self->additive) {
147                        if(exists($accum{$key})) {
148                            $ymark = $accum{$key};
149                        } else {
150                            $accum{$key} = $ymark;
151                        }
152                    }
153
154                    my $y = $height - $ymark;
155
156                    $self->move_to($x, $y);
157                    $self->draw_point($x, $y, $series, $vals[$_]);
158                }
159
160                # Fill the shape
161                my $op2 = Graphics::Primitive::Operation::Fill->new(
162                    paint => Graphics::Primitive::Paint::Solid->new(
163                        color => $color
164                    )
165                );
166                if(defined($self->shape_brush)) {
167                    $op2->preserve(1);
168                }
169                $self->do($op2);
170
171                # Optionally stroke the shape
172                if(defined($self->shape_brush)) {
173                    my $op3 = Graphics::Primitive::Operation::Stroke->new;
174                    $op3->brush($self->shape_brush->clone);
175                    $self->do($op3);
176                }
177            }
178        }
179    }
180
181    return 1;
182}
183
184
185sub draw_point {
186    my ($self, $x, $y, $series, $count) = @_;
187
188    my $shape = $self->shape->clone;
189    $shape->origin(Geometry::Primitive::Point->new(x => $x, y => $y));
190    $self->path->add_primitive($shape);
191}
192
193__PACKAGE__->meta->make_immutable;
194
195no Moose;
196
1971;
198
199__END__
200
201=pod
202
203=head1 NAME
204
205Chart::Clicker::Renderer::Line - Line renderer
206
207=head1 VERSION
208
209version 2.90
210
211=head1 SYNOPSIS
212
213  my $lr = Chart::Clicker::Renderer::Line->new(
214    brush => Graphics::Primitive::Brush->new({
215      #...
216    })
217  );
218
219=head1 DESCRIPTION
220
221Chart::Clicker::Renderer::Line renders a dataset as lines.
222
223=for HTML <p><img src="http://gphat.github.com/chart-clicker/static/images/examples/line.png" width="500" height="250" alt="Line Chart" /></p>
224
225=for HTML <p><img src="http://gphat.github.com/chart-clicker/static/images/examples/line-shapes.png" width="500" height="250" alt="Line + Shape Chart" /></p>
226
227=for HTML <p><img src="http://gphat.github.com/chart-clicker/static/images/examples/line-shapes-brushed.png" width="500" height="250" alt="Line + Shape (Brushed) Chart" /></p>
228
229=head1 ATTRIBUTES
230
231=head2 additive
232
233If true, the lines are drawn "stacked", each key accumulates based on those
234drawn below it.
235
236=head2 brush
237
238Set/Get a L<brush|Graphics::Primitive::Brush> to be used for the lines.
239
240=head2 shape
241
242Set a L<shape|Geometry::Primitive::Shape> object to draw at each of the data points.  Adding a shape results
243in:
244
245=head2 shape_brush
246
247Set/Get the L<brush|Graphics::Primitive::Brush> to be used on the shapes at
248each point.  If no shape_brush is provided, then the shapes will be filled.
249The brush allows you to draw a "halo" around each shape.  This sometimes help
250to separate the points from the lines and make them more distinct.
251
252=head1 METHODS
253
254=head2 draw_point
255
256Called for each point encountered on the line.
257
258=head1 AUTHOR
259
260Cory G Watson <gphat@cpan.org>
261
262=head1 COPYRIGHT AND LICENSE
263
264This software is copyright (c) 2016 by Cory G Watson.
265
266This is free software; you can redistribute it and/or modify it under
267the same terms as the Perl 5 programming language system itself.
268
269=cut
270