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 +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
|
+
|