superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: Full Annotation Framework (#3518)
Date Sun, 17 Dec 2017 00:10:49 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 500e625  Full Annotation Framework (#3518)
500e625 is described below

commit 500e6256c0590d5ffac254652f1de98a97e14d42
Author: fabianmenges <fabianmenges@users.noreply.github.com>
AuthorDate: Sat Dec 16 19:10:45 2017 -0500

    Full Annotation Framework (#3518)
    
    * Adding full Annotation Framework
    
    * Viz types
    
    * Re organizing native annotations
    
    * liniting
    
    * Bug fix
    
    * Handle no data
    
    * Cleanup
    
    * Refactor slice form_data to data
---
 superset/assets/backendSync.json                   | 1135 +++++++++++++++-----
 superset/assets/javascripts/chart/Chart.jsx        |    5 +-
 .../assets/javascripts/chart/ChartContainer.jsx    |    1 +
 superset/assets/javascripts/chart/chartAction.js   |   96 +-
 superset/assets/javascripts/chart/chartReducer.js  |   59 +-
 .../javascripts/dashboard/components/GridCell.jsx  |    4 +-
 .../dashboard/components/GridLayout.jsx            |    2 +
 .../dashboard/components/SliceHeader.jsx           |   22 +
 .../components/controls/AnnotationLayer.jsx        |  602 +++++++++++
 .../components/controls/AnnotationLayerControl.jsx |  177 +++
 .../explore/components/controls/index.js           |    2 +
 .../assets/javascripts/explore/exploreUtils.js     |   19 +-
 superset/assets/javascripts/explore/index.jsx      |    3 +-
 superset/assets/javascripts/explore/main.css       |    1 +
 .../assets/javascripts/explore/stores/controls.jsx |   26 +-
 .../assets/javascripts/explore/stores/visTypes.js  |    2 +-
 .../assets/javascripts/modules/AnnotationTypes.js  |   94 ++
 superset/assets/package.json                       |    2 +
 .../spec/javascripts/explore/chartActions_spec.js  |   10 +-
 superset/assets/stylesheets/dashboard.css          |    8 +
 superset/assets/stylesheets/superset.less          |    2 +-
 superset/assets/visualizations/main.js             |  127 ++-
 superset/assets/visualizations/nvd3_vis.css        |   29 +
 superset/assets/visualizations/nvd3_vis.js         |  251 +++--
 superset/connectors/sqla/models.py                 |   37 +
 superset/models/annotations.py                     |    1 +
 superset/views/core.py                             |  140 ++-
 superset/viz.py                                    |   30 +-
 tests/viz_tests.py                                 |    6 +-
 29 files changed, 2418 insertions(+), 475 deletions(-)

diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json
index 71e7130..ba91b47 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,30 +770,22 @@
     },
     "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"
         ],
@@ -817,6 +794,10 @@
           "France"
         ],
         [
+          "Germany",
+          "Germany"
+        ],
+        [
           "Italy",
           "Italy"
         ],
@@ -825,8 +806,8 @@
           "Morocco"
         ],
         [
-          "Nederlanden",
-          "Nederlanden"
+          "Netherlands",
+          "Netherlands"
         ],
         [
           "Russia",
@@ -845,6 +826,10 @@
           "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 a4e3dd2..3dd0355 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 d517677..b731412 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 17205a4..a6341dd 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 ade8c5b..3cc9e5e 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: (
-        `<strong>${t('Query timeout')}</strong> - ` +
-        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.')),
+            `<strong>${t('Query timeout')}</strong> - ` +
+            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 854aea0..4f7213d 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 (
       <div
@@ -104,6 +105,7 @@ class GridCell extends React.PureComponent {
             toggleExpandSlice={toggleExpandSlice}
             forceRefresh={forceRefresh}
             editMode={this.props.editMode}
+            annotationQuery={annotationQuery}
           />
         </div>
         <div
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index dad66e1..b5dd49a 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -164,6 +164,8 @@ class GridLayout extends React.Component {
             clearFilter={this.props.clearFilter}
             removeFilter={this.props.removeFilter}
             editMode={this.props.editMode}
+            annotationQuery={currentChart.annotationQuery}
+            annotationError={currentChart.annotationError}
           />
         </div>);
     });
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
index 36107fe..1f4b475 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 (
       <div className="row chart-header">
@@ -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 &&
+              <TooltipWrapper
+                label="annotations-loading"
+                placement="top"
+                tooltip={annoationsLoading}
+              >
+                <i className="fa fa-refresh warning" />
+              </TooltipWrapper>
+            }
+            {!!Object.values(this.props.annotationError || {}).length &&
+              <TooltipWrapper
+                label="annoation-errors"
+                placement="top"
+                tooltip={annoationsError}
+              >
+                <i className="fa fa-exclamation-circle danger" />
+              </TooltipWrapper>
+            }
           </div>
           <div className="chart-controls">
             <div id={'controls_' + slice.slice_id} className="pull-right">
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 0000000..aa34fb0
--- /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 (
+        <SelectControl
+          name="annotation-layer-value"
+          showHeader
+          hovered
+          description={description}
+          label={label}
+          placeholder=""
+          options={valueOptions}
+          isLoading={isLoadingOptions}
+          value={value}
+          onChange={this.handleValue}
+          validationErrors={!value ? ['Mandatory'] : []}
+        />
+      );
+    } if (annotationType === AnnotationTypes.FORMULA) {
+      return (
+        <TextControl
+          name="annotation-layer-value"
+          hovered
+          showHeader
+          description={description}
+          label={label}
+          placeholder=""
+          value={value}
+          onChange={this.handleValue}
+          validationErrors={this.isValidFormula(value, annotationType) ? ['Bad formula.'] : []}
+        />
+      );
+    }
+    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 (
+        <div style={{ marginRight: '2rem' }}>
+          <PopoverSection
+            isSelected
+            onSelect={() => {
+            }}
+            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
+              ) &&
+              <SelectControl
+                hovered
+                name="annotation-layer-time-column"
+                label={
+                  annotationType === AnnotationTypes.INTERVAL ?
+                    'Interval Start column' : 'Event Time Column'
+                }
+                description={'This column must contain date/time information.'}
+                validationErrors={!timeColumn ? ['Mandatory'] : []}
+                clearable={false}
+                options={timeColumnOptions}
+                value={timeColumn}
+                onChange={v => this.setState({ timeColumn: v })}
+              />
+            }
+            {
+              annotationType === AnnotationTypes.INTERVAL &&
+              <SelectControl
+                hovered
+                name="annotation-layer-intervalEnd"
+                label="Interval End column"
+                description={'This column must contain date/time information.'}
+                validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
+                options={columns}
+                value={intervalEndColumn}
+                onChange={v => this.setState({ intervalEndColumn: v })}
+              />
+            }
+            <SelectControl
+              hovered
+              name="annotation-layer-title"
+              label="Title Column"
+              description={'Pick a title for you annotation.'}
+              options={
+                [{ value: '', label: 'None' }].concat(columns)
+              }
+              value={titleColumn}
+              onChange={v => this.setState({ titleColumn: v })}
+            />
+            {
+              annotationType !== AnnotationTypes.TIME_SERIES &&
+              <SelectControl
+                hovered
+                name="annotation-layer-title"
+                label="Description Columns"
+                description={`Pick one or more columns that should be shown in the
+                  annotation. If you don't select a column all of them will be shown.`}
+                multi
+                options={
+                  columns
+                }
+                value={descriptionColumns}
+                onChange={v => this.setState({ descriptionColumns: v })}
+              />
+            }
+            <div style={{ marginTop: '1rem' }}>
+              <CheckboxControl
+                hovered
+                name="annotation-override-since"
+                label="Override 'Since'"
+                description={`This controls whether the "Since" field from the current
+                  view should be passed down to the slice containing the annotation data.`}
+                value={!!Object.keys(overrides).find(x => x === 'since')}
+                onChange={(v) => {
+                  delete overrides.since;
+                  if (v) {
+                    this.setState({ overrides: { ...overrides, since: null } });
+                  } else {
+                    this.setState({ overrides: { ...overrides } });
+                  }
+                }}
+              />
+              <CheckboxControl
+                hovered
+                name="annotation-override-until"
+                label="Override 'Until'"
+                description={`This controls whether the "Until" field from the current
+                  view should be passed down to the slice containing the annotation data.`}
+                value={!!Object.keys(overrides).find(x => x === 'until')}
+                onChange={(v) => {
+                  delete overrides.until;
+                  if (v) {
+                    this.setState({ overrides: { ...overrides, until: null } });
+                  } else {
+                    this.setState({ overrides: { ...overrides } });
+                  }
+                }}
+              />
+              <TextControl
+                hovered
+                name="annotation-layer-timeshift"
+                label="Time Shift"
+                description={`Time delta in natural language
+                  (example:  24 hours, 7 days, 56 weeks, 365 days)`}
+                placeholder=""
+                value={overrides.time_shift}
+                onChange={v => this.setState({ overrides: { ...overrides, time_shift: v } })}
+              />
+            </div>
+          </PopoverSection>
+        </div>
+      );
+    }
+    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 (
+      <PopoverSection
+        isSelected
+        onSelect={() => {}}
+        title="Display configuration"
+        info="Configure your how you overlay is displayed here."
+      >
+        <SelectControl
+          name="annotation-layer-stroke"
+          label="Style"
+            // see '../../../../visualizations/nvd3_vis.css'
+          options={[
+              { value: 'solid', label: 'Solid' },
+              { value: 'dashed', label: 'Dashed' },
+              { value: 'longDashed', label: 'Long Dashed' },
+              { value: 'dotted', label: 'Dotted' },
+          ]}
+          value={style}
+          onChange={v => this.setState({ style: v })}
+        />
+        <SelectControl
+          name="annotation-layer-opacity"
+          label="Opacity"
+            // see '../../../../visualizations/nvd3_vis.css'
+          options={[
+              { value: '', label: 'Solid' },
+              { value: 'opacityLow', label: '0.2' },
+              { value: 'opacityMedium', label: '0.5' },
+              { value: 'opacityHigh', label: '0.8' },
+          ]}
+          value={opacity}
+          onChange={v => this.setState({ opacity: v })}
+        />
+        <div>
+          <ControlHeader label="Color" />
+          <div style={{ display: 'flex', flexDirection: 'column' }}>
+            <CompactPicker
+              color={color}
+              colors={colorScheme}
+              onChangeComplete={v => this.setState({ color: v.hex })}
+            />
+            <Button
+              style={{ marginTop: '0.5rem', marginBottom: '0.5rem' }}
+              bsStyle={color === AUTOMATIC_COLOR ? 'success' : 'default'}
+              bsSize="xsmall"
+              onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
+            >
+              Automatic Color
+            </Button>
+          </div>
+        </div>
+        <TextControl
+          name="annotation-layer-stroke-width"
+          label="Line Width"
+          isInt
+          value={width}
+          onChange={v => this.setState({ width: v })}
+        />
+      </PopoverSection>
+    );
+  }
+
+  render() {
+    const { isNew, name, annotationType,
+      sourceType, show } = this.state;
+    const isValid = this.isValidForm();
+    return (
+      <div>
+        {
+          this.props.error &&
+          <span style={{ color: 'red' }}>
+            ERROR: {this.props.error}
+          </span>
+        }
+        <div style={{ display: 'flex', flexDirection: 'row' }}>
+          <div style={{ marginRight: '2rem' }}>
+            <PopoverSection
+              isSelected
+              onSelect={() => {}}
+              title="Layer Configuration"
+              info="Configure the basics of your Annotation Layer."
+            >
+              <TextControl
+                name="annotation-layer-name"
+                label="Name"
+                placeholder=""
+                value={name}
+                onChange={v => this.setState({ name: v })}
+                validationErrors={!name ? ['Mandatory'] : []}
+              />
+              <CheckboxControl
+                name="annotation-layer-hide"
+                label="Hide Layer"
+                value={!show}
+                onChange={v => this.setState({ show: !v })}
+              />
+              <SelectControl
+                hovered
+                description="Choose the Annotation Layer Type"
+                label="Annotation Layer Type"
+                name="annotation-layer-type"
+                options={getSupportedAnnotationTypes(this.props.vizType).map(
+                    x => ({ value: x, label: getAnnotationTypeLabel(x) }))}
+                value={annotationType}
+                onChange={this.handleAnnotationType}
+              />
+              {!!getSupportedSourceTypes(annotationType).length &&
+                <SelectControl
+                  hovered
+                  description="Choose the source of your annotations"
+                  label="Annotation Source"
+                  name="annotation-source-type"
+                  options={getSupportedSourceTypes(annotationType).map(
+                        x => ({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
+                  value={sourceType}
+                  onChange={this.handleAnnotationSourceType}
+                />
+              }
+              { this.renderValueConfiguration() }
+            </PopoverSection>
+          </div>
+          { this.renderSliceConfiguration() }
+          { this.renderDisplayConfiguration() }
+        </div>
+        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+          <Button
+            bsSize="sm"
+            onClick={this.deleteAnnotation}
+          >
+            { !isNew ? 'Remove' : 'Cancel' }
+          </Button>
+          <div>
+            <Button
+              bsSize="sm"
+              disabled={!isValid}
+              onClick={this.applyAnnotation}
+            >
+              Apply
+            </Button>
+
+            <Button
+              bsSize="sm"
+              disabled={!isValid}
+              onClick={this.submitAnnotation}
+            >
+              OK
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+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 0000000..3e4cd24
--- /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 (
+      <Popover
+        style={{ maxWidth: 'none' }}
+        title={annotation ? 'Edit Annotation Layer' : 'Add Annotation Layer'}
+        id={`annotation-pop-${id}`}
+      >
+        <AnnotationLayer
+          {...annotation}
+          error={error}
+          colorScheme={this.props.colorScheme}
+          vizType={this.props.vizType}
+          addAnnotationLayer={this.addAnnotationLayer}
+          removeAnnotationLayer={this.removeAnnotationLayer}
+          close={() => this.refs[parent].hide()}
+        />
+      </Popover>
+    );
+  }
+
+  renderInfo(anno) {
+    const { annotationError, annotationQuery } = this.props;
+    if (annotationQuery[anno.name]) {
+      return (
+        <i className="fa fa-refresh" style={{ color: 'orange' }} aria-hidden />
+      );
+    }
+    if (annotationError[anno.name]) {
+      return (
+        <InfoTooltipWithTrigger
+          label="validation-errors"
+          bsStyle="danger"
+          tooltip={annotationError[anno.name]}
+        />
+      );
+    }
+    if (!anno.show) {
+      return <span style={{ color: 'red' }}> Hidden </span>;
+    }
+    return '';
+  }
+
+  render() {
+    const annotations = this.props.value.map((anno, i) => (
+      <OverlayTrigger
+        key={i}
+        trigger="click"
+        rootClose
+        ref={`overlay-${i}`}
+        placement="right"
+        overlay={this.renderPopover(`overlay-${i}`, anno,
+          this.props.annotationError[anno.name])}
+      >
+        <ListGroupItem>
+          <span>{anno.name}</span>
+          <span style={{ float: 'right' }}>
+            {this.renderInfo(anno)}
+          </span>
+        </ListGroupItem>
+      </OverlayTrigger>
+    ));
+    return (
+      <div>
+        <ListGroup>
+          {annotations}
+          <OverlayTrigger
+            trigger="click"
+            rootClose
+            ref="overlay-new"
+            placement="right"
+            overlay={this.renderPopover('overlay-new')}
+          >
+            <ListGroupItem>
+              <i className="fa fa-plus" /> &nbsp; {t('Add Annotation Layer')}
+            </ListGroupItem>
+          </OverlayTrigger>
+        </ListGroup>
+      </div>
+    );
+  }
+}
+
+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 94b8c66..35aaeef 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 2356da5..8a01745 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 7e2ed35..d66ad52 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 a6afe5e..434e6f8 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 cf657ac..006a1b4 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 a243cbf..ef9dc41 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 0000000..28684bb
--- /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 9d8b01a..905e770 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 f88de8f..4caeccd 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 b6d86ab..c1f08a7 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 d86ad74..ae0be2c 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 9976614..fdcb7b1 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 1a5897f..fed0d01 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 f1b6c11..0e3d9e8 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 '<div><strong>' + title + '</strong></div><br/>' +
-            '<div>' + body + '</div>';
+              '<div>' + body.join(', ') + '</div>';
           });
 
-        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 e095228..18803c5 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -21,12 +21,49 @@ import sqlparse
 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 8aac6a2..e082be0 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 8801419..4d55fae 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -38,7 +38,7 @@ from superset import (
     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
@@ -952,7 +952,7 @@ class Superset(BaseSupersetView):
     def get_viz(
             self,
             slice_id=None,
-            args=None,
+            form_data=None,
             datasource_type=None,
             datasource_id=None):
         if slice_id:
@@ -963,7 +963,6 @@ class Superset(BaseSupersetView):
             )
             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)
@@ -977,14 +976,11 @@ class Superset(BaseSupersetView):
     @expose('/slice/<slice_id>/')
     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)
@@ -1003,15 +999,13 @@ class Superset(BaseSupersetView):
             status=200,
             mimetype='application/json')
 
-    @log_this
-    @has_access_api
-    @expose('/explore_json/<datasource_type>/<datasource_id>/')
-    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(
@@ -1021,20 +1015,19 @@ class Superset(BaseSupersetView):
         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))
@@ -1046,6 +1039,70 @@ class Superset(BaseSupersetView):
         return json_success(viz_obj.json_dumps(payload), status=status)
 
     @log_this
+    @has_access_api
+    @expose('/slice_json/<slice_id>')
+    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/<layer_id>')
+    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/<datasource_type>/<datasource_id>/')
+    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'])
     def import_dashboards(self):
@@ -1650,9 +1707,51 @@ class Superset(BaseSupersetView):
 
     @api
     @has_access_api
+    @expose('/user_slices', methods=['GET'])
+    @expose('/user_slices/<user_id>/', 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/<user_id>/', 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)
@@ -1669,15 +1768,19 @@ class Superset(BaseSupersetView):
             '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/<user_id>/', 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,
@@ -1702,6 +1805,7 @@ class Superset(BaseSupersetView):
                 '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 6551577..9b913e4 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -61,7 +61,7 @@ class BaseViz(object):
             '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 @@ class BaseViz(object):
                         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 @@ class BaseViz(object):
 
         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 @@ class BaseViz(object):
             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 @@ class BaseViz(object):
         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 @@ class BaseViz(object):
             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 @@ class BaseViz(object):
                 '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 06096e9..67f4bf8 100644
--- a/tests/viz_tests.py
+++ b/tests/viz_tests.py
@@ -89,9 +89,13 @@ class BaseVizTestCase(unittest.TestCase):
         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]')
 

-- 
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <commits@superset.apache.org>'].

Mime
View raw message