diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json index 71e713032849..ba91b47cde46 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 a4e3dd2abddf..3dd0355ab08f 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 d517677ec4d4..b731412fc5ff 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 17205a41a3cc..a6341ddb6a7b 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 ade8c5bf68f0..3cc9e5e6dfc3 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 854aea01fe74..4f7213d3b08c 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 36107fedf1e0..1f4b475d8807 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 000000000000..aa34fb04f7db --- /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 000000000000..3e4cd24e31c4 --- /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 94b8c66ef606..35aaeeff2b77 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 2356da5eb625..8a01745d9a39 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 7e2ed3592643..d66ad52e79fa 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 a6afe5eba935..434e6f8acaf5 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 b18039f232aa..eb4efba5ada7 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 a243cbfd2d9c..ef9dc4112cc5 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 000000000000..28684bbcb688 --- /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 9d8b01a81135..905e770294b0 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 f88de8f95531..4caeccd3e2b9 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 b6d86abbd081..c1f08a7e38b7 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 d86ad74b8218..ae0be2cd64ee 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 9976614048ad..fdcb7b1eab2e 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 1a5897f14d36..fed0d013dc53 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 f1b6c11cde4a..0e3d9e87a6d5 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 e0952288ffdc..18803c565b76 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 8aac6a2217b0..e082be0923d1 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 00254b4ca27b..e83d34f70b61 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 6551577de15c..9b913e4dbe15 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 06096e9292dd..67f4bf89b374 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]')