Completed
Push — master ( 6a5067...3fd1c7 )
by Chris
01:17
created

app.js ➔ ... ➔ d3.map   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
nc 1
nop 1
1
/** global: d3 */
2
/**
3
 * Bootstrapping functions, event handling, etc... for application.
4
 */
5
6
var jsondash = function() {
7
    var my = {
8
        chart_wall: null
9
    };
10
    var dashboard_data = null;
11
    var $API_ROUTE_URL = '[name="dataSource"]';
12
    var $API_PREVIEW = '#api-output';
13
    var $API_PREVIEW_BTN = '#api-output-preview';
14
    var $MODULE_FORM = '#module-form';
15
    var $VIEW_BUILDER = '#view-builder';
16
    var $ADD_MODULE = '#add-module';
17
    var $MAIN_CONTAINER = '#container';
18
    var $EDIT_MODAL = '#chart-options';
19
    var $DELETE_BTN = '#delete-widget';
20
    var $DELETE_DASHBOARD = '.delete-dashboard';
21
    var $SAVE_MODULE = '#save-module';
22
    var $EDIT_CONTAINER = '#edit-view-container';
23
24
    function addWidget(container, model) {
25
        if(document.querySelector('[data-guid="' + model.guid + '"]')) return d3.select('[data-guid="' + model.guid + '"]');
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
26
        return d3.select(container).select('div')
27
            .append('div')
28
            .classed({item: true, widget: true})
29
            .attr('data-guid', model.guid)
30
            .attr('data-refresh', model.refresh)
31
            .attr('data-refresh-interval', model.refreshInterval)
32
            .style('width', model.width + 'px')
33
            .style('height', model.height + 'px')
34
            .html(d3.select('#chart-template').html())
35
            .select('.widget-title .widget-title-text').text(model.name);
36
    }
37
38
    function previewAPIRoute(e) {
39
        e.preventDefault();
40
        // Shows the response of the API field as a json payload, inline.
41
        $.ajax({
42
            type: 'get',
43
            url: $($API_ROUTE_URL).val().trim(),
44
            success: function(d) {
45
               $($API_PREVIEW).html(prettyCode(d));
46
            },
47
            error: function(d, status, error) {
48
                $($API_PREVIEW).html(error);
49
            }
50
        });
51
    }
52
53
    function refreshableType(type) {
54
        if(type === 'youtube') {return false;}
55
        return true;
56
    }
57
58
    function saveModule(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
59
        var data     = jsondash.util.serializeToJSON($($MODULE_FORM).serializeArray());
60
        var newfield = $('<input class="form-control" type="text">');
61
        var id       = jsondash.util.guid();
62
        // Add a unique guid for referencing later.
63
        data['guid'] = id;
64
        // Add family for lookups
65
        data['family'] = $($MODULE_FORM).find('[name="type"]').find('option:selected').data().family;
66
        if(!data.refresh || !refreshableType(data.type)) {data['refresh'] = false;}
67
        if(!data.override) {data['override'] = false;}
68
        newfield.attr('name', 'module_' + id);
69
        newfield.val(JSON.stringify(data));
70
        $('.modules').append(newfield);
71
        // Add new visual block to view grid
72
        addWidget($VIEW_BUILDER, data);
73
        // Refit the grid
74
        fitGrid();
75
    }
76
77
    function isModalButton(e) {
78
        return e.relatedTarget.id === $ADD_MODULE.replace('#', '');
79
    }
80
81
    function updateEditForm(e) {
82
        var module_form = $($MODULE_FORM);
83
        // If the modal caller was the add modal button, skip populating the field.
84
        if(isModalButton(e)) {
85
            module_form.find('input').each(function(_, input){
86
                $(input).val('');
87
            });
88
            $($API_PREVIEW).empty();
89
            $($DELETE_BTN).hide();
90
            return;
91
        }
92
        $($DELETE_BTN).show();
93
        // Updates the fields in the edit form to the active widgets values.
94
        var item = $(e.relatedTarget).closest('.item.widget');
95
        var guid = item.data().guid;
96
        var module = getModule(item);
97
        // Update the modal window fields with this one's value.
98
        $.each(module, function(field, val){
99
            if(field === 'override' || field === 'refresh') {
100
                module_form.find('[name="' + field + '"]').prop('checked', val);
101
            } else {
102
                module_form.find('[name="' + field + '"]').val(val);
103
            }
104
        });
105
        // Update with current guid for referencing the module.
106
        module_form.attr('data-guid', guid);
107
        populateOrderField(module);
108
    }
109
110
    function populateOrderField(module) {
111
        var module_form = $($MODULE_FORM);
112
        var widgets = $('.item.widget');
113
        // Add the number of items to order field.
114
        var order_field = module_form.find('[name="order"]');
115
        var max_options = widgets.length > 0 ? widgets.length + 1 : 2;
116
        order_field.find('option').remove();
117
        // Add empty option.
118
        order_field.append('<option value=""></option>');
119
        d3.map(d3.range(1, max_options), function(i){
120
            var option = $('<option></option>');
121
            option.val(i).text(i);
122
            order_field.append(option);
123
        });
124
        order_field.val(module && module.order ? module.order : '');
125
    }
126
127
    function updateModule(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
128
        var module_form = $($MODULE_FORM);
129
        // Updates the module input fields with new data by rewriting them all.
130
        var guid = module_form.attr('data-guid');
131
        var active = getModuleByGUID(guid);
132
        // Update the modules values to the current input values.
133
        module_form.find('input').each(function(_, input){
134
            var name = $(input).attr('name');
135
            if(name) {
136
                if(name === 'override' || name === 'refresh') {
137
                    // Convert checkbox to json friendly format.
138
                    active[name] = $(input).is(':checked');
139
                } else {
140
                    active[name] = $(input).val();
141
                }
142
            }
143
        });
144
        // Update bar chart type
145
        active['type'] = module_form.find('[name="type"]').val();
146
        // Update order
147
        active['order'] = parseInt(module_form.find('[name="order"]').val(), 10);
148
        // Clear out module input values
149
        $('.modules').empty();
150
        $.each(dashboard_data.modules, function(i, module){
151
            var val = JSON.stringify(module, module);
152
            var input = $('<input type="text" name="module_' + i + '" class="form-control">');
153
            input.val(val);
154
            $('.modules').append(input);
155
        });
156
        updateWidget(active);
157
        $($EDIT_CONTAINER).collapse();
158
        // Refit the grid
159
        fitGrid();
160
    }
161
162
    function updateWidget(config) {
163
        // Trigger update form into view since data is dirty
164
        // Update visual size to existing widget.
165
        var widget = getModuleWidgetByGUID(config.guid);
166
        loader(widget);
167
        widget.style({
168
            height: config.height + 'px',
169
            width: config.width + 'px'
170
        });
171
        widget.select('.widget-title .widget-title-text').text(config.name);
172
        loadWidgetData(widget, config);
173
    }
174
175
    function refreshWidget(e) {
176
        e.preventDefault();
177
        var config = getModule($(this).closest('.widget'));
178
        var widget = addWidget($MAIN_CONTAINER, config);
179
        loadWidgetData(widget, config);
180
        fitGrid();
181
    }
182
183
    function addChartContainers(container, data) {
184
        for(var name in data.modules){
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
185
            // Closure to maintain each chart data value in loop
186
            (function(config){
187
                var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 184. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
188
                // Add div wrappers for js grid layout library,
189
                // and add title, icons, and buttons
190
                var widget = addWidget(container, config);
191
                // Determine how to load this widget
192
                loadWidgetData(widget, config);
193
            })(data.modules[name]);
194
        }
195
        fitGrid();
196
    }
197
198
    function getModuleWidgetByGUID(guid) {
199
        return d3.select('.item.widget[data-guid="' + guid + '"]');
200
    }
201
202
    function getModuleByGUID(guid) {
203
        return dashboard_data.modules.find(function(n){return n['guid'] === guid});
204
    }
205
206
    function deleteModule(e) {
207
        e.preventDefault();
208
        if(!confirm('Are you sure?')) {return;}
209
        var guid = $($MODULE_FORM).attr('data-guid');
210
        // Remove form input and visual widget
211
        $('.modules').find('#' + guid).remove();
212
        $('.item.widget[data-guid="' + guid + '"]').remove();
213
        $($EDIT_MODAL).modal('hide');
214
        // Redraw wall to replace visual 'hole'
215
        fitGrid();
216
        // Trigger update form into view since data is dirty
217
        $($EDIT_CONTAINER).collapse('show');
218
    }
219
220
    function addDomEvents() {
221
        // TODO: debounce/throttle
222
        $($API_ROUTE_URL).on('change.charts', previewAPIRoute);
223
        $($API_PREVIEW_BTN).on('click.charts', previewAPIRoute);
224
        // Save module popup form
225
        $($SAVE_MODULE).on('click.charts.module', saveModule);
226
        // Edit existing modules
227
        $($EDIT_MODAL).on('show.bs.modal', updateEditForm);
228
        $('#update-module').on('click.charts.module', updateModule);
229
        // Allow swapping of edit/update events
230
        // for the add module button and form modal
231
        $($ADD_MODULE).on('click.charts', function(){
232
            $('#update-module')
233
            .attr('id', $SAVE_MODULE.replace('#', ''))
234
            .text('Save module')
235
            .off('click.charts.module')
236
            .on('click.charts', saveModule);
237
        });
238
        // Allow swapping of edit/update events
239
        // for the edit button and form modal
240
        $('.widget-edit').on('click.charts', function(){
241
            $($SAVE_MODULE)
242
            .attr('id', 'update-module')
243
            .text('Update module')
244
            .off('click.charts.module')
245
            .on('click.charts', updateModule);
246
        });
247
        // Add delete button for existing widgets.
248
        $($DELETE_BTN).on('click.charts', deleteModule);
249
        // Add delete confirm for dashboards.
250
        $($DELETE_DASHBOARD).on('submit.charts', function(e){
251
            if(!confirm('Are you sure?')) e.preventDefault();
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
252
        });
253
    }
254
255
    function initGrid(container) {
0 ignored issues
show
Unused Code introduced by
The parameter container is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
256
        fitGrid({
257
            columnWidth: 5,
258
            itemSelector: '.item',
259
            transitionDuration: 0,
260
            fitWidth: true
261
        }, true);
262
        $('.item.widget').removeClass('hidden');
263
    }
264
265
    function fitGrid(opts, init) {
266
        var valid_options = $.isPlainObject(opts);
0 ignored issues
show
Unused Code introduced by
The variable valid_options seems to be never used. Consider removing it.
Loading history...
267
        var options = $.extend({}, opts, {});
268
        if(init) {
269
            my.chart_wall = $('#container').packery(options);
270
            items = my.chart_wall.find('.item').draggable({
0 ignored issues
show
Bug introduced by
The variable items seems to be never declared. Assigning variables without defining them first makes them global. If this was intended, consider making it explicit like using window.items.
Loading history...
271
                scroll: true,
272
                handle: '.dragger',
273
                stop: function(){
274
                    $($EDIT_CONTAINER).collapse('show');
275
                    updateModuleOrder();
276
                    my.chart_wall.packery(options);
277
                }
278
            });
279
            my.chart_wall.packery('bindUIDraggableEvents', items);
280
        } else {
281
            my.chart_wall.packery(options);
282
        }
283
    }
284
285
    function updateModuleOrder() {
286
        var items = my.chart_wall.packery('getItemElements');
287
        // Update module order
288
        $.each(items, function(i, el){
0 ignored issues
show
Unused Code introduced by
The parameter el is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
289
            var module = getModule($(this));
290
            var config = $.extend(module, {order: i});
291
            updateModuleInput(config);
292
        });
293
    }
294
295
    function getModule(el) {
296
        // Return module by element
297
        var data = el.data();
298
        var guid = data.guid;
299
        var module = getModuleByGUID(guid);
300
        return module;
301
    }
302
303
    function loader(container) {
304
        container.select('.loader-overlay').classed({hidden: false});
305
        container.select('.widget-loader').classed({hidden: false});
306
    }
307
308
    function unload(container) {
309
        container.select('.loader-overlay').classed({hidden: true});
310
        container.select('.widget-loader').classed({hidden: true});
311
    }
312
313
    function handleInputs(widget, config) {
314
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
315
        // Load event handlers for these newly created forms.
316
        $(inputs_selector).find('form').on('submit', function(e){
317
            e.stopImmediatePropagation();
318
            e.preventDefault();
319
            // Just create a new url for this, but use existing config.
320
            // The global object config will not be altered.
321
            // The first {} here is important, as it enforces a deep copy,
322
            // not a mutation of the original object.
323
            var url = config.dataSource;
324
            // Ensure we don't lose params already save on this endpoint url.
325
            var existing_params = url.split('?')[1];
326
            var params = getValidParamString($(this).serializeArray());
327
            var _config = $.extend({}, config, {
328
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
329
            });
330
            // Otherwise reload like normal.
331
            loadWidgetData(widget, _config);
332
            // Hide the form again
333
            $(inputs_selector).removeClass('in');
334
        });
335
    }
336
337
    function getValidParamString(arr) {
338
        // Jquery $.serialize and $.serializeArray will
339
        // return empty query parameters, which is undesirable and can
340
        // be error prone for RESTFUL endpoints.
341
        // e.g. `foo=bar&bar=` becomes `foo=bar`
342
        var param_str = '';
343
        arr = arr.filter(function(param, i){return param.value !== '';});
0 ignored issues
show
Unused Code introduced by
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
344
        $.each(arr, function(i, param){
345
            param_str += (param.name + '=' + param.value);
346
            if(i < arr.length - 1 && arr.length > 1) param_str += '&';
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
347
        });
348
        return param_str;
349
    }
350
351
    function loadWidgetData(widget, config) {
352
        loader(widget);
353
        try {
354
            // Handle any custom inputs the user specified for this module.
355
            // They map to standard form inputs and correspond to query
356
            // arguments for this dataSource.
357
            if(config.inputs) {handleInputs(widget, config);}
358
359
            if(config.type === 'datatable') {
360
                jsondash.handlers.handleDataTable(widget, config);
361
            }
362
            else if(jsondash.util.isSparkline(config.type)) {
363
                jsondash.handlers.handleSparkline(widget, config);
364
            }
365
            else if(config.type === 'iframe') {
366
                jsondash.handlers.handleIframe(widget, config);
367
            }
368
            else if(config.type === 'timeline') {
369
                jsondash.handlers.handleTimeline(widget, config);
370
            }
371
            else if(config.type === 'venn') {
372
                jsondash.handlers.handleVenn(widget, config);
373
            }
374
            else if(config.type === 'number') {
375
                jsondash.handlers.handleSingleNum(widget, config);
376
            }
377
            else if(config.type === 'youtube') {
378
                jsondash.handlers.handleYoutube(widget, config);
379
            }
380
            else if(config.type === 'graph'){
381
                jsondash.handlers.handleGraph(widget, config);
382
            }
383
            else if(config.type === 'custom') {
384
                jsondash.handlers.handleCustom(widget, config);
385
            }
386
            else if(config.type === 'plotly-any') {
387
                jsondash.handlers.handlePlotly(widget, config);
388
            }
389
            else if(jsondash.util.isD3Subtype(config)) {
390
                jsondash.handlers.handleD3(widget, config);
391
            } else {
392
                jsondash.handlers.handleC3(widget, config);
393
            }
394
        } catch(e) {
395
            if(console && console.error) console.error(e);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
396
            unload(widget);
397
        }
398
        addResizeEvent(widget, config);
399
    }
400
401
    function addResizeEvent(widget, config) {
402
        // Add resize event
403
        $(widget[0]).resizable({
404
            helper: 'resizable-helper',
405
            minWidth: 200,
406
            minHeight: 200,
407
            stop: function(event, ui) {
408
                // Update the configs dimensions.
409
                config = $.extend(config, {width: ui.size.width, height: ui.size.height});
410
                updateModuleInput(config);
411
                loadWidgetData(widget, config);
412
                fitGrid();
413
                // Open save panel
414
                $($EDIT_CONTAINER).collapse('show');
415
            }
416
        });
417
    }
418
419
    function updateModuleInput(config) {
420
        $('input[id="' + config.guid + '"]').val(JSON.stringify(config));
421
    }
422
423
    function prettyCode(code) {
424
        if(typeof code === "object") return JSON.stringify(code, null, 4);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
425
        return JSON.stringify(JSON.parse(code), null, 4);
426
    }
427
428
    function addRefreshers(modules) {
429
        $.each(modules, function(_, module){
430
            if(module.refresh && module.refreshInterval) {
431
                var container = d3.select('[data-guid="' + module.guid + '"]');
432
                setInterval(function(){
433
                    loadWidgetData(container, module);
434
                }, parseInt(module.refreshInterval, 10));
435
            }
436
        });
437
    }
438
439
    function loadDashboard(data) {
440
        // Load the grid before rendering the ajax, since the DOM
441
        // is rendered server side.
442
        initGrid($MAIN_CONTAINER);
443
        // Add actual ajax data.
444
        addChartContainers($MAIN_CONTAINER, data);
445
        dashboard_data = data;
446
447
        // Add event handlers for widget UI
448
        $('.widget-refresh').on('click.charts', refreshWidget);
449
450
        // Setup refresh intervals for all widgets that specify it.
451
        addRefreshers(data.modules);
452
453
        // Format json config display
454
        $('#json-output').on('show.bs.modal', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
455
            var code = $(this).find('code').text();
456
            $(this).find('code').text(prettyCode(code));
457
        });
458
459
        // Reformat the code inside of the raw json field, to pretty print
460
        // for the user.
461
        $('#raw-config').text(prettyCode($('#raw-config').text()));
462
463
        // Setup responsive handlers
464
        var jres = jRespond([
465
        {
466
            label: 'handheld',
467
            enter: 0,
468
            exit: 767
469
        }
470
        ]);
471
        jres.addFunc({
472
            breakpoint: 'handheld',
473
            enter: function() {
474
                $('.widget').css({
475
                    'max-width': '100%',
476
                    'width': '100%',
477
                    'position': 'static'
478
                });
479
            }
480
        });
481
        populateOrderField();
482
    }
483
    my.config = {
484
        WIDGET_MARGIN_X: 20,
485
        WIDGET_MARGIN_Y: 60
486
    };
487
    my.loadDashboard = loadDashboard;
488
    my.handlers = {};
489
    my.util = {};
490
    my.loader = loader;
491
    my.unload = unload;
492
    my.addDomEvents = addDomEvents;
493
    return my;
494
}();
495