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