pragprog_sales_chart 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Dave Thomas
1
+ Copyright (c) 2012 Jim Wilson
2
2
 
3
3
  MIT License
4
4
 
@@ -1,3 +1,3 @@
1
1
  module PragprogSalesChart
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -9,8 +9,6 @@ Gem::Specification.new do |gem|
9
9
  gem.homepage = ""
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
12
- gem.executables = []
13
- gem.test_files = []
14
12
  gem.name = "pragprog_sales_chart"
15
13
  gem.require_paths = ["lib"]
16
14
  gem.version = PragprogSalesChart::VERSION
@@ -0,0 +1,731 @@
1
+ /**
2
+ * This is Jim Wilson's script that creates charts of sales data
3
+ * from the json api
4
+ */
5
+ pragprog_sales_chart = function(window, document){
6
+
7
+ var
8
+
9
+ // jQuery - loaded dynamically
10
+ $,
11
+
12
+ /**
13
+ * set a user-visible status message.
14
+ */
15
+ $status,
16
+ status = window.s = function(message, type) {
17
+ console.log(message);
18
+ if ($status) {
19
+ if (type === 'error' && $status.writeError) {
20
+ $status.writeError(message);
21
+ } else if ($status.writeAlert) {
22
+ $status.writeAlert(message);
23
+ } else {
24
+ $status.text(message);
25
+ }
26
+ }
27
+ return message;
28
+ },
29
+
30
+ /**
31
+ * convenience methods for interacting with localStorage.
32
+ */
33
+ storage = window.localStorage,
34
+ prefix = 'pragcharts-',
35
+ get = function(key) {
36
+ return JSON.parse(storage.getItem(prefix + key));
37
+ },
38
+ set = function(key, value) {
39
+ storage.setItem(prefix + key, JSON.stringify(value));
40
+ return value;
41
+ },
42
+
43
+ // page content element
44
+ $content,
45
+
46
+ /**
47
+ * mapping of prag book codes (lower case) to book data objects
48
+ */
49
+ codes = {},
50
+
51
+ /**
52
+ * mapping of prag book titles (lower case) to book data objects
53
+ */
54
+ titles = {},
55
+
56
+ /**
57
+ * mapping of prag SKU mid-letters to binding semantics.
58
+ */
59
+ bindings = {
60
+ 'p': 'ebook',
61
+ 'b': 'paperback',
62
+ 's': 'pod',
63
+ 'v': 'screencast'
64
+ },
65
+
66
+ /**
67
+ * mapping of modes.
68
+ */
69
+ modes = {
70
+ 'direct': 'direct',
71
+ 'channel': 'channel'
72
+ },
73
+
74
+ /**
75
+ * products to graph.
76
+ */
77
+ products = get("products") || set("products", []),
78
+
79
+ /**
80
+ * jQuery result object pointing to the input for book code.
81
+ */
82
+ $code,
83
+
84
+ /**
85
+ * jQuery result objects pointing to various important fields.
86
+ */
87
+ $chart,
88
+ $from,
89
+ $until,
90
+ $groupby,
91
+
92
+ /**
93
+ * initial page setup, called after dependencies have been resolved.
94
+ */
95
+ setup = function(err){
96
+
97
+ if (err) {
98
+ return status("Unable to proceed due to error: " + err.message, 'error');
99
+ }
100
+
101
+ $ = window.jQuery;
102
+
103
+ uiAlertsAndErrors($);
104
+
105
+ // setup page content
106
+ $content = $('#content')
107
+ .empty()
108
+ .css({
109
+ 'margin-top': '2em'
110
+ })
111
+ .append('<h3><span>Sales Dashboard</span></h3>')
112
+ .append('<p>Add products on the left to update the chart on the right.</p>')
113
+ //.append('<div class="status"></div>')
114
+ .append(
115
+ $('<div class="settings-section"></div>')
116
+ .css({
117
+ 'float': 'left',
118
+ 'width': '40%'
119
+ })
120
+ .append(
121
+ $('<h4><span>Settings</span></h4>')
122
+ .css({
123
+ 'margin-top': 0
124
+ })
125
+ )
126
+ .append('<h5><span>Timeframe</span></h5>')
127
+ .append(
128
+ $('<p></p>')
129
+ .append('<label><span>From</span> <input type="text" class="date from"/></label> ')
130
+ .append('<label><span>to</span> <input type="text" class="date until"/></label> ')
131
+ .find('.date')
132
+ .css({
133
+ width: '6em'
134
+ })
135
+ .end()
136
+ .append('by <select class="groupby"></select>')
137
+ .find('select')
138
+ .append('<option value="day">day</option>')
139
+ .append('<option value="week">week</option>')
140
+ .append('<option value="month">month</option>')
141
+ .end()
142
+ )
143
+ .append(
144
+ $('<div class="products-section"></div>')
145
+ .css({
146
+ 'padding-right': '1em'
147
+ })
148
+ .append('<h5><span>Products</span></h5>')
149
+ .append('<p><button class="addproduct">add</button></p>')
150
+ .append('<div class="products"></div>')
151
+ )
152
+ )
153
+ .append(
154
+ $('<div class="chart-section"></div>')
155
+ .css({
156
+ 'float': 'right',
157
+ height: '300px',
158
+ width: '60%'
159
+ })
160
+ .append(
161
+ $('<h4><span>Chart</span></h4>')
162
+ .css({
163
+ 'margin-top': 0
164
+ })
165
+ )
166
+ .append(
167
+ $('<div class="chart"></div>')
168
+ .css({
169
+ height: '100%',
170
+ width: '100%'
171
+ })
172
+ )
173
+ );
174
+
175
+ $status = $content.find('.status');
176
+ $code = $content.find('input.code');
177
+ $chart = $content.find('.chart');
178
+
179
+ var
180
+ today = new Date,
181
+ fortnight = new Date;
182
+ fortnight.setDate(fortnight.getDate() - 14);
183
+
184
+ $from = $content.find('.from')
185
+ .datepicker()
186
+ .datepicker('setDate', fortnight)
187
+ .change(makeChart);
188
+ $until = $content.find('.until')
189
+ .datepicker()
190
+ .datepicker('setDate', today)
191
+ .change(makeChart);
192
+ $groupby = $content.find('.groupby')
193
+ .change(makeChart);
194
+
195
+ var
196
+ $products = $content.find('.products');
197
+
198
+ // setup addproduct button
199
+ $content.find('.addproduct')
200
+ .button({
201
+ icons: { primary:"ui-icon-plus" },
202
+ text: false
203
+ })
204
+ .click(function(){
205
+
206
+ $products.accordion('destroy');
207
+
208
+ addProduct($products);
209
+
210
+ $products
211
+ .accordion({ active: -1 })
212
+ .accordion('option', 'autoHeight', false)
213
+ .accordion('option', 'clearStyle', true)
214
+ .accordion('option', 'collapsible', true);
215
+
216
+ });
217
+
218
+ // get info on titles
219
+ status('Retrieving title data...');
220
+ $.get(
221
+ '/api/author/titles.json',
222
+ function(raw) {
223
+ status('Ready.');
224
+ codes = {};
225
+ titles = {};
226
+ $.each(raw, function(_, book) {
227
+ codes[book.code.toLowerCase()] = book;
228
+ titles[book.title.toLowerCase()] = book;
229
+ });
230
+
231
+ // fill in products
232
+ $.each(products, function(_, product){
233
+ addProduct($products, product);
234
+ });
235
+ $products
236
+ .css({
237
+ 'font-size': '14px'
238
+ })
239
+ .accordion({ active: -1 })
240
+ .accordion('option', 'autoHeight', false)
241
+ .accordion('option', 'clearStyle', true)
242
+ .accordion('option', 'collapsible', true);
243
+
244
+ makeChart();
245
+ }
246
+ );
247
+
248
+ // setup the book code autocompleter
249
+ $content.find('input.code').autocomplete({
250
+ source: codeAutocomplete
251
+ });
252
+
253
+ // setup the date pickers
254
+ $content.find('.date').datepicker();
255
+
256
+ },
257
+
258
+ /**
259
+ * get list of products by inspecting a collection element.
260
+ */
261
+ getProducts = function($elem) {
262
+ var
263
+ p = [];
264
+ $elem
265
+ .find('h6')
266
+ .each(function(_, h){
267
+ var
268
+ $h = $(h),
269
+ $opts = $h.next(),
270
+ product = {
271
+ code: $opts.find('.code').val(),
272
+ binding: $opts.find('.binding input:[checked]').val(),
273
+ mode: $opts.find('.mode input:[checked]').val()
274
+ };
275
+ p[p.length] = product;
276
+ $h.find('a').html(productHeading(product));
277
+ });
278
+ return p;
279
+ },
280
+
281
+ /**
282
+ * get a product heading for a given product.
283
+ */
284
+ productHeading = function(product) {
285
+ product = product || {};
286
+ var
287
+ code = product.code,
288
+ book = codes[code],
289
+ heading = '--unspecifed product--';
290
+ if (book && product.binding && product.mode) {
291
+ heading = book.title + '<br />' + bindings[product.binding] + ' - ' + modes[product.mode] + ' sales';
292
+ }
293
+ return heading;
294
+ },
295
+
296
+ /**
297
+ * add a product to a set.
298
+ * @param {jQuery} $elem Collection of one element to which to add a product.
299
+ * @param {object} product Optional product to use for settings.
300
+ */
301
+ addProduct = function($elem, product) {
302
+ product = product || {};
303
+ var
304
+ code = product.code,
305
+ title = productHeading(product),
306
+ $binding = makeButtonSet(bindings, product.binding)
307
+ .addClass('buttonset')
308
+ .addClass('binding'),
309
+ $mode = makeButtonSet(modes, product.mode)
310
+ .addClass('buttonset')
311
+ .addClass('mode'),
312
+ $heading = $('<h6><a href="#">' + title + '</a></h6>');
313
+
314
+ // pick the first code in the codes hash
315
+ if (!code) {
316
+ for (code in codes) {
317
+ break;
318
+ }
319
+ }
320
+
321
+ $elem
322
+ .append($heading)
323
+ .append(
324
+ $('<div><table><tbody></tbody></table></div>')
325
+ .find('tbody')
326
+ .append(
327
+ $('<tr></tr>')
328
+ .append(
329
+ $('<th><span>book code</span></th>')
330
+ .css('text-align', 'right')
331
+ )
332
+ .append(
333
+ $('<td><input type="text" class="code" /></td>')
334
+ .find('.code')
335
+ .val(code)
336
+ .autocomplete({ source: codeAutocomplete })
337
+ .end()
338
+ )
339
+ )
340
+ .append(
341
+ $('<tr></tr>')
342
+ .append(
343
+ $('<th><span>binding</span></th>')
344
+ .css('text-align', 'right')
345
+ )
346
+ .append(
347
+ $('<td></td>').append($binding)
348
+ )
349
+ )
350
+ .append(
351
+ $('<tr></tr>')
352
+ .append(
353
+ $('<th><span>sales mode</span></th>')
354
+ .css('text-align', 'right')
355
+ )
356
+ .append(
357
+ $('<td></td>').append($mode)
358
+ )
359
+ )
360
+ .end() // end tbody
361
+ .append(
362
+ $('<div></div>')
363
+ .append(
364
+ $('<button class="save">Save</button>')
365
+ .button()
366
+ .click(function(){
367
+ saveProducts($elem);
368
+ })
369
+ )
370
+ .append(
371
+ $('<button class="remove">Remove</button>')
372
+ .button()
373
+ .click(function(){
374
+ var
375
+ $confirm = $('<div></div>')
376
+ .appendTo('body')
377
+ .text('Remove this product from the list?')
378
+ .dialog({
379
+ resizable: false,
380
+ height:140,
381
+ modal: true,
382
+ buttons: {
383
+ 'Remove': function() {
384
+ $heading
385
+ .next()
386
+ .remove()
387
+ .end()
388
+ .remove();
389
+ saveProducts($elem);
390
+ cleanup()
391
+ },
392
+ 'Cancel': function() {
393
+ cleanup()
394
+ }
395
+ }
396
+ }),
397
+ cleanup = function(){
398
+ $confirm
399
+ .dialog('close')
400
+ .dialog('destroy')
401
+ .remove();
402
+ };
403
+ })
404
+ )
405
+ )
406
+ );
407
+ },
408
+
409
+ /**
410
+ * save product choices specified in a container element.
411
+ */
412
+ saveProducts = function($elem){
413
+ products = getProducts($elem);
414
+ set("products", products);
415
+ $elem.accordion({ active: -1 });
416
+ makeChart();
417
+ },
418
+
419
+ /**
420
+ * autocompleter function for book codes.
421
+ */
422
+ codeAutocomplete = function(obj, callback){
423
+ var
424
+ term = obj.term.toLowerCase(),
425
+ options = [];
426
+ $.each(codes, function(code, book) {
427
+ if (code.indexOf(term) !== -1) {
428
+ options[options.length] = book.code;
429
+ }
430
+ });
431
+ $.each(titles, function(title, book) {
432
+ if (title.indexOf(term) !== -1) {
433
+ options[options.length] = {
434
+ label: book.title + " (" + book.code + ")",
435
+ value: book.code
436
+ };
437
+ }
438
+ });
439
+ callback(options);
440
+ },
441
+
442
+ /**
443
+ * produce a raido button set.
444
+ * @param {map} choices Map of value/label choices to turn into a set.
445
+ * @param {string} selected Which choice to make selected.
446
+ */
447
+ makeButtonSet = function(choices, selected){
448
+
449
+ var
450
+ suffix = makeButtonSet.suffix || 0,
451
+ name = 'buttonset-' + suffix,
452
+ $set = $('<span></span>'),
453
+ index = 0;
454
+
455
+ $.each(choices, function(value, label){
456
+
457
+ var
458
+ id = name + '-' + index,
459
+ $input = $('<input type="radio"/>')
460
+ .attr({
461
+ name: name,
462
+ id: id,
463
+ value: value
464
+ });
465
+
466
+ if (value === selected) {
467
+ $input.attr('checked', 'checked');
468
+ }
469
+
470
+ $set
471
+ .append($input)
472
+ .append('<label for="' + id + '"><span>' + label + '</span></label>')
473
+
474
+ index++;
475
+
476
+ });
477
+
478
+ makeButtonSet.suffix = suffix + 1;
479
+
480
+ return $set;
481
+ },
482
+
483
+ /**
484
+ * make the chart
485
+ */
486
+ makeChart = function() {
487
+
488
+ var
489
+
490
+ from = $from.datepicker('getDate'),
491
+ until = $until.datepicker('getDate'),
492
+ by = $groupby.val(),
493
+ url,
494
+
495
+ keys = {},
496
+ key,
497
+
498
+ ticks = {
499
+ day: [1, 'day'],
500
+ week: [7, 'day'],
501
+ month: [1, 'month']
502
+ };
503
+
504
+ // increment until date to account for sku_sales being exclusive at the end of the range.
505
+ until.setDate(until.getDate() + 1);
506
+
507
+ $.each(products, function(index, product){
508
+ key = product.code + '-' + product.binding + '-' + product.mode;
509
+ keys[key] = index;
510
+ });
511
+
512
+ if (isNaN(+from) || isNaN(+until)) {
513
+ return status("Unable to draw, please enter valid date boundaries", 'error');
514
+ }
515
+
516
+ if (from >= until) {
517
+ return status("Unable to draw, please make sure start date is before end date", 'error');
518
+ }
519
+
520
+ status('Retrieving sales data...');
521
+ $.get(
522
+ '/api/author/sku_sales.json', {
523
+ start_date: dateformat(from),
524
+ end_date: dateformat(until)
525
+ }, function(raw){
526
+
527
+ status('Drawing...');
528
+ var datasets = [];
529
+
530
+ $.each(raw, function(sku, tuples){
531
+ status(sku);
532
+ var
533
+
534
+ m = sku.match(/(.+)-([VPBS])-\d+/),
535
+ code = m[1].toLowerCase(),
536
+ binding = m[2].toLowerCase(),
537
+ book = codes[code],
538
+
539
+ map = {},
540
+ direct = [],
541
+ channel = [],
542
+ d = new Date(from),
543
+ v;
544
+
545
+ if ((code + '-' + binding + '-direct') in keys) {
546
+ datasets[datasets.length] = {
547
+ data: direct,
548
+ label: book.title + ' (direct - ' + bindings[binding] + ')',
549
+ lines: { show: true },
550
+ points: { show: true },
551
+ color: keys[code + '-' + binding + '-direct']
552
+ };
553
+ }
554
+ if ((code + '-' + binding + '-channel') in keys) {
555
+ datasets[datasets.length] = {
556
+ data: channel,
557
+ label: book.title + ' (channel - ' + bindings[binding] + ')',
558
+ lines: { show: true },
559
+ points: { show: true },
560
+ color: keys[code + '-' + binding + '-channel']
561
+ };
562
+ }
563
+
564
+ $.each(tuples, function(_, tuple){
565
+ map[tuple[0]] = [tuple[1] || 0, tuple[2] || 0];
566
+ });
567
+
568
+ while (d < until) {
569
+ v = map[dateformat(d)] || [0, 0];
570
+ direct[direct.length] = [+d].concat(v[0]);
571
+ channel[channel.length] = [+d].concat(v[1]);
572
+ d.setDate(d.getDate() + 1);
573
+ }
574
+
575
+ aggregate(direct, by);
576
+ aggregate(channel, by);
577
+
578
+ });
579
+
580
+ datasets.sort(function(a,b){ return a.color > b.color; });
581
+
582
+ $.plot($chart, datasets, {
583
+ grid: { hoverable: true },
584
+ xaxis: {
585
+ minTickSize: ticks[by],
586
+ mode: "time"
587
+ }
588
+ });
589
+
590
+ var $tooltip = $('<div id="tooltip"></div>')
591
+ .css( {
592
+ color: 'black',
593
+ position: 'absolute',
594
+ display: 'none',
595
+ border: '1px solid #fdd',
596
+ padding: '2px',
597
+ 'background-color': '#fee',
598
+ opacity: 0.80
599
+ })
600
+ .appendTo("body")
601
+ .hide();
602
+
603
+ function showTooltip(x, y, contents) {
604
+ $tooltip
605
+ .empty()
606
+ .append(contents)
607
+ .css( {
608
+ top: y + 5,
609
+ left: x + 5
610
+ })
611
+ .fadeIn(200);
612
+ }
613
+
614
+ var previousPoint = null;
615
+ $chart.bind("plothover", function (event, pos, item) {
616
+ if (item) {
617
+ if (previousPoint != item.dataIndex) {
618
+ previousPoint = item.dataIndex;
619
+
620
+ $tooltip.hide();
621
+ var
622
+ x = item.datapoint[0],
623
+ y = item.datapoint[1],
624
+
625
+ d = (new Date(x)).toLocaleDateString();
626
+
627
+ showTooltip(item.pageX, item.pageY,
628
+ item.series.label + "<br />" + d + ": " + y);
629
+ }
630
+ }
631
+ else {
632
+ $tooltip.hide();
633
+ previousPoint = null;
634
+ }
635
+ });
636
+
637
+ status('Done.');
638
+ }
639
+ );
640
+
641
+ },
642
+
643
+ /**
644
+ * aggregate date-based data by week or month by walking backwards and summing.
645
+ */
646
+ aggregate = function(data, by) {
647
+
648
+ if (by !== 'week' && by !== 'month') {
649
+ return;
650
+ }
651
+
652
+ var
653
+
654
+ byWeek = function(date){
655
+ return (new Date(date)).getDay() % 7 === 0;
656
+ },
657
+ byMonth = function(date) {
658
+ return (new Date(date)).getDate() === 1;
659
+ },
660
+ test = (by === 'week' ? byWeek : byMonth),
661
+
662
+ i = data.length,
663
+ sum = 0,
664
+ tuple;
665
+
666
+ while (i--) {
667
+ tuple = data[i];
668
+ sum += tuple[1];
669
+ if (test(tuple[0])) {
670
+ tuple[1] = sum;
671
+ sum = 0;
672
+ } else {
673
+ data.splice(i, 1);
674
+ }
675
+ }
676
+
677
+ if (sum) {
678
+ data[0][1] = sum;
679
+ }
680
+
681
+ }
682
+
683
+ /**
684
+ * format a date obj as yyyy-mm-dd
685
+ */
686
+ dateformat = function(date) {
687
+ var
688
+ y = '' + date.getFullYear(),
689
+ m = '0' + (date.getMonth() + 1),
690
+ d = '0' + date.getDate();
691
+ return y + '-' + m.substr(-2) + '-' + d.substr(-2);
692
+ },
693
+
694
+ /**
695
+ * jQuery UI helper for alerts and errors.
696
+ * @see http://www.brian-driscoll.com/2010/11/jqueryui-plugin-highlight-and-error.html
697
+ */
698
+ uiAlertsAndErrors = function($){
699
+ $.fn.writeError = function(message){
700
+ return this.each(function(){
701
+ var $this = $(this);
702
+ var errorHtml = "<div class=\"ui-widget\">";
703
+ errorHtml+= "<div class=\"ui-state-error ui-corner-all\" style=\"padding: 0 .7em;\">";
704
+ errorHtml+= "<p>";
705
+ errorHtml+= "<span class=\"ui-icon ui-icon-alert\" style=\"float:left; margin-right: .3em;\"></span>";
706
+ errorHtml+= message;
707
+ errorHtml+= "</p>";
708
+ errorHtml+= "</div>";
709
+ errorHtml+= "</div>";
710
+ $this.html(errorHtml);
711
+ });
712
+ };
713
+ $.fn.writeAlert = function(message){
714
+ return this.each(function(){
715
+ var $this = $(this);
716
+ var alertHtml = "<div class=\"ui-widget\">";
717
+ alertHtml+= "<div class=\"ui-state-highlight ui-corner-all\" style=\"padding: 0 .7em;\">";
718
+ alertHtml+= "<p>";
719
+ alertHtml+= "<span class=\"ui-icon ui-icon-info\" style=\"float:left; margin-right: .3em;\"></span>";
720
+ alertHtml+= message;
721
+ alertHtml+= "</p>";
722
+ alertHtml+= "</div>";
723
+ alertHtml+= "</div>";
724
+ $this.html(alertHtml);
725
+ });
726
+ };
727
+ },
728
+
729
+ setup(false);
730
+
731
+ };
metadata CHANGED
@@ -1,23 +1,33 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: pragprog_sales_chart
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.1
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - Dave Thomas
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2012-06-19 00:00:00.000000000 Z
17
+
18
+ date: 2012-06-19 00:00:00 Z
13
19
  dependencies: []
20
+
14
21
  description: Asset gem for the author sales chart
15
- email:
22
+ email:
16
23
  - dave@pragprog.com
17
24
  executables: []
25
+
18
26
  extensions: []
27
+
19
28
  extra_rdoc_files: []
20
- files:
29
+
30
+ files:
21
31
  - .gitignore
22
32
  - Gemfile
23
33
  - LICENSE
@@ -26,28 +36,39 @@ files:
26
36
  - lib/pragprog_sales_chart.rb
27
37
  - lib/pragprog_sales_chart/version.rb
28
38
  - pragprog_sales_chart.gemspec
29
- homepage: ''
39
+ - vendor/assets/javascripts/pragprog_sales_chart.js
40
+ homepage: ""
30
41
  licenses: []
42
+
31
43
  post_install_message:
32
44
  rdoc_options: []
33
- require_paths:
45
+
46
+ require_paths:
34
47
  - lib
35
- required_ruby_version: !ruby/object:Gem::Requirement
48
+ required_ruby_version: !ruby/object:Gem::Requirement
36
49
  none: false
37
- requirements:
38
- - - ! '>='
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ hash: 3
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
58
  none: false
43
- requirements:
44
- - - ! '>='
45
- - !ruby/object:Gem::Version
46
- version: '0'
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
47
66
  requirements: []
67
+
48
68
  rubyforge_project:
49
- rubygems_version: 1.8.21
69
+ rubygems_version: 1.8.10
50
70
  signing_key:
51
71
  specification_version: 3
52
72
  summary: Asset gem for the author sales chart
53
73
  test_files: []
74
+