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