File Coverage

blib/lib/VS/Chart.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package VS::Chart;
2              
3 6     6   196256 use strict;
  6         15  
  6         219  
4 6     6   28 use warnings;
  6         9  
  6         163  
5              
6 6     6   1257168 use Cairo;
  0            
  0            
7             use Carp qw(croak);
8             use Scalar::Util qw(refaddr);
9              
10             use VS::Chart::Dataset;
11             use VS::Chart::RowIterator;
12              
13             our $VERSION = "0.08";
14              
15             my %Datasets;
16             my %NextRow;
17             my %MaxCache;
18             my %MinCache;
19             my %SpanCache;
20              
21             sub _clear_cache {
22             my $self = shift;
23             my $id = refaddr $self;
24              
25             delete $MaxCache{$id};
26             delete $MinCache{$id};
27             delete $SpanCache{$id};
28              
29             1;
30             }
31              
32             sub new {
33             my ($pkg, %attrs) = @_;
34            
35             my $defaults = 1 - (delete $attrs{no_defaults} || 0);
36            
37             my $self = bless {
38             _defaults => $defaults,
39             %attrs,
40             }, $pkg;
41              
42             $Datasets{refaddr $self} = [];
43             $NextRow{refaddr $self} = 0;
44            
45             return $self;
46             }
47              
48             sub has {
49             my ($self, $key) = @_;
50             return exists $self->{$key};
51             }
52              
53             sub get {
54             my ($self, $key) = @_;
55             return $self->{$key};
56             }
57              
58             sub set {
59             my ($self, %attrs) = @_;
60            
61             while (my ($key, $value) = each %attrs) {
62             if ($key eq 'min') {
63             $self->_clear_cache;
64             my $min = $self->_min;
65             $self->{_min} = $value if $value < $min;
66             next;
67             }
68             elsif ($key eq 'max') {
69             $self->_clear_cache;
70             my $max = $self->_max;
71             $self->{_max} = $value if $value > $max;
72             next;
73             }
74             elsif ($key eq 'y_grid_steps') {
75             $value = 1 if $value < 1;
76             $value = 10 if $value > 10;
77             }
78            
79             if ($key =~ /^(\d+)\s*:\s*(.*)$/) {
80             my $id = $1;
81             $key = $2;
82             my $ds = $self->_dataset($id);
83             if (defined $ds) {
84             $ds->set($key => $value);
85             }
86             }
87             else {
88             $self->{$key} = $value;
89             }
90             }
91             }
92              
93             sub dataset {
94             my ($self, $id) = @_;
95             return $self->_dataset($id, 0);
96             }
97              
98             sub _dataset {
99             my ($self, $idx, $create) = @_;
100             my $ptr = refaddr $self;
101             return $Datasets{$ptr}->[$idx] if defined $Datasets{$ptr}->[$idx];
102             return undef if !$create;
103            
104             $Datasets{$ptr}->[$idx] = VS::Chart::Dataset->new();
105            
106             return $self->_dataset($idx);
107             }
108              
109             sub _datasets {
110             my ($self) = @_;
111             return $Datasets{refaddr $self};
112             };
113              
114             sub rows {
115             my ($self) = @_;
116            
117             return $NextRow{refaddr $self};
118             }
119              
120             sub add {
121             my ($self, @data) = @_;
122            
123             my $id = refaddr $self;
124            
125             delete $self->{_max};
126             delete $self->{_min};
127              
128             $self->_clear_cache;
129            
130             my $row = $self->rows;
131              
132             if (ref $data[0]) {
133             $self->set(x_column => 1);
134             }
135            
136             for (my $ds = 0; $ds < @data; $ds++) {
137             my $dataset = $self->_dataset($ds, 1);
138             $dataset->insert($row, $data[$ds]);
139             }
140            
141             $NextRow{$id}++;
142            
143             1;
144             }
145              
146             sub _max {
147             my ($self) = @_;
148            
149             return $self->{_max} if exists $self->{_max};
150              
151             my $id = refaddr $self;
152             return $MaxCache{$id} if exists $MaxCache{$id};
153            
154             my $datasets = $self->_datasets;
155             return 0 unless @$datasets;
156              
157             my $x_column = $self->get("x_column") || 0;
158            
159             my $max = $datasets->[$x_column]->max;
160             for (($x_column + 1)..@$datasets - 1) {
161             my $ds_max = $datasets->[$_]->max;
162             next if !defined $ds_max;
163             $max = $ds_max if $ds_max > $max;
164             }
165            
166             $MaxCache{$id} = $max;
167            
168             return $max;
169             }
170              
171             sub _min {
172             my ($self) = @_;
173            
174             return $self->{_min} if exists $self->{_min};
175              
176             my $id = refaddr $self;
177             return $MinCache{$id} if exists $MinCache{$id};
178            
179             my $datasets = $self->_datasets;
180             return 0 unless @$datasets;
181              
182             my $x_column = $self->get("x_column") || 0;
183             my $min = $datasets->[$x_column]->min;
184             for (($x_column + 1)..@$datasets - 1) {
185             my $ds_min = $datasets->[$_]->min;
186             next if !defined $ds_min;
187             $min = $ds_min if $ds_min < $min;
188             }
189            
190             $MinCache{$id} = $min;
191            
192             return $min;
193             }
194              
195             sub _span {
196             my ($self) = @_;
197              
198             my $id = refaddr $self;
199             return $SpanCache{$id} if exists $SpanCache{$id};
200            
201             my $span = $self->_max - $self->_min;
202             $SpanCache{$id} = $span;
203              
204             return $span;
205             }
206              
207             sub _row_iterator {
208             my ($self) = @_;
209             my $x_column = $self->get("x_column") || 0;
210             if ($x_column) {
211             return VS::Chart::RowIterator->new($self->_dataset(0)->data);
212             }
213             return VS::Chart::RowIterator->new([1..$self->rows]);
214             }
215              
216             sub _offset {
217             my ($self, $value) = @_;
218            
219             if ($value < $self->_min || $value > $self->_max) {
220             croak "Value '${value}' is outside value range (", $self->_min, ", ", $self->_max, ")";
221             }
222              
223             return ($value - $self->_min) / $self->_span;
224             }
225              
226             sub _offsets {
227             my ($self, @values) = @_;
228            
229             my $min = $self->_min;
230             my $span = $self->_span;
231              
232             for (@values) {
233             $_ = ($_ - $min) / $self->_span;
234             }
235            
236             return @values;
237             }
238              
239             {
240             use Module::Pluggable
241             search_path => [qw(VS::Chart::Renderer)],
242             require => 1,
243             sub_name => 'renderers',
244             inner => 0;
245            
246             my %Renderer;
247             BEGIN {
248             for (__PACKAGE__->renderers) {
249             if ($_->can("type")) {
250             my $type = lc($_->type);
251             $Renderer{$type} = $_;
252             }
253             }
254             }
255            
256             sub supported_types {
257             return sort keys %Renderer;
258             }
259            
260             my %Create = (
261             'png' => sub {
262             my $self = shift;
263             return Cairo::ImageSurface->create("argb32", $self->get("width"), $self->get("height"));
264             },
265             'svg' => sub {
266             my ($self, $path) = @_;
267             return Cairo::SvgSurface->create($path, $self->get("width"), $self->get("height"));
268             },
269             'pdf' => sub {
270             my ($self, $path) = @_;
271             return Cairo::PdfSurface->create($path, $self->get("width"), $self->get("height"));
272             },
273             );
274              
275             my %Save = (
276             'png' => sub {
277             my ($surface, $target) = @_;
278            
279             if (ref $target eq "CODE") {
280             $surface->write_to_png_stream($target)
281             }
282             else {
283             $surface->write_to_png($target);
284             }
285             },
286             'svg' => sub {
287             },
288             'pdf' => sub {
289             },
290             );
291            
292             sub render {
293             my ($self, %args) = @_;
294              
295             croak "Missing argument 'type'" if !exists $args{type};
296             my $type = $args{type};
297             croak "Unsupported chart type: $type" if !exists $Renderer{$type};
298              
299             croak "Missing argument 'to'" if !exists $args{to};
300             my $to = $args{to};
301              
302             my $as;
303             if (exists $args{as}) {
304             $as = $args{as};
305             }
306             else {
307             ($as) = $to =~ /\.(\w+)$/;
308             $as = lc $as;
309             }
310              
311             croak "Unsupported output: $as" if !exists $Create{$as};
312            
313             local $self->{width} = 640 unless $self->{width};
314             local $self->{height} = 480 unless $self->{height};
315             local $self->{width} = $args{width} if $args{width};
316             local $self->{height} = $args{height} if $args{height};
317            
318             my $renderer = $Renderer{$type}->new;
319             my @default_keys = $renderer->set_defaults($self);
320            
321             my $surface = $Create{$as}->($self, $to);
322             $renderer->render($self, $surface);
323             $Save{$as}->($surface, $to);
324             }
325             }
326              
327             1;
328             __END__