Skip to content
Snippets Groups Projects
card.services.js 18.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • Cam Saul's avatar
    Cam Saul committed
    'use strict';
    /*global _*/
    
    // Card Services
    
    var CardServices = angular.module('metabase.card.services', ['ngResource', 'ngCookies']);
    
    Cam Saul's avatar
    Cam Saul committed
    
    CardServices.factory('Card', ['$resource', '$cookies', function($resource, $cookies) {
        return $resource('/api/card/:cardId', {}, {
            list: {
                url: '/api/card/?org=:orgId&f=:filterMode',
                method: 'GET',
                isArray: true
            },
            create: {
                url: '/api/card',
                method: 'POST',
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            get: {
                method: 'GET',
                params: {
                    cardId: '@cardId'
                }
            },
            update: {
                method: 'PUT',
                params: {
                    cardId: '@id'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            delete: {
                method: 'DELETE',
                params: {
                    cardId: '@cardId'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            isfavorite: {
                url: '/api/card/:cardId/favorite',
                method: 'GET',
                params: {
                    cardId: '@cardId'
                }
            },
            favorite: {
                url: '/api/card/:cardId/favorite',
                method: 'POST',
                params: {
                    cardId: '@cardId'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            unfavorite: {
                url: '/api/card/:cardId/favorite',
                method: 'DELETE',
                params: {
                    cardId: '@cardId'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            }
        });
    }]);
    
    
    Cam Saul's avatar
    Cam Saul committed
    CardServices.service('VisualizationUtils', [function() {
        this.visualizationTypes = {
            scalar: {
                display: 'scalar',
                label: 'Scalar',
                available: false,
                notAvailableReasons: []
            },
            table: {
                display: 'table',
                label: 'Table',
                available: false,
                notAvailableReasons: []
            },
            pie: {
                display: 'pie',
                label: 'Pie Chart',
                available: false,
                notAvailableReasons: []
            },
            bar: {
                display: 'bar',
                label: 'Bar Chart',
                available: false,
                notAvailableReasons: []
            },
            line: {
                display: 'line',
                label: 'Line Chart',
                available: false,
                notAvailableReasons: []
            },
            area: {
                display: 'area',
                label: 'Area Chart',
                available: false,
                notAvailableReasons: []
            },
            timeseries: {
                display: 'timeseries',
                label: 'Time Series',
                available: false,
                notAvailableReasons: []
            },
            pin_map: {
                display: 'pin_map',
                label: 'Pin Map',
                available: false,
                notAvailableReasons: []
            },
            state: {
                display: 'state',
                label: 'State Heatmap',
                available: false,
                notAvailableReasons: []
            },
            country: {
                display: 'country',
                label: 'World Heatmap',
                available: false,
                notAvailableReasons: []
            }
        };
    
    Cam Saul's avatar
    Cam Saul committed
        this.zoomTypes = [{
            'label': 'Disabled',
            'value': null
        }, {
            'label': 'X',
            'value': 'x'
        }, {
            'label': 'Y',
            'value': 'y'
        }, {
            'label': 'XY',
            'value': 'xy'
        }];
    
    Cam Saul's avatar
    Cam Saul committed
    CardServices.service('QueryUtils', function() {
        this.limitOptions = [{
            label: "1",
            value: 1
        }, {
            label: "10",
            value: 10
        }, {
            label: "25",
            value: 25
        }, {
            label: "50",
            value: 50
        }, {
            label: "100",
            value: 100
        }, {
            label: "1000",
            value: 1000
        }];
    
        this.emptyQuery = function() {
    
            return {
                filter: [
                    null,
                    null
                ],
                source_table: null,
    
    Cam Saul's avatar
    Cam Saul committed
                breakout: [],
    
    Cam Saul's avatar
    Cam Saul committed
                aggregation: [],
    
                database: 1,
                type: null,
                native: {}
            };
        };
    
        // default query card settings
        this.queryCardSettings = {
            "allowFavorite": true,
            "allowAddToDash": true,
            "allowRemoveFromDash": false,
            "allowCardPermalink": false,
            "allowLinkToComments": false,
            "allowSend": false,
            "allowTitleEdits": false
        };
    
    
    Cam Saul's avatar
    Cam Saul committed
        this.populateQueryOptions = function(table) {
    
            // create empty objects to store our lookups
            table.fields_lookup = {};
    
            _.each(table.fields, function(field) {
                table.fields_lookup[field.id] = field;
                field.operators_lookup = {};
                _.each(field.valid_operators, function(operator) {
                    field.operators_lookup[operator.name] = operator;
                });
            });
    
            return table;
        };
    
        // @TODO - this really should not touch $scope in any way
        this.getFirstColumnBySpecialType = function(special_type, data) {
            if (!data) {
                return null;
            }
            var result;
            data.cols.forEach(function(col, index) {
                if (typeof col.special_type !== "undefined" && col.special_type == special_type) {
                    col.index = index;
                    if (typeof result == "undefined") {
                        result = col;
                    }
                }
            });
            return result;
        };
        /* @check validity */
    
        /// Check that QUERY is valid (i.e., can be ran or saved, to enable/disable corresponding buttons)
        /// Try not to make this too expensive since it gets ran on basically every event loop in the Card Builder
        ///
        /// Currently the only thing we're doing here is checking the 'filter' clause of QUERY
    
        this.queryIsValid = function(query) {
    
            if (!query) return false;
    
            // ******************** CHECK THAT QUERY.FILTER IS VALID ******************** //
            // if query.filter is undefined or [null, null] then we'll consider it to be "unset" which means it's ok
    
            if (!query.filter || (query.filter.length === 2 && query.filter[0] === null && query.filter[1] === null)) return true;
    
    
            // a filter is valid if it and its children don't contain any nulls
            var containsNulls = function(obj) {
                if (obj === null) return true;
    
                // if we're looking at an Array recurse over each child
                if (obj.constructor === Array) {
                    var len = obj.length;
                    for (var i = 0; i < len; i++) {
                        if (containsNulls(obj[i])) return true; // return immediately if we see a null
                    }
                }
                return false;
            };
    
            return !containsNulls(query.filter);
        };
    
        this.clearExtraQueryData = function(query) {
            var typelist = ['native', 'query', 'result'];
            for (var i = 0; i < typelist.length; i++) {
                if (query.type != typelist[i]) {
                    delete query[typelist[i]];
                }
            }
    
            return query;
        };
    
        this.getEncodedQuery = function(query) {
            return encodeURIComponent(JSON.stringify(query));
        };
    
    });
    
    
    Cam Saul's avatar
    Cam Saul committed
    CardServices.service('VisualizationSettings', [function() {
    
        var DEFAULT_COLOR_HARMONY = [
            '#ac457d',
            '#7fb846',
            '#5994cb',
            '#434348',
            '#90ed7d',
            '#f7a35c',
            '#8085e9',
            '#f15c80',
            '#e4d354',
            '#8d4653',
            '#91e8e1',
            '#7cb5ec'
        ];
    
    
    Cam Saul's avatar
    Cam Saul committed
        /* *** visualization settings ***
         *
         * This object defines default settings for card visualizations (i.e. charts, maps, etc).
         * Each visualization type can be associated with zero or more top-level settings groups defined in this object
         * (i.e. line charts may use 'chart', 'xAxis', 'yAxis', 'line'), depending on the settings that are appropriate
         * for the particular visualization type (the associations are defined below in groupsForVisualizations).
         *
         * Before a card renders, the default settings from the appropriate groups are first copied from this object,
         * creating an in-memory default settings object for that rendering.
         * Then, a settings object stored in the card's record in the database is read and any attributes defined there
         * are applied to that in-memory default settings object (using _.defaults()).
         * The resulting in-memory settings object is made available to the card renderer at the time
         * visualization is rendered.
         *
         * The settings object stored in the DB is 'sparse': only settings that differ from the defaults
         * (at the time the settings were set) are recorded in the DB. This allows us to easily change the appearance of
         * visualizations globally, except in cases where the user has explicitly changed the default setting.
         *
         * Some settings accept aribtrary numbers or text (i.e. titles) and some settings accept only certain values
         * (i.e. *_enabled settings must be one of true or false). However, this object does not define the constraints.
         * Instead, the controller that presents the UI to change the settings is currently responsible for enforcing the
         * appropriate contraints for each setting.
         *
         * Search for '*** visualization settings ***' in card.controllers.js to find the objects that contain
         * choices for the settings that require them.
         * Additional constraints are enforced by the input elements in the views for each settings group
         * (see app/card/partials/settings/*.html).
         *
         */
        var settings = {
            'global': {
                'title': null
            },
            'columns': {
                'dataset_column_titles': [] //allows the user to define custom titles for each column in the resulting dataset. Each item in this array corresponds to a column in the dataset's data.columns array.
            },
            'chart': {
                'plotBackgroundColor': '#FFFFFF',
                'borderColor': '#528ec5',
                'zoomType': 'x',
                'panning': true,
                'panKey': 'shift',
                'export_menu_enabled': false,
                'legend_enabled': false
            },
            'xAxis': {
                'title_enabled': true,
                'title_text': null,
                'title_text_default_READONLY': 'Values', //copied into title_text when re-enabling title from disabled state; user will be expected to change title_text
                'title_color': "#707070",
                'title_font_size': 12, //in pixels
                'min': null,
                'max': null,
                'gridLine_enabled': false,
                'gridLineColor': '#999999',
                'gridLineWidth': 0,
                'gridLineWidth_default_READONLY': 1, //copied into gridLineWidth when re-enabling grid lines from disabled state
                'tickInterval': null,
                'labels_enabled': true,
                'labels_step': null,
                'labels_staggerLines': null
            },
            'yAxis': {
                'title_enabled': true,
                'title_text': null,
                'title_text_default_READONLY': 'Values', //copied into title_text when re-enabling title from disabled state; user will be expected to change title_text
                'title_color': "#707070",
                'title_font_size': 12, //in pixels
                'min': null,
                'max': null,
                'gridLine_enabled': true,
                'gridLineColor': '#999999',
                'gridLineWidth': 1,
                'gridLineWidth_default_READONLY': 1, //copied into gridLineWidth when re-enabling grid lines from disabled state
                'tickInterval': null,
                'labels_enabled': true,
                'labels_step': null
            },
            'line': {
    
    Cam Saul's avatar
    Cam Saul committed
                'colors': DEFAULT_COLOR_HARMONY,
                'lineWidth': 2,
                'step': false,
                'marker_enabled': true,
                'marker_fillColor': '#528ec5',
                'marker_lineColor': '#FFFFFF',
                'marker_radius': 2,
                'xAxis_column': null,
                'yAxis_columns': []
            },
            'area': {
    
    Cam Saul's avatar
    Cam Saul committed
                'fillOpacity': 0.75
            },
            'pie': {
                'legend_enabled': true,
                'dataLabels_enabled': false,
                'dataLabels_color': '#777',
                'connectorColor': '#999',
                'colors': DEFAULT_COLOR_HARMONY
            },
            'bar': {
                'colors': DEFAULT_COLOR_HARMONY,
    
    Cam Saul's avatar
    Cam Saul committed
            },
            'map': {
                'latitude_source_table_field_id': null,
                'longitude_source_table_field_id': null,
                'latitude_dataset_col_index': null,
                'longitude_dataset_col_index': null,
                'zoom': 10,
                'center_latitude': 37.7577, //defaults to SF ;-)
                'center_longitude': -122.4376
            }
        };
    
        var groupsForVisualizations = {
            'scalar': ['global'],
            'table': ['global', 'columns'],
            'pie': ['global', 'chart', 'pie'],
            'bar': ['global', 'columns', 'chart', 'xAxis', 'yAxis', 'bar'],
            'line': ['global', 'columns', 'chart', 'xAxis', 'yAxis', 'line'],
            'area': ['global', 'columns', 'chart', 'xAxis', 'yAxis', 'line', 'area'],
            'timeseries': ['global', 'columns', 'chart', 'xAxis', 'yAxis', 'line'],
            'country': ['global', 'columns', 'chart', 'map'],
            'state': ['global', 'columns', 'chart', 'map'],
            'pin_map': ['global', 'columns', 'chart', 'map']
        };
    
    
        this.getDefaultColor = function() {
            return DEFAULT_COLOR;
        };
    
    
    Cam Saul's avatar
    Cam Saul committed
        this.getDefaultColorHarmony = function() {
            return DEFAULT_COLOR_HARMONY;
        };
    
        this.getSettingsForGroup = function(dbSettings, groupName) {
            if (typeof dbSettings != "object") {
                dbSettings = {};
            }
    
            if (typeof settings[groupName] == "undefined") {
                return dbSettings;
            }
    
            if (typeof dbSettings[groupName] == "undefined") {
                dbSettings[groupName] = {};
            }
            //make a deep copy of default settings, otherwise default settings that are objects
            //will not be recognized as 'dirty' after changing the value in the UI, because
            //_.defaults make a shallow copy, so objects / arrays are copied by reference,
            //so changing the settings in the UI would change the default settings.
            var newSettings = _.defaults(dbSettings[groupName], angular.copy(settings[groupName]));
    
            return newSettings;
        };
    
        this.getSettingsForGroups = function(dbSettings, groups) {
            var newSettings = {};
            for (var i = 0; i < groups.length; i++) {
                var groupName = groups[i];
                newSettings[groupName] = this.getSettingsForGroup(dbSettings, groupName);
            }
            return newSettings;
        };
    
        this.getSettingsGroupsForVisualization = function(visualization) {
            var groups = ['global'];
            if (typeof groupsForVisualizations[visualization] != "undefined") {
                groups = groupsForVisualizations[visualization];
            }
            return groups;
        };
    
        this.getSettingsForVisualization = function(dbSettings, visualization) {
    
            var settings = angular.copy(dbSettings);
            var groups = _.union(_.keys(settings), this.getSettingsGroupsForVisualization(visualization));
            return this.getSettingsForGroups(settings, groups);
    
    Cam Saul's avatar
    Cam Saul committed
        };
    
        //Clean visualization settings to only keep the settings that are "dirty".
        //This is determined by comparing the state of the current settings model to the
        //defaults provided by this service.
        this.cleanUserSettings = function(userSettings, visualization) {
            var defaultSettings = {};
            var cleanSettings = {};
            var groups = _.union(_.keys(userSettings), this.getSettingsGroupsForVisualization(visualization));
            for (var i = 0; i < groups.length; i++) {
                var groupName = groups[i];
                defaultSettings[groupName] = settings[groupName];
            }
    
            _.each(userSettings, function(settings, category) {
                var truncatedSettings = _.omit(userSettings[category], function(value, key) {
    
                    if ((typeof defaultSettings[category] == "undefined") || (typeof defaultSettings[category][key] == "undefined")) {
                        return false;
                    }
                    return _.isEqual(defaultSettings[category][key], value);
    
                });
                if (_.keys(truncatedSettings).length > 0) {
                    cleanSettings[category] = truncatedSettings;
                }
            });
    
            return cleanSettings;
        };
    
        this.getDefaultSettingsForVisualization = function(visualization) {
            var groups = this.getSettingsGroupsForVisualization(visualization);
            var defaults = {};
            for (var i = 0; i < groups.length; i++) {
                var groupName = groups[i];
                if (typeof settings[groupName] != "undefined") {
                    defaults[groupName] = settings[groupName];
                } else {
                    console.log("WARN: no settings for " + groupName);
                }
            }
    
            return defaults;
        };
    
    
        this.setLatitudeAndLongitude = function(settings, columnDefs) {
            // latitude
            var latitudeColumn,
                latitudeColumnIndex;
            columnDefs.forEach(function(col, index) {
                if (col.special_type &&
                        col.special_type === "latitude" &&
                        latitudeColumn === undefined) {
                    latitudeColumn = col;
                    latitudeColumnIndex = index;
                }
            });
    
            // longitude
            var longitudeColumn,
                longitudeColumnIndex;
            columnDefs.forEach(function(col, index) {
                if (col.special_type &&
                        col.special_type === "longitude" &&
                        longitudeColumn === undefined) {
                    longitudeColumn = col;
                    longitudeColumnIndex = index;
                }
            });
    
            if (latitudeColumn && longitudeColumn) {
                var settingsWithLatAndLon = angular.copy(settings);
    
                settingsWithLatAndLon.map.latitude_source_table_field_id = latitudeColumn.id;
                settingsWithLatAndLon.map.latitude_dataset_col_index = latitudeColumnIndex;
                settingsWithLatAndLon.map.longitude_source_table_field_id = longitudeColumn.id;
                settingsWithLatAndLon.map.longitude_dataset_col_index = longitudeColumnIndex;
    
                return settingsWithLatAndLon;
            } else {
                return settings;
            }
        };