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 +1 -1
- data/lib/pragprog_sales_chart/version.rb +1 -1
- data/pragprog_sales_chart.gemspec +0 -2
- data/vendor/assets/javascripts/pragprog_sales_chart.js +731 -0
- metadata +41 -20
data/LICENSE
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
39
|
+
- vendor/assets/javascripts/pragprog_sales_chart.js
|
40
|
+
homepage: ""
|
30
41
|
licenses: []
|
42
|
+
|
31
43
|
post_install_message:
|
32
44
|
rdoc_options: []
|
33
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
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.
|
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
|
+
|