Apply theme to VegaLite Charts
diff --git a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-0.snap.png b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-0.snap.png
index 8fe2b51c7377..9eb7bc187f89 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-0.snap.png and b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-0.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-1.snap.png b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-1.snap.png
index 03f15758dcb4..585e9895b3ca 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-1.snap.png and b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-1.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-2.snap.png b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-2.snap.png
index cfa1677cca9f..b620c67226e8 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-2.snap.png and b/frontend/cypress/snapshots/linux/2x/st_chart_utc_time.spec.js/chartUTCTime-2.snap.png differ
diff --git a/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.test.tsx b/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.test.tsx
index ba50b23bc674..cf94c2ded1de 100644
--- a/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.test.tsx
+++ b/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.test.tsx
@@ -16,15 +16,17 @@
  */
 
 import React from "react"
-import { shallow } from "lib/test_util"
+import { mount } from "lib/test_util"
 import { fromJS } from "immutable"
 import { VegaLiteChart as VegaLiteChartProto } from "autogen/proto"
+import { darkTheme, lightTheme } from "theme"
 
 import mock from "./mock"
 import { PropsWithHeight, VegaLiteChart } from "./VegaLiteChart"
 
 const getProps = (
-  elementProps: Partial<VegaLiteChartProto> = {}
+  elementProps: Partial<VegaLiteChartProto> = {},
+  props: Partial<PropsWithHeight> = {}
 ): PropsWithHeight => ({
   element: fromJS({
     ...mock,
@@ -32,13 +34,52 @@ const getProps = (
   }),
   width: 0,
   height: 0,
+  theme: lightTheme.emotion,
+  ...props,
 })
 
 describe("VegaLiteChart Element", () => {
   it("renders without crashing", () => {
     const props = getProps()
-    const wrapper = shallow(<VegaLiteChart {...props} />)
+    const wrapper = mount(<VegaLiteChart {...props} />)
 
     expect(wrapper.find("StyledVegaLiteChartContainer").length).toBe(1)
   })
+
+  it("pulls default config values from theme", () => {
+    const props = getProps(undefined, { theme: darkTheme.emotion })
+
+    const wrapper = mount(<VegaLiteChart {...props} />)
+    const generatedSpec = wrapper.instance().generateSpec()
+
+    expect(generatedSpec.config.background).toBe(
+      darkTheme.emotion.colors.bgColor
+    )
+    expect(generatedSpec.config.axis.labelColor).toBe(
+      darkTheme.emotion.colors.bodyText
+    )
+  })
+
+  it("has user specified config take priority", () => {
+    const props = getProps(undefined, { theme: darkTheme.emotion })
+
+    const spec = JSON.parse(props.element.get("spec"))
+    spec.config = { background: "purple", axis: { labelColor: "blue" } }
+
+    props.element = fromJS({
+      ...props.element.toObject(),
+      spec: JSON.stringify(spec),
+    })
+
+    const wrapper = mount(<VegaLiteChart {...props} />)
+    const generatedSpec = wrapper.instance().generateSpec()
+
+    expect(generatedSpec.config.background).toBe("purple")
+    expect(generatedSpec.config.axis.labelColor).toBe("blue")
+    // Verify that things not overwritten by the user still fall back to the
+    // theme default.
+    expect(generatedSpec.config.axis.titleColor).toBe(
+      darkTheme.emotion.colors.bodyText
+    )
+  })
 })
diff --git a/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.tsx b/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.tsx
index be60c3bc9859..de517e92a396 100644
--- a/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.tsx
+++ b/frontend/src/components/elements/VegaLiteChart/VegaLiteChart.tsx
@@ -16,10 +16,12 @@
  */
 
 import React, { PureComponent } from "react"
+import { withTheme } from "emotion-theming"
 import { logMessage } from "lib/log"
 import { Map as ImmutableMap } from "immutable"
 import withFullScreenWrapper from "hocs/withFullScreenWrapper"
 import { tableGetRowsAndCols, indexGet, tableGet } from "lib/dataFrameProto"
+import { Theme } from "theme"
 import embed from "vega-embed"
 import * as vega from "vega"
 import { StyledVegaLiteChartContainer } from "./styled-components"
@@ -54,6 +56,7 @@ const SUPPORTED_INDEX_TYPES = new Set([
 interface Props {
   width: number
   element: ImmutableMap<string, any>
+  theme: Theme
 }
 
 export interface PropsWithHeight extends Props {
@@ -115,8 +118,8 @@ export class VegaLiteChart extends PureComponent<PropsWithHeight, State> {
   }
 
   public async componentDidUpdate(prevProps: PropsWithHeight): Promise<void> {
-    const prevElement = prevProps.element
-    const { element } = this.props
+    const { element: prevElement, theme: prevTheme } = prevProps
+    const { element, theme } = this.props
 
     const prevSpec = prevElement.get("spec")
     const spec = element.get("spec")
@@ -124,6 +127,7 @@ export class VegaLiteChart extends PureComponent<PropsWithHeight, State> {
     if (
       !this.vegaView ||
       prevSpec !== spec ||
+      prevTheme !== theme ||
       prevProps.width !== this.props.width ||
       prevProps.height !== this.props.height
     ) {
@@ -163,10 +167,12 @@ export class VegaLiteChart extends PureComponent<PropsWithHeight, State> {
   }
 
   public generateSpec = (): any => {
-    const el = this.props.element
+    const { element: el, theme } = this.props
     const spec = JSON.parse(el.get("spec"))
     const useContainerWidth = JSON.parse(el.get("useContainerWidth"))
 
+    spec.config = configWithThemeDefaults(spec.config, theme)
+
     if (this.props.height) {
       // fullscreen
       spec.width = this.props.width - EMBED_PADDING
@@ -457,4 +463,56 @@ function dataIsAnAppendOfPrev(
   return true
 }
 
-export default withFullScreenWrapper(VegaLiteChart)
+function configWithThemeDefaults(config: any, theme: Theme): any {
+  const textColor = theme.colors.bodyText
+  const themeFonts = {
+    labelFont: theme.genericFonts.bodyFont,
+    titleFont: theme.genericFonts.bodyFont,
+  }
+  const themeBg = theme.inSidebar
+    ? theme.colors.sidebarBg
+    : theme.colors.bgColor
+
+  const themeDefaults = {
+    background: themeBg,
+    axis: {
+      labelColor: textColor,
+      titleColor: textColor,
+      ...themeFonts,
+    },
+    legend: {
+      labelColor: textColor,
+      titleColor: textColor,
+      ...themeFonts,
+    },
+    title: {
+      color: textColor,
+      subtitleColor: textColor,
+      ...themeFonts,
+    },
+  }
+
+  if (!config) {
+    return themeDefaults
+  }
+
+  // Fill in theme defaults where the user didn't specify config options.
+  return {
+    ...config,
+    background: config.background || themeDefaults.background,
+    axis: {
+      ...themeDefaults.axis,
+      ...config.axis,
+    },
+    legend: {
+      ...themeDefaults.legend,
+      ...config.legend,
+    },
+    title: {
+      ...themeDefaults.title,
+      ...config.title,
+    },
+  }
+}
+
+export default withTheme(withFullScreenWrapper(VegaLiteChart))
