File Coverage

blib/lib/Data/Google/Visualization/DataTable.pm
Criterion Covered Total %
statement 206 222 92.7
branch 75 102 73.5
condition 19 31 61.2
subroutine 16 17 94.1
pod 8 8 100.0
total 324 380 85.2


line stmt bran cond sub pod time code
1             package Data::Google::Visualization::DataTable;
2             BEGIN {
3 5     5   1216217 $Data::Google::Visualization::DataTable::VERSION = '0.10';
4             }
5              
6 5     5   41 use strict;
  5         9  
  5         154  
7 5     5   25 use warnings;
  5         11  
  5         180  
8              
9 5     5   32 use Carp qw(croak carp);
  5         9  
  5         467  
10 5     5   7162 use Storable qw(dclone);
  5         24406  
  5         522  
11 5     5   248414 use Time::Local;
  5         11515  
  5         20368  
12              
13             =head1 NAME
14              
15             Data::Google::Visualization::DataTable - Easily create Google DataTable objects
16              
17             =head1 VERSION
18              
19             version 0.10
20              
21             =head1 DESCRIPTION
22              
23             Easily create Google DataTable objects without worrying too much about typed
24             data
25              
26             =head1 OVERVIEW
27              
28             Google's excellent Visualization suite requires you to format your Javascript
29             data very carefully. It's entirely possible to do this by hand, especially with
30             the help of the most excellent L but it's a bit fiddly, largely
31             because Perl doesn't natively support data types and Google's API accepts a
32             super-set of JSON - see L below.
33              
34             This module is attempts to hide the gory details of preparing your data before
35             sending it to a JSON serializer - more specifically, hiding some of the hoops
36             that have to be jump through for making sure your data serializes to the right
37             data types.
38              
39             More about the
40             L.
41              
42             Every effort has been made to keep naming conventions as close as possible to
43             those in the API itself.
44              
45             B
46             familiar with L and L.>
47              
48             =head1 SYNOPSIS
49              
50             use Data::Google::Visualization::DataTable;
51              
52             my $datatable = Data::Google::Visualization::DataTable->new();
53              
54             $datatable->add_columns(
55             { id => 'date', label => "A Date", type => 'date', p => {}},
56             { id => 'datetime', label => "A Datetime", type => 'datetime' },
57             { id => 'timeofday',label => "A Time of Day", type => 'timeofday' },
58             { id => 'bool', label => "True or False", type => 'boolean' },
59             { id => 'number', label => "Number", type => 'number' },
60             { id => 'string', label => "Some String", type => 'string' },
61             );
62              
63             $datatable->add_rows(
64              
65             # Add as array-refs
66             [
67             { v => DateTime->new() },
68             { v => Time::Piece->new(), f => "Right now!" },
69             { v => [6, 12, 1], f => '06:12:01' },
70             { v => 1, f => 'YES' },
71             15.6, # If you're getting lazy
72             { v => 'foobar', f => 'Foo Bar', p => { display => 'none' } },
73             ],
74              
75             # And/or as hash-refs (but only if you defined id's for each of your columns)
76             {
77             date => DateTime->new(),
78             datetime => { v => Time::Piece->new(), f => "Right now!" },
79             timeofday => [6, 12, 1],
80             bool => 1,
81             number => 15.6,
82             string => { v => 'foobar', f => 'Foo Bar' },
83             },
84              
85             );
86              
87             # Get the data...
88              
89             # Fancy-pants
90             my $output = $datatable->output_javascript(
91             columns => ['date','number','string' ],
92             pretty => 1,
93             );
94              
95             # Vanilla
96             my $output = $datatable->output_javascript();
97              
98             =head1 COLUMNS, ROWS AND CELLS
99              
100             We've tried as far as possible to stay as close as possible to the underlying
101             API, so make sure you've had a good read of:
102             L.
103              
104             =head2 Columns
105              
106             I are specified using a hashref, and follow exactly the format of the
107             underlying API itself. All of C, C, C
108             supported. The contents of C

will be passed directly to L to

109             serialize as a whole.
110              
111             =head2 Rows
112              
113             A row is either a hash-ref where the keys are column IDs and the values are
114             I, or an array-ref where the values are I.
115              
116             =head2 Cells
117              
118             I can be specified in several ways, but the best way is using a hash-ref
119             that exactly conforms to the API. C is NOT checked against your data type -
120             but we will attempt to convert it. If you pass in an undefined value, it will
121             return a JS 'null', regardless of the data type. C needs to be a string if
122             you provide it. C

will be bassed directly to L.

123              
124             For any of the date-like fields (C, C, C), you can
125             pass in 4 types of values. We accept L objects, L
126             objects, epoch seconds (as a string - converted internally using
127             L), or an array-ref of values that will be passed
128             directly to the resulting Javascript Date object eg:
129              
130             Perl:
131             date => [ 5, 4, 3 ]
132             JS:
133             new Date( 5, 4, 3 )
134              
135             Remember that JS dates 0-index the month. B
136             Dates and Times below if you want any chance of doing this right>...
137              
138             For non-date fields, if you specify a cell using a string or number, rather than
139             a hashref, that'll be mapped to a cell with C set to the string you
140             specified.
141              
142             C: we test the value you pass in for truth, the Perl way, although
143             undef values will come out as null, not 0.
144              
145             =head2 Properties
146              
147             Properties can be defined for the whole datatable (using C), for
148             each column (using C

), for each row (using C

) and for each cell (again

149             using C

). The documentation provided is a little unclear as to exactly

150             what you're allowed to put in this, so we provide you ample rope and let you
151             specify anything you like.
152              
153             When defining properties for rows, you must use the hashref method of row
154             creation. If you have a column with id of C

, you must use C<_p> as your key

155             for defining properties.
156              
157             =head1 METHODS
158              
159             =head2 new
160              
161             Constructor. B
162             all to the constructor>. Accepts a hashref of arguments:
163              
164             C

- a datatable-wide properties element (see C above and the

165             Google docs).
166              
167             C - defaults to false. An experimental feature for doing dates
168             the right way. See: L for discussion below.
169              
170             C - optional, and defaults to a sensibly configured L
171             object. If you really want to avoid using L for some reason, you can
172             pass in something else here that supports an C method (and also avoid
173             loading L at all, as we lazy-load it). If you just want to configure
174             the L object we use, consider using the C method
175             specified below instead. B.
176              
177             =cut
178              
179             sub new {
180 8     8 1 39921 my $class = shift;
181 8   100     55 my $args = shift || {};
182 8   100     116 my $self = {
183             columns => [],
184             column_mapping => {},
185             rows => [],
186             all_columns_have_ids => 0,
187             column_count => 0,
188             pedantic => 1,
189             with_timezone => ($args->{'with_timezone'} || 0)
190             };
191 8         26 bless $self, $class;
192              
193 8 100       44 $self->{'properties'} = $args->{'p'} if defined $args->{'p'};
194 8   33     177 $self->{'json_xs'} = $args->{'json_object'} ||
195             $self->_create_json_xs_object();
196              
197 8         35 return $self;
198             }
199              
200             # We don't actually need JSON::XS, and in fact, there's a user who'd rather we
201             # didn't insist on it, so we lazy load both the class and our object
202             sub _create_json_xs_object {
203 8     8   20 my $self = shift;
204 8         3713 require JSON::XS;
205 8         19182 return JSON::XS->new()->canonical(1)->allow_nonref;
206             }
207              
208             =head2 add_columns
209              
210             Accepts zero or more columns, in the format specified above, and adds them to
211             our list of columns. Returns the object. You can't call this method after you've
212             called C for the first time.
213              
214             =cut
215              
216             our %ACCEPTABLE_TYPES = map { $_ => 1 } qw(
217             date datetime timeofday boolean number string
218             );
219              
220             our %JAVASCRIPT_RESERVED = map { $_ => 1 } qw(
221             break case catch continue default delete do else finally for function if in
222             instanceof new return switch this throw try typeof var void while with
223             abstract boolean byte char class const debugger double enum export extends
224             final float goto implements import int interface long native package
225             private protected public short static super synchronized throws transient
226             volatile const export import
227             );
228              
229             sub add_columns {
230 8     8 1 110 my ($self, @columns) = @_;
231              
232 8         114 croak "You can't add columns once you've added rows"
233 8 50       15 if @{$self->{'rows'}};
234              
235             # Add the columns to our internal store
236 8         25 for my $column ( @columns ) {
237              
238             # Check the type
239 21         39 my $type = $column->{'type'};
240 21 50       48 croak "Every column must have a 'type'" unless $type;
241 21 50       65 croak "Unknown column type '$type'" unless $ACCEPTABLE_TYPES{ $type };
242              
243             # Check label and ID are sane
244 21         112 for my $key (qw( label id pattern ) ) {
245 63 50 66     247 if ( $column->{$key} && ref( $column->{$key} ) ) {
246 0         0 croak "'$key' needs to be a simple string";
247             }
248             }
249              
250             # Check the 'p' column is ok if it was provided, and convert now to JSON
251 21 100       76 if ( defined($column->{'p'}) ) {
252 3         8 eval { $self->json_xs_object->encode( $column->{'p'} ) };
  3         14  
253 3 50       12 croak "Serializing 'p' failed: $@" if $@;
254             }
255              
256             # ID must be unique
257 21 100       57 if ( $column->{'id'} ) {
258 19         24 my $id = $column->{'id'};
259 19 50       22 if ( grep { $id eq $_->{'id'} } @{ $self->{'columns'} } ) {
  18         59  
  19         56  
260 0         0 croak "We already have a column with the id '$id'";
261             }
262             }
263              
264             # Pedantic checking of that ID
265 21 50       60 if ( $self->pedantic ) {
266 21 100       65 if ( $column->{'id'} ) {
267 19 50       133 if ( $column->{'id'} !~ m/^[a-zA-Z0-9_]+$/ ) {
    50          
268 0         0 carp "The API recommends that t ID's should be both simple:"
269             . $column->{'id'};
270             } elsif ( $JAVASCRIPT_RESERVED{ $column->{'id'} } ) {
271 0         0 carp "The API recommends avoiding Javascript reserved " .
272             "words for IDs: " . $column->{'id'};
273             }
274             }
275             }
276              
277             # Add that column to our collection
278 21         25 push( @{ $self->{'columns'} }, $column );
  21         83  
279             }
280              
281             # Reset column statistics
282 8         22 $self->{'column_mapping'} = {};
283 8         18 $self->{'column_count' } = 0;
284 8         12 $self->{'all_columns_have_ids'} = 1;
285              
286             # Map the IDs to column indexes, redo column stats, and encode the column
287             # data
288 8         14 my $i = 0;
289 8         30 for my $column ( @{ $self->{'columns'} } ) {
  8         19  
290              
291 21         29 $self->{'column_count'}++;
292              
293             # Encode as JSON
294 21         31 delete $column->{'json'};
295 21         49 my $column_json = $self->json_xs_object->encode( $column );
296 21         49 $column->{'json'} = $column_json;
297              
298             # Column mapping
299 21 100       77 if ( $column->{'id'} ) {
300 19         44 $self->{'column_mapping'}->{ $column->{'id'} } = $i;
301             } else {
302 2         4 $self->{'all_columns_have_ids'} = 0;
303             }
304 21         45 $i++;
305             }
306              
307 8         31 return $self;
308             }
309              
310             =head2 add_rows
311              
312             Accepts zero or more rows, either as a list of hash-refs or a list of
313             array-refs. If you've provided hash-refs, we'll map the key name to the column
314             via its ID (you must have given every column an ID if you want to do this, or
315             it'll cause a fatal error).
316              
317             If you've provided array-refs, we'll assume each cell belongs in subsequent
318             columns - your array-ref must have the same number of members as you have set
319             columns.
320              
321             =cut
322              
323             sub add_rows {
324 10     10 1 236 my ( $self, @rows_to_add ) = @_;
325              
326             # Loop over our input rows
327 10         23 for my $row (@rows_to_add) {
328              
329 13         17 my @columns;
330             my $properties;
331              
332             # Map hash-refs to columns
333 13 100       57 if ( ref( $row ) eq 'HASH' ) {
    50          
334              
335             # Grab the properties, if they exist
336 8 50       32 if ( exists $self->{'column_mapping'}->{'p'} ) {
337 0         0 $properties = delete $row->{'_p'};
338             } else {
339 8         17 $properties = delete $row->{'p'};
340             }
341              
342             # We can't be going forward unless they specified IDs for each of
343             # their columns
344 8 50       28 croak "All your columns must have IDs if you want to add hashrefs" .
345             " as rows" unless $self->{'all_columns_have_ids'};
346              
347             # Loop through the keys, populating @columns
348 8         24 for my $key ( keys %$row ) {
349             # Get the relevant column index for the key, or handle 'p'
350             # properly
351 20 50       57 unless ( exists $self->{'column_mapping'}->{ $key } ) {
352 0         0 croak "Couldn't find a column with id '$key'";
353             }
354 20         29 my $index = $self->{'column_mapping'}->{ $key };
355              
356             # Populate @columns with the data-type and value
357 20         78 $columns[ $index ] = [
358             $self->{'columns'}->[ $index ]->{'type'},
359             $row->{ $key }
360             ];
361              
362             }
363              
364             # Map array-refs to columns
365             } elsif ( ref( $row ) eq 'ARRAY' ) {
366              
367             # Populate @columns with the data-type and value
368 5         9 my $i = 0;
369 5         12 for my $col (@$row) {
370 12         133 $columns[ $i ] = [
371             $self->{'columns'}->[ $i ]->{'type'},
372             $col
373             ];
374 12         23 $i++;
375             }
376              
377             # Rows must be array-refs or hash-refs
378             } else {
379 0         0 croak "Rows must be array-refs or hash-refs: $row";
380             }
381              
382             # Force the length of columns to be the same as actual columns, to
383             # handle undef values better.
384 13 100       57 $columns[ $self->{'column_count'} - 1 ] = undef
385             unless defined $columns[ $self->{'column_count'} - 1 ];
386              
387             # Convert each cell in to the long cell format
388 13         9638 my @formatted_columns;
389 13         32 for ( @columns ) {
390 35 100       69 if ( $_ ) {
391 32         64 my ($type, $column) = @$_;
392              
393 32 100       71 if ( ref( $column ) eq 'HASH' ) {
394             # Check f is a simple string if defined
395 7 50 66     78 if ( defined($column->{'f'}) && ref( $column->{'f'} ) ) {
396 0         0 croak "Cell's 'f' values must be strings: " .
397             $column->{'f'};
398             }
399             # If p is defined, check it serializes
400 7 100       24 if ( defined($column->{'p'}) ) {
401 1 50       5 croak "'p' must be a reference"
402             unless ref( $column->{'p'} );
403 1         2 eval { $self->json_xs_object->encode( $column->{'p'} ) };
  1         3  
404 1 50       4 croak "Serializing 'p' failed: $@" if $@;
405             }
406             # Complain about any unauthorized keys
407 7 50       21 if ( $self->pedantic ) {
408 7         23 for my $key ( keys %$column ) {
409 14 50       67 carp "'$key' is not a recognized key"
410             unless $key =~ m/^[fvp]$/;
411             }
412             }
413 7         29 push( @formatted_columns, [ $type, $column ] );
414             } else {
415 25         122 push( @formatted_columns, [ $type, { v => $column } ] );
416             }
417             # Undefined that become nulls
418             } else {
419 3         10 push( @formatted_columns, [ 'null', { v => undef } ] );
420             }
421             }
422              
423             # Serialize each cell
424 13         24 my @cells;
425 13         25 for (@formatted_columns) {
426 35         65 my ($type, $cell) = @$_;
427              
428             # Force 'f' to be a string
429 35 100       89 if ( defined( $cell->{'f'} ) ) {
430 6         18 $cell->{'f'} .= '';
431             }
432              
433             # Handle null/undef
434 35 100       440 if ( ! defined($cell->{'v'}) ) {
    100          
    100          
    100          
435 10         21 push(@cells, $self->json_xs_object->encode( $cell ) );
436              
437             # Convert boolean
438             } elsif ( $type eq 'boolean' ) {
439 3 50       10 $cell->{'v'} = $cell->{'v'} ? \1 : \0;
440 3         8 push(@cells, $self->json_xs_object->encode( $cell ) );
441              
442             # Convert number
443             } elsif ( $type eq 'number' ) {
444 6 100       23 $cell->{'v'} = 0 unless $cell->{'v'}; # Force false values to 0
445 6         12 $cell->{'v'} += 0; # Force numeric for JSON encoding
446 6         18 push(@cells, $self->json_xs_object->encode( $cell ) );
447              
448             # Convert string
449             } elsif ( $type eq 'string' ) {
450 4         11 $cell->{'v'} .= '';
451 4         20 push(@cells, $self->json_xs_object->encode( $cell ) );
452              
453             # It's a date!
454             } else {
455 12         17 my @date_digits;
456              
457             # Date digits specified manually
458 12 50       24 if ( ref( $cell->{'v'} ) eq 'ARRAY' ) {
459 0         0 @date_digits = @{ $cell->{'v'} };
  0         0  
460             # We're going to have to retrieve them ourselves
461             } else {
462 12         12 my @initial_date_digits;
463              
464             # Epoch timestamp
465 12 100       70 if (! ref( $cell->{'v'} ) ) {
    50          
    50          
466 6         181 my ($sec,$min,$hour,$mday,$mon,$year) =
467             localtime( $cell->{'v'} );
468 6         14 $year += 1900;
469 6         19 @initial_date_digits =
470             ( $year, $mon, $mday, $hour, $min, $sec );
471              
472             } elsif ( $cell->{'v'}->isa('DateTime') ) {
473 0         0 my $dt = $cell->{'v'};
474 0         0 @initial_date_digits = (
475             $dt->year, ( $dt->mon - 1 ), $dt->day,
476             $dt->hour, $dt->min, $dt->sec
477             );
478              
479             } elsif ( $cell->{'v'}->isa('Time::Piece') ) {
480 6         8 my $tp = $cell->{'v'};
481 6         19 @initial_date_digits = (
482             $tp->year, $tp->_mon, $tp->mday,
483             $tp->hour, $tp->min, $tp->sec
484             );
485              
486             } else {
487 0         0 croak "Unknown date format";
488             }
489              
490 12 100       181 if ( $type eq 'date' ) {
    100          
491 4         15 @date_digits = @initial_date_digits[ 0 .. 2 ];
492             } elsif ( $type eq 'datetime' ) {
493 4         14 @date_digits = @initial_date_digits[ 0 .. 5 ];
494             } else { # Time of day
495 4         11 @date_digits = @initial_date_digits[ 3 .. 5 ];
496             }
497             }
498              
499 12         44 my $json_date = join ', ', @date_digits;
500 12 100       23 if ( $type eq 'timeofday' ) {
501 4         8 $json_date = '[' . $json_date . ']';
502             } else {
503 8         16 $json_date = 'new Date( ' . $json_date . ' )';
504             }
505              
506             # Actually, having done all this, timezone hack date...
507 12 0 100     81 if (
      66        
      66        
      0        
      33        
508             $self->{'with_timezone'} &&
509             ref ( $cell->{'v'} ) &&
510             ref ( $cell->{'v'} ) ne 'ARRAY' &&
511             $cell->{'v'}->isa('DateTime') &&
512             ( $type eq 'date' || $type eq 'datetime' )
513             ) {
514 0         0 $json_date = 'new Date("' .
515             $cell->{'v'}->strftime('%a, %d %b %Y %H:%M:%S GMT%z') .
516             '")';
517             }
518              
519 12         15 my $placeholder = '%%%PLEHLDER%%%';
520 12         20 $cell->{'v'} = $placeholder;
521 12         24 my $json_string = $self->json_xs_object->encode( $cell );
522 12         108 $json_string =~ s/"$placeholder"/$json_date/;
523 12         52 push(@cells, $json_string );
524             }
525             }
526              
527 13         62 my %data = ( cells => \@cells );
528 13 100       118 $data{'properties'} = $properties if defined $properties;
529              
530 13         26 push( @{ $self->{'rows'} }, \%data );
  13         95  
531             }
532              
533 10         29 return $self;
534             }
535              
536             =head2 pedantic
537              
538             We do some data checking for sanity, and we'll issue warnings about things the
539             API considers bad data practice - using reserved words or fancy characters and
540             IDs so far. If you don't want that, simple say:
541              
542             $object->pedantic(0);
543              
544             Defaults to true.
545              
546             =cut
547              
548             sub pedantic {
549 28     28 1 42 my ($self, $arg) = @_;
550 28 50       62 $self->{'pedantic'} = $arg if defined $arg;
551 28         81 return $self->{'pedantic'};
552             }
553              
554             =head2 set_properties
555              
556             Sets the datatable-wide properties value. See the Google docs.
557              
558             =cut
559              
560             sub set_properties {
561 2     2 1 4 my ( $self, $arg ) = @_;
562 2         4 $self->{'properties'} = $arg;
563 2         5 return $self->{'properties'};
564             }
565              
566             =head2 json_xs_object
567              
568             You may want to configure your L object in some magical way. This is
569             a read/write accessor to it. If you didn't understand that, or why you'd want
570             to do that, you can ignore this method.
571              
572             =cut
573              
574             sub json_xs_object {
575 65     65 1 236 my ($self, $arg) = @_;
576 65 50       130 $self->{'json_xs'} = $arg if defined $arg;
577 65         650 return $self->{'json_xs'};
578             }
579              
580             =head2 output_javascript
581              
582             Returns a Javascript serialization of your object. You can optionally specify two
583             parameters:
584              
585             C - I - defaults to false - that specifies if you'd like your Javascript
586             spread-apart with whitespace. Useful for debugging.
587              
588             C - I - pick out certain columns only (and in the
589             order you specify). If you don't provide an argument here, we'll use them all
590             and in the order set in C.
591              
592             =head2 output_json
593              
594             An alias to C above, with a very misleading name, as it outputs
595             Javascript, not JSON - see L below.
596              
597             =cut
598              
599 0     0 1 0 sub output_json { my ( $self, %params ) = @_; $self->output_javascript( %params ) }
  0         0  
600              
601             sub output_javascript {
602 22     22 1 11015 my ($self, %params) = @_;
603              
604 22         95 my ($columns, $rows) = $self->_select_data( %params );
605              
606 22         49 my ($t, $s, $n) = ('','','');
607 22 100       61 if ( $params{'pretty'} ) {
608 6         9 $t = " ";
609 6         8 $s = " ";
610 6         9 $n = "\n";
611             }
612              
613             # Columns
614 22         214 my $columns_string = join ',' .$n.$t.$t, @$columns;
615              
616             # Rows
617 31         182 my @rows = map {
618 22         38 my $tt = $t x 3;
619             # Turn the cells in to constituent values
620 31         54 my $individual_row_string = join ',' .$n.$tt.$t, @{$_->{'cells'}};
  31         80  
621             # Put together the output itself
622 31         207 my $output =
623             '{' .$n.
624             $tt. '"c":[' .$n.
625             $tt.$t. $individual_row_string .$n.
626             $tt.']';
627              
628             # Add properties
629 31 100       93 if ( $_->{'properties'} ) {
630 3         9 my $properties = $self->_encode_properties( $_->{'properties'} );
631 3         9 $output .= ',' .$n.$tt.'"p":' . $properties;
632             }
633              
634 31         82 $output .= $n.$t.$t.'}';
635 31         92 $output;
636             } @$rows;
637 22         56 my $rows_string = join ',' . $n . $t . $t, @rows;
638              
639 22         154 my $return =
640             '{' .$n.
641             $t. '"cols": [' .$n.
642             $t. $t. $columns_string .$n.
643             $t. '],' .$n.
644             $t. '"rows": [' .$n.
645             $t. $t. $rows_string .$n.
646             $t. ']';
647              
648 22 100       1380 if ( defined $self->{'properties'} ) {
649 2         5 my $properties = $self->_encode_properties( $self->{'properties'} );
650 2         6 $return .= ',' .$n.$t.'"p":' . $properties;
651             }
652              
653 22         49 $return .= $n.'}';
654 22         247 return $return;
655             }
656              
657             sub _select_data {
658 22     22   42 my ($self, %params) = @_;
659              
660 22         1299 my $rows = dclone $self->{'rows'};
661 22         41 my $columns = [map { $_->{'json'} } @{$self->{'columns'}}];
  59         149  
  22         59  
662              
663             # Select certain columns by id only
664 22 100 66     89 if ( $params{'columns'} && @{ $params{'columns'} } ) {
  13         57  
665 13         17 my @column_spec;
666              
667             # Get the name of each column
668 13         15 for my $column ( @{$params{'columns'}} ) {
  13         28  
669              
670             # And push it's place in the array in to our specification
671 13         24 my $index = $self->{'column_mapping'}->{ $column };
672 13 50       30 croak "Couldn't find a column named '$column'" unless
673             defined $index;
674 13         35 push(@column_spec, $index);
675             }
676              
677             # Grab the column selection
678 13         23 my @new_columns;
679 13         18 for my $index (@column_spec) {
680 13         13 my $column = splice( @{$columns}, $index, 1, '' );
  13         39  
681 13         38 push(@new_columns, $column);
682             }
683              
684             # Grab the row selection
685 13         20 my @new_rows;
686 13         21 for my $original_row (@$rows) {
687 14         18 my @new_cells;
688 14         19 for my $index (@column_spec) {
689 14         19 my $column = splice( @{$original_row->{'cells'}}, $index, 1, '' );
  14         38  
690 14         39 push(@new_cells, $column);
691             }
692 14         20 my $new_row = $original_row;
693 14         26 $new_row->{'cells'} = \@new_cells;
694              
695 14         47 push(@new_rows, $new_row);
696             }
697              
698 13         20 $rows = \@new_rows;
699 13         65 $columns = \@new_columns;
700             }
701              
702 22         73 return ( $columns, $rows );
703             }
704              
705             sub _encode_properties {
706 5     5   6 my ( $self, $properties ) = @_;
707 5         9 return $self->json_xs_object->encode( $properties );
708             }
709              
710             =head1 JSON vs Javascript
711              
712             Please note this module outputs Javascript, and not JSON. JSON is a subset of Javascript,
713             and Google's API requires a similar - but different - subset of Javascript. Specifically
714             some values need to be set to native Javascript objects, such as (and currently limited to)
715             the Date object. That means we output code like:
716              
717             {"v":new Date( 2011, 2, 21, 2, 6, 25 )}
718              
719             which is valid Javascript, but not valid JSON.
720              
721             =head1 DATES AND TIMES
722              
723             Dates are one of the reasons this module is needed at all - Google's API in
724             theory accepts Date objects, rather than a JSON equivalent of it. However,
725             given:
726              
727             new Date( 2011, 2, 21, 2, 6, 25 )
728              
729             in Javascript, what timezone is that? If you guessed UTC because that would be
730             The Right Thing To Do, sadly you guessed wrong - it's actually set in the
731             timezone of the client. And as you don't know what the client's timezone is,
732             if you're going to actually use this data for anything other than display to
733             that user, you're a little screwed.
734              
735             Even if we don't attempt to rescue that, if you pass in an Epoch timestamp, I
736             have no idea which timezone you want me to use to convert that in to the above.
737             We started off using C, which shows I hadn't really thought about it,
738             and will continue to use it for backwards compatibility, but:
739              
740             B. Either do the conversion in your
741             code using C or C, or pass in a L object whose
742             C<<->hour>> and friends return the right thing.
743              
744             We accept four types of date input, and this is how we handle each one:
745              
746             =head2 epoch seconds
747              
748             We use C, and then drop the returned fields straight in to a call to
749             C in JS.
750              
751             =head2 DateTime and Time::Piece
752              
753             We use whatever's being returned by C, C and C. Timezone messin'
754             in the object itself to get the output you want is left to you.
755              
756             =head2 Raw values
757              
758             We stick it straight in as you specified it.
759              
760             =head2 ... and one more thing
761              
762             So it is actually possible - although a PITA - to create a Date object in
763             Javascript using C which has an offset. In theory, all browsers
764             should support dates in L:
765              
766             Thu, 01 Jan 1970 00:00:00 GMT-0400
767              
768             If you're thinking L at
769             this point, you're on the right track...
770              
771             So here's the deal: B you specify C to this module's C
772             AND you pass in a L object, you'll get dates like:
773              
774             new Date("Thu, 01 Jan 1970 00:00:00 GMT-0400")
775              
776             in your output.
777              
778             =head1 BUG BOUNTY
779              
780             Find a reproducible bug, file a bug report, and I (Peter Sergeant) will donate
781             $10 to The Perl Foundation (or Wikipedia). Feature Requests are not bugs :-)
782             Offer subject to author's discretion...
783              
784             $20 donated 31Dec2010 to TPF re L
785              
786             $10 donated 11Nov2010 to TPF re L
787              
788             =head1 SUPPORT
789              
790             If you find a bug, please use
791             L
792             to raise it, or I might never see.
793              
794             =head1 AUTHOR
795              
796             Peter Sergeant C on behalf of
797             L - I
798             your market is thinking>.
799              
800             =head1 SEE ALSO
801              
802             L
803              
804             L - The underlying module
805              
806             L.
807              
808             L
809              
810             =head1 COPYRIGHT
811              
812             Copyright 2010 Investor Dynamics Ltd, some rights reserved.
813              
814             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
815              
816             =cut
817              
818             1;