From 2322a76687cbaade9c2265224ec0202784b5965a Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 24 Jan 2017 14:03:17 -0800 Subject: [PATCH] Simplifying the viz interface (#2005) --- superset/__init__.py | 21 + .../javascripts/components/FaveStar.jsx | 2 +- .../explorev2/actions/exploreActions.js | 2 +- .../explorev2/components/ChartContainer.jsx | 4 +- .../javascripts/explorev2/exploreUtils.js | 6 +- superset/assets/javascripts/syncBackend.js | 21 + superset/assets/js_build.sh | 1 + superset/assets/package.json | 3 +- superset/assets/visualizations/big_number.js | 1 - superset/assets/webpack.config.js | 11 +- superset/forms.py | 1165 ----------------- superset/models.py | 43 +- superset/templates/superset/explore.html | 307 ----- superset/views.py | 84 +- superset/viz.py | 619 +-------- 15 files changed, 126 insertions(+), 2164 deletions(-) create mode 100644 superset/assets/javascripts/syncBackend.js delete mode 100755 superset/forms.py delete mode 100644 superset/templates/superset/explore.html diff --git a/superset/__init__.py b/superset/__init__.py index 8ab8ded5e80db..9c9bf136c1cd5 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -6,6 +6,7 @@ import logging import os +import json from logging.handlers import TimedRotatingFileHandler from flask import Flask, redirect @@ -21,6 +22,26 @@ APP_DIR = os.path.dirname(__file__) CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config') +with open(APP_DIR + '/assets/dist/backendSync.json', 'r') as f: + frontend_config = json.load(f) +def cast_form_data(form_data): + d = {} + fields = frontend_config.get('fields', {}) + print('0982--' * 10) + print(fields) + for k, v in form_data.items(): + print([k, v]) + field_config = fields.get(k, {}) + ft = field_config.get('type') + if ft == 'CheckboxField': + v = True if v == 'true' else False + elif ft == 'TextField' and field_config.get('isInt'): + v = int(v) + elif ft == 'TextField' and field_config.get('isFloat'): + v = float(v) + d[k] = v + return d + app = Flask(__name__) app.config.from_object(CONFIG_MODULE) conf = app.config diff --git a/superset/assets/javascripts/components/FaveStar.jsx b/superset/assets/javascripts/components/FaveStar.jsx index 4e6afa2883f87..ce19fcb945444 100644 --- a/superset/assets/javascripts/components/FaveStar.jsx +++ b/superset/assets/javascripts/components/FaveStar.jsx @@ -3,7 +3,7 @@ import cx from 'classnames'; import TooltipWrapper from './TooltipWrapper'; const propTypes = { - sliceId: PropTypes.string.isRequired, + sliceId: PropTypes.number.isRequired, actions: PropTypes.object.isRequired, isStarred: PropTypes.bool.isRequired, }; diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js index 29ea8e40dfaa2..275f8fcb56a26 100644 --- a/superset/assets/javascripts/explorev2/actions/exploreActions.js +++ b/superset/assets/javascripts/explorev2/actions/exploreActions.js @@ -182,7 +182,7 @@ export function runQuery(formData, datasourceType) { $.getJSON(url, function (queryResponse) { dispatch(chartUpdateSucceeded(queryResponse)); }).fail(function (err) { - dispatch(chartUpdateFailed(err)); + dispatch(chartUpdateFailed(err.responseJSON)); }); }; } diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx index a025d50c2d9b9..7a4191a90f602 100644 --- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx +++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx @@ -18,7 +18,7 @@ const CHART_STATUS_MAP = { const propTypes = { actions: PropTypes.object.isRequired, can_download: PropTypes.bool.isRequired, - slice_id: PropTypes.string.isRequired, + slice_id: PropTypes.number.isRequired, slice_name: PropTypes.string.isRequired, viz_type: PropTypes.string.isRequired, height: PropTypes.string.isRequired, @@ -43,9 +43,9 @@ class ChartContainer extends React.PureComponent { renderViz() { const mockSlice = this.getMockedSliceObject(); + this.setState({ mockSlice }); try { visMap[this.props.viz_type](mockSlice, this.props.queryResponse); - this.setState({ mockSlice }); } catch (e) { this.props.actions.chartRenderingFailed(e); } diff --git a/superset/assets/javascripts/explorev2/exploreUtils.js b/superset/assets/javascripts/explorev2/exploreUtils.js index bd0457a6be0c4..a341bebecaf57 100644 --- a/superset/assets/javascripts/explorev2/exploreUtils.js +++ b/superset/assets/javascripts/explorev2/exploreUtils.js @@ -1,5 +1,4 @@ /* eslint camelcase: 0 */ -const $ = require('jquery'); function formatFilters(filters) { // outputs an object of url params of filters // prefix can be 'flt' or 'having' @@ -38,9 +37,8 @@ export function getParamObject(form_data, datasource_type, saveNewSlice) { } export function getExploreUrl(form_data, datasource_type, endpoint = 'base') { - const data = getParamObject(form_data, datasource_type); - const params = `${datasource_type}/` + - `${form_data.datasource}/?${$.param(data, true)}`; + let params = `${datasource_type}/${form_data.datasource}/`; + params += '?form_data=' + encodeURIComponent(JSON.stringify(form_data)); switch (endpoint) { case 'base': return `/superset/explore/${params}`; diff --git a/superset/assets/javascripts/syncBackend.js b/superset/assets/javascripts/syncBackend.js new file mode 100644 index 0000000000000..8ae75eb63487f --- /dev/null +++ b/superset/assets/javascripts/syncBackend.js @@ -0,0 +1,21 @@ +import fs from 'fs'; +import path from 'path'; +import { fields } from './explorev2/stores/fields'; + +function exportFile(fileLocation, content) { + fs.writeFile(fileLocation, content, function(err) { + if (err) { + console.log(`File ${fileLocation} was not saved... :(`); + } + console.log(`File ${fileLocation} was saved!`); + }); +} + +function main() { + const APP_DIR = path.resolve(__dirname, './'); + const blob = { + fields, + }; + exportFile(APP_DIR + '/../dist/backendSync.json', JSON.stringify(blob, null, 2)); +} +main(); diff --git a/superset/assets/js_build.sh b/superset/assets/js_build.sh index ec1fee68a973a..3231d915483c5 100755 --- a/superset/assets/js_build.sh +++ b/superset/assets/js_build.sh @@ -4,6 +4,7 @@ cd "$(dirname "$0")" npm --version node --version npm install +npm run sync-backend npm run lint npm run test npm run build diff --git a/superset/assets/package.json b/superset/assets/package.json index c3484559b8759..ffb084a3fbbd7 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -13,7 +13,8 @@ "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool inline-source-map", "prod": "NODE_ENV=production node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js -p --colors --progress", "build": "NODE_ENV=production webpack --colors --progress", - "lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx ." + "lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx .", + "sync-backend": "babel-node --presets es2015 javascripts/syncBackend.js" }, "repository": { "type": "git", diff --git a/superset/assets/visualizations/big_number.js b/superset/assets/visualizations/big_number.js index 3d801a4dc8f32..28bd930f0524e 100644 --- a/superset/assets/visualizations/big_number.js +++ b/superset/assets/visualizations/big_number.js @@ -7,7 +7,6 @@ function bigNumberVis(slice, payload) { const div = d3.select(slice.selector); // Define the percentage bounds that define color from red to green div.html(''); // reset - const fd = payload.form_data; const json = payload.data; diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js index 128840cfb2e45..bfadccbd3e90b 100644 --- a/superset/assets/webpack.config.js +++ b/superset/assets/webpack.config.js @@ -14,13 +14,12 @@ const config = { entry: { 'css-theme': APP_DIR + '/javascripts/css-theme.js', common: APP_DIR + '/javascripts/common.js', - dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'], - explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/explore.jsx'], + //dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'], explorev2: ['babel-polyfill', APP_DIR + '/javascripts/explorev2/index.jsx'], - sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'], - standalone: ['babel-polyfill', APP_DIR + '/javascripts/standalone.js'], - welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'], - profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'], + //sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'], + //standalone: ['babel-polyfill', APP_DIR + '/javascripts/standalone.js'], + //welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'], + //profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'], }, output: { path: BUILD_DIR, diff --git a/superset/forms.py b/superset/forms.py deleted file mode 100755 index 63e1f1084257c..0000000000000 --- a/superset/forms.py +++ /dev/null @@ -1,1165 +0,0 @@ -"""Contains the logic to create cohesive forms on the explore view""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from collections import OrderedDict -from copy import copy -import json -import math - -from flask_babel import lazy_gettext as _ -from wtforms import ( - Form, SelectMultipleField, SelectField, TextField, TextAreaField, - BooleanField, IntegerField, HiddenField, DecimalField) -from wtforms import validators, widgets - -from superset import app - -config = app.config - -TIMESTAMP_CHOICES = [ - ('smart_date', 'Adaptive formatting'), - ("%m/%d/%Y", '"%m/%d/%Y" | 01/14/2019'), - ("%Y-%m-%d", '"%Y-%m-%d" | 2019-01-14'), - ("%Y-%m-%d %H:%M:%S", - '"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'), - ("%H:%M:%S", '"%H:%M:%S" | 01:32:10'), -] -AXIS_FORMAT_CHOICES = [ - ('.3s', '".3s" | 12.3k'), - ('.3%', '".3%" | 1234543.210%'), - ('.4r', '".4r" | 12350'), - ('.3f', '".3f" | 12345.432'), - ('+,', '"+," | +12,345.4321'), - ('$,.2f', '"$,.2f" | $12,345.43'), -] -D3_FORMAT_DOCS = _( - "D3 format syntax " - "https://github.com/d3/d3-format") - - -class BetterBooleanField(BooleanField): - - """Fixes the html checkbox to distinguish absent from unchecked - - (which doesn't distinguish False from NULL/missing ) - If value is unchecked, this hidden fills in False value - """ - - def __call__(self, **kwargs): - html = super(BetterBooleanField, self).__call__(**kwargs) - html += u''.format(self.name) - return widgets.HTMLString(html) - - -class SelectMultipleSortableField(SelectMultipleField): - - """Works along with select2sortable to preserves the sort order""" - - def iter_choices(self): - d = OrderedDict() - for value, label in self.choices: - selected = self.data is not None and self.coerce(value) in self.data - d[value] = (value, label, selected) - if self.data: - for value in self.data: - if value and value in d: - yield d.pop(value) - while d: - yield d.popitem(last=False)[1] - - -class FreeFormSelect(widgets.Select): - - """A WTF widget that allows for free form entry""" - - def __call__(self, field, **kwargs): - kwargs.setdefault('id', field.id) - if self.multiple: - kwargs['multiple'] = True - html = ['') - return widgets.HTMLString(''.join(html)) - - -class FreeFormSelectField(SelectField): - - """A WTF SelectField that allows for free form input""" - - widget = FreeFormSelect() - - def pre_validate(self, form): - return - - -class OmgWtForm(Form): - - """Supersetification of the WTForm Form object""" - - fieldsets = {} - css_classes = dict() - - def get_field(self, fieldname): - return getattr(self, fieldname) - - def field_css_classes(self, fieldname): - if fieldname in self.css_classes: - return " ".join(self.css_classes[fieldname]) - return "" - - -class FormFactory(object): - - """Used to create the forms in the explore view dynamically""" - - series_limits = [0, 5, 10, 25, 50, 100, 500] - fieltype_class = { - SelectField: 'select2', - SelectMultipleField: 'select2', - FreeFormSelectField: 'select2_freeform', - SelectMultipleSortableField: 'select2Sortable', - } - - def __init__(self, viz): - self.viz = viz - from superset.viz import viz_types - viz = self.viz - datasource = viz.datasource - if not datasource.metrics_combo: - raise Exception("Please define at least one metric for your table") - default_metric = datasource.metrics_combo[0][0] - - gb_cols = datasource.groupby_column_names - default_groupby = gb_cols[0] if gb_cols else None - group_by_choices = self.choicify(gb_cols) - order_by_choices = [] - for s in sorted(datasource.column_names): - order_by_choices.append((json.dumps([s, True]), s + ' [asc]')) - order_by_choices.append((json.dumps([s, False]), s + ' [desc]')) - # Pool of all the fields that can be used in Superset - field_data = { - 'viz_type': (SelectField, { - "label": _("Viz"), - "default": 'table', - "choices": [(k, v.verbose_name) for k, v in viz_types.items()], - "description": _("The type of visualization to display") - }), - 'metrics': (SelectMultipleSortableField, { - "label": _("Metrics"), - "choices": datasource.metrics_combo, - "default": [default_metric], - "description": _("One or many metrics to display") - }), - 'order_by_cols': (SelectMultipleSortableField, { - "label": _("Ordering"), - "choices": order_by_choices, - "description": _("One or many metrics to display") - }), - 'metric': (SelectField, { - "label": _("Metric"), - "choices": datasource.metrics_combo, - "default": default_metric, - "description": _("Choose the metric") - }), - 'metric_2': (SelectField, { - "label": _("Right Axis Metric"), - "choices": datasource.metrics_combo, - "default": default_metric, - "description": _("Choose the metric for second y axis") - }), - 'stacked_style': (SelectField, { - "label": _("Chart Style"), - "choices": ( - ('stack', _('stack')), - ('stream', _('stream')), - ('expand', _('expand')), - ), - "default": 'stack', - "description": "" - }), - 'linear_color_scheme': (SelectField, { - "label": _("Color Scheme"), - "choices": ( - ('fire', _('fire')), - ('blue_white_yellow', _('blue_white_yellow')), - ('white_black', _('white_black')), - ('black_white', _('black_white')), - ), - "default": 'blue_white_yellow', - "description": "" - }), - 'normalize_across': (SelectField, { - "label": _("Normalize Across"), - "choices": ( - ('heatmap', _('heatmap')), - ('x', _('x')), - ('y', _('y')), - ), - "default": 'heatmap', - "description": _( - "Color will be rendered based on a ratio " - "of the cell against the sum of across this " - "criteria") - }), - 'horizon_color_scale': (SelectField, { - "label": _("Color Scale"), - "choices": ( - ('series', _('series')), - ('overall', _('overall')), - ('change', _('change')), - ), - "default": 'series', - "description": _("Defines how the color are attributed.") - }), - 'canvas_image_rendering': (SelectField, { - "label": _("Rendering"), - "choices": ( - ('pixelated', _('pixelated (Sharp)')), - ('auto', _('auto (Smooth)')), - ), - "default": 'pixelated', - "description": _( - "image-rendering CSS attribute of the canvas object that " - "defines how the browser scales up the image") - }), - 'xscale_interval': (SelectField, { - "label": _("XScale Interval"), - "choices": self.choicify(range(1, 50)), - "default": '1', - "description": _( - "Number of step to take between ticks when " - "printing the x scale") - }), - 'yscale_interval': (SelectField, { - "label": _("YScale Interval"), - "choices": self.choicify(range(1, 50)), - "default": '1', - "description": _( - "Number of step to take between ticks when " - "printing the y scale") - }), - 'bar_stacked': (BetterBooleanField, { - "label": _("Stacked Bars"), - "default": False, - "description": "" - }), - 'show_markers': (BetterBooleanField, { - "label": _("Show Markers"), - "default": False, - "description": ( - "Show data points as circle markers on top of the lines " - "in the chart") - }), - 'show_bar_value': (BetterBooleanField, { - "label": _("Bar Values"), - "default": False, - "description": "Show the value on top of the bars or not" - }), - 'order_bars': (BetterBooleanField, { - "label": _("Sort Bars"), - "default": False, - "description": _("Sort bars by x labels."), - }), - 'show_controls': (BetterBooleanField, { - "label": _("Extra Controls"), - "default": False, - "description": _( - "Whether to show extra controls or not. Extra controls " - "include things like making mulitBar charts stacked " - "or side by side.") - }), - 'reduce_x_ticks': (BetterBooleanField, { - "label": _("Reduce X ticks"), - "default": False, - "description": _( - "Reduces the number of X axis ticks to be rendered. " - "If true, the x axis wont overflow and labels may be " - "missing. If false, a minimum width will be applied " - "to columns and the width may overflow into an " - "horizontal scroll."), - }), - 'include_series': (BetterBooleanField, { - "label": _("Include Series"), - "default": False, - "description": _("Include series name as an axis") - }), - 'secondary_metric': (SelectField, { - "label": _("Color Metric"), - "choices": datasource.metrics_combo, - "default": default_metric, - "description": _("A metric to use for color") - }), - 'country_fieldtype': (SelectField, { - "label": _("Country Field Type"), - "default": 'cca2', - "choices": ( - ('name', _('Full name')), - ('cioc', _('code International Olympic Committee (cioc)')), - ('cca2', _('code ISO 3166-1 alpha-2 (cca2)')), - ('cca3', _('code ISO 3166-1 alpha-3 (cca3)')), - ), - "description": _( - "The country code standard that Superset should expect " - "to find in the [country] column") - }), - 'groupby': (SelectMultipleSortableField, { - "label": _("Group by"), - "choices": self.choicify(datasource.groupby_column_names), - "description": _("One or many fields to group by") - }), - 'columns': (SelectMultipleSortableField, { - "label": _("Columns"), - "choices": self.choicify(datasource.groupby_column_names), - "description": _("One or many fields to pivot as columns") - }), - 'all_columns': (SelectMultipleSortableField, { - "label": _("Columns"), - "choices": self.choicify(datasource.column_names), - "description": _("Columns to display") - }), - 'all_columns_x': (SelectField, { - "label": _("X"), - "choices": self.choicify(datasource.column_names), - "default": datasource.column_names[0], - "description": _("Columns to display") - }), - 'all_columns_y': (SelectField, { - "label": _("Y"), - "choices": self.choicify(datasource.column_names), - "default": datasource.column_names[0], - "description": _("Columns to display") - }), - 'druid_time_origin': (FreeFormSelectField, { - "label": _("Origin"), - "choices": ( - ('', _('default')), - ('now', _('now')), - ), - "default": '', - "description": _( - "Defines the origin where time buckets start, " - "accepts natural dates as in 'now', 'sunday' or '1970-01-01'") - }), - 'bottom_margin': (FreeFormSelectField, { - "label": _("Bottom Margin"), - "choices": self.choicify(['auto', 50, 75, 100, 125, 150, 200]), - "default": 'auto', - "description": _( - "Bottom margin, in pixels, allowing for more room for " - "axis labels"), - }), - 'page_length': (FreeFormSelectField, { - "label": _("Page Length"), - "default": 0, - "choices": self.choicify([0, 10, 25, 50, 100, 250, 500]), - "description": _( - "Number of rows per page, 0 means no pagination") - }), - 'granularity': (FreeFormSelectField, { - "label": _("Time Granularity"), - "default": "one day", - "choices": ( - ('all', _('all')), - ('5 seconds', _('5 seconds')), - ('30 seconds', _('30 seconds')), - ('1 minute', _('1 minute')), - ('5 minutes', _('5 minutes')), - ('1 hour', _('1 hour')), - ('6 hour', _('6 hour')), - ('1 day', _('1 day')), - ('7 days', _('7 days')), - ('week', _('week')), - ('week_starting_sunday', _('week_starting_sunday')), - ('week_ending_saturday', _('week_ending_saturday')), - ('month', _('month')), - ), - "description": _( - "The time granularity for the visualization. Note that you " - "can type and use simple natural language as in '10 seconds', " - "'1 day' or '56 weeks'") - }), - 'domain_granularity': (SelectField, { - "label": _("Domain"), - "default": "month", - "choices": ( - ('hour', _('hour')), - ('day', _('day')), - ('week', _('week')), - ('month', _('month')), - ('year', _('year')), - ), - "description": _( - "The time unit used for the grouping of blocks") - }), - 'subdomain_granularity': (SelectField, { - "label": _("Subdomain"), - "default": "day", - "choices": ( - ('min', _('min')), - ('hour', _('hour')), - ('day', _('day')), - ('week', _('week')), - ('month', _('month')), - ), - "description": _( - "The time unit for each block. Should be a smaller unit than " - "domain_granularity. Should be larger or equal to Time Grain") - }), - 'link_length': (FreeFormSelectField, { - "label": _("Link Length"), - "default": "200", - "choices": self.choicify([ - '10', - '25', - '50', - '75', - '100', - '150', - '200', - '250', - ]), - "description": _("Link length in the force layout") - }), - 'charge': (FreeFormSelectField, { - "label": _("Charge"), - "default": "-500", - "choices": self.choicify([ - '-50', - '-75', - '-100', - '-150', - '-200', - '-250', - '-500', - '-1000', - '-2500', - '-5000', - ]), - "description": _("Charge in the force layout") - }), - 'granularity_sqla': (SelectField, { - "label": _("Time Column"), - "default": datasource.main_dttm_col or datasource.any_dttm_col, - "choices": self.choicify(datasource.dttm_cols), - "description": _( - "The time column for the visualization. Note that you " - "can define arbitrary expression that return a DATETIME " - "column in the table editor. Also note that the " - "filter below is applied against this column or " - "expression") - }), - 'resample_rule': (FreeFormSelectField, { - "label": _("Resample Rule"), - "default": '', - "choices": ( - ('1T', _('1T')), - ('1H', _('1H')), - ('1D', _('1D')), - ('7D', _('7D')), - ('1M', _('1M')), - ('1AS', _('1AS')), - ), - "description": _("Pandas resample rule") - }), - 'resample_how': (FreeFormSelectField, { - "label": _("Resample How"), - "default": '', - "choices": ( - ('', ''), - ('mean', _('mean')), - ('sum', _('sum')), - ('median', _('median')), - ), - "description": _("Pandas resample how") - }), - 'resample_fillmethod': (FreeFormSelectField, { - "label": _("Resample Fill Method"), - "default": '', - "choices": ( - ('', ''), - ('ffill', _('ffill')), - ('bfill', _('bfill')), - ), - "description": _("Pandas resample fill method") - }), - 'since': (FreeFormSelectField, { - "label": _("Since"), - "default": "7 days ago", - "choices": ( - ('1 hour ago', _('1 hour ago')), - ('12 hours ago', _('12 hours ago')), - ('1 day ago', _('1 day ago')), - ('7 days ago', _('7 days ago')), - ('28 days ago', _('28 days ago')), - ('90 days ago', _('90 days ago')), - ('1 year ago', _('1 year ago')), - ), - "description": _( - "Timestamp from filter. This supports free form typing and " - "natural language as in '1 day ago', '28 days' or '3 years'") - }), - 'until': (FreeFormSelectField, { - "label": _("Until"), - "default": "now", - "choices": ( - ('now', _('now')), - ('1 day ago', _('1 day ago')), - ('7 days ago', _('7 days ago')), - ('28 days ago', _('28 days ago')), - ('90 days ago', _('90 days ago')), - ('1 year ago', _('1 year ago')), - ) - }), - 'max_bubble_size': (FreeFormSelectField, { - "label": _("Max Bubble Size"), - "default": "25", - "choices": self.choicify([ - '5', - '10', - '15', - '25', - '50', - '75', - '100', - ]) - }), - 'whisker_options': (FreeFormSelectField, { - "label": _("Whisker/outlier options"), - "default": "Tukey", - "description": _( - "Determines how whiskers and outliers are calculated."), - "choices": ( - ('Tukey', _('Tukey')), - ('Min/max (no outliers)', _('Min/max (no outliers)')), - ('2/98 percentiles', _('2/98 percentiles')), - ('9/91 percentiles', _('9/91 percentiles')), - ) - }), - 'treemap_ratio': (DecimalField, { - "label": _("Ratio"), - "default": 0.5 * (1 + math.sqrt(5)), # d3 default, golden ratio - "description": _('Target aspect ratio for treemap tiles.'), - }), - 'number_format': (FreeFormSelectField, { - "label": _("Number format"), - "default": '.3s', - "choices": [ - ('.3s', '".3s" | 12.3k'), - ('.3%', '".3%" | 1234543.210%'), - ('.4r', '".4r" | 12350'), - ('.3f', '".3f" | 12345.432'), - ('+,', '"+," | +12,345.4321'), - ('$,.2f', '"$,.2f" | $12,345.43'), - ], - "description": D3_FORMAT_DOCS, - }), - 'row_limit': (FreeFormSelectField, { - "label": _('Row limit'), - "default": config.get("VIZ_ROW_LIMIT"), - "choices": self.choicify( - [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000]) - }), - 'limit': (FreeFormSelectField, { - "label": _('Series limit'), - "choices": self.choicify(self.series_limits), - "default": 50, - "description": _( - "Limits the number of time series that get displayed") - }), - 'timeseries_limit_metric': (SelectField, { - "label": _("Sort By"), - "choices": [('', '')] + datasource.metrics_combo, - "default": "", - "description": _("Metric used to define the top series") - }), - 'rolling_type': (SelectField, { - "label": _("Rolling"), - "default": 'None', - "choices": [(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']], - "description": _( - "Defines a rolling window function to apply, works along " - "with the [Periods] text box") - }), - 'rolling_periods': (IntegerField, { - "label": _("Periods"), - "validators": [validators.optional()], - "description": _( - "Defines the size of the rolling window function, " - "relative to the time granularity selected") - }), - 'series': (SelectField, { - "label": _("Series"), - "choices": group_by_choices, - "default": default_groupby, - "description": _( - "Defines the grouping of entities. " - "Each series is shown as a specific color on the chart and " - "has a legend toggle") - }), - 'entity': (SelectField, { - "label": _("Entity"), - "choices": group_by_choices, - "default": default_groupby, - "description": _("This define the element to be plotted on the chart") - }), - 'x': (SelectField, { - "label": _("X Axis"), - "choices": datasource.metrics_combo, - "default": default_metric, - "description": _("Metric assigned to the [X] axis") - }), - 'y': (SelectField, { - "label": _("Y Axis"), - "choices": datasource.metrics_combo, - "default": default_metric, - "description": _("Metric assigned to the [Y] axis") - }), - 'size': (SelectField, { - "label": _('Bubble Size'), - "default": default_metric, - "choices": datasource.metrics_combo - }), - 'url': (TextField, { - "label": _("URL"), - "description": _( - "The URL, this field is templated, so you can integrate " - "{{ width }} and/or {{ height }} in your URL string." - ), - "default": 'https: //www.youtube.com/embed/JkI5rg_VcQ4', - }), - 'x_axis_label': (TextField, { - "label": _("X Axis Label"), - "default": '', - }), - 'y_axis_label': (TextField, { - "label": _("Y Axis Label"), - "default": '', - }), - 'where': (TextField, { - "label": _("Custom WHERE clause"), - "default": '', - "description": _( - "The text in this box gets included in your query's WHERE " - "clause, as an AND to other criteria. You can include " - "complex expression, parenthesis and anything else " - "supported by the backend it is directed towards.") - }), - 'having': (TextField, { - "label": _("Custom HAVING clause"), - "default": '', - "description": _( - "The text in this box gets included in your query's HAVING" - " clause, as an AND to other criteria. You can include " - "complex expression, parenthesis and anything else " - "supported by the backend it is directed towards.") - }), - 'compare_lag': (TextField, { - "label": _("Comparison Period Lag"), - "description": _( - "Based on granularity, number of time periods to " - "compare against") - }), - 'compare_suffix': (TextField, { - "label": _("Comparison suffix"), - "description": _("Suffix to apply after the percentage display") - }), - 'table_timestamp_format': (FreeFormSelectField, { - "label": _("Table Timestamp Format"), - "default": 'smart_date', - "choices": TIMESTAMP_CHOICES, - "description": _("Timestamp Format") - }), - 'series_height': (FreeFormSelectField, { - "label": _("Series Height"), - "default": 25, - "choices": self.choicify([10, 25, 40, 50, 75, 100, 150, 200]), - "description": _("Pixel height of each series") - }), - 'x_axis_format': (FreeFormSelectField, { - "label": _("X axis format"), - "default": 'smart_date', - "choices": TIMESTAMP_CHOICES, - "description": D3_FORMAT_DOCS, - }), - 'y_axis_format': (FreeFormSelectField, { - "label": _("Y axis format"), - "default": '.3s', - "choices": AXIS_FORMAT_CHOICES, - "description": D3_FORMAT_DOCS, - }), - 'y_axis_2_format': (FreeFormSelectField, { - "label": _("Right axis format"), - "default": '.3s', - "choices": AXIS_FORMAT_CHOICES, - "description": D3_FORMAT_DOCS, - }), - 'markup_type': (SelectField, { - "label": _("Markup Type"), - "choices": ( - ('markdown', _('markdown')), - ('html', _('html')) - ), - "default": "markdown", - "description": _("Pick your favorite markup language") - }), - 'rotation': (SelectField, { - "label": _("Rotation"), - "choices": ( - ('random', _('random')), - ('flat', _('flat')), - ('square', _('square')), - ), - "default": "random", - "description": _("Rotation to apply to words in the cloud") - }), - 'line_interpolation': (SelectField, { - "label": _("Line Style"), - "choices": ( - ('linear', _('linear')), - ('basis', _('basis')), - ('cardinal', _('cardinal')), - ('monotone', _('monotone')), - ('step-before', _('step-before')), - ('step-after', _('step-after')), - ), - "default": 'linear', - "description": _("Line interpolation as defined by d3.js") - }), - 'pie_label_type': (SelectField, { - "label": _("Label Type"), - "default": 'key', - "choices": ( - ('key', _("Category Name")), - ('value', _("Value")), - ('percent', _("Percentage")), - ), - "description": _("What should be shown on the label?") - }), - 'code': (TextAreaField, { - "label": _("Code"), - "description": _("Put your code here"), - "default": '' - }), - 'pandas_aggfunc': (SelectField, { - "label": _("Aggregation function"), - "choices": ( - ('sum', _('sum')), - ('mean', _('mean')), - ('min', _('min')), - ('max', _('max')), - ('median', _('median')), - ('stdev', _('stdev')), - ('var', _('var')), - ), - "default": 'sum', - "description": _( - "Aggregate function to apply when pivoting and " - "computing the total rows and columns") - }), - 'size_from': (TextField, { - "label": _("Font Size From"), - "default": "20", - "description": _("Font size for the smallest value in the list") - }), - 'size_to': (TextField, { - "label": _("Font Size To"), - "default": "150", - "description": _("Font size for the biggest value in the list") - }), - 'show_brush': (BetterBooleanField, { - "label": _("Range Filter"), - "default": False, - "description": _( - "Whether to display the time range interactive selector") - }), - 'date_filter': (BetterBooleanField, { - "label": _("Date Filter"), - "default": False, - "description": _("Whether to include a time filter") - }), - 'show_datatable': (BetterBooleanField, { - "label": _("Data Table"), - "default": False, - "description": _("Whether to display the interactive data table") - }), - 'include_search': (BetterBooleanField, { - "label": _("Search Box"), - "default": False, - "description": _( - "Whether to include a client side search box") - }), - 'table_filter': (BetterBooleanField, { - "label": _("Table Filter"), - "default": False, - "description": _( - "Whether to apply filter when table cell is clicked") - }), - 'show_bubbles': (BetterBooleanField, { - "label": _("Show Bubbles"), - "default": False, - "description": _( - "Whether to display bubbles on top of countries") - }), - 'show_legend': (BetterBooleanField, { - "label": _("Legend"), - "default": True, - "description": _("Whether to display the legend (toggles)") - }), - 'x_axis_showminmax': (BetterBooleanField, { - "label": _("X bounds"), - "default": True, - "description": _( - "Whether to display the min and max values of the X axis") - }), - 'rich_tooltip': (BetterBooleanField, { - "label": _("Rich Tooltip"), - "default": True, - "description": _( - "The rich tooltip shows a list of all series for that" - " point in time") - }), - 'y_axis_zero': (BetterBooleanField, { - "label": _("Y Axis Zero"), - "default": False, - "description": _( - "Force the Y axis to start at 0 instead of the minimum " - "value") - }), - 'y_log_scale': (BetterBooleanField, { - "label": _("Y Log"), - "default": False, - "description": _("Use a log scale for the Y axis") - }), - 'x_log_scale': (BetterBooleanField, { - "label": _("X Log"), - "default": False, - "description": _("Use a log scale for the X axis") - }), - 'donut': (BetterBooleanField, { - "label": _("Donut"), - "default": False, - "description": _("Do you want a donut or a pie?") - }), - 'labels_outside': (BetterBooleanField, { - "label": _("Put labels outside"), - "default": True, - "description": _("Put the labels outside the pie?") - }), - 'contribution': (BetterBooleanField, { - "label": _("Contribution"), - "default": False, - "description": _("Compute the contribution to the total") - }), - 'num_period_compare': (IntegerField, { - "label": _("Period Ratio"), - "default": None, - "validators": [validators.optional()], - "description": _( - "[integer] Number of period to compare against, " - "this is relative to the granularity selected") - }), - 'period_ratio_type': (SelectField, { - "label": _("Period Ratio Type"), - "default": 'growth', - "choices": ( - ('factor', _('factor')), - ('growth', _('growth')), - ('value', _('value')), - ), - "description": _( - "`factor` means (new/previous), `growth` is " - "((new/previous) - 1), `value` is (new-previous)") - }), - 'time_compare': (TextField, { - "label": _("Time Shift"), - "default": "", - "description": _( - "Overlay a timeseries from a " - "relative time period. Expects relative time delta " - "in natural language (example: 24 hours, 7 days, " - "56 weeks, 365 days") - }), - 'subheader': (TextField, { - "label": _("Subheader"), - "description": _( - "Description text that shows up below your Big " - "Number") - }), - 'mapbox_label': (SelectMultipleSortableField, { - "label": "Label", - "choices": self.choicify(["count"] + datasource.column_names), - "description": _( - "'count' is COUNT(*) if a group by is used. " - "Numerical columns will be aggregated with the aggregator. " - "Non-numerical columns will be used to label points. " - "Leave empty to get a count of points in each cluster."), - }), - 'mapbox_style': (SelectField, { - "label": "Map Style", - "choices": [ - ("mapbox://styles/mapbox/streets-v9", "Streets"), - ("mapbox://styles/mapbox/dark-v9", "Dark"), - ("mapbox://styles/mapbox/light-v9", "Light"), - ("mapbox://styles/mapbox/satellite-streets-v9", "Satellite Streets"), - ("mapbox://styles/mapbox/satellite-v9", "Satellite"), - ("mapbox://styles/mapbox/outdoors-v9", "Outdoors"), - ], - "default": "mapbox://styles/mapbox/streets-v9", - "description": _("Base layer map style") - }), - 'clustering_radius': (FreeFormSelectField, { - "label": _("Clustering Radius"), - "default": "60", - "choices": self.choicify([ - '0', - '20', - '40', - '60', - '80', - '100', - '200', - '500', - '1000', - ]), - "description": _( - "The radius (in pixels) the algorithm uses to define a cluster. " - "Choose 0 to turn off clustering, but beware that a large " - "number of points (>1000) will cause lag.") - }), - 'point_radius': (SelectField, { - "label": _("Point Radius"), - "default": "Auto", - "choices": self.choicify(["Auto"] + datasource.column_names), - "description": _( - "The radius of individual points (ones that are not in a cluster). " - "Either a numerical column or 'Auto', which scales the point based " - "on the largest cluster") - }), - 'point_radius_unit': (SelectField, { - "label": _("Point Radius Unit"), - "default": "Pixels", - "choices": self.choicify([ - "Pixels", - "Miles", - "Kilometers", - ]), - "description": _("The unit of measure for the specified point radius") - }), - 'global_opacity': (DecimalField, { - "label": _("Opacity"), - "default": 1, - "description": _( - "Opacity of all clusters, points, and labels. " - "Between 0 and 1."), - }), - 'viewport_zoom': (DecimalField, { - "label": _("Zoom"), - "default": 11, - "validators": [validators.optional()], - "description": _("Zoom level of the map"), - "places": 8, - }), - 'viewport_latitude': (DecimalField, { - "label": _("Default latitude"), - "default": 37.772123, - "description": _("Latitude of default viewport"), - "places": 8, - }), - 'viewport_longitude': (DecimalField, { - "label": _("Default longitude"), - "default": -122.405293, - "description": _("Longitude of default viewport"), - "places": 8, - }), - 'render_while_dragging': (BetterBooleanField, { - "label": _("Live render"), - "default": True, - "description": _( - "Points and clusters will update as viewport " - "is being changed"), - }), - 'mapbox_color': (FreeFormSelectField, { - "label": _("RGB Color"), - "default": "rgb(0, 122, 135)", - "choices": [ - ("rgb(0, 139, 139)", "Dark Cyan"), - ("rgb(128, 0, 128)", "Purple"), - ("rgb(255, 215, 0)", "Gold"), - ("rgb(69, 69, 69)", "Dim Gray"), - ("rgb(220, 20, 60)", "Crimson"), - ("rgb(34, 139, 34)", "Forest Green"), - ], - "description": _("The color for points and clusters in RGB") - }), - 'ranges': (TextField, { - "label": _("Ranges"), - "default": "", - "description": _("Ranges to highlight with shading") - }), - 'range_labels': (TextField, { - "label": _("Range labels"), - "default": "", - "description": _("Labels for the ranges") - }), - 'markers': (TextField, { - "label": _("Markers"), - "default": "", - "description": _("List of values to mark with triangles") - }), - 'marker_labels': (TextField, { - "label": _("Marker labels"), - "default": "", - "description": _("Labels for the markers") - }), - 'marker_lines': (TextField, { - "label": _("Marker lines"), - "default": "", - "description": _("List of values to mark with lines") - }), - 'marker_line_labels': (TextField, { - "label": _("Marker line labels"), - "default": "", - "description": _("Labels for the marker lines") - }), - } - - # Override default arguments with form overrides - for field_name, override_map in viz.form_overrides.items(): - if field_name in field_data: - field_data[field_name][1].update(override_map) - - self.field_dict = { - field_name: v[0](**v[1]) - for field_name, v in field_data.items() - } - - @staticmethod - def choicify(l): - return [("{}".format(obj), "{}".format(obj)) for obj in l] - - def get_form(self): - """Returns a form object based on the viz/datasource/context""" - viz = self.viz - field_css_classes = {} - for name, obj in self.field_dict.items(): - field_css_classes[name] = ['form-control', 'input-sm'] - s = self.fieltype_class.get(obj.field_class) - if s: - field_css_classes[name] += [s] - - for field in ('show_brush', 'show_legend', 'rich_tooltip'): - field_css_classes[field] += ['input-sm'] - - class QueryForm(OmgWtForm): - - """The dynamic form object used for the explore view""" - - fieldsets = copy(viz.fieldsets) - css_classes = field_css_classes - standalone = HiddenField() - async = HiddenField() - force = HiddenField() - extra_filters = HiddenField() - json = HiddenField() - slice_id = HiddenField() - slice_name = HiddenField() - previous_viz_type = HiddenField(default=viz.viz_type) - collapsed_fieldsets = HiddenField() - viz_type = self.field_dict.get('viz_type') - - for field in viz.flat_form_fields(): - setattr(QueryForm, field, self.field_dict[field]) - - def add_to_form(attrs): - for attr in attrs: - setattr(QueryForm, attr, self.field_dict[attr]) - - filter_choices = self.choicify(['in', 'not in']) - having_op_choices = [] - filter_prefixes = ['flt'] - # datasource type specific form elements - datasource_classname = viz.datasource.__class__.__name__ - time_fields = None - if datasource_classname == 'SqlaTable': - QueryForm.fieldsets += ({ - 'label': _('SQL'), - 'fields': ['where', 'having'], - 'description': _( - "This section exposes ways to include snippets of " - "SQL in your query"), - },) - add_to_form(('where', 'having')) - grains = viz.datasource.database.grains() - - if grains: - grains_choices = [(grain.name, grain.label) for grain in grains] - time_fields = ('granularity_sqla', 'time_grain_sqla') - self.field_dict['time_grain_sqla'] = SelectField( - _('Time Grain'), - choices=grains_choices, - default="Time Column", - description=_( - "The time granularity for the visualization. This " - "applies a date transformation to alter " - "your time column and defines a new time granularity." - "The options here are defined on a per database " - "engine basis in the Superset source code")) - add_to_form(time_fields) - field_css_classes['time_grain_sqla'] = ['form-control', 'select2'] - field_css_classes['granularity_sqla'] = ['form-control', 'select2'] - else: - time_fields = 'granularity_sqla' - add_to_form((time_fields, )) - elif datasource_classname == 'DruidDatasource': - time_fields = ('granularity', 'druid_time_origin') - add_to_form(('granularity', 'druid_time_origin')) - field_css_classes['granularity'] = ['form-control', 'select2_freeform'] - field_css_classes['druid_time_origin'] = ['form-control', 'select2_freeform'] - filter_choices = self.choicify(['in', 'not in', 'regex']) - having_op_choices = self.choicify( - ['==', '!=', '>', '<', '>=', '<=']) - filter_prefixes += ['having'] - add_to_form(('since', 'until')) - - # filter_cols defaults to ''. Filters with blank col will be ignored - filter_cols = self.choicify( - ([''] + viz.datasource.filterable_column_names) or ['']) - having_cols = filter_cols + viz.datasource.metrics_combo - for field_prefix in filter_prefixes: - is_having_filter = field_prefix == 'having' - col_choices = filter_cols if not is_having_filter else having_cols - op_choices = filter_choices if not is_having_filter else \ - having_op_choices - for i in range(10): - setattr(QueryForm, field_prefix + '_col_' + str(i), - SelectField( - _('Filter 1'), - default=col_choices[0][0], - choices=col_choices)) - setattr(QueryForm, field_prefix + '_op_' + str(i), SelectField( - _('Filter 1'), - default=op_choices[0][0], - choices=op_choices)) - setattr( - QueryForm, field_prefix + '_eq_' + str(i), - TextField(_("Super"), default='')) - - if time_fields: - QueryForm.fieldsets = ({ - 'label': _('Time'), - 'fields': ( - time_fields, - ('since', 'until'), - ), - 'description': _("Time related form attributes"), - },) + tuple(QueryForm.fieldsets) - return QueryForm diff --git a/superset/models.py b/superset/models.py index ae0ba525c7844..940383bdf1ef6 100644 --- a/superset/models.py +++ b/superset/models.py @@ -13,6 +13,7 @@ import pickle import re import textwrap +import urllib from copy import deepcopy, copy from datetime import timedelta, datetime, date @@ -52,10 +53,9 @@ from sqlalchemy.sql.expression import ColumnClause, TextAsFrom from sqlalchemy_utils import EncryptedType -from werkzeug.datastructures import ImmutableMultiDict - from superset import ( - app, db, db_engine_specs, get_session, utils, sm, import_util + app, db, db_engine_specs, get_session, utils, sm, import_util, + cast_form_data, ) from superset.source_registry import SourceRegistry from superset.viz import viz_types @@ -324,19 +324,16 @@ def json_data(self): @property def slice_url(self): """Defines the url to access the slice""" - try: - slice_params = json.loads(self.params) - except Exception as e: - logging.exception(e) - slice_params = {} - slice_params['slice_id'] = self.id - slice_params['json'] = "false" - slice_params['slice_name'] = self.slice_name - from werkzeug.urls import Href - href = Href( + form_data = json.loads(self.params) + form_data = cast_form_data(form_data) + form_data['slice_id'] = self.id + form_data['slice_name'] = self.slice_name + form_data['viz_type'] = self.viz_type + print(form_data) + return ( "/superset/explore/{obj.datasource_type}/" - "{obj.datasource_id}/".format(obj=self)) - return href(slice_params) + "{obj.datasource_id}/?form_data={params}".format( + obj=self, params=urllib.quote(json.dumps(form_data)))) @property def slice_id_url(self): @@ -364,21 +361,15 @@ def get_viz(self, url_params_multidict=None): url_params_multidict or self.params. :rtype: :py:class:viz.BaseViz """ - slice_params = json.loads(self.params) # {} + slice_params = json.loads(self.params) slice_params['slice_id'] = self.id slice_params['json'] = "false" slice_params['slice_name'] = self.slice_name slice_params['viz_type'] = self.viz_type if self.viz_type else "table" - if url_params_multidict: - slice_params.update(url_params_multidict) - to_del = [k for k in slice_params if k not in url_params_multidict] - for k in to_del: - del slice_params[k] - - immutable_slice_params = ImmutableMultiDict(slice_params) - return viz_types[immutable_slice_params.get('viz_type')]( + + return viz_types[slice_params.get('viz_type')]( self.datasource, - form_data=immutable_slice_params, + form_data=slice_params, slice_=self ) @@ -1442,6 +1433,8 @@ def visit_column(element, compiler, **kw): qry.compile( engine, compile_kwargs={"literal_binds": True},), ) + print(sql) + logging.info(sql) sql = sqlparse.format(sql, reindent=True) status = QueryStatus.SUCCESS error_message = None diff --git a/superset/templates/superset/explore.html b/superset/templates/superset/explore.html deleted file mode 100644 index 8324adf4eeeef..0000000000000 --- a/superset/templates/superset/explore.html +++ /dev/null @@ -1,307 +0,0 @@ -{% extends "superset/basic.html" %} - -{% block title %} - {% if slice %} - [slice] {{ slice.slice_name }} - {% else %} - [explore] {{ viz.datasource.table_name }} - {% endif %} -{% endblock %} - -{% block body %} - {% set datasource = viz.datasource %} - {% set form = viz.form %} - - {% macro panofield(fieldname)%} -
- {% set field = form.get_field(fieldname)%} -
- {{ field.label }} - {% if field.description %} - - {% endif %} - {{ field(class_=form.field_css_classes(field.name)) }} -
-
- {% endmacro %} - -
- -{% endblock %} - -{% block tail_js %} - {{ super() }} - {% with filename="explore" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} diff --git a/superset/views.py b/superset/views.py index c9b4aea096e38..109cc8082e3d4 100755 --- a/superset/views.py +++ b/superset/views.py @@ -32,7 +32,6 @@ from sqlalchemy import create_engine from werkzeug.routing import BaseConverter -from wtforms.validators import ValidationError import superset from superset import ( @@ -331,7 +330,7 @@ def validate_json(form, field): # noqa json.loads(field.data) except Exception as e: logging.exception(e) - raise ValidationError("json isn't valid") + raise Exception("json isn't valid") def generate_download_headers(extension): @@ -1422,6 +1421,16 @@ def clean_fulfilled_requests(session): session.commit() return redirect('/accessrequestsmodelview/list/') + def get_form_data(self): + form_data = request.args.get("form_data") + if not form_data: + form_data = request.form.get("form_data") + if not form_data: + form_data = {} + print("-=*=-" * 10) + print(form_data) + return json.loads(form_data) + def get_viz( self, slice_id=None, @@ -1429,14 +1438,21 @@ def get_viz( datasource_type=None, datasource_id=None): if slice_id: - slc = db.session.query(models.Slice).filter_by(id=slice_id).one() + slc = ( + db.session.query(models.Slice) + .filter_by(id=slice_id) + .one() + ) return slc.get_viz() else: - viz_type = args.get('viz_type', 'table') + form_data=self.get_form_data() + viz_type = form_data.get('viz_type') datasource = SourceRegistry.get_datasource( datasource_type, datasource_id, db.session) viz_obj = viz.viz_types[viz_type]( - datasource, request.args if request.args else args) + datasource, + form_data=form_data, + ) return viz_obj @has_access @@ -1509,6 +1525,7 @@ def import_dashboards(self): @has_access @expose("/explore///") def explore(self, datasource_type, datasource_id): + viz_type = request.args.get("viz_type") slice_id = request.args.get('slice_id') slc = None @@ -1528,11 +1545,11 @@ def explore(self, datasource_type, datasource_id): datasource_id=datasource_id, args=request.args) except Exception as e: - flash('{}'.format(e), "alert") + flash('{}'.format(e), "danger") return redirect(error_redirect) if not viz_obj.datasource: - flash(DATASOURCE_MISSING_ERR, "alert") + flash(DATASOURCE_MISSING_ERR, "danger") return redirect(error_redirect) if not self.datasource_access(viz_obj.datasource): @@ -1573,37 +1590,28 @@ def explore(self, datasource_type, datasource_id): mimetype="application/csv") elif request.args.get("standalone") == "true": return self.render_template("superset/standalone.html", viz=viz_obj, standalone_mode=True) - elif request.args.get("V2") == "true" or is_in_explore_v2_beta: - # bootstrap data for explore V2 - bootstrap_data = { - "can_add": slice_add_perm, - "can_download": slice_download_perm, - "can_edit": slice_edit_perm, - # TODO: separate endpoint for fetching datasources - "datasources": [(d.id, d.full_name) for d in datasources], - "datasource_id": datasource_id, - "datasource_name": viz_obj.datasource.name, - "datasource_type": datasource_type, - "user_id": user_id, - "viz": json.loads(viz_obj.json_data), - "filter_select": viz_obj.datasource.filter_select_enabled - } - table_name = viz_obj.datasource.table_name \ - if datasource_type == 'table' \ - else viz_obj.datasource.datasource_name - return self.render_template( - "superset/explorev2.html", - bootstrap_data=json.dumps(bootstrap_data), - slice=slc, - table_name=table_name) - else: - return self.render_template( - "superset/explore.html", - viz=viz_obj, slice=slc, datasources=datasources, - can_add=slice_add_perm, can_edit=slice_edit_perm, - can_download=slice_download_perm, - userid=g.user.get_id() if g.user else '' - ) + # bootstrap data for explore V2 + bootstrap_data = { + "can_add": slice_add_perm, + "can_download": slice_download_perm, + "can_edit": slice_edit_perm, + # TODO: separate endpoint for fetching datasources + "datasources": [(d.id, d.full_name) for d in datasources], + "datasource_id": datasource_id, + "datasource_name": viz_obj.datasource.name, + "datasource_type": datasource_type, + "user_id": user_id, + "viz": json.loads(viz_obj.json_data), + "filter_select": viz_obj.datasource.filter_select_enabled + } + table_name = viz_obj.datasource.table_name \ + if datasource_type == 'table' \ + else viz_obj.datasource.datasource_name + return self.render_template( + "superset/explorev2.html", + bootstrap_data=json.dumps(bootstrap_data), + slice=slc, + table_name=table_name) @api @has_access_api diff --git a/superset/viz.py b/superset/viz.py index 6d7532359ad58..2e54e50bc6e54 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -29,7 +29,6 @@ from dateutil import relativedelta as rdelta from superset import app, utils, cache -from superset.forms import FormFactory from superset.utils import flasher, DTTM_ALIAS config = app.config @@ -43,13 +42,6 @@ class BaseViz(object): verbose_name = "Base Viz" credits = "" is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'metrics', 'groupby', - ) - },) - form_overrides = {} def __init__(self, datasource, form_data, slice_=None): self.orig_form_data = form_data @@ -59,101 +51,17 @@ def __init__(self, datasource, form_data, slice_=None): self.request = request self.viz_type = form_data.get("viz_type") self.slice = slice_ + self.form_data = form_data - # TODO refactor all form related logic out of here and into forms.py - ff = FormFactory(self) - form_class = ff.get_form() - defaults = form_class().data.copy() - previous_viz_type = form_data.get('previous_viz_type') - if isinstance(form_data, (MultiDict, ImmutableMultiDict)): - form = form_class(form_data) - else: - form = form_class(**form_data) - data = form.data.copy() - - if not form.validate(): - for k, v in form.errors.items(): - if not data.get('json') and not data.get('async'): - flasher("{}: {}".format(k, " ".join(v)), 'danger') - if previous_viz_type != self.viz_type: - data = { - k: form.data[k] - for k in form_data.keys() - if k in form.data} - defaults.update(data) - self.form_data = defaults self.query = "" - self.form_data['previous_viz_type'] = self.viz_type self.token = self.form_data.get( 'token', 'token_' + uuid.uuid4().hex[:8]) self.metrics = self.form_data.get('metrics') or [] self.groupby = self.form_data.get('groupby') or [] - self.reassignments() self.status = None self.error_message = None - @classmethod - def flat_form_fields(cls): - l = set() - for d in cls.fieldsets: - for obj in d['fields']: - if obj and isinstance(obj, (tuple, list)): - l |= {a for a in obj if a} - elif obj: - l.add(obj) - return tuple(l) - - def reassignments(self): - pass - - def get_url(self, for_cache_key=False, json_endpoint=False, **kwargs): - """Returns the URL for the viz - - :param for_cache_key: when getting the url as the identifier to hash - for the cache key - :type for_cache_key: boolean - """ - d = self.orig_form_data.copy() - if 'json' in d: - del d['json'] - if 'action' in d: - del d['action'] - if 'slice_id' in d: - del d['slice_id'] - d.update(kwargs) - # Remove unchecked checkboxes because HTML is weird like that - od = MultiDict() - for key in sorted(d.keys()): - # if MultiDict is initialized with MD({key:[emptyarray]}), - # key is included in d.keys() but accessing it throws - try: - if d[key] is False: - del d[key] - continue - except IndexError: - pass - - if isinstance(d, (MultiDict, ImmutableMultiDict)): - v = d.getlist(key) - else: - v = d.get(key) - if not isinstance(v, list): - v = [v] - for item in v: - od.add(key, item) - - base_endpoint = '/superset/explore' - if json_endpoint: - base_endpoint = '/superset/explore_json' - - href = Href( - '{base_endpoint}/{self.datasource.type}/' - '{self.datasource.id}/'.format(**locals())) - if for_cache_key and 'force' in od: - del od['force'] - return href(od) - def get_filter_url(self): """Returns the URL to retrieve column values used in the filter""" data = self.orig_form_data.copy() @@ -225,14 +133,6 @@ def get_df(self, query_obj=None): df = df.fillna(0) return df - @property - def form(self): - return self.form_class(**self.form_data) - - @property - def form_class(self): - return FormFactory(self).get_form() - def get_extra_filters(self): extra_filters = self.form_data.get('extra_filters') if not extra_filters: @@ -333,6 +233,11 @@ def get_json(self, force=False): self.get_payload(force), default=utils.json_int_dttm_ser, ignore_nan=True) + @property + def cache_key(self): + s = str((k, v) for k, v in self.form_data.items()) + return hashlib.md5(s.encode('utf-8')).hexdigest() + def get_payload(self, force=False): """Handles caching around the json payload retrieval""" cache_key = self.cache_key @@ -369,14 +274,11 @@ def get_payload(self, force=False): 'cache_key': cache_key, 'cache_timeout': cache_timeout, 'column_formats': self.data['column_formats'], - 'csv_endpoint': self.csv_endpoint, 'data': data, 'error': self.error_message, 'filter_endpoint': self.filter_endpoint, 'form_data': self.form_data, - 'json_endpoint': self.json_endpoint, 'query': self.query, - 'standalone_endpoint': self.standalone_endpoint, 'status': self.status, } payload['cached_dttm'] = datetime.now().isoformat().split('.')[0] @@ -406,11 +308,8 @@ def json_dumps(self, obj): def data(self): """This is the data object serialized to the js layer""" content = { - 'csv_endpoint': self.csv_endpoint, 'form_data': self.form_data, - 'json_endpoint': self.json_endpoint, 'filter_endpoint': self.filter_endpoint, - 'standalone_endpoint': self.standalone_endpoint, 'token': self.token, 'viz_name': self.viz_type, 'filter_select_enabled': self.datasource.filter_select_enabled, @@ -458,27 +357,10 @@ def get_values_for_column(self, column): def get_data(self): return [] - @property - def json_endpoint(self): - return self.get_url(json_endpoint=True) - @property def filter_endpoint(self): return self.get_filter_url() - @property - def cache_key(self): - url = self.get_url(for_cache_key=True, json="true", force="false") - return hashlib.md5(url.encode('utf-8')).hexdigest() - - @property - def csv_endpoint(self): - return self.get_url(csv="true") - - @property - def standalone_endpoint(self): - return self.get_url(standalone="true") - @property def json_data(self): return json.dumps(self.data) @@ -491,28 +373,6 @@ class TableViz(BaseViz): viz_type = "table" verbose_name = _("Table View") credits = 'a Superset original' - fieldsets = ({ - 'label': _("GROUP BY"), - 'description': _('Use this section if you want a query that aggregates'), - 'fields': ('groupby', 'metrics') - }, { - 'label': _("NOT GROUPED BY"), - 'description': _('Use this section if you want to query atomic rows'), - 'fields': ('all_columns', 'order_by_cols'), - }, { - 'label': _("Options"), - 'fields': ( - 'table_timestamp_format', - 'row_limit', - 'page_length', - ('include_search', 'table_filter'), - ) - }) - form_overrides = ({ - 'metrics': { - 'default': [], - }, - }) is_timeseries = False def query_obj(self): @@ -553,15 +413,6 @@ class PivotTableViz(BaseViz): verbose_name = _("Pivot Table") credits = 'a Superset original' is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'groupby', - 'columns', - 'metrics', - 'pandas_aggfunc', - ) - },) def query_obj(self): d = super(PivotTableViz, self).query_obj() @@ -610,10 +461,6 @@ class MarkupViz(BaseViz): viz_type = "markup" verbose_name = _("Markup") - fieldsets = ({ - 'label': None, - 'fields': ('markup_type', 'code') - },) is_timeseries = False def get_data(self): @@ -630,17 +477,6 @@ class SeparatorViz(MarkupViz): viz_type = "separator" verbose_name = _("Separator") - form_overrides = { - 'code': { - 'default': ( - "####Section Title\n" - "A paragraph describing the section" - "of the dashboard, right before the separator line " - "\n\n" - "---------------" - ), - } - } class WordCloudViz(BaseViz): @@ -654,14 +490,6 @@ class WordCloudViz(BaseViz): viz_type = "word_cloud" verbose_name = _("Word Cloud") is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'series', 'metric', 'limit', - ('size_from', 'size_to'), - 'rotation', - ) - },) def query_obj(self): d = super(WordCloudViz, self).query_obj() @@ -687,19 +515,6 @@ class TreemapViz(BaseViz): verbose_name = _("Treemap") credits = 'd3.js' is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'metrics', - 'groupby', - ), - }, { - 'label': _('Chart Options'), - 'fields': ( - 'treemap_ratio', - 'number_format', - ) - },) def _nest(self, metric, df): nlevels = df.index.nlevels @@ -728,14 +543,6 @@ class CalHeatmapViz(BaseViz): credits = ( 'cal-heatmap') is_timeseries = True - fieldsets = ({ - 'label': None, - 'fields': ( - 'metric', - 'domain_granularity', - 'subdomain_granularity', - ), - },) def get_data(self): df = self.get_df() @@ -794,18 +601,6 @@ class BoxPlotViz(NVD3Viz): verbose_name = _("Box Plot") sort_series = False is_timeseries = True - fieldsets = ({ - 'label': None, - 'fields': ( - 'metrics', - 'groupby', 'limit', - ), - }, { - 'label': _('Chart Options'), - 'fields': ( - 'whisker_options', - ) - },) def get_df(self, query_obj=None): form_data = self.form_data @@ -900,22 +695,6 @@ class BubbleViz(NVD3Viz): viz_type = "bubble" verbose_name = _("Bubble Chart") is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'series', 'entity', - 'x', 'y', - 'size', 'limit', - ) - }, { - 'label': _('Chart Options'), - 'fields': ( - ('x_log_scale', 'y_log_scale'), - ('show_legend', None), - 'max_bubble_size', - ('x_axis_label', 'y_axis_label'), - ) - },) def query_obj(self): form_data = self.form_data @@ -969,15 +748,6 @@ class BulletViz(NVD3Viz): viz_type = "bullet" verbose_name = _("Bullet Chart") is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'metric', - 'ranges', 'range_labels', - 'markers', 'marker_labels', - 'marker_lines', 'marker_line_labels', - ) - },) def query_obj(self): form_data = self.form_data @@ -1033,25 +803,6 @@ class BigNumberViz(BaseViz): verbose_name = _("Big Number with Trendline") credits = 'a Superset original' is_timeseries = True - fieldsets = ({ - 'label': None, - 'fields': ( - 'metric', - 'compare_lag', - 'compare_suffix', - 'y_axis_format', - ) - },) - form_overrides = { - 'y_axis_format': { - 'label': _('Number format'), - } - } - - def reassignments(self): - metric = self.form_data.get('metric') - if not metric: - self.form_data['metric'] = self.orig_form_data.get('metrics') def query_obj(self): d = super(BigNumberViz, self).query_obj() @@ -1083,24 +834,6 @@ class BigNumberTotalViz(BaseViz): verbose_name = _("Big Number") credits = 'a Superset original' is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'metric', - 'subheader', - 'y_axis_format', - ) - },) - form_overrides = { - 'y_axis_format': { - 'label': _('Number format'), - } - } - - def reassignments(self): - metric = self.form_data.get('metric') - if not metric: - self.form_data['metric'] = self.orig_form_data.get('metrics') def query_obj(self): d = super(BigNumberTotalViz, self).query_obj() @@ -1129,38 +862,6 @@ class NVD3TimeSeriesViz(NVD3Viz): verbose_name = _("Time Series - Line Chart") sort_series = False is_timeseries = True - fieldsets = ({ - 'label': None, - 'fields': ( - 'metrics', - 'groupby', - ('limit', 'timeseries_limit_metric'), - ), - }, { - 'label': _('Chart Options'), - 'fields': ( - ('show_brush', 'show_legend'), - ('rich_tooltip', 'y_axis_zero'), - ('y_log_scale', 'contribution'), - ('show_markers', 'x_axis_showminmax'), - ('line_interpolation', None), - ('x_axis_format', 'y_axis_format'), - ('x_axis_label', 'y_axis_label'), - ), - }, { - 'label': _('Advanced Analytics'), - 'description': _( - "This section contains options " - "that allow for advanced analytical post processing " - "of query results"), - 'fields': ( - ('rolling_type', 'rolling_periods'), - 'time_compare', - ('num_period_compare', 'period_ratio_type'), - None, - ('resample_how', 'resample_rule',), 'resample_fillmethod' - ), - },) def get_df(self, query_obj=None): form_data = self.form_data @@ -1289,31 +990,6 @@ class NVD3DualLineViz(NVD3Viz): verbose_name = _("Time Series - Dual Axis Line Chart") sort_series = False is_timeseries = True - fieldsets = ({ - 'label': _('Chart Options'), - 'fields': ('x_axis_format',), - }, { - 'label': _('Y Axis 1'), - 'fields': ( - 'metric', - 'y_axis_format' - ), - }, { - 'label': _('Y Axis 2'), - 'fields': ( - 'metric_2', - 'y_axis_2_format' - ), - },) - form_overrides = { - 'y_axis_format': { - 'label': _('Left Axis Format'), - 'description': _("Select the numeric column to draw the histogram"), - }, - 'metric': { - 'label': _("Left Axis Metric"), - } - } def get_df(self, query_obj=None): if not query_obj: @@ -1398,18 +1074,6 @@ class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz): viz_type = "bar" sort_series = True verbose_name = _("Time Series - Bar Chart") - fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ - 'label': _('Chart Options'), - 'fields': ( - ('show_brush', 'show_legend', 'show_bar_value'), - ('rich_tooltip', 'y_axis_zero'), - ('y_log_scale', 'contribution'), - ('x_axis_format', 'y_axis_format'), - ('line_interpolation', 'bar_stacked'), - ('x_axis_showminmax', 'bottom_margin'), - ('x_axis_label', 'y_axis_label'), - ('reduce_x_ticks', 'show_controls'), - ), }] + [NVD3TimeSeriesViz.fieldsets[2]] class NVD3CompareTimeSeriesViz(NVD3TimeSeriesViz): @@ -1427,16 +1091,6 @@ class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz): viz_type = "area" verbose_name = _("Time Series - Stacked") sort_series = True - fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ - 'label': _('Chart Options'), - 'fields': ( - ('show_brush', 'show_legend'), - ('rich_tooltip', 'y_axis_zero'), - ('y_log_scale', 'contribution'), - ('x_axis_format', 'y_axis_format'), - ('x_axis_showminmax', 'show_controls'), - ('line_interpolation', 'stacked_style'), - ), }] + [NVD3TimeSeriesViz.fieldsets[2]] class DistributionPieViz(NVD3Viz): @@ -1446,16 +1100,6 @@ class DistributionPieViz(NVD3Viz): viz_type = "pie" verbose_name = _("Distribution - NVD3 - Pie Chart") is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'metrics', 'groupby', - 'limit', - 'pie_label_type', - ('donut', 'show_legend'), - 'labels_outside', - ) - },) def query_obj(self): d = super(DistributionPieViz, self).query_obj() @@ -1484,30 +1128,6 @@ class HistogramViz(BaseViz): viz_type = "histogram" verbose_name = _("Histogram") is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - ('all_columns_x',), - 'row_limit', - ) - }, { - 'label': _("Histogram Options"), - 'fields': ( - 'link_length', - ) - },) - - form_overrides = { - 'all_columns_x': { - 'label': _('Numeric Column'), - 'description': _("Select the numeric column to draw the histogram"), - }, - 'link_length': { - 'label': _("No of Bins"), - 'description': _("Select number of bins for the histogram"), - 'default': 5 - } - } def query_obj(self): """Returns the query object for this visualization""" @@ -1534,29 +1154,6 @@ class DistributionBarViz(DistributionPieViz): viz_type = "dist_bar" verbose_name = _("Distribution - Bar Chart") is_timeseries = False - fieldsets = ({ - 'label': _('Chart Options'), - 'fields': ( - 'groupby', - 'columns', - 'metrics', - 'row_limit', - ('show_legend', 'show_bar_value', 'bar_stacked'), - ('y_axis_format', 'bottom_margin'), - ('x_axis_label', 'y_axis_label'), - ('reduce_x_ticks', 'contribution'), - ('show_controls', 'order_bars'), - ) - },) - form_overrides = { - 'groupby': { - 'label': _('Series'), - }, - 'columns': { - 'label': _('Breakdowns'), - 'description': _("Defines how each series is broken down"), - }, - } def query_obj(self): d = super(DistributionPieViz, self).query_obj() # noqa @@ -1624,33 +1221,6 @@ class SunburstViz(BaseViz): credits = ( 'Kerry Rodden ' '@bl.ocks.org') - fieldsets = ({ - 'label': None, - 'fields': ( - 'groupby', - 'metric', 'secondary_metric', - 'row_limit', - ) - },) - form_overrides = { - 'metric': { - 'label': _('Primary Metric'), - 'description': _( - "The primary metric is used to " - "define the arc segment sizes"), - }, - 'secondary_metric': { - 'label': _('Secondary Metric'), - 'description': _( - "This secondary metric is used to " - "define the color as a ratio against the primary metric. " - "If the two metrics match, color is mapped level groups"), - }, - 'groupby': { - 'label': _('Hierarchy'), - 'description': _("This defines the level of the hierarchy"), - }, - } def get_data(self): df = self.get_df() @@ -1683,20 +1253,6 @@ class SankeyViz(BaseViz): verbose_name = _("Sankey") is_timeseries = False credits = 'd3-sankey on npm' - fieldsets = ({ - 'label': None, - 'fields': ( - 'groupby', - 'metric', - 'row_limit', - ) - },) - form_overrides = { - 'groupby': { - 'label': _('Source / Target'), - 'description': _("Choose a source and a target"), - }, - } def query_obj(self): qry = super(SankeyViz, self).query_obj() @@ -1747,26 +1303,6 @@ class DirectedForceViz(BaseViz): verbose_name = _("Directed Force Layout") credits = 'd3noob @bl.ocks.org' is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'groupby', - 'metric', - 'row_limit', - ) - }, { - 'label': _('Force Layout'), - 'fields': ( - 'link_length', - 'charge', - ) - },) - form_overrides = { - 'groupby': { - 'label': _('Source / Target'), - 'description': _("Choose a source and a target"), - }, - } def query_obj(self): qry = super(DirectedForceViz, self).query_obj() @@ -1789,35 +1325,6 @@ class WorldMapViz(BaseViz): verbose_name = _("World Map") is_timeseries = False credits = 'datamaps on npm' - fieldsets = ({ - 'label': None, - 'fields': ( - 'entity', - 'country_fieldtype', - 'metric', - ) - }, { - 'label': _('Bubbles'), - 'fields': ( - ('show_bubbles', None), - 'secondary_metric', - 'max_bubble_size', - ) - }) - form_overrides = { - 'entity': { - 'label': _('Country Field'), - 'description': _("3 letter code of the country"), - }, - 'metric': { - 'label': _('Metric for color'), - 'description': _("Metric that defines the color of the country"), - }, - 'secondary_metric': { - 'label': _('Bubble size'), - 'description': _("Metric that defines the size of the bubble"), - }, - } def query_obj(self): qry = super(WorldMapViz, self).query_obj() @@ -1868,21 +1375,6 @@ class FilterBoxViz(BaseViz): verbose_name = _("Filters") is_timeseries = False credits = 'a Superset original' - fieldsets = ({ - 'label': None, - 'fields': ( - ('date_filter', None), - 'groupby', - 'metric', - ) - },) - form_overrides = { - 'groupby': { - 'label': _('Filter fields'), - 'description': _("The fields you want to filter on"), - 'default': [], - }, - } def query_obj(self): qry = super(FilterBoxViz, self).query_obj() @@ -1918,10 +1410,6 @@ class IFrameViz(BaseViz): verbose_name = _("iFrame") credits = 'a Superset original' is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ('url',) - },) class ParallelCoordinatesViz(BaseViz): @@ -1938,16 +1426,6 @@ class ParallelCoordinatesViz(BaseViz): '' 'Syntagmatic\'s library') is_timeseries = False - fieldsets = ({ - 'label': None, - 'fields': ( - 'series', - 'metrics', - 'secondary_metric', - 'limit', - ('show_datatable', 'include_series'), - ) - },) def query_obj(self): d = super(ParallelCoordinatesViz, self).query_obj() @@ -1974,22 +1452,6 @@ class HeatmapViz(BaseViz): credits = ( 'inspired from mbostock @' 'bl.ocks.org') - fieldsets = ({ - 'label': None, - 'fields': ( - 'all_columns_x', - 'all_columns_y', - 'metric', - ) - }, { - 'label': _('Heatmap Options'), - 'fields': ( - 'linear_color_scheme', - ('xscale_interval', 'yscale_interval'), - 'canvas_image_rendering', - 'normalize_across', - ) - },) def query_obj(self): d = super(HeatmapViz, self).query_obj() @@ -2041,11 +1503,6 @@ class HorizonViz(NVD3TimeSeriesViz): credits = ( '' 'd3-horizon-chart') - fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ - 'label': _('Chart Options'), - 'fields': ( - ('series_height', 'horizon_color_scale'), - ), }] class MapboxViz(BaseViz): @@ -2057,70 +1514,6 @@ class MapboxViz(BaseViz): is_timeseries = False credits = ( 'Mapbox GL JS') - fieldsets = ({ - 'label': None, - 'fields': ( - ('all_columns_x', 'all_columns_y'), - 'clustering_radius', - 'row_limit', - 'groupby', - 'render_while_dragging', - ) - }, { - 'label': _('Points'), - 'fields': ( - 'point_radius', - 'point_radius_unit', - ) - }, { - 'label': _('Labelling'), - 'fields': ( - 'mapbox_label', - 'pandas_aggfunc', - ) - }, { - 'label': _('Visual Tweaks'), - 'fields': ( - 'mapbox_style', - 'global_opacity', - 'mapbox_color', - ) - }, { - 'label': _('Viewport'), - 'fields': ( - 'viewport_longitude', - 'viewport_latitude', - 'viewport_zoom', - ) - },) - - form_overrides = { - 'all_columns_x': { - 'label': _('Longitude'), - 'description': _("Column containing longitude data"), - }, - 'all_columns_y': { - 'label': _('Latitude'), - 'description': _("Column containing latitude data"), - }, - 'pandas_aggfunc': { - 'label': _('Cluster label aggregator'), - 'description': _( - "Aggregate function applied to the list of points " - "in each cluster to produce the cluster label."), - }, - 'rich_tooltip': { - 'label': _('Tooltip'), - 'description': _( - "Show a tooltip when hovering over points and clusters " - "describing the label"), - }, - 'groupby': { - 'description': _( - "One or many fields to group by. If grouping, latitude " - "and longitude columns must be present."), - }, - } def query_obj(self): d = super(MapboxViz, self).query_obj()