Repository: lux-org/lux (language: python)

Issue:
New action: "All-column" vis when only few columns in dataframe
When there are only two columns in a dataframe involving a quantitative and a temporal attribute, we display separate univariate charts. The better chart to display here would be a line chart with both attributes. We need to change the action logic to account for this use case.

```python
tmax = pd.read_csv('https://raw.githubusercontent.com/koldunovn/python_for_geosciences/master/DelhiTmax.txt', delimiter=r"\s+", parse_dates=[[0,1,2]], header=None)
tmax.columns = ['Date', 'Temp']
```

![image](https://user-images.githubusercontent.com/5554675/103758845-bd166f00-504d-11eb-8cda-09d4c46233bd.png)

[This notebook](https://github.com/koldunovn/python_for_geosciences/blob/master/06%20-%20Time%20series%20(Pandas).ipynb) has a good use case example for time series.




Discussion / hints:
A similar use case is when there are 1 dimension and 2 quantitative variables, the visualization shows as two separate charts.
![image](https://user-images.githubusercontent.com/5554675/104676717-e35d9e00-5722-11eb-8581-ab448dd2d64e.png)
![image](https://user-images.githubusercontent.com/5554675/104676725-e8225200-5722-11eb-9bf7-8d650801e749.png)
A single visualization like this would be more appropriate

![image](https://user-images.githubusercontent.com/5554675/104676820-1ef86800-5723-11eb-90e5-79a45b1bd72d.png)
![image](https://user-images.githubusercontent.com/5554675/104676837-26b80c80-5723-11eb-948d-32e9eac1d998.png)

Example code:
```python
df = pd.DataFrame({'nPts': {0: 49999, 1: 71174, 2: 101317, 3: 144224, 4: 205303, 5: 292249, 6: 416016, 7: 592198, 8: 842993, 9: 1200000}, 't_heatmap': {0: 0.15121674537658691, 1: 0.1811518669128418, 2: 0.2179429531097412, 3: 0.2787730693817139, 4: 0.3973350524902344, 5: 0.4233138561248779, 6: 0.580251932144165, 7: 0.7928099632263184, 8: 1.0876789093017578, 9: 1.6242818832397459}, 't_color_heatmap': {0: 0.14242982864379886, 1: 0.18866705894470212, 2: 0.1566781997680664, 3: 0.16737699508666992, 4: 0.19900894165039065, 5: 0.2701129913330078, 6: 0.2533812522888184, 7: 0.37183785438537603, 8: 0.3830866813659668, 9: 0.39321017265319824}, 't_bar': {0: 0.01940608024597168, 1: 0.02618718147277832, 2: 0.024693727493286133, 3: 0.029685020446777344, 4: 0.03471803665161133, 5: 0.04173588752746582, 6: 0.04706382751464844, 7: 0.0667569637298584, 8: 0.08260798454284668, 9: 0.1006929874420166}, 't_cbar': {0: 0.04185795783996582, 1: 0.050965070724487305, 2: 0.052091121673583984, 3: 0.0610501766204834, 4: 0.07504606246948242, 5: 0.09924101829528807, 6: 0.10392117500305176, 7: 0.13044309616088867, 8: 0.18137121200561526, 9: 0.2069528102874756}, 't_hist': {0: 0.01846003532409668, 1: 0.018136978149414062, 2: 0.018748998641967773, 3: 0.01876473426818848, 4: 0.02434706687927246, 5: 0.025368213653564453, 6: 0.02530217170715332, 7: 0.02823114395141602, 8: 0.029034852981567383, 9: 0.034781217575073235}, 't_scatter': {0: 0.8179378509521484, 1: 1.0982017517089844, 2: 1.4875690937042236, 3: 2.146117925643921, 4: 3.1123709678649902, 5: 3.92416787147522, 6: 6.097048044204713, 7: 8.803220987319945, 8: 12.054464101791382, 9: 17.740845680236816}, 't_color_scatter': {0: 1.3474130630493164, 1: 1.7953579425811768, 2: 2.6268541812896733, 3: 3.646371126174927, 4: 4.901940107345581, 5: 6.974237203598023, 6: 10.197483777999876, 7: 14.134275913238525, 8: 21.13951110839844, 9: 27.127063989639282}})
pdf = df.melt(id_vars=["nPts"],value_vars=['t_heatmap', 't_color_heatmap', 't_bar', 't_cbar', 't_hist','t_scatter', 't_color_scatter'], var_name="type", value_name='time')
```

Fix patch:
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 26fcb844..ee2ff91c 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -45,6 +45,15 @@ jobs:
         pip install -r requirements.txt
         pip install -r requirements-dev.txt
         pip install sqlalchemy
+
+        pip uninstall -y  lux-widget
+        pip install git+git://github.com/lux-org/lux-widget.git
+        # Temporary Fix (#372)
+        cd /opt/hostedtoolcache/Python/3.7.10/x64/lib/python3.7/site-packages/luxwidget/
+        mkdir labextension
+        cd labextension
+        wget https://raw.githubusercontent.com/lux-org/lux-widget/master/luxwidget/nbextension/package.json
+
     - name: Upload data to Postgres
       run: |
         python lux/data/upload_car_data.py
@@ -55,4 +64,4 @@ jobs:
     - name: Test with Pytest and Code Coverage Report
       run: |
         pytest --cov-report term --cov=lux tests/ tests_sql/
-        bash <(curl -s https://codecov.io/bash)
+        bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/README.md b/README.md
index 5ecc3603..75a1d616 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
     <a href='https://forms.gle/XKv3ejrshkCi3FJE6'>
         <img src='https://img.shields.io/static/v1?label=email&message=signup&color=brightgreen' alt='Mailing List'  align="center"/>
     </a>
-    <a href='https://mybinder.org/v2/gh/lux-org/lux-binder/master'>
+    <a href='https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=lab'>
         <img src='https://img.shields.io/badge/launch-binder-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC' alt='Binder'  align="center"/>
     </a>
     <a href="https://codecov.io/gh/lux-org/lux">
@@ -37,7 +37,7 @@ Lux is a Python library that facilitate fast and easy data exploration by automa
 Here is a [1-min video](https://www.youtube.com/watch?v=qmnYP-LmbNU) introducing Lux, and [slides](http://dorisjunglinlee.com/files/Zillow_07_2020_Slide.pdf) from a more extended talk.
 
 Check out our [notebook gallery](https://lux-api.readthedocs.io/en/latest/source/reference/gallery.html) with examples of how Lux can be used with different datasets and analyses. 
-<br>Or try out Lux on your own in a [live Jupyter Notebook](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/demo/employee_demo.ipynb)! 
+<br>Or try out Lux on your own in a [live Jupyter Notebook](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=lab/tree/demo/hpi_covid_demo.ipynb)! 
 
 # Getting Started
 
@@ -181,6 +181,8 @@ Other additional resources:
 
 - Follow us on [Twitter](https://twitter.com/intent/follow?original_referer=https%3A%2F%2Fpublish.twitter.com%2F&ref_src=twsrc%5Etfw&screen_name=lux_api&tw_p=followbutton) or sign up to our [mailing list](https://forms.gle/XKv3ejrshkCi3FJE6) to stay tuned for upcoming releases and updates. 
 - Visit [ReadTheDoc](https://lux-api.readthedocs.io/en/latest/) for more detailed documentation.
-- Try out these hands-on [exercises](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/exercise) or [tutorials](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/tutorial) on [Binder](https://mybinder.org/v2/gh/lux-org/lux-binder/master). Or clone and run [lux-binder](https://github.com/lux-org/lux-binder) locally.
+- Try out these hands-on [exercises](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=lab/tree/exercise) or [tutorials](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=lab/tree/tutorial) on [Binder](https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=lab/). Or clone and run [lux-binder](https://github.com/lux-org/lux-binder) locally.
 - Join our community [Slack](https://communityinviter.com/apps/lux-project/lux) to discuss and ask questions.
+- Read [our paper](https://arxiv.org/pdf/2105.00121.pdf) for more details on how Lux works under the hoods.
 - Report any bugs, issues, or requests through [Github Issues](https://github.com/lux-org/lux/issues). 
+[![Analytics](https://ga-beacon.appspot.com/UA-188430203-1/readme?pixel)](https://github.com/igrigorik/ga-beacon)
diff --git a/doc/index.rst b/doc/index.rst
index 2ed0c498..ed217512 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -33,7 +33,7 @@ This website contains pages that overview of the basic and advanced functionalit
    source/guide/export
    source/guide/style
    source/guide/FAQ
-.. source/guide/example
+   source/reference/gallery
 
 .. toctree::
    :maxdepth: 1
@@ -56,6 +56,7 @@ This website contains pages that overview of the basic and advanced functionalit
    
    source/reference/config
    source/reference/API
+   source/reference/about
    
 
 Indices and tables
diff --git a/doc/source/advanced/date.rst b/doc/source/advanced/date.rst
index 6ecc9c69..f47f8c3e 100644
--- a/doc/source/advanced/date.rst
+++ b/doc/source/advanced/date.rst
@@ -89,6 +89,22 @@ After changing the Pandas data type to datetime, we see that date field is recog
   :align: center
   :alt: add screenshot
 
+Visualizing Trends across Different Timescales
+----------------------------------------------
+
+Lux automatically detects the temporal attribute and plots the visualizations across different timescales to showcase any cyclical patterns. Here, we see that the `Temporal` tab displays the yearly, monthly, and weekly trends for the number of stock records.
+
+.. code-block:: python
+
+  from vega_datasets import data
+  df = data.stocks()
+
+  df.recommendation["Temporal"]
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/date-12.png?raw=true
+  :width: 600
+  :align: center
+
 Advanced Date Manipulation
 --------------------------
 
diff --git a/doc/source/guide/export.rst b/doc/source/guide/export.rst
index c0571fd8..4daff073 100644
--- a/doc/source/guide/export.rst
+++ b/doc/source/guide/export.rst
@@ -4,7 +4,7 @@ Exporting Vis From Widget
 
 .. note:: You can follow along this tutorial in a Jupyter notebook. [`Github <https://github.com/lux-org/lux-binder/blob/master/tutorial/3-widget-vis-export.ipynb>`_] [`Binder <https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/tutorial/3-widget-vis-export.ipynb>`_]
 
-In this tutorial, we look at the `Happy Planet Index <http://happyplanetindex.org/>`_ dataset, which contains metrics related to well-being for 140 countries around the world. We demonstrate how you can select visualizations of interest and export them for further analysis. 
+In this tutorial, we will first look at how you can access and export a single visualizations in the widget as code to further work with in the notebook. Then, we walk through how you can export more than one visualizations in the widget as an interactive report for sharing or presentation.
 
 .. code-block:: python
 
@@ -13,51 +13,17 @@ In this tutorial, we look at the `Happy Planet Index <http://happyplanetindex.or
 
 .. code-block:: python
 
-    df = pd.read_csv("lux/data/hpi.csv")
+    df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/hpi.csv")
     df.default_display = "lux" # Set Lux as default display
 
-Note that for the convienience of this tutorial, we have set Lux as the default display so we don't have to Toggle from the Pandas table display everytime we print the dataframe.
+As an example, we load in the `Happy Planet Index <http://happyplanetindex.org/>`_ dataset, which contains metrics related to well-being for 140 countries around the world. Note that for the convienience of this tutorial, we set Lux as the default display so we don't have to Toggle from the Pandas table display everytime we print the dataframe.
 
-Exporting widget visualizations as static HTML
-----------------------------------------------------------------
+Working with a Single Visualizations from Widget
+--------------------------------------------------
 
-Let's say that you are interested in sharing the visualizations displayed in Lux with others, you can export the visualizations into a static HTML using the following command: 
-
-.. code-block:: python
-
-    df.save_as_html()
-
-By default, the file is saved as `export.html`, you can optionally specify the HTML filename in the input parameter. 
-
-.. code-block:: python
-
-    df.save_as_html('hpi.html')
-
-Selecting visualizations from recommendation widget
-----------------------------------------------------------------
-
-You can also click on visualizations of interest and export them into a separate widget for further processing.
-
-.. code-block:: python
-
-    df
-
-.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-1.gif?raw=true
-  :width: 700
-  :align: center
-  :alt: 1) scroll through Correlation, then 2) click on any 3 visualization (let's say 2nd, 5th and something towards the end), then 3) click on the export button and make sure the blue message box show up
-
-.. code-block:: python
-
-    bookmarked_charts = df.exported
-    bookmarked_charts
-
-.. image:: ../img/export-2.png
-  :width: 700
-  :align: center
-  :alt: add screenshot of exported VisList (include the Out[] __repr__ string) in screenshot
-
-From the dataframe recommendations, the visualization showing the relationship between `GDPPerCapita` and `Footprint` is very interesting. In particular, there is an outlier with extremely high ecological footprint as well as high GDP per capita. So we click on this visualization and click on the export button.
+Selecting a Single Visualization as :code:`Vis`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+From the dataframe recommendations, the visualization showing the relationship between :code:`GDPPerCapita` and :code:`Footprint` is very interesting. In particular, there is an outlier with extremely high ecological footprint as well as high GDP per capita. So we click on this visualization and click on the export button.
 
 .. code-block:: python
 
@@ -79,7 +45,7 @@ From the dataframe recommendations, the visualization showing the relationship b
   :alt: add screenshot of exported vis
 
 Setting Vis as the Updated Intent
-----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Often, we might be interested in other visualizations that is related to a visualization of interest and want to learn more. With the exported Vis, we can update the intent associated with dataframe to be based on the selected Vis to get more recommendations related to this visualization.
 
@@ -91,64 +57,21 @@ Often, we might be interested in other visualizations that is related to a visua
 .. image:: ../img/export-5.png
   :width: 700
   :align: center
-  :alt: add screenshot
-
-Accessing Widget State
-------------------------
-
-We can access the set of recommendations generated for the dataframes via the properties `recommendation`.
-
-.. code-block:: python
-    
-    df.recommendation
-
-.. image:: ../img/export-6.png
-  :width: 700
-  :align: center
-  :alt: add screenshot
-
-The resulting output is a dictionary, keyed by the name of the recommendation category.
-
-.. code-block:: python
-    
-    df.recommendation["Enhance"]
-
-.. image:: ../img/export-7.png
-  :width: 700
-  :align: center
-  :alt: add screenshot
 
-You can also access the vis represented by the current intent via the property `current_vis`.
-
-.. code-block:: python
-
-    df.current_vis
-
-.. image:: ../img/export-8.png
-  :width: 700
-  :align: center
-  :alt: add screenshot
 
 Exporting Visualizations as Code
---------------------------------
-
-Let's revist our earlier recommendations by clearing the specified intent.
-
-.. code-block:: python
-
-    df.clear_intent()
-    df
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 .. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-9.gif?raw=true
   :width: 700
   :align: center
   :alt: 1) click on `Occurrence` tab, then 2) hover around the SubRegion v.s. Number of Records chart
 
-Looking at the Occurrence tab, we are interested in the bar chart distribution of country `SubRegion`.
+Looking at the Occurrence tab, we can select and export the bar chart distribution of :code:`SubRegion`.
 
 .. code-block:: python
 
-    vis = df.recommendation["Occurrence"][0]
+    vis = df.exported[0]
     vis
 
 .. image:: ../img/export-10.png
@@ -164,8 +87,8 @@ To allow further edits of visualizations, visualizations can be exported to code
     print (vis.to_code("altair"))
     print (vis.to_code("vegalite"))
 
-Exporting Visualizations to Matplotlib
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Exporting to Matplotlib
+==========================
 We can also export the visualization as code in `Matplotlib <https://matplotlib.org/>`_.
 
 .. code-block:: python
@@ -211,21 +134,14 @@ This code can be copy-and-pasted back into a new notebook cell for further editi
   :align: center
   :alt: add screenshot
 
-Exporting Visualizations to Altair
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Exporting to Altair
+==========================
 
 
 .. code-block:: python
 
     print (vis.to_altair())
 
-.. .. image:: ../img/export-11.png
-..   :width: 700
-..   :align: center
-..   :alt: add screenshot
-
-.. This can be copy-and-pasted back into a new notebook cell for further editing.
-
 .. code-block:: python
 
     import altair as alt
@@ -247,11 +163,11 @@ Exporting Visualizations to Altair
 .. image:: ../img/export-12.png
   :width: 300
   :align: center
-  :alt: add screenshot 
+  :alt: add screenshot
+
+Exporting to Vega-Lite
+==========================
 
-Exporting Visualizations to Vega-Lite
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-  
 You can also export this as Vega-Lite specification and view/edit the specification on `Vega Editor <https://vega.github.io/editor>`_.
 
 .. code-block:: python
@@ -263,10 +179,10 @@ You can also export this as Vega-Lite specification and view/edit the specificat
   :align: center
   :alt: add screenshot of what this looks like in Vega Editor
 
-Exporting Standalone Visualizations 
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Exporting Standalone Visualization Code
+====================================================
 
-Let's say now we are interested in the scatter plot of the `HPIRank` and `HappyPlanetIndex`.
+Let's say now we are interested in the scatter plot of the :code:`HPIRank` and :code:`HappyPlanetIndex`.
 
 .. code-block:: python
 
@@ -292,4 +208,178 @@ If we wanted to include the actual data in the returned codeblock, we would use
 .. image:: ../img/export-15.png
   :width: 700
   :align: center
-  :alt: screenshot of code with embedded data
\ No newline at end of file
+  :alt: screenshot of code with embedded data
+
+
+
+Working with Multiple Visualizations from Widget
+--------------------------------------------------
+
+We have seen how you can select and export a single visualization of interest. Now let's say that we found several visualizations that displays interesting trends to filter to and investigate further.
+
+Selecting Multiple Visualization as :code:`VisList`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To do this, we can select one or more visualizations of interest and click on the export button to extract the selected visualizations into a separate widget for further processing.
+
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-1.gif?raw=true
+  :width: 700
+  :align: center
+  :alt: 1) scroll through Correlation, then 2) click on any 3 visualization (let's say 2nd, 5th and something towards the end), then 3) click on the export button and make sure the blue message box show up
+
+After clicking on the export button, the selected visualizations are stored inside the :code:`exported` property of the dataframe as a :code:`VisList`, which can be accessed programmatically.
+
+.. code-block:: python
+
+    bookmarked_charts = df.exported
+    bookmarked_charts
+
+.. image:: ../img/export-2.png
+  :width: 700
+  :align: center
+  :alt: add screenshot of exported VisList (include the Out[] __repr__ string) in screenshot
+
+
+Accessing Widget Recommendations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We can access the set of recommendations generated for the dataframes via the dataframe property :code:`recommendation`.
+
+.. code-block:: python
+
+    df.recommendation
+
+.. image:: ../img/export-6.png
+  :width: 700
+  :align: center
+
+The resulting output is a Python dictionary, with the key as the name of the recommendation category and values as a :code:`VisList` of recommendations.
+
+.. code-block:: python
+
+    df.recommendation["Enhance"]
+
+.. image:: ../img/export-7.png
+  :width: 700
+  :align: center
+
+
+Exporting Entire Dataframe Widgets
+-----------------------------------
+
+Let's say that you are interested in export all the visualizations recommended by Lux, you can export the dataframe widget to an interactive HTML report or data application to share with others. By integrating Lux with `DataPane <https://datapane.com/>`_, `Panel <https://panel.holoviz.org/>`_ or `Streamlit <https://streamlit.io/>`_, you can intersperse Markdown text, images, and other rich graphics and widgets, alongside the output widget generated by Lux to compose and author your own data science narrative.
+
+Exporting to HTML Report
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can export the visualizations into a static HTML using the following command:
+
+.. code-block:: python
+
+    df.save_as_html()
+
+By default, the file is saved as `export.html`, you can optionally specify the HTML filename in the input parameter.
+
+.. code-block:: python
+
+    df.save_as_html('hpi.html')
+
+If you would like to output HTML be returned directly via :code:`save_as_html`, you can input the parameter :code:`output=True`.
+
+.. code-block:: python
+
+  html_content  = df.save_as_html(output=True)
+
+The HTML export functionality is the basis for exporting to interactive data apps, described next.
+
+Exporting to DataPane
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`DataPane <https://datapane.com/>`_ is a Python library used for creating interactive data science reports. To integrate Lux with a DataPane report, you can wrap the HTML widget output with a `dp.HTML <https://docs.datapane.com/reports/blocks/text-code-and-html#html>`_ block as shown below:
+
+.. code-block:: python
+
+  import pandas as pd
+  import datapane as dp
+  import lux
+  df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/hpi.csv")
+  html_content = df.save_as_html(output=True)
+  dp.Report("## Analysis of Happy Planet Index Dataset\n Check out these cool visualizations!",
+    dp.HTML(html_content)
+  ).save(path='report.html', open=True)
+
+You should find that a webpage :code:`report.html` that pops up automatically.
+
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-datapane.png?raw=true
+  :width: 700
+  :align: center
+
+Exporting to Panel
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`Panel <https://panel.holoviz.org/>`_ is a powerful Python library for interactive data exploration and data apps. To integrate Lux with Panel, you can wrap the HTML widget output with a `panel.pane.HTML <https://panel.holoviz.org/reference/panes/HTML.html>`_ pane as shown below:
+
+.. code-block:: python
+
+  import html
+
+  import pandas as pd
+  import panel as pn
+  import lux
+
+  pn.extension(sizing_mode="stretch_width")
+
+  df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/hpi.csv")
+
+  def save_as_iframe(df, style="width:100%;height:100%", frameborder="0"):
+      html_content = df.save_as_html(output=True)
+      html_content = html.escape(html_content)
+      return f"""<iframe srcdoc="{html_content}" style={style} frameborder="{frameborder}"
+  "allowfullscreen></iframe>"""
+
+  lux_iframe_report = save_as_iframe(df)
+
+  # Can display in notebook
+  lux_panel = pn.pane.HTML(lux_iframe_report, height=425)
+
+  # Can display as app with panel serve
+  pn.template.FastListTemplate(site="💡 Lux and Panel", title="Analysis of Happy Planet Index Dataset", main=[lux_panel]).servable();
+
+After running :code:`panel serve app.ipynb` or :code:`panel serve app.py` in the command line, you should find the Panel app hosted in your browser (e.g., :code:`http://localhost:5006/app`):
+
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-panel.png?raw=true
+  :width: 700
+  :align: center
+
+Exporting to Streamlit
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`Streamlit <https://streamlit.io/>`_ is a Python library that simplifies the process of creating interactive data apps that can be shared and deployed anywhere.
+To integrate Lux with Streamlit, you can wrap the HTML widget output with a `Streamlit Component <https://docs.streamlit.io/en/stable/streamlit_components.html>`_. As shown below, you can create a file called :code:`app.py`:
+
+.. code-block:: python
+
+  import streamlit as st
+  import streamlit.components.v1 as components
+  from pathlib import Path
+  import pandas as pd
+  import lux
+
+  def app():
+      st.title('Analysis of Happy Planet Index Dataset')
+      st.write('Check out these cool visualizations!')
+      df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/hpi.csv")
+      export_file = 'visualizations.html'
+      html_content = df.save_as_html(output=True)
+      components.html(html_content, width=800, height=350)
+
+  app()
+
+After running :code:`streamlit run app.py` in the command line, you should find the Streamlit app hosted in your browser (e.g., :code:`localhost:8501`):
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/export-streamlit.png?raw=true
+  :width: 700
+  :align: center
\ No newline at end of file
diff --git a/doc/source/reference/about.rst b/doc/source/reference/about.rst
new file mode 100644
index 00000000..0463ea9f
--- /dev/null
+++ b/doc/source/reference/about.rst
@@ -0,0 +1,17 @@
+About
+=======
+
+Lux is a project developed at the `RISE Lab <https://rise.cs.berkeley.edu/>`_ and `School of Information <https://www.ischool.berkeley.edu/>`_ at UC Berkeley. A full list of Github contributors can be found `here <https://github.com/lux-org/lux/graphs/contributors>`_.
+
+If you use Lux for an academic publication, please consider citing `our paper <https://arxiv.org/pdf/2105.00121.pdf>`_:
+
+.. code-block:: latex
+
+    @misc{lee2021lux,
+        title={Lux: Always-on Visualization Recommendations for Exploratory Data Science}, 
+        author={Doris Jung-Lin Lee and Dixin Tang and Kunal Agarwal and Thyne Boonmark and Caitlyn Chen and Jake Kang and Ujjaini Mukhopadhyay and Jerry Song and Micah Yong and Marti A. Hearst and Aditya G. Parameswaran},
+        year={2021},
+        eprint={2105.00121},
+        archivePrefix={arXiv},
+        primaryClass={cs.DB}
+    }
\ No newline at end of file
diff --git a/doc/source/reference/config.rst b/doc/source/reference/config.rst
index a68835c7..5c45f7d5 100644
--- a/doc/source/reference/config.rst
+++ b/doc/source/reference/config.rst
@@ -65,6 +65,43 @@ If you try to set the default_display to anything other than 'lux' or 'pandas,'
   :width: 700
   :align: center
 
+
+Change the plotting scale 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We can adjust the sizes of charts displayed in the Lux widget based on a :code:`plotting_scale` applied to all charts. The default plotting scale is set to 1, displayed as followed: 
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/scale-0.png?raw=true
+  :width: 700
+  :align: center
+
+In the following block, we set the :code:`plotting_scale` to 2. We see that the Lux widget displays charts two times larger than the default. 
+
+.. code-block:: python
+
+    lux.config.plotting_scale = 2 
+    df
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/scale-1.png?raw=true
+  :width: 700
+  :align: center
+
+When further operations are performed (e.g., specifying intent), the user-defined plotting scale continues to apply to both the current visualization and recommended charts. In addition to specifying :code:`plotting_scale` as a positive float (i.e., >1.0, enlarging the charts), :code:`plotting_scale` can also take in any positive float between [0,1] to shrink the size of the charts.
+
+
+.. code-block:: python
+
+    lux.config.plotting_scale = 0.5
+    df
+
+.. image:: https://github.com/lux-org/lux-resources/blob/master/doc_img/scale-3.png?raw=true
+  :width: 700
+  :align: center
+
+The :code:`plotting_scale` config takes in any positive float or integer and functions for both Altair and Matplotlib plotting backends.
+
+
+
 Change plotting backend for rendering visualizations in Lux
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/doc/source/reference/gallery.rst b/doc/source/reference/gallery.rst
index d7fd7910..c39a5f2f 100644
--- a/doc/source/reference/gallery.rst
+++ b/doc/source/reference/gallery.rst
@@ -43,7 +43,7 @@ Here are some teaching resources on Lux. The materials are suited for a 1-1.5 ho
 
 2. How to create visualizations using :code:`Vis` and :code:`VisList`? [`Source <https://github.com/lux-org/lux-binder/blob/master/exercise/2-Quick-Vis.ipynb>`_] [`Live Notebook <https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/exercise/2-Quick-Vis.ipynb>`_]
 
-3. How to export selected visualization(s) out of the notebook for sharing? [`Source <https://github.com/lux-org/lux-binder/blob/master/exercise/3-Export-Widget.ipynb>`_] [`Live Notebook <https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/exercise/3-Export-Widget.ipynb>`_]
+3. How to export selected visualization for downstream sharing? [`Source <https://github.com/lux-org/lux-binder/blob/master/exercise/3-Export-Widget.ipynb>`_] [`Live Notebook <https://mybinder.org/v2/gh/lux-org/lux-binder/master?urlpath=tree/exercise/3-Export-Widget.ipynb>`_]
 
 Here are the `solutions <https://github.com/lux-org/lux-binder/blob/master/exercise/solution.txt>`_ to the notebook exercises.
 
diff --git a/doc/source/reference/gen/lux._config.config.Config.rst b/doc/source/reference/gen/lux._config.config.Config.rst
index bd257d73..c529beec 100644
--- a/doc/source/reference/gen/lux._config.config.Config.rst
+++ b/doc/source/reference/gen/lux._config.config.Config.rst
@@ -1,4 +1,4 @@
-lux.\_config.config.Config
+﻿lux.\_config.config.Config
 ==========================
 
 .. currentmodule:: lux._config.config
@@ -32,6 +32,7 @@ lux.\_config.config.Config
       ~Config.interestingness_fallback
       ~Config.pandas_fallback
       ~Config.plotting_backend
+      ~Config.plotting_scale
       ~Config.sampling
       ~Config.sampling_cap
       ~Config.sampling_start
diff --git a/doc/source/reference/gen/lux.core.frame.LuxDataFrame.rst b/doc/source/reference/gen/lux.core.frame.LuxDataFrame.rst
index 7022de85..940caa39 100644
--- a/doc/source/reference/gen/lux.core.frame.LuxDataFrame.rst
+++ b/doc/source/reference/gen/lux.core.frame.LuxDataFrame.rst
@@ -41,6 +41,7 @@
       ~LuxDataFrame.combine
       ~LuxDataFrame.combine_first
       ~LuxDataFrame.compare
+      ~LuxDataFrame.compute_metadata
       ~LuxDataFrame.convert_dtypes
       ~LuxDataFrame.copy
       ~LuxDataFrame.copy_intent
diff --git a/doc/source/reference/gen/lux.executor.Executor.Executor.rst b/doc/source/reference/gen/lux.executor.Executor.Executor.rst
index c45473c6..9a0dbbc5 100644
--- a/doc/source/reference/gen/lux.executor.Executor.Executor.rst
+++ b/doc/source/reference/gen/lux.executor.Executor.Executor.rst
@@ -1,4 +1,4 @@
-lux.executor.Executor.Executor
+﻿lux.executor.Executor.Executor
 ==============================
 
 .. currentmodule:: lux.executor.Executor
@@ -19,6 +19,7 @@ lux.executor.Executor.Executor
       ~Executor.compute_data_type
       ~Executor.compute_stats
       ~Executor.execute
+      ~Executor.execute_2D_binning
       ~Executor.execute_aggregate
       ~Executor.execute_binning
       ~Executor.execute_filter
diff --git a/doc/source/reference/gen/lux.executor.PandasExecutor.PandasExecutor.rst b/doc/source/reference/gen/lux.executor.PandasExecutor.PandasExecutor.rst
index a65b633e..c464971e 100644
--- a/doc/source/reference/gen/lux.executor.PandasExecutor.PandasExecutor.rst
+++ b/doc/source/reference/gen/lux.executor.PandasExecutor.PandasExecutor.rst
@@ -1,4 +1,4 @@
-lux.executor.PandasExecutor.PandasExecutor
+﻿lux.executor.PandasExecutor.PandasExecutor
 ==========================================
 
 .. currentmodule:: lux.executor.PandasExecutor
@@ -23,6 +23,7 @@ lux.executor.PandasExecutor.PandasExecutor
       ~PandasExecutor.execute
       ~PandasExecutor.execute_2D_binning
       ~PandasExecutor.execute_aggregate
+      ~PandasExecutor.execute_approx_sample
       ~PandasExecutor.execute_binning
       ~PandasExecutor.execute_filter
       ~PandasExecutor.execute_sampling
diff --git a/examples/mysql_query_template.txt b/examples/mysql_query_template.txt
index ee7446c4..b70be61e 100644
--- a/examples/mysql_query_template.txt
+++ b/examples/mysql_query_template.txt
@@ -1,6 +1,6 @@
 preview_query:SELECT * from {table_name} LIMIT {num_rows}
 length_query:SELECT COUNT(*) as length FROM {table_name} {where_clause}
-sample_query:SELECT {columns} FROM {table_name} {where_clause} LIMIT {num_rows}
+sample_query:SELECT * FROM {table_name} {where_clause} LIMIT {num_rows}
 scatter_query:SELECT {columns} FROM {table_name} {where_clause}
 colored_barchart_counts:SELECT {groupby_attr}, {color_attr}, COUNT({groupby_attr}) as count FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
 colored_barchart_average:SELECT {groupby_attr}, {color_attr}, AVG({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
@@ -10,8 +10,8 @@ barchart_counts:SELECT {groupby_attr}, COUNT({groupby_attr}) as count FROM {tabl
 barchart_average:SELECT {groupby_attr}, AVG({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
 barchart_sum:SELECT {groupby_attr}, SUM({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
 barchart_max:SELECT {groupby_attr}, MAX({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
-histogram_counts:SELECT width_bucket, COUNT(width_bucket) FROM (SELECT width_bucket(CAST ({} AS FLOAT), {}) FROM {} {}) as Buckets GROUP BY width_bucket ORDER BY width_bucket
-heatmap_counts:SELECT width_bucket1, width_bucket2, count(*) FROM (SELECT width_bucket(CAST ({} AS FLOAT), {}) as width_bucket1, width_bucket(CAST ({} AS FLOAT), {}) as width_bucket2 FROM {} {}) as foo GROUP BY width_bucket1, width_bucket2
+histogram_counts:SELECT width_bucket, count(width_bucket) as count from (SELECT ({bucket_cases}) as width_bucket from {table_name} {where_clause}) as buckets GROUP BY width_bucket order by width_bucket
+heatmap_counts:SELECT width_bucket1, width_bucket2, count(*) as count FROM (SELECT ({bucket_cases1}) as width_bucket1, ({bucket_cases2}) as width_bucket2 FROM {table_name} {where_clause}) as labeled_data GROUP BY width_bucket1, width_bucket2
 table_attributes_query:SELECT COLUMN_NAME as column_name FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '{table_name}'
 min_max_query:SELECT MIN({attribute}) as min, MAX({attribute}) as max FROM {table_name}
 cardinality_query:SELECT COUNT(Distinct({attribute})) as count FROM {table_name} WHERE {attribute} IS NOT NULL
diff --git a/examples/postgres_query_template.txt b/examples/postgres_query_template.txt
index ae4a0000..f004a169 100644
--- a/examples/postgres_query_template.txt
+++ b/examples/postgres_query_template.txt
@@ -1,6 +1,6 @@
 preview_query:SELECT * from {table_name} LIMIT {num_rows}
 length_query:SELECT COUNT(1) as length FROM {table_name} {where_clause}
-sample_query:SELECT {columns} FROM {table_name} {where_clause} ORDER BY random() LIMIT {num_rows}
+sample_query:SELECT * FROM {table_name} {where_clause} ORDER BY random() LIMIT {num_rows}
 scatter_query:SELECT {columns} FROM {table_name} {where_clause}
 colored_barchart_counts:SELECT "{groupby_attr}", "{color_attr}", COUNT("{groupby_attr}") FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
 colored_barchart_average:SELECT "{groupby_attr}", "{color_attr}", AVG("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
diff --git a/lux/_config/config.py b/lux/_config/config.py
index 23316868..efde7475 100644
--- a/lux/_config/config.py
+++ b/lux/_config/config.py
@@ -26,19 +26,31 @@ def __init__(self):
         # flags whether or not an action has been registered or removed and should be re-rendered by frame.py
         self.update_actions: Dict[str, bool] = {}
         self.update_actions["flag"] = False
-        self._sampling_start = 10000
-        self._sampling_cap = 30000
-        self._sampling_flag = True
-        self._heatmap_flag = True
         self._plotting_backend = "vegalite"
+        self._plotting_scale = 1
         self._topk = 15
         self._sort = "descending"
         self._pandas_fallback = True
         self._interestingness_fallback = True
         self.heatmap_bin_size = 40
-        self.tracer_relevant_lines=[]
+        self.tracer_relevant_lines = []
         self.tracer = LuxTracer()
         self.query_templates = {}
+        self.handle_quotes = True
+        #####################################
+        #### Optimization Configurations ####
+        #####################################
+        self._sampling_start = 100000
+        self._sampling_cap = 1000000
+        self._sampling_flag = True
+        self._heatmap_flag = True
+        self.lazy_maintain = True
+        self.early_pruning = True
+        self.early_pruning_sample_cap = 30000
+        # Apply sampling only if the dataset is 150% larger than the sample cap
+        self.early_pruning_sample_start = self.early_pruning_sample_cap * 1.5
+        self.streaming = False
+        self.render_widget = True
 
     @property
     def topk(self):
@@ -289,6 +301,26 @@ def plotting_backend(self, type: str) -> None:
                 stacklevel=2,
             )
 
+    @property
+    def plotting_scale(self):
+        return self._plotting_scale
+
+    @plotting_scale.setter
+    def plotting_scale(self, scale: float) -> None:
+        """
+        Set the scale factor for charts displayed in Lux.
+        ----------
+        type : float (default = 1.0)
+        """
+        scale = float(scale) if isinstance(scale, int) else scale
+        if isinstance(scale, float) and scale > 0:
+            self._plotting_scale = scale
+        else:
+            warnings.warn(
+                "Scaling factor for charts must be a positive float.",
+                stacklevel=2,
+            )
+
     def _get_action(self, pat: str, silent: bool = False):
         return lux.actions[pat]
 
@@ -351,22 +383,22 @@ def set_SQL_connection(self, connection):
         self.SQLconnection = connection
 
     def read_query_template(self, query_file):
+        from lux.executor.SQLExecutor import SQLExecutor
+
         query_dict = {}
         with open(query_file) as f:
             for line in f:
-               (key, val) = line.split(":")
-               query_dict[key] = val.strip()
+                (key, val) = line.split(":")
+                query_dict[key] = val.strip()
         self.query_templates = query_dict
+        self.executor = SQLExecutor()
 
     def set_executor_type(self, exe):
         if exe == "SQL":
             from lux.executor.SQLExecutor import SQLExecutor
 
             self.executor = SQLExecutor()
-        elif exe == "GeneralDatabase":
-            from lux.executor.GeneralDatabaseExecutor import GeneralDatabaseExecutor
-
-            self.executor = GeneralDatabaseExecutor()
+            self.read_query_template("../lux/lux/_config/postgres_query_template.txt")
         elif exe == "Pandas":
             from lux.executor.PandasExecutor import PandasExecutor
 
diff --git a/lux/_config/mysql_query_template.txt b/lux/_config/mysql_query_template.txt
new file mode 100644
index 00000000..b70be61e
--- /dev/null
+++ b/lux/_config/mysql_query_template.txt
@@ -0,0 +1,19 @@
+preview_query:SELECT * from {table_name} LIMIT {num_rows}
+length_query:SELECT COUNT(*) as length FROM {table_name} {where_clause}
+sample_query:SELECT * FROM {table_name} {where_clause} LIMIT {num_rows}
+scatter_query:SELECT {columns} FROM {table_name} {where_clause}
+colored_barchart_counts:SELECT {groupby_attr}, {color_attr}, COUNT({groupby_attr}) as count FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
+colored_barchart_average:SELECT {groupby_attr}, {color_attr}, AVG({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
+colored_barchart_sum:SELECT {groupby_attr}, {color_attr}, SUM({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
+colored_barchart_max:SELECT {groupby_attr}, {color_attr}, MAX({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}, {color_attr}
+barchart_counts:SELECT {groupby_attr}, COUNT({groupby_attr}) as count FROM {table_name} {where_clause} GROUP BY {groupby_attr}
+barchart_average:SELECT {groupby_attr}, AVG({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
+barchart_sum:SELECT {groupby_attr}, SUM({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
+barchart_max:SELECT {groupby_attr}, MAX({measure_attr}) as {measure_attr} FROM {table_name} {where_clause} GROUP BY {groupby_attr}
+histogram_counts:SELECT width_bucket, count(width_bucket) as count from (SELECT ({bucket_cases}) as width_bucket from {table_name} {where_clause}) as buckets GROUP BY width_bucket order by width_bucket
+heatmap_counts:SELECT width_bucket1, width_bucket2, count(*) as count FROM (SELECT ({bucket_cases1}) as width_bucket1, ({bucket_cases2}) as width_bucket2 FROM {table_name} {where_clause}) as labeled_data GROUP BY width_bucket1, width_bucket2
+table_attributes_query:SELECT COLUMN_NAME as column_name FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '{table_name}'
+min_max_query:SELECT MIN({attribute}) as min, MAX({attribute}) as max FROM {table_name}
+cardinality_query:SELECT COUNT(Distinct({attribute})) as count FROM {table_name} WHERE {attribute} IS NOT NULL
+unique_query:SELECT Distinct({attribute}) FROM {table_name} WHERE {attribute} IS NOT NULL
+datatype_query:SELECT DATA_TYPE as data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{table_name}' AND COLUMN_NAME = '{attribute}'
\ No newline at end of file
diff --git a/lux/_config/postgres_query_template.txt b/lux/_config/postgres_query_template.txt
new file mode 100644
index 00000000..f004a169
--- /dev/null
+++ b/lux/_config/postgres_query_template.txt
@@ -0,0 +1,19 @@
+preview_query:SELECT * from {table_name} LIMIT {num_rows}
+length_query:SELECT COUNT(1) as length FROM {table_name} {where_clause}
+sample_query:SELECT * FROM {table_name} {where_clause} ORDER BY random() LIMIT {num_rows}
+scatter_query:SELECT {columns} FROM {table_name} {where_clause}
+colored_barchart_counts:SELECT "{groupby_attr}", "{color_attr}", COUNT("{groupby_attr}") FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
+colored_barchart_average:SELECT "{groupby_attr}", "{color_attr}", AVG("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
+colored_barchart_sum:SELECT "{groupby_attr}", "{color_attr}", SUM("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
+colored_barchart_max:SELECT "{groupby_attr}", "{color_attr}", MAX("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}", "{color_attr}"
+barchart_counts:SELECT "{groupby_attr}", COUNT("{groupby_attr}") FROM {table_name} {where_clause} GROUP BY "{groupby_attr}"
+barchart_average:SELECT "{groupby_attr}", AVG("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}"
+barchart_sum:SELECT "{groupby_attr}", SUM("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}"
+barchart_max:SELECT "{groupby_attr}", MAX("{measure_attr}") as "{measure_attr}" FROM {table_name} {where_clause} GROUP BY "{groupby_attr}"
+histogram_counts:SELECT width_bucket, COUNT(width_bucket) FROM (SELECT width_bucket(CAST ("{bin_attribute}" AS FLOAT), '{upper_edges}') FROM {table_name} {where_clause}) as Buckets GROUP BY width_bucket ORDER BY width_bucket
+heatmap_counts:SELECT width_bucket1, width_bucket2, count(*) FROM (SELECT width_bucket(CAST ("{x_attribute}" AS FLOAT), '{x_upper_edges_string}') as width_bucket1, width_bucket(CAST ("{y_attribute}" AS FLOAT), '{y_upper_edges_string}') as width_bucket2 FROM {table_name} {where_clause}) as foo GROUP BY width_bucket1, width_bucket2
+table_attributes_query:SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '{table_name}'
+min_max_query:SELECT MIN("{attribute}") as min, MAX("{attribute}") as max FROM {table_name}
+cardinality_query:SELECT Count(Distinct("{attribute}")) FROM {table_name} WHERE "{attribute}" IS NOT NULL
+unique_query:SELECT Distinct("{attribute}") FROM {table_name} WHERE "{attribute}" IS NOT NULL
+datatype_query:SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{table_name}' AND COLUMN_NAME = '{attribute}'
\ No newline at end of file
diff --git a/lux/action/default.py b/lux/action/default.py
index e8c2e5c9..30b61644 100644
--- a/lux/action/default.py
+++ b/lux/action/default.py
@@ -6,6 +6,7 @@ def register_default_actions():
     from lux.action.enhance import enhance
     from lux.action.filter import add_filter
     from lux.action.generalize import generalize
+    from lux.action.temporal import temporal
 
     # display conditions for default actions
     no_vis = lambda ldf: (ldf.current_vis is None) or (
@@ -18,7 +19,7 @@ def register_default_actions():
     lux.config.register_action("correlation", correlation, no_vis)
     lux.config.register_action("distribution", univariate, no_vis, "quantitative")
     lux.config.register_action("occurrence", univariate, no_vis, "nominal")
-    lux.config.register_action("temporal", univariate, no_vis, "temporal")
+    lux.config.register_action("temporal", temporal, no_vis)
     lux.config.register_action("geographical", univariate, no_vis, "geographical")
 
     lux.config.register_action("Enhance", enhance, one_current_vis)
diff --git a/lux/action/temporal.py b/lux/action/temporal.py
new file mode 100644
index 00000000..6bf831ce
--- /dev/null
+++ b/lux/action/temporal.py
@@ -0,0 +1,128 @@
+#  Copyright 2019-2020 The Lux Authors.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import lux
+from lux.vis.VisList import VisList
+from lux.vis.Vis import Vis
+import pandas as pd
+from lux.core.frame import LuxDataFrame
+from lux.interestingness.interestingness import interestingness
+from lux.utils import utils
+
+
+def temporal(ldf):
+    """
+    Generates line charts for temporal fields at different granularities.
+    Parameters
+    ----------
+    ldf : lux.core.frame
+            LuxDataFrame with underspecified intent.
+    Returns
+    -------
+    recommendations : Dict[str,obj]
+            Object with a collection of visualizations that result from the Temporal action.
+    """
+    vlist = []
+    recommendation = {
+        "action": "Temporal",
+        "description": "Show trends over <p class='highlight-descriptor'>time-related</p> attributes.",
+        "long_description": "Temporal displays line charts for all attributes related to datetimes in the dataframe.",
+    }
+    for c in ldf.columns:
+        if ldf.data_type[c] == "temporal":
+            try:
+                generated_vis = create_temporal_vis(ldf, c)
+                vlist.extend(generated_vis)
+            except:
+                pass
+
+    # If no temporal visualizations were generated via parsing datetime, fallback to default behavior.
+    if len(vlist) == 0:
+        intent = [lux.Clause("?", data_type="temporal")]
+        intent.extend(utils.get_filter_specs(ldf._intent))
+        vlist = VisList(intent, ldf)
+        for vis in vlist:
+            vis.score = interestingness(vis, ldf)
+    else:
+        vlist = VisList(vlist)
+        recommendation["long_description"] += (
+            " Lux displays the overall temporal trend first,"
+            + " followed by trends across other timescales (e.g., year, month, week, day)."
+        )
+
+    # Doesn't make sense to generate a line chart if there is less than 3 datapoints (pre-aggregated)
+    if len(ldf) < 3:
+        recommendation["collection"] = []
+        return recommendation
+    vlist.sort()
+    recommendation["collection"] = vlist
+    return recommendation
+
+
+def create_temporal_vis(ldf, col):
+    """
+    Creates and populates Vis objects for different timescales in the provided temporal column.
+    Parameters
+    ----------
+    ldf : lux.core.frame
+            LuxDataFrame with underspecified intent.
+
+    col : str
+            Name of temporal column.
+
+    Returns
+    -------
+    vlist : [Vis]
+            Collection of Vis objects.
+    """
+    formatted_date = pd.to_datetime(ldf[col], format="%Y-%m-%d")
+
+    overall_vis = Vis([lux.Clause(col, data_type="temporal")], source=ldf, score=5)
+
+    year_col = col + " (year)"
+    year_df = LuxDataFrame({year_col: pd.to_datetime(formatted_date.dt.year, format="%Y")})
+    year_vis = Vis([lux.Clause(year_col, data_type="temporal")], source=year_df, score=4)
+
+    month_col = col + " (month)"
+    month_df = LuxDataFrame({month_col: formatted_date.dt.month})
+    month_vis = Vis(
+        [lux.Clause(month_col, data_type="temporal", timescale="month")], source=month_df, score=3
+    )
+
+    day_col = col + " (day)"
+    day_df = LuxDataFrame({day_col: formatted_date.dt.day})
+    day_df.set_data_type(
+        {day_col: "nominal"}
+    )  # Since day is high cardinality 1-31, it can get recognized as quantitative
+    day_vis = Vis([lux.Clause(day_col, data_type="temporal", timescale="day")], source=day_df, score=2)
+
+    week_col = col + " (day of week)"
+    week_df = lux.LuxDataFrame({week_col: formatted_date.dt.dayofweek})
+    week_vis = Vis(
+        [lux.Clause(week_col, data_type="temporal", timescale="day of week")], source=week_df, score=1
+    )
+
+    unique_year_values = len(year_df[year_col].unique())
+    unique_month_values = len(month_df[month_col].unique())
+    unique_week_values = len(week_df[week_col].unique())
+    vlist = []
+
+    vlist.append(overall_vis)
+    if unique_year_values != 1:
+        vlist.append(year_vis)
+    if unique_month_values != 1:
+        vlist.append(month_vis)
+    if unique_week_values != 1:
+        vlist.append(week_vis)
+    return vlist
diff --git a/lux/action/univariate.py b/lux/action/univariate.py
index e94af9bd..1e6d357e 100644
--- a/lux/action/univariate.py
+++ b/lux/action/univariate.py
@@ -89,17 +89,6 @@ def univariate(ldf, *args):
             "description": "Show choropleth maps of <p class='highlight-descriptor'>geographic</p> attributes",
             "long_description": f"Occurence displays choropleths of averages for some geographic attribute{examples}. Visualizations are ranked by diversity of the geographic attribute.",
         }
-    elif data_type_constraint == "temporal":
-        intent = [lux.Clause("?", data_type="temporal")]
-        intent.extend(filter_specs)
-        recommendation = {
-            "action": "Temporal",
-            "description": "Show trends over <p class='highlight-descriptor'>time-related</p> attributes.",
-            "long_description": "Temporal displays line charts for all attributes related to datetimes in the dataframe.",
-        }
-        # Doesn't make sense to generate a line chart if there is less than 3 datapoints (pre-aggregated)
-        if len(ldf) < 3:
-            ignore_rec_flag = True
     if ignore_rec_flag:
         recommendation["collection"] = []
         return recommendation
diff --git a/lux/core/frame.py b/lux/core/frame.py
index e8d58527..29f0f2a9 100644
--- a/lux/core/frame.py
+++ b/lux/core/frame.py
@@ -76,9 +76,10 @@ def __init__(self, *args, **kw):
         else:
             from lux.executor.SQLExecutor import SQLExecutor
 
-            #lux.config.executor = SQLExecutor()
+            # lux.config.executor = SQLExecutor()
 
         self._sampled = None
+        self._approx_sample = None
         self._toggle_pandas_display = True
         self._message = Message()
         self._pandas_only = False
@@ -115,43 +116,59 @@ def data_type(self):
             self.maintain_metadata()
         return self._data_type
 
+    def compute_metadata(self) -> None:
+        """
+        Compute dataset metadata and statistics
+        """
+        if len(self) > 0:
+            if lux.config.executor.name != "SQLExecutor":
+                lux.config.executor.compute_stats(self)
+            lux.config.executor.compute_dataset_metadata(self)
+            self._infer_structure()
+            self._metadata_fresh = True
+
     def maintain_metadata(self):
+        """
+        Maintain dataset metadata and statistics (Compute only if needed)
+        """
         is_sql_tbl = lux.config.executor.name != "PandasExecutor"
 
         if lux.config.SQLconnection != "" and is_sql_tbl:
             from lux.executor.SQLExecutor import SQLExecutor
 
-            #lux.config.executor = SQLExecutor()
+            # lux.config.executor = SQLExecutor()
 
         # Check that metadata has not yet been computed
-        if not hasattr(self, "_metadata_fresh") or not self._metadata_fresh:
-            # only compute metadata information if the dataframe is non-empty
-            if len(self) > 0:
-                lux.config.executor.compute_stats(self)
-                lux.config.executor.compute_dataset_metadata(self)
-                self._infer_structure()
-                self._metadata_fresh = True
+        if lux.config.lazy_maintain:
+            # Check that metadata has not yet been computed
+            if not hasattr(self, "_metadata_fresh") or not self._metadata_fresh:
+                # only compute metadata information if the dataframe is non-empty
+                self.compute_metadata()
+        else:
+            self.compute_metadata()
 
-    def expire_recs(self):
+    def expire_recs(self) -> None:
         """
         Expires and resets all recommendations
         """
-        self._recs_fresh = False
-        self._recommendation = {}
-        self._widget = None
-        self._rec_info = None
-        self._sampled = None
+        if lux.config.lazy_maintain:
+            self._recs_fresh = False
+            self._recommendation = {}
+            self._widget = None
+            self._rec_info = None
+            self._sampled = None
 
-    def expire_metadata(self):
+    def expire_metadata(self) -> None:
         """
         Expire all saved metadata to trigger a recomputation the next time the data is required.
         """
-        self._metadata_fresh = False
-        self._data_type = None
-        self.unique_values = None
-        self.cardinality = None
-        self._min_max = None
-        self.pre_aggregated = None
+        if lux.config.lazy_maintain:
+            self._metadata_fresh = False
+            self._data_type = None
+            self.unique_values = None
+            self.cardinality = None
+            self._min_max = None
+            self.pre_aggregated = None
 
     #####################
     ## Override Pandas ##
@@ -342,7 +359,7 @@ def _append_rec(self, rec_infolist, recommendations: Dict):
             rec_infolist.append(recommendations)
 
     def show_all_column_vis(self):
-        if self.intent == [] or self.intent is None:
+        if len(self.columns) > 1 and len(self.columns) < 4 and self.intent == [] or self.intent is None:
             vis = Vis(list(self.columns), self)
             if vis.mark != "":
                 vis._all_column = True
@@ -354,6 +371,7 @@ def maintain_recs(self, is_series="DataFrame"):
         if lux.config.update_actions["flag"] == True:
             self._recs_fresh = False
         show_prev = False  # flag indicating whether rec_df is showing previous df or current self
+
         if self._prev is not None:
             rec_df = self._prev
             rec_df._message = Message()
@@ -391,9 +409,15 @@ def maintain_recs(self, is_series="DataFrame"):
 
         rec_df._prev = None  # reset _prev
 
+        # If lazy, check that recs has not yet been computed
+        lazy_but_not_computed = lux.config.lazy_maintain and (
+            not hasattr(rec_df, "_recs_fresh") or not rec_df._recs_fresh
+        )
+        eager = not lux.config.lazy_maintain
+
         # Check that recs has not yet been computed
-        if not hasattr(rec_df, "_recs_fresh") or not rec_df._recs_fresh:
-            is_sql_tbl = lux.config.executor.name != "PandasExecutor"
+        if lazy_but_not_computed or eager:
+            is_sql_tbl = lux.config.executor.name == "SQLExecutor"
             rec_infolist = []
             from lux.action.row_group import row_group
             from lux.action.column_group import column_group
@@ -423,11 +447,13 @@ def maintain_recs(self, is_series="DataFrame"):
                     rec_df._recommendation[action_type] = vlist
             rec_df._rec_info = rec_infolist
             rec_df.show_all_column_vis()
-            self._widget = rec_df.render_widget()
+            if lux.config.render_widget:
+                self._widget = rec_df.render_widget()
         # re-render widget for the current dataframe if previous rec is not recomputed
         elif show_prev:
             rec_df.show_all_column_vis()
-            self._widget = rec_df.render_widget()
+            if lux.config.render_widget:
+                self._widget = rec_df.render_widget()
         self._recs_fresh = True
 
     #######################################################
@@ -658,6 +684,7 @@ def render_widget(self, renderer: str = "altair", input_current_vis=""):
             recommendations=widgetJSON["recommendation"],
             intent=LuxDataFrame.intent_to_string(self._intent),
             message=self._message.to_html(),
+            config={"plottingScale": lux.config.plotting_scale},
         )
 
     @staticmethod
@@ -726,7 +753,7 @@ def rec_to_JSON(recs):
                 del rec_lst[idx]["collection"]
         return rec_lst
 
-    def save_as_html(self, filename: str = "export.html") -> None:
+    def save_as_html(self, filename: str = "export.html", output=False):
         """
         Save dataframe widget as static HTML file
 
@@ -796,7 +823,7 @@ def save_as_html(self, filename: str = "export.html") -> None:
             </script>
             
             <div id="footer-description">
-            These visualizations were generated by <a href="https://github.com/lux-org/lux/"><img src="https://raw.githubusercontent.com/lux-org/lux-resources/master/logo/logo.png" width="65px" style="vertical-align: middle;"></img></a>
+            These visualizations were generated by <a href="https://github.com/lux-org/lux/" target="_blank" rel="noopener noreferrer"><img src="https://raw.githubusercontent.com/lux-org/lux-resources/master/logo/logo.png" width="65px" style="vertical-align: middle;"></img></a>
             </div>
 
         </body>
@@ -808,9 +835,12 @@ def save_as_html(self, filename: str = "export.html") -> None:
         rendered_template = html_template.format(
             header=header, manager_state=manager_state, widget_view=widget_view
         )
-        with open(filename, "w") as fp:
-            fp.write(rendered_template)
-            print(f"Saved HTML to {filename}")
+        if output:
+            return rendered_template
+        else:
+            with open(filename, "w") as fp:
+                fp.write(rendered_template)
+                print(f"Saved HTML to {filename}")
 
     # Overridden Pandas Functions
     def head(self, n: int = 5):
diff --git a/lux/core/joinedsqltable.py b/lux/core/joinedsqltable.py
index 6a5e0f47..87bd8b94 100644
--- a/lux/core/joinedsqltable.py
+++ b/lux/core/joinedsqltable.py
@@ -56,17 +56,17 @@ class JoinedSQLTable(lux.LuxSQLTable):
         "pre_aggregated",
         "_type_override",
         "joins",
-        "using_view"
+        "using_view",
     ]
 
     def __init__(self, *args, joins=[], **kw):
         super(JoinedSQLTable, self).__init__(*args, **kw)
         from lux.executor.SQLExecutor import SQLExecutor
-        
+
         lux.config.executor = SQLExecutor()
         # self._metadata.joins = []
         tables = self.extract_tables(joins)
-        if (len(tables) > 4):
+        if len(tables) > 4:
             warnings.warn(
                 f"\nPlease provide a maximum of 4 (Four) unique tables to ensure optimal performance.",
                 stacklevel=2,
@@ -84,34 +84,38 @@ def len(self):
     def extract_tables(self, joins):
         tables = set()
         for condition in joins:
-            lhs = condition[0:condition.index("=")].strip()
-            rhs = condition[condition.index("=") + 1:].strip()
-            table1 = lhs[0:lhs.index(".")].strip()
-            table2 = rhs[0:rhs.index(".")].strip()
+            lhs = condition[0 : condition.index("=")].strip()
+            rhs = condition[condition.index("=") + 1 :].strip()
+            table1 = lhs[0 : lhs.index(".")].strip()
+            table2 = rhs[0 : rhs.index(".")].strip()
             tables.add(table1)
             tables.add(table2)
         return tables
 
     def create_view(self, tables, joins):
         import psycopg2
+
         dbc = lux.config.SQLconnection.cursor()
         import time
+
         curr_time = str(int(time.time()))
         viewname = "lux_view_" + curr_time
         table_entry = ""
         for idx, table in enumerate(tables, 1):
             table_entry += table
-            if (idx < len(tables)):
+            if idx < len(tables):
                 table_entry += ", "
-        
+
         condition_entry = ""
         for idx, join in enumerate(joins, 1):
             condition_entry += join
-            if (idx < len(joins)):
+            if idx < len(joins):
                 condition_entry += " AND "
         try:
-        #     # s = "CREATE VIEW {} AS SELECT * FROM cars_join cj JOIN cars_power_join cpj using (id)".format(viewname)
-            s = "CREATE VIEW {} AS SELECT * FROM {} where {}".format(viewname, table_entry, condition_entry)
+            #     # s = "CREATE VIEW {} AS SELECT * FROM cars_join cj JOIN cars_power_join cpj using (id)".format(viewname)
+            s = "CREATE VIEW {} AS SELECT * FROM {} where {}".format(
+                viewname, table_entry, condition_entry
+            )
             # lux.config.executor.create_view(self)
             dbc.execute(s)
             lux.config.SQLconnection.commit()
@@ -155,7 +159,7 @@ def _ipython_display_(self):
                 layout=widgets.Layout(width="200px", top="6px", bottom="6px"),
             )
             self.output = widgets.Output()
-            lux.config.executor.execute_preview(self)
+            self._sampled = lux.config.executor.execute_preview(self)
             display(button, self.output)
 
             def on_button_clicked(b):
diff --git a/lux/core/series.py b/lux/core/series.py
index 2b902730..75e7602d 100644
--- a/lux/core/series.py
+++ b/lux/core/series.py
@@ -105,6 +105,25 @@ def to_pandas(self) -> pd.Series:
 
         return lux.core.originalSeries(self, copy=False)
 
+    def unique(self):
+        """
+        Overridden method for pd.Series.unique with cached results.
+        Return unique values of Series object.
+        Uniques are returned in order of appearance. Hash table-based unique,
+        therefore does NOT sort.
+        Returns
+        -------
+        ndarray or ExtensionArray
+            The unique values returned as a NumPy array.
+        See Also
+        --------
+        https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.unique.html
+        """
+        if self.unique_values and self.name in self.unique_values.keys():
+            return np.array(self.unique_values[self.name])
+        else:
+            return super(LuxSeries, self).unique()
+
     def _ipython_display_(self):
         from IPython.display import display
         from IPython.display import clear_output
diff --git a/lux/core/sqltable.py b/lux/core/sqltable.py
index 19731f4f..2e54f8d0 100644
--- a/lux/core/sqltable.py
+++ b/lux/core/sqltable.py
@@ -62,7 +62,7 @@ class LuxSQLTable(lux.LuxDataFrame):
     def __init__(self, *args, table_name="", **kw):
         super(LuxSQLTable, self).__init__(*args, **kw)
 
-        if lux.config.executor.name != 'GeneralDatabaseExecutor':
+        if lux.config.executor.name != "GeneralDatabaseExecutor":
             from lux.executor.SQLExecutor import SQLExecutor
 
             lux.config.executor = SQLExecutor()
diff --git a/lux/executor/Executor.py b/lux/executor/Executor.py
index 429525d7..f967def1 100644
--- a/lux/executor/Executor.py
+++ b/lux/executor/Executor.py
@@ -11,8 +11,9 @@
 #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
-
+from lux.core.frame import LuxDataFrame
 from lux.vis.VisList import VisList
+from lux.vis.Vis import Vis
 from lux.utils import utils
 
 
@@ -28,19 +29,38 @@ def __repr__(self):
         return f"<Executor>"
 
     @staticmethod
-    def execute(vis_collection: VisList, ldf):
+    def execute(vislist: VisList, ldf: LuxDataFrame, approx: bool = False):
+        """
+        Given a VisList, fetch the data required to render the vis.
+        """
         return NotImplemented
 
     @staticmethod
-    def execute_aggregate(vis, ldf):
+    def execute_aggregate(vis: Vis, ldf: LuxDataFrame):
+        """
+        Aggregate data points on an axis for bar or line charts
+        """
         return NotImplemented
 
     @staticmethod
-    def execute_binning(vis, ldf):
+    def execute_binning(ldf: LuxDataFrame, vis: Vis):
+        """
+        Binning of data points for generating histograms
+        """
         return NotImplemented
 
     @staticmethod
-    def execute_filter(vis, ldf):
+    def execute_filter(vis: Vis):
+        """
+        Apply a Vis's filter to vis.data
+        """
+        return NotImplemented
+
+    @staticmethod
+    def execute_2D_binning(vis: Vis):
+        """
+        Apply 2D binning (heatmap) to vis.data
+        """
         return NotImplemented
 
     @staticmethod
@@ -55,12 +75,10 @@ def compute_data_type(self):
     def compute_dataset_metadata(self, ldf):
         return NotImplemented
 
-    # @staticmethod
-    # def compute_data_model(self):
-    #     return NotImplemented
-
     def mapping(self, rmap):
         group_map = {}
+        if rmap == {}:
+            return {}
         for val in ["quantitative", "id", "nominal", "temporal", "geographical"]:
             group_map[val] = list(filter(lambda x: rmap[x] == val, rmap))
         return group_map
diff --git a/lux/executor/GeneralDatabaseExecutor.py b/lux/executor/GeneralDatabaseExecutor.py
deleted file mode 100644
index 99453bc8..00000000
--- a/lux/executor/GeneralDatabaseExecutor.py
+++ /dev/null
@@ -1,808 +0,0 @@
-import pandas
-from lux.vis.VisList import VisList
-from lux.vis.Vis import Vis
-from lux.core.sqltable import LuxSQLTable
-from lux.executor.Executor import Executor
-from lux.utils import utils
-from lux.utils.utils import check_import_lux_widget, check_if_id_like
-import lux
-
-import math
-
-
-class GeneralDatabaseExecutor(Executor):
-    """
-    Given a Vis objects with complete specifications, fetch and process data using SQL operations.
-    """
-
-    def __init__(self):
-        self.name = "GeneralDatabaseExecutor"
-        self.selection = []
-        self.tables = []
-        self.filters = ""
-
-    def __repr__(self):
-        return f"<GeneralDatabaseExecutor>"
-
-    @staticmethod
-    def execute_preview(tbl: LuxSQLTable, preview_size=5):
-        preview_query = lux.config.query_templates['preview_query']
-        output = pandas.read_sql(
-            preview_query.format(table_name = tbl.table_name, num_rows = preview_size), lux.config.SQLconnection
-        )
-        return output
-
-    @staticmethod
-    def execute_sampling(tbl: LuxSQLTable):
-        SAMPLE_FLAG = lux.config.sampling
-        SAMPLE_START = lux.config.sampling_start
-        SAMPLE_CAP = lux.config.sampling_cap
-        SAMPLE_FRAC = 0.2
-
-        length_query = pandas.read_sql(
-            lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = ""),
-            lux.config.SQLconnection,
-        )
-        limit = int(list(length_query["length"])[0]) * SAMPLE_FRAC
-        tbl._sampled = pandas.read_sql(
-            lux.config.query_templates['sample_query'].format(table_name = tbl.table_name, where_clause = "", num_rows = str(limit)), lux.config.SQLconnection
-        )
-
-    @staticmethod
-    def execute(view_collection: VisList, tbl: LuxSQLTable):
-        """
-        Given a VisList, fetch the data required to render the view
-        1) Generate Necessary WHERE clauses
-        2) Query necessary data, applying appropriate aggregation for the chart type
-        3) populates vis' data with a DataFrame with relevant results
-        """
-
-        for view in view_collection:
-            # choose execution method depending on vis mark type
-
-            # when mark is empty, deal with lazy execution by filling the data with a small sample of the dataframe
-            if view.mark == "":
-                GeneralDatabaseExecutor.execute_sampling(tbl)
-                view._vis_data = tbl._sampled
-            if view.mark == "scatter":
-                where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-                length_query = pandas.read_sql(
-                    lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-                    lux.config.SQLconnection,
-                )
-                view_data_length = list(length_query["length"])[0]
-                if len(view.get_attr_by_channel("color")) == 1 or view_data_length < 5000:
-                    # NOTE: might want to have a check somewhere to not use categorical variables with greater than some number of categories as a Color variable----------------
-                    has_color = True
-                    GeneralDatabaseExecutor.execute_scatter(view, tbl)
-                else:
-                    view._mark = "heatmap"
-                    GeneralDatabaseExecutor.execute_2D_binning(view, tbl)
-            elif view.mark == "bar" or view.mark == "line":
-                GeneralDatabaseExecutor.execute_aggregate(view, tbl)
-            elif view.mark == "histogram":
-                GeneralDatabaseExecutor.execute_binning(view, tbl)
-
-    @staticmethod
-    def execute_scatter(view: Vis, tbl: LuxSQLTable):
-        """
-        Given a scatterplot vis and a Lux Dataframe, fetch the data required to render the vis.
-        1) Generate WHERE clause for the SQL query
-        2) Check number of datapoints to be included in the query
-        3) If the number of datapoints exceeds 10000, perform a random sample from the original data
-        4) Query datapoints needed for the scatterplot visualization
-        5) return a DataFrame with relevant results
-
-        Parameters
-        ----------
-        vislist: list[lux.Vis]
-            vis list that contains lux.Vis objects for visualization.
-        tbl : lux.core.frame
-            LuxSQLTable with specified intent.
-
-        Returns
-        -------
-        None
-        """
-
-        attributes = set([])
-        for clause in view._inferred_intent:
-            if clause.attribute:
-                if clause.attribute != "Record":
-                    attributes.add(clause.attribute)
-        where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-
-        length_query = pandas.read_sql(
-            lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-            lux.config.SQLconnection,
-        )
-
-        def add_quotes(var_name):
-            return '"' + var_name + '"'
-
-        required_variables = attributes | set(filterVars)
-        required_variables = map(add_quotes, required_variables)
-        required_variables = ",".join(required_variables)
-        row_count = list(
-            pandas.read_sql(
-                lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-                lux.config.SQLconnection,
-            )["length"]
-        )[0]
-        if row_count > lux.config.sampling_cap:
-            query = lux.config.query_templates['sample_query'].format(columns = required_variables, table_name = tbl.table_name, where_clause = where_clause, num_rows = 10000)
-            #query = f"SELECT {required_variables} FROM {tbl.table_name} {where_clause} ORDER BY random() LIMIT 10000"
-        else:
-            query = lux.config.query_templates['scatter_query'].format(columns = required_variables, table_name = tbl.table_name, where_clause = where_clause)
-        data = pandas.read_sql(query, lux.config.SQLconnection)
-        view._vis_data = utils.pandas_to_lux(data)
-        view._query = query
-        # view._vis_data.length = list(length_query["length"])[0]
-
-        tbl._message.add_unique(
-            f"Large scatterplots detected: Lux is automatically binning scatterplots to heatmaps.",
-            priority=98,
-        )
-
-    @staticmethod
-    def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
-        """
-        Aggregate data points on an axis for bar or line charts
-        Parameters
-        ----------
-        vis: lux.Vis
-            lux.Vis object that represents a visualization
-        tbl : lux.core.frame
-            LuxSQLTable with specified intent.
-        isFiltered: boolean
-            boolean that represents whether a vis has had a filter applied to its data
-        Returns
-        -------
-        None
-        """
-        x_attr = view.get_attr_by_channel("x")[0]
-        y_attr = view.get_attr_by_channel("y")[0]
-        has_color = False
-        groupby_attr = ""
-        measure_attr = ""
-        if x_attr.aggregation is None or y_attr.aggregation is None:
-            return
-        if y_attr.aggregation != "":
-            groupby_attr = x_attr
-            measure_attr = y_attr
-            agg_func = y_attr.aggregation
-        if x_attr.aggregation != "":
-            groupby_attr = y_attr
-            measure_attr = x_attr
-            agg_func = x_attr.aggregation
-        if groupby_attr.attribute in tbl.unique_values.keys():
-            attr_unique_vals = tbl.unique_values[groupby_attr.attribute]
-        # checks if color is specified in the Vis
-        if len(view.get_attr_by_channel("color")) == 1:
-            color_attr = view.get_attr_by_channel("color")[0]
-            color_attr_vals = tbl.unique_values[color_attr.attribute]
-            color_cardinality = len(color_attr_vals)
-            # NOTE: might want to have a check somewhere to not use categorical variables with greater than some number of categories as a Color variable----------------
-            has_color = True
-        else:
-            color_cardinality = 1
-        if measure_attr != "":
-            # barchart case, need count data for each group
-            if measure_attr.attribute == "Record":
-                where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-
-                length_query = pandas.read_sql(
-                    lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-                    lux.config.SQLconnection,
-                )
-                # generates query for colored barchart case
-                if has_color:
-                    count_query = lux.config.query_templates['colored_barchart_counts'].format(
-                        groupby_attr = groupby_attr.attribute,
-                        color_attr = color_attr.attribute,
-                        #groupby_attr = groupby_attr.attribute,
-                        table_name = tbl.table_name,
-                        where_clause = where_clause,
-                        #groupby_attr = groupby_attr.attribute,
-                        #color_attr = color_attr.attribute,
-                    )
-                    view._vis_data = pandas.read_sql(count_query, lux.config.SQLconnection)
-                    view._vis_data = view._vis_data.rename(columns={"count": "Record"})
-                    view._vis_data = utils.pandas_to_lux(view._vis_data)
-                # generates query for normal barchart case
-                else:
-                    count_query = lux.config.query_templates['barchart_counts'].format(
-                        groupby_attr = groupby_attr.attribute,
-                        #groupby_attr = groupby_attr.attribute,
-                        table_name = tbl.table_name,
-                        where_clause = where_clause,
-                        #groupby_attr = groupby_attr.attribute,
-                    )
-                    view._vis_data = pandas.read_sql(count_query, lux.config.SQLconnection)
-                    view._vis_data = view._vis_data.rename(columns={"count": "Record"})
-                    view._vis_data = utils.pandas_to_lux(view._vis_data)
-                view._query = count_query
-                # view._vis_data.length = list(length_query["length"])[0]
-            # aggregate barchart case, need aggregate data (mean, sum, max) for each group
-            else:
-                where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-                length_query = pandas.read_sql(
-                    lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-                    lux.config.SQLconnection,
-                )
-                # generates query for colored barchart case
-                if has_color:
-                    if agg_func == "mean":
-                        agg_query = (
-                            lux.config.query_templates['colored_barchart_average'].format(
-                                groupby_attr = groupby_attr.attribute,
-                                color_attr = color_attr.attribute,
-                                measure_attr = measure_attr.attribute,
-                                #measure_attr = measure_attr.attribute,
-                                table_name = tbl.table_name,
-                                where_clause = where_clause,
-                                #groupby_attr = groupby_attr.attribute,
-                                #color_attr = color_attr.attribute,
-                            )
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                    if agg_func == "sum":
-                        agg_query = (
-                            lux.config.query_templates['colored_barchart_sum'].format(
-                                groupby_attr = groupby_attr.attribute,
-                                color_attr = color_attr.attribute,
-                                measure_attr = measure_attr.attribute,
-                                #measure_attr = measure_attr.attribute,
-                                table_name = tbl.table_name,
-                                where_clause = where_clause,
-                                #groupby_attr = groupby_attr.attribute,
-                                #color_attr = color_attr.attribute,
-                            )
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                    if agg_func == "max":
-                        agg_query = (
-                            lux.config.query_templates['colored_barchart_max'].format(
-                                groupby_attr = groupby_attr.attribute,
-                                color_attr = color_attr.attribute,
-                                measure_attr = measure_attr.attribute,
-                                #measure_attr = measure_attr.attribute,
-                                table_name = tbl.table_name,
-                                where_clause = where_clause,
-                                #groupby_attr = groupby_attr.attribute,
-                                #color_attr = color_attr.attribute,
-                            )
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                # generates query for normal barchart case
-                else:
-                    if agg_func == "mean":
-                        agg_query = lux.config.query_templates['barchart_average'].format(
-                            groupby_attr = groupby_attr.attribute,
-                            measure_attr = measure_attr.attribute,
-                            table_name = tbl.table_name,
-                            where_clause = where_clause,
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                    if agg_func == "sum":
-                        agg_query = lux.config.query_templates['barchart_sum'].format(
-                            groupby_attr = groupby_attr.attribute,
-                            measure_attr = measure_attr.attribute,
-                            table_name = tbl.table_name,
-                            where_clause = where_clause,
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                    if agg_func == "max":
-                        agg_query = lux.config.query_templates['barchart_max'].format(
-                            groupby_attr = groupby_attr.attribute,
-                            measure_attr = measure_attr.attribute,
-                            table_name = tbl.table_name,
-                            where_clause = where_clause,
-                        )
-                        view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-                        view._vis_data = utils.pandas_to_lux(view._vis_data)
-                view._query = agg_query
-            result_vals = list(view._vis_data[groupby_attr.attribute])
-            # create existing group by attribute combinations if color is specified
-            # this is needed to check what combinations of group_by_attr and color_attr values have a non-zero number of elements in them
-            if has_color:
-                res_color_combi_vals = []
-                result_color_vals = list(view._vis_data[color_attr.attribute])
-                for i in range(0, len(result_vals)):
-                    res_color_combi_vals.append([result_vals[i], result_color_vals[i]])
-            # For filtered aggregation that have missing groupby-attribute values, set these aggregated value as 0, since no datapoints
-            if isFiltered or has_color and attr_unique_vals:
-                N_unique_vals = len(attr_unique_vals)
-                if len(result_vals) != N_unique_vals * color_cardinality:
-                    columns = view._vis_data.columns
-                    if has_color:
-                        df = pandas.DataFrame(
-                            {
-                                columns[0]: attr_unique_vals * color_cardinality,
-                                columns[1]: pandas.Series(color_attr_vals).repeat(N_unique_vals),
-                            }
-                        )
-                        view._vis_data = view._vis_data.merge(
-                            df,
-                            on=[columns[0], columns[1]],
-                            how="right",
-                            suffixes=["", "_right"],
-                        )
-                        for col in columns[2:]:
-                            view._vis_data[col] = view._vis_data[col].fillna(0)  # Triggers __setitem__
-                        assert len(list(view._vis_data[groupby_attr.attribute])) == N_unique_vals * len(
-                            color_attr_vals
-                        ), f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute, color_attr.attribute}`."
-                        view._vis_data = view._vis_data.iloc[
-                            :, :3
-                        ]  # Keep only the three relevant columns not the *_right columns resulting from merge
-                    else:
-                        df = pandas.DataFrame({columns[0]: attr_unique_vals})
-
-                        view._vis_data = view._vis_data.merge(
-                            df, on=columns[0], how="right", suffixes=["", "_right"]
-                        )
-
-                        for col in columns[1:]:
-                            view._vis_data[col] = view._vis_data[col].fillna(0)
-                        assert (
-                            len(list(view._vis_data[groupby_attr.attribute])) == N_unique_vals
-                        ), f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute}`."
-            view._vis_data = view._vis_data.sort_values(by=groupby_attr.attribute, ascending=True)
-            view._vis_data = view._vis_data.reset_index()
-            view._vis_data = view._vis_data.drop(columns="index")
-            # view._vis_data.length = list(length_query["length"])[0]
-
-    @staticmethod
-    def execute_binning(view: Vis, tbl: LuxSQLTable):
-        """
-        Binning of data points for generating histograms
-        Parameters
-        ----------
-        vis: lux.Vis
-            lux.Vis object that represents a visualization
-        tbl : lux.core.frame
-            LuxSQLTable with specified intent.
-        Returns
-        -------
-        None
-        """
-        import numpy as np
-
-        bin_attribute = list(filter(lambda x: x.bin_size != 0, view._inferred_intent))[0]
-
-        num_bins = bin_attribute.bin_size
-        attr_min = tbl._min_max[bin_attribute.attribute][0]
-        attr_max = tbl._min_max[bin_attribute.attribute][1]
-        attr_type = type(tbl.unique_values[bin_attribute.attribute][0])
-
-        # get filters if available
-        where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-
-        length_query = pandas.read_sql(
-            lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-            lux.config.SQLconnection,
-        )
-        # need to calculate the bin edges before querying for the relevant data
-        bin_width = (attr_max - attr_min) / num_bins
-        upper_edges = []
-        for e in range(1, num_bins):
-            curr_edge = attr_min + e * bin_width
-            if attr_type == int:
-                upper_edges.append(str(math.ceil(curr_edge)))
-            else:
-                upper_edges.append(str(curr_edge))
-        upper_edges = ",".join(upper_edges)
-        view_filter, filter_vars = GeneralDatabaseExecutor.execute_filter(view)
-        bin_count_query = lux.config.query_templates['histogram_counts'].format(
-            bin_attribute = bin_attribute.attribute,
-            upper_edges = "{" + upper_edges + "}",
-            table_name = tbl.table_name,
-            where_clause = where_clause,
-        )
-
-        bin_count_data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
-        if not bin_count_data["width_bucket"].isnull().values.any():
-            # np.histogram breaks if data contain NaN
-
-            # counts,binEdges = np.histogram(tbl[bin_attribute.attribute],bins=bin_attribute.bin_size)
-            # binEdges of size N+1, so need to compute binCenter as the bin location
-            upper_edges = [float(i) for i in upper_edges.split(",")]
-            if attr_type == int:
-                bin_centers = np.array([math.ceil((attr_min + attr_min + bin_width) / 2)])
-            else:
-                bin_centers = np.array([(attr_min + attr_min + bin_width) / 2])
-            bin_centers = np.append(
-                bin_centers,
-                np.mean(np.vstack([upper_edges[0:-1], upper_edges[1:]]), axis=0),
-            )
-            if attr_type == int:
-                bin_centers = np.append(
-                    bin_centers,
-                    math.ceil((upper_edges[len(upper_edges) - 1] + attr_max) / 2),
-                )
-            else:
-                bin_centers = np.append(bin_centers, (upper_edges[len(upper_edges) - 1] + attr_max) / 2)
-
-            if len(bin_centers) > len(bin_count_data):
-                bucket_lables = bin_count_data["width_bucket"].unique()
-                for i in range(0, len(bin_centers)):
-                    if i not in bucket_lables:
-                        bin_count_data = bin_count_data.append(
-                            pandas.DataFrame([[i, 0]], columns=bin_count_data.columns)
-                        )
-            view._vis_data = pandas.DataFrame(
-                np.array([bin_centers, list(bin_count_data["count"])]).T,
-                columns=[bin_attribute.attribute, "Number of Records"],
-            )
-            view._vis_data = utils.pandas_to_lux(view.data)
-            # view._vis_data.length = list(length_query["length"])[0]
-
-    @staticmethod
-    def execute_2D_binning(view: Vis, tbl: LuxSQLTable):
-        import numpy as np
-
-        x_attribute = list(filter(lambda x: x.channel == "x", view._inferred_intent))[0]
-
-        y_attribute = list(filter(lambda x: x.channel == "y", view._inferred_intent))[0]
-
-        num_bins = lux.config.heatmap_bin_size
-        x_attr_min = tbl._min_max[x_attribute.attribute][0]
-        x_attr_max = tbl._min_max[x_attribute.attribute][1]
-        x_attr_type = type(tbl.unique_values[x_attribute.attribute][0])
-
-        y_attr_min = tbl._min_max[y_attribute.attribute][0]
-        y_attr_max = tbl._min_max[y_attribute.attribute][1]
-        y_attr_type = type(tbl.unique_values[y_attribute.attribute][0])
-
-        # get filters if available
-        where_clause, filterVars = GeneralDatabaseExecutor.execute_filter(view)
-
-        # need to calculate the bin edges before querying for the relevant data
-        x_bin_width = (x_attr_max - x_attr_min) / num_bins
-        y_bin_width = (y_attr_max - y_attr_min) / num_bins
-
-        x_upper_edges = []
-        y_upper_edges = []
-        for e in range(0, num_bins):
-            x_curr_edge = x_attr_min + e * x_bin_width
-            y_curr_edge = y_attr_min + e * y_bin_width
-            # get upper edges for x attribute bins
-            if x_attr_type == int:
-                x_upper_edges.append(math.ceil(x_curr_edge))
-            else:
-                x_upper_edges.append(x_curr_edge)
-            # get upper edges for y attribute bins
-            if y_attr_type == int:
-                y_upper_edges.append(str(math.ceil(y_curr_edge)))
-            else:
-                y_upper_edges.append(str(y_curr_edge))
-        x_upper_edges_string = [str(int) for int in x_upper_edges]
-        x_upper_edges_string = ",".join(x_upper_edges_string)
-        y_upper_edges_string = ",".join(y_upper_edges)
-
-        bin_count_query = lux.config.query_templates['heatmap_counts'].format(
-            x_attribute = x_attribute.attribute,
-            x_upper_edges_string = "{" + x_upper_edges_string + "}",
-            y_attribute = y_attribute.attribute,
-            y_upper_edges_string = "{" + y_upper_edges_string + "}",
-            table_name = tbl.table_name,
-            where_clause = where_clause,
-        )
-
-        # data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
-
-        data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
-        # data = data[data["width_bucket1"] != num_bins - 1]
-        # data = data[data["width_bucket2"] != num_bins - 1]
-        if len(data) > 0:
-            data["xBinStart"] = data.apply(
-                lambda row: float(x_upper_edges[int(row["width_bucket1"]) - 1]) - x_bin_width, axis=1
-            )
-            data["xBinEnd"] = data.apply(
-                lambda row: float(x_upper_edges[int(row["width_bucket1"]) - 1]), axis=1
-            )
-            data["yBinStart"] = data.apply(
-                lambda row: float(y_upper_edges[int(row["width_bucket2"]) - 1]) - y_bin_width, axis=1
-            )
-            data["yBinEnd"] = data.apply(
-                lambda row: float(y_upper_edges[int(row["width_bucket2"]) - 1]), axis=1
-            )
-        view._vis_data = utils.pandas_to_lux(data)
-
-    @staticmethod
-    def execute_filter(view: Vis):
-        """
-        Helper function to convert a Vis' filter specification to a SQL where clause.
-        Takes in a Vis object and returns an appropriate SQL WHERE clause based on the filters specified in the vis' _inferred_intent.
-
-        Parameters
-        ----------
-        vis: lux.Vis
-            lux.Vis object that represents a visualization
-
-        Returns
-        -------
-        where_clause: string
-            String representation of a SQL WHERE clause
-        filter_vars: list of strings
-            list of variables that have been used as filters
-        """
-        filters = utils.get_filter_specs(view._inferred_intent)
-        return GeneralDatabaseExecutor.create_where_clause(filters, view=view)
-
-    def create_where_clause(filter_specs, view=""):
-        where_clause = []
-        filter_vars = []
-        filters = filter_specs
-        if filters:
-            for f in range(0, len(filters)):
-                if f == 0:
-                    where_clause.append("WHERE")
-                else:
-                    where_clause.append("AND")
-                curr_value = str(filters[f].value)
-                curr_value = curr_value.replace("'", "''")
-                if lux.config.quoted_queries == True:
-                    where_clause.extend(
-                        [
-                            '"' + str(filters[f].attribute) + '"',
-                            str(filters[f].filter_op),
-                            "'" + curr_value + "'",
-                        ]
-                    )
-                else:
-                    where_clause.extend(
-                        [
-                            str(filters[f].attribute),
-                            str(filters[f].filter_op),
-                            "'" + curr_value + "'",
-                        ]
-                    )
-                if filters[f].attribute not in filter_vars:
-                    filter_vars.append(filters[f].attribute)
-        if view != "":
-            attributes = utils.get_attrs_specs(view._inferred_intent)
-
-            # need to ensure that no null values are included in the data
-            # null values breaks binning queries
-            for a in attributes:
-                if a.attribute != "Record":
-                    if where_clause == []:
-                        where_clause.append("WHERE")
-                    else:
-                        where_clause.append("AND")
-                    if lux.config.quoted_queries == True:
-                        where_clause.extend(
-                            [
-                                '"' + str(a.attribute) + '"',
-                                "IS NOT NULL",
-                            ]
-                        )
-                    else:
-                        where_clause.extend(
-                            [
-                                str(a.attribute),
-                                "IS NOT NULL",
-                            ]
-                        )
-
-        if where_clause == []:
-            return ("", [])
-        else:
-            where_clause = " ".join(where_clause)
-        return (where_clause, filter_vars)
-
-    def get_filtered_size(filter_specs, tbl):
-        clause_info = GeneralDatabaseExecutor.create_where_clause(filter_specs=filter_specs, view="")
-        where_clause = clause_info[0]
-        filter_intents = filter_specs[0]
-        filtered_length = pandas.read_sql(
-            lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),
-            lux.config.SQLconnection,
-        )
-        return list(filtered_length["length"])[0]
-
-    #######################################################
-    ########## Metadata, type, model schema ###############
-    #######################################################
-
-    def compute_dataset_metadata(self, tbl: LuxSQLTable):
-        """
-        Function which computes the metadata required for the Lux recommendation system.
-        Populates the metadata parameters of the specified Lux DataFrame.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose metadata will be calculated
-
-        Returns
-        -------
-        None
-        """
-        if not tbl._setup_done:
-            self.get_SQL_attributes(tbl)
-        tbl._data_type = {}
-        #####NOTE: since we aren't expecting users to do much data processing with the SQL database, should we just keep this
-        #####      in the initialization and do it just once
-        self.compute_data_type(tbl)
-        self.compute_stats(tbl)
-
-    def get_SQL_attributes(self, tbl: LuxSQLTable):
-        """
-        Retrieves the names of variables within a specified Lux DataFrame's Postgres SQL table.
-        Uses these variables to populate the Lux DataFrame's columns list.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose columns will be populated
-
-        Returns
-        -------
-        None
-        """
-        if "." in tbl.table_name:
-            table_name = tbl.table_name[self.table_name.index(".") + 1 :]
-        else:
-            table_name = tbl.table_name
-        attr_query = lux.config.query_templates['table_attributes_query'].format(
-            table_name = table_name, 
-        )
-        attributes = list(pandas.read_sql(attr_query, lux.config.SQLconnection)["column_name"])
-        for attr in attributes:
-            tbl[attr] = None
-        tbl._setup_done = True
-
-    def compute_stats(self, tbl: LuxSQLTable):
-        """
-        Function which computes the min and max values for each variable within the specified Lux DataFrame's SQL table.
-        Populates the metadata parameters of the specified Lux DataFrame.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose metadata will be calculated
-
-        Returns
-        -------
-        None
-        """
-        # precompute statistics
-        tbl.unique_values = {}
-        tbl._min_max = {}
-        length_query = pandas.read_sql(
-            lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = ""),
-            lux.config.SQLconnection,
-        )
-        tbl._length = list(length_query["length"])[0]
-
-        self.get_unique_values(tbl)
-        for attribute in tbl.columns:
-            if tbl._data_type[attribute] == "quantitative":
-                min_max_query = pandas.read_sql(
-                    lux.config.query_templates['min_max_query'].format(
-                        attribute = attribute, table_name = tbl.table_name
-                    ),
-                    lux.config.SQLconnection,
-                )
-                tbl._min_max[attribute] = (
-                    list(min_max_query["min"])[0],
-                    list(min_max_query["max"])[0],
-                )
-
-    def get_cardinality(self, tbl: LuxSQLTable):
-        """
-        Function which computes the cardinality for each variable within the specified Lux DataFrame's SQL table.
-        Populates the metadata parameters of the specified Lux DataFrame.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose metadata will be calculated
-
-        Returns
-        -------
-        None
-        """
-        cardinality = {}
-        for attr in list(tbl.columns):
-            card_query = lux.config.query_templates['cardinality_query'].format(
-                attribute = attr, table_name = tbl.table_name
-            )
-            card_data = pandas.read_sql(
-                card_query,
-                lux.config.SQLconnection,
-            )
-            cardinality[attr] = list(card_data["count"])[0]
-        tbl.cardinality = cardinality
-
-    def get_unique_values(self, tbl: LuxSQLTable):
-        """
-        Function which collects the unique values for each variable within the specified Lux DataFrame's SQL table.
-        Populates the metadata parameters of the specified Lux DataFrame.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose metadata will be calculated
-
-        Returns
-        -------
-        None
-        """
-        unique_vals = {}
-        for attr in list(tbl.columns):
-            unique_query = lux.config.query_templates['unique_query'].format(
-                attribute = attr, table_name = tbl.table_name
-            )
-            unique_data = pandas.read_sql(
-                unique_query,
-                lux.config.SQLconnection,
-            )
-            unique_vals[attr] = list(unique_data[attr])
-        tbl.unique_values = unique_vals
-
-    def compute_data_type(self, tbl: LuxSQLTable):
-        """
-        Function which the equivalent Pandas data type of each variable within the specified Lux DataFrame's SQL table.
-        Populates the metadata parameters of the specified Lux DataFrame.
-
-        Parameters
-        ----------
-        tbl: lux.LuxSQLTable
-            lux.LuxSQLTable object whose metadata will be calculated
-
-        Returns
-        -------
-        None
-        """
-        data_type = {}
-        self.get_cardinality(tbl)
-        if "." in tbl.table_name:
-            table_name = tbl.table_name[tbl.table_name.index(".") + 1 :]
-        else:
-            table_name = tbl.table_name
-        # get the data types of the attributes in the SQL table
-        for attr in list(tbl.columns):
-            datatype_query = lux.config.query_templates['datatype_query'].format(
-                table_name = table_name, attribute = attr
-            )
-            datatype = list(pandas.read_sql(datatype_query, lux.config.SQLconnection)["data_type"])[0]
-            if str(attr).lower() in {"month", "year"} or "time" in datatype or "date" in datatype:
-                data_type[attr] = "temporal"
-            elif datatype in {
-                "character",
-                "character varying",
-                "boolean",
-                "uuid",
-                "text",
-            }:
-                data_type[attr] = "nominal"
-            elif datatype in {
-                "integer",
-                "numeric",
-                "decimal",
-                "bigint",
-                "real",
-                "smallint",
-                "smallserial",
-                "serial",
-                "double",
-                "double precision",
-            }:
-                if tbl.cardinality[attr] < 13:
-                    data_type[attr] = "nominal"
-                elif check_if_id_like(tbl, attr):
-                    data_type[attr] = "id"
-                else:
-                    data_type[attr] = "quantitative"
-
-        tbl._data_type = data_type
diff --git a/lux/executor/PandasExecutor.py b/lux/executor/PandasExecutor.py
index eabdce44..8ffa636d 100644
--- a/lux/executor/PandasExecutor.py
+++ b/lux/executor/PandasExecutor.py
@@ -39,7 +39,19 @@ def __repr__(self):
 
     @staticmethod
     def execute_sampling(ldf: LuxDataFrame):
-        # General Sampling for entire dataframe
+        """
+        Compute and cache a sample for the overall dataframe
+
+        - When # of rows exceeds lux.config.sampling_start, take 75% df as sample
+        - When # of rows exceeds lux.config.sampling_cap, cap the df at {lux.config.sampling_cap} rows
+
+        lux.config.sampling_start = 100k rows
+        lux.config.sampling_cap = 1M rows
+
+        Parameters
+        ----------
+        ldf : LuxDataFrame
+        """
         SAMPLE_FLAG = lux.config.sampling
         SAMPLE_START = lux.config.sampling_start
         SAMPLE_CAP = lux.config.sampling_cap
@@ -49,21 +61,39 @@ def execute_sampling(ldf: LuxDataFrame):
             if ldf._sampled is None:  # memoize unfiltered sample df
                 ldf._sampled = ldf.sample(n=SAMPLE_CAP, random_state=1)
             ldf._message.add_unique(
-                f"Large dataframe detected: Lux is only visualizing a random sample capped at {SAMPLE_CAP} rows.",
+                f"Large dataframe detected: Lux is only visualizing a sample capped at {SAMPLE_CAP} rows.",
                 priority=99,
             )
         elif SAMPLE_FLAG and len(ldf) > SAMPLE_START:
             if ldf._sampled is None:  # memoize unfiltered sample df
                 ldf._sampled = ldf.sample(frac=SAMPLE_FRAC, random_state=1)
             ldf._message.add_unique(
-                f"Large dataframe detected: Lux is only visualizing a random sample of {len(ldf._sampled)} rows.",
+                f"Large dataframe detected: Lux is visualizing a sample of {SAMPLE_FRAC}% of the dataframe ({len(ldf._sampled)} rows).",
                 priority=99,
             )
         else:
             ldf._sampled = ldf
 
     @staticmethod
-    def execute(vislist: VisList, ldf: LuxDataFrame):
+    def execute_approx_sample(ldf: LuxDataFrame):
+        """
+        Compute and cache an approximate sample of the overall dataframe
+        for the purpose of early pruning of the visualization search space
+
+        Parameters
+        ----------
+        ldf : LuxDataFrame
+        """
+        if ldf._approx_sample is None:
+            if len(ldf._sampled) > lux.config.early_pruning_sample_start:
+                ldf._approx_sample = ldf._sampled.sample(
+                    n=lux.config.early_pruning_sample_cap, random_state=1
+                )
+            else:
+                ldf._approx_sample = ldf._sampled
+
+    @staticmethod
+    def execute(vislist: VisList, ldf: LuxDataFrame, approx=False):
         """
         Given a VisList, fetch the data required to render the vis.
         1) Apply filters
@@ -88,6 +118,12 @@ def execute(vislist: VisList, ldf: LuxDataFrame):
             # The vis data starts off being original or sampled dataframe
             vis._source = ldf
             vis._vis_data = ldf._sampled
+            # Approximating vis for early pruning
+            if approx:
+                vis._original_df = vis._vis_data
+                PandasExecutor.execute_approx_sample(ldf)
+                vis._vis_data = ldf._approx_sample
+                vis.approx = True
             filter_executed = PandasExecutor.execute_filter(vis)
             # Select relevant data based on attribute information
             attributes = set([])
@@ -101,16 +137,13 @@ def execute(vislist: VisList, ldf: LuxDataFrame):
                 PandasExecutor.execute_aggregate(vis, isFiltered=filter_executed)
             elif vis.mark == "histogram":
                 PandasExecutor.execute_binning(ldf, vis)
-            elif vis.mark == "scatter":
-                HBIN_START = 5000
-                if lux.config.heatmap and len(ldf) > HBIN_START:
-                    vis._postbin = True
-                    ldf._message.add_unique(
-                        f"Large scatterplots detected: Lux is automatically binning scatterplots to heatmaps.",
-                        priority=98,
-                    )
-                    # vis._mark = "heatmap"
-                    # PandasExecutor.execute_2D_binning(vis) # Lazy Evaluation (Early pruning based on interestingness)
+            elif vis.mark == "heatmap":
+                # Early pruning based on interestingness of scatterplots
+                if approx:
+                    vis._mark = "scatter"
+                else:
+                    vis._mark = "heatmap"
+                    PandasExecutor.execute_2D_binning(vis)
             vis.data.clear_intent()  # Ensure that intent is not propogated to the vis data
 
     @staticmethod
@@ -202,15 +235,14 @@ def execute_aggregate(vis: Vis, isFiltered=True):
                         df = pd.DataFrame({columns[0]: attr_unique_vals * color_cardinality,columns[1]: pd.Series(color_attr_vals).repeat(N_unique_vals),})
                         vis._vis_data = vis.data.merge(df,on=[columns[0], columns[1]],how="right",suffixes=["", "_right"],)
                         for col in columns[2:]:
-                            vis.data[col] = vis.data[col].fillna(0)  # Triggers __setitem__
+                            # Triggers __setitem__
+                            vis.data[col] = vis.data[col].fillna(0)
                         assert len(list(vis.data[groupby_attr.attribute])) == N_unique_vals * len(color_attr_vals), f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute, color_attr.attribute}`."
-
                         # Keep only the three relevant columns not the *_right columns resulting from merge
-                        vis._vis_data = vis.data.iloc[:, :3]
+                        vis._vis_data = vis.data[[groupby_attr.attribute, color_attr.attribute, measure_attr.attribute]]
 
                     else:
                         df = pd.DataFrame({columns[0]: attr_unique_vals})
-
                         vis._vis_data = vis.data.merge(df, on=columns[0], how="right", suffixes=["", "_right"])
 
                         for col in columns[1:]:
@@ -232,7 +264,7 @@ def execute_aggregate(vis: Vis, isFiltered=True):
             vis._vis_data = vis._vis_data.drop(columns="index")
 
     @staticmethod
-    def execute_binning(ldf, vis: Vis):
+    def execute_binning(ldf: LuxDataFrame, vis: Vis):
         """
         Binning of data points for generating histograms
 
@@ -270,7 +302,19 @@ def execute_binning(ldf, vis: Vis):
         vis._vis_data = pd.DataFrame(binned_result, columns=[bin_attr, "Number of Records"])
 
     @staticmethod
-    def execute_filter(vis: Vis):
+    def execute_filter(vis: Vis) -> bool:
+        """
+        Apply a Vis's filter to vis.data
+
+        Parameters
+        ----------
+        vis : Vis
+
+        Returns
+        -------
+        bool
+            Boolean flag indicating if any filter was applied
+        """
         assert (vis.data is not None), "execute_filter assumes input vis.data is populated (if not, populate with LuxDataFrame values)"
         filters = utils.get_filter_specs(vis._inferred_intent)
 
@@ -328,7 +372,14 @@ def apply_filter(df: pd.DataFrame, attribute: str, op: str, val: object) -> pd.D
         return df
 
     @staticmethod
-    def execute_2D_binning(vis: Vis):
+    def execute_2D_binning(vis: Vis) -> None:
+        """
+        Apply 2D binning (heatmap) to vis.data
+
+        Parameters
+        ----------
+        vis : Vis
+        """
         pd.reset_option("mode.chained_assignment")
         with pd.option_context("mode.chained_assignment", None):
             x_attr = vis.get_attr_by_channel("x")[0].attribute
diff --git a/lux/executor/SQLExecutor.py b/lux/executor/SQLExecutor.py
index 6205e380..7031caa9 100644
--- a/lux/executor/SQLExecutor.py
+++ b/lux/executor/SQLExecutor.py
@@ -4,7 +4,7 @@
 from lux.core.sqltable import LuxSQLTable
 from lux.executor.Executor import Executor
 from lux.utils import utils
-from lux.utils.utils import check_import_lux_widget, check_if_id_like_for_sql
+from lux.utils.utils import check_import_lux_widget, check_if_id_like
 import lux
 
 import math
@@ -26,7 +26,8 @@ def __repr__(self):
 
     @staticmethod
     def execute_preview(tbl: LuxSQLTable, preview_size=5):
-        output = pandas.read_sql("SELECT * from {} LIMIT {}".format(tbl.table_name, preview_size), lux.config.SQLconnection)
+        preview_query = lux.config.query_templates['preview_query']
+        output = pandas.read_sql(preview_query.format(table_name = tbl.table_name, num_rows = preview_size), lux.config.SQLconnection)
         return output
 
     @staticmethod
@@ -36,30 +37,32 @@ def execute_sampling(tbl: LuxSQLTable):
         SAMPLE_CAP = lux.config.sampling_cap
         SAMPLE_FRAC = 0.2
 
-        length_query = pandas.read_sql("SELECT COUNT(*) as length FROM {}".format(tbl.table_name),lux.config.SQLconnection,)
+        length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = ""),lux.config.SQLconnection,)
         limit = int(list(length_query["length"])[0]) * SAMPLE_FRAC
-        tbl._sampled = pandas.read_sql("SELECT * from {} LIMIT {}".format(tbl.table_name, str(limit)), lux.config.SQLconnection)
+        sample_query = lux.config.query_templates['sample_query'].format(table_name = tbl.table_name, where_clause = "", num_rows = str(int(limit)))
+        tbl._sampled = pandas.read_sql(sample_query, lux.config.SQLconnection)
 
     @staticmethod
-    def execute(view_collection: VisList, tbl: LuxSQLTable):
+    def execute(view_collection: VisList, tbl: LuxSQLTable, approx: bool = False):
         """
         Given a VisList, fetch the data required to render the view
         1) Generate Necessary WHERE clauses
         2) Query necessary data, applying appropriate aggregation for the chart type
         3) populates vis' data with a DataFrame with relevant results
         """
+
         for view in view_collection:
             # choose execution method depending on vis mark type
-            view._source = tbl
+
             # when mark is empty, deal with lazy execution by filling the data with a small sample of the dataframe
             if view.mark == "":
                 SQLExecutor.execute_sampling(tbl)
                 view._vis_data = tbl._sampled
             if view.mark == "scatter":
                 where_clause, filterVars = SQLExecutor.execute_filter(view)
-                length_query = pandas.read_sql("SELECT COUNT(1) as length FROM {} {}".format(tbl.table_name, where_clause),lux.config.SQLconnection,)
+                length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
                 view_data_length = list(length_query["length"])[0]
-                if len(view.get_attr_by_channel("color")) == 1 or view_data_length < 5000:
+                if not lux.config.heatmap:
                     # NOTE: might want to have a check somewhere to not use categorical variables with greater than some number of categories as a Color variable----------------
                     has_color = True
                     SQLExecutor.execute_scatter(view, tbl)
@@ -97,26 +100,37 @@ def execute_scatter(view: Vis, tbl: LuxSQLTable):
 
         attributes = set([])
         for clause in view._inferred_intent:
-            if clause.attribute and clause.attribute != "Record":
-                attributes.add(clause.attribute)
+            if clause.attribute:
+                if clause.attribute != "Record":
+                    attributes.add(clause.attribute)
         where_clause, filterVars = SQLExecutor.execute_filter(view)
 
-        length_query = pandas.read_sql("SELECT COUNT(1) as length FROM {} {}".format(tbl.table_name, where_clause),lux.config.SQLconnection,)
+        length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
 
         def add_quotes(var_name):
             return '"' + var_name + '"'
 
         required_variables = attributes | set(filterVars)
-        required_variables = map(add_quotes, required_variables)
-        required_variables = ",".join(required_variables)
-        row_count = list(pandas.read_sql(f"SELECT COUNT(*) FROM {tbl.table_name} {where_clause}",lux.config.SQLconnection,)["count"])[0]
+        if lux.config.handle_quotes:
+            required_variables = map(add_quotes, required_variables)
+        required_variables_str = ",".join(required_variables)
+        row_count = list(pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)["length"])[0]
         if row_count > lux.config.sampling_cap:
-            query = f"SELECT {required_variables} FROM {tbl.table_name} {where_clause} ORDER BY random() LIMIT {str(lux.config.sampling_cap)}"
+            query = lux.config.query_templates['sample_query'].format(columns = required_variables_str, table_name = tbl.table_name, where_clause = where_clause, num_rows = 10000)
+            #query = f"SELECT {required_variables} FROM {tbl.table_name} {where_clause} ORDER BY random() LIMIT 10000"
         else:
-            query = "SELECT {} FROM {} {}".format(required_variables, tbl.table_name, where_clause)
+            query = lux.config.query_templates['scatter_query'].format(columns = required_variables_str, table_name = tbl.table_name, where_clause = where_clause)
         data = pandas.read_sql(query, lux.config.SQLconnection)
+
+        if len(attributes | set(filterVars)) == 2:
+            assert(len(data.columns) == 2)
+        else:
+            assert(len(data.columns) == 3)
         view._vis_data = utils.pandas_to_lux(data)
         view._query = query
+        # view._vis_data.length = list(length_query["length"])[0]
+
+        tbl._message.add_unique(f"Large scatterplots detected: Lux is automatically binning scatterplots to heatmaps.",priority=98,)
 
     @staticmethod
     def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
@@ -164,18 +178,20 @@ def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
             # barchart case, need count data for each group
             if measure_attr.attribute == "Record":
                 where_clause, filterVars = SQLExecutor.execute_filter(view)
-                length_query = pandas.read_sql("SELECT COUNT(*) as length FROM {} {}".format(tbl.table_name, where_clause),lux.config.SQLconnection,)
 
+                length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
                 # generates query for colored barchart case
                 if has_color:
-                    count_query = 'SELECT "{}", "{}", COUNT("{}") FROM {} {} GROUP BY "{}", "{}"'.format(groupby_attr.attribute,color_attr.attribute,groupby_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,color_attr.attribute,)
+                    count_query = lux.config.query_templates['colored_barchart_counts'].format(groupby_attr = groupby_attr.attribute, color_attr = color_attr.attribute, table_name = tbl.table_name, where_clause = where_clause,)
                     view._vis_data = pandas.read_sql(count_query, lux.config.SQLconnection)
+                    assert((len(view._vis_data.columns) == 3) & ("count" in view._vis_data.columns))
                     view._vis_data = view._vis_data.rename(columns={"count": "Record"})
                     view._vis_data = utils.pandas_to_lux(view._vis_data)
                 # generates query for normal barchart case
                 else:
-                    count_query = 'SELECT "{}", COUNT("{}") FROM {} {} GROUP BY "{}"'.format(groupby_attr.attribute,groupby_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,)
+                    count_query = lux.config.query_templates['barchart_counts'].format(groupby_attr = groupby_attr.attribute, table_name = tbl.table_name, where_clause = where_clause,)
                     view._vis_data = pandas.read_sql(count_query, lux.config.SQLconnection)
+                    assert((len(view._vis_data.columns) == 2) & ("count" in view._vis_data.columns))
                     view._vis_data = view._vis_data.rename(columns={"count": "Record"})
                     view._vis_data = utils.pandas_to_lux(view._vis_data)
                 view._query = count_query
@@ -183,45 +199,40 @@ def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
             # aggregate barchart case, need aggregate data (mean, sum, max) for each group
             else:
                 where_clause, filterVars = SQLExecutor.execute_filter(view)
-                length_query = pandas.read_sql("SELECT COUNT(*) as length FROM {} {}".format(tbl.table_name, where_clause),lux.config.SQLconnection,)
-
+                length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
                 # generates query for colored barchart case
                 if has_color:
                     if agg_func == "mean":
-                        agg_query = 'SELECT "{}", "{}", AVG("{}") as "{}" FROM {} {} GROUP BY "{}", "{}"'.format(groupby_attr.attribute,color_attr.attribute,measure_attr.attribute,measure_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,color_attr.attribute,)
+                        agg_query = (lux.config.query_templates['colored_barchart_average'].format(groupby_attr = groupby_attr.attribute,color_attr = color_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,))
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
-
+                        assert((len(view._vis_data.columns) == 3) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                     if agg_func == "sum":
-                        agg_query = 'SELECT "{}", "{}", SUM("{}") as "{}" FROM {} {} GROUP BY "{}", "{}"'.format(
-                            groupby_attr.attribute,
-                            color_attr.attribute,
-                            measure_attr.attribute,
-                            measure_attr.attribute,
-                            tbl.table_name,
-                            where_clause,
-                            groupby_attr.attribute,
-                            color_attr.attribute,
-                        )
+                        agg_query = (lux.config.query_templates['colored_barchart_sum'].format(groupby_attr = groupby_attr.attribute,color_attr = color_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,))
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
+                        assert((len(view._vis_data.columns) == 3) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                     if agg_func == "max":
-                        agg_query = 'SELECT "{}", "{}", MAX("{}") as "{}" FROM {} {} GROUP BY "{}", "{}"'.format(groupby_attr.attribute,color_attr.attribute,measure_attr.attribute,measure_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,color_attr.attribute,)
+                        agg_query = (lux.config.query_templates['colored_barchart_max'].format(groupby_attr = groupby_attr.attribute,color_attr = color_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,))
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
+                        assert((len(view._vis_data.columns) == 3) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                 # generates query for normal barchart case
                 else:
                     if agg_func == "mean":
-                        agg_query = 'SELECT "{}", AVG("{}") as "{}" FROM {} {} GROUP BY "{}"'.format(groupby_attr.attribute,measure_attr.attribute,measure_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,)
+                        agg_query = lux.config.query_templates['barchart_average'].format(groupby_attr = groupby_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,)
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
+                        assert((len(view._vis_data.columns) == 2) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                     if agg_func == "sum":
-                        agg_query = 'SELECT "{}", SUM("{}") as "{}" FROM {} {} GROUP BY "{}"'.format(groupby_attr.attribute,measure_attr.attribute,measure_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,)
+                        agg_query = lux.config.query_templates['barchart_sum'].format(groupby_attr = groupby_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,)
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
+                        assert((len(view._vis_data.columns) == 2) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                     if agg_func == "max":
-                        agg_query = 'SELECT "{}", MAX("{}") as "{}" FROM {} {} GROUP BY "{}"'.format(groupby_attr.attribute,measure_attr.attribute,measure_attr.attribute,tbl.table_name,where_clause,groupby_attr.attribute,)
+                        agg_query = lux.config.query_templates['barchart_max'].format(groupby_attr = groupby_attr.attribute,measure_attr = measure_attr.attribute,table_name = tbl.table_name,where_clause = where_clause,)
                         view._vis_data = pandas.read_sql(agg_query, lux.config.SQLconnection)
+                        assert((len(view._vis_data.columns) == 2) & (measure_attr.attribute in view._vis_data.columns))
                         view._vis_data = utils.pandas_to_lux(view._vis_data)
                 view._query = agg_query
             result_vals = list(view._vis_data[groupby_attr.attribute])
@@ -241,9 +252,11 @@ def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
                         df = pandas.DataFrame({columns[0]: attr_unique_vals * color_cardinality,columns[1]: pandas.Series(color_attr_vals).repeat(N_unique_vals),})
                         view._vis_data = view._vis_data.merge(df,on=[columns[0], columns[1]],how="right",suffixes=["", "_right"],)
                         for col in columns[2:]:
-                            view._vis_data[col] = view._vis_data[col].fillna(0)  # Triggers __setitem__
+                            # Triggers __setitem__
+                            view._vis_data[col] = view._vis_data[col].fillna(0)
                         assert len(list(view._vis_data[groupby_attr.attribute])) == N_unique_vals * len(color_attr_vals), f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute, color_attr.attribute}`."
-                        view._vis_data = view._vis_data.iloc[:, :3]  # Keep only the three relevant columns not the *_right columns resulting from merge
+                        # Keep only the three relevant columns not the *_right columns resulting from merge
+                        view._vis_data = view._vis_data.iloc[:, :3]
                     else:
                         df = pandas.DataFrame({columns[0]: attr_unique_vals})
 
@@ -251,7 +264,7 @@ def execute_aggregate(view: Vis, tbl: LuxSQLTable, isFiltered=True):
 
                         for col in columns[1:]:
                             view._vis_data[col] = view._vis_data[col].fillna(0)
-                        assert len(list(view._vis_data[groupby_attr.attribute])) == N_unique_vals, f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute}`."
+                        assert (len(list(view._vis_data[groupby_attr.attribute])) == N_unique_vals), f"Aggregated data missing values compared to original range of values of `{groupby_attr.attribute}`."
             view._vis_data = view._vis_data.sort_values(by=groupby_attr.attribute, ascending=True)
             view._vis_data = view._vis_data.reset_index()
             view._vis_data = view._vis_data.drop(columns="index")
@@ -283,8 +296,8 @@ def execute_binning(view: Vis, tbl: LuxSQLTable):
         # get filters if available
         where_clause, filterVars = SQLExecutor.execute_filter(view)
 
-        length_query = pandas.read_sql("SELECT COUNT(1) as length FROM {} {}".format(tbl.table_name, where_clause),lux.config.SQLconnection,)
-        # need to calculate the bin edges before querying for the relevant data
+        length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
+
         bin_width = (attr_max - attr_min) / num_bins
         upper_edges = []
         for e in range(1, num_bins):
@@ -295,9 +308,35 @@ def execute_binning(view: Vis, tbl: LuxSQLTable):
                 upper_edges.append(str(curr_edge))
         upper_edges = ",".join(upper_edges)
         view_filter, filter_vars = SQLExecutor.execute_filter(view)
-        bin_count_query = "SELECT width_bucket, COUNT(width_bucket) FROM (SELECT width_bucket(CAST (\"{}\" AS FLOAT), '{}') FROM {} {}) as Buckets GROUP BY width_bucket ORDER BY width_bucket".format(bin_attribute.attribute,"{" + upper_edges + "}",tbl.table_name,where_clause,)
+
+        #handling for non postgres case
+        if "cases" in lux.config.query_templates['histogram_counts']:
+            bucket_edges = [attr_min]
+            for e in range(1, num_bins):
+                curr_edge = attr_min + e * bin_width
+                bucket_edges.append(str(curr_edge))
+            bucket_edges.append(attr_max)
+
+            when_line = "WHEN {column} BETWEEN {lower_edge} AND {upper_edge} THEN {label}"
+            when_lines = "CASE "
+            for i in range(1, len(bucket_edges)):
+                when_lines = when_lines + when_line.format(column = bin_attribute.attribute, lower_edge = bucket_edges[i-1], upper_edge = bucket_edges[i], label = str(i-1)) + " "
+
+            when_lines = when_lines + "end"
+
+            #hist_query = "select width_bucket, count(width_bucket) as count from (select ({bucket_cases}) as width_bucket from {table_name} {where_clause}) as buckets group by width_bucket order by width_bucket"
+            bin_count_query = lux.config.query_templates['histogram_counts'].format(bucket_cases = when_lines, table_name = tbl.table_name, where_clause = where_clause)
+        # need to calculate the bin edges before querying for the relevant data
+        else:
+            bin_count_query = lux.config.query_templates['histogram_counts'].format(
+                bin_attribute = bin_attribute.attribute,
+                upper_edges = "{" + upper_edges + "}",
+                table_name = tbl.table_name,
+                where_clause = where_clause,
+            )
 
         bin_count_data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
+        assert((len(bin_count_data.columns) ==2) & (set(['width_bucket', 'count']).issubset(bin_count_data.columns)))
         if not bin_count_data["width_bucket"].isnull().values.any():
             # np.histogram breaks if data contain NaN
 
@@ -308,7 +347,10 @@ def execute_binning(view: Vis, tbl: LuxSQLTable):
                 bin_centers = np.array([math.ceil((attr_min + attr_min + bin_width) / 2)])
             else:
                 bin_centers = np.array([(attr_min + attr_min + bin_width) / 2])
-            bin_centers = np.append(bin_centers,np.mean(np.vstack([upper_edges[0:-1], upper_edges[1:]]), axis=0),)
+            bin_centers = np.append(
+                bin_centers,
+                np.mean(np.vstack([upper_edges[0:-1], upper_edges[1:]]), axis=0),
+            )
             if attr_type == int:
                 bin_centers = np.append(bin_centers,math.ceil((upper_edges[len(upper_edges) - 1] + attr_max) / 2),)
             else:
@@ -319,9 +361,9 @@ def execute_binning(view: Vis, tbl: LuxSQLTable):
                 for i in range(0, len(bin_centers)):
                     if i not in bucket_lables:
                         bin_count_data = bin_count_data.append(pandas.DataFrame([[i, 0]], columns=bin_count_data.columns))
+
             view._vis_data = pandas.DataFrame(np.array([bin_centers, list(bin_count_data["count"])]).T,columns=[bin_attribute.attribute, "Number of Records"],)
             view._vis_data = utils.pandas_to_lux(view.data)
-            view._query = bin_count_query
             # view._vis_data.length = list(length_query["length"])[0]
 
     @staticmethod
@@ -367,18 +409,37 @@ def execute_2D_binning(view: Vis, tbl: LuxSQLTable):
         x_upper_edges_string = ",".join(x_upper_edges_string)
         y_upper_edges_string = ",".join(y_upper_edges)
 
-        bin_count_query = "SELECT width_bucket1, width_bucket2, count(*) FROM (SELECT width_bucket(CAST (\"{}\" AS FLOAT), '{}') as width_bucket1, width_bucket(CAST (\"{}\" AS FLOAT), '{}') as width_bucket2 FROM {} {}) as foo GROUP BY width_bucket1, width_bucket2".format(
-            x_attribute.attribute,
-            "{" + x_upper_edges_string + "}",
-            y_attribute.attribute,
-            "{" + y_upper_edges_string + "}",
-            tbl.table_name,
-            where_clause,
-        )
+        if "cases" in lux.config.query_templates['histogram_counts']:
+            x_bucket_edges = [x_attr_min]
+            y_bucket_edges = [y_attr_min]
+            for e in range(1, num_bins):
+                x_curr_edge = x_attr_min + e * x_bin_width
+                x_bucket_edges.append(str(x_curr_edge))
+
+                y_curr_edge = y_attr_min + e * y_bin_width
+                y_bucket_edges.append(str(y_curr_edge))
+            x_bucket_edges.append(x_attr_max)
+            y_bucket_edges.append(y_attr_max)
+
+            when_line = "WHEN {column} BETWEEN {lower_edge} AND {upper_edge} THEN {label}"
+            x_when_lines = "CASE "
+            y_when_lines = "CASE "
+            for i in range(1, len(x_bucket_edges)):
+                x_when_lines = x_when_lines + when_line.format(column = x_attribute.attribute, lower_edge = x_bucket_edges[i-1], upper_edge = x_bucket_edges[i], label = str(i-1)) + " "
+                y_when_lines = y_when_lines + when_line.format(column = y_attribute.attribute, lower_edge = y_bucket_edges[i-1], upper_edge = y_bucket_edges[i], label = str(i-1)) + " "
+            x_when_lines = x_when_lines + "end"
+            y_when_lines = y_when_lines + "end"
+
+            #hist_query = "select width_bucket, count(width_bucket) as count from (select ({bucket_cases}) as width_bucket from {table_name} {where_clause}) as buckets group by width_bucket order by width_bucket"
+            bin_count_query = lux.config.query_templates['heatmap_counts'].format(bucket_cases1 = x_when_lines, bucket_cases2 = y_when_lines, table_name = tbl.table_name, where_clause = where_clause)
+
+        else:
+            bin_count_query = lux.config.query_templates['heatmap_counts'].format(x_attribute = x_attribute.attribute,x_upper_edges_string = "{" + x_upper_edges_string + "}",y_attribute = y_attribute.attribute,y_upper_edges_string = "{" + y_upper_edges_string + "}",table_name = tbl.table_name,where_clause = where_clause,)
 
         # data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
 
         data = pandas.read_sql(bin_count_query, lux.config.SQLconnection)
+        assert((len(data.columns) == 3) & (set(['width_bucket1', 'width_bucket2', 'count']).issubset(data.columns)))
         # data = data[data["width_bucket1"] != num_bins - 1]
         # data = data[data["width_bucket2"] != num_bins - 1]
         if len(data) > 0:
@@ -387,7 +448,6 @@ def execute_2D_binning(view: Vis, tbl: LuxSQLTable):
             data["yBinStart"] = data.apply(lambda row: float(y_upper_edges[int(row["width_bucket2"]) - 1]) - y_bin_width, axis=1)
             data["yBinEnd"] = data.apply(lambda row: float(y_upper_edges[int(row["width_bucket2"]) - 1]), axis=1)
         view._vis_data = utils.pandas_to_lux(data)
-        view._query = bin_count_query
 
     @staticmethod
     def execute_filter(view: Vis):
@@ -410,9 +470,10 @@ def execute_filter(view: Vis):
         filters = utils.get_filter_specs(view._inferred_intent)
         return SQLExecutor.create_where_clause(filters, view=view)
 
-    def create_where_clause(filters, view=""):
+    def create_where_clause(filter_specs, view=""):
         where_clause = []
         filter_vars = []
+        filters = filter_specs
         if filters:
             for f in range(0, len(filters)):
                 if f == 0:
@@ -421,7 +482,22 @@ def create_where_clause(filters, view=""):
                     where_clause.append("AND")
                 curr_value = str(filters[f].value)
                 curr_value = curr_value.replace("'", "''")
-                where_clause.extend(['"' + str(filters[f].attribute) + '"', str(filters[f].filter_op), "'" + curr_value + "'",])
+                if lux.config.handle_quotes == True:
+                    where_clause.extend(
+                        [
+                            '"' + str(filters[f].attribute) + '"',
+                            str(filters[f].filter_op),
+                            "'" + curr_value + "'",
+                        ]
+                    )
+                else:
+                    where_clause.extend(
+                        [
+                            str(filters[f].attribute),
+                            str(filters[f].filter_op),
+                            "'" + curr_value + "'",
+                        ]
+                    )
                 if filters[f].attribute not in filter_vars:
                     filter_vars.append(filters[f].attribute)
         if view != "":
@@ -435,7 +511,20 @@ def create_where_clause(filters, view=""):
                         where_clause.append("WHERE")
                     else:
                         where_clause.append("AND")
-                    where_clause.extend(['"' + str(a.attribute) + '"', "IS NOT NULL",])
+                    if lux.config.handle_quotes == True:
+                        where_clause.extend(
+                            [
+                                '"' + str(a.attribute) + '"',
+                                "IS NOT NULL",
+                            ]
+                        )
+                    else:
+                        where_clause.extend(
+                            [
+                                str(a.attribute),
+                                "IS NOT NULL",
+                            ]
+                        )
 
         if where_clause == []:
             return ("", [])
@@ -444,13 +533,10 @@ def create_where_clause(filters, view=""):
         return (where_clause, filter_vars)
 
     def get_filtered_size(filter_specs, tbl):
-        clause_info = SQLExecutor.create_where_clause(filters=filter_specs, view="")
+        clause_info = SQLExecutor.create_where_clause(filter_specs=filter_specs, view="")
         where_clause = clause_info[0]
         filter_intents = filter_specs[0]
-        filtered_length = pandas.read_sql(
-            "SELECT COUNT(1) as length FROM {} {}".format(tbl.table_name, where_clause),
-            lux.config.SQLconnection,
-        )
+        filtered_length = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = where_clause),lux.config.SQLconnection,)
         return list(filtered_length["length"])[0]
 
     #######################################################
@@ -473,31 +559,11 @@ def compute_dataset_metadata(self, tbl: LuxSQLTable):
         """
         if not tbl._setup_done:
             self.get_SQL_attributes(tbl)
-            tbl._data_type = {}
-            tbl._min_max = {}
-            length_query = pandas.read_sql("SELECT COUNT(1) as length FROM {}".format(tbl.table_name),lux.config.SQLconnection,)
-            tbl._length = list(length_query["length"])[0]
-            #####NOTE: since we aren't expecting users to do much data processing with the SQL database, should we just keep this
-            #####      in the initialization and do it just once
-            self.get_unique_values(tbl)
-            self.get_cardinality(tbl)
-            self.compute_data_type(tbl)
-            self.compute_min_max(tbl)
-
-    def compute_min_max(self, tbl: LuxSQLTable):
-        for attribute in tbl.columns:
-            if tbl._data_type[attribute] == "quantitative":
-                # min_max_query = pandas.read_sql(
-                #     'SELECT MIN("{}") as min, MAX("{}") as max FROM {}'.format(
-                #         attribute, attribute, tbl.table_name
-                #     ),
-                #     lux.config.SQLconnection,
-                # )
-                # tbl._min_max[attribute] = (
-                #     list(min_max_query["min"])[0],
-                #     list(min_max_query["max"])[0],
-                # )
-                tbl._min_max[attribute] = (min(tbl.unique_values[attribute]), max(tbl.unique_values[attribute]))
+        tbl._data_type = {}
+        #####NOTE: since we aren't expecting users to do much data processing with the SQL database, should we just keep this
+        #####      in the initialization and do it just once
+        self.compute_data_type(tbl)
+        self.compute_stats(tbl)
 
     def get_SQL_attributes(self, tbl: LuxSQLTable):
         """
@@ -517,7 +583,9 @@ def get_SQL_attributes(self, tbl: LuxSQLTable):
             table_name = tbl.table_name[self.table_name.index(".") + 1 :]
         else:
             table_name = tbl.table_name
-        attr_query = "SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '{}'".format(table_name)
+        attr_query = lux.config.query_templates['table_attributes_query'].format(
+            table_name = table_name, 
+        )
         attributes = list(pandas.read_sql(attr_query, lux.config.SQLconnection)["column_name"])
         for attr in attributes:
             tbl[attr] = None
@@ -538,13 +606,16 @@ def compute_stats(self, tbl: LuxSQLTable):
         None
         """
         # precompute statistics
-        # tbl.unique_values = {}
-        # tbl._min_max = {}
-        length_query = pandas.read_sql("SELECT COUNT(1) as length FROM {}".format(tbl.table_name),lux.config.SQLconnection,)
+        tbl.unique_values = {}
+        tbl._min_max = {}
+        length_query = pandas.read_sql(lux.config.query_templates['length_query'].format(table_name = tbl.table_name, where_clause = ""),lux.config.SQLconnection,)
         tbl._length = list(length_query["length"])[0]
 
         self.get_unique_values(tbl)
-        self.get_cardinality(tbl)
+        for attribute in tbl.columns:
+            if tbl._data_type[attribute] == "quantitative":
+                min_max_query = pandas.read_sql(lux.config.query_templates['min_max_query'].format(attribute = attribute, table_name = tbl.table_name),lux.config.SQLconnection,)
+                tbl._min_max[attribute] = (list(min_max_query["min"])[0],list(min_max_query["max"])[0],)
 
     def get_cardinality(self, tbl: LuxSQLTable):
         """
@@ -562,7 +633,9 @@ def get_cardinality(self, tbl: LuxSQLTable):
         """
         cardinality = {}
         for attr in list(tbl.columns):
-            cardinality[attr] = len(tbl.unique_values[attr])
+            card_query = lux.config.query_templates['cardinality_query'].format(attribute = attr, table_name = tbl.table_name)
+            card_data = pandas.read_sql(card_query,lux.config.SQLconnection,)
+            cardinality[attr] = list(card_data["count"])[0]
         tbl.cardinality = cardinality
 
     def get_unique_values(self, tbl: LuxSQLTable):
@@ -581,11 +654,8 @@ def get_unique_values(self, tbl: LuxSQLTable):
         """
         unique_vals = {}
         for attr in list(tbl.columns):
-            unique_query = 'SELECT Distinct("{}") FROM {} WHERE "{}" IS NOT NULL'.format(attr, tbl.table_name, attr)
-            unique_data = pandas.read_sql(
-                unique_query,
-                lux.config.SQLconnection,
-            )
+            unique_query = lux.config.query_templates['unique_query'].format(attribute = attr, table_name = tbl.table_name)
+            unique_data = pandas.read_sql(unique_query,lux.config.SQLconnection,)
             unique_vals[attr] = list(unique_data[attr])
         tbl.unique_values = unique_vals
 
@@ -604,13 +674,14 @@ def compute_data_type(self, tbl: LuxSQLTable):
         None
         """
         data_type = {}
+        self.get_cardinality(tbl)
         if "." in tbl.table_name:
             table_name = tbl.table_name[tbl.table_name.index(".") + 1 :]
         else:
             table_name = tbl.table_name
         # get the data types of the attributes in the SQL table
         for attr in list(tbl.columns):
-            datatype_query = "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{}' AND COLUMN_NAME = '{}'".format(table_name, attr)
+            datatype_query = lux.config.query_templates['datatype_query'].format(table_name = table_name, attribute = attr)
             datatype = list(pandas.read_sql(datatype_query, lux.config.SQLconnection)["data_type"])[0]
             if str(attr).lower() in {"month", "year"} or "time" in datatype or "date" in datatype:
                 data_type[attr] = "temporal"
@@ -631,11 +702,12 @@ def compute_data_type(self, tbl: LuxSQLTable):
                 "smallint",
                 "smallserial",
                 "serial",
+                "double",
                 "double precision",
             }:
                 if tbl.cardinality[attr] < 13:
                     data_type[attr] = "nominal"
-                elif check_if_id_like_for_sql(tbl, attr):
+                elif check_if_id_like(tbl, attr):
                     data_type[attr] = "id"
                 else:
                     data_type[attr] = "quantitative"
diff --git a/lux/interestingness/interestingness.py b/lux/interestingness/interestingness.py
index 417310b7..51620de5 100644
--- a/lux/interestingness/interestingness.py
+++ b/lux/interestingness/interestingness.py
@@ -150,10 +150,9 @@ def interestingness(vis: Vis, ldf: LuxDataFrame) -> int:
                 color_cardinality = ldf.cardinality[color_column]
                 groupby_cardinality = ldf.cardinality[groupby_column]
                 # scale down score based on number of categories
-                chi2_score = chi2_contingency(contingency_tbl)[0] * 0.9 ** (
+                score = chi2_contingency(contingency_tbl)[0] * 0.9 ** (
                     color_cardinality + groupby_cardinality
                 )
-                score = min(0.01, chi2_score)
             except (ValueError, KeyError):
                 # ValueError results if an entire column of the contingency table is 0, can happen if an applied filter results in a category having no counts
                 score = -1
@@ -312,10 +311,7 @@ def unevenness(vis: Vis, ldf: LuxDataFrame, measure_lst: list, dimension_lst: li
     v_flat = pd.Series([1 / C] * len(v))
     if is_datetime(v):
         v = v.astype("int")
-    try:
-        return D * euclidean(v, v_flat)
-    except (ValueError):
-        return 0.01
+    return D * euclidean(v, v_flat)
 
 
 def mutual_information(v_x: list, v_y: list) -> int:
diff --git a/lux/processor/Compiler.py b/lux/processor/Compiler.py
index 2cb227b5..1179b39c 100644
--- a/lux/processor/Compiler.py
+++ b/lux/processor/Compiler.py
@@ -164,13 +164,11 @@ def populate_data_type_model(ldf, vlist):
         from lux.utils.date_utils import is_datetime_string
 
         data_model_lookup = lux.config.executor.compute_data_model_lookup(ldf.data_type)
-
         for vis in vlist:
             for clause in vis._inferred_intent:
                 if clause.description == "?":
                     clause.description = ""
                 # TODO: Note that "and not is_datetime_string(clause.attribute))" is a temporary hack and breaks the `test_row_column_group` example
-                # and not is_datetime_string(clause.attribute):
                 if clause.attribute != "" and clause.attribute != "Record":
                     if clause.data_type == "":
                         clause.data_type = ldf.data_type[clause.attribute]
@@ -273,7 +271,15 @@ def line_or_bar_or_geo(ldf, dimension: Clause, measure: Clause):
             if measure.aggregation == "":
                 measure.set_aggregation("mean")
             if dim_type == "temporal" or dim_type == "oridinal":
-                return "line", {"x": dimension, "y": measure}
+                if isinstance(dimension.attribute, pd.Timestamp):
+                    # If timestamp, use the _repr_ (e.g., TimeStamp('2020-04-05 00.000')--> '2020-04-05')
+                    attr = str(dimension.attribute._date_repr)
+                else:
+                    attr = dimension.attribute
+                if ldf.cardinality[attr] == 1:
+                    return "bar", {"x": measure, "y": dimension}
+                else:
+                    return "line", {"x": dimension, "y": measure}
             else:  # unordered categorical
                 # if cardinality large than 5 then sort bars
                 if ldf.cardinality[dimension.attribute] > 5:
@@ -366,6 +372,15 @@ def line_or_bar_or_geo(ldf, dimension: Clause, measure: Clause):
             for attr in relevant_attributes
             if attr != "Record" and attr in ldf._min_max
         )
+        # Replace scatterplot with heatmap
+        HBIN_START = 5000
+        if vis.mark == "scatter" and lux.config.heatmap and len(ldf) > HBIN_START:
+            vis._postbin = True
+            ldf._message.add_unique(
+                f"Large scatterplots detected: Lux is automatically binning scatterplots to heatmaps.",
+                priority=98,
+            )
+            vis._mark = "heatmap"
         vis._min_max = relevant_min_max
         if auto_channel != {}:
             vis = Compiler.enforce_specified_channel(vis, auto_channel)
diff --git a/lux/utils/date_utils.py b/lux/utils/date_utils.py
index 66086f1c..69d32c3c 100644
--- a/lux/utils/date_utils.py
+++ b/lux/utils/date_utils.py
@@ -83,7 +83,8 @@ def compute_date_granularity(date_column: pd.core.series.Series):
             A str specifying the granularity of dates for the inspected temporal column
     """
     # supporting a limited set of Vega-Lite TimeUnit (https://vega.github.io/vega-lite/docs/timeunit.html)
-    date_fields = ["day", "month", "year"]
+    # corresponding to Pandas timescales
+    date_fields = ["day", "month", "year", "dayofweek"]
     date_index = pd.DatetimeIndex(date_column)
     for field in date_fields:
         # can be changed to sum(getattr(date_index, field)) != 0
diff --git a/lux/utils/tracing_utils.py b/lux/utils/tracing_utils.py
index 76b130b9..f69a4fdc 100644
--- a/lux/utils/tracing_utils.py
+++ b/lux/utils/tracing_utils.py
@@ -2,46 +2,63 @@
 import sys
 import pickle as pkl
 import lux
+import autopep8
+import math
 
-class LuxTracer():            
+
+class LuxTracer:
     def profile_func(self, frame, event, arg):
-        #Profile functions should have three arguments: frame, event, and arg. 
-        #frame is the current stack frame. 
-        #event is a string: 'call', 'return', 'c_call', 'c_return', or 'c_exception'.
-        #arg depends on the event type.
+        # Profile functions should have three arguments: frame, event, and arg.
+        # frame is the current stack frame.
+        # event is a string: 'call', 'return', 'c_call', 'c_return', or 'c_exception'.
+        # arg depends on the event type.
         # See: https://docs.python.org/3/library/sys.html#sys.settrace
-        try: 
-            if event == 'line' :
-                #frame objects are described here: https://docs.python.org/3/library/inspect.html
-                fcode = frame.f_code             
+        try:
+            if event == "line":
+                # frame objects are described here: https://docs.python.org/3/library/inspect.html
+                fcode = frame.f_code
                 line_no = frame.f_lineno
                 func_name = fcode.co_name
-                #includeMod = ['lux/vis', 'lux/action', 'lux/vislib', 'lux/executor', 'lux/interestingness']
-                includeMod = ['lux\\vis', 'lux\\vislib', 'lux\\executor']
-                includeFunc = ['add_quotes', 'execute_sampling', 'execute_filter', 'execute_binning', 'execute_scatter', 'execute_aggregate', 'execute_2D_binning']
+                # includeMod = ['lux/vis', 'lux/action', 'lux/vislib', 'lux/executor', 'lux/interestingness']
+                includeMod = ["lux\\vis", "lux\\vislib", "lux\\executor"]
+                includeFunc = [
+                    "add_quotes",
+                    "execute_sampling",
+                    "execute_filter",
+                    "execute_binning",
+                    "execute_scatter",
+                    "execute_aggregate",
+                    "execute_2D_binning",
+                ]
                 if any(x in frame.f_code.co_filename for x in includeMod):
-                    if (func_name!="<lambda>"): #ignore one-liner lambda functions (repeated line events)
-                        if any(x in f"{frame.f_code.co_filename}--{func_name}--{line_no}" for x in includeFunc):
-                            lux.config.tracer_relevant_lines.append([frame.f_code.co_filename,func_name,line_no])
-                            #print(f"{frame.f_code.co_filename}--{func_name}--{line_no}")
-                    
+                    if (
+                        func_name != "<lambda>"
+                    ):  # ignore one-liner lambda functions (repeated line events)
+                        if any(
+                            x in f"{frame.f_code.co_filename}--{func_name}--{line_no}"
+                            for x in includeFunc
+                        ):
+                            lux.config.tracer_relevant_lines.append(
+                                [frame.f_code.co_filename, func_name, line_no]
+                            )
+                            # print(f"{frame.f_code.co_filename}--{func_name}--{line_no}")
+
         except:
             # pass  # just swallow errors to avoid interference with traced processes
             raise  # for debugging
         return self.profile_func
 
     def start_tracing(self):
-        #print ("-----------start_tracing-----------")
+        # print ("-----------start_tracing-----------")
         # Implement python source debugger: https://docs.python.org/3/library/sys.html#sys.settrace
         # setprofile faster than settrace (only go through I/O)
-        sys.settrace(self.profile_func) 
+        sys.settrace(self.profile_func)
 
     def stop_tracing(self):
-        #print ("-----------stop_tracing-----------")
-        sys.settrace(None) 
-
+        # print ("-----------stop_tracing-----------")
+        sys.settrace(None)
 
-    def process_executor_code(self,executor_lines):
+    def process_executor_code(self, executor_lines):
         selected = {}
         selected_index = {}
         index = 0
@@ -55,26 +72,49 @@ def process_executor_code(self,executor_lines):
             line = executor_lines[l]
             filename = line[0]
             funcname = line[1]
-            line_no = line[2]-1
-            
-            codelines = open(filename).readlines()# TODO: do sharing of file content here
-            if (funcname not in ['__init__']):
+            line_no = line[2] - 1
+
+            codelines = open(filename).readlines()  # TODO: do sharing of file content here
+            if funcname not in ["__init__"]:
                 code = codelines[line_no]
-                ignore_construct = ['    if','elif','return', 'try'] # prune out these control flow programming constructs                    
-                ignore_lux_keyword = ['self.code','self.name','__init__',"'''",'self.output_type', 'message.add_unique', 'Large scatterplots detected', 'priority=']# Lux-specific keywords to ignore
-                ignore = ignore_construct+ignore_lux_keyword
-                #print("PandasExecutor.apply_filter" in codelines[line_no].lstrip(), codelines[line_no].lstrip())
+                ignore_construct = [
+                    "    if",
+                    "elif",
+                    "return",
+                    "try",
+                    "assert",
+                ]  # prune out these control flow programming constructs
+                ignore_lux_keyword = [
+                    "self.code",
+                    "self.name",
+                    "__init__",
+                    "'''",
+                    "self.output_type",
+                    "message.add_unique",
+                    "Large scatterplots detected",
+                    "priority=",
+                ]  # Lux-specific keywords to ignore
+                ignore = ignore_construct + ignore_lux_keyword
                 if not any(construct in code for construct in ignore):
-                    #need to handle for loops, this keeps track of when a for loop shows up and when the for loop code is repeated
                     clean_code_line = codelines[line_no].lstrip()
-                    if clean_code_line not in selected:
+                    code_line = codelines[line_no].replace("    ", "", 2)
+                    if clean_code_line.lstrip() not in selected:
                         if "def add_quotes(var_name):" in clean_code_line:
-                            clean_code_line = "def add_quotes(var_name):\n\treturn \'\"\' + var_name + \'\"\'\n"
+                            clean_code_line = (
+                                "def add_quotes(var_name):\n\treturn '\"' + var_name + '\"'\n"
+                            )
                             selected[clean_code_line] = index
-                            selected_index[index] = clean_code_line.replace("    ", "", 3)
+                            selected_index[index] = clean_code_line.lstrip()
                         else:
-                            selected[clean_code_line] = index
-                            selected_index[index] = codelines[line_no].replace("    ", "", 3)
+                            leading_spaces = len(code_line) - len(code_line.lstrip())
+                            num_tabs = math.ceil(leading_spaces / 8)
+                            # if num_tabs > 0: num_tabs = num_tabs-1
+                            # clean_code_line = "\t" + code_line.lstrip()
+                            # clean_code_line = code_line.replace("    ", "\t")
+                            clean_code_line = "\t" * num_tabs + code_line.lstrip()
+                            if clean_code_line.lstrip() not in selected:
+                                selected_index[index] = clean_code_line
+                                selected[clean_code_line.lstrip()] = index
                         index += 1
 
         curr_executor = lux.config.executor.name
@@ -90,7 +130,12 @@ def process_executor_code(self,executor_lines):
             output += selected_index[key]
 
         if curr_executor != "PandasExecutor":
-            output+="\nview"
+            output += "\nview"
         else:
-            output+="\nvis"
-        return(output)
\ No newline at end of file
+            output += "\nvis"
+
+        # options = autopep8.parse_args(['--max-line-length', '100000', '-', "--ignore", "E231,E225,E226,E227,E228,E22"])
+        # options = autopep8.parse_args(['--max-line-length', '100000', '-', "--ignore", "E101,E128,E211,E22,E27,W191,E231"])
+        options = autopep8.parse_args(["--max-line-length", "100000", "-", "--select", "E20,E1"])
+        output = autopep8.fix_code(output, options)
+        return output
diff --git a/lux/utils/utils.py b/lux/utils/utils.py
index b5be5b07..639c9778 100644
--- a/lux/utils/utils.py
+++ b/lux/utils/utils.py
@@ -109,9 +109,11 @@ def check_if_id_like(df, attribute):
             almost_all_vals_unique = df.cardinality[attribute] >= 0.75 * len(df)
         return high_cardinality and (almost_all_vals_unique or evenly_spaced)
 
+
 def check_if_id_like_for_sql(df, attribute):
     return df.cardinality[attribute] >= 0.98 * len(df)
 
+
 def like_nan(val):
     if isinstance(val, str):
         return val.lower() == "nan"
diff --git a/lux/vis/Clause.py b/lux/vis/Clause.py
index fcaf71d3..19a8d134 100644
--- a/lux/vis/Clause.py
+++ b/lux/vis/Clause.py
@@ -33,6 +33,7 @@ def __init__(
         bin_size: int = 0,
         weight: float = 1,
         sort: str = "",
+        timescale: str = "",
         exclude: typing.Union[str, list] = "",
     ):
         """
@@ -67,6 +68,9 @@ def __init__(
                 Number of bins for histograms, by default 0
         weight : float, optional
                 A number between 0 and 1 indicating the importance of this Clause, by default 1
+        timescale : str, optional
+                If data type is temporal, indicate whether temporal associated with timescale (if empty, then plot overall).
+                If timescale is present, the line chart axis is based on ordinal data type (non-date axis).
         sort : str, optional
                 Specifying whether and how the bar chart should be sorted
                 Possible values: 'ascending', 'descending', by default ""
@@ -86,6 +90,7 @@ def __init__(
         self.bin_size = bin_size
         self.weight = weight
         self.sort = sort
+        self.timescale = timescale
         self.exclude = exclude
 
     def get_attr(self):
diff --git a/lux/vis/Vis.py b/lux/vis/Vis.py
index a6158d08..79f53546 100644
--- a/lux/vis/Vis.py
+++ b/lux/vis/Vis.py
@@ -36,6 +36,7 @@ def __init__(self, intent, source=None, title="", score=0.0):
         self.title = title
         self.score = score
         self._all_column = False
+        self.approx = False
         self.refresh_source(self._source)
 
     def __repr__(self):
@@ -130,6 +131,7 @@ def _ipython_display_(self):
                 recommendations=[],
                 intent="",
                 message="",
+                config={"plottingScale": lux.config.plotting_scale},
             )
             display(widget)
 
@@ -315,15 +317,15 @@ def to_code(self, language="vegalite", **kwargs):
         elif language == "python":
             lux.config.tracer.start_tracing()
             lux.config.executor.execute(lux.vis.VisList.VisList(input_lst=[self]), self._source)
-            lux.config.tracer.stop_tracing() 
+            lux.config.tracer.stop_tracing()
             self._trace_code = lux.config.tracer.process_executor_code(lux.config.tracer_relevant_lines)
             lux.config.tracer_relevant_lines = []
-            return(self._trace_code)
+            return self._trace_code
         elif language == "SQL":
             if self._query:
                 return self._query
             else:
-                 warnings.warn(
+                warnings.warn(
                     "The data for this Vis was not collected via a SQL database. Use the 'python' parameter to view the code used to generate the data.",
                     stacklevel=2,
                 )
diff --git a/lux/vis/VisList.py b/lux/vis/VisList.py
index d1746f68..97e51eb3 100644
--- a/lux/vis/VisList.py
+++ b/lux/vis/VisList.py
@@ -272,7 +272,13 @@ def _ipython_display_(self):
         import luxwidget
 
         recJSON = LuxDataFrame.rec_to_JSON([recommendation])
-        self._widget = luxwidget.LuxWidget(currentVis={}, recommendations=recJSON, intent="", message="")
+        self._widget = luxwidget.LuxWidget(
+            currentVis={},
+            recommendations=recJSON,
+            intent="",
+            message="",
+            config={"plottingScale": lux.config.plotting_scale},
+        )
         display(self._widget)
 
     def refresh_source(self, ldf):
@@ -303,6 +309,7 @@ def refresh_source(self, ldf):
             self._source = ldf
             self._source.maintain_metadata()
             if len(self._input_lst) > 0:
+                approx = False
                 if self._is_vis_input():
                     compiled_collection = []
                     for vis in self._collection:
@@ -315,4 +322,15 @@ def refresh_source(self, ldf):
                     self._inferred_intent = Parser.parse(self._intent)
                     Validator.validate_intent(self._inferred_intent, ldf)
                     self._collection = Compiler.compile_intent(ldf, self._inferred_intent)
-                lux.config.executor.execute(self._collection, ldf)
+
+                # Early pruning determination criteria
+                width_criteria = len(self._collection) > (lux.config.topk + 3)
+                length_criteria = len(ldf) > lux.config.early_pruning_sample_start
+                if lux.config.early_pruning and width_criteria and length_criteria:
+                    # print("Apply approx to this VisList")
+                    ldf._message.add_unique(
+                        "Large search space detected: Lux is approximating the interestingness of recommended visualizations.",
+                        priority=1,
+                    )
+                    approx = True
+                lux.config.executor.execute(self._collection, ldf, approx=approx)
diff --git a/lux/vislib/altair/AltairChart.py b/lux/vislib/altair/AltairChart.py
index 03c4c676..c56d7d2f 100644
--- a/lux/vislib/altair/AltairChart.py
+++ b/lux/vislib/altair/AltairChart.py
@@ -16,6 +16,7 @@
 import numpy as np
 import altair as alt
 from lux.utils.date_utils import compute_date_granularity
+import lux
 
 
 class AltairChart:
@@ -69,7 +70,8 @@ def apply_default_config(self):
             labelFontSize=9,
             labelFont="Helvetica Neue",
         )
-        self.chart = self.chart.properties(width=160, height=150)
+        plotting_scale = lux.config.plotting_scale
+        self.chart = self.chart.properties(width=160 * plotting_scale, height=150 * plotting_scale)
         self.code += (
             "\nchart = chart.configure_title(fontWeight=500,fontSize=13,font='Helvetica Neue')\n"
         )
@@ -77,7 +79,9 @@ def apply_default_config(self):
         self.code += "\t\t\t\t\tlabelFontWeight=400,labelFontSize=8,labelFont='Helvetica Neue',labelColor='#505050')\n"
         self.code += "chart = chart.configure_legend(titleFontWeight=500,titleFontSize=10,titleFont='Helvetica Neue',\n"
         self.code += "\t\t\t\t\tlabelFontWeight=400,labelFontSize=8,labelFont='Helvetica Neue')\n"
-        self.code += "chart = chart.properties(width=160,height=150)\n"
+        self.code += (
+            f"chart = chart.properties(width={160 * plotting_scale},height={150 * plotting_scale})\n"
+        )
 
     def encode_color(self):
         color_attr = self.vis.get_attr_by_channel("color")
diff --git a/lux/vislib/altair/AltairRenderer.py b/lux/vislib/altair/AltairRenderer.py
index 713f78a6..81388e52 100644
--- a/lux/vislib/altair/AltairRenderer.py
+++ b/lux/vislib/altair/AltairRenderer.py
@@ -50,10 +50,13 @@ def create_vis(self, vis, standalone=True):
                 Output Altair Chart Object
         """
         # Lazy Evaluation for 2D Binning
-        if vis.mark == "scatter" and vis._postbin:
-            if lux.config.executor.name == "PandasExecutor":
+        if vis.approx:
+            if vis.mark == "scatter" and vis._postbin:
                 vis._mark = "heatmap"
                 lux.config.executor.execute_2D_binning(vis)
+            else:
+                # Exactly recompute the selected vis (e.g., top k) to display
+                lux.config.executor.execute([vis], vis._original_df, approx=False)
         # If a column has a Period dtype, or contains Period objects, convert it back to Datetime
         if vis.data is not None:
             for attr in list(vis.data.columns):
diff --git a/lux/vislib/altair/BarChart.py b/lux/vislib/altair/BarChart.py
index 30b8fcfe..4624c0d0 100644
--- a/lux/vislib/altair/BarChart.py
+++ b/lux/vislib/altair/BarChart.py
@@ -14,6 +14,7 @@
 
 from lux.vislib.altair.AltairChart import AltairChart
 import altair as alt
+import lux
 
 alt.data_transformers.disable_max_rows()
 from lux.utils.utils import get_agg_title
@@ -51,6 +52,13 @@ def initialize_chart(self):
             x_attr.attribute = x_attr.attribute.replace(".", "")
         if isinstance(y_attr.attribute, str):
             y_attr.attribute = y_attr.attribute.replace(".", "")
+        # To get datetime to display correctly on bar charts
+        if x_attr.data_type == "temporal":
+            x_attr.data_type = "nominal"
+            self.code += "from pandas import Timestamp\n"
+        if y_attr.data_type == "temporal":
+            y_attr.data_type = "nominal"
+            self.code += "from pandas import Timestamp\n"
 
         if x_attr.data_model == "measure":
             agg_title = get_agg_title(x_attr)
@@ -94,14 +102,15 @@ def initialize_chart(self):
         k = 10
         self._topkcode = ""
         n_bars = len(self.data.iloc[:, 0].unique())
+        plotting_scale = lux.config.plotting_scale
 
         if n_bars > k:  # Truncating to only top k
             remaining_bars = n_bars - k
             self.data = self.data.nlargest(k, columns=measure_attr)
             self.data = AltairChart.sanitize_dataframe(self.data)
             self.text = alt.Chart(self.data).mark_text(
-                x=155,
-                y=142,
+                x=155 * plotting_scale,
+                y=142 * plotting_scale,
                 align="right",
                 color="#ff8e04",
                 fontSize=11,
@@ -109,8 +118,8 @@ def initialize_chart(self):
             )
 
             self._topkcode = f"""text = alt.Chart(visData).mark_text(
-			x=155, 
-			y=142,
+			x={155 * plotting_scale}, 
+			y={142 * plotting_scale},
 			align="right",
 			color = "#ff8e04",
 			fontSize = 11,
diff --git a/lux/vislib/altair/Choropleth.py b/lux/vislib/altair/Choropleth.py
index 2e3e969b..3a0f0b30 100644
--- a/lux/vislib/altair/Choropleth.py
+++ b/lux/vislib/altair/Choropleth.py
@@ -14,7 +14,7 @@
 
 from lux.vislib.altair.AltairChart import AltairChart
 import altair as alt
-import us
+import pandas as pd
 from iso3166 import countries
 
 alt.data_transformers.disable_max_rows()
@@ -62,8 +62,6 @@ def initialize_chart(self):
             y_attr.attribute = y_attr.attribute.replace(".", "")
 
         self.data = AltairChart.sanitize_dataframe(self.data)
-        height = 175
-        width = int(height * (5 / 3))
 
         points = (
             alt.Chart(geo_map)
@@ -76,9 +74,7 @@ def initialize_chart(self):
                 from_=alt.LookupData(self.data, str(x_attr.attribute), [str(y_attr.attribute)]),
             )
             .project(type=map_type)
-            .properties(
-                width=width, height=height, title=f"Mean of {y_attr_abv} across {geographical_name}"
-            )
+            .properties(title=f"Mean of {y_attr_abv} across {geographical_name}")
         )
 
         chart = background + points
@@ -101,8 +97,6 @@ def initialize_chart(self):
 ).project(
     type="{map_type}"
 ).properties(
-    width={width},
-    height={height},
     title="Mean of {y_attr_abv} across {geographical_name}"
 )
 chart = background + points
@@ -124,14 +118,12 @@ def get_background(self, feature):
             ),
         }
         assert feature in maps
-        height = 175
         background = (
             alt.Chart(maps[feature][0])
             .mark_geoshape(fill="lightgray", stroke="white")
-            .properties(width=int(height * (5 / 3)), height=height)
             .project(maps[feature][1])
         )
-        background_str = f"(alt.Chart({maps[feature][2]}).mark_geoshape(fill='lightgray', stroke='white').properties(width=int({height} * (5 / 3)), height={height}).project('{maps[feature][1]}'))"
+        background_str = f"(alt.Chart({maps[feature][2]}).mark_geoshape(fill='lightgray', stroke='white').project('{maps[feature][1]}'))"
         return background, background_str
 
     def get_geomap(self, feature):
@@ -156,12 +148,72 @@ def get_geomap(self, feature):
 
     def get_us_fips_code(self, attribute):
         """Returns FIPS code given a US state"""
+        usa = pd.DataFrame(
+            [
+                {"fips": 1, "state": "alabama", "abbrev": "al"},
+                {"fips": 2, "state": "alaska", "abbrev": "ak"},
+                {"fips": 4, "state": "arizona", "abbrev": "az"},
+                {"fips": 5, "state": "arkansas", "abbrev": "ar"},
+                {"fips": 6, "state": "california", "abbrev": "ca"},
+                {"fips": 8, "state": "colorado", "abbrev": "co"},
+                {"fips": 9, "state": "connecticut", "abbrev": "ct"},
+                {"fips": 10, "state": "delaware", "abbrev": "de"},
+                {"fips": 11, "state": "district of columbia", "abbrev": "dc"},
+                {"fips": 12, "state": "florida", "abbrev": "fl"},
+                {"fips": 13, "state": "georgia", "abbrev": "ga"},
+                {"fips": 15, "state": "hawaii", "abbrev": "hi"},
+                {"fips": 16, "state": "idaho", "abbrev": "id"},
+                {"fips": 17, "state": "illinois", "abbrev": "il"},
+                {"fips": 18, "state": "indiana", "abbrev": "in"},
+                {"fips": 19, "state": "iowa", "abbrev": "ia"},
+                {"fips": 20, "state": "kansas", "abbrev": "ks"},
+                {"fips": 21, "state": "kentucky", "abbrev": "ky"},
+                {"fips": 22, "state": "louisiana", "abbrev": "la"},
+                {"fips": 23, "state": "maine", "abbrev": "me"},
+                {"fips": 24, "state": "maryland", "abbrev": "md"},
+                {"fips": 25, "state": "massachusetts", "abbrev": "ma"},
+                {"fips": 26, "state": "michigan", "abbrev": "mi"},
+                {"fips": 27, "state": "minnesota", "abbrev": "mn"},
+                {"fips": 28, "state": "mississippi", "abbrev": "ms"},
+                {"fips": 29, "state": "missouri", "abbrev": "mo"},
+                {"fips": 30, "state": "montana", "abbrev": "mt"},
+                {"fips": 31, "state": "nebraska", "abbrev": "ne"},
+                {"fips": 32, "state": "nevada", "abbrev": "nv"},
+                {"fips": 33, "state": "new hampshire", "abbrev": "nh"},
+                {"fips": 34, "state": "new jersey", "abbrev": "nj"},
+                {"fips": 35, "state": "new mexico", "abbrev": "nm"},
+                {"fips": 36, "state": "new york", "abbrev": "ny"},
+                {"fips": 37, "state": "north carolina", "abbrev": "nc"},
+                {"fips": 38, "state": "north dakota", "abbrev": "nd"},
+                {"fips": 39, "state": "ohio", "abbrev": "oh"},
+                {"fips": 40, "state": "oklahoma", "abbrev": "ok"},
+                {"fips": 41, "state": "oregon", "abbrev": "or"},
+                {"fips": 42, "state": "pennsylvania", "abbrev": "pa"},
+                {"fips": 44, "state": "rhode island", "abbrev": "ri"},
+                {"fips": 45, "state": "south carolina", "abbrev": "sc"},
+                {"fips": 46, "state": "south dakota", "abbrev": "sd"},
+                {"fips": 47, "state": "tennessee", "abbrev": "tn"},
+                {"fips": 48, "state": "texas", "abbrev": "tx"},
+                {"fips": 49, "state": "utah", "abbrev": "ut"},
+                {"fips": 50, "state": "vermont", "abbrev": "vt"},
+                {"fips": 51, "state": "virginia", "abbrev": "va"},
+                {"fips": 53, "state": "washington", "abbrev": "wa"},
+                {"fips": 54, "state": "west virginia", "abbrev": "wv"},
+                {"fips": 55, "state": "wisconsin", "abbrev": "wi"},
+                {"fips": 56, "state": "wyoming", "abbrev": "wy"},
+            ]
+        )
+        attribute = attribute.lower()
         if not isinstance(attribute, str):
             return attribute
-        try:
-            return int(us.states.lookup(attribute).fips)
-        except:
-            return attribute
+        match = usa[(usa.state == attribute) | (usa.abbrev == attribute)]
+        if len(match) == 1:
+            return match["fips"].values[0]
+        else:
+            if attribute in ["washington d.c.", "washington dc", "d.c.", "d.c"]:
+                return 11
+            else:
+                return attribute
 
     def get_country_iso_code(self, attribute):
         """Returns country ISO code given a country"""
diff --git a/lux/vislib/altair/LineChart.py b/lux/vislib/altair/LineChart.py
index ae589030..09703b3a 100644
--- a/lux/vislib/altair/LineChart.py
+++ b/lux/vislib/altair/LineChart.py
@@ -43,6 +43,11 @@ def initialize_chart(self):
         x_attr_abv = str(x_attr.attribute)
         y_attr_abv = str(y_attr.attribute)
 
+        if x_attr.timescale != "":
+            x_attr.data_type = "ordinal"
+        if y_attr.timescale != "":
+            y_attr.data_type = "ordinal"
+
         if len(x_attr_abv) > 25:
             x_attr_abv = x_attr.attribute[:15] + "..." + x_attr.attribute[-10:]
         if len(y_attr_abv) > 25:
@@ -56,8 +61,7 @@ def initialize_chart(self):
         # Remove NaNs only for Line Charts (offsets axis range)
         self.data = self.data.dropna(subset=[x_attr.attribute, y_attr.attribute])
         self.code += "import altair as alt\n"
-        self.code += "import pandas._libs.tslibs.timestamps\n"
-        self.code += "from pandas._libs.tslibs.timestamps import Timestamp\n"
+        self.code += "from pandas import Timestamp\n"
         self.code += f"visData = pd.DataFrame({str(self.data.to_dict())})\n"
 
         if y_attr.data_model == "measure":
@@ -72,7 +76,7 @@ def initialize_chart(self):
                 axis=alt.Axis(title=y_attr_abv),
             )
             x_attr_field_code = f"alt.X('{x_attr.attribute}', type = '{x_attr.data_type}', axis=alt.Axis(title='{x_attr_abv}'))"
-            y_attr_fieldCode = f"alt.Y('{y_attr.attribute}', type= '{y_attr.data_type}', title='{agg_title}', axis=alt.Axis(title='{y_attr_abv}')"
+            y_attr_field_code = f"alt.Y('{y_attr.attribute}', type= '{y_attr.data_type}', title='{agg_title}', axis=alt.Axis(title='{y_attr_abv}'))"
         else:
             agg_title = get_agg_title(x_attr)
             x_attr_spec = alt.X(
@@ -84,14 +88,14 @@ def initialize_chart(self):
             y_attr_spec = alt.Y(
                 str(y_attr.attribute), type=y_attr.data_type, axis=alt.Axis(title=y_attr_abv)
             )
-            x_attr_field_code = f"alt.X('{x_attr.attribute}', type = '{x_attr.data_type}', title='{agg_title}', axis=alt.Axis(title='{x_attr_abv}')"
-            y_attr_fieldCode = f"alt.Y('{y_attr.attribute}', type= '{y_attr.data_type}', axis=alt.Axis(title='{y_attr_abv}')"
+            x_attr_field_code = f"alt.X('{x_attr.attribute}', type = '{x_attr.data_type}', title='{agg_title}', axis=alt.Axis(title='{x_attr_abv}'))"
+            y_attr_field_code = f"alt.Y('{y_attr.attribute}', type= '{y_attr.data_type}', axis=alt.Axis(title='{y_attr_abv}'))"
         self.data = AltairChart.sanitize_dataframe(self.data)
         chart = alt.Chart(self.data).mark_line().encode(x=x_attr_spec, y=y_attr_spec)
         chart = chart.interactive()  # Enable Zooming and Panning
         self.code += f"""
 		chart = alt.Chart(visData).mark_line().encode(
-		    y = {y_attr_fieldCode},
+		    y = {y_attr_field_code},
 		    x = {x_attr_field_code},
 		)
 		chart = chart.interactive() # Enable Zooming and Panning
diff --git a/pyproject.toml b/pyproject.toml
index f8f8a67b..b7a13980 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,2 +1,3 @@
 [tool.black]
-line-length = 105
\ No newline at end of file
+line-length = 105
+exclude = 'Executor\.py$'
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index dd3b9615..660d5020 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,8 +4,8 @@ sphinx-automodapi>=0.13
 Sphinx>=3.0.2
 sphinx-rtd-theme>=0.4.3
 xlrd
-black
+black==20.8b1
 # Install to use SQLExecutor
 psycopg2>=2.8.5
 psycopg2-binary>=2.8.5
-lxml
\ No newline at end of file
+lxml
diff --git a/requirements.txt b/requirements.txt
index 0beb7050..340daf5e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,5 +5,4 @@ pandas>=1.2.0
 scikit-learn>=0.22
 matplotlib>=3.0.0
 lux-widget>=0.1.4
-us
 iso3166
\ No newline at end of file


Test patch:
diff --git a/tests/test_action.py b/tests/test_action.py
index 73889715..b1d5ddbd 100644
--- a/tests/test_action.py
+++ b/tests/test_action.py
@@ -19,6 +19,56 @@
 from lux.vis.Vis import Vis
 
 
+def test_temporal_action(global_var):
+    airbnb_df = pd.read_csv(
+        "https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/airbnb_nyc.csv"
+    )
+    flights_df = pd.read_csv(
+        "https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/flights.csv"
+    )
+    date_df = pd.DataFrame(
+        {
+            "date": [
+                "2019-01",
+                "2014-02",
+                "2020-03",
+                "2013-04",
+                "2012-05",
+                "2019-01",
+                "2020-02",
+                "2013-03",
+                "2020-04",
+                "2000-05",
+                "2000-01",
+                "2000-02",
+                "2004-12",
+                "2004-06",
+                "2020-05",
+                "2020-01",
+                "2020-02",
+                "2020-03",
+                "2020-04",
+                "2020-05",
+            ]
+        }
+    )
+    test_data = [airbnb_df, flights_df, date_df, pytest.car_df, pytest.olympic]
+    test_data_vis_count = [4, 4, 2, 1, 1]
+    for entry in zip(test_data, test_data_vis_count):
+        df, num_vis = entry[0], entry[1]
+        df._repr_html_()
+        assert ("Temporal" in df.recommendation, "Temporal visualizations should be generated.")
+        recommended = df.recommendation["Temporal"]
+        assert (len(recommended) == num_vis, "Incorrect number of temporal visualizations generated.")
+        temporal_col = [c for c in df.columns if df.data_type[c] == "temporal"]
+        overall_vis = [
+            vis.get_attr_by_channel("x")[0].attribute
+            for vis in recommended
+            if vis.score == 4 or vis.score == 5
+        ]
+        assert temporal_col.sort() == overall_vis.sort()
+
+
 def test_vary_filter_val(global_var):
     lux.config.set_executor_type("Pandas")
     df = pytest.olympic
@@ -202,6 +252,7 @@ def test_year_filter_value(global_var):
 
 
 def test_similarity(global_var):
+    lux.config.early_pruning = False
     df = pytest.car_df
     df["Year"] = pd.to_datetime(df["Year"], format="%Y")
     df.set_intent(
@@ -229,6 +280,7 @@ def test_similarity(global_var):
     )[0]
     assert japan_vis.score > europe_vis.score
     df.clear_intent()
+    lux.config.early_pruning = True
 
 
 def test_similarity2():
diff --git a/tests/test_config.py b/tests/test_config.py
index 34e2de83..701b4c6e 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -221,14 +221,18 @@ def change_color_make_transparent_add_title(chart):
 
 
 def test_sampling_flag_config():
-    df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/airbnb_nyc.csv")
-    df._ipython_display_()
-    assert df.recommendation["Correlation"][0].data.shape[0] == 30000
-    lux.config.sampling = False
-    df = df.copy()
-    df._ipython_display_()
-    assert df.recommendation["Correlation"][0].data.shape[0] == 48895
+    lux.config.heatmap = False
     lux.config.sampling = True
+    lux.config.early_pruning = False
+    import numpy as np
+
+    N = int(1.1 * lux.config.sampling_cap)
+    df = pd.DataFrame({"col1": np.random.rand(N), "col2": np.random.rand(N)})
+    df.maintain_recs()
+    assert len(df.recommendation["Correlation"][0].data) == lux.config.sampling_cap
+    lux.config.sampling = True
+    lux.config.heatmap = True
+    lux.config.early_pruning = True
 
 
 def test_sampling_parameters_config():
@@ -245,6 +249,7 @@ def test_sampling_parameters_config():
 
 
 def test_heatmap_flag_config():
+    lux.config.heatmap = True
     df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/airbnb_nyc.csv")
     df._ipython_display_()
     assert df.recommendation["Correlation"][0]._postbin
diff --git a/tests/test_export.py b/tests/test_export.py
new file mode 100644
index 00000000..0c135fa2
--- /dev/null
+++ b/tests/test_export.py
@@ -0,0 +1,106 @@
+#  Copyright 2019-2020 The Lux Authors.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from .context import lux
+import pytest
+import pandas as pd
+import numpy as np
+import psycopg2
+from lux.vis.Vis import Vis
+from lux.executor.PandasExecutor import PandasExecutor
+
+
+def test_scatter_code_export(global_var):
+    df = pytest.car_df
+
+    vis = Vis([lux.Clause("Horsepower"), lux.Clause("Acceleration")], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_color_scatter_code_export(global_var):
+    df = pytest.car_df
+
+    vis = Vis([lux.Clause("Horsepower"), lux.Clause("Acceleration"), lux.Clause("Origin")], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_histogram_code_export(global_var):
+    df = pytest.car_df
+
+    vis = Vis([lux.Clause("Horsepower")], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_barchart_code_export(global_var):
+    df = pytest.car_df
+
+    vis = Vis([lux.Clause("Origin")], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_color_barchart_code_export(global_var):
+    df = pytest.car_df
+
+    vis = Vis([lux.Clause("Origin"), lux.Clause("Cylinders")], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_heatmap_code_export(global_var):
+    df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/airbnb_nyc.csv")
+
+    vis = Vis(["price", "longitude"], df)
+    PandasExecutor.execute([vis], df)
+    code = vis.to_code("python")
+
+    try:
+        code = code.replace("'insert your LuxDataFrame variable here'", "df")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        assert False
diff --git a/tests/test_nan.py b/tests/test_nan.py
index 13ca88e9..0dac6499 100644
--- a/tests/test_nan.py
+++ b/tests/test_nan.py
@@ -116,12 +116,35 @@ def test_nan_series_occurence():
 
 
 def test_numeric_with_nan():
-    df = pd.read_html(
-        "https://archive.ics.uci.edu/ml/datasets.php?format=&task=&att=&area=&numAtt=&numIns=&type=&sort=nameUp&view=table"
-    )[5]
-    df.columns = df.loc[0]
-    df = df.loc[1:]
-    df["Year"] = pd.to_datetime(df["Year"], format="%Y")
+    # df = pd.read_html(
+    #     "http://web.archive.org/web/20200309053509/https://archive.ics.uci.edu/ml/datasets.php?format=&task=&att=&area=&numAtt=&numIns=&type=&sort=nameUp&view=table"
+    # )[7]
+    # df.columns = df.loc[0]
+    # df = df.loc[1:]
+    # df = df[['# Instances','# Attributes']]
+    # df = df.sample(15)
+    # df.to_dict(orient="records")
+    from numpy import nan
+
+    df = pd.DataFrame(
+        [
+            {"# Instances": nan, "# Attributes": nan},
+            {"# Instances": "989818", "# Attributes": nan},
+            {"# Instances": "303", "# Attributes": "75"},
+            {"# Instances": nan, "# Attributes": nan},
+            {"# Instances": nan, "# Attributes": nan},
+            {"# Instances": "745000", "# Attributes": "411"},
+            {"# Instances": "65554", "# Attributes": "29"},
+            {"# Instances": "640", "# Attributes": nan},
+            {"# Instances": "6435", "# Attributes": "36"},
+            {"# Instances": "270", "# Attributes": "13"},
+            {"# Instances": "182", "# Attributes": "13"},
+            {"# Instances": "22632", "# Attributes": "70"},
+            {"# Instances": "3960456", "# Attributes": "4"},
+            {"# Instances": "2500", "# Attributes": "10000"},
+            {"# Instances": "3850505", "# Attributes": "52"},
+        ]
+    )
     assert (
         df.data_type["# Instances"] == "quantitative"
     ), "Testing a numeric columns with NaN, check if type can be detected correctly"
@@ -141,3 +164,12 @@ def test_numeric_with_nan():
     #     len(a.recommendation["Distribution"]) == 2
     # ), "Example where dtype might be off after dropna(), check if histograms are still displayed"
     assert "" in a._message.to_html(), "No warning message for NaN should be displayed"
+
+
+def test_empty_filter(global_var):
+    df = pytest.car_df
+    df["Year"] = pd.to_datetime(df["Year"], format="%Y")
+    fdf = df[df["Origin"] == "U"]
+    fdf._ipython_display_()
+    assert fdf.data_type == None
+    assert len(df[df["Origin"] == "U"]) == 0
diff --git a/tests/test_pandas_coverage.py b/tests/test_pandas_coverage.py
index 69d60068..7e7cd8c4 100644
--- a/tests/test_pandas_coverage.py
+++ b/tests/test_pandas_coverage.py
@@ -405,7 +405,12 @@ def test_loc(global_var):
     assert len(new_df.cardinality) == 6
     new_df = df.loc[0:10, "Displacement":"Origin"]
     new_df._ipython_display_()
-    assert list(new_df.recommendation.keys()) == ["Correlation", "Distribution", "Occurrence"]
+    assert list(new_df.recommendation.keys()) == [
+        "Correlation",
+        "Distribution",
+        "Occurrence",
+        "Temporal",
+    ]
     assert len(new_df.cardinality) == 6
     new_df = df.loc[0:10, "Displacement":"Horsepower"]
     new_df._ipython_display_()
@@ -434,7 +439,12 @@ def test_iloc(global_var):
     assert len(new_df.cardinality) == 6
     new_df = df.iloc[0:11, 3:9]
     new_df._ipython_display_()
-    assert list(new_df.recommendation.keys()) == ["Correlation", "Distribution", "Occurrence"]
+    assert list(new_df.recommendation.keys()) == [
+        "Correlation",
+        "Distribution",
+        "Occurrence",
+        "Temporal",
+    ]
     assert len(new_df.cardinality) == 6
     new_df = df.iloc[0:11, 3:5]
     new_df._ipython_display_()
@@ -503,7 +513,7 @@ def compare_vis(vis1, vis2):
     assert len(vis1._inferred_intent) == len(vis2._inferred_intent)
     for j in range(len(vis1._inferred_intent)):
         compare_clauses(vis1._inferred_intent[j], vis2._inferred_intent[j])
-    assert vis1._source.equals(vis2._source)
+    compare_df(vis1._source, vis2._source)
     assert vis1._code == vis2._code
     assert vis1._mark == vis2._mark
     assert vis1._min_max == vis2._min_max
@@ -511,6 +521,11 @@ def compare_vis(vis1, vis2):
     assert vis1.score == vis2.score
 
 
+def compare_df(df1, df2):
+    if df1 is not None and df2 is not None:
+        assert df1.head().to_dict() == df2.head().to_dict()
+
+
 def test_index(global_var):
     # testing set_index and reset_index functions
     # setting a column as an index should remove it from the dataframe's column list
diff --git a/tests/test_performance.py b/tests/test_performance.py
index fca6a1c6..c268d69b 100644
--- a/tests/test_performance.py
+++ b/tests/test_performance.py
@@ -20,24 +20,43 @@
 
 # To run the script and see the printed result, run:
 # python -m pytest -s tests/test_performance.py
-def test_q1_performance_census(global_var):
+def test_lazy_maintain_performance_census(global_var):
+    lux.config.lazy_maintain = True
     df = pd.read_csv("https://github.com/lux-org/lux-datasets/blob/master/data/census.csv?raw=true")
     tic = time.perf_counter()
-    df._ipython_display_()
+    df.maintain_recs()
     toc = time.perf_counter()
     delta = toc - tic
-    df._ipython_display_()
+    df.maintain_recs()
     toc2 = time.perf_counter()
     delta2 = toc2 - toc
     print(f"1st display Performance: {delta:0.4f} seconds")
     print(f"2nd display Performance: {delta2:0.4f} seconds")
     assert (
-        delta < 4.5
+        delta < 4
     ), "The recommendations on Census dataset took a total of {delta:0.4f} seconds, longer than expected."
     assert (
-        delta2 < 0.15 < delta
+        delta2 < 0.1 < delta
     ), "Subsequent display of recommendations on Census dataset took a total of {delta2:0.4f} seconds, longer than expected."
 
+    lux.config.lazy_maintain = False
+    tic = time.perf_counter()
+    df.maintain_recs()
+    toc = time.perf_counter()
+    delta = toc - tic
+    df.maintain_recs()
+    toc2 = time.perf_counter()
+    delta2 = toc2 - toc
+    print(f"1st display Performance: {delta:0.4f} seconds")
+    print(f"2nd display Performance: {delta2:0.4f} seconds")
+
+    assert (
+        delta > 2
+    ), "The recompute of recommendations on Census dataset took a total of {delta:0.4f} seconds, shorter than expected."
+    assert (
+        delta2 > 2
+    ), "Subsequent recompute of recommendations on Census dataset took a total of {delta2:0.4f} seconds, shorter than expected."
+
     assert df.data_type == {
         "age": "quantitative",
         "workclass": "nominal",
@@ -55,3 +74,27 @@ def test_q1_performance_census(global_var):
         "native-country": "nominal",
         "income": "nominal",
     }
+
+
+def test_early_prune_performance_spotify():
+    df = pd.read_csv("https://github.com/lux-org/lux-datasets/blob/master/data/spotify.csv?raw=True")
+    df.maintain_metadata()
+    # With Early Pruning
+    lux.config.early_pruning = True
+    df.clear_intent()
+    start = time.time()
+    df.maintain_recs()
+    end = time.time()
+    with_prune_time = end - start
+    assert "Large search space detected" in df._message.to_html()
+    # Without Early Pruning
+    lux.config.early_pruning = False
+    df.clear_intent()
+    start = time.time()
+    df.maintain_recs()
+    end = time.time()
+    without_prune_time = end - start
+    assert "Large search space detected" not in df._message.to_html()
+    assert (
+        without_prune_time > with_prune_time
+    ), "Early pruning should speed up Spotify dataset recommendations"
diff --git a/tests/test_series.py b/tests/test_series.py
index b19136e1..681f6386 100644
--- a/tests/test_series.py
+++ b/tests/test_series.py
@@ -75,6 +75,17 @@ def test_series_recommendation():
     assert len(df.recommendation["Distribution"]) > 0, "Recommendation property empty for LuxSeries"
 
 
+def test_series_multivis_recommendation():
+    covid = pd.read_csv(
+        "https://github.com/lux-org/lux-datasets/blob/master/data/covid-stringency.csv?raw=True"
+    )
+    covid = covid.rename(columns={"stringency_index": "stringency"})
+    covid["Day"] = pd.to_datetime(covid["Day"], format="%Y-%m-%d")
+    series = covid["Day"]
+    assert len(series.recommendation["Temporal"]) == 4, "Display 4 temporal vis based on `Day`"
+    assert hasattr(series, "current_vis") == False
+
+
 def test_unnamed_column():
     lux.config.plotting_backend = "matplotlib"
     df = pd.read_csv("https://raw.githubusercontent.com/lux-org/lux-datasets/master/data/employee.csv")
diff --git a/tests_sql/test_sql_compiler.py b/tests_sql/test_sql_compiler.py
index 56d4d0f5..4f090b65 100644
--- a/tests_sql/test_sql_compiler.py
+++ b/tests_sql/test_sql_compiler.py
@@ -38,6 +38,7 @@ def test_underspecified_no_vis(global_var, test_recs):
 
 def test_underspecified_single_vis(global_var, test_recs):
     one_vis_actions = ["Enhance", "Filter", "Generalize"]
+    lux.config.heatmap = False
     sql_df = lux.LuxSQLTable(table_name="cars")
     sql_df.set_intent([lux.Clause(attribute="milespergal"), lux.Clause(attribute="weight")])
     test_recs(sql_df, one_vis_actions)
diff --git a/tests_sql/test_sql_export.py b/tests_sql/test_sql_export.py
new file mode 100644
index 00000000..affadde6
--- /dev/null
+++ b/tests_sql/test_sql_export.py
@@ -0,0 +1,112 @@
+#  Copyright 2019-2020 The Lux Authors.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from .context import lux
+import pytest
+import pandas as pd
+import numpy as np
+import psycopg2
+from lux.vis.Vis import Vis
+from lux.executor.SQLExecutor import SQLExecutor
+
+
+def test_scatter_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("cars")
+
+    vis = Vis([lux.Clause("horsepower"), lux.Clause("acceleration")], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_color_scatter_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("cars")
+
+    vis = Vis([lux.Clause("horsepower"), lux.Clause("acceleration"), lux.Clause("origin")], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_histogram_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("cars")
+
+    vis = Vis([lux.Clause("horsepower")], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_barchart_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("cars")
+
+    vis = Vis([lux.Clause("origin")], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_color_barchart_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("cars")
+
+    vis = Vis([lux.Clause("origin"), lux.Clause("cylinders")], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        print("failed to run Vis code")
+
+
+def test_heatmap_code_export():
+    tbl = lux.LuxSQLTable()
+    tbl.set_SQL_table("airbnb_nyc")
+
+    vis = Vis(["price", "longitude"], tbl)
+    SQLExecutor.execute([vis], tbl)
+    code = vis.to_code("python")
+
+    try:
+        code = code.replace("'insert your LuxSQLTable variable here'", "tbl")
+        code = code.replace("'insert the name of your Vis object here'", "vis")
+        exec(code)
+    except:
+        assert False