diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json
index 71e7130328492..ba91b47cde46c 100644
--- a/superset/assets/backendSync.json
+++ b/superset/assets/backendSync.json
@@ -1,175 +1,15 @@
{
"controls": {
"datasource": {
- "type": "SelectControl",
+ "type": "DatasourceControl",
"label": "Datasource",
- "isLoading": true,
- "clearable": false,
"default": null,
- "description": ""
+ "description": null
},
"viz_type": {
- "type": "SelectControl",
+ "type": "VizTypeControl",
"label": "Visualization Type",
- "clearable": false,
"default": "table",
- "choices": [
- [
- "dist_bar",
- "Distribution - Bar Chart",
- "/static/assets/images/viz_thumbnails/dist_bar.png"
- ],
- [
- "pie",
- "Pie Chart",
- "/static/assets/images/viz_thumbnails/pie.png"
- ],
- [
- "line",
- "Time Series - Line Chart",
- "/static/assets/images/viz_thumbnails/line.png"
- ],
- [
- "dual_line",
- "Time Series - Dual Axis Line Chart",
- "/static/assets/images/viz_thumbnails/dual_line.png"
- ],
- [
- "bar",
- "Time Series - Bar Chart",
- "/static/assets/images/viz_thumbnails/bar.png"
- ],
- [
- "compare",
- "Time Series - Percent Change",
- "/static/assets/images/viz_thumbnails/compare.png"
- ],
- [
- "area",
- "Time Series - Stacked",
- "/static/assets/images/viz_thumbnails/area.png"
- ],
- [
- "table",
- "Table View",
- "/static/assets/images/viz_thumbnails/table.png"
- ],
- [
- "markup",
- "Markup",
- "/static/assets/images/viz_thumbnails/markup.png"
- ],
- [
- "pivot_table",
- "Pivot Table",
- "/static/assets/images/viz_thumbnails/pivot_table.png"
- ],
- [
- "separator",
- "Separator",
- "/static/assets/images/viz_thumbnails/separator.png"
- ],
- [
- "word_cloud",
- "Word Cloud",
- "/static/assets/images/viz_thumbnails/word_cloud.png"
- ],
- [
- "treemap",
- "Treemap",
- "/static/assets/images/viz_thumbnails/treemap.png"
- ],
- [
- "cal_heatmap",
- "Calendar Heatmap",
- "/static/assets/images/viz_thumbnails/cal_heatmap.png"
- ],
- [
- "box_plot",
- "Box Plot",
- "/static/assets/images/viz_thumbnails/box_plot.png"
- ],
- [
- "bubble",
- "Bubble Chart",
- "/static/assets/images/viz_thumbnails/bubble.png"
- ],
- [
- "bullet",
- "Bullet Chart",
- "/static/assets/images/viz_thumbnails/bullet.png"
- ],
- [
- "big_number",
- "Big Number with Trendline",
- "/static/assets/images/viz_thumbnails/big_number.png"
- ],
- [
- "big_number_total",
- "Big Number",
- "/static/assets/images/viz_thumbnails/big_number_total.png"
- ],
- [
- "histogram",
- "Histogram",
- "/static/assets/images/viz_thumbnails/histogram.png"
- ],
- [
- "sunburst",
- "Sunburst",
- "/static/assets/images/viz_thumbnails/sunburst.png"
- ],
- [
- "sankey",
- "Sankey",
- "/static/assets/images/viz_thumbnails/sankey.png"
- ],
- [
- "directed_force",
- "Directed Force Layout",
- "/static/assets/images/viz_thumbnails/directed_force.png"
- ],
- [
- "country_map",
- "Country Map",
- "/static/assets/images/viz_thumbnails/country_map.png"
- ],
- [
- "world_map",
- "World Map",
- "/static/assets/images/viz_thumbnails/world_map.png"
- ],
- [
- "filter_box",
- "Filter Box",
- "/static/assets/images/viz_thumbnails/filter_box.png"
- ],
- [
- "iframe",
- "iFrame",
- "/static/assets/images/viz_thumbnails/iframe.png"
- ],
- [
- "para",
- "Parallel Coordinates",
- "/static/assets/images/viz_thumbnails/para.png"
- ],
- [
- "heatmap",
- "Heatmap",
- "/static/assets/images/viz_thumbnails/heatmap.png"
- ],
- [
- "horizon",
- "Horizon",
- "/static/assets/images/viz_thumbnails/horizon.png"
- ],
- [
- "mapbox",
- "Mapbox",
- "/static/assets/images/viz_thumbnails/mapbox.png"
- ]
- ],
"description": "The type of visualization to display"
},
"metrics": {
@@ -179,8 +19,26 @@
"validators": [
null
],
+ "valueKey": "metric_name",
"description": "One or many metrics to display"
},
+ "percent_metrics": {
+ "type": "SelectControl",
+ "multi": true,
+ "label": "Percentage Metrics",
+ "valueKey": "metric_name",
+ "description": "Metrics for which percentage of total are to be displayed"
+ },
+ "y_axis_bounds": {
+ "type": "BoundsControl",
+ "label": "Y Axis Bounds",
+ "renderTrigger": true,
+ "default": [
+ null,
+ null
+ ],
+ "description": "Bounds for the Y axis. When left empty, the bounds are dynamically defined based on the min/max of the data. Note that this feature will only expand the axis range. It won't narrow the data's extent."
+ },
"order_by_cols": {
"type": "SelectControl",
"multi": true,
@@ -188,18 +46,38 @@
"default": [],
"description": "One or many metrics to display"
},
+ "color_picker": {
+ "label": "Fixed Color",
+ "description": "Use this to define a static color for all circles",
+ "type": "ColorPickerControl",
+ "default": {
+ "r": 0,
+ "g": 122,
+ "b": 135,
+ "a": 1
+ },
+ "renderTrigger": true
+ },
"metric": {
"type": "SelectControl",
"label": "Metric",
"clearable": false,
- "description": "Choose the metric"
+ "description": "Choose the metric",
+ "validators": [
+ null
+ ],
+ "valueKey": "metric_name"
},
"metric_2": {
"type": "SelectControl",
"label": "Right Axis Metric",
- "choices": [],
- "default": [],
- "description": "Choose a metric for right axis"
+ "default": null,
+ "validators": [
+ null
+ ],
+ "clearable": true,
+ "description": "Choose a metric for right axis",
+ "valueKey": "metric_name"
},
"stacked_style": {
"type": "SelectControl",
@@ -221,8 +99,56 @@
"default": "stack",
"description": ""
},
- "linear_color_scheme": {
+ "sort_x_axis": {
"type": "SelectControl",
+ "label": "Sort X Axis",
+ "choices": [
+ [
+ "alpha_asc",
+ "Axis ascending"
+ ],
+ [
+ "alpha_desc",
+ "Axis descending"
+ ],
+ [
+ "value_asc",
+ "sum(value) ascending"
+ ],
+ [
+ "value_desc",
+ "sum(value) descending"
+ ]
+ ],
+ "clearable": false,
+ "default": "alpha_asc"
+ },
+ "sort_y_axis": {
+ "type": "SelectControl",
+ "label": "Sort Y Axis",
+ "choices": [
+ [
+ "alpha_asc",
+ "Axis ascending"
+ ],
+ [
+ "alpha_desc",
+ "Axis descending"
+ ],
+ [
+ "value_asc",
+ "sum(value) ascending"
+ ],
+ [
+ "value_desc",
+ "sum(value) descending"
+ ]
+ ],
+ "clearable": false,
+ "default": "alpha_asc"
+ },
+ "linear_color_scheme": {
+ "type": "ColorSchemeControl",
"label": "Linear Color Scheme",
"choices": [
[
@@ -240,10 +166,54 @@
[
"black_white",
"black/white"
+ ],
+ [
+ "dark_blue",
+ "light/dark blue"
+ ],
+ [
+ "pink_grey",
+ "pink/white/grey"
]
],
"default": "blue_white_yellow",
- "description": ""
+ "clearable": false,
+ "description": "",
+ "renderTrigger": true,
+ "schemes": {
+ "blue_white_yellow": [
+ "#00d1c1",
+ "white",
+ "#ffb400"
+ ],
+ "fire": [
+ "white",
+ "yellow",
+ "red",
+ "black"
+ ],
+ "white_black": [
+ "white",
+ "black"
+ ],
+ "black_white": [
+ "black",
+ "white"
+ ],
+ "dark_blue": [
+ "#EBF5F8",
+ "#6BB1CC",
+ "#357E9B",
+ "#1B4150",
+ "#092935"
+ ],
+ "pink_grey": [
+ "#E70B81",
+ "#FAFAFA",
+ "#666666"
+ ]
+ },
+ "isLinear": true
},
"normalize_across": {
"type": "SelectControl",
@@ -288,6 +258,7 @@
"canvas_image_rendering": {
"type": "SelectControl",
"label": "Rendering",
+ "renderTrigger": true,
"choices": [
[
"pixelated",
@@ -723,6 +694,13 @@
"description": "Whether to include the time granularity as defined in the time section",
"default": false
},
+ "show_perc": {
+ "type": "CheckboxControl",
+ "label": "Show percentage",
+ "renderTrigger": true,
+ "description": "Whether to include the percentage in the tooltip",
+ "default": true
+ },
"bar_stacked": {
"type": "CheckboxControl",
"label": "Stacked Bars",
@@ -730,6 +708,13 @@
"default": false,
"description": null
},
+ "pivot_margins": {
+ "type": "CheckboxControl",
+ "label": "Show totals",
+ "renderTrigger": false,
+ "default": true,
+ "description": "Display total row/column"
+ },
"show_markers": {
"type": "CheckboxControl",
"label": "Show Markers",
@@ -785,29 +770,21 @@
},
"select_country": {
"type": "SelectControl",
- "label": "Country Name Type",
+ "label": "Country Name",
"default": "France",
"choices": [
- [
- "Algeria",
- "Algeria"
- ],
[
"Belgium",
"Belgium"
],
[
- "Brasil",
- "Brasil"
+ "Brazil",
+ "Brazil"
],
[
"China",
"China"
],
- [
- "Germany",
- "Germany"
- ],
[
"Egypt",
"Egypt"
@@ -816,6 +793,10 @@
"France",
"France"
],
+ [
+ "Germany",
+ "Germany"
+ ],
[
"Italy",
"Italy"
@@ -825,8 +806,8 @@
"Morocco"
],
[
- "Nederlanden",
- "Nederlanden"
+ "Netherlands",
+ "Netherlands"
],
[
"Russia",
@@ -844,6 +825,10 @@
"Uk",
"Uk"
],
+ [
+ "Ukraine",
+ "Ukraine"
+ ],
[
"Usa",
"Usa"
@@ -875,19 +860,66 @@
],
"description": "The country code standard that Superset should expect to find in the [country] column"
},
+ "freq": {
+ "type": "SelectControl",
+ "label": "Frequency",
+ "default": "W-MON",
+ "freeForm": true,
+ "clearable": false,
+ "choices": [
+ [
+ "AS",
+ "Year (freq=AS)"
+ ],
+ [
+ "52W-MON",
+ "52 weeks starting Monday (freq=52W-MON)"
+ ],
+ [
+ "W-SUN",
+ "1 week starting Sunday (freq=W-SUN)"
+ ],
+ [
+ "W-MON",
+ "1 week starting Monday (freq=W-MON)"
+ ],
+ [
+ "D",
+ "Day (freq=D)"
+ ],
+ [
+ "4W-MON",
+ "4 weeks (freq=4W-MON)"
+ ]
+ ],
+ "description": "The periodicity over which to pivot time. Users can provide\n \"Pandas\" offset alias.\n Click on the info bubble for more details on accepted \"freq\" expressions."
+ },
"groupby": {
"type": "SelectControl",
"multi": true,
"label": "Group by",
"default": [],
- "description": "One or many controls to group by"
+ "includeTime": false,
+ "description": "One or many controls to group by",
+ "valueKey": "column_name"
+ },
+ "dimension": {
+ "type": "SelectControl",
+ "multi": false,
+ "label": "Dimension",
+ "default": null,
+ "includeTime": false,
+ "description": "Select a dimension",
+ "valueKey": "column_name"
},
"columns": {
"type": "SelectControl",
"multi": true,
"label": "Columns",
"default": [],
- "description": "One or many controls to pivot as columns"
+ "includeTime": false,
+ "description": "One or many controls to pivot as columns",
+ "valueKey": "column_name"
},
"all_columns": {
"type": "SelectControl",
@@ -896,6 +928,24 @@
"default": [],
"description": "Columns to display"
},
+ "longitude": {
+ "type": "SelectControl",
+ "label": "Longitude",
+ "default": 1,
+ "validators": [
+ null
+ ],
+ "description": "Select the longitude column"
+ },
+ "latitude": {
+ "type": "SelectControl",
+ "label": "Latitude",
+ "default": 1,
+ "validators": [
+ null
+ ],
+ "description": "Select the latitude column"
+ },
"all_columns_x": {
"type": "SelectControl",
"label": "X",
@@ -960,7 +1010,46 @@
]
],
"default": "auto",
- "description": "Bottom marging, in pixels, allowing for more room for axis labels"
+ "renderTrigger": true,
+ "description": "Bottom margin, in pixels, allowing for more room for axis labels"
+ },
+ "left_margin": {
+ "type": "SelectControl",
+ "freeForm": true,
+ "label": "Left Margin",
+ "choices": [
+ [
+ "auto",
+ "auto"
+ ],
+ [
+ 50,
+ "50"
+ ],
+ [
+ 75,
+ "75"
+ ],
+ [
+ 100,
+ "100"
+ ],
+ [
+ 125,
+ "125"
+ ],
+ [
+ 150,
+ "150"
+ ],
+ [
+ 200,
+ "200"
+ ]
+ ],
+ "default": "auto",
+ "renderTrigger": true,
+ "description": "Left margin, in pixels, allowing for more room for axis labels"
},
"granularity": {
"type": "SelectControl",
@@ -1172,7 +1261,9 @@
"granularity_sqla": {
"type": "SelectControl",
"label": "Time Column",
- "description": "The time column for the visualization. Note that you can define arbitrary expression that return a DATETIME column in the table or. Also note that the filter below is applied against this column or expression"
+ "description": "The time column for the visualization. Note that you can define arbitrary expression that return a DATETIME column in the table. Also note that the filter below is applied against this column or expression",
+ "clearable": false,
+ "valueKey": "column_name"
},
"time_grain_sqla": {
"type": "SelectControl",
@@ -1263,77 +1354,16 @@
"description": "Pandas resample fill method"
},
"since": {
- "type": "SelectControl",
+ "type": "DateFilterControl",
"freeForm": true,
"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"
- ],
- [
- "100 year ago",
- "100 year ago"
- ]
- ],
- "description": "Timestamp from filter. This supports free form typing and natural language as in `1 day ago`, `28 days` or `3 years`"
+ "default": "7 days ago"
},
"until": {
- "type": "SelectControl",
+ "type": "DateFilterControl",
"freeForm": true,
"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"
- ]
- ]
+ "default": "now"
},
"max_bubble_size": {
"type": "SelectControl",
@@ -1441,6 +1471,9 @@
"type": "SelectControl",
"freeForm": true,
"label": "Row limit",
+ "validators": [
+ null
+ ],
"default": null,
"choices": [
[
@@ -1485,6 +1518,9 @@
"type": "SelectControl",
"freeForm": true,
"label": "Series limit",
+ "validators": [
+ null
+ ],
"choices": [
[
0,
@@ -1524,6 +1560,12 @@
"default": null,
"description": "Metric used to define the top series"
},
+ "order_desc": {
+ "type": "CheckboxControl",
+ "label": "Sort Descending",
+ "default": true,
+ "description": "Whether to sort descending or ascending"
+ },
"rolling_type": {
"type": "SelectControl",
"label": "Rolling",
@@ -1552,12 +1594,33 @@
],
"description": "Defines a rolling window function to apply, works along with the [Periods] text box"
},
+ "multiplier": {
+ "type": "TextControl",
+ "label": "Multiplier",
+ "isFloat": true,
+ "default": 1,
+ "description": "Factor to multiply the metric by"
+ },
"rolling_periods": {
"type": "TextControl",
"label": "Periods",
"isInt": true,
"description": "Defines the size of the rolling window function, relative to the time granularity selected"
},
+ "grid_size": {
+ "type": "TextControl",
+ "label": "Grid Size",
+ "renderTrigger": true,
+ "default": 20,
+ "isInt": true,
+ "description": "Defines the grid size in pixels"
+ },
+ "min_periods": {
+ "type": "TextControl",
+ "label": "Min Periods",
+ "isInt": true,
+ "description": "The minimum number of rolling periods required to show a value. For instance if you do a cumulative sum on 7 days you may want your \"Min Period\" to be 7, so that all data points shown are the total of 7 periods. This will hide the \"ramp up\" taking place over the first 7 periods"
+ },
"series": {
"type": "SelectControl",
"label": "Series",
@@ -1568,24 +1631,39 @@
"type": "SelectControl",
"label": "Entity",
"default": null,
- "description": "This define the element to be plotted on the chart"
+ "validators": [
+ null
+ ],
+ "description": "This defines the element to be plotted on the chart"
},
"x": {
"type": "SelectControl",
"label": "X Axis",
+ "description": "Metric assigned to the [X] axis",
"default": null,
- "description": "Metric assigned to the [X] axis"
+ "validators": [
+ null
+ ],
+ "valueKey": "metric_name"
},
"y": {
"type": "SelectControl",
"label": "Y Axis",
"default": null,
- "description": "Metric assigned to the [Y] axis"
+ "validators": [
+ null
+ ],
+ "description": "Metric assigned to the [Y] axis",
+ "valueKey": "metric_name"
},
"size": {
"type": "SelectControl",
"label": "Bubble Size",
- "default": null
+ "default": null,
+ "validators": [
+ null
+ ],
+ "valueKey": "metric_name"
},
"url": {
"type": "TextControl",
@@ -1632,7 +1710,11 @@
"type": "SelectControl",
"freeForm": true,
"label": "Table Timestamp Format",
- "default": "smart_date",
+ "default": "%Y-%m-%d %H:%M:%S",
+ "validators": [
+ null
+ ],
+ "clearable": false,
"choices": [
[
"smart_date",
@@ -1746,7 +1828,41 @@
"x_axis_format": {
"type": "SelectControl",
"freeForm": true,
- "label": "X axis format",
+ "label": "X Axis Format",
+ "renderTrigger": true,
+ "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 syntax: https://github.com/d3/d3-format"
+ },
+ "x_axis_time_format": {
+ "type": "SelectControl",
+ "freeForm": true,
+ "label": "X Axis Format",
"renderTrigger": true,
"default": "smart_date",
"choices": [
@@ -1776,7 +1892,7 @@
"y_axis_format": {
"type": "SelectControl",
"freeForm": true,
- "label": "Y axis format",
+ "label": "Y Axis Format",
"renderTrigger": true,
"default": ".3s",
"choices": [
@@ -1810,7 +1926,7 @@
"y_axis_2_format": {
"type": "SelectControl",
"freeForm": true,
- "label": "Right axis format",
+ "label": "Right Axis Format",
"default": ".3s",
"choices": [
[
@@ -1840,9 +1956,40 @@
],
"description": "D3 format syntax: https://github.com/d3/d3-format"
},
+ "date_time_format": {
+ "type": "SelectControl",
+ "freeForm": true,
+ "label": "Date Time Format",
+ "renderTrigger": true,
+ "default": "smart_date",
+ "choices": [
+ [
+ "smart_date",
+ "Adaptative formating"
+ ],
+ [
+ "%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"
+ ]
+ ],
+ "description": "D3 format syntax: https://github.com/d3/d3-format"
+ },
"markup_type": {
"type": "SelectControl",
"label": "Markup Type",
+ "clearable": false,
"choices": [
[
"markdown",
@@ -1854,6 +2001,9 @@
]
],
"default": "markdown",
+ "validators": [
+ null
+ ],
"description": "Pick your favorite markup language"
},
"rotation": {
@@ -1925,6 +2075,14 @@
[
"percent",
"Percentage"
+ ],
+ [
+ "key_value",
+ "Category and Value"
+ ],
+ [
+ "key_percent",
+ "Category and Percentage"
]
],
"description": "What should be shown on the label?"
@@ -1993,6 +2151,13 @@
"default": true,
"description": "Whether to apply filters as they change, or wait forusers to hit an [Apply] button"
},
+ "extruded": {
+ "type": "CheckboxControl",
+ "label": "Extruded",
+ "renderTrigger": true,
+ "default": true,
+ "description": "Whether to make the grid 3D"
+ },
"show_brush": {
"type": "CheckboxControl",
"label": "Range Filter",
@@ -2006,6 +2171,30 @@
"default": false,
"description": "Whether to include a time filter"
},
+ "show_sqla_time_granularity": {
+ "type": "CheckboxControl",
+ "label": "Show SQL Granularity Dropdown",
+ "default": false,
+ "description": "Check to include SQL Granularity dropdown"
+ },
+ "show_sqla_time_column": {
+ "type": "CheckboxControl",
+ "label": "Show SQL Time Column",
+ "default": false,
+ "description": "Check to include Time Column dropdown"
+ },
+ "show_druid_time_granularity": {
+ "type": "CheckboxControl",
+ "label": "Show Druid Granularity Dropdown",
+ "default": false,
+ "description": "Check to include Druid Granularity dropdown"
+ },
+ "show_druid_time_origin": {
+ "type": "CheckboxControl",
+ "label": "Show Druid Time Origin",
+ "default": false,
+ "description": "Check to include Time Origin dropdown"
+ },
"show_datatable": {
"type": "CheckboxControl",
"label": "Data Table",
@@ -2039,6 +2228,13 @@
"default": true,
"description": "Whether to display the legend (toggles)"
},
+ "show_values": {
+ "type": "CheckboxControl",
+ "label": "Show Values",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Whether to display the numerical values within the cells"
+ },
"x_axis_showminmax": {
"type": "CheckboxControl",
"label": "X bounds",
@@ -2046,19 +2242,19 @@
"default": true,
"description": "Whether to display the min and max values of the X axis"
},
- "rich_tooltip": {
+ "y_axis_showminmax": {
"type": "CheckboxControl",
- "label": "Rich Tooltip",
+ "label": "Y bounds",
"renderTrigger": true,
"default": true,
- "description": "The rich tooltip shows a list of all series for that point in time"
+ "description": "Whether to display the min and max values of the Y axis"
},
- "y_axis_zero": {
+ "rich_tooltip": {
"type": "CheckboxControl",
- "label": "Y Axis Zero",
- "default": false,
+ "label": "Rich Tooltip",
"renderTrigger": true,
- "description": "Force the Y axis to start at 0 instead of the minimum value"
+ "default": true,
+ "description": "The rich tooltip shows a list of all series for that point in time"
},
"y_log_scale": {
"type": "CheckboxControl",
@@ -2074,16 +2270,25 @@
"renderTrigger": true,
"description": "Use a log scale for the X axis"
},
+ "log_scale": {
+ "type": "CheckboxControl",
+ "label": "Log Scale",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Use a log scale"
+ },
"donut": {
"type": "CheckboxControl",
"label": "Donut",
"default": false,
+ "renderTrigger": true,
"description": "Do you want a donut or a pie?"
},
"labels_outside": {
"type": "CheckboxControl",
"label": "Put labels outside",
"default": true,
+ "renderTrigger": true,
"description": "Put the labels outside the pie?"
},
"contribution": {
@@ -2140,6 +2345,7 @@
"mapbox_style": {
"type": "SelectControl",
"label": "Map Style",
+ "renderTrigger": true,
"choices": [
[
"mapbox://styles/mapbox/streets-v9",
@@ -2214,6 +2420,11 @@
],
"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_fixed": {
+ "type": "FixedOrMetricControl",
+ "label": "Point Size",
+ "description": "Fixed point radius"
+ },
"point_radius": {
"type": "SelectControl",
"label": "Point Radius",
@@ -2240,6 +2451,39 @@
],
"description": "The unit of measure for the specified point radius"
},
+ "point_unit": {
+ "type": "SelectControl",
+ "label": "Point Unit",
+ "default": "square_m",
+ "clearable": false,
+ "choices": [
+ [
+ "square_m",
+ "Square meters"
+ ],
+ [
+ "square_km",
+ "Square kilometers"
+ ],
+ [
+ "square_miles",
+ "Square miles"
+ ],
+ [
+ "radius_m",
+ "Radius in meters"
+ ],
+ [
+ "radius_km",
+ "Radius in kilometers"
+ ],
+ [
+ "radius_miles",
+ "Radius in miles"
+ ]
+ ],
+ "description": "The unit of measure for the specified point radius"
+ },
"global_opacity": {
"type": "TextControl",
"label": "Opacity",
@@ -2247,6 +2491,19 @@
"isFloat": true,
"description": "Opacity of all clusters, points, and labels. Between 0 and 1."
},
+ "viewport": {
+ "type": "ViewportControl",
+ "label": "Viewport",
+ "renderTrigger": true,
+ "description": "Parameters related to the view and perspective on the map",
+ "default": {
+ "longitude": 6.85236157047845,
+ "latitude": 31.222656842808707,
+ "zoom": 1,
+ "bearing": 0,
+ "pitch": 0
+ }
+ },
"viewport_zoom": {
"type": "TextControl",
"label": "Zoom",
@@ -2310,6 +2567,17 @@
],
"description": "The color for points and clusters in RGB"
},
+ "color": {
+ "type": "ColorPickerControl",
+ "label": "Color",
+ "default": {
+ "r": 0,
+ "g": 122,
+ "b": 135,
+ "a": 1
+ },
+ "description": "Pick a color"
+ },
"ranges": {
"type": "TextControl",
"label": "Ranges",
@@ -2352,6 +2620,13 @@
"default": [],
"description": ""
},
+ "annotation_layers": {
+ "type": "AnnotationLayerControl",
+ "label": "",
+ "default": [],
+ "description": "Annotation Layers",
+ "renderTrigger": true
+ },
"having_filters": {
"type": "FilterControl",
"label": "",
@@ -2369,6 +2644,334 @@
"label": "Cache Timeout (seconds)",
"hidden": true,
"description": "The number of seconds before expiring the cache"
+ },
+ "order_by_entity": {
+ "type": "CheckboxControl",
+ "label": "Order by entity id",
+ "description": "Important! Select this if the table is not already sorted by entity id, else there is no guarantee that all events for each entity are returned.",
+ "default": true
+ },
+ "min_leaf_node_event_count": {
+ "type": "SelectControl",
+ "freeForm": false,
+ "label": "Minimum leaf node event count",
+ "default": 1,
+ "choices": [
+ [
+ 1,
+ "1"
+ ],
+ [
+ 2,
+ "2"
+ ],
+ [
+ 3,
+ "3"
+ ],
+ [
+ 4,
+ "4"
+ ],
+ [
+ 5,
+ "5"
+ ],
+ [
+ 6,
+ "6"
+ ],
+ [
+ 7,
+ "7"
+ ],
+ [
+ 8,
+ "8"
+ ],
+ [
+ 9,
+ "9"
+ ],
+ [
+ 10,
+ "10"
+ ]
+ ],
+ "description": "Leaf nodes that represent fewer than this number of events will be initially hidden in the visualization"
+ },
+ "color_scheme": {
+ "type": "ColorSchemeControl",
+ "label": "Color Scheme",
+ "default": "bnbColors",
+ "renderTrigger": true,
+ "choices": [
+ [
+ "bnbColors",
+ "bnbColors"
+ ],
+ [
+ "d3Category10",
+ "d3Category10"
+ ],
+ [
+ "d3Category20",
+ "d3Category20"
+ ],
+ [
+ "d3Category20b",
+ "d3Category20b"
+ ],
+ [
+ "d3Category20c",
+ "d3Category20c"
+ ],
+ [
+ "googleCategory10c",
+ "googleCategory10c"
+ ],
+ [
+ "googleCategory20c",
+ "googleCategory20c"
+ ]
+ ],
+ "description": "The color scheme for rendering chart",
+ "schemes": {
+ "bnbColors": [
+ "#ff5a5f",
+ "#7b0051",
+ "#007A87",
+ "#00d1c1",
+ "#8ce071",
+ "#ffb400",
+ "#b4a76c",
+ "#ff8083",
+ "#cc0086",
+ "#00a1b3",
+ "#00ffeb",
+ "#bbedab",
+ "#ffd266",
+ "#cbc29a",
+ "#ff3339",
+ "#ff1ab1",
+ "#005c66",
+ "#00b3a5",
+ "#55d12e",
+ "#b37e00",
+ "#988b4e"
+ ],
+ "d3Category10": [
+ "#1f77b4",
+ "#ff7f0e",
+ "#2ca02c",
+ "#d62728",
+ "#9467bd",
+ "#8c564b",
+ "#e377c2",
+ "#7f7f7f",
+ "#bcbd22",
+ "#17becf"
+ ],
+ "d3Category20": [
+ "#1f77b4",
+ "#aec7e8",
+ "#ff7f0e",
+ "#ffbb78",
+ "#2ca02c",
+ "#98df8a",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5",
+ "#8c564b",
+ "#c49c94",
+ "#e377c2",
+ "#f7b6d2",
+ "#7f7f7f",
+ "#c7c7c7",
+ "#bcbd22",
+ "#dbdb8d",
+ "#17becf",
+ "#9edae5"
+ ],
+ "d3Category20b": [
+ "#393b79",
+ "#5254a3",
+ "#6b6ecf",
+ "#9c9ede",
+ "#637939",
+ "#8ca252",
+ "#b5cf6b",
+ "#cedb9c",
+ "#8c6d31",
+ "#bd9e39",
+ "#e7ba52",
+ "#e7cb94",
+ "#843c39",
+ "#ad494a",
+ "#d6616b",
+ "#e7969c",
+ "#7b4173",
+ "#a55194",
+ "#ce6dbd",
+ "#de9ed6"
+ ],
+ "d3Category20c": [
+ "#3182bd",
+ "#6baed6",
+ "#9ecae1",
+ "#c6dbef",
+ "#e6550d",
+ "#fd8d3c",
+ "#fdae6b",
+ "#fdd0a2",
+ "#31a354",
+ "#74c476",
+ "#a1d99b",
+ "#c7e9c0",
+ "#756bb1",
+ "#9e9ac8",
+ "#bcbddc",
+ "#dadaeb",
+ "#636363",
+ "#969696",
+ "#bdbdbd",
+ "#d9d9d9"
+ ],
+ "googleCategory10c": [
+ "#3366cc",
+ "#dc3912",
+ "#ff9900",
+ "#109618",
+ "#990099",
+ "#0099c6",
+ "#dd4477",
+ "#66aa00",
+ "#b82e2e",
+ "#316395"
+ ],
+ "googleCategory20c": [
+ "#3366cc",
+ "#dc3912",
+ "#ff9900",
+ "#109618",
+ "#990099",
+ "#0099c6",
+ "#dd4477",
+ "#66aa00",
+ "#b82e2e",
+ "#316395",
+ "#994499",
+ "#22aa99",
+ "#aaaa11",
+ "#6633cc",
+ "#e67300",
+ "#8b0707",
+ "#651067",
+ "#329262",
+ "#5574a6",
+ "#3b3eac"
+ ]
+ }
+ },
+ "significance_level": {
+ "type": "TextControl",
+ "label": "Significance Level",
+ "default": 0.05,
+ "description": "Threshold alpha level for determining significance"
+ },
+ "pvalue_precision": {
+ "type": "TextControl",
+ "label": "p-value precision",
+ "default": 6,
+ "description": "Number of decimal places with which to display p-values"
+ },
+ "liftvalue_precision": {
+ "type": "TextControl",
+ "label": "Lift percent precision",
+ "default": 4,
+ "description": "Number of decimal places with which to display lift values"
+ },
+ "column_collection": {
+ "type": "CollectionControl",
+ "label": "Time Series Columns",
+ "validators": [
+ null
+ ],
+ "controlName": "TimeSeriesColumnControl"
+ },
+ "time_series_option": {
+ "type": "SelectControl",
+ "label": "Options",
+ "validators": [
+ null
+ ],
+ "default": "not_time",
+ "valueKey": "value",
+ "options": [
+ {
+ "label": "Not Time Series",
+ "value": "not_time",
+ "description": "Ignore time"
+ },
+ {
+ "label": "Time Series",
+ "value": "time_series",
+ "description": "Standard time series"
+ },
+ {
+ "label": "Aggregate Mean",
+ "value": "agg_mean",
+ "description": "Mean of values over specified period"
+ },
+ {
+ "label": "Aggregate Sum",
+ "value": "agg_sum",
+ "description": "Sum of values over specified period"
+ },
+ {
+ "label": "Difference",
+ "value": "point_diff",
+ "description": "Metric change in value from `since` to `until`"
+ },
+ {
+ "label": "Percent Change",
+ "value": "point_percent",
+ "description": "Metric percent change in value from `since` to `until`"
+ },
+ {
+ "label": "Factor",
+ "value": "point_factor",
+ "description": "Metric factor change from `since` to `until`"
+ },
+ {
+ "label": "Advanced Analytics",
+ "value": "adv_anal",
+ "description": "Use the Advanced Analytics options below"
+ }
+ ],
+ "description": "Settings for time series"
+ },
+ "equal_date_size": {
+ "type": "CheckboxControl",
+ "label": "Equal Date Sizes",
+ "default": true,
+ "renderTrigger": true,
+ "description": "Check to force date partitions to have the same height"
+ },
+ "partition_limit": {
+ "type": "TextControl",
+ "label": "Partition Limit",
+ "isInt": true,
+ "default": "5",
+ "description": "The maximum number of subdivisions of each group; lower values are pruned first"
+ },
+ "partition_threshold": {
+ "type": "TextControl",
+ "label": "Partition Threshold",
+ "isFloat": true,
+ "default": "0.05",
+ "description": "Partitions whose height to parent height proportions are below this value are pruned"
}
}
}
\ No newline at end of file
diff --git a/superset/assets/javascripts/chart/Chart.jsx b/superset/assets/javascripts/chart/Chart.jsx
index a4e3dd2abddfe..3dd0355ab08fc 100644
--- a/superset/assets/javascripts/chart/Chart.jsx
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -10,6 +10,7 @@ import StackTraceMessage from '../components/StackTraceMessage';
import visMap from '../../visualizations/main';
const propTypes = {
+ annotationData: PropTypes.object,
actions: PropTypes.object,
chartKey: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
@@ -47,8 +48,8 @@ const defaultProps = {
class Chart extends React.PureComponent {
constructor(props) {
super(props);
-
// these properties are used by visualizations
+ this.annotationData = props.annotationData;
this.containerId = props.containerId;
this.selector = `#${this.containerId}`;
this.formData = props.formData;
@@ -71,6 +72,7 @@ class Chart extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
+ this.annotationData = nextProps.annotationData;
this.containerId = nextProps.containerId;
this.selector = `#${this.containerId}`;
this.formData = nextProps.formData;
@@ -82,6 +84,7 @@ class Chart extends React.PureComponent {
this.props.queryResponse &&
this.props.chartStatus === 'success' &&
!this.props.queryResponse.error && (
+ prevProps.annotationData !== this.props.annotationData ||
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height ||
prevProps.width !== this.props.width ||
diff --git a/superset/assets/javascripts/chart/ChartContainer.jsx b/superset/assets/javascripts/chart/ChartContainer.jsx
index d517677ec4d41..b731412fc5ff7 100644
--- a/superset/assets/javascripts/chart/ChartContainer.jsx
+++ b/superset/assets/javascripts/chart/ChartContainer.jsx
@@ -7,6 +7,7 @@ import Chart from './Chart';
function mapStateToProps({ charts }, ownProps) {
const chart = charts[ownProps.chartKey];
return {
+ annotationData: chart.annotationData,
chartAlert: chart.chartAlert,
chartStatus: chart.chartStatus,
chartUpdateEndTime: chart.chartUpdateEndTime,
diff --git a/superset/assets/javascripts/chart/chartAction.js b/superset/assets/javascripts/chart/chartAction.js
index 17205a41a3cc6..a6341ddb6a7ba 100644
--- a/superset/assets/javascripts/chart/chartAction.js
+++ b/superset/assets/javascripts/chart/chartAction.js
@@ -1,5 +1,5 @@
-import { getExploreUrl } from '../explore/exploreUtils';
-import { t } from '../locales';
+import { getExploreUrl, getAnnotationJsonUrl } from '../explore/exploreUtils';
+import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
const $ = window.$ = require('jquery');
@@ -41,6 +41,57 @@ export function removeChart(key) {
return { type: REMOVE_CHART, key };
}
+export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
+export function annotationQuerySuccess(annotation, queryResponse, key) {
+ return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
+}
+
+export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
+export function annotationQueryStarted(annotation, queryRequest, key) {
+ return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
+}
+
+export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
+export function annotationQueryFailed(annotation, queryResponse, key) {
+ return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
+}
+
+export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) {
+ return function (dispatch, getState) {
+ const sliceKey = key || Object.keys(getState().charts)[0];
+ const fd = formData || getState().charts[sliceKey].latestQueryFormData;
+
+ if (!requiresQuery(annotation.sourceType)) {
+ return Promise.resolve();
+ }
+
+ const sliceFormData = Object.keys(annotation.overrides)
+ .reduce((d, k) => ({
+ ...d,
+ [k]: annotation.overrides[k] || fd[k],
+ }), {});
+ const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
+ const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
+ const queryRequest = $.ajax({
+ url,
+ dataType: 'json',
+ timeout: timeout * 1000,
+ });
+ dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
+ return queryRequest
+ .then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
+ .catch((err) => {
+ if (err.statusText === 'timeout') {
+ dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
+ } else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
+ dispatch(annotationQuerySuccess(annotation, err, sliceKey));
+ } else if (err.statusText !== 'abort') {
+ dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
+ }
+ });
+ };
+}
+
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
export function triggerQuery(value = true, key) {
return { type: TRIGGER_QUERY, value, key };
@@ -60,32 +111,23 @@ export function runQuery(formData, force = false, timeout = 60, key) {
url,
dataType: 'json',
timeout: timeout * 1000,
- success: (queryResponse =>
- dispatch(chartUpdateSucceeded(queryResponse, key))
- ),
- error: ((xhr) => {
- if (xhr.statusText === 'timeout') {
- dispatch(chartUpdateTimeout(xhr.statusText, timeout, key));
- } else {
- let error = '';
- if (!xhr.responseText) {
- const status = xhr.status;
- if (status === 0) {
- // This may happen when the worker in gunicorn times out
- error += (
- t('The server could not be reached. You may want to ' +
- 'verify your connection and try again.'));
- } else {
- error += (t('An unknown error occurred. (Status: %s )', status));
- }
- }
- const errorResponse = Object.assign({}, xhr.responseJSON, error);
- dispatch(chartUpdateFailed(errorResponse, key));
- }
- }),
});
- dispatch(chartUpdateStarted(queryRequest, key));
- dispatch(triggerQuery(false, key));
+ const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, key)))
+ .then(() => queryRequest)
+ .then(queryResponse => dispatch(chartUpdateSucceeded(queryResponse, key)))
+ .catch((err) => {
+ if (err.statusText === 'timeout') {
+ dispatch(chartUpdateTimeout(err.statusText, timeout, key));
+ } else if (err.statusText !== 'abort') {
+ dispatch(chartUpdateFailed(err.responseJSON, key));
+ }
+ });
+ const annotationLayers = formData.annotation_layers || [];
+ return Promise.all([
+ queryPromise,
+ dispatch(triggerQuery(false, key)),
+ ...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))),
+ ]);
};
}
diff --git a/superset/assets/javascripts/chart/chartReducer.js b/superset/assets/javascripts/chart/chartReducer.js
index ade8c5bf68f03..3cc9e5e6dfc3f 100644
--- a/superset/assets/javascripts/chart/chartReducer.js
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -65,12 +65,12 @@ export default function chartReducer(charts = {}, action) {
return { ...state,
chartStatus: 'failed',
chartAlert: (
- `${t('Query timeout')} - ` +
- t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
- t('Perhaps your data has grown, your database is under unusual load, ' +
- 'or you are simply querying a data source that is too large ' +
- 'to be processed within the timeout range. ' +
- 'If that is the case, we recommend that you summarize your data further.')),
+ `${t('Query timeout')} - ` +
+ t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
+ t('Perhaps your data has grown, your database is under unusual load, ' +
+ 'or you are simply querying a data source that is too large ' +
+ 'to be processed within the timeout range. ' +
+ 'If that is the case, we recommend that you summarize your data further.')),
};
},
[actions.CHART_UPDATE_FAILED](state) {
@@ -87,6 +87,53 @@ export default function chartReducer(charts = {}, action) {
[actions.RENDER_TRIGGERED](state) {
return { ...state, lastRendered: action.value };
},
+ [actions.ANNOTATION_QUERY_STARTED](state) {
+ if (state.annotationQuery &&
+ state.annotationQuery[action.annotation.name]) {
+ state.annotationQuery[action.annotation.name].abort();
+ }
+ const annotationQuery = {
+ ...state.annotationQuery,
+ [action.annotation.name]: action.queryRequest,
+ };
+ return {
+ ...state,
+ annotationQuery,
+ };
+ },
+ [actions.ANNOTATION_QUERY_SUCCESS](state) {
+ const annotationData = {
+ ...state.annotationData,
+ [action.annotation.name]: action.queryResponse.data,
+ };
+ const annotationError = { ...state.annotationError };
+ delete annotationError[action.annotation.name];
+ const annotationQuery = { ...state.annotationQuery };
+ delete annotationQuery[action.annotation.name];
+ return {
+ ...state,
+ annotationData,
+ annotationError,
+ annotationQuery,
+ };
+ },
+ [actions.ANNOTATION_QUERY_FAILED](state) {
+ const annotationData = { ...state.annotationData };
+ delete annotationData[action.annotation.name];
+ const annotationError = {
+ ...state.annotationError,
+ [action.annotation.name]: action.queryResponse ?
+ action.queryResponse.error : t('Network error.'),
+ };
+ const annotationQuery = { ...state.annotationQuery };
+ delete annotationQuery[action.annotation.name];
+ return {
+ ...state,
+ annotationData,
+ annotationError,
+ annotationQuery,
+ };
+ },
};
/* eslint-disable no-param-reassign */
diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx b/superset/assets/javascripts/dashboard/components/GridCell.jsx
index 854aea01fe74a..4f7213d3b08cc 100644
--- a/superset/assets/javascripts/dashboard/components/GridCell.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx
@@ -31,6 +31,7 @@ const propTypes = {
clearFilter: PropTypes.func,
removeFilter: PropTypes.func,
editMode: PropTypes.bool,
+ annotationQuery: PropTypes.object,
};
const defaultProps = {
@@ -84,7 +85,7 @@ class GridCell extends React.PureComponent {
const {
exploreChartUrl, exportCSVUrl, isExpanded, isLoading, isCached, cachedDttm,
removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
- chartKey, slice, datasource, formData, timeout,
+ chartKey, slice, datasource, formData, timeout, annotationQuery,
} = this.props;
return (
);
});
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
index 36107fedf1e0d..1f4b475d8807f 100644
--- a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
@@ -19,6 +19,8 @@ const propTypes = {
toggleExpandSlice: PropTypes.func,
forceRefresh: PropTypes.func,
editMode: PropTypes.bool,
+ annotationQuery: PropTypes.object,
+ annotationError: PropTypes.object,
};
const defaultProps = {
@@ -50,6 +52,8 @@ class SliceHeader extends React.PureComponent {
const refreshTooltip = isCached ?
t('Served from data cached %s . Click to force refresh.', cachedWhen) :
t('Force refresh data');
+ const annoationsLoading = t('Annotation layers are still loading.');
+ const annoationsError = t('One ore more annotation layers failed loading.');
return (
@@ -61,6 +65,24 @@ class SliceHeader extends React.PureComponent {
onSaveTitle={this.onSaveTitle}
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
/>
+ {!!Object.values(this.props.annotationQuery || {}).length &&
+
+
+
+ }
+ {!!Object.values(this.props.annotationError || {}).length &&
+
+
+
+ }
diff --git a/superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx b/superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx
new file mode 100644
index 0000000000000..aa34fb04f7db1
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx
@@ -0,0 +1,602 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { CompactPicker } from 'react-color';
+import { Button } from 'react-bootstrap';
+
+import $ from 'jquery';
+import mathjs from 'mathjs';
+
+import SelectControl from './SelectControl';
+import TextControl from './TextControl';
+import CheckboxControl from './CheckboxControl';
+
+import AnnotationTypes, {
+ DEFAULT_ANNOTATION_TYPE,
+ ANNOTATION_SOURCE_TYPES,
+ getAnnotationSourceTypeLabels,
+ getAnnotationTypeLabel,
+ getSupportedSourceTypes,
+ getSupportedAnnotationTypes,
+ requiresQuery,
+} from '../../../modules/AnnotationTypes';
+
+import { ALL_COLOR_SCHEMES } from '../../../modules/colors';
+import PopoverSection from '../../../components/PopoverSection';
+import ControlHeader from '../ControlHeader';
+import { nonEmpty } from '../../validators';
+import vizTypes from '../../stores/visTypes';
+
+const AUTOMATIC_COLOR = '';
+
+const propTypes = {
+ name: PropTypes.string,
+ annotationType: PropTypes.string,
+ sourceType: PropTypes.string,
+ color: PropTypes.string,
+ opacity: PropTypes.string,
+ style: PropTypes.string,
+ width: PropTypes.number,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ overrides: PropTypes.object,
+ show: PropTypes.bool,
+ titleColumn: PropTypes.string,
+ descriptionColumns: PropTypes.arrayOf(PropTypes.string),
+ timeColumn: PropTypes.string,
+ intervalEndColumn: PropTypes.string,
+ vizType: PropTypes.string,
+
+ error: PropTypes.string,
+ colorScheme: PropTypes.string,
+
+ addAnnotationLayer: PropTypes.func,
+ removeAnnotationLayer: PropTypes.func,
+ close: PropTypes.func,
+};
+
+const defaultProps = {
+ name: '',
+ annotationType: DEFAULT_ANNOTATION_TYPE,
+ sourceType: '',
+ color: AUTOMATIC_COLOR,
+ opacity: '',
+ style: 'solid',
+ width: 1,
+ overrides: {},
+ colorScheme: 'd3Category10',
+ show: true,
+ titleColumn: '',
+ descriptionColumns: [],
+ timeColumn: '',
+ intervalEndColumn: '',
+
+ addAnnotationLayer: () => {},
+ removeAnnotationLayer: () => {},
+ close: () => {},
+};
+
+export default class AnnotationLayer extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ const { name, annotationType, sourceType,
+ color, opacity, style, width, value,
+ overrides, show, titleColumn, descriptionColumns,
+ timeColumn, intervalEndColumn } = props;
+ this.state = {
+ // base
+ name,
+ oldName: !this.props.name ? null : name,
+ annotationType,
+ sourceType,
+ value,
+ overrides,
+ show,
+ // slice
+ titleColumn,
+ descriptionColumns,
+ timeColumn,
+ intervalEndColumn,
+ // display
+ color: color || AUTOMATIC_COLOR,
+ opacity,
+ style,
+ width,
+ // refData
+ isNew: !this.props.name,
+ isLoadingOptions: true,
+ valueOptions: [],
+ };
+ this.submitAnnotation = this.submitAnnotation.bind(this);
+ this.deleteAnnotation = this.deleteAnnotation.bind(this);
+ this.applyAnnotation = this.applyAnnotation.bind(this);
+ this.fetchOptions = this.fetchOptions.bind(this);
+ this.handleAnnotationType = this.handleAnnotationType.bind(this);
+ this.handleAnnotationSourceType =
+ this.handleAnnotationSourceType.bind(this);
+ this.handleValue = this.handleValue.bind(this);
+ this.isValidForm = this.isValidForm.bind(this);
+ }
+
+ componentDidMount() {
+ const { annotationType, sourceType, isLoadingOptions } = this.state;
+ this.fetchOptions(annotationType, sourceType, isLoadingOptions);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.sourceType !== this.state.sourceType) {
+ this.fetchOptions(this.state.annotationType, this.state.sourceType, true);
+ }
+ }
+
+ isValidFormula(value, annotationType) {
+ if (annotationType === AnnotationTypes.FORMULA) {
+ try {
+ mathjs.parse(value).compile().eval({ x: 0 });
+ } catch (err) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ isValidForm() {
+ const {
+ name, annotationType, sourceType,
+ value, timeColumn, intervalEndColumn,
+ } = this.state;
+ const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
+ if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) {
+ if (annotationType === AnnotationTypes.EVENT) {
+ errors.push(nonEmpty(timeColumn));
+ }
+ if (annotationType === AnnotationTypes.INTERVAL) {
+ errors.push(nonEmpty(timeColumn));
+ errors.push(nonEmpty(intervalEndColumn));
+ }
+ }
+ errors.push(this.isValidFormula(value, annotationType));
+ return !errors.filter(x => x).length;
+ }
+
+
+ handleAnnotationType(annotationType) {
+ this.setState({
+ annotationType,
+ sourceType: null,
+ validationErrors: {},
+ value: null,
+ });
+ }
+
+ handleAnnotationSourceType(sourceType) {
+ this.setState({
+ sourceType,
+ isLoadingOptions: true,
+ validationErrors: {},
+ value: null,
+ });
+ }
+
+ handleValue(value) {
+ this.setState({
+ value,
+ descriptionColumns: null,
+ intervalEndColumn: null,
+ timeColumn: null,
+ titleColumn: null,
+ overrides: { since: null, until: null },
+ });
+ }
+
+ fetchOptions(annotationType, sourceType, isLoadingOptions) {
+ if (isLoadingOptions === true) {
+ if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
+ $.ajax({
+ type: 'GET',
+ url: '/annotationlayermodelview/api/read?',
+ }).then((data) => {
+ const layers = data ? data.result.map(layer => ({
+ value: layer.id,
+ label: layer.name,
+ })) : [];
+ this.setState({
+ isLoadingOptions: false,
+ valueOptions: layers,
+ });
+ });
+ } else if (requiresQuery(sourceType)) {
+ $.ajax({
+ type: 'GET',
+ url: '/superset/user_slices',
+ }).then(data =>
+ this.setState({
+ isLoadingOptions: false,
+ valueOptions: data.filter(
+ x => getSupportedSourceTypes(annotationType)
+ .find(v => v === x.viz_type))
+ .map(x => ({ value: x.id, label: x.title, slice: x }),
+ ),
+ }),
+ );
+ } else {
+ this.setState({
+ isLoadingOptions: false,
+ valueOptions: [],
+ });
+ }
+ }
+ }
+
+ deleteAnnotation() {
+ this.props.close();
+ if (!this.state.isNew) {
+ this.props.removeAnnotationLayer(this.state);
+ }
+ }
+
+ applyAnnotation() {
+ if (this.state.name.length) {
+ const annotation = { ...this.state };
+ annotation.color = annotation.color === AUTOMATIC_COLOR ? null : annotation.color;
+ delete annotation.isNew;
+ delete annotation.valueOptions;
+ delete annotation.isLoadingOptions;
+ this.props.addAnnotationLayer(annotation);
+ this.setState({ isNew: false, oldName: this.state.name });
+ }
+ }
+
+ submitAnnotation() {
+ this.applyAnnotation();
+ this.props.close();
+ }
+
+ renderValueConfiguration() {
+ const { annotationType, sourceType, value,
+ valueOptions, isLoadingOptions } = this.state;
+ let label = '';
+ let description = '';
+ if (requiresQuery(sourceType)) {
+ if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
+ label = 'Annotation Layer';
+ description = 'Select the Annotation Layer you would like to use.';
+ } else {
+ label = 'Slice';
+ description = `Use a pre defined Superset Slice as a source for annotations and overlays.
+ 'your Slice must be one of these visualization types:
+ '[${getSupportedSourceTypes(sourceType)
+ .map(x => vizTypes[x].label).join(', ')}]'`;
+ }
+ } else if (annotationType === AnnotationTypes.FORMULA) {
+ label = 'Formula';
+ description = `Expects a formula with depending time parameter 'x'
+ in milliseconds since epoch. mathjs is used to evaluate the formulas.
+ Example: '2x+5'`;
+ }
+ if (requiresQuery(sourceType)) {
+ return (
+
+ );
+ } if (annotationType === AnnotationTypes.FORMULA) {
+ return (
+
+ );
+ }
+ return '';
+ }
+
+ renderSliceConfiguration() {
+ const { annotationType, sourceType, value, valueOptions, overrides, titleColumn,
+ timeColumn, intervalEndColumn, descriptionColumns } = this.state;
+ const slice = (valueOptions.find(x => x.value === value) || {}).slice;
+ if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) {
+ const columns = (slice.data.groupby || []).concat(
+ (slice.data.all_columns || [])).map(x => ({ value: x, label: x }));
+ const timeColumnOptions = slice.data.include_time ?
+ [{ value: '__timestamp', label: '__timestamp' }].concat(columns) : columns;
+ return (
+
+
{
+ }}
+ title="Annotation Slice Configuration"
+ info={
+ `This section allows you to configure how to use the slice
+ to generate annotations.`
+ }
+ >
+ {
+ (
+ annotationType === AnnotationTypes.EVENT ||
+ annotationType === AnnotationTypes.INTERVAL
+ ) &&
+ this.setState({ timeColumn: v })}
+ />
+ }
+ {
+ annotationType === AnnotationTypes.INTERVAL &&
+ this.setState({ intervalEndColumn: v })}
+ />
+ }
+ this.setState({ titleColumn: v })}
+ />
+ {
+ annotationType !== AnnotationTypes.TIME_SERIES &&
+ this.setState({ descriptionColumns: v })}
+ />
+ }
+
+ x === 'since')}
+ onChange={(v) => {
+ delete overrides.since;
+ if (v) {
+ this.setState({ overrides: { ...overrides, since: null } });
+ } else {
+ this.setState({ overrides: { ...overrides } });
+ }
+ }}
+ />
+ x === 'until')}
+ onChange={(v) => {
+ delete overrides.until;
+ if (v) {
+ this.setState({ overrides: { ...overrides, until: null } });
+ } else {
+ this.setState({ overrides: { ...overrides } });
+ }
+ }}
+ />
+ this.setState({ overrides: { ...overrides, time_shift: v } })}
+ />
+
+
+
+ );
+ }
+ return ('');
+ }
+
+ renderDisplayConfiguration() {
+ const { color, opacity, style, width } = this.state;
+ const colorScheme = [...ALL_COLOR_SCHEMES[this.props.colorScheme]];
+ if (color && color !== AUTOMATIC_COLOR &&
+ !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
+ colorScheme.push(color);
+ }
+ return (
+
{}}
+ title="Display configuration"
+ info="Configure your how you overlay is displayed here."
+ >
+ this.setState({ style: v })}
+ />
+ this.setState({ opacity: v })}
+ />
+
+
+
+ this.setState({ color: v.hex })}
+ />
+
+
+
+ this.setState({ width: v })}
+ />
+
+ );
+ }
+
+ render() {
+ const { isNew, name, annotationType,
+ sourceType, show } = this.state;
+ const isValid = this.isValidForm();
+ return (
+
+ {
+ this.props.error &&
+
+ ERROR: {this.props.error}
+
+ }
+
+
+
{}}
+ title="Layer Configuration"
+ info="Configure the basics of your Annotation Layer."
+ >
+ this.setState({ name: v })}
+ validationErrors={!name ? ['Mandatory'] : []}
+ />
+ this.setState({ show: !v })}
+ />
+ ({ value: x, label: getAnnotationTypeLabel(x) }))}
+ value={annotationType}
+ onChange={this.handleAnnotationType}
+ />
+ {!!getSupportedSourceTypes(annotationType).length &&
+ ({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
+ value={sourceType}
+ onChange={this.handleAnnotationSourceType}
+ />
+ }
+ { this.renderValueConfiguration() }
+
+
+ { this.renderSliceConfiguration() }
+ { this.renderDisplayConfiguration() }
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+AnnotationLayer.propTypes = propTypes;
+AnnotationLayer.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/AnnotationLayerControl.jsx b/superset/assets/javascripts/explore/components/controls/AnnotationLayerControl.jsx
new file mode 100644
index 0000000000000..3e4cd24e31c4d
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/AnnotationLayerControl.jsx
@@ -0,0 +1,177 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { OverlayTrigger, Popover, ListGroup, ListGroupItem } from 'react-bootstrap';
+import { connect } from 'react-redux';
+import { getChartKey } from '../../exploreUtils';
+import { runAnnotationQuery } from '../../../chart/chartAction';
+import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
+
+
+import AnnotationLayer from './AnnotationLayer';
+import { t } from '../../../locales';
+
+
+const propTypes = {
+ colorScheme: PropTypes.string.isRequired,
+ annotationError: PropTypes.object,
+ annotationQuery: PropTypes.object,
+ vizType: PropTypes.string,
+
+ validationErrors: PropTypes.array,
+ name: PropTypes.string.isRequired,
+ actions: PropTypes.object,
+ value: PropTypes.arrayOf(PropTypes.object),
+ onChange: PropTypes.func,
+ refreshAnnotationData: PropTypes.func,
+};
+
+const defaultProps = {
+ vizType: '',
+ value: [],
+ annotationError: {},
+ annotationQuery: {},
+ onChange: () => {},
+};
+
+class AnnotationLayerControl extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.addAnnotationLayer = this.addAnnotationLayer.bind(this);
+ this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { name, annotationError, validationErrors, value } = nextProps;
+ if (Object.keys(annotationError).length && !validationErrors.length) {
+ this.props.actions.setControlValue(name, value, Object.keys(annotationError));
+ }
+ if (!Object.keys(annotationError).length && validationErrors.length) {
+ this.props.actions.setControlValue(name, value, []);
+ }
+ }
+
+ addAnnotationLayer(annotationLayer) {
+ const annotation = annotationLayer;
+ let annotations = this.props.value.slice();
+ const i = annotations.findIndex(x => x.name === (annotation.oldName || annotation.name));
+ delete annotation.oldName;
+ if (i > -1) {
+ annotations[i] = annotation;
+ } else {
+ annotations = annotations.concat(annotation);
+ }
+ this.props.refreshAnnotationData(annotation);
+ this.props.onChange(annotations);
+ }
+
+ removeAnnotationLayer(annotation) {
+ const annotations = this.props.value.slice()
+ .filter(x => x.name !== annotation.oldName);
+ this.props.onChange(annotations);
+ }
+
+ renderPopover(parent, annotation, error) {
+ const id = !annotation ? '_new' : annotation.name;
+ return (
+
+ this.refs[parent].hide()}
+ />
+
+ );
+ }
+
+ renderInfo(anno) {
+ const { annotationError, annotationQuery } = this.props;
+ if (annotationQuery[anno.name]) {
+ return (
+
+ );
+ }
+ if (annotationError[anno.name]) {
+ return (
+
+ );
+ }
+ if (!anno.show) {
+ return
Hidden ;
+ }
+ return '';
+ }
+
+ render() {
+ const annotations = this.props.value.map((anno, i) => (
+
+
+ {anno.name}
+
+ {this.renderInfo(anno)}
+
+
+
+ ));
+ return (
+
+
+ {annotations}
+
+
+ {t('Add Annotation Layer')}
+
+
+
+
+ );
+ }
+}
+
+AnnotationLayerControl.propTypes = propTypes;
+AnnotationLayerControl.defaultProps = defaultProps;
+
+// Tried to hook this up through stores/control.jsx instead of using redux
+// directly, could not figure out how to get access to the color_scheme
+function mapStateToProps({ charts, explore }) {
+ const chartKey = getChartKey(explore);
+ return {
+ colorScheme: (explore.controls || {}).color_scheme.value,
+ annotationError: charts[chartKey].annotationError,
+ annotationQuery: charts[chartKey].annotationQuery,
+ vizType: explore.controls.viz_type.value,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ refreshAnnotationData: annotationLayer => dispatch(runAnnotationQuery(annotationLayer)),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AnnotationLayerControl);
diff --git a/superset/assets/javascripts/explore/components/controls/index.js b/superset/assets/javascripts/explore/components/controls/index.js
index 94b8c66ef6061..35aaeeff2b774 100644
--- a/superset/assets/javascripts/explore/components/controls/index.js
+++ b/superset/assets/javascripts/explore/components/controls/index.js
@@ -1,3 +1,4 @@
+import AnnotationLayerControl from './AnnotationLayerControl';
import BoundsControl from './BoundsControl';
import CheckboxControl from './CheckboxControl';
import CollectionControl from './CollectionControl';
@@ -18,6 +19,7 @@ import ViewportControl from './ViewportControl';
import VizTypeControl from './VizTypeControl';
const controlMap = {
+ AnnotationLayerControl,
BoundsControl,
CheckboxControl,
CollectionControl,
diff --git a/superset/assets/javascripts/explore/exploreUtils.js b/superset/assets/javascripts/explore/exploreUtils.js
index 2356da5eb6256..8a01745d9a39f 100644
--- a/superset/assets/javascripts/explore/exploreUtils.js
+++ b/superset/assets/javascripts/explore/exploreUtils.js
@@ -1,13 +1,30 @@
/* eslint camelcase: 0 */
import URI from 'urijs';
+export function getChartKey(explore) {
+ const slice = explore.slice;
+ return slice ? ('slice_' + slice.slice_id) : 'slice';
+}
+
+export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
+ if (slice_id === null || slice_id === undefined) {
+ return null;
+ }
+ const uri = URI(window.location.search);
+ const endpoint = isNative ? 'annotation_json' : 'slice_json';
+ return uri.pathname(`/superset/${endpoint}/${slice_id}`)
+ .search({
+ form_data: JSON.stringify(form_data,
+ (key, value) => value === null ? undefined : value),
+ }).toString();
+}
+
export function getExploreUrl(form_data, endpointType = 'base', force = false,
curUrl = null, requestParams = {}) {
if (!form_data.datasource) {
return null;
}
-
// The search params from the window.location are carried through,
// but can be specified with curUrl (used for unit tests to spoof
// the window.location).
diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx
index 7e2ed35926431..d66ad52e79fab 100644
--- a/superset/assets/javascripts/explore/index.jsx
+++ b/superset/assets/javascripts/explore/index.jsx
@@ -7,6 +7,7 @@ import thunk from 'redux-thunk';
import { now } from '../modules/dates';
import { initEnhancer } from '../reduxUtils';
+import { getChartKey } from './exploreUtils';
import AlertsWrapper from '../components/AlertsWrapper';
import { getControlsState, getFormDataFromControls } from './stores/store';
import { initJQueryAjax } from '../modules/utils';
@@ -41,7 +42,7 @@ const sliceFormData = slice ?
getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
:
null;
-const chartKey = slice ? ('slice_' + slice.slice_id) : 'slice';
+const chartKey = getChartKey(bootstrappedState);
const initState = {
charts: {
[chartKey]: {
diff --git a/superset/assets/javascripts/explore/main.css b/superset/assets/javascripts/explore/main.css
index a6afe5eba935f..434e6f8acaf50 100644
--- a/superset/assets/javascripts/explore/main.css
+++ b/superset/assets/javascripts/explore/main.css
@@ -121,3 +121,4 @@
padding: 0;
background-color: transparent;
}
+
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index b18039f232aa7..eb4efba5ada77 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -135,7 +135,6 @@ export const controls = {
choices: (state.datasource) ? state.datasource.order_by_choices : [],
}),
},
-
color_picker: {
label: t('Fixed Color'),
description: t('Use this to define a static color for all circles'),
@@ -144,23 +143,6 @@ export const controls = {
renderTrigger: true,
},
- annotation_layers: {
- type: 'SelectAsyncControl',
- multi: true,
- label: t('Annotation Layers'),
- default: [],
- description: t('Annotation layers to overlay on the visualization'),
- dataEndpoint: '/annotationlayermodelview/api/read?',
- placeholder: t('Select a annotation layer'),
- onAsyncErrorMessage: t('Error while fetching annotation layers'),
- mutator: (data) => {
- if (!data || !data.result) {
- return [];
- }
- return data.result.map(layer => ({ value: layer.id, label: layer.name }));
- },
- },
-
metric: {
type: 'SelectControl',
label: t('Metric'),
@@ -1561,6 +1543,14 @@ export const controls = {
}),
},
+ annotation_layers: {
+ type: 'AnnotationLayerControl',
+ label: '',
+ default: [],
+ description: 'Annotation Layers',
+ renderTrigger: true,
+ },
+
having_filters: {
type: 'FilterControl',
label: '',
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index a243cbfd2d9c7..ef9dc4112cc51 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -45,7 +45,7 @@ export const sections = {
description: t('This section exposes ways to include snippets of SQL in your query'),
},
annotations: {
- label: t('Annotations'),
+ label: t('Annotations and Layers'),
expanded: true,
controlSetRows: [
['annotation_layers'],
diff --git a/superset/assets/javascripts/modules/AnnotationTypes.js b/superset/assets/javascripts/modules/AnnotationTypes.js
new file mode 100644
index 0000000000000..28684bbcb6884
--- /dev/null
+++ b/superset/assets/javascripts/modules/AnnotationTypes.js
@@ -0,0 +1,94 @@
+import { VIZ_TYPES } from '../../visualizations/main';
+import vizTypes from '../explore/stores/visTypes';
+
+export const ANNOTATION_TYPES = {
+ FORMULA: 'FORMULA',
+ EVENT: 'EVENT',
+ INTERVAL: 'INTERVAL',
+ TIME_SERIES: 'TIME_SERIES',
+};
+
+export const ANNOTATION_TYPE_LABELS = {
+ FORMULA: 'Formula ',
+ EVENT: 'Event',
+ INTERVAL: 'Interval',
+ TIME_SERIES: 'Time Series',
+};
+
+export function getAnnotationTypeLabel(annotationType) {
+ return ANNOTATION_TYPE_LABELS[annotationType];
+}
+
+export const DEFAULT_ANNOTATION_TYPE = ANNOTATION_TYPES.FORMULA;
+
+export const ANNOTATION_SOURCE_TYPES = {
+ NATIVE: 'NATIVE',
+ ...VIZ_TYPES,
+};
+
+export function getAnnotationSourceTypeLabels(sourceType) {
+ return ANNOTATION_SOURCE_TYPES.NATIVE === sourceType ? 'Superset annotation' :
+ vizTypes[sourceType].label;
+}
+
+export function requiresQuery(annotationSourceType) {
+ return !!annotationSourceType;
+}
+
+// Map annotation type to annotation source type
+const SUPPORTED_SOURCE_TYPE_MAP = {
+ [ANNOTATION_TYPES.EVENT]: [
+ ANNOTATION_SOURCE_TYPES.NATIVE,
+ ANNOTATION_SOURCE_TYPES.table,
+ ],
+ [ANNOTATION_TYPES.INTERVAL]: [
+ ANNOTATION_SOURCE_TYPES.NATIVE,
+ ANNOTATION_SOURCE_TYPES.table,
+ ],
+ [ANNOTATION_TYPES.TIME_SERIES]: [
+ ANNOTATION_SOURCE_TYPES.line,
+ ],
+};
+
+export function getSupportedSourceTypes(annotationType) {
+ return SUPPORTED_SOURCE_TYPE_MAP[annotationType] || [];
+}
+
+// Map from viz type to supported annotation
+const SUPPORTED_ANNOTATIONS = {
+ [VIZ_TYPES.line]: [
+ ANNOTATION_TYPES.TIME_SERIES,
+ ANNOTATION_TYPES.INTERVAL,
+ ANNOTATION_TYPES.EVENT,
+ ANNOTATION_TYPES.FORMULA,
+ ],
+ [VIZ_TYPES.bar]: [
+ ANNOTATION_TYPES.INTERVAL,
+ ANNOTATION_TYPES.EVENT,
+ ],
+ [VIZ_TYPES.area]: [
+ ANNOTATION_TYPES.INTERVAL,
+ ANNOTATION_TYPES.EVENT,
+ ],
+};
+
+export function getSupportedAnnotationTypes(vizType) {
+ return SUPPORTED_ANNOTATIONS[vizType] || [];
+}
+
+const NATIVE_COLUMN_NAMES = {
+ timeColumn: 'start_dttm',
+ intervalEndColumn: 'end_dttm',
+ titleColumn: 'short_descr',
+ descriptionColumns: ['long_descr'],
+};
+
+export function applyNativeColumns(annotation) {
+ if (annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
+ return { ...annotation, ...NATIVE_COLUMN_NAMES };
+ }
+ return annotation;
+}
+
+export default ANNOTATION_TYPES;
+
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 9d8b01a811355..905e770294b06 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -15,6 +15,7 @@
"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-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx .",
"sync-backend": "babel-node --presets env javascripts/syncBackend.js"
},
"repository": {
@@ -64,6 +65,7 @@
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
"luma.gl": "^4.0.5",
+ "mathjs": "^3.16.3",
"moment": "2.18.1",
"mustache": "^2.2.1",
"nvd3": "1.8.6",
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index f88de8f955310..4caeccd3e2b90 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -23,14 +23,16 @@ describe('chart actions', () => {
});
it('should handle query timeout', () => {
- ajaxStub.yieldsTo('error', { statusText: 'timeout' });
+ ajaxStub.rejects({ statusText: 'timeout' });
request = actions.runQuery({});
- request(dispatch, sinon.stub().returns({
+ const promise = request(dispatch, sinon.stub().returns({
explore: {
controls: [],
},
}));
- expect(dispatch.callCount).to.equal(3);
- expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT);
+ promise.then(() => {
+ expect(dispatch.callCount).to.equal(3);
+ expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT);
+ });
});
});
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.css
index b6d86abbd081e..c1f08a7e38b74 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.css
@@ -146,3 +146,11 @@ div.widget:hover .chart-controls {
.slice_container .alert {
margin: 10px;
}
+
+i.danger {
+ color: red;
+}
+
+i.warning {
+ color: orange;
+}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index d86ad74b8218b..ae0be2cd64ee5 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -376,7 +376,7 @@ iframe {
padding-bottom: 10px;
}
.popover {
- max-width: 500px !important;
+ max-width: 500px;
}
.float-left {
float: left;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 9976614048ada..fdcb7b1eab2e0 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -1,45 +1,90 @@
/* eslint-disable global-require */
+
+// You ***should*** use these to reference viz_types in code
+export const VIZ_TYPES = {
+ area: 'area',
+ bar: 'bar',
+ big_number: 'big_number',
+ big_number_total: 'big_number_total',
+ box_plot: 'box_plot',
+ bubble: 'bubble',
+ bullet: 'bullet',
+ cal_heatmap: 'cal_heatmap',
+ compare: 'compare',
+ directed_force: 'directed_force',
+ chord: 'chord',
+ dist_bar: 'dist_bar',
+ filter_box: 'filter_box',
+ heatmap: 'heatmap',
+ histogram: 'histogram',
+ horizon: 'horizon',
+ iframe: 'iframe',
+ line: 'line',
+ mapbox: 'mapbox',
+ markup: 'markup',
+ para: 'para',
+ pie: 'pie',
+ pivot_table: 'pivot_table',
+ sankey: 'sankey',
+ separator: 'separator',
+ sunburst: 'sunburst',
+ table: 'table',
+ time_table: 'time_table',
+ treemap: 'treemap',
+ country_map: 'country_map',
+ word_cloud: 'word_cloud',
+ world_map: 'world_map',
+ dual_line: 'dual_line',
+ event_flow: 'event_flow',
+ paired_ttest: 'paired_ttest',
+ partition: 'partition',
+ deck_scatter: 'deck_scatter',
+ deck_screengrid: 'deck_screengrid',
+ deck_grid: 'deck_grid',
+ deck_hex: 'deck_hex',
+};
+
const vizMap = {
- area: require('./nvd3_vis.js'),
- bar: require('./nvd3_vis.js'),
- big_number: require('./big_number.js'),
- big_number_total: require('./big_number.js'),
- box_plot: require('./nvd3_vis.js'),
- bubble: require('./nvd3_vis.js'),
- bullet: require('./nvd3_vis.js'),
- cal_heatmap: require('./cal_heatmap.js'),
- compare: require('./nvd3_vis.js'),
- directed_force: require('./directed_force.js'),
- chord: require('./chord.jsx'),
- dist_bar: require('./nvd3_vis.js'),
- filter_box: require('./filter_box.jsx'),
- heatmap: require('./heatmap.js'),
- histogram: require('./histogram.js'),
- horizon: require('./horizon.js'),
- iframe: require('./iframe.js'),
- line: require('./nvd3_vis.js'),
- time_pivot: require('./nvd3_vis.js'),
- mapbox: require('./mapbox.jsx'),
- markup: require('./markup.js'),
- para: require('./parallel_coordinates.js'),
- pie: require('./nvd3_vis.js'),
- pivot_table: require('./pivot_table.js'),
- sankey: require('./sankey.js'),
- separator: require('./markup.js'),
- sunburst: require('./sunburst.js'),
- table: require('./table.js'),
- time_table: require('./time_table.jsx'),
- treemap: require('./treemap.js'),
- country_map: require('./country_map.js'),
- word_cloud: require('./word_cloud.js'),
- world_map: require('./world_map.js'),
- dual_line: require('./nvd3_vis.js'),
- event_flow: require('./EventFlow.jsx'),
- paired_ttest: require('./paired_ttest.jsx'),
- partition: require('./partition.js'),
- deck_scatter: require('./deckgl/scatter.jsx'),
- deck_screengrid: require('./deckgl/screengrid.jsx'),
- deck_grid: require('./deckgl/grid.jsx'),
- deck_hex: require('./deckgl/hex.jsx'),
+ [VIZ_TYPES.area]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.bar]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.big_number]: require('./big_number.js'),
+ [VIZ_TYPES.big_number_total]: require('./big_number.js'),
+ [VIZ_TYPES.box_plot]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.bubble]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.bullet]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.cal_heatmap]: require('./cal_heatmap.js'),
+ [VIZ_TYPES.compare]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.directed_force]: require('./directed_force.js'),
+ [VIZ_TYPES.chord]: require('./chord.jsx'),
+ [VIZ_TYPES.dist_bar]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.filter_box]: require('./filter_box.jsx'),
+ [VIZ_TYPES.heatmap]: require('./heatmap.js'),
+ [VIZ_TYPES.histogram]: require('./histogram.js'),
+ [VIZ_TYPES.horizon]: require('./horizon.js'),
+ [VIZ_TYPES.iframe]: require('./iframe.js'),
+ [VIZ_TYPES.line]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.time_pivot]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.mapbox]: require('./mapbox.jsx'),
+ [VIZ_TYPES.markup]: require('./markup.js'),
+ [VIZ_TYPES.para]: require('./parallel_coordinates.js'),
+ [VIZ_TYPES.pie]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.pivot_table]: require('./pivot_table.js'),
+ [VIZ_TYPES.sankey]: require('./sankey.js'),
+ [VIZ_TYPES.separator]: require('./markup.js'),
+ [VIZ_TYPES.sunburst]: require('./sunburst.js'),
+ [VIZ_TYPES.table]: require('./table.js'),
+ [VIZ_TYPES.time_table]: require('./time_table.jsx'),
+ [VIZ_TYPES.treemap]: require('./treemap.js'),
+ [VIZ_TYPES.country_map]: require('./country_map.js'),
+ [VIZ_TYPES.word_cloud]: require('./word_cloud.js'),
+ [VIZ_TYPES.world_map]: require('./world_map.js'),
+ [VIZ_TYPES.dual_line]: require('./nvd3_vis.js'),
+ [VIZ_TYPES.event_flow]: require('./EventFlow.jsx'),
+ [VIZ_TYPES.paired_ttest]: require('./paired_ttest.jsx'),
+ [VIZ_TYPES.partition]: require('./partition.js'),
+ [VIZ_TYPES.deck_scatter]: require('./deckgl/scatter.jsx'),
+ [VIZ_TYPES.deck_screengrid]: require('./deckgl/screengrid.jsx'),
+ [VIZ_TYPES.deck_grid]: require('./deckgl/grid.jsx'),
+ [VIZ_TYPES.deck_hex]: require('./deckgl/hex.jsx'),
};
export default vizMap;
diff --git a/superset/assets/visualizations/nvd3_vis.css b/superset/assets/visualizations/nvd3_vis.css
index 1a5897f14d36f..fed0d013dc531 100644
--- a/superset/assets/visualizations/nvd3_vis.css
+++ b/superset/assets/visualizations/nvd3_vis.css
@@ -35,3 +35,32 @@ text.nv-axislabel {
text.nv-axislabel {
font-size: 14px !important;
}
+
+g.solid path, line.solid {
+ stroke-dasharray: unset;
+}
+
+g.dashed path, line.dashed {
+ stroke-dasharray: 5, 5;
+}
+
+g.longDashed path, line.longDashed {
+ stroke-dasharray: 10, 2;
+}
+
+g.dotted path, line.dotted {
+ stroke-dasharray: 1, 1;
+}
+
+g.opacityLow path, line.opacityLow {
+ stroke-opacity: .2
+}
+
+g.opacityMedium path, line.opacityMedium {
+ stroke-opacity: .5
+}
+
+g.opacityHigh path, line.opacityHigh {
+ stroke-opacity: .8
+}
+
diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js
index f1b6c11cde4a9..0e3d9e87a6d51 100644
--- a/superset/assets/visualizations/nvd3_vis.js
+++ b/superset/assets/visualizations/nvd3_vis.js
@@ -3,14 +3,19 @@ import $ from 'jquery';
import throttle from 'lodash.throttle';
import d3 from 'd3';
import nv from 'nvd3';
+import mathjs from 'mathjs';
import d3tip from 'd3-tip';
import { getColorFromScheme } from '../javascripts/modules/colors';
+import AnnotationTypes, {
+ applyNativeColumns,
+} from '../javascripts/modules/AnnotationTypes';
import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../javascripts/modules/utils';
// CSS
import '../node_modules/nvd3/build/nv.d3.min.css';
import './nvd3_vis.css';
+import { VIZ_TYPES } from './main';
const minBarWidth = 15;
const animationTime = 1000;
@@ -392,7 +397,7 @@ function nvd3Vis(slice, payload) {
return `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`;
});
} else if (vizType !== 'bullet') {
- chart.color(d => getColorFromScheme(d[colorKey], fd.color_scheme));
+ chart.color(d => d.color || getColorFromScheme(d[colorKey], fd.color_scheme));
}
if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
chart.useInteractiveGuideline(true);
@@ -526,82 +531,200 @@ function nvd3Vis(slice, payload) {
.attr('width', width)
.call(chart);
- // add annotation_layer
- if (isTimeSeries && payload.annotations && payload.annotations.length) {
- const tip = d3tip()
+ // on scroll, hide tooltips. throttle to only 4x/second.
+ $(window).scroll(throttle(hideTooltips, 250));
+
+ const annotationLayers = (slice.formData.annotation_layers || [])
+ .filter(x => x.show);
+ if (isTimeSeries && annotationLayers) {
+ // Formula annotations
+ const formulas = annotationLayers.filter(a => a.annotationType === AnnotationTypes.FORMULA)
+ .map(a => ({ ...a, formula: mathjs.parse(a.value) }));
+
+ let xMax;
+ let xMin;
+ let xScale;
+ if (vizType === VIZ_TYPES.bar) {
+ xMin = d3.min(data[0].values, d => (d.x));
+ xMax = d3.max(data[0].values, d => (d.x));
+ xScale = d3.scale.quantile()
+ .domain([xMin, xMax])
+ .range(chart.xAxis.range());
+ } else {
+ xMin = chart.xAxis.scale().domain()[0].valueOf();
+ xMax = chart.xAxis.scale().domain()[1].valueOf();
+ xScale = chart.xScale();
+ }
+
+ if (Array.isArray(formulas) && formulas.length) {
+ const xValues = [];
+ if (vizType === VIZ_TYPES.bar) {
+ // For bar-charts we want one data point evaluated for every
+ // data point that will be displayed.
+ const distinct = data.reduce((xVals, d) => {
+ d.values.forEach(x => xVals.add(x.x));
+ return xVals;
+ }, new Set());
+ xValues.push(...distinct.values());
+ xValues.sort();
+ } else {
+ // For every other time visualization it should be ok, to have a
+ // data points in even intervals.
+ let period = Math.min(...data.map(d =>
+ Math.min(...d.values.slice(1).map((v, i) => v.x - d.values[i].x))));
+ const dataPoints = (xMax - xMin) / (period || 1);
+ // make sure that there are enough data points and not too many
+ period = dataPoints < 100 ? (xMax - xMin) / 100 : period;
+ period = dataPoints > 500 ? (xMax - xMin) / 500 : period;
+ xValues.push(xMin);
+ for (let x = xMin; x < xMax; x += period) {
+ xValues.push(x);
+ }
+ xValues.push(xMax);
+ }
+ const formulaData = formulas.map(fo => ({
+ key: fo.name,
+ values: xValues.map((x => ({ y: fo.formula.eval({ x }), x }))),
+ color: fo.color,
+ strokeWidth: fo.width,
+ classed: `${fo.opacity} ${fo.style}`,
+ }));
+ data.push(...formulaData);
+ }
+
+ const annotationHeight = chart.yAxis.scale().range()[0];
+ const tipFactory = layer => d3tip()
.attr('class', 'd3-tip')
.direction('n')
.offset([-5, 0])
.html((d) => {
- if (!d || !d.layer) {
+ if (!d) {
return '';
}
-
- const title = d.short_descr ?
- d.short_descr + ' - ' + d.layer :
- d.layer;
- const body = d.long_descr;
+ const title = d[layer.titleColumn] && d[layer.titleColumn].length ?
+ d[layer.titleColumn] + ' - ' + layer.name :
+ layer.name;
+ const body = Array.isArray(layer.descriptionColumns) ?
+ layer.descriptionColumns.map(c => d[c]) : Object.values(d);
return '
' + title + '
' +
- '
' + body + '
';
+ '
' + body.join(', ') + '
';
});
- const hh = chart.yAxis.scale().range()[0];
+ if (slice.annotationData && Object.keys(slice.annotationData).length) {
+ // Event annotations
+ annotationLayers.filter(x => (
+ x.annotationType === AnnotationTypes.EVENT &&
+ slice.annotationData && slice.annotationData[x.name]
+ )).forEach((config, index) => {
+ const e = applyNativeColumns(config);
+ // Add event annotation layer
+ const annotations = d3.select(slice.selector).select('.nv-wrap').append('g')
+ .attr('class', `nv-event-annotation-layer-${index}`);
+ const aColor = e.color || getColorFromScheme(e.name, fd.color_scheme);
+
+ const tip = tipFactory(e);
+ const records = (slice.annotationData[e.name].records || []).map((r) => {
+ const timeColumn = new Date(r[e.timeColumn]);
+ return {
+ ...r,
+ [e.timeColumn]: timeColumn,
+ };
+ }).filter(r => !Number.isNaN(r[e.timeColumn].getMilliseconds()));
+ if (records.length) {
+ annotations.selectAll('line')
+ .data(records)
+ .enter()
+ .append('line')
+ .attr({
+ x1: d => xScale(new Date(d[e.timeColumn])),
+ y1: 0,
+ x2: d => xScale(new Date(d[e.timeColumn])),
+ y2: annotationHeight,
+ })
+ .attr('class', `${e.opacity} ${e.style}`)
+ .style('stroke', aColor)
+ .style('stroke-width', e.width)
+ .on('mouseover', tip.show)
+ .on('mouseout', tip.hide)
+ .call(tip);
+ }
+ });
- let annotationLayer;
- let xScale;
- let minStep;
- if (vizType === 'bar') {
- const xMax = d3.max(payload.data[0].values, d => (d.x));
- const xMin = d3.min(payload.data[0].values, d => (d.x));
- minStep = chart.xAxis.range()[1] - chart.xAxis.range()[0];
- annotationLayer = svg.select('.nv-barsWrap')
- .insert('g', ':first-child');
- xScale = d3.scale.quantile()
- .domain([xMin, xMax])
- .range(chart.xAxis.range());
- } else {
- minStep = 1;
- annotationLayer = svg.select('.nv-background')
- .append('g');
- xScale = chart.xScale();
- }
- annotationLayer
- .attr('class', 'annotation-container')
- .append('defs')
- .append('pattern')
- .attr('id', 'diagonal')
- .attr('patternUnits', 'userSpaceOnUse')
- .attr('width', 8)
- .attr('height', 10)
- .attr('patternTransform', 'rotate(45 50 50)')
- .append('line')
- .attr('stroke-width', 7)
- .attr('y2', 10);
-
- annotationLayer.selectAll('rect')
- .data(payload.annotations)
- .enter()
- .append('rect')
- .attr('class', 'annotation')
- .attr('x', d => (xScale(d.start_dttm)))
- .attr('y', 0)
- .attr('width', (d) => {
- const w = xScale(d.end_dttm) - xScale(d.start_dttm);
- return w === 0 ? minStep : w;
- })
- .attr('height', hh)
- .attr('fill', 'url(#diagonal)')
- .on('mouseover', tip.show)
- .on('mouseout', tip.hide);
-
- annotationLayer.selectAll('rect').call(tip);
- }
- }
+ // Interval annotations
+ annotationLayers.filter(x => (
+ x.annotationType === AnnotationTypes.INTERVAL &&
+ slice.annotationData && slice.annotationData[x.name]
+ )).forEach((config, index) => {
+ const e = applyNativeColumns(config);
+ // Add interval annotation layer
+ const annotations = d3.select(slice.selector).select('.nv-wrap').append('g')
+ .attr('class', `nv-interval-annotation-layer-${index}`);
+
+ const aColor = e.color || getColorFromScheme(e.name, fd.color_scheme);
+ const tip = tipFactory(e);
+
+ const records = (slice.annotationData[e.name].records || []).map((r) => {
+ const timeColumn = new Date(r[e.timeColumn]);
+ const intervalEndColumn = new Date(r[e.intervalEndColumn]);
+ return {
+ ...r,
+ [e.timeColumn]: timeColumn,
+ [e.intervalEndColumn]: intervalEndColumn,
+ };
+ }).filter(r => !Number.isNaN(r[e.timeColumn].getMilliseconds()) &&
+ !Number.isNaN(r[e.intervalEndColumn].getMilliseconds()));
+ if (records.length) {
+ annotations.selectAll('rect')
+ .data(records)
+ .enter()
+ .append('rect')
+ .attr({
+ x: d => Math.min(xScale(new Date(d[e.timeColumn])),
+ xScale(new Date(d[e.intervalEndColumn]))),
+ y: 0,
+ width: d => Math.abs(xScale(new Date(d[e.intervalEndColumn])) -
+ xScale(new Date(d[e.timeColumn]))),
+ height: annotationHeight,
+ })
+ .attr('class', `${e.opacity} ${e.style}`)
+ .style('stroke-width', e.width)
+ .style('stroke', aColor)
+ .style('fill', aColor)
+ .style('fill-opacity', 0.2)
+ .on('mouseover', tip.show)
+ .on('mouseout', tip.hide)
+ .call(tip);
+ }
+ });
- // on scroll, hide tooltips. throttle to only 4x/second.
- $(window).scroll(throttle(hideTooltips, 250));
+ // Time series annotations
+ const timeSeriesAnnotations = annotationLayers
+ .filter(a => a.annotationType === AnnotationTypes.TIME_SERIES).reduce((bushel, a) =>
+ bushel.concat((slice.annotationData[a.name] || []).map((series) => {
+ if (!series) {
+ return {};
+ }
+ const key = Array.isArray(series.key) ?
+ `${a.name}, ${series.key.join(', ')}` : a.name;
+ return {
+ ...series,
+ key,
+ color: a.color,
+ strokeWidth: a.width,
+ classed: `${a.opacity} ${a.style}`,
+ };
+ })), []);
+ data.push(...timeSeriesAnnotations);
+ }
+ }
+ // rerender chart
+ svg.datum(data)
+ .attr('height', height)
+ .attr('width', width)
+ .call(chart);
+ }
return chart;
};
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index e0952288ffdcb..18803c565b76f 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -21,12 +21,49 @@
from superset import db, import_util, sm, utils
from superset.connectors.base.models import BaseColumn, BaseDatasource, BaseMetric
from superset.jinja_context import get_template_processor
+from superset.models.annotations import Annotation
from superset.models.core import Database
from superset.models.helpers import QueryResult
from superset.models.helpers import set_perm
from superset.utils import DTTM_ALIAS, QueryStatus
+class AnnotationDatasource(BaseDatasource):
+ """ Dummy object so we can query annotations using 'Viz' objects just like
+ regular datasources.
+ """
+
+ cache_timeout = 0
+
+ def query(self, query_obj):
+ df = None
+ error_message = None
+ qry = db.session.query(Annotation)
+ qry = qry.filter(Annotation.layer_id == query_obj['filter'][0]['val'])
+ qry = qry.filter(Annotation.start_dttm >= query_obj['from_dttm'])
+ qry = qry.filter(Annotation.end_dttm <= query_obj['to_dttm'])
+ status = QueryStatus.SUCCESS
+ try:
+ df = pd.read_sql_query(qry.statement, db.engine)
+ except Exception as e:
+ status = QueryStatus.FAILED
+ logging.exception(e)
+ error_message = (
+ utils.error_msg_from_exception(e))
+ return QueryResult(
+ status=status,
+ df=df,
+ duration=0,
+ query='',
+ error_message=error_message)
+
+ def get_query_str(self, query_obj):
+ raise NotImplementedError()
+
+ def values_for_column(self, column_name, limit=10000):
+ raise NotImplementedError()
+
+
class TableColumn(Model, BaseColumn):
"""ORM object for table columns, each table can have multiple columns"""
diff --git a/superset/models/annotations.py b/superset/models/annotations.py
index 8aac6a2217b09..e082be0923d11 100644
--- a/superset/models/annotations.py
+++ b/superset/models/annotations.py
@@ -48,6 +48,7 @@ class Annotation(Model, AuditMixinNullable):
@property
def data(self):
return {
+ 'layer_id': self.layer_id,
'start_dttm': self.start_dttm,
'end_dttm': self.end_dttm,
'short_descr': self.short_descr,
diff --git a/superset/views/core.py b/superset/views/core.py
index 00254b4ca27ba..e83d34f70b610 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -37,7 +37,7 @@
viz,
)
from superset.connectors.connector_registry import ConnectorRegistry
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import AnnotationDatasource, SqlaTable
from superset.forms import CsvToDatabaseForm
from superset.legacy import cast_form_data
import superset.models.core as models
@@ -946,7 +946,7 @@ def get_form_data(self):
def get_viz(
self,
slice_id=None,
- args=None,
+ form_data=None,
datasource_type=None,
datasource_id=None):
if slice_id:
@@ -957,7 +957,6 @@ def get_viz(
)
return slc.get_viz()
else:
- form_data = self.get_form_data()
viz_type = form_data.get('viz_type', 'table')
datasource = ConnectorRegistry.get_datasource(
datasource_type, datasource_id, db.session)
@@ -971,14 +970,11 @@ def get_viz(
@expose('/slice/
/')
def slice(self, slice_id):
viz_obj = self.get_viz(slice_id)
- endpoint = (
- '/superset/explore/{}/{}?form_data={}'
- .format(
+ endpoint = '/superset/explore/{}/{}?form_data={}'.format(
viz_obj.datasource.type,
viz_obj.datasource.id,
parse.quote(json.dumps(viz_obj.form_data)),
)
- )
if request.args.get('standalone') == 'true':
endpoint += '&standalone=true'
return redirect(endpoint)
@@ -997,15 +993,13 @@ def get_query_string_response(self, viz_obj):
status=200,
mimetype='application/json')
- @log_this
- @has_access_api
- @expose('/explore_json///')
- def explore_json(self, datasource_type, datasource_id):
+ def generate_json(self, datasource_type, datasource_id, form_data,
+ csv=False, query=False, force=False):
try:
viz_obj = self.get_viz(
datasource_type=datasource_type,
datasource_id=datasource_id,
- args=request.args)
+ form_data=form_data)
except Exception as e:
logging.exception(e)
return json_error_response(
@@ -1015,20 +1009,19 @@ def explore_json(self, datasource_type, datasource_id):
if not self.datasource_access(viz_obj.datasource):
return json_error_response(DATASOURCE_ACCESS_ERR, status=404)
- if request.args.get('csv') == 'true':
+ if csv:
return CsvResponse(
viz_obj.get_csv(),
status=200,
headers=generate_download_headers('csv'),
mimetype='application/csv')
- if request.args.get('query') == 'true':
+ if query:
return self.get_query_string_response(viz_obj)
- payload = {}
try:
payload = viz_obj.get_payload(
- force=request.args.get('force') == 'true')
+ force=force)
except Exception as e:
logging.exception(e)
return json_error_response(utils.error_msg_from_exception(e))
@@ -1039,6 +1032,70 @@ def explore_json(self, datasource_type, datasource_id):
return json_success(viz_obj.json_dumps(payload), status=status)
+ @log_this
+ @has_access_api
+ @expose('/slice_json/')
+ def slice_json(self, slice_id):
+ try:
+ viz_obj = self.get_viz(slice_id)
+ datasource_type = viz_obj.datasource.type
+ datasource_id = viz_obj.datasource.id
+ form_data = viz_obj.form_data
+ # This allows you to override the saved slice form data with
+ # data from the current request (e.g. change the time window)
+ form_data.update(self.get_form_data())
+ except Exception as e:
+ return json_error_response(
+ utils.error_msg_from_exception(e),
+ stacktrace=traceback.format_exc())
+ return self.generate_json(datasource_type=datasource_type,
+ datasource_id=datasource_id,
+ form_data=form_data)
+
+ @log_this
+ @has_access_api
+ @expose('/annotation_json/')
+ def annotation_json(self, layer_id):
+ form_data = self.get_form_data()
+ form_data['layer_id'] = layer_id
+ form_data['filters'] = [{'col': 'layer_id',
+ 'op': '==',
+ 'val': layer_id}]
+ datasource = AnnotationDatasource()
+ viz_obj = viz.viz_types['table'](
+ datasource,
+ form_data=form_data,
+ )
+ try:
+ payload = viz_obj.get_payload(force=False)
+ except Exception as e:
+ logging.exception(e)
+ return json_error_response(utils.error_msg_from_exception(e))
+ status = 200
+ if payload.get('status') == QueryStatus.FAILED:
+ status = 400
+ return json_success(viz_obj.json_dumps(payload), status=status)
+
+ @log_this
+ @has_access_api
+ @expose('/explore_json///')
+ def explore_json(self, datasource_type, datasource_id):
+ try:
+ csv = request.args.get('csv') == 'true'
+ query = request.args.get('query') == 'true'
+ force = request.args.get('force') == 'true'
+ form_data = self.get_form_data()
+ except Exception as e:
+ return json_error_response(
+ utils.error_msg_from_exception(e),
+ stacktrace=traceback.format_exc())
+ return self.generate_json(datasource_type=datasource_type,
+ datasource_id=datasource_id,
+ form_data=form_data,
+ csv=csv,
+ query=query,
+ force=force)
+
@log_this
@has_access
@expose('/import_dashboards', methods=['GET', 'POST'])
@@ -1644,9 +1701,51 @@ def created_dashboards(self, user_id):
@api
@has_access_api
+ @expose('/user_slices', methods=['GET'])
+ @expose('/user_slices//', methods=['GET'])
+ def user_slices(self, user_id=None):
+ """List of slices a user created, or faved"""
+ if not user_id:
+ user_id = g.user.id
+ Slice = models.Slice # noqa
+ FavStar = models.FavStar # noqa
+ qry = (
+ db.session.query(Slice,
+ FavStar.dttm).join(
+ models.FavStar,
+ sqla.and_(
+ models.FavStar.user_id == int(user_id),
+ models.FavStar.class_name == 'slice',
+ models.Slice.id == models.FavStar.obj_id,
+ ),
+ isouter=True).filter(
+ sqla.or_(
+ Slice.created_by_fk == user_id,
+ Slice.changed_by_fk == user_id,
+ FavStar.user_id == user_id,
+ ),
+ )
+ .order_by(Slice.slice_name.asc())
+ )
+ payload = [{
+ 'id': o.Slice.id,
+ 'title': o.Slice.slice_name,
+ 'url': o.Slice.slice_url,
+ 'data': o.Slice.form_data,
+ 'dttm': o.dttm if o.dttm else o.Slice.changed_on,
+ 'viz_type': o.Slice.viz_type,
+ } for o in qry.all()]
+ return json_success(
+ json.dumps(payload, default=utils.json_int_dttm_ser))
+
+ @api
+ @has_access_api
+ @expose('/created_slices', methods=['GET'])
@expose('/created_slices//', methods=['GET'])
- def created_slices(self, user_id):
+ def created_slices(self, user_id=None):
"""List of slices created by this user"""
+ if not user_id:
+ user_id = g.user.id
Slice = models.Slice # noqa
qry = (
db.session.query(Slice)
@@ -1663,15 +1762,19 @@ def created_slices(self, user_id):
'title': o.slice_name,
'url': o.slice_url,
'dttm': o.changed_on,
+ 'viz_type': o.viz_type,
} for o in qry.all()]
return json_success(
json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
+ @expose('/fave_slices', methods=['GET'])
@expose('/fave_slices//', methods=['GET'])
- def fave_slices(self, user_id):
+ def fave_slices(self, user_id=None):
"""Favorite slices for a user"""
+ if not user_id:
+ user_id = g.user.id
qry = (
db.session.query(
models.Slice,
@@ -1696,6 +1799,7 @@ def fave_slices(self, user_id):
'title': o.Slice.slice_name,
'url': o.Slice.slice_url,
'dttm': o.dttm,
+ 'viz_type': o.Slice.viz_type,
}
if o.Slice.created_by:
user = o.Slice.created_by
diff --git a/superset/viz.py b/superset/viz.py
index 6551577de15c4..9b913e4dbe150 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -61,7 +61,7 @@ def __init__(self, datasource, form_data):
'token', 'token_' + uuid.uuid4().hex[:8])
self.metrics = self.form_data.get('metrics') or []
self.groupby = self.form_data.get('groupby') or []
- self.annotation_layers = []
+ self.time_shift = timedelta()
self.status = None
self.error_message = None
@@ -121,6 +121,7 @@ def get_df(self, query_obj=None):
df[DTTM_ALIAS], utc=False, format=timestamp_format)
if self.datasource.offset:
df[DTTM_ALIAS] += timedelta(hours=self.datasource.offset)
+ df[DTTM_ALIAS] += self.time_shift
df.replace([np.inf, -np.inf], np.nan)
fillna = self.get_fillna_for_columns(df.columns)
df = df.fillna(fillna)
@@ -158,6 +159,7 @@ def query_obj(self):
since = form_data.get('since', '')
until = form_data.get('until', 'now')
+ time_shift = form_data.get('time_shift', '')
# Backward compatibility hack
if since:
@@ -166,15 +168,15 @@ def query_obj(self):
if (len(since_words) == 2 and since_words[1] in grains):
since += ' ago'
- from_dttm = utils.parse_human_datetime(since)
+ self.time_shift = utils.parse_human_timedelta(time_shift)
- to_dttm = utils.parse_human_datetime(until)
+ from_dttm = utils.parse_human_datetime(since) - self.time_shift
+ to_dttm = utils.parse_human_datetime(until) - self.time_shift
if from_dttm and to_dttm and from_dttm > to_dttm:
raise Exception(_('From date cannot be larger than to date'))
self.from_dttm = from_dttm
self.to_dttm = to_dttm
- self.annotation_layers = form_data.get('annotation_layers') or []
# extras are used to query elements specific to a datasource type
# for instance the extra where clause that applies only to Tables
@@ -227,23 +229,6 @@ def cache_key(self):
s = str([(k, form_data[k]) for k in sorted(form_data.keys())])
return hashlib.md5(s.encode('utf-8')).hexdigest()
- def get_annotations(self):
- """Fetches the annotations for the specified layers and date range"""
- annotations = []
- if self.annotation_layers:
- from superset.models.annotations import Annotation
- from superset import db
- qry = (
- db.session
- .query(Annotation)
- .filter(Annotation.layer_id.in_(self.annotation_layers)))
- if self.from_dttm:
- qry = qry.filter(Annotation.start_dttm >= self.from_dttm)
- if self.to_dttm:
- qry = qry.filter(Annotation.end_dttm <= self.to_dttm)
- annotations = [o.data for o in qry.all()]
- return annotations
-
def get_payload(self, force=False):
"""Handles caching around the json payload retrieval"""
cache_key = self.cache_key
@@ -272,13 +257,11 @@ def get_payload(self, force=False):
is_cached = False
cache_timeout = self.cache_timeout
stacktrace = None
- annotations = []
rowcount = None
try:
df = self.get_df()
if not self.error_message:
data = self.get_data(df)
- annotations = self.get_annotations()
rowcount = len(df.index)
except Exception as e:
logging.exception(e)
@@ -296,7 +279,6 @@ def get_payload(self, force=False):
'query': self.query,
'status': self.status,
'stacktrace': stacktrace,
- 'annotations': annotations,
'rowcount': rowcount,
}
payload['cached_dttm'] = datetime.utcnow().isoformat().split('.')[0]
diff --git a/tests/viz_tests.py b/tests/viz_tests.py
index 06096e9292dd1..67f4bf89b374e 100644
--- a/tests/viz_tests.py
+++ b/tests/viz_tests.py
@@ -89,9 +89,13 @@ def test_get_df_handles_dttm_col(self):
mock_call = df.__setitem__.mock_calls[2]
self.assertEqual(mock_call[1][0], DTTM_ALIAS)
self.assertFalse(mock_call[1][1].empty)
- self.assertEqual(mock_call[1][1][0].hour, 6)
+ self.assertEqual(mock_call[1][1][0].hour, 7)
mock_call = df.__setitem__.mock_calls[3]
self.assertEqual(mock_call[1][0], DTTM_ALIAS)
+ self.assertEqual(mock_call[1][1][0].hour, 6)
+ self.assertEqual(mock_call[1][1].dtype, 'datetime64[ns]')
+ mock_call = df.__setitem__.mock_calls[4]
+ self.assertEqual(mock_call[1][0], DTTM_ALIAS)
self.assertEqual(mock_call[1][1][0].hour, 7)
self.assertEqual(mock_call[1][1].dtype, 'datetime64[ns]')