diff --git a/.gitignore b/.gitignore
index dff199f7db092831450b3e700dbe28f4cc6e632f..a45c2e8a8167fb7f89916af744378f5b3058171c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,6 @@ bin/release/aws-eb/metabase-aws-eb.zip
 /locales/metabase-*.pot
 /stats.json
 coverage-summary.json
+.DS_Store
+bin/node_modules/
+*.log
\ No newline at end of file
diff --git a/docs/developers-guide.md b/docs/developers-guide.md
index 64958e5cfc3df9b13f9bf64769df6d3e81792d98..526e3a3215e0751668252282fb2e638971fcc0b7 100644
--- a/docs/developers-guide.md
+++ b/docs/developers-guide.md
@@ -113,7 +113,7 @@ All frontend tests are located in `frontend/test` directory. Run all frontend te
 ./bin/build version uberjar && yarn run test
 ```
 
-which will first build the backend JAR and then run integration, unit and Karma browser tests in sequence. 
+which will first build the backend JAR and then run integration, unit and Karma browser tests in sequence.
 
 ### Jest integration tests
 Integration tests simulate realistic sequences of user interactions. They render a complete DOM tree using [Enzyme](http://airbnb.io/enzyme/docs/api/index.html) and use temporary backend instances for executing API calls.
@@ -152,7 +152,7 @@ describe("Query builder", () => {
     })
 
     it("should let you run a new query", async () => {
-        // Create a superpowered Redux store. 
+        // Create a superpowered Redux store.
         // Remember `await` here!
         const store = await createTestStore()
 
@@ -192,17 +192,17 @@ You can also skim through [`__support__/integrated_tests.js`](https://github.com
 
 ### Jest unit tests
 
-Unit tests are focused around isolated parts of business logic. 
+Unit tests are focused around isolated parts of business logic.
 
 Unit tests use an enforced file naming convention `<test-suite-name>.unit.js` to separate them from integration tests.
 
 ```
-yarn run jest-test # Run all tests at once
-yarn run jest-test-watch # Watch for file changes
+yarn run test-unit # Run all tests at once
+yarn run test-unit-watch # Watch for file changes
 ```
 
 ### Karma browser tests
-If you need to test code which uses browser APIs that are only available in real browsers, you can add a Karma test to `frontend/test/legacy-karma` directory. 
+If you need to test code which uses browser APIs that are only available in real browsers, you can add a Karma test to `frontend/test/legacy-karma` directory.
 
 ```
 yarn run test-karma # Run all tests once
diff --git a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
index 03022d287bc3af57e72a9e242113f992ed5b9db4..77c02e6a6ce737796d8f171a4383b950f7f15042 100644
--- a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
@@ -306,7 +306,7 @@ export default class PeopleListingApp extends Component {
 
         return (
             <Modal small
-                title={user.first_name+"'s Password Has Been Reset"}
+                title={user.first_name+"'s password has been reset"}
                 footer={<button className="Button Button--primary mr2" onClick={this.onCloseModal}>Done</button>}
                 onClose={this.onCloseModal}
             >
@@ -325,7 +325,7 @@ export default class PeopleListingApp extends Component {
         return (
             <Modal
                 small
-                title={user.first_name+"'s Password Has Been Reset"}
+                title={user.first_name+"'s password has been reset"}
                 footer={<Button primary onClick={this.onCloseModal}>Done</Button>}
                 onClose={this.onCloseModal}
             >
diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
index a74be885760e404ad3e38bd688c978246895d7ae..19b99f7a9691ff9e2fa46134d84b5a733a8b7333 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
@@ -195,7 +195,7 @@ export default class SettingsEmailForm extends Component {
                 <SettingsSetting
                     key={element.key}
                     setting={{ ...element, value }}
-                    updateSetting={this.handleChangeEvent.bind(this, element)}
+                    onChange={this.handleChangeEvent.bind(this, element)}
                     errorMessage={errorMessage}
                 />
             );
diff --git a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
index 9a98e719c64a9be46b088a255e73668e7c1c1b8f..cbb78eba9fe9d1b8dc7076cedf5b4bbf06258439 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
@@ -186,7 +186,7 @@ export default class SettingsLdapForm extends Component {
                     <SettingsSetting
                         key={element.key}
                         setting={{ ...element, value }}
-                        updateSetting={this.handleChangeEvent.bind(this, element)}
+                        onChange={this.handleChangeEvent.bind(this, element)}
                         mappings={formData['ldap-group-mappings']}
                         updateMappings={this.handleChangeEvent.bind(this, { key: 'ldap-group-mappings' })}
                         errorMessage={errorMessage}
@@ -198,7 +198,7 @@ export default class SettingsLdapForm extends Component {
                 <SettingsSetting
                     key={element.key}
                     setting={{ ...element, value }}
-                    updateSetting={this.handleChangeEvent.bind(this, element)}
+                    onChange={this.handleChangeEvent.bind(this, element)}
                     errorMessage={errorMessage}
                 />
             );
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
index 6adc903198c980c0e42352a9c0d225b5e912d6eb..8e3a63ffcd992d650a0981dd57e849a6c2956d87 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
@@ -22,17 +22,18 @@ const SETTING_WIDGET_MAP = {
 
 const updatePlaceholderForEnvironmentVars = (props) => {
     if (props && props.setting && props.setting.is_env_setting){
-        return assocIn(props, ["setting", "placeholder"], "Using " + props.setting.env_name) 
+        return assocIn(props, ["setting", "placeholder"], "Using " + props.setting.env_name)
     }
     return props
 }
 
 export default class SettingsSetting extends Component {
-    
+
 
     static propTypes = {
         setting: PropTypes.object.isRequired,
-        updateSetting: PropTypes.func.isRequired,
+        onChange: PropTypes.func.isRequired,
+        onChangeSetting: PropTypes.func,
         autoFocus: PropTypes.bool,
         disabled: PropTypes.bool,
     };
@@ -50,7 +51,7 @@ export default class SettingsSetting extends Component {
                     <SettingHeader setting={setting} />
                 }
                 <div className="flex">
-                    <Widget {...updatePlaceholderForEnvironmentVars(this.props)} 
+                    <Widget {...updatePlaceholderForEnvironmentVars(this.props)}
                     />
                 </div>
                 { errorMessage &&
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
index 221d5e6514ec57ed6526949da21fcfcdf2accf7d..b4fcbdfb7ca1a141ccb10e17198c1abbc318f455 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
@@ -166,7 +166,7 @@ export default class SettingsSlackForm extends Component {
                     <SettingsSetting
                         key={element.key}
                         setting={{ ...element, value }}
-                        updateSetting={(value) => this.handleChangeEvent(element, value)}
+                        onChange={(value) => this.handleChangeEvent(element, value)}
                         errorMessage={errorMessage}
                         fireOnChange
                     />
@@ -176,7 +176,7 @@ export default class SettingsSlackForm extends Component {
                     <SettingsSetting
                         key={element.key}
                         setting={{ ...element, value }}
-                        updateSetting={(value) => this.handleChangeEvent(element, value)}
+                        onChange={(value) => this.handleChangeEvent(element, value)}
                         errorMessage={errorMessage}
                         disabled={!this.state.formData["slack-token"]}
                     />
diff --git a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
index 1b137d567f2aaa9a04b20395e11a6ff6d883ec87..512a1573ff3fabede90c331491227495a57f1706 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
@@ -103,7 +103,7 @@ export default class SettingsUpdatesForm extends Component {
             <SettingsSetting
                 key={setting.key}
                 setting={setting}
-                updateSetting={(value) => updateSetting(setting, value)}
+                onChange={(value) => updateSetting(setting, value)}
                 autoFocus={index === 0}
             />
         );
diff --git a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
index 01f0e7738e80f86f6f723d0e36821c242e6267e5..f59991aca7288b697ed30661cdbd442f8d28d9bc 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
@@ -25,7 +25,7 @@ const SettingsXrayForm = ({ settings, elements, updateSetting }) => {
                 <SettingsSetting
                     key={enabled.key}
                     setting={enabled}
-                    updateSetting={(value) => updateSetting(enabled, value)}
+                    onChange={(value) => updateSetting(enabled, value)}
                 />
             </ol>
 
diff --git a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
index 4164092608824599e1bc3431c749e9aeeae1e11b..bb55c02274df6d7f5979e5691a9a05de926e27f5 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
@@ -33,7 +33,6 @@ export default class CustomGeoJSONWidget extends Component {
 
     static propTypes = {
         setting: PropTypes.object.isRequired,
-        updateSetting: PropTypes.func.isRequired,
         reloadSettings: PropTypes.func.isRequired
     };
     static defaultProps = {};
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
index 6ef4cea0940b0674af77500e30e371dedabe732b..cf4b4dd1c77702fa502f8331cc44f21c3d2f0890 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
@@ -1,25 +1,21 @@
 import React from 'react';
 import MetabaseAnalytics from 'metabase/lib/analytics';
 
-const EmbeddingLegalese = ({ updateSetting }) =>
+const EmbeddingLegalese = ({ onChange }) =>
     <div className="bordered rounded text-measure p4">
         <h3 className="text-brand">Using embedding</h3>
         <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
             By enabling embedding you're agreeing to the embedding license located at <a className="link"  href="http://www.metabase.com/license/embedding" target="_blank">metabase.com/license/embedding</a>.
         </p>
-        <p className="text-grey-4" style={{ lineHeight: 1.48 }}>            In plain english, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds.
-            You should however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.
+        <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
+            In plain english, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds. You should however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.
         </p>
-        {/* TODO: contact form link */}
-        {/* <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
-            If you want to hide the Metabase logo inside your own application we'd love to get in touch.
-        </p> */}
         <div className="flex layout-centered mt4">
             <button
                 className="Button Button--primary"
                 onClick={() => {
                     MetabaseAnalytics.trackEvent("Admin Embed Settings", "Embedding Enable Click");
-                    updateSetting(true);
+                    onChange(true);
                 }}
             >
                 Enable
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4c7d5d069be77f8e4413f977d34c38f0f9d7793a
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import ReactRetinaImage from 'react-retina-image'
+
+import SettingsInput from "./SettingInput"
+
+const PREMIUM_EMBEDDING_STORE_URL = 'https://store.metabase.com/product/embedding'
+const PREMIUM_EMBEDDING_SETTING_KEY = 'premium-embedding-token'
+
+const PremiumTokenInput = ({ token, onChangeSetting }) =>
+    <div className="mb3">
+        <h3 className="mb1">
+            { token
+                ? `Premium embedding enabled`
+                : `Enter the token you bought from the Metabase Store`
+            }
+        </h3>
+        <SettingsInput
+            onChange={(value) =>
+                onChangeSetting(PREMIUM_EMBEDDING_SETTING_KEY, value)
+            }
+            setting={{ value: token }}
+            autoFocus={!token}
+        />
+    </div>
+
+const PremiumExplanation = ({ showEnterScreen }) =>
+    <div>
+        <h2>Premium embedding</h2>
+        <p className="mt1">Premium embedding lets you disable "Powered by Metabase" on your embeded dashboards and questions.</p>
+        <div className="mt2 mb3">
+            <a className="link mx1" href={PREMIUM_EMBEDDING_STORE_URL} target="_blank">
+                Buy a token
+            </a>
+            <a className="link mx1" onClick={showEnterScreen}>
+                Enter a token
+            </a>
+        </div>
+    </div>
+
+class PremiumEmbedding extends Component {
+    constructor(props) {
+        super(props)
+        this.state = {
+            showEnterScreen: props.token
+        }
+    }
+    render () {
+        const { token, onChangeSetting } = this.props
+        const { showEnterScreen } = this.state
+
+        return (
+            <div className="text-centered text-paragraph">
+                { showEnterScreen
+                    ? (
+                        <PremiumTokenInput
+                            onChangeSetting={onChangeSetting}
+                            token={token}
+                        />
+                    )
+                    : (
+                        <PremiumExplanation
+                            showEnterScreen={() =>
+                                this.setState({ showEnterScreen: true })
+                            }
+                        />
+                    )
+                }
+            </div>
+        )
+    }
+}
+
+class EmbeddingLevel extends Component {
+    render () {
+        const { onChangeSetting, settingValues } = this.props
+
+        const premiumToken = settingValues[PREMIUM_EMBEDDING_SETTING_KEY]
+
+        return (
+            <div className="bordered rounded full text-centered" style={{ maxWidth: 820 }}>
+                <ReactRetinaImage src={`app/assets/img/${ premiumToken ? 'premium_embed_added' : 'premium_embed'}.png`}/>
+                <div className="flex align-center justify-center">
+                    <PremiumEmbedding
+                        token={premiumToken}
+                        onChangeSetting={onChangeSetting}
+                    />
+                </div>
+            </div>
+        )
+    }
+}
+
+export default EmbeddingLevel
diff --git a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
index 660cd988163f5024d59fe5e9816522a27bafc9f6..ae83d27ba13d8129190632d07ea42643e809f87b 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
@@ -20,7 +20,7 @@ import SettingToggle from './SettingToggle';
 
 type Props = {
     setting: any,
-    updateSetting: (value: any) => void,
+    onChange: (value: any) => void,
     mappings: { [string]: number[] },
     updateMappings: (value: { [string]: number[] }) => void
 };
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2394a20a3404bf0e0ec664dca7f523c6ada93d76
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx
@@ -0,0 +1,21 @@
+import React, { Component } from "react";
+
+import MetabaseSettings from "metabase/lib/settings";
+
+import SettingInput from "./SettingInput.jsx";
+
+export default class PremiumEmbeddingWidget extends Component {
+    render() {
+        return (
+            <div>
+                <SettingInput {...this.props} />
+                <h3 className="mt4 mb4">
+                    Getting your very own premium embedding token
+                </h3>
+                <a href={MetabaseSettings.metastoreUrl()} target="_blank">
+                    Visit the MetaStore
+                </a>
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 8e2d000e936987e87427bf8a971eca2180eb7abb..b5a279a48e97219a50e3947db57a81e9382fa465 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -170,17 +170,21 @@ export const PublicLinksQuestionListing = () =>
     />;
 
 export const EmbeddedDashboardListing = () =>
-    <PublicLinksListing
-        load={DashboardApi.listEmbeddable}
-        getUrl={({ id }) => Urls.dashboard(id)}
-        type='Embedded Dashboard Listing'
-        noLinksMessage="No dashboards have been embedded yet."
-    />;
+    <div className="bordered rounded full" style={{ maxWidth: 820 }}>
+        <PublicLinksListing
+            load={DashboardApi.listEmbeddable}
+            getUrl={({ id }) => Urls.dashboard(id)}
+            type='Embedded Dashboard Listing'
+            noLinksMessage="No dashboards have been embedded yet."
+        />
+    </div>
 
 export const EmbeddedQuestionListing = () =>
-    <PublicLinksListing
-        load={CardApi.listEmbeddable}
-        getUrl={({ id }) => Urls.question(id)}
-        type='Embedded Card Listing'
-        noLinksMessage="No questions have been embedded yet."
-    />;
+    <div className="bordered rounded full" style={{ maxWidth: 820 }}>
+        <PublicLinksListing
+            load={CardApi.listEmbeddable}
+            getUrl={({ id }) => Urls.question(id)}
+            type='Embedded Card Listing'
+            noLinksMessage="No questions have been embedded yet."
+        />
+    </div>
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
index ec1a2b0fae7a218958d6992fc9eee32d8ad64c0a..014123be2b18f24cecdc6c85e0367907e4e18691 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
@@ -9,7 +9,7 @@ import Confirm from "metabase/components/Confirm";
 import { UtilApi } from "metabase/services";
 
 type Props = {
-    updateSetting: (value: any) => void,
+    onChange: (value: any) => void,
     setting: {}
 };
 
@@ -17,15 +17,15 @@ export default class SecretKeyWidget extends Component {
     props: Props;
 
     _generateToken = async () => {
-        const { updateSetting } = this.props;
+        const { onChange } = this.props;
         const result = await UtilApi.random_token();
-        updateSetting(result.token);
+        onChange(result.token);
     }
 
     render() {
         const { setting } = this.props;
         return (
-            <div className="flex align-center">
+            <div className="p2 flex align-center full bordered rounded" style={{ maxWidth: 820 }}>
                 <SettingInput {...this.props} />
                 { setting.value ?
                     <Confirm
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
index bffd0c9279b6837a06a291a071be7862e2f86b78..b88888d44b26da696f51b79f0e3e23b7e0f85a1e 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
@@ -3,7 +3,7 @@ import React from "react";
 import Input from "metabase/components/Input.jsx";
 import cx from "classnames";
 
-const SettingInput = ({ setting, updateSetting, disabled, autoFocus, errorMessage, fireOnChange, type = "text" }) =>
+const SettingInput = ({ setting, onChange, disabled, autoFocus, errorMessage, fireOnChange, type = "text" }) =>
     <Input
         className={cx(" AdminInput bordered rounded h3", {
             "SettingsInput": type !== "password",
@@ -13,8 +13,8 @@ const SettingInput = ({ setting, updateSetting, disabled, autoFocus, errorMessag
         type={type}
         value={setting.value || ""}
         placeholder={setting.placeholder}
-        onChange={fireOnChange ? (e) => updateSetting(e.target.value) : null }
-        onBlurChange={!fireOnChange ? (e) => updateSetting(e.target.value) : null }
+        onChange={fireOnChange ? (e) => onChange(e.target.value) : null }
+        onBlurChange={!fireOnChange ? (e) => onChange(e.target.value) : null }
         autoFocus={autoFocus}
     />
 
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
index f255a0e69f5c9f71e0a3c989766d84b8cd16b14d..1f796f5aa8279b3275ea82f9a9f23074b637f1c0 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
@@ -3,11 +3,11 @@ import React from "react";
 import Radio from "metabase/components/Radio";
 import cx from "classnames";
 
-const SettingRadio = ({ setting, updateSetting, disabled }) =>
+const SettingRadio = ({ setting, onChange, disabled }) =>
     <Radio
         className={cx({ "disabled": disabled })}
         value={setting.value}
-        onChange={(value) => updateSetting(value)}
+        onChange={(value) => onChange(value)}
         options={Object.entries(setting.options).map(([value, name]) => ({ name, value }))}
     />
 
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
index 3c6da5871b3cb1ea14e9fa9cef34c6a5ad8673e3..028cff5bda5030e7bc4a0cdc4ac1132fbb2a907a 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
@@ -3,13 +3,13 @@ import React from "react";
 import Select from "metabase/components/Select.jsx";
 import _ from "underscore";
 
-const SettingSelect = ({ setting, updateSetting, disabled }) =>
+const SettingSelect = ({ setting, onChange, disabled }) =>
     <Select
         className="full-width"
         placeholder={setting.placeholder}
         value={_.findWhere(setting.options, { value: setting.value }) || setting.value}
         options={setting.options}
-        onChange={updateSetting}
+        onChange={onChange}
         optionNameFn={option => typeof option === "object" ? option.name : option }
         optionValueFn={option => typeof option === "object" ? option.value : option }
     />
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
index f924f4719bd5396f2c56149a9c7e6bfac032e076..591c2f9f0eba8a67442bc65c14d4a14b05c93524 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
@@ -2,12 +2,12 @@ import React from "react";
 
 import Toggle from "metabase/components/Toggle.jsx";
 
-const SettingToggle = ({ setting, updateSetting, disabled }) => {
+const SettingToggle = ({ setting, onChange, disabled }) => {
     const value = setting.value == null ? setting.default : setting.value;
     const on = value === true || value === "true";
     return (
         <div className="flex align-center pt1">
-            <Toggle value={on} onChange={!disabled ? () => updateSetting(!on) : null}/>
+            <Toggle value={on} onChange={!disabled ? () => onChange(!on) : null}/>
             <span className="text-bold mx1">{on ? "Enabled" : "Disabled"}</span>
         </div>
     );
diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
index bb685cfcef94f1927a401a01d8be81e3751e7012..7959d2ae447e06c920fbf679741097da6df10f7d 100644
--- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
+++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
@@ -66,7 +66,7 @@ export default class SettingsEditorApp extends Component {
     }
 
     updateSetting = async (setting, newValue) => {
-        const { settings, settingValues, updateSetting } = this.props;
+        const { settingValues, updateSetting } = this.props;
 
         this.layout.setSaving();
 
@@ -78,14 +78,7 @@ export default class SettingsEditorApp extends Component {
             await updateSetting(setting);
 
             if (setting.onChanged) {
-                await setting.onChanged(oldValue, newValue, settingValues, (key, value) => {
-                    let setting = _.findWhere(settings, { key });
-                    if (!setting) {
-                        throw new Error("Unknown setting " + key);
-                    }
-                    setting.value = value;
-                    return updateSetting(setting);
-                })
+                await setting.onChanged(oldValue, newValue, settingValues, this.handleChangeSetting)
             }
 
             this.layout.setSaved();
@@ -106,6 +99,15 @@ export default class SettingsEditorApp extends Component {
         }
     }
 
+    handleChangeSetting = (key, value) => {
+        const { settings, updateSetting } = this.props;
+        const setting = _.findWhere(settings, { key });
+        if (!setting) {
+            throw new Error("Unknown setting " + key);
+        }
+        return updateSetting({ ...setting, value });
+    }
+
     renderSettingsPane() {
         const { activeSection, settingValues } = this.props;
 
@@ -187,7 +189,8 @@ export default class SettingsEditorApp extends Component {
                         <SettingsSetting
                             key={setting.key}
                             setting={setting}
-                            updateSetting={this.updateSetting.bind(this, setting)}
+                            onChange={this.updateSetting.bind(this, setting)}
+                            onChangeSetting={this.handleChangeSetting}
                             reloadSettings={this.props.reloadSettings}
                             autoFocus={index === 0}
                             settingValues={settingValues}
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 754b9b2fa4b959dcc6dbb30f49655562838f761f..7b3f258be3feb1c0d45b9182b5a6c3b5346a4ecf 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -12,6 +12,7 @@ import {
 } from "./components/widgets/PublicLinksListing.jsx";
 import SecretKeyWidget from "./components/widgets/SecretKeyWidget.jsx";
 import EmbeddingLegalese from "./components/widgets/EmbeddingLegalese";
+import EmbeddingLevel from "./components/widgets/EmbeddingLevel";
 import LdapGroupMappingsWidget from "./components/widgets/LdapGroupMappingsWidget";
 
 import { UtilApi } from "metabase/services";
@@ -310,19 +311,23 @@ const SECTIONS = [
                 description: null,
                 widget: EmbeddingLegalese,
                 getHidden: (settings) => settings["enable-embedding"],
-                onChanged: async (oldValue, newValue, settingsValues, onChange) => {
+                onChanged: async (oldValue, newValue, settingsValues, onChangeSetting) => {
+                    // Generate a secret key if none already exists
                     if (!oldValue && newValue && !settingsValues["embedding-secret-key"]) {
                         let result = await UtilApi.random_token();
-                        await onChange("embedding-secret-key", result.token);
+                        await onChangeSetting("embedding-secret-key", result.token);
                     }
                 }
-            },
-            {
+            }, {
                 key: "enable-embedding",
                 display_name: "Enable Embedding Metabase in other Applications",
                 type: "boolean",
                 getHidden: (settings) => !settings["enable-embedding"]
             },
+            {
+                widget: EmbeddingLevel,
+                getHidden: (settings) => !settings["enable-embedding"]
+            },
             {
                 key: "embedding-secret-key",
                 display_name: "Embedding secret key",
@@ -390,7 +395,19 @@ const SECTIONS = [
 
             }
         ]
+    },
+    /*
+    {
+        name: "Premium Embedding",
+        settings: [
+            {
+                key: "premium-embedding-token",
+                display_name: "Premium Embedding Token",
+                widget: PremiumEmbeddingWidget
+            }
+        ]
     }
+    */
 ];
 for (const section of SECTIONS) {
     section.slug = slugify(section.name);
diff --git a/frontend/src/metabase/lib/settings.js b/frontend/src/metabase/lib/settings.js
index 4c0fdfaefbcdeaa6d6d91ebc95a184cc27127efe..13980c72975d7f1e91764798b7ee3635bd010921 100644
--- a/frontend/src/metabase/lib/settings.js
+++ b/frontend/src/metabase/lib/settings.js
@@ -55,6 +55,10 @@ const MetabaseSettings = {
         return mb_settings.ldap_configured;
     },
 
+    hideEmbedBranding: () => mb_settings.hide_embed_branding,
+
+    metastoreUrl: () => mb_settings.metastore_url,
+
     newVersionAvailable: function(settings) {
         let versionInfo = _.findWhere(settings, {key: "version-info"}),
             currentVersion = MetabaseSettings.get("version").tag;
diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx
index b467df4ffe62a8d09ae3bfd8f365346657b706c3..59760f3b596d2154678ca91839a95206ed75c67a 100644
--- a/frontend/src/metabase/public/components/EmbedFrame.jsx
+++ b/frontend/src/metabase/public/components/EmbedFrame.jsx
@@ -6,6 +6,8 @@ import { withRouter } from "react-router";
 import { IFRAMED } from "metabase/lib/dom";
 import { parseHashOptions } from "metabase/lib/browser";
 
+import MetabaseSettings from "metabase/lib/settings";
+
 import Parameters from "metabase/parameters/components/Parameters";
 import LogoBadge from "./LogoBadge";
 
@@ -102,7 +104,9 @@ export default class EmbedFrame extends Component {
                 </div>
                 { footer &&
                     <div className="EmbedFrame-footer p1 md-p2 lg-p3 border-top flex-no-shrink flex align-center">
-                        <LogoBadge dark={theme} />
+                        {!MetabaseSettings.hideEmbedBranding() &&
+                            <LogoBadge dark={theme} />
+                        }
                         {actionButtons &&
                             <div className="flex-align-right text-grey-3">{actionButtons}</div>
                         }
diff --git a/frontend/src/metabase/xray/containers/CardXRay.jsx b/frontend/src/metabase/xray/containers/CardXRay.jsx
index e5852277c95bb8d0ffd422b2b404084e69f66b97..5083579ee505316b23f6206e97dd358b2dff4bf3 100644
--- a/frontend/src/metabase/xray/containers/CardXRay.jsx
+++ b/frontend/src/metabase/xray/containers/CardXRay.jsx
@@ -125,7 +125,7 @@ class CardXRay extends Component {
                                         series={[
                                             {
                                                 card: xray.features.model,
-                                                data: xray.dataset
+                                                data: xray.features.series
                                             },
                                             {
                                                 card: {
diff --git a/project.clj b/project.clj
index 23dd60b44934582cf11278381926bd932d3baec4..46e5884a8c7ea925af02c4a74a0e6517044f05b7 100644
--- a/project.clj
+++ b/project.clj
@@ -76,7 +76,7 @@
                  [mysql/mysql-connector-java "5.1.39"]                ;  !!! Don't upgrade to 6.0+ yet -- that's Java 8 only !!!
                  [jdistlib "0.5.1"                                    ; Distribution statistic tests
                   :exclusions [com.github.wendykierp/JTransforms]]
-                 [net.cgrand/xforms "0.13.0"                          ; Transducer utilites
+                 [net.cgrand/xforms "0.13.0"                          ; Additional transducers
                   :exclusions [org.clojure/clojurescript]]
                  [net.sf.cssbox/cssbox "4.12"                         ; HTML / CSS rendering
                   :exclusions [org.slf4j/slf4j-api]]
diff --git a/resources/frontend_client/app/assets/img/premium_embed.png b/resources/frontend_client/app/assets/img/premium_embed.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d893b11232c4cd08900202d81487cecbfcfc9af
Binary files /dev/null and b/resources/frontend_client/app/assets/img/premium_embed.png differ
diff --git a/resources/frontend_client/app/assets/img/premium_embed@2x.png b/resources/frontend_client/app/assets/img/premium_embed@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aae842a9e5579b2b252d86bda4b6a0c8c5715ac
Binary files /dev/null and b/resources/frontend_client/app/assets/img/premium_embed@2x.png differ
diff --git a/resources/frontend_client/app/assets/img/premium_embed_added.png b/resources/frontend_client/app/assets/img/premium_embed_added.png
new file mode 100644
index 0000000000000000000000000000000000000000..c458400f10e68f66815a8898c436ceedc8b85c7c
Binary files /dev/null and b/resources/frontend_client/app/assets/img/premium_embed_added.png differ
diff --git a/resources/frontend_client/app/assets/img/premium_embed_added@2x.png b/resources/frontend_client/app/assets/img/premium_embed_added@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..734cd2cb1c490a7fcc6847fc01dcabb6cb30b897
Binary files /dev/null and b/resources/frontend_client/app/assets/img/premium_embed_added@2x.png differ
diff --git a/src/metabase/email/_footer.mustache b/src/metabase/email/_footer.mustache
index a8f2b8a0277481b0f3213ce77ee14bd02be93154..fffd414890ec1b324f0b6e7429f2aec86637bb54 100644
--- a/src/metabase/email/_footer.mustache
+++ b/src/metabase/email/_footer.mustache
@@ -3,7 +3,7 @@
     <div style="padding-top: 2em; padding-bottom: 1em; text-align: center; color: #CCCCCC; font-size: small;">"{{quotation}}"<br/>- {{quotationAuthor}}</div>
   {{/quotation}}
   {{#logoFooter}}
-    <div style="padding-bottom: 1em; padding-top: 1em; text-align: center;">
+    <div style="padding-bottom: 2em; padding-top: 1em; text-align: center;">
       <img width="32" height="40" src="http://static.metabase.com/email_logo.png"/>
     </div>
   {{/logoFooter}}
diff --git a/src/metabase/email/_header.mustache b/src/metabase/email/_header.mustache
index 8cda8c15ba0da367951bf81091040a39f4319d7f..9541f783cac427b737057b370af6a5eae331689a 100644
--- a/src/metabase/email/_header.mustache
+++ b/src/metabase/email/_header.mustache
@@ -8,10 +8,10 @@
     }
   </style>
 </head>
-<body class="{{emailType}}" style="font-family: 'Helvetica Neue', Helvetica, sans-serif; font-size: 0.875rem; color: #727479; padding: 1em; background-color: #F9FBFC; ">
+<body class="{{emailType}}" style="font-family: 'Helvetica Neue', Helvetica, sans-serif; padding: 1em;">
   {{#logoHeader}}
     <div style="padding-bottom: 2em; padding-top: 1em; text-align: center;">
-      <img width="32" height="40" src="http://static.metabase.com/email_logo.png"/>
+      <img width="47" height="60" src="http://static.metabase.com/email_logo.png"/>
     </div>
   {{/logoHeader}}
-  <div class="container" style="margin: 0 auto; padding: 0; max-width: 660px; color: #9F9F9F;">
+  <div class="container" style="margin: 0 auto; padding: 0 0 2em 0; max-width: 500px; font-size: 16px; line-height: 24px; color: #616D75;">
diff --git a/src/metabase/email/new_user_invite.mustache b/src/metabase/email/new_user_invite.mustache
index 38a0a9c8328df780bacffd832093163a8917c0b9..0480b1161106f9d523639b97e5f25861293b6397 100644
--- a/src/metabase/email/new_user_invite.mustache
+++ b/src/metabase/email/new_user_invite.mustache
@@ -1,29 +1,25 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08); text-align: center;">
+  <div style="text-align: center;">
     <div style="padding-bottom: 1em;">
-      <h2 style="font-weight: normal; color: #4C545B;line-height: 1.65rem;">{{invitedName}}, you're invited to {{company}}'s&nbsp;Metabase</h2>
-      <h4 style="font-weight: normal;"><a style="color: #4A90E2; text-decoration: none;" href="mailto:{{invitorEmail}}">{{invitorName}} ({{invitorEmail}})</a> invited you to join them.</h4>
+      <h2 style="font-weight: normal; color: #4C545B; line-height: 2rem;">{{invitorName}} wants you to join them on Metabase</h2>
     </div>
-    <div style="border-top: 1px solid #ededed; border-bottom: 1px solid #ededed; padding: 3em 0em 2em 0em; text-align: center; margin-left: auto; margin-right: auto; max-width: 400px; position: relative;">
+    <div style="padding: 0.25em 0em .25em 0em; text-align: center; margin-left: auto; margin-right: auto; max-width: 400px; position: relative;">
       <table width="296" height="141" cellpadding="0" cellspacing="0" style="display:block;margin:0 auto;">
         <tr><td colspan="3"><img src="http://static.metabase.com/email_graph_top.png" width="296" height="73" style="display:block" /></td></tr>
         <tr>
           <td height="15" width="60"><img src="http://static.metabase.com/email_graph_left.png" width="60" height="15" style="display:block" /></td>
-          <td height="15" width="68" valign="middle" align="center" style="font-weight: bold; font-size: 0.72rem; line-height:15px;color: #fff; background-color:#333">{{{today}}}</td>
+          <td height="15" width="68" valign="middle" align="center" style="font-weight: bold; font-size: 0.6rem; line-height:15px;color: #fff; background-color:#333">{{{today}}}</td>
           <td height="15" width="168"><img src="http://static.metabase.com/email_graph_right.png" width="168" height="15" style="display:block" /></td></tr>
         <tr><td colspan="3" height="46"><img src="http://static.metabase.com/email_graph_bottom.png" width="296" height="56" style="display:block" /></td></tr>
       </table>
-      <p style="line-height: 1.3rem;">{{invitedName}}'s Happiness and Productivity Over&nbsp;Time</p>
+      <p style="line-height: 1.3rem; font-size: small">{{invitedName}}'s Happiness and Productivity Over&nbsp;Time</p>
     </div>
-    <div style="max-width: 400px; margin-left: auto; margin-right: auto; padding-top: 1em; line-height: 1.2rem;">
-      <p>Metabase is a simple and powerful analytics tool which lets <span style="color: #595959;">anyone</span> learn and <span style="color: #595959;">make decisions</span> from their company's data.</p>
+    <div style="max-width: 450px; margin-left: auto; margin-right: auto; line-height: 1.2rem;">
+      <p>Metabase is a simple and powerful analytics tool which lets <b>anyone</b> learn and <b>make decisions</b> from their company's data.</p>
       <p>No technical knowledge required!</p>
     </div>
-    <div style="padding: 1em;">
-      <a style="display: inline-block; box-sizing: border-box; text-decoration: none; font-size: 1.063rem; padding: 0.5rem 1.375rem; background: #FBFCFD; border: 1px solid #ddd; color: #444; cursor: pointer; text-decoration: none; font-weight: bold; border-radius: 4px; background-color: #4990E2; border-color: #4990E2; color: #fff;" href="{{joinUrl}}">Join Now</a>
-    </div>
-    <div style="padding-bottom: 2em; font-size: x-small;">
-      Or you can paste this link into your browser:<br/>{{joinUrl}}
+    <div style="padding: 1em 0 1em 0;">
+      <a style="display: inline-block; box-sizing: border-box; text-decoration: none; font-size: 1.063rem; padding: 0.8rem 2.25rem; background: #FBFCFD; border: 1px solid #ddd; color: #444; cursor: pointer; text-decoration: none; font-weight: bold; border-radius: 4px; background-color: #4990E2; border-color: #4990E2; color: #fff;" href="{{joinUrl}}">Join now</a>
     </div>
   </div>
 {{> metabase/email/_footer }}
diff --git a/src/metabase/email/password_reset.mustache b/src/metabase/email/password_reset.mustache
index 87158ed55bd84f7904da3b03a41aa1e96d1ea9e4..80f349b80ba627b92f67f9afc3a8cb5a2f304a5b 100644
--- a/src/metabase/email/password_reset.mustache
+++ b/src/metabase/email/password_reset.mustache
@@ -1,16 +1,15 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
-    <p>
-      You're receiving this e-mail because you or someone else has requested a password for your user account at {{hostname}}.
-      It can be safely ignored if you did not request a password reset.
-    </p>
+  <div>
     {{#sso}}
       <p>You're using Google to log in to Metabase, so you don't have a password. You can log in to Metabase by clicking "Sign in with Google"</p>
       <a href="{{hostname}}">Go to Metabase</a>
     {{/sso}}
     {{^sso}}
-      <p>Click the link below to reset your password.</p>
-      <a href="{{passwordResetUrl}}">{{passwordResetUrl}}</a>
+    <div style="text-align: center">
+      <p>Click the button below to reset the password for your Metabase account at {{hostname}}.</p>
+      <a style="display: inline-block; box-sizing: border-box; text-decoration: none; font-size: 1.063rem; padding: 0.5rem 1.375rem; background: #FBFCFD; border: 1px solid #ddd; color: #444; cursor: pointer; text-decoration: none; border-radius: 4px; background-color: #4990E2; border-color: #4990E2; color: #fff;" href="{{passwordResetUrl}}">Reset password</a>
+      <p style="padding-top: 2em; font-size: small;">Didn't request this password reset? It's safe to ignore it.</p>
+    </div>
     {{/sso}}
   </div>
 {{> metabase/email/footer }}
diff --git a/src/metabase/feature_extraction/core.clj b/src/metabase/feature_extraction/core.clj
index ddb1c1391713655577b9c1de977a2d58a8fe5bff..f40d8ad643505d1f62aadc7081bb4e80ace6629b 100644
--- a/src/metabase/feature_extraction/core.clj
+++ b/src/metabase/feature_extraction/core.clj
@@ -160,7 +160,6 @@
                              (ensure-aligment fields cols rows)))
                           {:model card
                            :table (Table (:table_id card))})
-     :dataset      dataset
      :sample?      (sampled? opts dataset)
      :comparables  (comparables card)}))
 
diff --git a/src/metabase/feature_extraction/feature_extractors.clj b/src/metabase/feature_extraction/feature_extractors.clj
index d7d3e5bd6b41e06769eb1b882a1a0db153d533f3..54a3965089fbcebeff5bcfee9f2f6486b4e8a96e 100644
--- a/src/metabase/feature_extraction/feature_extractors.clj
+++ b/src/metabase/feature_extraction/feature_extractors.clj
@@ -15,6 +15,7 @@
             [metabase
              [query-processor :as qp]
              [util :as u]]
+            [net.cgrand.xforms :as x]
             [redux.core :as redux]
             [toucan.db :as db])
   (:import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus))
@@ -70,10 +71,70 @@
                 :strategy  :num-bins})]
           (h/equidistant-bins min-value max-value bin-width histogram))))))
 
+(defn- largest-triangle
+  "Find the point in `points` that frorms the largest triangle with verteices
+   `a` and `b`."
+  [a b points]
+  (apply max-key (partial math/triangle-area a b) points))
+
+(defn largest-triangle-three-buckets
+  "Downsample series `series` to (approximately) `target-size` data points using
+   Largest-Triangle-Three-Buckets algorithm. Series needs to be at least
+   2*`target-size` long for the algorithm to make sense. If it is not, the
+   original series is returned.
+
+   Note: this is true downsampling (selecting just some points), with no
+   smoothing performed.
+   https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf"
+  [target-size series]
+  (let [current-size (count series)]
+    (if (< current-size (* 2 target-size))
+      series
+      (let [[head & body] series
+            tail          (last body)
+            body          (butlast body)
+            bucket-size   (-> (/ current-size target-size) Math/floor int)]
+        (transduce (x/partition 2 1 (x/into []))
+                   (fn
+                     ([] [head])
+                     ([points] (conj points tail))
+                     ([points [middle right]]
+                      (conj points (largest-triangle (last points)
+                                                     (math/centroid right)
+                                                     middle))))
+                   (conj (partition bucket-size body) [tail]))))))
+
+(defn saddles
+  "Returns the number of saddles in a given series."
+  [series]
+  (->> series
+       (partition 2 1)
+       (partition-by (fn [[[_ y1] [_ y2]]]
+                       (>= y2 y1)))
+       rest
+       count))
+
+; The largest dataset returned will be 2*target-1 points as we need at least
+; 2 points per bucket for downsampling to have any effect.
+(def ^:private ^Integer datapoint-target-smooth 100)
+(def ^:private ^Integer datapoint-target-noisy  300)
+
+(def ^:private ^Double noisiness-threshold 0.05)
+
+(defn- target-size
+  [series]
+  (if (some-> series
+              saddles
+              (safe-divide (count series))
+              (> noisiness-threshold))
+    datapoint-target-noisy
+    datapoint-target-smooth))
+
 (defn- series->dataset
   ([fields series] (series->dataset identity fields series))
   ([keyfn fields series]
-   {:rows    (for [[x y] series]
+   {:rows    (for [[x y] (largest-triangle-three-buckets (target-size series)
+                                                         series)]
                [(keyfn x) y])
     :columns (map :name fields)
     :cols    (map #(dissoc % :remapped_from) fields)}))
@@ -377,7 +438,8 @@
                                           (->> series
                                                (partition 2 1)
                                                (map (fn [[[_ y1] [x y2]]]
-                                                      [x (math/growth y2 y1)]))))
+                                                      [x (or (math/growth y2 y1)
+                                                             0)]))))
                 :seasonal-decomposition
                 (when (and resolution
                            (costs/unbounded-computation? max-cost))
@@ -409,13 +471,15 @@
                     {:name         "TREND"
                      :display_name "Linear regression trend"
                      :base_type    :type/Float}]
-                   (for [[x _] series]
+                   ; 2 points fully define a line
+                   (for [[x y] [(first series) (last series)]]
                      [x (+ (* slope x) offset)])))
 
 (defmethod x-ray [DateTime Num]
   [{:keys [field series] :as features}]
   (let [x-field (first field)]
     (-> features
+        (update :series (partial series->dataset ts/from-double field))
         (dissoc :series :autocorrelation :best-fit)
         (assoc :insights ((merge-juxt insights/noisiness
                                       insights/variation-trend
diff --git a/src/metabase/feature_extraction/math.clj b/src/metabase/feature_extraction/math.clj
index 92201172514d0f9beb09e95e5d4787b99b99f0e6..43cc7539d8e3e7cd90a30bd249465257b2990acf 100644
--- a/src/metabase/feature_extraction/math.clj
+++ b/src/metabase/feature_extraction/math.clj
@@ -153,3 +153,21 @@
            lower-bound         (- q1 (* 1.5 iqr))
            upper-bound         (+ q3 (* 1.5 iqr))]
        (remove (comp #(< lower-bound % upper-bound) keyfn) xs)))))
+
+(defn triangle-area
+  "Return the area of triangle specified by vertices `[x1, y1]`, `[x2, y2]`, and
+   `[x3, y3].`
+   http://mathworld.wolfram.com/TriangleArea.html"
+  [[x1 y1] [x2 y2] [x3 y3]]
+  (* 0.5 (+ (* (- x2) y1)
+            (* x3 y1)
+            (* x1 y2)
+            (* (- x3) y2)
+            (* (- x1) y3)
+            (* x2 y3))))
+
+(def centroid
+  "Calculate the centroid of given points.
+   https://en.wikipedia.org/wiki/Centroid"
+  (partial transduce identity (redux/juxt ((map first) stats/mean)
+                                          ((map second) stats/mean))))
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 2b4656aae5a0b9a1d91be3769aad717638bcd4c3..683b13f234c678c7a29c8ca5ff47de70f26c7c73 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -6,6 +6,7 @@
             [metabase.models
              [common :as common]
              [setting :as setting :refer [defsetting]]]
+            [metabase.public-settings.metastore :as metastore]
             [metabase.util.i18n :refer [available-locales-with-names set-locale]]
             [metabase.util.password :as password]
             [toucan.db :as db])
@@ -36,7 +37,8 @@
                                                (not (s/starts-with? new-value "http")) (str "http://"))))))
 
 (defsetting site-locale
-  "The default language for this Metabase instance. This only applies to emails, Pulses, etc. Users' browsers will specify the language used in the user interface."
+  "The default language for this Metabase instance. This only applies to emails, Pulses, etc. Users' browsers will
+   specify the language used in the user interface."
   :type    :string
   :setter  (fn [new-value]
              (setting/set-string! :site-locale new-value)
@@ -117,8 +119,8 @@
   :default 10.0)
 
 (defn remove-public-uuid-if-public-sharing-is-disabled
-  "If public sharing is *disabled* and OBJECT has a `:public_uuid`, remove it so people don't try to use it (since it won't work).
-   Intended for use as part of a `post-select` implementation for Cards and Dashboards."
+  "If public sharing is *disabled* and OBJECT has a `:public_uuid`, remove it so people don't try to use it (since it
+   won't work). Intended for use as part of a `post-select` implementation for Cards and Dashboards."
   [object]
   (if (and (:public_uuid object)
            (not (enable-public-sharing)))
@@ -138,7 +140,8 @@
 
 
 (defn public-settings
-  "Return a simple map of key/value pairs which represent the public settings (`MetabaseBootstrap`) for the front-end application."
+  "Return a simple map of key/value pairs which represent the public settings (`MetabaseBootstrap`) for the front-end
+   application."
   []
   {:admin_email           (admin-email)
    :anon_tracking_enabled (anon-tracking-enabled)
@@ -152,10 +155,13 @@
    :ga_code               "UA-60817802-1"
    :google_auth_client_id (setting/get :google-auth-client-id)
    :has_sample_dataset    (db/exists? 'Database, :is_sample true)
+   :hide_embed_branding   (metastore/hide-embed-branding?)
    :ldap_configured       ((resolve 'metabase.integrations.ldap/ldap-configured?))
    :available_locales     (available-locales-with-names)
    :map_tile_server_url   (map-tile-server-url)
+   :metastore_url         metastore/store-url
    :password_complexity   password/active-password-complexity
+   :premium_token         (metastore/premium-embedding-token)
    :public_sharing        (enable-public-sharing)
    :report_timezone       (setting/get :report-timezone)
    :setup_token           ((resolve 'metabase.setup/token-value))
diff --git a/src/metabase/public_settings/metastore.clj b/src/metabase/public_settings/metastore.clj
new file mode 100644
index 0000000000000000000000000000000000000000..36133805fed1ba3783f4371488f67bd7c16aee6c
--- /dev/null
+++ b/src/metabase/public_settings/metastore.clj
@@ -0,0 +1,100 @@
+(ns metabase.public-settings.metastore
+  "Settings related to checking token validity and accessing the MetaStore."
+  (:require [cheshire.core :as json]
+            [clojure.core.memoize :as memoize]
+            [clojure.tools.logging :as log]
+            [clj-http.client :as client]
+            [environ.core :refer [env]]
+            [metabase.models.setting :as setting :refer [defsetting]]
+            [metabase.config :as config]
+            [metabase.util :as u]
+            [metabase.util.schema :as su]
+            [schema.core :as s]))
+
+(def ^:private ValidToken
+  "Schema for a valid metastore token. Must be 64 lower-case hex characters."
+  #"^[0-9a-f]{64}$")
+
+(def store-url
+  "URL to the MetaStore. Hardcoded by default but for development purposes you can use a local server. Specify the env
+   var `METASTORE_DEV_SERVER_URL`."
+  (or
+   ;; only enable the changing the store url during dev because we don't want people switching it out in production!
+   (when config/is-dev?
+     (env :metastore-dev-server-url))
+   "https://store.metabase.com"))
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                TOKEN VALIDATION                                                |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+(defn- token-status-url [token]
+  (when (seq token)
+    (format "%s/api/%s/status" store-url token)))
+
+(def ^:private ^:const fetch-token-status-timeout-ms 10000) ; 10 seconds
+
+(s/defn ^:private ^:always-validate fetch-token-status :- {:valid s/Bool, :status su/NonBlankString}
+  "Fetch info about the validity of TOKEN from the MetaStore. "
+  [token :- ValidToken]
+  (try
+    ;; attempt to query the metastore API about the status of this token. If the request doesn't complete in a
+    ;; reasonable amount of time throw a timeout exception
+    (deref (future
+             (try (some-> (token-status-url token)
+                          slurp
+                          (json/parse-string keyword))
+                  ;; slurp will throw a FileNotFoundException for 404s, so in that case just return an appropriate
+                  ;; 'Not Found' message
+                  (catch java.io.FileNotFoundException e
+                    {:valid false, :status "invalid token: not found."})
+                  ;; if there was any other error fetching the token, log it and return a generic message about the
+                  ;; token being invalid. This message will get displayed in the Settings page in the admin panel so
+                  ;; we do not want something complicated
+                  (catch Throwable e
+                    (log/error "Error fetching token status:" e)
+                    {:valid false, :status "there was an error checking whether this token was valid."})))
+           fetch-token-status-timeout-ms
+           {:valid false, :status "token validation timed out."})))
+
+(defn- check-embedding-token-is-valid* [token]
+  (when (s/check ValidToken token)
+    (throw (Exception. "Invalid token: token isn't in the right format.")))
+  (log/info "Checking with the MetaStore to see whether" token "is valid...")
+  (let [{:keys [valid status]} (fetch-token-status token)]
+    (or valid
+        ;; if token isn't valid throw an Exception with the `:status` message
+        (throw (Exception. ^String status)))))
+
+(def ^:private ^:const valid-token-recheck-interval-ms
+  "Amount of time to cache the status of a valid embedding token before forcing a re-check"
+  (* 1000 60 60 24)) ; once a day
+
+(def ^:private ^{:arglists '([token])} check-embedding-token-is-valid
+  "Check whether TOKEN is valid. Throws an Exception if not."
+  ;; this is just `check-embedding-token-is-valid*` with some light caching
+  (memoize/ttl check-embedding-token-is-valid*
+    :ttl/threshold valid-token-recheck-interval-ms))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                             SETTING & RELATED FNS                                              |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+;; TODO - better docstring
+(defsetting premium-embedding-token
+  "Token for premium embedding. Go to the MetaStore to get yours!"
+  :setter (fn [new-value]
+            ;; validate the new value if we're not unsetting it
+            (when (seq new-value)
+              (check-embedding-token-is-valid new-value)
+              (log/info "Token is valid."))
+            (setting/set-string! :premium-embedding-token new-value)))
+
+(defn hide-embed-branding?
+  "Should we hide the 'Powered by Metabase' attribution on the embedding pages? `true` if we have a valid premium
+   embedding token."
+  []
+  (boolean
+   (u/ignore-exceptions
+     (check-embedding-token-is-valid (premium-embedding-token)))))
diff --git a/test/metabase/feature_extraction/feature_extractors_test.clj b/test/metabase/feature_extraction/feature_extractors_test.clj
index 73c395d6b3b57dec24480a194ce35906bfdb1d1d..c52e7ea7d156ae1c8b0df1cf262f395833d26e71 100644
--- a/test/metabase/feature_extraction/feature_extractors_test.clj
+++ b/test/metabase/feature_extraction/feature_extractors_test.clj
@@ -7,6 +7,7 @@
              [feature-extractors :refer :all :as fe]
              [histogram :as h]
              [timeseries :as ts]]
+            [medley.core :as m]
             [redux.core :as redux]))
 
 (expect
@@ -127,3 +128,14 @@
     [(-> x-ray :zeros :quality)
      (-> x-ray :nils :quality)
      (-> x-ray :normal-range :upper)]))
+
+(expect
+  [(var-get #'fe/datapoint-target-smooth)
+   (var-get #'fe/datapoint-target-noisy)]
+  [(#'fe/target-size (m/indexed (range 10)))
+   (#'fe/target-size (m/indexed (repeatedly 1000 rand)))])
+
+(expect
+  [32 10]
+  [(count (largest-triangle-three-buckets 30 (m/indexed (repeatedly 1000 rand))))
+   (count (largest-triangle-three-buckets 30 (m/indexed (repeatedly 10 rand))))])
diff --git a/test/metabase/feature_extraction/insights_test.clj b/test/metabase/feature_extraction/insights_test.clj
index 3a47a4db900abff4dc2d4ac0c4d4737d9df9e35d..91309aa2c4c0489e0ece3a519b9d7bd2a20ad285 100644
--- a/test/metabase/feature_extraction/insights_test.clj
+++ b/test/metabase/feature_extraction/insights_test.clj
@@ -13,7 +13,7 @@
 
 (expect
   [{:mode :increasing}
-   {:mode :increasing}
+   {:mode :decreasing}
    nil]
   (map :variation-trend
        [(variation-trend {:series (map-indexed
@@ -26,7 +26,7 @@
                                    (fn [i x]
                                      [i (* x (+ 1 (* (/ (- 100 i) 100)
                                                      (- (* 2 (rand)) 0.9))))])
-                                   (repeatedly 100 rand))
+                                   (repeatedly 100 #(rand-int 10)))
                           :resolution :month})
         (variation-trend {:series (m/indexed (repeat 100 1))
                           :resolution :month})]))
diff --git a/test/metabase/feature_extraction/math_test.clj b/test/metabase/feature_extraction/math_test.clj
index 2ff7fda741612096666b77f792c2c4f9c0d5f1d0..7558973b7df2b9a0be29cd54b6107e53e100fe94 100644
--- a/test/metabase/feature_extraction/math_test.clj
+++ b/test/metabase/feature_extraction/math_test.clj
@@ -92,3 +92,7 @@
                         (assoc-in [30] 100)
                         (assoc-in [70] 35))))
      (outliers nil)]))
+
+(expect
+  8.0
+  (triangle-area [-2 0] [2 0] [0 4]))