diff --git a/OSX/Metabase/Backend/MetabaseTask.m b/OSX/Metabase/Backend/MetabaseTask.m
index 9fa21608b078e5f94ebe64c36b4ada41e6755ca3..078d848e31723930d2cfa518950e347e07d638e6 100644
--- a/OSX/Metabase/Backend/MetabaseTask.m
+++ b/OSX/Metabase/Backend/MetabaseTask.m
@@ -65,7 +65,8 @@
 		self.task.launchPath	= JREPath();
 		self.task.environment	= @{@"MB_DB_FILE": DBPath(),
 									@"MB_PLUGINS_DIR": PluginsDirPath(),
-									@"MB_JETTY_PORT": @(self.port)};
+									@"MB_JETTY_PORT": @(self.port),
+									@"MB_CLIENT": @"OSX"};
 		self.task.arguments		= @[@"-Djava.awt.headless=true", // this prevents the extra java icon from popping up in the dock when running
                                     @"-client",                  // make sure we're running in -client mode, which has a faster lanuch time
                                     @"-Xverify:none",            // disable bytecode verification for faster launch speed, not really needed here since JAR is packaged as part of signed .app
diff --git a/README.md b/README.md
index 95c11f35d0ab933f095d0085367851d5b7f7a1b0..5928a072353ba2c469e5aa2e4406f71779dc1960 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Metabase is the easy, open source way for everyone in your company to ask questi
 - Let anyone on your team [ask questions](http://www.metabase.com/docs/latest/users-guide/03-asking-questions) without knowing SQL
 - Rich beautiful [dashboards](http://www.metabase.com/docs/latest/users-guide/05-sharing-answers) with auto refresh and fullscreen
 - SQL Mode for analysts and data pros
-- Create canonical [segments and metrics](http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics) for your team to use
+- Create canonical [segments and metrics](http://www.metabase.com/docs/latest/administration-guide/06-segments-and-metrics) for your team to use
 - Send data to Slack or email on a schedule with [Pulses](http://www.metabase.com/docs/latest/users-guide/09-pulses)
 - View data in Slack anytime with [Metabot](http://www.metabase.com/docs/latest/users-guide/10-metabot)
 - [Humanize data](http://www.metabase.com/docs/latest/administration-guide/03-metadata-editing) for your team by renaming, annotating and hiding fields
diff --git a/bin/ci b/bin/ci
index ecf061bf82562b11ac4de9e46993cf2450c4f613..47825dc2f50bfa615264607cd45a42d9e43dc630 100755
--- a/bin/ci
+++ b/bin/ci
@@ -48,13 +48,13 @@ node-6() {
     if is_enabled "jar" || is_enabled "e2e" || is_enabled "screenshots"; then
         run_step ./bin/build version frontend sample-dataset uberjar
     fi
-    if is_enabled "e2e" || is_enabled "compare_screenshots"; then
-        USE_SAUCE=true \
-            run_step yarn run test-e2e
-    fi
-    if is_enabled "screenshots"; then
-        run_step node_modules/.bin/babel-node ./bin/compare-screenshots
-    fi
+    # if is_enabled "e2e" || is_enabled "compare_screenshots"; then
+    #     USE_SAUCE=true \
+    #         run_step yarn run test-e2e
+    # fi
+    # if is_enabled "screenshots"; then
+    #     run_step node_modules/.bin/babel-node ./bin/compare-screenshots
+    # fi
 }
 
 
diff --git a/docs/administration-guide/01-managing-databases.md b/docs/administration-guide/01-managing-databases.md
index 2458a780f520e7c4868dfbad228ef7d5617042ed..b25b3cc4739e5c0a5b2f53a39b39681541ddbc16 100644
--- a/docs/administration-guide/01-managing-databases.md
+++ b/docs/administration-guide/01-managing-databases.md
@@ -18,7 +18,7 @@ Now you’ll see a list of your databases. To connect another database to Metaba
 * Postgres
 * SQLite
 * SQL Server
-* Driud
+* Druid
 * Crate
 * [Oracle](databases/oracle.md)
 * [Vertica](databases/vertica.md)
diff --git a/frontend/src/metabase/components/AccordianList.jsx b/frontend/src/metabase/components/AccordianList.jsx
index 7f4d5b90890244dc54b8844ac5c161e23b885dd4..80959e32caf11ad13d009c49d2cce63a46347bee 100644
--- a/frontend/src/metabase/components/AccordianList.jsx
+++ b/frontend/src/metabase/components/AccordianList.jsx
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
 
 import cx from "classnames";
 import _ from "underscore";
+import { elementIsInView } from "metabase/lib/dom";
 
 import Icon from "metabase/components/Icon.jsx";
 import ListSearchField from "metabase/components/ListSearchField.jsx";
@@ -64,7 +65,7 @@ export default class AccordianList extends Component {
     componentDidMount() {
         // when the component is mounted and an item is selected then scroll to it
         const element = this.refs.selected && ReactDOM.findDOMNode(this.refs.selected);
-        if (element) {
+        if (element && !elementIsInView(element)) {
             element.scrollIntoView();
         }
     }
diff --git a/frontend/src/metabase/components/Calendar.css b/frontend/src/metabase/components/Calendar.css
index 80e0d97dac1d7b9fd6d445f75604327882f9d4d9..9c94a6899fe54cfecc691506efcd514af2786dbb 100644
--- a/frontend/src/metabase/components/Calendar.css
+++ b/frontend/src/metabase/components/Calendar.css
@@ -126,3 +126,13 @@
   position: absolute;
   right: -12px;
 }
+
+.Calendar--noContext .Calendar-day {
+  visibility: hidden;
+  pointer-events: none;
+}
+
+.Calendar--noContext .Calendar-day--this-month {
+  visibility: visible;
+  pointer-events: all;
+}
diff --git a/frontend/src/metabase/components/Calendar.jsx b/frontend/src/metabase/components/Calendar.jsx
index a54e57f4ab6217733b8433fb799bbd5d735b2970..d3b16c32f139ff32148120ca6c12c32ab8d95f2b 100644
--- a/frontend/src/metabase/components/Calendar.jsx
+++ b/frontend/src/metabase/components/Calendar.jsx
@@ -3,34 +3,34 @@ import React, { Component, PropTypes } from 'react';
 import "./Calendar.css";
 
 import cx from 'classnames';
-import moment from "moment";
+import moment from 'moment';
 
-import Icon from 'metabase/components/Icon.jsx';
-import Tooltip from 'metabase/components/Tooltip.jsx';
-
-const MODES = ['month', 'year', 'decade'];
+import Icon from 'metabase/components/Icon';
 
 export default class Calendar extends Component {
-    constructor(props, context) {
-        super(props, context);
+    constructor(props) {
+        super(props);
 
         this.state = {
-            current: moment(props.initial || undefined),
-            currentMode: MODES[0]
+            current: moment(props.initial || undefined)
         };
 
         this.previous = this.previous.bind(this);
         this.next = this.next.bind(this);
-        this.cycleMode = this.cycleMode.bind(this);
         this.onClickDay = this.onClickDay.bind(this);
     }
 
     static propTypes = {
         selected: PropTypes.object,
-        selectedEnd: PropTypes.object,
         onChange: PropTypes.func.isRequired,
         onAfterClick: PropTypes.func,
         onBeforeClick: PropTypes.func,
+        isRangePicker: PropTypes.bool,
+        isDual: PropTypes.bool,
+    };
+
+    static defaultProps = {
+        isRangePicker: true
     };
 
     componentWillReceiveProps(nextProps) {
@@ -50,8 +50,8 @@ export default class Calendar extends Component {
     }
 
     onClickDay(date, e) {
-        let { selected, selectedEnd } = this.props;
-        if (!selected || selectedEnd) {
+        let { selected, selectedEnd, isRangePicker } = this.props;
+        if (!isRangePicker || !selected || selectedEnd) {
             this.props.onChange(date.format("YYYY-MM-DD"), null);
         } else if (!selectedEnd) {
             if (date.isAfter(selected)) {
@@ -62,15 +62,6 @@ export default class Calendar extends Component {
         }
     }
 
-    cycleMode() {
-        // let i = this.currentMode
-        // console.log('mode cycle y\'all')
-        // let i = ++i%this.state.modes.length;
-        // this.setState({
-        //     mode: this.modes[i]
-        // })
-    }
-
     previous() {
         let month = this.state.current;
         month.add(-1, "M");
@@ -83,18 +74,24 @@ export default class Calendar extends Component {
         this.setState({ month: month });
     }
 
-    renderMonthHeader() {
+    renderMonthHeader(current, side) {
         return (
             <div className="Calendar-header flex align-center">
-                <div className="bordered rounded p1 cursor-pointer transition-border border-hover px1" onClick={this.previous}>
-                    <Icon name="chevronleft" size={10} />
-                </div>
+                { side !=="right" &&
+                    <div className="bordered rounded p1 cursor-pointer transition-border border-hover px1" onClick={this.previous}>
+                        <Icon name="chevronleft" size={10} />
+                    </div>
+                }
                 <span className="flex-full" />
-                <h4 className="bordered border-hover cursor-pointer rounded p1" onClick={this.cycleMode}>{this.state.current.format("MMMM YYYY")}</h4>
+                <h4 className="cursor-pointer rounded p1">
+                    {current.format("MMMM YYYY")}
+                </h4>
                 <span className="flex-full" />
-                <div className="bordered border-hover rounded p1 transition-border cursor-pointer px1" onClick={this.next}>
-                    <Icon name="chevronright" size={10} />
-                </div>
+                { side !=="left" &&
+                    <div className="bordered border-hover rounded p1 transition-border cursor-pointer px1" onClick={this.next}>
+                        <Icon name="chevronright" size={10} />
+                    </div>
+                }
             </div>
         )
     }
@@ -108,10 +105,10 @@ export default class Calendar extends Component {
         );
     }
 
-    renderWeeks() {
+    renderWeeks(current) {
         var weeks = [],
             done = false,
-            date = moment(this.state.current).startOf("month").add("w" -1).day("Sunday"),
+            date = moment(current).startOf("month").add("w" -1).day("Sunday"),
             monthIndex = date.month(),
             count = 0;
 
@@ -120,7 +117,7 @@ export default class Calendar extends Component {
                 <Week
                     key={date.toString()}
                     date={moment(date)}
-                    month={this.state.current}
+                    month={current}
                     onClickDay={this.onClickDay}
                     selected={this.props.selected}
                     selectedEnd={this.props.selectedEnd}
@@ -132,36 +129,38 @@ export default class Calendar extends Component {
         }
 
         return (
-            <div className="relative">
-                <div className="Calendar-weeks">
-                    {weeks}
-                </div>
-                {this.props.onBeforeClick &&
-                    <div className="absolute top left z2" style={{marginTop: "-12px", marginLeft: "-12px"}}>
-                        <Tooltip tooltip={"Everything before " + this.props.selected.format("MMMM Do, YYYY")}>
-                            <a className="circle-button cursor-pointer" onClick={this.props.onBeforeClick}>«</a>
-                        </Tooltip>
-                    </div>
-                }
-                {this.props.onAfterClick &&
-                    <div className="absolute bottom right z2" style={{marginBottom: "-12px", marginRight: "-12px"}}>
-                        <Tooltip tooltip={"Everything after " + this.props.selected.format("MMMM Do, YYYY")}>
-                            <a className="circle-button cursor-pointer" onClick={this.props.onAfterClick}>»</a>
-                        </Tooltip>
-                    </div>
-                }
+            <div className="Calendar-weeks relative">
+                {weeks}
             </div>
         );
     }
-    render() {
+
+    renderCalender(current, side) {
         return (
-            <div className={cx("Calendar", { "Calendar--range": this.props.selected && this.props.selectedEnd })}>
-                {this.renderMonthHeader()}
-                {this.renderDayNames()}
-                {this.renderWeeks()}
+            <div className={
+                cx("Calendar", { "Calendar--range": this.props.selected && this.props.selectedEnd })}>
+                {this.renderMonthHeader(current, side)}
+                {this.renderDayNames(current)}
+                {this.renderWeeks(current)}
             </div>
         );
     }
+
+    render() {
+        const { current } = this.state;
+        if (this.props.isDual) {
+            return (
+                <div className="flex">
+                    <div className="mr3">
+                        {this.renderCalender(current, "left")}
+                    </div>
+                    {this.renderCalender(moment(current).add(1, "month"), "right")}
+                </div>
+            )
+        } else {
+            return this.renderCalender(current);
+        }
+    }
 }
 
 class Week extends Component {
@@ -171,10 +170,6 @@ class Week extends Component {
         onClickDay: PropTypes.func.isRequired
     }
 
-    _dayIsSelected(day) {
-        return
-    }
-
     render() {
         let days = [];
         let { date, month, selected, selectedEnd } = this.props;
diff --git a/frontend/src/metabase/components/ExpandingContent.jsx b/frontend/src/metabase/components/ExpandingContent.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..205d5d27032c0cdd9d6f95a594e8964ca68b0128
--- /dev/null
+++ b/frontend/src/metabase/components/ExpandingContent.jsx
@@ -0,0 +1,24 @@
+import React, { Component } from "react";
+
+class ExpandingContent extends Component {
+    constructor () {
+        super();
+        this.state = { open: false };
+    }
+    render () {
+        const { children, open } = this.props;
+        return (
+            <div
+                style={{
+                    maxHeight: open ? 'none' : 0,
+                    overflow: 'hidden',
+                    transition: 'max-height 0.3s ease'
+                }}
+            >
+                { children }
+            </div>
+        );
+    }
+}
+
+export default ExpandingContent;
diff --git a/frontend/src/metabase/components/Popover.jsx b/frontend/src/metabase/components/Popover.jsx
index 35d705f02d4b6042215886a468d1e28d49e37dd1..fd31dbff49690b021147c959680ca1014c596ba8 100644
--- a/frontend/src/metabase/components/Popover.jsx
+++ b/frontend/src/metabase/components/Popover.jsx
@@ -2,7 +2,7 @@ import React, { Component, PropTypes } from "react";
 import ReactDOM from "react-dom";
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
-import OnClickOutsideWrapper from "./OnClickOutsideWrapper.jsx";
+import OnClickOutsideWrapper from "./OnClickOutsideWrapper";
 import Tether from "tether";
 
 import cx from "classnames";
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index 609209a7b821411e7a40815677aecd5fec59e3cc..ff2fb912a348bb93a2555bfe3c801af9c1fead18 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -190,6 +190,10 @@ export var ICON_PATHS = {
         svg: '<g fill="currentcolor" fill-rule="evenodd"><path d="M10.9665007,4.7224988 C11.5372866,3.77118898 12.455761,3.75960159 13.0334993,4.7224988 L22.9665007,21.2775012 C23.5372866,22.228811 23.1029738,23 21.9950534,23 L2.00494659,23 C0.897645164,23 0.455760956,22.2403984 1.03349928,21.2775012 L10.9665007,4.7224988 Z M13.0184348,11.258 L13.0184348,14.69 C13.0184348,15.0580018 12.996435,15.4229982 12.9524348,15.785 C12.9084346,16.1470018 12.8504351,16.5159981 12.7784348,16.892 L11.5184348,16.892 C11.4464344,16.5159981 11.388435,16.1470018 11.3444348,15.785 C11.3004346,15.4229982 11.2784348,15.0580018 11.2784348,14.69 L11.2784348,11.258 L13.0184348,11.258 Z M11.0744348,19.058 C11.0744348,18.9139993 11.1014345,18.7800006 11.1554348,18.656 C11.2094351,18.5319994 11.2834343,18.4240005 11.3774348,18.332 C11.4714353,18.2399995 11.5824341,18.1670003 11.7104348,18.113 C11.8384354,18.0589997 11.978434,18.032 12.1304348,18.032 C12.2784355,18.032 12.4164341,18.0589997 12.5444348,18.113 C12.6724354,18.1670003 12.7844343,18.2399995 12.8804348,18.332 C12.9764353,18.4240005 13.0514345,18.5319994 13.1054348,18.656 C13.1594351,18.7800006 13.1864348,18.9139993 13.1864348,19.058 C13.1864348,19.2020007 13.1594351,19.3369994 13.1054348,19.463 C13.0514345,19.5890006 12.9764353,19.6979995 12.8804348,19.79 C12.7844343,19.8820005 12.6724354,19.9539997 12.5444348,20.006 C12.4164341,20.0580003 12.2784355,20.084 12.1304348,20.084 C11.978434,20.084 11.8384354,20.0580003 11.7104348,20.006 C11.5824341,19.9539997 11.4714353,19.8820005 11.3774348,19.79 C11.2834343,19.6979995 11.2094351,19.5890006 11.1554348,19.463 C11.1014345,19.3369994 11.0744348,19.2020007 11.0744348,19.058 Z"></path></g>',
         attrs: { viewBox: '0 0 23 23' }
     },
+    warning2: {
+        path: 'M12.3069589,4.52260192 C14.3462632,1.2440969 17.653446,1.24541073 19.691933,4.52260192 L31.2249413,23.0637415 C33.2642456,26.3422466 31.7889628,29 27.9115531,29 L4.08733885,29 C0.218100769,29 -1.26453645,26.3409327 0.77395061,23.0637415 L12.3069589,4.52260192 Z M18.0499318,23.0163223 C18.0499772,23.0222378 18.05,23.0281606 18.05,23.0340907 C18.05,23.3266209 17.9947172,23.6030345 17.8840476,23.8612637 C17.7737568,24.1186089 17.6195847,24.3426723 17.4224081,24.5316332 C17.2266259,24.7192578 16.998292,24.8660439 16.7389806,24.9713892 C16.4782454,25.0773129 16.1979962,25.1301134 15.9,25.1301134 C15.5950083,25.1301134 15.3111795,25.0774024 15.0502239,24.9713892 C14.7901813,24.8657469 14.5629613,24.7183609 14.3703047,24.5298034 C14.177545,24.3411449 14.0258626,24.1177208 13.9159524,23.8612637 C13.8052827,23.6030345 13.75,23.3266209 13.75,23.0340907 C13.75,22.7411889 13.8054281,22.4661013 13.9165299,22.2109786 C14.0264627,21.9585404 14.1779817,21.7374046 14.3703047,21.5491736 C14.5621821,21.3613786 14.7883231,21.2126553 15.047143,21.1034656 C15.3089445,20.9930181 15.593871,20.938068 15.9,20.938068 C16.1991423,20.938068 16.4804862,20.9931136 16.7420615,21.1034656 C17.0001525,21.2123478 17.2274115,21.360472 17.4224081,21.5473437 C17.6191428,21.7358811 17.7731504,21.957652 17.88347,22.2109786 C17.9124619,22.2775526 17.9376628,22.3454862 17.9590769,22.414741 C18.0181943,22.5998533 18.05,22.7963729 18.05,23 C18.05,23.0054459 18.0499772,23.0108867 18.0499318,23.0163223 L18.0499318,23.0163223 Z M17.7477272,14.1749999 L17.7477272,8.75 L14.1170454,8.75 L14.1170454,14.1749999 C14.1170454,14.8471841 14.1572355,15.5139742 14.2376219,16.1753351 C14.3174838,16.8323805 14.4227217,17.5019113 14.5533248,18.1839498 L14.5921937,18.3869317 L17.272579,18.3869317 L17.3114479,18.1839498 C17.442051,17.5019113 17.5472889,16.8323805 17.6271507,16.1753351 C17.7075371,15.5139742 17.7477272,14.8471841 17.7477272,14.1749999 Z',
+        attrs: { fillRule: "evenodd" }
+    },
     "illustration-icon-pie": {
         svg: "<path d='M29.8065455,22.2351515 L15.7837576,15.9495758 L15.7837576,31.2174545 C22.0004848,31.2029091 27.3444848,27.5258182 29.8065455,22.2351515' fill='#78B5EC'></path><g id='Fill-1-+-Fill-3'><path d='M29.8065455,22.2351515 C30.7316364,20.2482424 31.2630303,18.0402424 31.2630303,15.7032727 C31.2630303,11.8138182 29.8220606,8.26763636 27.4569697,5.54472727 L15.7837576,15.9495758 L29.8065455,22.2351515' fill='#3875AC'></path><path d='M27.4569697,5.54472727 C24.6118788,2.26909091 20.4266667,0.188121212 15.7478788,0.188121212 C7.17963636,0.188121212 0.232727273,7.1350303 0.232727273,15.7032727 C0.232727273,24.2724848 7.17963636,31.2184242 15.7478788,31.2184242 C15.7604848,31.2184242 15.7721212,31.2174545 15.7837576,31.2174545 L15.7837576,15.9495758 L27.4569697,5.54472727' fill='#4C9DE6'></path></g>"
     },
diff --git a/frontend/src/metabase/lib/dom.js b/frontend/src/metabase/lib/dom.js
index 5bd4c6e644fd0a334dab9e145d84da47bd0487c3..71b6bb86aed5f7c806b978efe94210ce18599cb0 100644
--- a/frontend/src/metabase/lib/dom.js
+++ b/frontend/src/metabase/lib/dom.js
@@ -33,3 +33,24 @@ export function findPosition(element, excludeScroll = false) {
     }
     return offset;
 }
+
+// based on http://stackoverflow.com/a/38039019/113
+export function elementIsInView(element, percentX = 1, percentY = 1) {
+    const tolerance = 0.01;   //needed because the rects returned by getBoundingClientRect provide the position up to 10 decimals
+
+    const elementRect = element.getBoundingClientRect();
+    const parentRects = [];
+
+    while (element.parentElement != null) {
+        parentRects.push(element.parentElement.getBoundingClientRect());
+        element = element.parentElement;
+    }
+
+    return parentRects.every((parentRect) => {
+        const visiblePixelX = Math.min(elementRect.right, parentRect.right) - Math.max(elementRect.left, parentRect.left);
+        const visiblePixelY = Math.min(elementRect.bottom, parentRect.bottom) - Math.max(elementRect.top, parentRect.top);
+        const visiblePercentageX = visiblePixelX / elementRect.width;
+        const visiblePercentageY = visiblePixelY / elementRect.height;
+        return visiblePercentageX + tolerance > percentX && visiblePercentageY + tolerance > percentY;
+    });
+}
diff --git a/frontend/src/metabase/lib/query_time.js b/frontend/src/metabase/lib/query_time.js
index 8da10e91f6a0dce8fca47675337649482b5966db..6cca2dc555127cb4f9d40477379cfb86f628cbd0 100644
--- a/frontend/src/metabase/lib/query_time.js
+++ b/frontend/src/metabase/lib/query_time.js
@@ -76,13 +76,13 @@ export function generateTimeIntervalDescription(n, unit) {
         switch (n) {
             case "current":
             case 0:
-                return "Today";
+                return ["Today"];
             case "next":
             case 1:
-                return "Tomorrow";
+                return ["Tomorrow"];
             case "last":
             case -1:
-                return "Yesterday";
+                return ["Yesterday"];
         }
     }
 
@@ -107,7 +107,12 @@ export function generateTimeIntervalDescription(n, unit) {
 
 export function generateTimeValueDescription(value, bucketing) {
     if (typeof value === "string") {
-        return moment(value).format("MMMM D, YYYY");
+        let m = moment(value);
+        if(m.hours() || m.minutes()) {
+            return m.format("MMMM D, YYYY hh:mm a");
+        } else {
+            return m.format("MMMM D, YYYY");
+        }
     } else if (Array.isArray(value) && value[0] === "relative_datetime") {
         let n = value[1];
         let unit = value[2];
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index 51a3773ddd07d7ab919fbf4de8ffc6670dffe660..1ad0c4ca0f84dc6d913d5251f1dcc4bc27fa7e28 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -179,6 +179,7 @@ export default class GuiQueryEditor extends Component {
                         triggerElement={addFilterButton}
                         triggerClasses="flex align-center"
                         getTarget={() => this.refs.addFilterTarget}
+                        horizontalAttachments={["left"]}
                     >
                         <FilterPopover
                             isNew={true}
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index adfac0aad64e20422a2e238ec4767f43ad47ec60..f0a2976ba6654ca848a67057f4639e7deb95923e 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -8,13 +8,16 @@ import VisualizationSettings from './VisualizationSettings.jsx';
 
 import VisualizationError from "./VisualizationError.jsx";
 import VisualizationResult from "./VisualizationResult.jsx";
+
+import Warnings from "./Warnings.jsx";
 import DownloadWidget from "./DownloadWidget.jsx";
 
+import { formatNumber, inflect } from "metabase/lib/formatting";
+import Utils from "metabase/lib/utils";
+
 import cx from "classnames";
 import _ from "underscore";
 
-const isEqualsDeep = (a, b) => JSON.stringify(a) === JSON.stringify(b);
-
 export default class QueryVisualization extends Component {
     constructor(props, context) {
         super(props, context);
@@ -64,8 +67,8 @@ export default class QueryVisualization extends Component {
     queryIsDirty() {
         // a query is considered dirty if ANY part of it has been changed
         return (
-            !isEqualsDeep(this.props.card.dataset_query, this.state.lastRunDatasetQuery) ||
-            !isEqualsDeep(this.props.parameterValues, this.state.lastRunParameterValues)
+            !Utils.equals(this.props.card.dataset_query, this.state.lastRunDatasetQuery) ||
+            !Utils.equals(this.props.parameterValues, this.state.lastRunParameterValues)
         );
     }
 
@@ -93,8 +96,11 @@ export default class QueryVisualization extends Component {
                         cancelFn={this.props.cancelQueryFn}
                     />
                 </div>
-                <div className="absolute right z4 flex align-center">
+                <div className="absolute right z4 flex align-center" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
                     { !this.queryIsDirty() && this.renderCount() }
+                    { !isObjectDetail &&
+                        <Warnings warnings={this.state.warnings} className="mx2" size={18} />
+                    }
                     { !this.queryIsDirty() && result && !result.error ?
                         <DownloadWidget
                             className="mx1"
@@ -108,29 +114,14 @@ export default class QueryVisualization extends Component {
         );
     }
 
-    hasTooManyRows() {
-        const dataset_query = this.props.card.dataset_query,
-              rows = this.props.result.data.rows;
-
-        if (this.props.result.data.rows_truncated ||
-            (dataset_query.type === "query" &&
-             dataset_query.query.aggregation[0] === "rows" &&
-             rows.length === 2000))
-        {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
     renderCount() {
         let { result, isObjectDetail, card } = this.props;
-        if (result &&  result.data && !isObjectDetail && card.display === "table") {
+        if (result && result.data && !isObjectDetail && card.display === "table") {
             return (
                 <div>
-                    { this.hasTooManyRows() ? ("Showing max of ") : ("Showing ")}
-                    <b>{result.row_count}</b>
-                    { (result.data.rows.length !== 1) ? (" rows") : (" row")}.
+                    { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
+                    <b>{formatNumber(result.row_count)}</b>
+                    { " " + inflect("row", result.data.rows.length) }.
                 </div>
             );
         }
@@ -149,7 +140,14 @@ export default class QueryVisualization extends Component {
             if (error) {
                 viz = <VisualizationError error={error} card={card} duration={result.duration} />
             } else if (result.data) {
-                viz = <VisualizationResult lastRunDatasetQuery={this.state.lastRunDatasetQuery} onOpenChartSettings={() => this.refs.settings.open()} {...this.props}/>
+                viz = (
+                    <VisualizationResult
+                        lastRunDatasetQuery={this.state.lastRunDatasetQuery}
+                        onUpdateWarnings={(warnings) => this.setState({ warnings })}
+                        onOpenChartSettings={() => this.refs.settings.open()}
+                        {...this.props}
+                    />
+                );
             }
         }
 
diff --git a/frontend/src/metabase/query_builder/components/Warnings.jsx b/frontend/src/metabase/query_builder/components/Warnings.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..97d41970bf28ec0d1d5ca54d787d57a270a1341f
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/Warnings.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+import Tooltip from "metabase/components/Tooltip.jsx";
+import Icon from "metabase/components/Icon.jsx";
+
+const Warnings = ({ warnings, className, size = 16 }) => {
+    if (!warnings || warnings.length === 0) {
+        return null;
+    }
+    const tooltip = (
+        <ul className="px2 pt2 pb1" style={{ maxWidth: 350 }}>
+            {warnings.map((warning) =>
+                <li className="pb1" key={warning}>
+                    {warning}
+                </li>
+            )}
+        </ul>
+    );
+
+    return (
+        <Tooltip tooltip={tooltip}>
+            <Icon className={className} name="warning2" size={size} />
+        </Tooltip>
+    )
+}
+
+
+export default Warnings;
diff --git a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..59b403a30530a3ed6744ea173ab7346f2e012336
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
@@ -0,0 +1,70 @@
+import React, { Component, PropTypes } from "react";
+import ReactCSSTransitionGroup from "react-addons-css-transition-group";
+import cx from "classnames";
+import { titleCase } from "humanize-plus";
+
+import Icon from "metabase/components/Icon";
+
+export default class DateOperatorSelector extends Component {
+    constructor() {
+        super();
+        this.state = { expanded: false };
+
+        this.toggleExpanded = this.toggleExpanded.bind(this);
+    }
+
+    static propTypes = {
+        operator: PropTypes.string,
+        operators: PropTypes.array.isRequired,
+        onOperatorChange: PropTypes.func.isRequired
+    };
+
+    toggleExpanded () {
+        this.setState({ expanded: !this.state.expanded });
+    }
+
+    render() {
+        const { operator, operators, onOperatorChange } = this.props;
+        const { expanded } = this.state;
+
+        return (
+            <div className="mx2">
+                <div
+                    className="flex align-center cursor-pointer text-purple-hover mb2"
+                    onClick={() => this.toggleExpanded()}
+                >
+                    <h3>{operator && titleCase(operator)}</h3>
+                    <Icon
+                        name='chevrondown'
+                        width="12"
+                        height="12"
+                        className="ml1"
+                    />
+                </div>
+                <ul
+                    className="text-purple"
+                    style={{
+                        height: expanded ? 'auto' : 0,
+                        overflow: 'hidden',
+                        display: 'block',
+                    }}
+                >
+                    { operators.map(o =>
+                        <li
+                            className={cx('List-item cursor-pointer p1', {
+                                'List-item--selected': o.name === operator
+                            })}
+                            key={o.name}
+                            onClick={() => {
+                                onOperatorChange(o);
+                                this.toggleExpanded();
+                            }}
+                        >
+                            <h4 className="List-item-title">{o.name}</h4>
+                        </li>
+                    )}
+                </ul>
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 2ec5c52351c585172a698e6668383894a33aeed3..fcc4d24743c3940a6b9f72ab8ea28a76fd42cbd7 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -52,7 +52,9 @@ export default class FilterPopover extends Component {
 
             // default to the first operator
             let { field } = Query.getFieldTarget(filter[1], this.props.tableMetadata);
-            let operator = field.valid_operators[0].name;
+
+            // let the DatePicker choose the default operator, otherwise use the first one
+            let operator = isDate(field) ? null : field.valid_operators[0].name;
 
             filter = this._updateOperator(filter, operator);
         }
@@ -209,7 +211,9 @@ export default class FilterPopover extends Component {
             let { table, field } = Query.getFieldTarget(filter[1], this.props.tableMetadata);
 
             return (
-                <div style={{width: 300}}>
+                <div style={{
+                    minWidth: 300
+                }}>
                     <div className="FilterPopover-header text-grey-3 p1 mt1 flex align-center">
                         <a className="cursor-pointer flex align-center" onClick={this.clearField}>
                             <Icon name="chevronleft" size={18}/>
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
index aa95d51cad38c3621581ec2fb1db1771ed208a78..dc678972aa4fc34635e0baf0cb6accccaeae99b9 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
@@ -119,6 +119,7 @@ export default class FilterWidget extends Component {
                     className="FilterPopover"
                     isInitiallyOpen={this.props.filter[1] === null}
                     onClose={this.close}
+                    horizontalAttachments={["left"]}
                 >
                     <FilterPopover
                         filter={this.props.filter}
diff --git a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
index b4062f483753743d6f087ddc00b0e35a3a1d194a..3827a927e2dd4ed304ed7624b01cd4d7a0bcb381 100644
--- a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
@@ -34,7 +34,9 @@ export default class OperatorSelector extends Component {
         }
 
         return (
-            <div id="OperatorSelector" className="border-bottom p1">
+            <div id="OperatorSelector" className="border-bottom p1" style={{
+                maxWidth: 300
+            }}>
                 { visibleOperators.map(o =>
                     <button
                         key={o.name}
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index fdfa48bc34756caba96ea743e1c568eeb849cbf9..b97632aa76b31575b791ce0c239e02fa71f49c8e 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -1,71 +1,157 @@
 import React, { Component, PropTypes } from "react";
 
-import SpecificDatePicker from "./SpecificDatePicker.jsx";
-import RelativeDatePicker from "./RelativeDatePicker.jsx";
-import OperatorSelector from "../OperatorSelector.jsx";
+import SpecificDatePicker from "./SpecificDatePicker";
+import RelativeDatePicker, { UnitPicker } from "./RelativeDatePicker";
+import DateOperatorSelector from "../DateOperatorSelector";
+import Calendar from "metabase/components/Calendar";
 
-export default class DatePicker extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            pane: this._detectPane(props)
-        };
+import moment from "moment";
+
+import _ from "underscore";
+
+const SingleDatePicker = ({ filter: [op, field, value], onFilterChange }) =>
+    <SpecificDatePicker value={value} onChange={(value) => onFilterChange([op, field, value])} calendar />
+
+const MultiDatePicker = ({ filter: [op, field, startValue, endValue], onFilterChange }) =>
+    <div className="mx2 mb1">
+        <div className="flex">
+            <SpecificDatePicker value={startValue} onChange={(value) => onFilterChange([op, field, value, endValue])}  />
+            <span className="mx2 mt2">&ndash;</span>
+            <SpecificDatePicker value={endValue} onChange={(value) => onFilterChange([op, field, startValue, value])} />
+        </div>
+        <div className="Calendar--noContext">
+            <Calendar
+                initial={startValue ? moment(startValue) : moment()}
+                selected={startValue && moment(startValue)}
+                selectedEnd={endValue && moment(endValue)}
+                onChange={(startValue, endValue) => onFilterChange([op, field, startValue, endValue])}
+                isDual
+            />
+        </div>
+    </div>
+
+const PreviousPicker =  (props) =>
+    <RelativeDatePicker {...props} formatter={(value) => value * -1} />
+
+const NextPicker = (props) =>
+    <RelativeDatePicker {...props} />
+
+class CurrentPicker extends Component {
+    constructor() {
+        super();
+        this.state = { showUnits: false };
+    }
+
+    render() {
+        const { filter: [operator, field, intervals, unit], onFilterChange } = this.props
+        return (
+            <div className="mx2">
+                <UnitPicker
+                    value={unit}
+                    open={this.state.showUnits}
+                    onChange={(value) => {
+                        onFilterChange([operator, field, intervals, value]);
+                        this.setState({ showUnits: false });
+                    }}
+                    togglePicker={() => this.setState({ showUnits: !this.state.showUnits })}
+                    formatter={(val) => val }
+                />
+            </div>
+        )
     }
+}
+
 
+const getIntervals = ([op, field, value, unit]) => op === "TIME_INTERVAL" && typeof value === "number" ? Math.abs(value) : 1;
+const getUnit      = ([op, field, value, unit]) => op === "TIME_INTERVAL" && unit ? unit : "day";
+const getDate      = (value) => typeof value === "string" && moment(value).isValid() ? value : moment().format("YYYY-MM-DD");
+
+const OPERATORS = [
+    {
+        name: "Previous",
+        init: (filter) => ["TIME_INTERVAL", filter[1], -getIntervals(filter), getUnit(filter)],
+        test: ([op, field, value]) => op === "TIME_INTERVAL" && value < 0 || Object.is(value, -0),
+        widget: PreviousPicker,
+    },
+    {
+        name: "Next",
+        init: (filter) => ["TIME_INTERVAL", filter[1], getIntervals(filter), getUnit(filter)],
+        test: ([op, field, value]) => op === "TIME_INTERVAL" && value >= 0,
+        widget: NextPicker,
+    },
+    {
+        name: "Current",
+        init: (filter) => ["TIME_INTERVAL", filter[1], "current", getUnit(filter)],
+        test: ([op, field, value]) => op === "TIME_INTERVAL" && value === "current",
+        widget: CurrentPicker,
+    },
+    {
+        name: "Before",
+        init: (filter) => ["<", filter[1], getDate(filter[2])],
+        test: ([op]) => op === "<",
+        widget: SingleDatePicker,
+    },
+    {
+        name: "After",
+        init: (filter) => [">", filter[1], getDate(filter[2])],
+        test: ([op]) => op === ">",
+        widget: SingleDatePicker,
+    },
+    {
+        name: "On",
+        init: (filter) => ["=", filter[1], getDate(filter[2])],
+        test: ([op]) => op === "=",
+        widget: SingleDatePicker,
+    },
+    {
+        name: "Between",
+        init: (filter) => ["BETWEEN", filter[1], null, null],
+        test: ([op]) => op === "BETWEEN",
+        widget: MultiDatePicker,
+    },
+    {
+        name: "Is Empty",
+        init: (filter) => ["IS_NULL", filter[1]],
+        test: ([op]) => op === "IS_NULL"
+    },
+    {
+        name: "Not Empty",
+        init: (filter) => ["NOT_NULL", filter[1]],
+        test: ([op]) => op === "NOT_NULL"
+    }
+];
+
+export default class DatePicker extends Component {
     static propTypes = {
         filter: PropTypes.array.isRequired,
         onFilterChange: PropTypes.func.isRequired,
-        onOperatorChange: PropTypes.func.isRequired,
         tableMetadata: PropTypes.object.isRequired
     };
 
-    _detectPane(props) {
-        if (props.filter[0] === "IS_NULL" || props.filter[0] === "NOT_NULL") {
-            return props.filter[0]
-        } else if (props.filter[0] !== "TIME_INTERVAL" && typeof props.filter[2] === "string") {
-            return "specific";
-        } else {
-            return "relative";
-        }
+    componentWillMount() {
+        const operator = this._getOperator() || OPERATORS[0];
+        this.props.onFilterChange(operator.init(this.props.filter));
     }
 
-    selectPane(pane) {
-        this.props.onFilterChange([null, this.props.filter[1]]);
-        this.setState({ pane });
+    _getOperator() {
+        return _.find(OPERATORS, (o) => o.test(this.props.filter));
     }
 
     render() {
-        var operators = [
-            { name: "relative", verboseName: "Relative date" },
-            { name: "specific", verboseName: "Specific date" },
-            { name: "IS_NULL", verboseName: "Is Empty", advanced: true },
-            { name: "NOT_NULL", verboseName: "Not Empty", advanced: true }
-        ];
+        let { filter, onFilterChange } = this.props;
+        const operator = this._getOperator();
+        const Widget = operator && operator.widget;
 
         return (
-            <div>
-                <OperatorSelector
-                    operator={this.state.pane}
-                    operators={operators}
-                    onOperatorChange={(operator) => {
-                        this.setState({ pane: operator });
-                        if (operator === "IS_NULL" || operator === "NOT_NULL") {
-                            this.props.onOperatorChange(operator);
-                        }
-                    }}
+            <div className="mt1 pt2 border-top">
+                <DateOperatorSelector
+                    operator={operator && operator.name}
+                    operators={OPERATORS}
+                    onOperatorChange={operator => onFilterChange(operator.init(filter))}
                 />
-                { this.state.pane === "relative" ?
-                    <RelativeDatePicker
-                        filter={this.props.filter}
-                        onFilterChange={this.props.onFilterChange}
-                    />
-                : this.state.pane === "specific" ?
-                    <SpecificDatePicker
-                        filter={this.props.filter}
-                        onFilterChange={this.props.onFilterChange}
-                        onOperatorChange={this.props.onOperatorChange}
-                    />
-                : null }
+                { Widget &&
+                    <Widget {...this.props} filter={filter} />
+                }
             </div>
         )
     }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..bbe9a4153654b88c7ad29f63ed15380c7f3d0f1b
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+import Input from "metabase/components/Input.jsx";
+
+const NumericInput = ({ value, onChange, ...props }) =>
+    <Input
+        value={value == null ? "" : String(value)}
+        onBlurChange={({ target: { value }}) => {
+            value = value ? parseInt(value, 10) : null;
+            if (!isNaN(value)) {
+                onChange(value);
+            }
+        }}
+        {...props}
+    />
+
+export default NumericInput;
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
index dafc15ca62e75d150abe32e3f0d61cfa1b07e2f4..469038ca4d18ffbd6ed42ad1621b8a206e726058 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
@@ -1,101 +1,89 @@
 import React, { Component, PropTypes } from "react";
-
+import { pluralize, titleCase } from "humanize-plus";
 import cx from "classnames";
-import _ from "underscore";
-
-const SHORTCUTS = [
-    { name: "Today",        operator: ["=", "<", ">"], values: [["relative_datetime", "current"]]},
-    { name: "Yesterday",    operator: ["=", "<", ">"], values: [["relative_datetime", -1, "day"]]},
-    { name: "Past 7 days",  operator: "TIME_INTERVAL", values: [-7, "day"]},
-    { name: "Past 30 days", operator: "TIME_INTERVAL", values: [-30, "day"]}
-];
 
-const RELATIVE_SHORTCUTS = {
-    "Last": [
-        { name: "Week",  operator: "TIME_INTERVAL", values: ["last", "week"]},
-        { name: "Month", operator: "TIME_INTERVAL", values: ["last", "month"]},
-        { name: "Year",  operator: "TIME_INTERVAL", values: ["last", "year"]}
-    ],
-    "This": [
-        { name: "Week",  operator: "TIME_INTERVAL", values: ["current", "week"]},
-        { name: "Month", operator: "TIME_INTERVAL", values: ["current", "month"]},
-        { name: "Year",  operator: "TIME_INTERVAL", values: ["current", "year"]}
-    ]
-};
+import Icon from "metabase/components/Icon";
+import NumericInput from "./NumericInput.jsx";
 
 export default class RelativeDatePicker extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        _.bindAll(this, "isSelectedShortcut", "onSetShortcut");
+    constructor () {
+        super();
+        this.state = { showUnits: false };
     }
 
     static propTypes = {
         filter: PropTypes.array.isRequired,
-        onFilterChange: PropTypes.func.isRequired
+        onFilterChange: PropTypes.func.isRequired,
+        formatter: PropTypes.func.isRequired
     };
 
-    isSelectedShortcut(shortcut) {
-        let { filter } = this.props;
-        return (
-            (Array.isArray(shortcut.operator) ? _.contains(shortcut.operator, filter[0]): filter[0] === shortcut.operator ) &&
-            _.isEqual(filter.slice(2), shortcut.values)
-        );
-    }
-
-    onSetShortcut(shortcut) {
-        let { filter } = this.props;
-        let operator;
-        if (Array.isArray(shortcut.operator)) {
-            if (_.contains(shortcut.operator, filter[0])) {
-                operator = filter[0];
-            } else {
-                operator = shortcut.operator[0];
-            }
-        } else {
-            operator = shortcut.operator;
-        }
-        this.props.onFilterChange([operator, filter[1], ...shortcut.values])
+    static defaultProps = {
+        formatter: (value) => value
     }
 
     render() {
+        const { filter: [op, field, intervals, unit], onFilterChange, formatter } = this.props;
         return (
-            <div className="p1 pt2">
-                <section>
-                    { SHORTCUTS.map((s, index) =>
-                        <span key={index} className={cx("inline-block half pb1", { "pr1": index % 2 === 0 })}>
-                            <button
-                                key={index}
-                                className={cx("Button Button-normal Button--medium text-normal text-centered full", { "Button--purple": this.isSelectedShortcut(s) })}
-                                onClick={() => this.onSetShortcut(s)}
-                            >
-                                {s.name}
-                            </button>
-                        </span>
-                    )}
-                </section>
-                {Object.keys(RELATIVE_SHORTCUTS).map(sectionName =>
-                    <section key={sectionName}>
-                        <div style={{}} className="border-bottom text-uppercase flex layout-centered mb2">
-                            <h6 style={{"position": "relative", "backgroundColor": "white", "top": "6px" }} className="px2">
-                                {sectionName}
-                            </h6>
-                        </div>
-                        <div className="flex">
-                            { RELATIVE_SHORTCUTS[sectionName].map((s, index) =>
-                                <button
-                                    key={index}
-                                    data-ui-tag={"relative-date-shortcut-" + sectionName.toLowerCase() + "-" + s.name.toLowerCase()}
-                                    className={cx("Button Button-normal Button--medium flex-full mb1", { "Button--purple": this.isSelectedShortcut(s), "mr1": index !== RELATIVE_SHORTCUTS[sectionName].length - 1 })}
-                                    onClick={() => this.onSetShortcut(s)}
-                                >
-                                    {s.name}
-                                </button>
-                            )}
-                        </div>
-                    </section>
-                )}
+            <div className="px2">
+                <NumericInput
+                    className="input h3 mb2 border-purple"
+                    value={typeof intervals === "number" ? Math.abs(intervals) : intervals}
+                    onChange={(value) =>
+                        onFilterChange([op, field, formatter(value), unit])
+                    }
+                    placeholder="30"
+                />
+                <UnitPicker
+                    open={this.state.showUnits}
+                    value={unit}
+                    onChange={(value) => {
+                        onFilterChange([op, field, intervals, value]);
+                        this.setState({ showUnits: false });
+                    }}
+                    togglePicker={() => this.setState({ showUnits: !this.state.showUnits})}
+                    intervals={intervals}
+                    formatter={formatter}
+                />
             </div>
         );
     }
 }
+
+export const UnitPicker = ({ open, value, onChange, togglePicker, intervals, formatter }) =>
+   <div>
+       <div
+           onClick={() => togglePicker()}
+           className="flex align-center cursor-pointer text-purple-hover mb2"
+       >
+           <h3>{pluralize(formatter(intervals) || 1, titleCase(value))}</h3>
+           <Icon
+               name='chevrondown'
+               width="12"
+               height="12"
+               className="ml1"
+           />
+        </div>
+        <ol
+            className="text-purple"
+            style={{
+                maxHeight: open ? 'none': 0,
+                overflow: 'hidden'
+            }}
+        >
+           { ['Minute', 'Hour', 'Day', 'Month', 'Year',].map((unit, index) =>
+               <li
+                   className={cx(
+                       'List-item cursor-pointer p1',
+                       { 'List-item--selected': unit === value }
+                   )}
+                   key={index}
+                   onClick={ () => onChange(unit.toLowerCase()) }
+               >
+                   <h4 className="List-item-title">
+                       {pluralize(formatter(intervals) || 1, unit)}
+                   </h4>
+               </li>
+             )
+           }
+       </ol>
+   </div>
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index 556bf50a94c9d67f7806b9517aa8ea0700a2fe1d..a79087c5da88eff9a7b488e0cd5096b24e4b7d8e 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -1,104 +1,173 @@
 import React, { Component, PropTypes } from 'react';
 
-import Calendar from "metabase/components/Calendar.jsx";
-import Input from "metabase/components/Input.jsx";
+import Calendar from "metabase/components/Calendar";
+import Input from "metabase/components/Input";
+import Icon from "metabase/components/Icon";
+import ExpandingContent from "metabase/components/ExpandingContent";
+import Tooltip from "metabase/components/Tooltip";
+import NumericInput from "./NumericInput.jsx";
 
-import { computeFilterTimeRange } from "metabase/lib/query_time";
-
-import _ from "underscore";
 import moment from "moment";
+import cx from "classnames";
+
+const DATE_FORMAT = "YYYY-MM-DD";
+const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss";
 
 export default class SpecificDatePicker extends Component {
-    constructor(props, context) {
-        super(props, context);
+    constructor() {
+        super();
+
+        this.state = {
+            showCalendar: true
+        }
 
-        _.bindAll(this, "onChange");
+        this.onChange = this.onChange.bind(this);
     }
 
     static propTypes = {
-        filter: PropTypes.array.isRequired,
-        onFilterChange: PropTypes.func.isRequired,
-        onOperatorChange: PropTypes.func.isRequired
+        value: PropTypes.string,
+        onChange: PropTypes.func.isRequired,
     };
 
-    toggleOperator(operator) {
-        if (this.props.filter[0] === operator) {
-            this.props.onOperatorChange("=");
-        } else {
-            this.props.onOperatorChange(operator);
+    onChange(date, hours, minutes) {
+        let m = moment(date);
+        if (!m.isValid()) {
+            this.props.onChange(null);
         }
-    }
 
-    onChange(start, end) {
-        let { filter } = this.props;
-        if (start && end && !moment(start).isSame(end)) {
-            this.props.onFilterChange(["BETWEEN", filter[1], start, end]);
+        let hasTime = false;
+        if (hours != null) {
+            m.hours(hours);
+            hasTime = true;
+        }
+        if (minutes != null) {
+            m.minutes(minutes);
+            hasTime = true;
+        }
+
+        if (hasTime) {
+            this.props.onChange(m.format(DATE_TIME_FORMAT));
         } else {
-            this.props.onFilterChange(["=", filter[1], start || end]);
+            this.props.onChange(m.format(DATE_FORMAT));
         }
     }
 
     render() {
-        let { filter } = this.props;
-        let [start, end] = computeFilterTimeRange(filter);
-
-        let initial;
-        if (start && end) {
-            initial = Math.abs(moment().diff(start)) < Math.abs(moment().diff(end)) ? start : end;
-        } else if (start) {
-            initial = start;
-        }
+        const { value, calendar } = this.props;
+        const { showCalendar } = this.state;
 
-        let singleDay = start && start.isSame(end, "day");
-        if (singleDay) {
-            end = null;
-        }
-
-        let startValue, startPlaceholder, endValue, endPlaceholder;
-        if (filter[0] === "<") {
-            startPlaceholder = "∞";
-            endValue = filter[2];
-        } else if (filter[0] === ">") {
-            startValue = filter[2];
-            endPlaceholder = "∞";
-        } else if (filter[0] === "BETWEEN") {
-            startValue = filter[2];
-            endValue = filter[3];
-        } else {
-            startValue = filter[2];
-            endValue = filter[2];
+        let date, hours, minutes;
+        if (moment(value, DATE_TIME_FORMAT, true).isValid()) {
+            date = moment(value, DATE_TIME_FORMAT, true);
+            hours = date.hours();
+            minutes = date.minutes();
+            date.startOf("day");
+        } else if (moment(value, DATE_FORMAT, true).isValid()) {
+            date = moment(value, DATE_FORMAT, true);
         }
 
         return (
-            <div>
-                <div className="mx2 mt2">
-                    <Calendar
-                        initial={initial}
-                        selected={start}
-                        selectedEnd={end}
-                        onChange={this.onChange}
-                        onBeforeClick={singleDay ? this.toggleOperator.bind(this, "<") : undefined}
-                        onAfterClick={singleDay ? this.toggleOperator.bind(this, ">") : undefined}
-                    />
-                    <div className="py2 text-centered">
+            <div className="px1">
+                <div className="flex align-center mb1">
+                    <div className={cx('border-top border-bottom full border-left', { 'border-right': !calendar })}>
                         <Input
-                            className="input input--small text-bold text-grey-4 text-centered"
-                            style={{width: "100px"}}
-                            value={startValue && moment(startValue).format("MM/DD/YYYY")}
-                            placeholder={startPlaceholder}
-                            onBlurChange={(e) => this.onChange(moment(e.target.value).format("YYYY-MM-DD"), singleDay ? null : endValue)}
-                        />
-                        <span className="px1">–</span>
-                        <Input
-                            className="input input--small text-bold text-grey-4 text-centered"
-                            style={{width: "100px"}}
-                            value={endValue && moment(endValue).format("MM/DD/YYYY")}
-                            placeholder={endPlaceholder}
-                            onBlurChange={(e) => this.onChange(startValue, moment(e.target.value).format("YYYY-MM-DD"))}
+                            placeholder={moment().format("MM/DD/YYYY")}
+                            className="borderless full p2 h3"
+                            style={{
+                                outline: 'none'
+                            }}
+                            value={date ? date.format("MM/DD/YYYY") : ""}
+                            onBlurChange={({ target: { value } }) => {
+                                let date = moment(value, "MM/DD/YYYY");
+                                if (date.isValid()) {
+                                    this.onChange(date, hours, minutes)
+                                } else {
+                                    this.onChange(null)
+                                }
+                            }}
+                            ref="value"
                         />
                     </div>
+                    { calendar &&
+                        <div className="border-right border-bottom border-top p2">
+                            <Tooltip
+                                tooltip={
+                                    showCalendar ? "Hide calendar" : "Show calendar"
+                                }
+                                children={
+                                    <Icon
+                                        className="text-purple-hover cursor-pointer"
+                                        name='calendar'
+                                        onClick={() => this.setState({ showCalendar: !this.state.showCalendar })}
+                                    />
+                                }
+                            />
+                        </div>
+                    }
+                </div>
+
+                { calendar &&
+                    <ExpandingContent open={showCalendar}>
+                        <Calendar
+                            selected={date}
+                            initial={date || moment()}
+                            onChange={(value) => this.onChange(value, hours, minutes)}
+                            isRangePicker={false}
+                        />
+                    </ExpandingContent>
+                }
+
+                <div className={cx({ 'py2': calendar }, { 'mb3': !calendar })}>
+                    { hours == null || minutes == null ?
+                        <div
+                            className="text-purple-hover cursor-pointer flex align-center"
+                            onClick={() => this.onChange(date, 12, 30) }
+                        >
+                            <Icon
+                                className="mr1"
+                                name='clock'
+                            />
+                            Add a time
+                        </div>
+                    :
+                        <HoursMinutes
+                            clear={() => this.onChange(date, null, null)}
+                            hours={hours}
+                            minutes={minutes}
+                            onChangeHours={hours => this.onChange(date, hours, minutes)}
+                            onChangeMinutes={minutes => this.onChange(date, hours, minutes)}
+                        />
+                    }
                 </div>
             </div>
         )
     }
 }
+
+const HoursMinutes = ({ hours, minutes, onChangeHours, onChangeMinutes, clear }) =>
+    <div className="flex align-center">
+        <NumericInput
+            className="input"
+            size={2}
+            maxLength={2}
+            value={(hours % 12) === 0 ? "12" : String(hours % 12)}
+            onChange={(value) => onChangeHours((hours >= 12 ? 12 : 0) + value) }
+        />
+        <span className="px1">:</span>
+        <NumericInput
+            className="input"
+            size={2}
+            maxLength={2}
+            value={minutes}
+            onChange={(value) => onChangeMinutes(value) }
+        />
+        <div className="flex align-center pl1">
+            <span className={cx("text-purple-hover mr1", { "text-purple": hours < 12, "cursor-pointer": hours >= 12 })} onClick={hours >= 12 ? () => onChangeHours(hours - 12) : null}>AM</span>
+            <span className={cx("text-purple-hover mr1", { "text-purple": hours >= 12, "cursor-pointer": hours < 12 })} onClick={hours < 12 ? () => onChangeHours(hours + 12) : null}>PM</span>
+        </div>
+        <Icon
+            className="text-grey-2 cursor-pointer text-grey-4-hover ml-auto"
+            name="close"
+            onClick={() => clear() }
+        />
+    </div>
diff --git a/frontend/src/metabase/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/PieChart.jsx
index 8d4cc684bbb6116e7641fb558dbdc1597e34b597..4490dc65694a03c06ddea37dc3844e9a04b79974 100644
--- a/frontend/src/metabase/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/PieChart.jsx
@@ -39,7 +39,7 @@ export default class PieChart extends Component {
 
     static checkRenderable(cols, rows, settings) {
         if (!settings["pie.dimension"] || !settings["pie.metric"]) {
-            throw new ChartSettingsError("Please select columns in the chart settings.", "Data");
+            throw new ChartSettingsError("Which columns do want to use?", "Data");
         }
     }
 
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 446d598bd95766e65c56c55d3139b4cb6fb29f65..8e288b4fac99241556d4caed26d32e3b201a892e 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -3,6 +3,8 @@ import cx from "classnames";
 import { assocIn } from "icepick";
 import _ from "underscore";
 
+import Warnings from "metabase/query_builder/components/Warnings.jsx";
+
 import Visualization from "metabase/visualizations/components/Visualization.jsx"
 import { getSettingsWidgets } from "metabase/lib/visualization_settings";
 import MetabaseAnalytics from "metabase/lib/analytics";
@@ -39,9 +41,11 @@ const Widget = ({ title, hidden, disabled, widget, value, onChange, props }) =>
 class ChartSettings extends Component {
     constructor (props) {
         super(props);
+        const initialSettings = props.series[0].card.visualization_settings;
         this.state = {
           currentTab: null,
-          settings: props.series[0].card.visualization_settings
+          settings: initialSettings,
+          series: this._getSeries(props.series, initialSettings)
       };
     }
 
@@ -49,15 +53,33 @@ class ChartSettings extends Component {
         this.setState({ currentTab: tab });
     }
 
+    _getSeries(series, settings) {
+        if (settings) {
+            series = assocIn(series, [0, "card", "visualization_settings"], settings);
+        }
+        const transformed = getVisualizationTransformed(series);
+        return transformed.series;
+    }
+
+    onResetSettings = () => {
+        MetabaseAnalytics.trackEvent("Chart Settings", "Reset Settings");
+        this.setState({
+            settings: {},
+            series: this._getSeries(this.props.series, {})
+        });
+    }
+
     onChangeSettings = (newSettings) => {
         for (const key of Object.keys(newSettings)) {
             MetabaseAnalytics.trackEvent("Chart Settings", "Change Setting", key);
         }
+        const settings = {
+            ...this.state.settings,
+            ...newSettings
+        }
         this.setState({
-            settings: {
-                ...this.state.settings,
-                ...newSettings
-            }
+            settings: settings,
+            series: this._getSeries(this.props.series, settings)
         });
     }
 
@@ -77,12 +99,8 @@ class ChartSettings extends Component {
     }
 
     render () {
-        let { series, onClose, isDashboard } = this.props;
-
-        series = assocIn(series, [0, "card", "visualization_settings"], this.state.settings);
-
-        const transformed = getVisualizationTransformed(series);
-        series = transformed.series;
+        const { onClose, isDashboard } = this.props;
+        const { series } = this.state;
 
         const tabs = {};
         for (let widget of getSettingsWidgets(series, this.onChangeSettings, isDashboard)) {
@@ -103,36 +121,44 @@ class ChartSettings extends Component {
         const isDirty = !_.isEqual(this.props.series[0].card.visualization_settings, this.state.settings);
 
         return (
-          <div className="flex flex-column spread p4">
-              <h2 className="my2">Customize this {this.getChartTypeName()}</h2>
-              { tabNames.length > 1 &&
-                  <ChartSettingsTabs tabs={tabNames} selectTab={this.selectTab} activeTab={currentTab}/>
-              }
-              <div className="Grid flex-full mt3">
-                  <div className="Grid-cell Cell--1of3 scroll-y p1">
-                      { widgets && widgets.map((widget) =>
-                          <Widget key={widget.id} {...widget} />
-                      )}
-                  </div>
-                  <div className="Grid-cell relative">
-                      <Visualization
-                          className="spread"
-                          series={series}
-                          isEditing
-                          isDashboard
-                          onUpdateVisualizationSettings={this.onChangeSettings}
-                      />
-                  </div>
-              </div>
-              <div className="pt1">
-                <a className={cx("Button Button--primary", { disabled: !isDirty })} onClick={() => this.onDone()} data-metabase-event="Chart Settings;Done">Done</a>
-                <a className="text-grey-2 ml2" onClick={onClose} data-metabase-event="Chart Settings;Cancel">Cancel</a>
-                { !_.isEqual(this.state.settings, {}) &&
-                    <a className="Button Button--warning float-right" onClick={() => this.setState({ settings: {} })} data-metabase-event="Chart Settings;Reset">Reset to defaults</a>
+            <div className="flex flex-column spread p4">
+                <h2 className="my2">Customize this {this.getChartTypeName()}</h2>
+
+                { tabNames.length > 1 &&
+                    <ChartSettingsTabs tabs={tabNames} selectTab={this.selectTab} activeTab={currentTab}/>
                 }
-              </div>
-          </div>
-        )
+                <div className="Grid flex-full mt3">
+                    <div className="Grid-cell Cell--1of3 scroll-y p1">
+                        { widgets && widgets.map((widget) =>
+                            <Widget key={widget.id} {...widget} />
+                        )}
+                    </div>
+                    <div className="Grid-cell flex flex-column">
+                        <div className="flex flex-column">
+                            <Warnings className="mx2 align-self-end text-gold" warnings={this.state.warnings} size={20} />
+                        </div>
+                        <div className="flex-full relative">
+                            <Visualization
+                                className="spread"
+                                series={series}
+                                isEditing
+                                isDashboard
+                                showWarnings
+                                onUpdateVisualizationSettings={this.onChangeSettings}
+                                onUpdateWarnings={(warnings) => this.setState({ warnings })}
+                            />
+                        </div>
+                    </div>
+                </div>
+                <div className="pt1">
+                  <a className={cx("Button Button--primary", { disabled: !isDirty })} onClick={() => this.onDone()} data-metabase-event="Chart Settings;Done">Done</a>
+                  <a className="text-grey-2 ml2" onClick={onClose} data-metabase-event="Chart Settings;Cancel">Cancel</a>
+                  { !_.isEqual(this.state.settings, {}) &&
+                      <a className="Button Button--warning float-right" onClick={this.onResetSettings} data-metabase-event="Chart Settings;Reset">Reset to defaults</a>
+                  }
+                </div>
+            </div>
+        );
     }
 }
 
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index 543b5bb6821e17d352197854eceba540f4a7b68a..7022b90c84e79ee04e36399c6ed55ade3d057115 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -35,7 +35,7 @@ export default class LineAreaBarChart extends Component {
         const dimensions = (settings["graph.dimensions"] || []).filter(name => name);
         const metrics = (settings["graph.metrics"] || []).filter(name => name);
         if (dimensions.length < 1 || metrics.length < 1) {
-            throw new ChartSettingsError("Please select columns for the X and Y axis in the visualization settings.", "Data");
+            throw new ChartSettingsError("Which fields do you want to use for the X and Y axes?", "Data", "Choose fields");
         }
     }
 
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 5778e43878561b341d8ee789962d95fb8a9a2912..c66217a0826d86e9e9278ad13479ac943318548c 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -8,11 +8,12 @@ import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
-import { duration } from "metabase/lib/formatting";
+import { duration, formatNumber } from "metabase/lib/formatting";
 
 import { getVisualizationTransformed } from "metabase/visualizations";
 import { getSettings } from "metabase/lib/visualization_settings";
 import { isSameSeries } from "metabase/visualizations/lib/utils";
+import Utils from "metabase/lib/utils";
 
 import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
 
@@ -29,9 +30,10 @@ export default class Visualization extends Component {
         super(props, context)
 
         this.state = {
-            renderInfo: null,
             hovered: null,
-            error: null
+            error: null,
+            warnings: [],
+            yAxisSplit: null,
         };
 
         _.bindAll(this, "onRender", "onRenderError", "onHoverChange");
@@ -70,6 +72,8 @@ export default class Visualization extends Component {
         cellIsClickableFn: PropTypes.func,
         cellClickedFn: PropTypes.func,
 
+        // misc
+        onUpdateWarnings: PropTypes.func,
         onOpenChartSettings: PropTypes.func,
     };
 
@@ -84,35 +88,60 @@ export default class Visualization extends Component {
     }
 
     componentWillReceiveProps(newProps) {
-        if (isSameSeries(newProps.series, this.props.series)) {
-            // clear the error so we can try to render again
-            this.setState({ error: null });
-        } else {
+        if (!isSameSeries(newProps.series, this.props.series) || !Utils.equals(newProps.settings, this.props.settings)) {
             this.transform(newProps);
         }
     }
 
+    componentDidMount() {
+        this.updateWarnings();
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        if (!Utils.equals(this.getWarnings(prevProps, prevState), this.getWarnings())) {
+            this.updateWarnings();
+        }
+    }
+
+    getWarnings(props = this.props, state = this.state) {
+        let warnings = state.warnings || [];
+        // don't warn about truncated data for table since we show a warning in the row count
+        if (state.series[0].card.display !== "table") {
+            warnings = warnings.concat(props.series
+                .filter(s => s.data && s.data.rows_truncated != null)
+                .map(s => `Data truncated to ${formatNumber(s.data.rows_truncated)} rows.`));
+        }
+        return warnings;
+    }
+
+    updateWarnings() {
+        if (this.props.onUpdateWarnings) {
+            this.props.onUpdateWarnings(this.getWarnings() || []);
+        }
+    }
+
     transform(newProps) {
         this.setState({
-            error: null,
+            yAxisSplit: null,
+            warnings: [],
             ...getVisualizationTransformed(newProps.series)
         });
     }
 
     onHoverChange(hovered) {
-        const { renderInfo } = this.state;
+        const { yAxisSplit } = this.state;
         if (hovered) {
             // if we have Y axis split info then find the Y axis index (0 = left, 1 = right)
-            if (renderInfo && renderInfo.yAxisSplit) {
-                const axisIndex = _.findIndex(renderInfo.yAxisSplit, (indexes) => _.contains(indexes, hovered.index));
+            if (yAxisSplit) {
+                const axisIndex = _.findIndex(yAxisSplit, (indexes) => _.contains(indexes, hovered.index));
                 hovered = assoc(hovered, "axisIndex", axisIndex);
             }
         }
         this.setState({ hovered });
     }
 
-    onRender(renderInfo) {
-        this.setState({ renderInfo });
+    onRender({ yAxisSplit, warnings = [] } = {}) {
+        this.setState({ yAxisSplit, warnings });
     }
 
     onRenderError(error) {
@@ -147,8 +176,8 @@ export default class Visualization extends Component {
                             <div>
                                 <div>{error}</div>
                                 <div className="mt2">
-                                    <button className="Button Button--primary Button--small" onClick={this.props.onOpenChartSettings}>
-                                        Edit Settings
+                                    <button className="Button Button--primary Button--medium" onClick={this.props.onOpenChartSettings}>
+                                        {e.buttonText}
                                     </button>
                                 </div>
                             </div>
@@ -200,7 +229,7 @@ export default class Visualization extends Component {
                     replacementContent
                 // on dashboards we should show the "No results!" warning if there are no rows or there's a MinRowsError and actualRows === 0
                 : isDashboard && noResults ?
-                    <div className="flex-full px1 pb1 text-centered text-slate flex flex-column layout-centered">
+                    <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
                         <Tooltip tooltip="No results!" isEnabled={small}>
                             <img src="/app/img/no_results.svg" />
                         </Tooltip>
@@ -211,7 +240,7 @@ export default class Visualization extends Component {
                         }
                     </div>
                 : error ?
-                    <div className="flex-full px1 pb1 text-centered text-slate-light flex flex-column layout-centered">
+                    <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
                         <Tooltip tooltip={error} isEnabled={small}>
                             <Icon className="mb2" name={errorIcon || "warning"} size={50} />
                         </Tooltip>
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
index b535c075ec8701dd9460baca502d69684902a423..c08db35589323e42ccd13ade0fdbf60045f805a8 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
@@ -43,6 +43,10 @@ const DOT_OVERLAP_DISTANCE = 8;
 const VORONOI_TARGET_RADIUS = 50;
 const VORONOI_MAX_POINTS = 300;
 
+
+const UNAGGREGATED_DATA_WARNING = (col) => `"${getFriendlyName(col)}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.`
+const NULL_DIMENSION_WARNING = "Data includes missing dimension values.";
+
 function adjustTicksIfNeeded(axis, axisSize, minPixelsPerTick) {
     let numTicks = axis.ticks();
     // d3.js is dumb and sometimes numTicks is a number like 10 and other times it is an Array like [10]
@@ -574,10 +578,32 @@ function lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis) {
     chart.render();
 }
 
-function reduceGroup(group, key) {
+function reduceGroup(group, key, warnUnaggregated) {
     return group.reduce(
-        (acc, d) => (acc == null && d[key] == null) ? null : (acc || 0) + (d[key] || 0),
-        (acc, d) => (acc == null && d[key] == null) ? null : (acc || 0) - (d[key] || 0),
+        (acc, d) => {
+            if (acc == null && d[key] == null) {
+                return null;
+            } else {
+                if (acc != null) {
+                    warnUnaggregated();
+                    return acc + (d[key] || 0);
+                } else {
+                    return (d[key] || 0);
+                }
+            }
+        },
+        (acc, d) => {
+            if (acc == null && d[key] == null) {
+                return null;
+            } else {
+                if (acc != null) {
+                    warnUnaggregated();
+                    return acc - (d[key] || 0);
+                } else {
+                    return - (d[key] || 0);
+                }
+            }
+        },
         () => null
     );
 }
@@ -615,10 +641,15 @@ function fillMissingValues(datas, xValues, fillValue, getKey = (v) => v) {
 // Crossfilter calls toString on each moment object, which calls format(), which is very slow.
 // Replace toString with a function that just returns the unparsed ISO input date, since that works
 // just as well and is much faster
-let HACK_parseTimestamp = (value, unit) => {
-    let m = parseTimestamp(value, unit);
-    m.toString = moment_fast_toString
-    return m;
+let HACK_parseTimestamp = (value, unit, warn) => {
+    if (value == null) {
+        warn(NULL_DIMENSION_WARNING);
+        return null;
+    } else {
+        let m = parseTimestamp(value, unit);
+        m.toString = moment_fast_toString
+        return m;
+    }
 }
 
 function moment_fast_toString() {
@@ -630,6 +661,7 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
 
     const isTimeseries = settings["graph.x_axis.scale"] === "timeseries";
     const isQuantitative = ["linear", "log", "pow"].indexOf(settings["graph.x_axis.scale"]) >= 0;
+    const isOrdinal = !isTimeseries && !isQuantitative;
 
     const isDimensionTimeseries = dimensionIsTimeseries(series[0].data);
     const isDimensionNumeric = dimensionIsNumeric(series[0].data);
@@ -642,11 +674,16 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
         throw "This chart type doesn't support more than 20 series";
     }
 
+    const warnings = {};
+    const warn = (id) => {
+        warnings[id] = (warnings[id] || 0) + 1;
+    }
+
     let datas = series.map((s, index) =>
         s.data.rows.map(row => [
             // don't parse as timestamp if we're going to display as a quantitative scale, e.x. years and Unix timestamps
             (isDimensionTimeseries && !isQuantitative) ?
-                HACK_parseTimestamp(row[0], s.data.cols[0].unit)
+                HACK_parseTimestamp(row[0], s.data.cols[0].unit, warn)
             : isDimensionNumeric ?
                 row[0]
             :
@@ -745,8 +782,8 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
 
         dimension = dataset.dimension(d => d[0]);
         groups = [
-            datas.map((data, i) =>
-                reduceGroup(dimension.group(), i + 1)
+            datas.map((data, seriesIndex) =>
+                reduceGroup(dimension.group(), seriesIndex + 1, () => warn(UNAGGREGATED_DATA_WARNING(series[seriesIndex].data.cols[0])))
             )
         ];
     } else {
@@ -754,10 +791,10 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
         datas.map(data => dataset.add(data));
 
         dimension = dataset.dimension(d => d[0]);
-        groups = datas.map(data => {
+        groups = datas.map((data, seriesIndex) => {
             let dim = crossfilter(data).dimension(d => d[0]);
-            return data[0].slice(1).map((_, i) =>
-                reduceGroup(dim.group(), i + 1)
+            return data[0].slice(1).map((_, metricIndex) =>
+                reduceGroup(dim.group(), metricIndex + 1, () => warn(UNAGGREGATED_DATA_WARNING(series[seriesIndex].data.cols[0])))
             );
         });
     }
@@ -956,7 +993,15 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
     // apply any on-rendering functions
     lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis);
 
-    onRender && onRender({ yAxisSplit });
+    // only ordinal axis can display "null" values
+    if (isOrdinal) {
+        delete warnings[NULL_DIMENSION_WARNING];
+    }
+
+    onRender && onRender({
+        yAxisSplit,
+        warnings: Object.keys(warnings)
+    });
 
     return chart;
 }
diff --git a/frontend/src/metabase/visualizations/lib/errors.js b/frontend/src/metabase/visualizations/lib/errors.js
index 72e8da0a04fddae2c72183d0d795e3539c36da86..c7e5e7a46eaf86b9c04135d5d023659a587eb63b 100644
--- a/frontend/src/metabase/visualizations/lib/errors.js
+++ b/frontend/src/metabase/visualizations/lib/errors.js
@@ -22,8 +22,9 @@ export class LatitudeLongitudeError {
 }
 
 export class ChartSettingsError {
-    constructor(message, section) {
+    constructor(message, section, buttonText) {
         this.message = message || "Please configure this chart in the chart settings";
         this.section = section;
+        this.buttonText = buttonText || "Edit Settings";
     }
 }
diff --git a/frontend/test/e2e/admin/settings.spec.js b/frontend/test/e2e/admin/settings.spec.js
index 60124fd413e27569365d1faaf2cbd6b4ddae26dd..142fa205697f840b7255c9db1d6572d7dd8c9f2a 100644
--- a/frontend/test/e2e/admin/settings.spec.js
+++ b/frontend/test/e2e/admin/settings.spec.js
@@ -12,7 +12,7 @@ describeE2E("admin/settings", () => {
     );
 
     describe("admin settings", () => {
-        fit("should persist a setting", async () => {
+        it("should persist a setting", async () => {
             const siteName = "Metabase" + Math.random();
 
             await d.get(`${server.host}/admin/settings/general`);
diff --git a/package.json b/package.json
index 355ad6ecf33f23597702c4627a6259b1b60b3d8c..c549711f9ddec6fc1e38496e0b1fc3deccce1083 100644
--- a/package.json
+++ b/package.json
@@ -36,14 +36,14 @@
     "react-addons-shallow-compare": "^15.2.1",
     "react-ansi-style": "^1.0.0",
     "react-dom": "^15.2.1",
-    "react-draggable": "^2.2.2",
+    "react-draggable": "^2.2.3",
     "react-redux": "^4.4.5",
     "react-resizable": "^1.0.1",
     "react-retina-image": "^2.0.0",
     "react-router": "^2.6.0",
     "react-router-redux": "^4.0.5",
     "react-sortable": "^1.0.1",
-    "react-virtualized": "^8.5.3",
+    "react-virtualized": "^8.6.0",
     "recompose": "^0.20.2",
     "redux": "^3.5.2",
     "redux-actions": "^0.9.1",
@@ -73,6 +73,7 @@
     "babel-preset-react": "^6.5.0",
     "babel-preset-stage-0": "^6.5.0",
     "babel-register": "^6.11.6",
+    "concurrently": "^3.1.0",
     "css-loader": "^0.23.1",
     "eslint": "^3.5.0",
     "eslint-loader": "^1.6.0",
@@ -121,6 +122,7 @@
     "webpack-postcss-tools": "^1.1.1"
   },
   "scripts": {
+    "dev": "yarn && concurrently --kill-others -p name -n 'backend,frontend' -c 'blue,green' 'lein ring server' 'yarn run build-hot'",
     "lint": "eslint --ext .js --ext .jsx --max-warnings 0 frontend/src",
     "flow": "flow check",
     "test": "karma start frontend/test/karma.conf.js --single-run",
@@ -129,7 +131,7 @@
     "test-e2e-sauce": "USE_SAUCE=true yarn run test-e2e",
     "build": "webpack --bail",
     "build-watch": "webpack --watch",
-    "build-hot": "NODE_ENV=hot webpack && NODE_ENV=hot webpack-dev-server",
+    "build-hot": "NODE_ENV=hot webpack --bail && NODE_ENV=hot webpack-dev-server",
     "start": "yarn run build && lein ring server",
     "storybook": "start-storybook -p 9001",
     "preinstall": "ps -fp $PPID | grep -q yarn || echo '\\033[0;33mSorry, npm is not supported. Please use Yarn (https://yarnpkg.com/).\\033[0m'"
diff --git a/src/metabase/api/permissions.clj b/src/metabase/api/permissions.clj
index 5a1b928fe2addb01ebc633c72fabbc88dd0b6f7f..091a91e323a4d67be4cb4a8ac273fb39be21e0ba 100644
--- a/src/metabase/api/permissions.clj
+++ b/src/metabase/api/permissions.clj
@@ -77,20 +77,34 @@
 ;;; |                                                             PERMISSIONS GROUP ENDPOINTS                                                              |
 ;;; +------------------------------------------------------------------------------------------------------------------------------------------------------+
 
+(defn- group-id->num-members
+  "Return a map of `PermissionsGroup` ID -> number of members in the group.
+  (This doesn't include entries for empty groups.)"
+  []
+  (into {} (for [{:keys [group_id members]} (db/query {:select    [[:pgm.group_id :group_id] [:%count.pgm.id :members]]
+                                                       :from      [[:permissions_group_membership :pgm]]
+                                                       :left-join [[:core_user :user] [:= :pgm.user_id :user.id]]
+                                                       :where     [:= :user.is_active true]
+                                                       :group-by  [:pgm.group_id]})]
+             {group_id members})))
+
+(defn- ordered-groups
+  "Return a sequence of ordered `PermissionsGroups`, including the `MetaBot` group only if MetaBot is enabled."
+  []
+  (db/select PermissionsGroup
+    {:where    (if (metabot/metabot-enabled)
+                 true
+                 [:not= :id (u/get-id (group/metabot))])
+     :order-by [:%lower.name]}))
+
 (defendpoint GET "/group"
-  "Fetch all `PermissionsGroups`."
+  "Fetch all `PermissionsGroups`, including a count of the number of `:members` in that group."
   []
   (check-superuser)
-  (db/query {:select    [:pg.id :pg.name [:%count.pgm.id :members]]
-             :from      [[:permissions_group :pg]]
-             :left-join [[:permissions_group_membership :pgm] [:= :pg.id :pgm.group_id]
-                         [:core_user :user]                   [:= :pgm.user_id :user.id]]
-             :where     [:and [:= :user.is_active true]
-                              (if (metabot/metabot-enabled)
-                                true
-                                [:not= :pg.id (:id (group/metabot))])]
-             :group-by  [:pg.id :pg.name]
-             :order-by  [:%lower.pg.name]}))
+  (let [group-id->members (group-id->num-members)]
+    (for [group (ordered-groups)]
+      (assoc group :members (or (group-id->members (u/get-id group))
+                                0)))))
 
 (defendpoint GET "/group/:id"
   "Fetch the details for a certain permissions group."
diff --git a/src/metabase/api/util.clj b/src/metabase/api/util.clj
index 1f1f47b8a158e691d580cb83b312f95e2d7e6745..b03ecc5426f4ff201c9b81a59a2b87ce7cb1796a 100644
--- a/src/metabase/api/util.clj
+++ b/src/metabase/api/util.clj
@@ -2,7 +2,8 @@
   (:require [compojure.core :refer [defroutes GET POST]]
             [metabase.api.common :refer :all]
             [metabase.logger :as logger]
-            [metabase.util.schema :as su]))
+            [metabase.util.schema :as su]
+            [metabase.util.stats :as stats]))
 
 (defendpoint POST "/password_check"
   "Endpoint that checks if the supplied password meets the currently configured password complexity rules."
@@ -16,5 +17,11 @@
   (check-superuser)
   (logger/get-messages))
 
+(defendpoint GET "/stats"
+  "Anonymous usage stats. Endpoint for testing, and eventually exposing this to instance admins to let them see
+  what is being phoned home."
+  []
+  (check-superuser)
+  (stats/anonymous-usage-stats))
 
 (define-routes)
diff --git a/src/metabase/task/follow_up_emails.clj b/src/metabase/task/follow_up_emails.clj
index 9e28041e1a597b168c5e6a427961ae94f20988bc..015f1dfc9585bc58ed9375e9b21ec6be6ae154c1 100644
--- a/src/metabase/task/follow_up_emails.clj
+++ b/src/metabase/task/follow_up_emails.clj
@@ -3,9 +3,9 @@
   (:require [clojure.tools.logging :as log]
             (clj-time [coerce :as c]
                       [core :as t])
-            (clojurewerkz.quartzite [jobs :as jobs]
-                                    [triggers :as triggers])
+            [clojurewerkz.quartzite.jobs :as jobs]
             [clojurewerkz.quartzite.schedule.cron :as cron]
+            [clojurewerkz.quartzite.triggers :as triggers]
             (metabase [db :as db]
                       [email :as email])
             [metabase.email.messages :as messages]
diff --git a/src/metabase/task/send_anonymous_stats.clj b/src/metabase/task/send_anonymous_stats.clj
new file mode 100644
index 0000000000000000000000000000000000000000..c6b2143c8860fcaa278f7bbf7694c9bae3a3944d
--- /dev/null
+++ b/src/metabase/task/send_anonymous_stats.clj
@@ -0,0 +1,44 @@
+(ns metabase.task.send-anonymous-stats
+  "Contains a Metabase task which periodically sends anonymous usage information to the Metabase team."
+  (:require [clojure.tools.logging :as log]
+            [clojurewerkz.quartzite.jobs :as jobs]
+            [clojurewerkz.quartzite.schedule.cron :as cron]
+            [clojurewerkz.quartzite.triggers :as triggers]
+            (metabase [config :as config]
+                      [public-settings :as public-settings]
+                      [task :as task])
+            [metabase.util.stats :as stats]))
+
+(def ^:private ^:const job-key     "metabase.task.anonymous-stats.job")
+(def ^:private ^:const trigger-key "metabase.task.anonymous-stats.trigger")
+
+(defonce ^:private job     (atom nil))
+(defonce ^:private trigger (atom nil))
+
+;; if we can collect usage data, do so and send it home
+(jobs/defjob SendAnonymousUsageStats
+  [ctx]
+  (when (public-settings/anon-tracking-enabled)
+    (log/debug "Sending anonymous usage stats.")
+    (try
+      ;; TODO: add in additional request params if anonymous tracking is enabled
+      (stats/phone-home-stats!)
+      (catch Throwable e
+        (log/error "Error sending anonymous usage stats: " e)))))
+
+(defn task-init
+  "Job initialization"
+  []
+  ;; build our job
+  (reset! job (jobs/build
+               (jobs/of-type SendAnonymousUsageStats)
+               (jobs/with-identity (jobs/key job-key))))
+  ;; build our trigger
+  (reset! trigger (triggers/build
+                   (triggers/with-identity (triggers/key trigger-key))
+                   (triggers/start-now)
+                   (triggers/with-schedule
+                     ;; run twice a day
+                     (cron/cron-schedule "0 15 7 * * ? *"))))
+  ;; submit ourselves to the scheduler
+  (task/schedule-task! @job @trigger))
diff --git a/src/metabase/task/send_pulses.clj b/src/metabase/task/send_pulses.clj
index d709b295098334b8c2185d6552f840038a4ae910..757f22446e753dd3b547d4c7c2cfcd6444a5e87a 100644
--- a/src/metabase/task/send_pulses.clj
+++ b/src/metabase/task/send_pulses.clj
@@ -1,9 +1,9 @@
 (ns metabase.task.send-pulses
   "Tasks related to running `Pulses`."
   (:require [clojure.tools.logging :as log]
-            (clojurewerkz.quartzite [jobs :as jobs]
-                                    [triggers :as triggers])
+            [clojurewerkz.quartzite.jobs :as jobs]
             [clojurewerkz.quartzite.schedule.cron :as cron]
+            [clojurewerkz.quartzite.triggers :as triggers]
             (clj-time [core :as time]
                       [predicates :as timepr])
             (metabase.models [pulse :refer [Pulse], :as pulse]
diff --git a/src/metabase/task/sync_databases.clj b/src/metabase/task/sync_databases.clj
index f9cedbf5cb9ba248dece1f0725f0210e826a5aca..7a6daa7c7e475d935b6d28622a1bf77de5355f3a 100644
--- a/src/metabase/task/sync_databases.clj
+++ b/src/metabase/task/sync_databases.clj
@@ -1,9 +1,9 @@
 (ns metabase.task.sync-databases
   (:require [clj-time.core :as t]
             [clojure.tools.logging :as log]
-            (clojurewerkz.quartzite [jobs :as jobs]
-                                    [triggers :as triggers])
+            [clojurewerkz.quartzite.jobs :as jobs]
             [clojurewerkz.quartzite.schedule.cron :as cron]
+            [clojurewerkz.quartzite.triggers :as triggers]
             (metabase [db :as db]
                       [task :as task])
             [metabase.driver :as driver]
diff --git a/src/metabase/task/upgrade_checks.clj b/src/metabase/task/upgrade_checks.clj
index 11741d17d422a9c22fdad2043384fe02d5092c39..21a922c736d5f2bf262ea3a2d2df50d533350a6b 100644
--- a/src/metabase/task/upgrade_checks.clj
+++ b/src/metabase/task/upgrade_checks.clj
@@ -3,9 +3,9 @@
   (:require [clojure.tools.logging :as log]
             [cheshire.core :as json]
             [clj-http.client :as http]
-            (clojurewerkz.quartzite [jobs :as jobs]
-                                    [triggers :as triggers])
+            [clojurewerkz.quartzite.jobs :as jobs]
             [clojurewerkz.quartzite.schedule.cron :as cron]
+            [clojurewerkz.quartzite.triggers :as triggers]
             (metabase [config :as config]
                       [public-settings :as public-settings]
                       [task :as task])))
diff --git a/src/metabase/util/stats.clj b/src/metabase/util/stats.clj
new file mode 100644
index 0000000000000000000000000000000000000000..e3e6ddcf6c8616fc951d9e3c6329eb8727851890
--- /dev/null
+++ b/src/metabase/util/stats.clj
@@ -0,0 +1,311 @@
+(ns metabase.util.stats
+  "Functions which summarize the usage of an instance"
+  (:require [clojure.tools.logging :as log]
+            [clj-http.client :as client]
+            (metabase [config :as config]
+                      [db :as db])
+            (metabase.models [field :as field]
+                             [humanization :as humanization]
+                             [table :as table]
+                             [setting :as setting])
+            [metabase.public-settings :as public-settings]
+            [metabase.util :as u]))
+
+(def ^:private ^:const ^String metabase-usage-url "https://xuq0fbkk0j.execute-api.us-east-1.amazonaws.com/prod")
+
+(def ^:private ^Integer anonymous-id
+  "Generate an anonymous id. Don't worry too much about hash collisions or localhost cases, etc.
+   The goal is to be able to get a rough sense for how many different hosts are throwing a specific error/event."
+  (hash (str (java.net.InetAddress/getLocalHost))))
+
+
+(defn- anon-tracking-enabled?
+  "To avoid a circular reference"
+  []
+  (require 'metabase.public-settings)
+  (resolve 'metabase.public-settings/anon-tracking-enabled))
+
+
+(defn- bin-micro-number
+  "Return really small bin number. Assumes positive inputs"
+  [x]
+  (case x
+    0 "0"
+    1 "1"
+    2 "2"
+    "3+"))
+
+
+(defn- bin-small-number
+  "Return small bin number. Assumes positive inputs"
+  [x]
+  (cond
+    (= 0 x) "0"
+    (<= 1 x 5) "1-5"
+    (<= 6 x 10) "6-10"
+    (<= 11 x 25) "11-25"
+    (> x 25) "25+"))
+
+(defn- bin-medium-number
+  "Return medium bin number. Assumes positive inputs"
+  [x]
+  (cond
+    (= 0 x) "0"
+    (<= 1 x 5) "1-5"
+    (<= 6 x 10) "6-10"
+    (<= 11 x 25) "11-25"
+    (<= 26 x 50) "26-50"
+    (<= 51 x 100) "51-100"
+    (<= 101 x 250) "101-250"
+    (> x 250) "250+"))
+
+
+(defn- bin-large-number
+  "Return large bin number. Assumes positive inputs"
+  [x]
+  (cond
+    (= 0 x) "0"
+    (< x 1) "< 1"
+    (<= 1 x 10) "1-10"
+    (<= 11 x 50) "11-50"
+    (<= 51 x 250) "51-250"
+    (<= 251 x 1000) "251-1000"
+    (<= 1001 x 10000) "1001-10000"
+    (> x 10000) "10000+"))
+
+(defn- value-frequencies
+  "go through a bunch of maps and count the frequency a given key's values"
+  [many-maps k]
+  (frequencies (map k many-maps)))
+
+(defn- histogram
+  "Bin some frequencies using a passed in binning function"
+  [binning-fn many-maps k]
+  (frequencies (map binning-fn (vals (value-frequencies many-maps k)))))
+
+(def micro-histogram
+  "Return a histogram for micro numbers"
+  (partial histogram bin-micro-number))
+
+(def small-histogram
+  "Return a histogram for small numbers"
+  (partial histogram bin-small-number))
+
+(def medium-histogram
+  "Return a histogram for medium numbers"
+  (partial histogram bin-medium-number))
+
+(def large-histogram
+  "Return a histogram for large numbers"
+  (partial histogram bin-large-number))
+
+(defn- instance-start-date
+  "Pull up the first user account and use that date"
+  []
+  (db/select-one-field :date_joined 'User {:order-by [[:date_joined :desc]]}))
+
+(defn- environment-type
+  "Figure out what we're running under"
+  []
+  (cond
+    (= (config/config-str :mb-client) "OSX") :osx
+    (config/config-str :rds-hostname) :elastic-beanstalk
+    (config/config-str :database-url) :heroku ;; Putting this last as 'database-url' seems least specific
+    :default :unknown))
+
+(defn- instance-settings
+  "Figure out global info about his instance"
+  []
+  {:version               (config/mb-version-info :tag)
+   :running_on            (environment-type)
+   :application_database  (config/config-str :mb-db-type)
+   :check_for_updates     (setting/get :check-for-updates)
+   :site_name             (not= (public-settings/site-name) "Metabase")
+   :report_timezone       (setting/get :report-timezone)
+   :friendly_names        (humanization/enable-advanced-humanization)
+   :email_configured      ((resolve 'metabase.email/email-configured?))
+   :slack_configured      ((resolve 'metabase.integrations.slack/slack-configured?))
+   :sso_configured        (boolean ((resolve 'metabase.api.session/google-auth-client-id)))
+   :instance_started      (instance-start-date)
+   :has_sample_data       (db/exists? 'Database, :is_sample true)})
+
+;; util function
+(def add-summaries
+  "add up some dictionaries"
+  (partial merge-with +))
+
+;; User metrics
+(defn- user-dims
+  "characterize a user record"
+  [user]
+  {:total 1
+   :active (if (:is_active user) 1 0) ;; HOW DO I GET THE LIST OF ALL USERS INCLUDING INACTIVES?
+   :admin (if (:is_superuser user) 1 0)
+   :logged_in (if (nil? (:last_login user)) 0 1)
+   :sso (if (nil? (:google_auth user)) 0 1)})
+
+
+(defn- user-metrics
+  "Get metrics based on user records
+  TODO: get activity in terms of created questions, pulses and dashboards"
+  []
+  (let [users (db/select 'User)]
+    {:users (apply add-summaries (map user-dims users))}))
+
+
+(defn- group-metrics
+  "Get metrics based on groups:
+  TODO characterize by # w/ sql access, # of users, no self-serve data access"
+  []
+  (let [groups (db/select 'PermissionsGroup)]
+    {:groups (count groups)}))
+
+;; Artifact Metrics
+(defn- question-dims
+  "characterize a saved question
+  TODO: characterize by whether it has params, # of revisions, created by an admin"
+  [question]
+    {:total 1
+     :native (if (= (:query_type question) "native") 1 0)
+     :gui (if (not= (:query_type question) "native") 1 0)})
+
+(defn- question-metrics
+  "Get metrics based on questions
+  TODO characterize by # executions and avg latency"
+  []
+  (let [questions (db/select 'Card)]
+    {:questions (apply add-summaries (map question-dims questions))}))
+
+(defn- dashboard-metrics
+  "Get metrics based on dashboards
+  TODO characterize by # of revisions, and created by an admin"
+  []
+  (let [dashboards (db/select 'Dashboard)
+        dashcards (db/select 'DashboardCard)]
+    {:dashboards (count dashboards)
+     :num_dashs_per_user (medium-histogram dashboards :creator_id)
+     :num_cards_per_dash (medium-histogram dashcards :dashboard_id)
+     :num_dashs_per_card (medium-histogram dashcards :card_id)}))
+
+(defn- pulse-metrics
+  "Get mes based on pulses
+  TODO: characterize by non-user account emails, # emails"
+  []
+  (let [pulses (db/select 'Pulse)
+        pulsecards (db/select 'PulseCard)
+        pulsechannels (db/select 'PulseChannel)]
+    {:pulses (count pulses)
+     :pulse_types (frequencies (map :channel_type pulsechannels))
+     :pulse_schedules (frequencies (map :schedule_type pulsechannels))
+     :num_pulses_per_user (medium-histogram pulses :creator_id)
+     :num_pulses_per_card (medium-histogram pulsecards :card_id)
+     :num_cards_per_pulses (medium-histogram pulsecards :pulse_id)}))
+
+
+(defn- label-metrics
+  "Get metrics based on labels"
+  []
+  (let [labels (db/select 'CardLabel)]
+    {:labels (count labels)
+     :num_labels_per_card (micro-histogram labels :card_id)
+     :num_cards_per_label (medium-histogram labels :label_id)}))
+
+;; Metadata Metrics
+(defn- database-dims
+  "characterize a database record"
+  [database]
+  {:total 1
+   :analyzed (if (:is_full_sync database) 1 0)})
+
+(defn- database-metrics
+  "Get metrics based on databases"
+  []
+  (let [databases (db/select 'Database)]
+    {:databases (apply add-summaries (map database-dims databases))}))
+
+
+(defn- table-metrics
+  "Get metrics based on tables"
+  []
+  (let [tables (db/select 'Table)]
+    {:tables (count tables)
+     :num_per_database (medium-histogram tables :db_id)
+     :num_per_schema (medium-histogram tables :schema)}))
+
+
+(defn- field-metrics
+  "Get metrics based on fields"
+  []
+  (let [fields (db/select 'Field)]
+    {:fields (count fields)
+     :num_per_table (medium-histogram fields :table_id)}))
+
+(defn- segment-metrics
+  "Get metrics based on segments"
+  []
+  (let [segments (db/select 'Segment)]
+    {:segments (count segments)}))
+
+
+(defn- metric-metrics
+  "Get metrics based on metrics"
+  []
+  (let [metrics (db/select 'Metric)]
+    {:metrics (count metrics)}))
+
+
+(defn- bin-latencies
+  "Bin latencies, which are in milliseconds"
+  [query-executions]
+  (let [latency-vals (map #(/ % 1000) (map :running_time query-executions))]
+    (frequencies (map bin-large-number latency-vals))))
+
+
+;; Execution Metrics
+(defn- execution-metrics
+  "Get metrics based on executions.
+  This should be done in a single pass, as there might
+  be a LOT of query executions in a normal instance
+  TODO: characterize by ad hoc vs cards"
+  []
+  (let [executions (db/select ['QueryExecution :executor_id :running_time :status])]
+    {:executions (count executions)
+     :by_status (frequencies (map :status executions))
+     :num_per_user (large-histogram executions :executor_id)
+     :num_by_latency (bin-latencies executions)}))
+
+
+(defn anonymous-usage-stats
+  "generate a map of the usage stats for this instance"
+  []
+  (merge (instance-settings)
+           {:uuid anonymous-id :timestamp (new java.util.Date)}
+            {:stats {:user (user-metrics)
+                     :question (question-metrics)
+                     :dashboard (dashboard-metrics)
+                     :database (database-metrics)
+                     :table (table-metrics)
+                     :field (field-metrics)
+                     :pulse (pulse-metrics)
+                     :segment (segment-metrics)
+                     :metric (metric-metrics)
+                     :group (group-metrics)
+                     :label (label-metrics)
+                     :execution (execution-metrics)}}))
+
+
+(defn- send-stats!
+  "send stats to Metabase tracking server"
+  [stats]
+   (try
+      (client/post metabase-usage-url {:form-params stats, :content-type :json, :throw-entire-message? true})
+      (catch Throwable e
+       (log/error "Sending usage stats FAILED: " (.getMessage e)))))
+
+
+(defn phone-home-stats!
+  "Collect usage stats and phone them home"
+  []
+  (when (anon-tracking-enabled?)
+    (let [stats (anonymous-usage-stats)]
+      (send-stats! stats))))
\ No newline at end of file
diff --git a/test/metabase/api/permissions_test.clj b/test/metabase/api/permissions_test.clj
index 989a16b1f2812c3085d97ed4926c8c893b997682..e0a242cc6e7e05b91959c93c316089b3d791b77e 100644
--- a/test/metabase/api/permissions_test.clj
+++ b/test/metabase/api/permissions_test.clj
@@ -1,28 +1,39 @@
 (ns metabase.api.permissions-test
-  "Tests for `api/permissions` endpoints."
+  "Tests for `/api/permissions` endpoints."
   (:require [expectations :refer :all]
-            [metabase.test.data.users :as tu]
-            [metabase.models.permissions-group :as group]
+            [metabase.models.permissions-group :refer [PermissionsGroup], :as group]
+            [metabase.test.data.users :as test-users]
+            [metabase.test.util :as tu]
             [metabase.util :as u]))
 
 
-;; GET /group
+;; GET /permissions/group
 ;; Should *not* include inactive users in the counts.
 ;; It should also *not* include the MetaBot group because MetaBot should *not* be enabled
+(defn- fetch-groups []
+  (test-users/delete-temp-users!)
+  (set ((test-users/user->client :crowberto) :get 200 "permissions/group")))
+
 (expect
   #{{:id (u/get-id (group/all-users)), :name "All Users",      :members 3}
     {:id (u/get-id (group/admin)),     :name "Administrators", :members 1}}
-  (do
-    (tu/delete-temp-users!)
-    (set ((tu/user->client :crowberto) :get 200 "permissions/group"))))
+  (fetch-groups))
+
+;; The endpoint should however return empty groups!
+(tu/expect-with-temp [PermissionsGroup [group]]
+  #{{:id (u/get-id (group/all-users)), :name "All Users",      :members 3}
+    {:id (u/get-id (group/admin)),     :name "Administrators", :members 1}
+    (assoc (into {} group) :members 0)}
+  (fetch-groups))
+
 
-;; GET /group/:id
+;; GET /permissions/group/:id
 ;; Should *not* include inactive users
 (expect
-  #{{:first_name "Crowberto", :last_name "Corv",   :email "crowberto@metabase.com", :user_id (tu/user->id :crowberto), :membership_id true}
-    {:first_name "Lucky",     :last_name "Pigeon", :email "lucky@metabase.com",     :user_id (tu/user->id :lucky),     :membership_id true}
-    {:first_name "Rasta",     :last_name "Toucan", :email "rasta@metabase.com",     :user_id (tu/user->id :rasta),     :membership_id true}}
+  #{{:first_name "Crowberto", :last_name "Corv",   :email "crowberto@metabase.com", :user_id (test-users/user->id :crowberto), :membership_id true}
+    {:first_name "Lucky",     :last_name "Pigeon", :email "lucky@metabase.com",     :user_id (test-users/user->id :lucky),     :membership_id true}
+    {:first_name "Rasta",     :last_name "Toucan", :email "rasta@metabase.com",     :user_id (test-users/user->id :rasta),     :membership_id true}}
   (do
-    (tu/delete-temp-users!)
-    (set (for [member (:members ((tu/user->client :crowberto) :get 200 (str "permissions/group/" (u/get-id (group/all-users)))))]
+    (test-users/delete-temp-users!)
+    (set (for [member (:members ((test-users/user->client :crowberto) :get 200 (str "permissions/group/" (u/get-id (group/all-users)))))]
            (update member :membership_id (complement nil?))))))
diff --git a/test/metabase/util/stats_test.clj b/test/metabase/util/stats_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..e1c86909aca5746ae1dc5ef6fd0c5f98051caddb
--- /dev/null
+++ b/test/metabase/util/stats_test.clj
@@ -0,0 +1,67 @@
+(ns metabase.util.stats-test
+  (:require [expectations :refer :all]
+            [metabase.util.stats :refer :all]
+            [metabase.test.util :as tu]))
+
+(tu/resolve-private-vars metabase.util.stats
+  bin-micro-number bin-small-number bin-medium-number bin-large-number)
+
+
+(expect "0" (bin-micro-number 0))
+(expect "1" (bin-micro-number 1))
+(expect "2" (bin-micro-number 2))
+(expect "3+" (bin-micro-number 3))
+(expect "3+" (bin-micro-number 100))
+
+
+(expect "0" (bin-small-number 0))
+(expect "1-5" (bin-small-number 1))
+(expect "1-5" (bin-small-number 5))
+(expect "6-10" (bin-small-number 6))
+(expect "6-10" (bin-small-number 10))
+(expect "11-25" (bin-small-number 11))
+(expect "11-25" (bin-small-number 25))
+(expect "25+" (bin-small-number 26))
+(expect "25+" (bin-small-number 500))
+
+(expect "0" (bin-medium-number 0))
+(expect "1-5" (bin-medium-number 1))
+(expect "1-5" (bin-medium-number 5))
+(expect "6-10" (bin-medium-number 6))
+(expect "6-10" (bin-medium-number 10))
+(expect "11-25" (bin-medium-number 11))
+(expect "11-25" (bin-medium-number 25))
+(expect "26-50" (bin-medium-number 26))
+(expect "26-50" (bin-medium-number 50))
+(expect "51-100" (bin-medium-number 51))
+(expect "51-100" (bin-medium-number 100))
+(expect "101-250" (bin-medium-number 101))
+(expect "101-250" (bin-medium-number 250))
+(expect "250+" (bin-medium-number 251))
+(expect "250+" (bin-medium-number 5000))
+
+
+(expect "0" (bin-large-number 0))
+(expect "1-10" (bin-large-number 1))
+(expect "1-10" (bin-large-number 10))
+
+(expect "11-50" (bin-large-number 11))
+(expect "11-50" (bin-large-number 50))
+(expect "51-250" (bin-large-number 51))
+(expect "51-250" (bin-large-number 250))
+(expect "251-1000" (bin-large-number 251))
+(expect "251-1000" (bin-large-number 1000))
+(expect "1001-10000" (bin-large-number 1001))
+(expect "1001-10000" (bin-large-number 10000))
+(expect "10000+" (bin-large-number 10001))
+(expect "10000+" (bin-large-number 100000))
+
+
+(expect :unknown ((anonymous-usage-stats) :running_on))
+(expect true ((anonymous-usage-stats) :check_for_updates))
+(expect true ((anonymous-usage-stats) :site_name))
+(expect true ((anonymous-usage-stats) :friendly_names))
+(expect false ((anonymous-usage-stats) :email_configured))
+(expect false ((anonymous-usage-stats) :slack_configured))
+(expect false ((anonymous-usage-stats) :sso_configured))
+(expect false ((anonymous-usage-stats) :has_sample_data))
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 35ce5ff9c9fd4053113b6dcf154148e83b548f6c..96befc4141c2cc3b27c2cf9df919f54d3598b50f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -168,6 +168,10 @@ ansi-html@0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.5.tgz#0dcaa5a081206866bc240a3b773a184ea3b88b64"
 
+ansi-regex@^0.2.0, ansi-regex@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
+
 ansi-regex@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107"
@@ -178,6 +182,10 @@ ansi-style-parser@^1.0.1:
   dependencies:
     ansi-regex "^2.0.0"
 
+ansi-styles@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
+
 ansi-styles@^2.1.0, ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@@ -1181,6 +1189,10 @@ block-stream@*:
   dependencies:
     inherits "~2.0.0"
 
+bluebird@2.9.6:
+  version "2.9.6"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.6.tgz#1fc3a6b1685267dc121b5ec89b32ce069d81ab7d"
+
 bluebird@^2.9.27:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
@@ -1358,6 +1370,16 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
+chalk@0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
+  dependencies:
+    ansi-styles "^1.1.0"
+    escape-string-regexp "^1.0.0"
+    has-ansi "^0.1.0"
+    strip-ansi "^0.3.0"
+    supports-color "^0.2.0"
+
 chalk@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.1.tgz#509afb67066e7499f7eb3535c77445772ae2d019"
@@ -1573,6 +1595,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
+
 commander@2.8.x:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
@@ -1634,6 +1660,19 @@ concat-stream@^1.4.6:
     readable-stream "~2.0.0"
     typedarray "~0.0.5"
 
+concurrently:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.1.0.tgz#dc5ef0459090012604756668894c04b434ef90d1"
+  dependencies:
+    bluebird "2.9.6"
+    chalk "0.5.1"
+    commander "2.6.0"
+    lodash "^4.5.1"
+    moment "^2.11.2"
+    rx "2.3.24"
+    spawn-default-shell "^1.1.0"
+    tree-kill "^1.1.0"
+
 connect-history-api-fallback@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
@@ -2291,7 +2330,7 @@ escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
@@ -2991,6 +3030,12 @@ har-validator@~2.0.6:
     is-my-json-valid "^2.12.4"
     pinkie-promise "^2.0.0"
 
+has-ansi@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
+  dependencies:
+    ansi-regex "^0.2.0"
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4126,7 +4171,7 @@ lodash@3.10.1, lodash@^3.7.0, lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.11.2, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+lodash@^4.0.0, lodash@^4.11.2, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
   version "4.16.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.4.tgz#01ce306b9bad1319f2a5528674f88297aeb70127"
 
@@ -4334,7 +4379,7 @@ mobx@^2.3.4:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/mobx/-/mobx-2.6.0.tgz#0ae83a20488b92d10d4ca326e18fe78a5ab7cb36"
 
-moment@2.14.1:
+moment@2.14.1, moment@^2.11.2:
   version "2.14.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.14.1.tgz#b35b27c47e57ed2ddc70053d6b07becdb291741c"
 
@@ -5512,12 +5557,18 @@ react-ansi-style@^1.0.0:
   version "15.3.2"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"
 
-react-draggable, react-draggable@^2.1.0:
+react-draggable@^2.1.0:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-2.2.2.tgz#80932da5ad81795bdfd141e846d7a9309680e270"
   dependencies:
     classnames "^2.2.5"
 
+react-draggable@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-2.2.3.tgz#17628cb8aaefed639d38e0021b978a685d80b08b"
+  dependencies:
+    classnames "^2.2.5"
+
 react-fuzzy@^0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/react-fuzzy/-/react-fuzzy-0.2.3.tgz#4fa08729524cd491e2b589509e8d2de7e3adfb9e"
@@ -5616,9 +5667,9 @@ react-sortable@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.1.0.tgz#022360e2ebee6a75c81fc2b48debb37c28ce7566"
 
-react-virtualized@^8.5.3:
-  version "8.5.3"
-  resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-8.5.3.tgz#52d06c67b8dfb4e8800b7b91b6aa20d4e842cf7a"
+react-virtualized@^8.6.0:
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-8.6.0.tgz#b54402ad5c75d3cd15481dc822de7f816d57da22"
   dependencies:
     babel-runtime "^6.11.6"
     classnames "^2.2.3"
@@ -6019,6 +6070,10 @@ rx-lite@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
 
+rx@2.3.24:
+  version "2.3.24"
+  resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
+
 sane@^1.3.3:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715"
@@ -6314,6 +6369,10 @@ source-map@~0.2.0:
   dependencies:
     amdefine ">=0.0.4"
 
+spawn-default-shell@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/spawn-default-shell/-/spawn-default-shell-1.1.0.tgz#095439d44c4b7c0aff56a53929fbaab87878e7c6"
+
 spdx-correct@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -6413,6 +6472,12 @@ stringstream@~0.0.4:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
 
+strip-ansi@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
+  dependencies:
+    ansi-regex "^0.2.1"
+
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -6445,6 +6510,10 @@ style-loader@^0.13.0:
   dependencies:
     loader-utils "^0.2.7"
 
+supports-color@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
+
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -6643,6 +6712,10 @@ traceur@0.0.105:
     semver "^4.3.3"
     source-map-support "~0.2.8"
 
+tree-kill@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.1.0.tgz#c963dcf03722892ec59cba569e940b71954d1729"
+
 trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"