pragprog_sales_chart 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+