datavis-glide 4.0.0-PRE.0

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.
Files changed (62) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +129 -0
  3. package/datavis.js +101 -0
  4. package/dist/wcdatavis.css +1957 -0
  5. package/dist/wcdatavis.min.js +1 -0
  6. package/global-jquery.js +4 -0
  7. package/ie-fixes.js +13 -0
  8. package/index.js +70 -0
  9. package/meteor.js +1 -0
  10. package/package.json +102 -0
  11. package/src/flags.js +6 -0
  12. package/src/graph.js +1079 -0
  13. package/src/graph_renderer.js +85 -0
  14. package/src/grid.js +2777 -0
  15. package/src/grid_control.js +1957 -0
  16. package/src/grid_filter.js +1073 -0
  17. package/src/grid_renderer.js +276 -0
  18. package/src/group_fun_win.js +121 -0
  19. package/src/lang/en-US.js +188 -0
  20. package/src/lang/es-MX.js +188 -0
  21. package/src/lang/fr-FR.js +188 -0
  22. package/src/lang/id-ID.js +188 -0
  23. package/src/lang/nl-NL.js +188 -0
  24. package/src/lang/pt-BR.js +188 -0
  25. package/src/lang/ru-RU.js +188 -0
  26. package/src/lang/th-TH.js +188 -0
  27. package/src/lang/vi-VN.js +188 -0
  28. package/src/lang/zh-Hans-CN.js +188 -0
  29. package/src/operations_palette.js +176 -0
  30. package/src/prefs_modules.js +132 -0
  31. package/src/reg/graph_renderer.js +17 -0
  32. package/src/renderers/graph/chartjs.js +457 -0
  33. package/src/renderers/graph/google.js +584 -0
  34. package/src/renderers/graph/jit.js +61 -0
  35. package/src/renderers/graph/svelte-gantt.js +168 -0
  36. package/src/renderers/grid/dummy.js +79 -0
  37. package/src/renderers/grid/handlebars.js +217 -0
  38. package/src/renderers/grid/squirrelly.js +215 -0
  39. package/src/renderers/grid/table/group_detail.js +1404 -0
  40. package/src/renderers/grid/table/group_summary.js +380 -0
  41. package/src/renderers/grid/table/pivot.js +915 -0
  42. package/src/renderers/grid/table/plain.js +1592 -0
  43. package/src/renderers/grid/table.js +2510 -0
  44. package/src/trans.js +101 -0
  45. package/src/ui/collapsible.js +234 -0
  46. package/src/ui/filters/date.js +283 -0
  47. package/src/ui/grid_filter.js +398 -0
  48. package/src/ui/popup_menu.js +224 -0
  49. package/src/ui/popup_window.js +572 -0
  50. package/src/ui/slider.js +156 -0
  51. package/src/ui/tabs.js +202 -0
  52. package/src/ui/templates.js +131 -0
  53. package/src/ui/toolbar.js +63 -0
  54. package/src/ui/toolbars/grid.js +873 -0
  55. package/src/ui/windows/col_config.js +341 -0
  56. package/src/ui/windows/debug.js +164 -0
  57. package/src/ui/windows/grid_table_opts.js +139 -0
  58. package/src/util/handlebars.js +158 -0
  59. package/src/util/jquery.js +630 -0
  60. package/src/util/misc.js +1058 -0
  61. package/src/util/squirrelly.js +155 -0
  62. package/wcdatavis.css +1601 -0
package/src/graph.js ADDED
@@ -0,0 +1,1079 @@
1
+ import _ from 'underscore';
2
+
3
+ import jQuery from 'jquery';
4
+
5
+ import {
6
+ deepCopy,
7
+ deepDefaults,
8
+ determineColumns,
9
+ icon,
10
+ gensym,
11
+ getProp,
12
+ getPropDef,
13
+ makeSubclass,
14
+ makeToggleCheckbox,
15
+ mixinLogging,
16
+ presentDownload,
17
+ setProp,
18
+ toInt,
19
+ } from './util/misc.js';
20
+ import { ComputedView, Prefs } from 'datavis-ace';
21
+ import {trans} from './trans.js';
22
+
23
+ import GRAPH_RENDERER_REGISTRY from './reg/graph_renderer.js';
24
+
25
+ // Graph {{{1
26
+
27
+ // JSDoc Types {{{2
28
+
29
+ /**
30
+ * @typedef {object} Graph~Config
31
+ *
32
+ * @property {Graph~Config_When} whenPlain
33
+ * Tells how to configure the graph when the data is plain (has not been grouped or pivotted).
34
+ *
35
+ * @property {Graph~Config_When} whenGroup
36
+ * Tells how to configure the graph when the data is grouped.
37
+ *
38
+ * @property {Graph~Config_When} whenPivot
39
+ * Tells how to configure the graph when the data is pivotted.
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} Graph~Config_When
44
+ * Can either be a function that returns an object, or just an object. If it's a function, it
45
+ * receives the group fields and pivot fields as arguments.
46
+ *
47
+ * @property {string} graphType
48
+ * Name of the type of graph.
49
+ *
50
+ * @property {number} [aggNum]
51
+ * When graphing grouped or pivotted data, the aggregate number that we're graphing for the value.
52
+ *
53
+ * @property {string} [aggType]
54
+ * When graphing pivotted data, the type of aggregate that we're graphing on the value axis.
55
+ *
56
+ * - `cell`: We're graphing separate series for each pivot colval.
57
+ * - `group`: We're graphing the aggregate calculated over all the data in each group.
58
+ * - `pivot`: We're graphing the aggregate calculated over all the data in each pivot.
59
+ *
60
+ * For example, when grouping by "Product" and pivotting by "Country", you can create a "Sum"
61
+ * aggregate on the field "Sales." Let's assume we're doing a column graph.
62
+ *
63
+ * - `cell`: Shows a stacked graph where each bar is a product, and each item in the stack is a country.
64
+ * - `group`: Shows a graph where each bar is a product (showing total sales in all countries).
65
+ * - `pivot`: Shows a graph where each bar is a country (showing total sales for all products).
66
+ *
67
+ * @property {object} [options]
68
+ * These options are passed directly to the graph rendering library (e.g. Google Charts, ChartJS).
69
+ */
70
+
71
+ // Constructor {{{2
72
+
73
+ /**
74
+ * Creates a new graph.
75
+ *
76
+ * @param {string} id
77
+ *
78
+ * @param {ComputedView} view
79
+ *
80
+ * @param {Graph~Config} opts
81
+ *
82
+ * @class
83
+ *
84
+ * Represents a graph.
85
+ *
86
+ * @property {string} id
87
+ * @property {ComputedView} view
88
+ * @property {object} devConfig
89
+ * @property {object} userConfig
90
+ * @property {object} opts
91
+ * @property {GraphRenderer} renderer
92
+ */
93
+
94
+ var Graph = makeSubclass('Graph', Object, function (id, view, devConfig, opts) {
95
+ var self = this;
96
+
97
+ self.id = id;
98
+ self.view = view;
99
+ self.devConfig = devConfig || {};
100
+ self.userConfig = {
101
+ plain: {},
102
+ group: {},
103
+ pivot: {}
104
+ };
105
+ self.opts = deepDefaults(opts, {
106
+ title: 'Graph',
107
+ runImmediately: true,
108
+ showToolbar: true,
109
+ showOnDataChange: false,
110
+ });
111
+ self.hasRun = false;
112
+
113
+ if (typeof id !== 'string') {
114
+ throw new Error('Call Error: `id` must be a string');
115
+ }
116
+
117
+ if (!(view instanceof ComputedView)) {
118
+ throw new Error('Call Error: `view` must be an instance of MIE.WC_DataVis.ComputedView');
119
+ }
120
+
121
+ if (self.opts.prefs != null && !(self.opts.prefs instanceof Prefs)) {
122
+ throw new Error('Call Error: `opts.prefs` must be an instance of MIE.WC_DataVis.Prefs');
123
+ }
124
+
125
+ if (self.opts.prefs != null) {
126
+ self.prefs = self.opts.prefs;
127
+ }
128
+ else if (self.view.prefs != null) {
129
+ self.prefs = self.view.prefs;
130
+ }
131
+ else {
132
+ self.prefs = new Prefs(self.id);
133
+ }
134
+
135
+ self.prefs.bind('graph', self);
136
+
137
+ self._makeUserInterface();
138
+
139
+ self.view.addClient(self, 'graph');
140
+
141
+ // Event handlers for keeping the spinner icon updated.
142
+
143
+ self.view.on('fetchDataBegin', function () {
144
+ self._setSpinner('loading');
145
+ self._showSpinner();
146
+ });
147
+ self.view.on('fetchDataEnd', function () {
148
+ self._hideSpinner();
149
+ });
150
+
151
+ self.view.on('workBegin', function () {
152
+ self._setSpinner('working');
153
+ self._showSpinner();
154
+ });
155
+ self.view.on('workEnd', function () {
156
+ self._hideSpinner();
157
+ });
158
+
159
+ // Event handler for keeping the UI in sync with the data. We don't let the graph renderer redraw itself because we need to keep the UI in sync with the data. For example, if a pivot is removed, the aggregate will change if we were graphing a pivot-specific aggregate like "Count by [Pivot Field]").
160
+
161
+ self.view.on('workEnd', function (info, ops) {
162
+ self.lastOps = ops;
163
+ // self.drawFromConfig();
164
+ }, {
165
+ who: self
166
+ });
167
+
168
+ self.view.on('dataUpdated', function () {
169
+ if (self.opts.showOnDataChange && !self.isVisible()) {
170
+ self.show({ redraw: false });
171
+ }
172
+
173
+ // Only need to redraw if there's no renderer. If there is, the renderer's own View
174
+ // (workEnd) handler will take care of it.
175
+
176
+ if (self.renderer == null) {
177
+ self.redraw();
178
+ }
179
+
180
+ /*
181
+ switch (self.lastDrawnFrom) {
182
+ case 'config':
183
+ self.drawFromConfig();
184
+ break;
185
+ case 'interactive':
186
+ default:
187
+ self.drawInteractive();
188
+ break;
189
+ }
190
+ */
191
+ });
192
+
193
+ // self.view.on('aggregateSet', function (spec, shouldGraph) {
194
+ // var aggType, aggNum;
195
+ //
196
+ // if (shouldGraph != null) {
197
+ // if (shouldGraph.group && shouldGraph.group.length > 0) {
198
+ // aggType = 'group';
199
+ // aggNum = shouldGraph.group[0].aggNum;
200
+ // }
201
+ // else if (shouldGraph.pivot && shouldGraph.pivot.length > 0) {
202
+ // aggType = 'pivot';
203
+ // aggNum = shouldGraph.pivot[0].aggNum;
204
+ // }
205
+ //
206
+ // if (aggType == null || aggNum == null) {
207
+ // // Couldn't find an aggregate we could graph.
208
+ // return;
209
+ // }
210
+ //
211
+ // // Set the dropdown to match the aggregate we're supposed to graph.
212
+ //
213
+ // var matchingAgg = self.ui.aggDropdown.find('option');
214
+ // matchingAgg = matchingAgg.filter(function (i, elt) {
215
+ // return elt.getAttribute('data-wcdv-agg-type') === aggType;
216
+ // });
217
+ // matchingAgg = matchingAgg.filter(function (i, elt) {
218
+ // return +elt.getAttribute('data-wcdv-agg-num') === aggNum;
219
+ // });
220
+ // if (matchingAgg.length === 1) {
221
+ // self.ui.aggDropdown.val(matchingAgg.attr('value'));
222
+ // // self.ui.aggDropdown.trigger('change');
223
+ // }
224
+ // }
225
+ // });
226
+
227
+ if (self.opts.runImmediately) {
228
+ self.show();
229
+ }
230
+ else {
231
+ self.hasRun = false;
232
+ self.hide();
233
+ }
234
+
235
+ /*
236
+ * Store self object so it can be accessed from other JavaScript in the page.
237
+ */
238
+
239
+ setProp(self, window, 'MIE', 'WC_DataVis', 'graphs', self.id);
240
+ });
241
+
242
+ mixinLogging(Graph);
243
+
244
+ // #toString {{{2
245
+
246
+ Graph.prototype.toString = function () {
247
+ return 'Graph(id="' + this.id + '")';
248
+ };
249
+
250
+ // #_makeUserInterface {{{2
251
+
252
+ Graph.prototype._makeUserInterface = function () {
253
+ var self = this;
254
+
255
+ // div.wcdv_graph (ui.root)
256
+ // |
257
+ // +-- div.wcdv_grid_titlebar (ui.titlebar)
258
+ // | |
259
+ // | +-- strong (ui.spinner)
260
+ // | +-- strong [[ the title ]]
261
+ // | `-- button [[ show/hide button ]]
262
+ // |
263
+ // `-- div.wcdv_grid_content (ui.content)
264
+ // |
265
+ // +-- div.wcdv_grid_toolbar (ui.toolbar)
266
+ // +-- div.wcdv_toolbar_section (ui.toolbar_source)
267
+ // +-- div.wcdv_toolbar_section (ui.toolbar_common)
268
+ // +-- div.wcdv_toolbar_section (ui.toolbar_aggregate)
269
+ // `-- div.wcdv_graph_render (ui.graph)
270
+
271
+ self.ui = {};
272
+ self.ui.root = jQuery(document.getElementById(self.id));
273
+
274
+ self.ui.root.addClass('wcdv_graph');
275
+ self.ui.root.children().remove();
276
+
277
+ self.ui.titlebar = jQuery('<div>')
278
+ .addClass('wcdv_grid_titlebar')
279
+ .on('click', function (evt) {
280
+ evt.stopPropagation();
281
+ self.toggle();
282
+ })
283
+ .appendTo(self.ui.root);
284
+
285
+ self._addTitleWidgets(self.ui.titlebar);
286
+
287
+ self.ui.content = jQuery('<div>', {
288
+ 'class': 'wcdv_grid_content'
289
+ }).appendTo(self.ui.root);
290
+
291
+ self.ui.toolbar = jQuery('<div>')
292
+ .addClass('wcdv_grid_toolbar')
293
+ .appendTo(self.ui.content)
294
+ ;
295
+
296
+ if (!self.opts.showToolbar) {
297
+ self.ui.toolbar.hide();
298
+ }
299
+
300
+ // The "pivot" toolbar section lets the user decide if colvals should show up stacked or as
301
+ // separate bars (for bar & column charts).
302
+
303
+ self.ui.toolbar_pivot = jQuery('<div>')
304
+ .addClass('wcdv_toolbar_section')
305
+ .css('visibility', 'hidden')
306
+ .appendTo(self.ui.toolbar);
307
+ self._addPivotButtons(self.ui.toolbar_pivot);
308
+
309
+ // The "aggregates" toolbar section lets the user control what is drawn based on the aggregate
310
+ // functions calculated by the view.
311
+
312
+ self.ui.toolbar_aggregates = jQuery('<div>')
313
+ .addClass('wcdv_toolbar_section pull-right')
314
+ .css('visibility', 'hidden')
315
+ .appendTo(self.ui.toolbar);
316
+ self._addAggregateButtons(self.ui.toolbar_aggregates);
317
+
318
+ self.ui.graph = jQuery('<div>', { 'id': self.id, 'class': 'wcdv_graph_render' });
319
+
320
+ self.ui.root
321
+ .append(self.ui.titlebar)
322
+ .append(self.ui.content
323
+ .append(self.ui.toolbar)
324
+ .append(self.ui.graph))
325
+ ;
326
+ };
327
+
328
+ // #_addTitleWidgets {{{2
329
+
330
+ /**
331
+ * Add widgets to the header of the graph.
332
+ *
333
+ * @private
334
+ *
335
+ * @param {jQuery} titlebar
336
+ */
337
+
338
+ Graph.prototype._addTitleWidgets = function (titlebar) {
339
+ var self = this;
340
+
341
+ self.ui.spinner = jQuery('<span>', {
342
+ 'style': 'font-size: 18px',
343
+ 'class': 'wcdv_icon_button wcdv_spinner'
344
+ })
345
+ .appendTo(titlebar)
346
+ ;
347
+
348
+ self._setSpinner(self.opts.runImmediately ? 'loading' : 'not-loaded');
349
+
350
+ jQuery('<strong>')
351
+ .text(self.opts.title)
352
+ .appendTo(titlebar);
353
+
354
+
355
+ // Create container to hold all the controls in the titlebar
356
+
357
+ self.ui.titlebar_controls = jQuery('<div>')
358
+ .addClass('wcdv_titlebar_controls pull-right')
359
+ .appendTo(titlebar);
360
+
361
+ // Create the Export button
362
+
363
+ self.ui.exportBtn = jQuery('<button>', {
364
+ 'type': 'button',
365
+ 'style': 'font-size: 18px',
366
+ 'class': 'wcdv_icon_button wcdv_text-primary'
367
+ })
368
+ .on('click', function (evt) {
369
+ evt.stopPropagation();
370
+ self.export();
371
+ })
372
+ .append(icon('download'))
373
+ .appendTo(self.ui.titlebar_controls)
374
+ ;
375
+
376
+ // Create the Refresh button
377
+
378
+ self.ui.refreshBtn = jQuery('<button>', {
379
+ 'type': 'button',
380
+ 'style': 'font-size: 18px',
381
+ 'class': 'wcdv_icon_button wcdv_text-primary'
382
+ })
383
+ .attr('title', 'Refresh')
384
+ .on('click', function (evt) {
385
+ evt.stopPropagation();
386
+ self.refresh();
387
+ })
388
+ .append(icon('refresh-cw'))
389
+ .appendTo(self.ui.titlebar_controls)
390
+ ;
391
+
392
+ // This is the "gear" icon that shows/hides the controls below the toolbar. The controls are used
393
+ // to set the group, pivot, aggregate, and filters. Ideally the user only has to utilize these
394
+ // once, and then switches between perspectives to get the same effect.
395
+
396
+ jQuery('<button>', {
397
+ 'type': 'button',
398
+ 'style': 'font-size: 18px',
399
+ 'class': 'wcdv_icon_button wcdv_text-primary'
400
+ })
401
+ .attr('title', trans('GRAPH.TITLEBAR.SHOW_HIDE_CONTROLS'))
402
+ .click(function (evt) {
403
+ evt.stopPropagation();
404
+ self.ui.toolbar.toggle();
405
+ })
406
+ .append(jQuery(icon('settings')))
407
+ .appendTo(self.ui.titlebar_controls)
408
+ ;
409
+
410
+ // Create the down-chevron button that shows/hides everything under the titlebar.
411
+
412
+ self.ui.showHideButton = jQuery('<button>', {
413
+ 'type': 'button',
414
+ 'style': 'font-size: 18px',
415
+ 'class': 'wcdv_icon_button wcdv_text-primary showhide'
416
+ })
417
+ .attr('title', trans('GRAPH.TITLEBAR.SHOW_HIDE'))
418
+ .click(function (evt) {
419
+ evt.stopPropagation();
420
+ self.toggle();
421
+ })
422
+ .append(jQuery(icon('chevron-down')))
423
+ .appendTo(self.ui.titlebar_controls)
424
+ ;
425
+ };
426
+
427
+ // #_addAggregateButtons {{{2
428
+
429
+ Graph.prototype._addAggregateButtons = function (toolbar) {
430
+ var self = this;
431
+
432
+ var graphTypeDropdownId = gensym();
433
+ jQuery('<label>', { 'for': graphTypeDropdownId }).text('Graph Type: ').appendTo(toolbar);
434
+ self.ui.graphTypeDropdown = jQuery('<select>', { 'id': graphTypeDropdownId })
435
+ .on('change', function () {
436
+ self.drawInteractive();
437
+ })
438
+ .appendTo(toolbar);
439
+
440
+ var aggDropdownId = gensym();
441
+ jQuery('<label>', { 'for': aggDropdownId }).text('Aggregate: ').appendTo(toolbar);
442
+ self.ui.aggDropdown = jQuery('<select>', { 'id': aggDropdownId })
443
+ .on('change', function () {
444
+ self.drawInteractive();
445
+ })
446
+ .appendTo(toolbar);
447
+
448
+ self.ui.zeroAxisCheckbox = makeToggleCheckbox(
449
+ null,
450
+ null,
451
+ false,
452
+ 'Y-Axis Starts at Zero',
453
+ toolbar,
454
+ function () {
455
+ self.drawInteractive();
456
+ }
457
+ );
458
+
459
+ self.view.on('workEnd', function () {
460
+ self._updateAggDropdown();
461
+ });
462
+ };
463
+
464
+ // #_setGraphTypeOptions {{{2
465
+
466
+ Graph.prototype._setGraphTypeOptions = function () {
467
+ var self = this;
468
+
469
+ if (self.ui.graphTypeDropdown == null) {
470
+ self.logError(self.makeLogTag('set graph type optoins') + ' Dropdown UI element does not exist');
471
+ return;
472
+ }
473
+
474
+ if (self.renderer == null) {
475
+ self.logError(self.makeLogTag('set graph type optoins') + ' Renderer does not exist');
476
+ return;
477
+ }
478
+
479
+ self.ui.graphTypeDropdown.children().remove();
480
+ if (self.renderer.graphTypes != null) {
481
+ self.renderer.graphTypes.each(function (gt) {
482
+ self.ui.graphTypeDropdown.append(jQuery('<option>', { 'value': gt.value }).text(gt.name));
483
+ });
484
+ }
485
+ };
486
+
487
+ // #_addPivotButtons {{{2
488
+
489
+ Graph.prototype._addPivotButtons = function (toolbar) {
490
+ var self = this;
491
+
492
+ self.ui.stackCheckbox = makeToggleCheckbox(
493
+ null,
494
+ null,
495
+ true,
496
+ 'Stack',
497
+ toolbar,
498
+ function () {
499
+ self.drawInteractive();
500
+ }
501
+ );
502
+ };
503
+
504
+ // #_udpateAggDropdown {{{2
505
+
506
+ Graph.prototype._updateAggDropdown = function () {
507
+ var self = this;
508
+
509
+ // options : [obj]
510
+ // obj : {
511
+ // name : string
512
+ // type : string ('group', 'pivot', 'cell')
513
+ // num : int
514
+ // }
515
+
516
+ var options = [];
517
+
518
+ // addOption : AggregateInfo, string -> ()
519
+
520
+ var addOption = function (aggInfo, appendToName) {
521
+ var name = aggInfo.name || aggInfo.instance.getFullName();
522
+ if (appendToName != null) {
523
+ name += appendToName;
524
+ }
525
+ options.push({
526
+ name: name,
527
+ type: aggInfo.aggType,
528
+ num: aggInfo.aggNum
529
+ });
530
+ };
531
+
532
+ self.view.getData(function (ok, data) {
533
+ self.ui.aggDropdown.children().remove();
534
+
535
+ if (data.isGroup) {
536
+ _.each(getPropDef([], data, 'agg', 'info', 'group'), function (ai) {
537
+ addOption(ai);
538
+ });
539
+ }
540
+ else if (data.isPivot) {
541
+ _.each(getPropDef([], data, 'agg', 'info', 'group'), function (ai) {
542
+ addOption(ai, ' by ' + data.groupFields.join(', '));
543
+ });
544
+ _.each(getPropDef([], data, 'agg', 'info', 'pivot'), function (ai) {
545
+ addOption(ai, ' by ' + data.pivotFields.join(', '));
546
+ });
547
+ _.each(getPropDef([], data, 'agg', 'info', 'cell'), function (ai) {
548
+ addOption(ai);
549
+ });
550
+ }
551
+
552
+ // For pivotted data, there are three different aggregates we could graph. We list them
553
+ // separately in the dropdown, and we want them in the order: cell, group, pivot. It just so
554
+ // happens that this is also alphabetical order, so we just sort by the aggType first before
555
+ // sorting by the aggNum so the dropdown will be in the right order.
556
+
557
+ _.each(_.sortBy(_.sortBy(options, 'type'), 'num'), function (opt) {
558
+ var option = jQuery('<option>', {
559
+ 'value': opt.name,
560
+ 'data-wcdv-agg-type': opt.type,
561
+ 'data-wcdv-agg-num': opt.num,
562
+ }).text(opt.name);
563
+ self.ui.aggDropdown.append(option);
564
+ });
565
+ }, 'Updating graph aggregate dropdown');
566
+ };
567
+
568
+ // #syncDrawnGraphConfigWithUi {{{2
569
+
570
+ /**
571
+ * Synchronize the configuration actually used to draw a graph with the user interface. This updates
572
+ * things like the graph type and aggregate dropdowns to reflect how the graph was drawn.
573
+ *
574
+ * @param {Graph~Config_When} config
575
+ * The configuration used by the graph that was just drawn.
576
+ */
577
+
578
+ Graph.prototype.syncDrawnGraphConfigWithUi = function (config) {
579
+ var self = this;
580
+
581
+ if (config == null) {
582
+ return;
583
+ }
584
+
585
+ var axis = config.graphType === 'bar' ? 'hAxis' : 'vAxis';
586
+ self.ui.graphTypeDropdown.val(config.graphType);
587
+
588
+ if (self.lastOps != null && self.lastOps.group) {
589
+ self.ui.toolbar_aggregates.css('visibility', 'visible');
590
+ var matchingAgg = self.ui.aggDropdown.find('option');
591
+ if (config.aggType != null) {
592
+ matchingAgg = matchingAgg.filter(function (i, elt) {
593
+ return elt.getAttribute('data-wcdv-agg-type') === config.aggType;
594
+ });
595
+ }
596
+ if (config.aggNum != null) {
597
+ matchingAgg = matchingAgg.filter(function (i, elt) {
598
+ return +elt.getAttribute('data-wcdv-agg-num') === config.aggNum;
599
+ });
600
+ }
601
+ if (matchingAgg.length === 1) {
602
+ self.ui.aggDropdown.val(matchingAgg.attr('value'));
603
+ }
604
+ self.ui.zeroAxisCheckbox.prop('checked', getProp(config, 'options', axis, 'minValue') == 0);
605
+ }
606
+ else {
607
+ self.ui.toolbar_aggregates.css('visibility', 'hidden');
608
+ }
609
+
610
+ if (self.lastOps != null && self.lastOps.pivot) {
611
+ self.ui.toolbar_pivot.css('visibility', 'visible');
612
+ self.ui.stackCheckbox.prop('checked', !!getProp(config, 'options', 'isStacked'));
613
+ }
614
+ else {
615
+ self.ui.toolbar_pivot.css('visibility', 'hidden');
616
+ }
617
+ };
618
+
619
+ // #export {{{2
620
+
621
+ Graph.prototype.export = function () {
622
+ var self = this;
623
+
624
+ if (self.exportBlob == null) {
625
+ return;
626
+ }
627
+
628
+ var fileName = (self.opts.title || self.id) + '.png';
629
+ presentDownload(self.exportBlob, fileName);
630
+ };
631
+
632
+ // #_setExportBlob {{{2
633
+
634
+ Graph.prototype._setExportBlob = function (blob) {
635
+ var self = this;
636
+
637
+ self.exportBlob = blob;
638
+ self.ui.exportBtn.prop('disabled', blob == null);
639
+ };
640
+
641
+ // #_clearExportBlob {{{2
642
+
643
+ Graph.prototype._clearExportBlob = function () {
644
+ var self = this;
645
+
646
+ self.exportBlob = null;
647
+ self.ui.exportBtn.prop('disabled', true);
648
+ };
649
+
650
+ // #drawFromConfig {{{2
651
+
652
+ Graph.prototype.drawFromConfig = function () {
653
+ var self = this;
654
+
655
+ self.lastDrawnFrom = 'config';
656
+ self.renderer.draw(self.devConfig, self.userConfig);
657
+ };
658
+
659
+ // #drawInteractive {{{2
660
+
661
+ Graph.prototype.drawInteractive = function () {
662
+ var self = this;
663
+
664
+ var graphType = self.ui.graphTypeDropdown.val();
665
+ var minValue = self.ui.zeroAxisCheckbox.prop('checked') ? 0 : null;
666
+
667
+ var config = {
668
+ group: {
669
+ graphs: {},
670
+ current: graphType
671
+ },
672
+ pivot: {
673
+ graphs: {},
674
+ current: graphType
675
+ }
676
+ };
677
+
678
+ // NOTE The `graphType` field here is useless except that it makes the rendering function (e.g.
679
+ // GraphRendererGoogle#draw_plain) more convenient to implement.
680
+
681
+ var selOptIdx = self.ui.aggDropdown.get(0).selectedIndex;
682
+ var selOpt = self.ui.aggDropdown.get(0).options[selOptIdx];
683
+
684
+ config.group.graphs[graphType] = {
685
+ graphType: graphType,
686
+ aggType: selOpt.getAttribute('data-wcdv-agg-type'),
687
+ aggNum: toInt(selOpt.getAttribute('data-wcdv-agg-num')),
688
+ options: {}
689
+ };
690
+
691
+ // At least with Google Charts, you have to swap the horizontal and vertical axis configuration
692
+ // for bar charts (since they're on their side).
693
+
694
+ switch (graphType) {
695
+ case 'bar':
696
+ config.group.graphs[graphType].options = {
697
+ vAxis: {
698
+ minValue: minValue
699
+ }
700
+ };
701
+ break;
702
+ default:
703
+ config.group.graphs[graphType].options = {
704
+ vAxis: {
705
+ minValue: minValue
706
+ }
707
+ };
708
+ }
709
+
710
+ // Copy everything... not strictly necessary AFAIK, but it's safe.
711
+ config.pivot = deepCopy(config.group);
712
+
713
+ // Make sure to add the stack setting for pivot mode.
714
+ config.pivot.graphs[graphType].options.isStacked = self.ui.stackCheckbox.prop('checked');
715
+
716
+ // Store this configuration in the userConfig so that it can be saved with prefs.
717
+ _.extend(self.userConfig, config);
718
+
719
+ if (self.prefs != null) {
720
+ self.prefs.save();
721
+ }
722
+
723
+ self.logDebug(self.makeLogTag() + ' Drawing graph based on interactive config [userConfig = %O]', self.userConfig);
724
+
725
+ self.lastDrawnFrom = 'interactive';
726
+ self.renderer.draw(self.devConfig, self.userConfig);
727
+ };
728
+
729
+ // #checkGraphConfig {{{2
730
+
731
+ Graph.prototype.checkGraphConfig = function () {
732
+ if (self.devConfig == null) {
733
+ return;
734
+ }
735
+
736
+ _.each(['whenPlain', 'whenGroup', 'whenPivot'], function (dataFormat) {
737
+ if (self.devConfig[dataFormat] === undefined) {
738
+ return;
739
+ }
740
+
741
+ var config = self.devConfig[dataFormat];
742
+
743
+ // Check the "graphType" property.
744
+
745
+ if (config.graphType != null) {
746
+ if (!_.isString(config.graphType)) {
747
+ throw new Error('Graph config error: data format "' + dataFormat + '": `graphType` must be a string');
748
+ }
749
+
750
+ if (['area', 'bar', 'column', 'line', 'pie'].indexOf(config.graphType) === -1) {
751
+ throw new Error('Graph config error: data format "' + dataFormat + '": invalid `graphType`: ' + config.graphType);
752
+ }
753
+ }
754
+
755
+ switch (config.graphType) {
756
+ case 'area':
757
+ case 'bar':
758
+ case 'column':
759
+ case 'line':
760
+ case 'pie':
761
+ if (config.valueField != null && config.valueFields != null) {
762
+ throw new Error('Graph config error: data format "' + dataFormat + '": can\'t define both `valueField` and `valueFields`');
763
+ }
764
+
765
+ // Turn the singular "valueField" into the plural "valueFields."
766
+
767
+ if (config.valueField != null) {
768
+ if (!_.isString(config.valueField)) {
769
+ throw new Error('Graph config error: data format "' + dataFormat + '": `valueField` must be a string');
770
+ }
771
+ config.valueFields = [config.valueField];
772
+ delete config.valueField;
773
+ }
774
+
775
+ // Check the "valueFields" property, if it exists.
776
+
777
+ if (config.valueFields != null) {
778
+ if (!_.isArray(config.valueFields)) {
779
+ throw new Error('Graph config error: data format "' + dataFormat + '": `valueFields` must be an array');
780
+ }
781
+
782
+ _.each(config.valueFields, function (f, i) {
783
+ if (!_.isString(f)) {
784
+ throw new Error('Graph config error: data format "' + dataFormat + '": `valueFields[' + i + ']` must be a string');
785
+ }
786
+ });
787
+ }
788
+ }
789
+ });
790
+ };
791
+
792
+ // #refresh {{{2
793
+
794
+ /**
795
+ * Refreshes the data from the data view in the grid.
796
+ *
797
+ * @method
798
+ * @memberof Grid
799
+ */
800
+
801
+ Graph.prototype.refresh = function () {
802
+ var self = this;
803
+
804
+ self.view.clearSourceData();
805
+ };
806
+
807
+ // #redraw {{{2
808
+
809
+ Graph.prototype.redraw = function () {
810
+ var self = this;
811
+
812
+ if (self.renderer != null) {
813
+ self.renderer.destroy();
814
+ }
815
+
816
+ self.prefs.prime(function () {
817
+ self.checkGraphConfig();
818
+ var ctor = getProp(self.opts, 'renderer') && GRAPH_RENDERER_REGISTRY.isSet(self.opts.renderer)
819
+ ? GRAPH_RENDERER_REGISTRY.get(self.opts.renderer)
820
+ : GRAPH_RENDERER_REGISTRY.get('google');
821
+ self.renderer = new ctor(self, self.ui.graph, self.view, self.opts);
822
+ self.renderer.on('draw', function (config) {
823
+ self.syncDrawnGraphConfigWithUi(config);
824
+ });
825
+ self._setGraphTypeOptions();
826
+ self.drawFromConfig();
827
+ }, {
828
+ who: self
829
+ });
830
+ };
831
+
832
+ // #hide {{{2
833
+
834
+ /**
835
+ * Hide the grid.
836
+ *
837
+ * @method
838
+ * @memberof Grid
839
+ */
840
+
841
+ Graph.prototype.hide = function () {
842
+ var self = this;
843
+
844
+ self.ui.content.hide({
845
+ duration: 0,
846
+ done: function () {
847
+ if (self.opts.title) {
848
+ self.ui.showHideButton.removeClass('open');
849
+ self.ui.showHideButton.children('svg.wcdv_icon').removeClass('wcdv_icon_rotate_180');
850
+ }
851
+ }
852
+ });
853
+ };
854
+
855
+ // #show {{{2
856
+
857
+ /**
858
+ * Make the grid visible. If the grid has not been "run" yet, it will be done now.
859
+ *
860
+ * @param {object} [opts]
861
+ *
862
+ * @param {boolean} [opts.redraw=true]
863
+ * If true, automatically redraw the grid after it has been shown. This is almost always what you
864
+ * want, unless you intend to manually call `redraw()` or `refresh()` immediately after showing it.
865
+ */
866
+
867
+ Graph.prototype.show = function (opts) {
868
+ var self = this;
869
+
870
+ opts = deepDefaults(opts, {
871
+ redraw: true
872
+ });
873
+
874
+ self.ui.content.show({
875
+ duration: 0,
876
+ done: function () {
877
+ if (self.opts.title) {
878
+ self.ui.showHideButton.addClass('open');
879
+ self.ui.showHideButton.children('svg.wcdv_icon').addClass('wcdv_icon_rotate_180');
880
+ }
881
+ if (!self.hasRun && opts.redraw) {
882
+ self.hasRun = true;
883
+ self.redraw();
884
+ }
885
+ }
886
+ });
887
+ };
888
+
889
+ // #toggle {{{2
890
+
891
+ /**
892
+ * Toggle graph visibility.
893
+ */
894
+
895
+ Graph.prototype.toggle = function () {
896
+ var self = this;
897
+
898
+ if (self.ui.content.css('display') === 'none') {
899
+ self.show();
900
+ }
901
+ else {
902
+ self.hide();
903
+ }
904
+ };
905
+
906
+ // #isVisible {{{2
907
+
908
+ /**
909
+ * Determine if the graph is currently visible.
910
+ *
911
+ * @returns {boolean}
912
+ * True if the graph is currently visible, false if it is not.
913
+ */
914
+
915
+ Graph.prototype.isVisible = function () {
916
+ var self = this;
917
+
918
+ return self.ui.content.css('display') !== 'none';
919
+ };
920
+
921
+ // #_setSpinner {{{2
922
+
923
+ /**
924
+ * Set the type of the spinner icon.
925
+ *
926
+ * @param {string} what
927
+ * The kind of spinner icon to show. Must be one of: loading, not-loaded, working.
928
+ */
929
+
930
+ Graph.prototype._setSpinner = function (what) {
931
+ var self = this;
932
+
933
+ switch (what) {
934
+ case 'loading':
935
+ self.ui.spinner.html(icon('refresh-cw', ['wcdv_icon_spin'], 'Loading...'));
936
+ break;
937
+ case 'not-loaded':
938
+ self.ui.spinner.html(icon('ban', null, 'Not Loaded'));
939
+ break;
940
+ case 'working':
941
+ self.ui.spinner.html(icon('loader-circle', ['wcdv_icon_spin'], 'Working...'));
942
+ break;
943
+ }
944
+ };
945
+
946
+ // #_showSpinner {{{2
947
+
948
+ /**
949
+ * Show the spinner icon.
950
+ */
951
+
952
+ Graph.prototype._showSpinner = function () {
953
+ var self = this;
954
+
955
+ self.ui.spinner.show();
956
+ };
957
+
958
+ // #_hideSpinner {{{2
959
+
960
+ /**
961
+ * Hide the spinner icon.
962
+ */
963
+
964
+ Graph.prototype._hideSpinner = function () {
965
+ var self = this;
966
+
967
+ self.ui.spinner.hide();
968
+ };
969
+
970
+ // #setUserConfig {{{2
971
+
972
+ Graph.prototype.setUserConfig = function (config) {
973
+ var self = this;
974
+
975
+ self.userConfig = config;
976
+
977
+ // When the constructor binds to prefs, this method can be called before the renderer is created.
978
+ // That's not a big deal, just don't do anything here if that's the case.
979
+
980
+ if (self.renderer != null) {
981
+ self.renderer.draw(self.devConfig, self.userConfig);
982
+ }
983
+ };
984
+
985
+ // GraphControl {{{1
986
+
987
+ var GraphControl = makeSubclass('GraphControl', Object, function () {
988
+ var self = this;
989
+
990
+ self.ui = {};
991
+ });
992
+
993
+ // #draw {{{2
994
+
995
+ GraphControl.prototype.draw = function () {
996
+ var self = this;
997
+
998
+ self.view.on('getTypeInfo', function (typeInfo) {
999
+ var fields = [];
1000
+
1001
+ _.each(determineColumns(null, null, typeInfo), function (fieldName) {
1002
+ var text = getProp(self.colConfig, fieldName, 'displayText') || fieldName;
1003
+ fields.push({ fieldName: fieldName, displayText: text });
1004
+ });
1005
+
1006
+ // Graph Type Dropdown
1007
+
1008
+ self.ui.graphType = jQuery('<select>');
1009
+
1010
+ if (getProp(self.renderer, 'prototype', 'graphTypes')) {
1011
+ self.renderer.prototype.graphTypes.each(function (gt) {
1012
+ self.ui.graphType.append(jQuery('<option>', { 'value': gt.value }).text(gt.name));
1013
+ });
1014
+ }
1015
+
1016
+ self.ui.root.append(jQuery('<div>').append(self.ui.graphType));
1017
+
1018
+ // Plain Data Configuration
1019
+
1020
+ self.ui.plainCheckbox = jQuery('<input>', { 'type': 'checkbox', 'checked': 'checked' })
1021
+ .on('change', function () {
1022
+ if (self.ui.plainCheckbox.prop('checked')) {
1023
+ self.ui.plainConfig.show();
1024
+ }
1025
+ else {
1026
+ self.ui.plainConfig.hide();
1027
+ }
1028
+ });
1029
+
1030
+ self.ui.root.append(
1031
+ jQuery('<span>', { 'class': 'wcdv_title' })
1032
+ .append(self.ui.plainCheckbox)
1033
+ .append('Plain Data')
1034
+ );
1035
+
1036
+ self.ui.plainCategoryField = jQuery('<select>')
1037
+ .on('change', function () {
1038
+ self.defn.whenPlain.categoryField = self.ui.plainCategoryField.val();
1039
+ });
1040
+ self.ui.plainValueField = jQuery('<select>')
1041
+ .on('change', function () {
1042
+ self.defn.whenPlain.valueField = self.ui.plainValueField.val();
1043
+ });
1044
+
1045
+ _.each(fields, function (f) {
1046
+ self.ui.plainCategoryField.append(
1047
+ jQuery('<option>', { 'value': f.fieldName }).text(f.displayText)
1048
+ );
1049
+ self.ui.plainValueField.append(
1050
+ jQuery('<option>', { 'value': f.fieldName }).text(f.displayText)
1051
+ );
1052
+ });
1053
+
1054
+ self.ui.plainConfig = jQuery('<div>')
1055
+ .append(
1056
+ jQuery('<div>')
1057
+ .append('Category Field: ')
1058
+ .append(self.ui.plainCategoryField)
1059
+ )
1060
+ .append(
1061
+ jQuery('<div>')
1062
+ .append('Value Field: ')
1063
+ .append(self.ui.plainValueField)
1064
+ )
1065
+ .appendTo(self.ui.root);
1066
+
1067
+ // Group Data Configuration
1068
+
1069
+
1070
+
1071
+ // Pivot Data Configuration
1072
+ }, { limit: 1 });
1073
+ };
1074
+
1075
+ // Exports {{{1
1076
+
1077
+ export {
1078
+ Graph,
1079
+ };