diff --git a/frontend/src/metabase/internal/pages/StaticVizPage.jsx b/frontend/src/metabase/internal/pages/StaticVizPage.jsx
index f2fd3be71383ccd6c80357eb71439b8f6eb3aaee..479a244ce12cc952ee031bf320732d516f43f3c2 100644
--- a/frontend/src/metabase/internal/pages/StaticVizPage.jsx
+++ b/frontend/src/metabase/internal/pages/StaticVizPage.jsx
@@ -20,12 +20,9 @@ import {
 } from "../../static-viz/components/ProgressBar/constants";
 import {
   TIME_SERIES_WATERFALL_CHART_DEFAULT_OPTIONS,
-  TIME_SERIES_WATERFALL_CHART_TYPE,
-} from "../../static-viz/components/TimeSeriesWaterfallChart/constants";
-import {
   CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS,
-  CATEGORICAL_WATERFALL_CHART_TYPE,
-} from "../../static-viz/components/CategoricalWaterfallChart/constants";
+  WATERFALL_CHART_TYPE,
+} from "../../static-viz/components/WaterfallChart/constants";
 import {
   FUNNEL_CHART_DEFAULT_OPTIONS,
   FUNNEL_CHART_TYPE,
@@ -160,14 +157,17 @@ export default function StaticVizPage() {
         <PageSection>
           <Subhead>Waterfall chart with timeseries data and no total</Subhead>
           <StaticChart
-            type={TIME_SERIES_WATERFALL_CHART_TYPE}
+            type={WATERFALL_CHART_TYPE}
             options={TIME_SERIES_WATERFALL_CHART_DEFAULT_OPTIONS}
           />
         </PageSection>
         <PageSection>
-          <Subhead>Waterfall chart with categorical data and total</Subhead>
+          <Subhead>
+            Waterfall chart with categorical data and total (rotated X-axis tick
+            labels)
+          </Subhead>
           <StaticChart
-            type={CATEGORICAL_WATERFALL_CHART_TYPE}
+            type={WATERFALL_CHART_TYPE}
             options={CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS}
           />
         </PageSection>
diff --git a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/CategoricalWaterfallChart.jsx b/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/CategoricalWaterfallChart.jsx
deleted file mode 100644
index 527be39e3f13055ea97d3fda03c3fa3f6ec4ccae..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/CategoricalWaterfallChart.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { AxisBottom, AxisLeft } from "@visx/axis";
-import { GridRows } from "@visx/grid";
-import { scaleBand, scaleLinear } from "@visx/scale";
-import { Bar } from "@visx/shape";
-import { Group } from "@visx/group";
-import { Text } from "@visx/text";
-import { truncateText } from "metabase/static-viz/lib/text";
-import {
-  getLabelProps,
-  getXTickLabelProps,
-  getYTickLabelProps,
-  getYTickWidth,
-  getXTickWidth,
-  getRotatedXTickHeight,
-} from "metabase/static-viz/lib/axes";
-import { formatNumber } from "metabase/static-viz/lib/numbers";
-import {
-  calculateWaterfallDomain,
-  calculateWaterfallEntries,
-  getWaterfallEntryColor,
-} from "metabase/static-viz/lib/waterfall";
-import { POSITIONAL_ACCESSORS } from "../../constants/accessors";
-import { getWaterfallColors } from "../../lib/colors";
-
-const propTypes = {
-  data: PropTypes.array.isRequired,
-  accessors: PropTypes.shape({
-    x: PropTypes.func.isRequired,
-    y: PropTypes.func.isRequired,
-  }),
-  settings: PropTypes.shape({
-    x: PropTypes.object,
-    y: PropTypes.object,
-    colors: PropTypes.object,
-    showTotal: PropTypes.bool,
-  }),
-  labels: PropTypes.shape({
-    left: PropTypes.string,
-    bottom: PropTypes.string,
-  }),
-  getColor: PropTypes.func,
-};
-
-const layout = {
-  width: 540,
-  height: 300,
-  margin: {
-    top: 0,
-    left: 55,
-    right: 40,
-    bottom: 40,
-  },
-  font: {
-    size: 11,
-    family: "Lato, sans-serif",
-  },
-  barPadding: 0.2,
-  labelFontWeight: 700,
-  labelPadding: 12,
-  strokeDasharray: "4",
-  maxTickWidth: 100,
-};
-
-const CategoricalWaterfallChart = ({
-  data,
-  accessors = POSITIONAL_ACCESSORS,
-  settings,
-  labels,
-  getColor,
-}) => {
-  const entries = calculateWaterfallEntries(
-    data,
-    accessors,
-    settings?.showTotal,
-  );
-  const isVertical = entries.length > 10;
-  const xTickWidth = getXTickWidth(
-    data,
-    accessors,
-    layout.maxTickWidth,
-    layout.font.size,
-  );
-  const xTickHeight = getRotatedXTickHeight(xTickWidth);
-  const yTickWidth = getYTickWidth(data, accessors, settings, layout.font.size);
-  const xLabelOffset = xTickHeight + layout.labelPadding + layout.font.size;
-  const yLabelOffset = yTickWidth + layout.labelPadding;
-  const xMin = yLabelOffset + layout.font.size * 1.5;
-  const xMax = layout.width - layout.margin.right - layout.margin.left;
-  const yMin = isVertical ? xLabelOffset : layout.margin.bottom;
-  const yMax = layout.height - yMin - layout.margin.top;
-  const innerWidth = xMax - xMin;
-  const textBaseline = Math.floor(layout.font.size / 2);
-  const leftLabel = labels?.left;
-  const bottomLabel = !isVertical ? labels?.bottom : undefined;
-
-  const xScale = scaleBand({
-    domain: entries.map(entry => entry.x),
-    range: [0, xMax],
-    padding: layout.barPadding,
-  });
-
-  const yScale = scaleLinear({
-    domain: calculateWaterfallDomain(entries),
-    range: [yMax, 0],
-  });
-
-  const getBarProps = entry => {
-    const width = xScale.bandwidth();
-
-    const height = Math.abs(yScale(entry.start) - yScale(entry.end));
-    const x = xScale(entry.x);
-    const y = yScale(Math.max(entry.start, entry.end));
-
-    const fill = getWaterfallEntryColor(
-      entry,
-      getWaterfallColors(settings?.colors, getColor),
-    );
-
-    return { x, y, width, height, fill };
-  };
-
-  const getXTickProps = ({ x, y, formattedValue, ...props }) => {
-    const textWidth = isVertical ? xTickWidth : xScale.bandwidth();
-    const truncatedText = truncateText(
-      formattedValue,
-      textWidth,
-      layout.font.size,
-    );
-    const transform = isVertical
-      ? `rotate(45, ${x} ${y}) translate(-${textBaseline} 0)`
-      : undefined;
-
-    return { ...props, x, y, transform, children: truncatedText };
-  };
-
-  return (
-    <svg width={layout.width} height={layout.height}>
-      <Group top={layout.margin.top} left={xMin}>
-        <GridRows
-          scale={yScale}
-          width={innerWidth}
-          strokeDasharray={layout.strokeDasharray}
-        />
-        {entries.map((entry, index) => (
-          <Bar key={index} {...getBarProps(entry)} />
-        ))}
-      </Group>
-      <AxisLeft
-        scale={yScale}
-        top={layout.margin.top}
-        left={xMin}
-        label={leftLabel}
-        labelOffset={yLabelOffset}
-        hideTicks
-        hideAxisLine
-        labelProps={getLabelProps(layout, getColor)}
-        tickFormat={value => formatNumber(value, settings?.y)}
-        tickLabelProps={() => getYTickLabelProps(layout, getColor)}
-      />
-
-      <AxisBottom
-        scale={xScale}
-        left={xMin}
-        top={yMax + layout.margin.top}
-        label={bottomLabel}
-        numTicks={entries.length}
-        stroke={getColor("text-light")}
-        tickStroke={getColor("text-light")}
-        labelProps={getLabelProps(layout, getColor)}
-        tickComponent={props => <Text {...getXTickProps(props)} />}
-        tickLabelProps={() => getXTickLabelProps(layout, isVertical, getColor)}
-      />
-    </svg>
-  );
-};
-
-CategoricalWaterfallChart.propTypes = propTypes;
-
-export default CategoricalWaterfallChart;
diff --git a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/constants.ts b/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/constants.ts
deleted file mode 100644
index fbcfe1b2a6fab47f93028b55d660d7990759cd7b..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/constants.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export const CATEGORICAL_WATERFALL_CHART_TYPE = "categorical/waterfall";
-
-export const CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS = {
-  data: [
-    ["Stage 1", 800],
-    ["Stage 2", 400],
-    ["Stage 3", -300],
-    ["Stage 4", -100],
-    ["Stage 5", -50],
-    ["Stage 6", 200],
-    ["Stage 7", -100],
-    ["Stage 8", 300],
-    ["Stage 9", 100],
-    ["Stage 10", -300],
-  ],
-  settings: {
-    showTotal: true,
-  },
-  labels: {
-    left: "Count",
-    bottom: "Created At",
-  },
-};
diff --git a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/index.js b/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/index.js
deleted file mode 100644
index 6a39b446ff7865d30a7a20626627cfd1a9e0c359..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/CategoricalWaterfallChart/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./CategoricalWaterfallChart";
diff --git a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/TimeSeriesWaterfallChart.jsx b/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/TimeSeriesWaterfallChart.jsx
deleted file mode 100644
index 1b9573821505b456b5bbc81bdc593980c8eaa534..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/TimeSeriesWaterfallChart.jsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { AxisBottom, AxisLeft } from "@visx/axis";
-import { GridRows } from "@visx/grid";
-import { scaleBand, scaleLinear } from "@visx/scale";
-import { Bar } from "@visx/shape";
-import { Group } from "@visx/group";
-import {
-  getLabelProps,
-  getXTickLabelProps,
-  getYTickLabelProps,
-  getYTickWidth,
-} from "metabase/static-viz/lib/axes";
-import { formatNumber } from "metabase/static-viz/lib/numbers";
-import {
-  calculateWaterfallDomain,
-  calculateWaterfallEntries,
-  formatTimescaleWaterfallTick,
-  getWaterfallEntryColor,
-} from "metabase/static-viz/lib/waterfall";
-import { sortTimeSeries } from "../../lib/sort";
-import { DATE_ACCESSORS } from "../../constants/accessors";
-import { getWaterfallColors } from "../../lib/colors";
-
-const propTypes = {
-  data: PropTypes.array.isRequired,
-  accessors: PropTypes.shape({
-    x: PropTypes.func.isRequired,
-    y: PropTypes.func.isRequired,
-  }),
-  settings: PropTypes.shape({
-    x: PropTypes.object,
-    y: PropTypes.object,
-    colors: PropTypes.object,
-    showTotal: PropTypes.bool,
-  }),
-  labels: PropTypes.shape({
-    left: PropTypes.string,
-    bottom: PropTypes.string,
-  }),
-  getColor: PropTypes.func,
-};
-
-const layout = {
-  width: 540,
-  height: 300,
-  margin: {
-    top: 0,
-    left: 55,
-    right: 40,
-    bottom: 40,
-  },
-  font: {
-    size: 11,
-    family: "Lato, sans-serif",
-  },
-  numTicks: 4,
-  barPadding: 0.2,
-  labelFontWeight: 700,
-  labelPadding: 12,
-  strokeDasharray: "4",
-};
-
-const TimeSeriesWaterfallChart = ({
-  data,
-  accessors = DATE_ACCESSORS,
-  settings,
-  labels,
-  getColor,
-}) => {
-  data = sortTimeSeries(data);
-  const yTickWidth = getYTickWidth(data, accessors, settings, layout.font.size);
-  const yLabelOffset = yTickWidth + layout.labelPadding;
-  const xMin = yLabelOffset + layout.font.size * 1.5;
-  const xMax = layout.width - layout.margin.right - layout.margin.left;
-  const yMax = layout.height - layout.margin.bottom;
-  const innerWidth = xMax - xMin;
-  const leftLabel = labels?.left;
-  const bottomLabel = labels?.bottom;
-
-  const entries = calculateWaterfallEntries(
-    data,
-    accessors,
-    settings?.showTotal,
-  );
-
-  const xScale = scaleBand({
-    domain: entries.map(entry => entry.x),
-    range: [0, xMax],
-    padding: layout.barPadding,
-  });
-
-  const yScale = scaleLinear({
-    domain: calculateWaterfallDomain(entries),
-    range: [yMax, 0],
-  });
-
-  const getBarProps = entry => {
-    const width = xScale.bandwidth();
-
-    const height = Math.abs(yScale(entry.start) - yScale(entry.end));
-    const x = xScale(entry.x);
-    const y = yScale(Math.max(entry.start, entry.end));
-    const fill = getWaterfallEntryColor(
-      entry,
-      getWaterfallColors(settings?.colors, getColor),
-    );
-
-    return { x, y, width, height, fill };
-  };
-
-  return (
-    <svg width={layout.width} height={layout.height}>
-      <Group top={layout.margin.top} left={xMin}>
-        <GridRows
-          scale={yScale}
-          width={innerWidth}
-          strokeDasharray={layout.strokeDasharray}
-        />
-        {entries.map((entry, index) => (
-          <Bar key={index} {...getBarProps(entry)} />
-        ))}
-      </Group>
-      <AxisLeft
-        scale={yScale}
-        top={layout.margin.top}
-        left={xMin}
-        label={leftLabel}
-        labelOffset={yLabelOffset}
-        hideTicks
-        hideAxisLine
-        labelProps={getLabelProps(layout, getColor)}
-        tickFormat={value => formatNumber(value, settings?.y)}
-        tickLabelProps={() => getYTickLabelProps(layout, getColor)}
-      />
-      <AxisBottom
-        scale={xScale}
-        left={xMin}
-        top={yMax + layout.margin.top}
-        label={bottomLabel}
-        numTicks={layout.numTicks}
-        stroke={getColor("text-light")}
-        tickStroke={getColor("text-light")}
-        labelProps={getLabelProps(layout, getColor)}
-        tickFormat={value => formatTimescaleWaterfallTick(value, settings)}
-        tickLabelProps={() => getXTickLabelProps(layout, false, getColor)}
-      />
-    </svg>
-  );
-};
-
-TimeSeriesWaterfallChart.propTypes = propTypes;
-
-export default TimeSeriesWaterfallChart;
diff --git a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/constants.ts b/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/constants.ts
deleted file mode 100644
index 7046f7608fbe5c064f502326f6fc70746a1f9fe7..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/constants.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export const TIME_SERIES_WATERFALL_CHART_TYPE = "timeseries/waterfall";
-
-export const TIME_SERIES_WATERFALL_CHART_DEFAULT_OPTIONS = {
-  data: [
-    ["2020-10-20", 20],
-    ["2020-10-21", 20],
-    ["2020-10-22", 100],
-    ["2020-10-23", -10],
-    ["2020-10-24", 20],
-    ["2020-10-25", -30],
-    ["2020-10-26", -10],
-    ["2020-10-27", 20],
-    ["2020-10-28", -15],
-  ],
-  labels: {
-    left: "Count",
-    bottom: "Created At",
-  },
-};
diff --git a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/index.js b/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/index.js
deleted file mode 100644
index 462d2653921943e124d0f82546575cafbd9b2f24..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/static-viz/components/TimeSeriesWaterfallChart/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./TimeSeriesWaterfallChart";
diff --git a/frontend/src/metabase/static-viz/components/XYChart/Values/Values.tsx b/frontend/src/metabase/static-viz/components/Values/Values.tsx
similarity index 80%
rename from frontend/src/metabase/static-viz/components/XYChart/Values/Values.tsx
rename to frontend/src/metabase/static-viz/components/Values/Values.tsx
index 86ff531ce434e90fa4600f25ed0a73c9ded64a3d..104e03909311454110db058a9a2954b450166a9b 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/Values/Values.tsx
+++ b/frontend/src/metabase/static-viz/components/Values/Values.tsx
@@ -6,7 +6,7 @@ import { scaleBand } from "@visx/scale";
 import type { TextProps } from "@visx/text";
 import type { AnyScaleBand, PositionScale } from "@visx/shape/lib/types";
 import OutlinedText from "metabase/static-viz/components/Text/OutlinedText";
-import { getValueStep, getY } from "../utils";
+import { getValueStep, getY, setY } from "../XYChart/utils";
 
 import type {
   HydratedSeries,
@@ -14,7 +14,7 @@ import type {
   StackedDatum,
   VisualizationType,
   XScale,
-} from "../types";
+} from "../XYChart/types";
 
 type XYAccessor<
   T extends SeriesDatum | StackedDatum = SeriesDatum | StackedDatum,
@@ -23,8 +23,11 @@ type XYAccessor<
   flipped?: boolean,
 ) => number;
 
+type Settings = Record<string, any>;
+
 const VALUES_MARGIN = 6;
-const VALUES_STROKE_MARGIN = 3;
+// From testing 1px is equal 3px of the stroke width, I'm not totally sure why.
+const VALUES_STROKE_MARGIN = 1;
 const FLIPPED_VALUES_MARGIN = VALUES_MARGIN + 8;
 
 interface ValuesProps {
@@ -37,10 +40,12 @@ interface ValuesProps {
   innerWidth: number;
   areStacked: boolean;
   xAxisYPos: number;
+  settings?: Settings;
 }
 
 interface Value {
   datum: SeriesDatum | StackedDatum;
+  datumForLabel: SeriesDatum | StackedDatum;
   flipped?: boolean;
   hidden?: boolean;
 }
@@ -61,6 +66,7 @@ export default function Values({
   innerWidth,
   areStacked,
   xAxisYPos,
+  settings,
 }: ValuesProps) {
   const containBars = Boolean(xScale.bandwidth);
   const barSeriesIndexMap = new WeakMap();
@@ -82,10 +88,10 @@ export default function Values({
     const singleSeriesValues =
       series.type === "bar"
         ? fixSmallBarChartValues(
-            getValues(series, areStacked),
+            getValues(series, areStacked, settings),
             innerBarScale?.domain().length ?? 0,
           )
-        : getValues(series, areStacked);
+        : getValues(series, areStacked, settings);
 
     return singleSeriesValues.map(value => {
       return {
@@ -160,20 +166,18 @@ export default function Values({
             ["line", "area"] as VisualizationType[]
           ).includes(value.series.type);
           return (
-            <>
+            <React.Fragment key={index}>
               <OutlinedText
-                key={index}
                 x={xAccessor(value.datum)}
                 y={yAccessor(value.datum)}
                 textAnchor="middle"
                 verticalAnchor="end"
                 {...valueProps}
               >
-                {formatter(getY(value.datum), compact)}
+                {formatter(getY(value.datumForLabel), compact)}
               </OutlinedText>
               {shouldRenderDataPoint && (
                 <circle
-                  key={index}
                   r={3}
                   fill="white"
                   stroke={value.series.color}
@@ -182,7 +186,7 @@ export default function Values({
                   cy={dataYAccessor(value.datum)}
                 />
               )}
-            </>
+            </React.Fragment>
           );
         });
       })}
@@ -218,10 +222,14 @@ export default function Values({
   }
 }
 
-function getValues(series: HydratedSeries, areStacked: boolean): Value[] {
+function getValues(
+  series: HydratedSeries,
+  areStacked: boolean,
+  settings?: Settings,
+): Value[] {
   const data = getData(series, areStacked);
 
-  return transformDataToValues(series.type, data);
+  return transformDataToValues(series.type, data, settings);
 }
 
 function getData(series: HydratedSeries, areStacked: boolean) {
@@ -257,13 +265,16 @@ function getXAccessor(
   xScale: XScale,
   barXOffset: number,
 ): XYAccessor {
-  if (type === "bar") {
-    return datum => (xScale.barAccessor as XYAccessor)(datum) + barXOffset;
-  }
-  if (type === "line" || type === "area") {
-    return xScale.lineAccessor as XYAccessor;
+  switch (type) {
+    case "bar":
+      return datum => (xScale.barAccessor as XYAccessor)(datum) + barXOffset;
+    case "line":
+    case "area":
+    case "waterfall":
+      return xScale.lineAccessor as XYAccessor;
+    default:
+      exhaustiveCheck(type);
   }
-  exhaustiveCheck(type);
 }
 
 function exhaustiveCheck(param: never): never {
@@ -273,37 +284,52 @@ function exhaustiveCheck(param: never): never {
 function transformDataToValues(
   type: VisualizationType,
   data: (SeriesDatum | StackedDatum)[],
+  settings?: Settings,
 ): Value[] {
-  if (type === "line") {
-    return data.map((datum, index) => {
-      // Use the similar logic as presented in https://github.com/metabase/metabase/blob/3f4ca9c70bd263a7579613971ea8d7c47b1f776e/frontend/src/metabase/visualizations/lib/chart_values.js#L130
-      const previousValue = data[index - 1];
-      const nextValue = data[index + 1];
-      const showLabelBelow =
-        // first point or prior is greater than y
-        (index === 0 || getY(previousValue) > getY(datum)) &&
-        // last point point or next is greater than y
-        (index >= data.length - 1 || getY(nextValue) > getY(datum));
-
-      return {
-        datum,
-        flipped: showLabelBelow,
-      };
-    });
-  }
-
-  if (type === "bar") {
-    return data.map(datum => {
-      const isNegative = getY(datum) < 0;
-      return { datum, flipped: isNegative };
-    });
+  switch (type) {
+    case "line":
+      return data.map((datum, index) => {
+        // Use the similar logic as presented in https://github.com/metabase/metabase/blob/3f4ca9c70bd263a7579613971ea8d7c47b1f776e/frontend/src/metabase/visualizations/lib/chart_values.js#L130
+        const previousValue = data[index - 1];
+        const nextValue = data[index + 1];
+        const showLabelBelow =
+          // first point or prior is greater than y
+          (index === 0 || getY(previousValue) > getY(datum)) &&
+          // last point point or next is greater than y
+          (index >= data.length - 1 || getY(nextValue) > getY(datum));
+
+        return {
+          datum,
+          datumForLabel: datum,
+          flipped: showLabelBelow,
+        };
+      });
+    case "bar":
+      return data.map(datum => {
+        const isNegative = getY(datum) < 0;
+        return { datum, datumForLabel: datum, flipped: isNegative };
+      });
+    case "area":
+      return data.map(datum => {
+        return {
+          datum,
+          datumForLabel: datum,
+        };
+      });
+    case "waterfall": {
+      let total = 0;
+      return data.map((datum, index) => {
+        total = total + getY(datum);
+        const isShowingTotal = settings?.showTotal && index === data.length - 1;
+        return {
+          datum: !isShowingTotal ? setY(datum, total) : datum,
+          datumForLabel: datum,
+        };
+      });
+    }
+    default:
+      exhaustiveCheck(type);
   }
-
-  return data.map(datum => {
-    return {
-      datum,
-    };
-  });
 }
 
 interface Position {
diff --git a/frontend/src/metabase/static-viz/components/XYChart/Values/index.ts b/frontend/src/metabase/static-viz/components/Values/index.ts
similarity index 100%
rename from frontend/src/metabase/static-viz/components/XYChart/Values/index.ts
rename to frontend/src/metabase/static-viz/components/Values/index.ts
diff --git a/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.jsx b/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f5e338cf50ef6223c714f65112ab3025cb43f20d
--- /dev/null
+++ b/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.jsx
@@ -0,0 +1,343 @@
+import React from "react";
+import { AxisBottom, AxisLeft } from "@visx/axis";
+import { GridRows } from "@visx/grid";
+import { scaleBand, scaleLinear } from "@visx/scale";
+import { Bar } from "@visx/shape";
+import { Group } from "@visx/group";
+import { Text } from "@visx/text";
+import { assoc, merge } from "icepick";
+import {
+  getLabelProps,
+  getXTickLabelProps,
+  getXTickWidth,
+  getYTickLabelProps,
+  getYTickWidth,
+} from "metabase/static-viz/lib/axes";
+import { formatNumber } from "metabase/static-viz/lib/numbers";
+import {
+  calculateWaterfallDomain,
+  calculateWaterfallEntries,
+  calculateWaterfallSeriesForValues,
+  formatTimescaleWaterfallTick,
+  getWaterfallEntryColor,
+} from "metabase/static-viz/lib/waterfall";
+import { measureTextHeight, truncateText } from "metabase/static-viz/lib/text";
+import { sortTimeSeries } from "../../lib/sort";
+import {
+  DATE_ACCESSORS,
+  POSITIONAL_ACCESSORS,
+} from "../../constants/accessors";
+import { getWaterfallColors } from "../../lib/colors";
+import Values from "../Values";
+import { createXScale } from "../XYChart/utils";
+
+const layout = {
+  width: 540,
+  height: 300,
+  margin: {
+    // Add some margin so when the chart scale down,
+    // elements that are rendered at the top of the chart doesn't get cut off
+    top: 12,
+    left: 55,
+    right: 40,
+  },
+  barPadding: 0.2,
+  labelPadding: 12,
+  strokeDasharray: "4",
+  numTicks: 4,
+  maxTickWidth: 100,
+};
+
+// If inner chart area is smaller than this then it will look cramped
+const MIN_INNER_HEIGHT = 250;
+const MAX_EXTRA_HEIGHT = 40;
+// The default value for tick length
+const TICK_LENGTH = 8;
+const VALUES_MARGIN = 6;
+
+// Since we're using JSDoc instead, which provide more static type checking than PropTypes.
+/* eslint-disable react/prop-types */
+/**
+ *
+ * @param {import("./types").WaterfallChartProps} props
+ * @returns {JSX.Element}
+ */
+function WaterfallChart({
+  data,
+  type,
+  accessors = getDefaultAccessors(type),
+  settings,
+  labels,
+  getColor,
+}) {
+  const chartStyle = {
+    fontFamily: "Lato, sans-serif",
+    axes: {
+      color: getColor("text-light"),
+      ticks: {
+        color: getColor("text-medium"),
+        fontSize: 12,
+      },
+      labels: {
+        color: getColor("text-medium"),
+        fontSize: 14,
+        fontWeight: 700,
+      },
+    },
+    value: {
+      color: getColor("text-dark"),
+      fontSize: 12,
+      fontWeight: 800,
+      stroke: getColor("white"),
+      strokeWidth: 3,
+    },
+  };
+
+  const axesProps = {
+    stroke: chartStyle.axes.color,
+    tickStroke: chartStyle.axes.color,
+  };
+
+  const valueProps = {
+    fontSize: chartStyle.value?.fontSize,
+    fontFamily: chartStyle.fontFamily,
+    fontWeight: chartStyle.value?.fontWeight,
+    letterSpacing: 0.5,
+    fill: chartStyle.value?.color,
+    stroke: chartStyle.value?.stroke,
+    strokeWidth: chartStyle.value?.strokeWidth,
+  };
+
+  if (type === "timeseries") {
+    data = sortTimeSeries(data);
+  }
+  const entries = calculateWaterfallEntries(
+    data,
+    accessors,
+    settings?.showTotal,
+  );
+
+  const isVertical = type === "timeseries" ? false : entries.length > 10;
+  const xTickWidth =
+    type === "timeseries"
+      ? // We don't know the width of the time-series label because it's formatted inside `<AxisBottom />`.
+        // We could extract those logic out, but it's gonna be nasty, and we didn't need `xTickWidth` for time-series anyway.
+        0
+      : getXTickWidth(
+          data,
+          accessors,
+          layout.maxTickWidth,
+          chartStyle.axes.ticks.fontSize,
+        );
+
+  const getXTickProps =
+    type === "timeseries"
+      ? ({ formattedValue, ...props }) => {
+          return {
+            ...props,
+            children: formatTimescaleWaterfallTick(formattedValue, settings),
+          };
+        }
+      : ({ x, y, formattedValue, ...props }) => {
+          const textWidth = isVertical ? xTickWidth : xScale.bandwidth();
+          const truncatedText = truncateText(
+            formattedValue,
+            textWidth,
+            chartStyle.axes.ticks.fontSize,
+          );
+          const xTickFontSize = chartStyle.axes.ticks.fontSize;
+          const transform = isVertical
+            ? `rotate(-90, ${x} ${y}) translate(${Math.floor(
+                xTickFontSize * 0.9,
+              )} ${Math.floor(xTickFontSize / 3)})`
+            : undefined;
+
+          const textAnchor = isVertical ? "end" : "middle";
+
+          return {
+            ...props,
+            x,
+            y,
+            transform,
+            children: truncatedText,
+            textAnchor,
+          };
+        };
+
+  const numTicks = type === "timeseries" ? layout.numTicks : entries.length;
+  const tickLabelProps = getXTickLabelProps(chartStyle, isVertical);
+  const topMargin = settings.show_values
+    ? layout.margin.top + VALUES_MARGIN
+    : layout.margin.top;
+
+  const xTickHeight = isVertical ? xTickWidth : chartStyle.axes.ticks.fontSize;
+  const yTickWidth = getYTickWidth(
+    data,
+    accessors,
+    settings,
+    chartStyle.axes.ticks.fontSize,
+  );
+  const yLabelOffset = yTickWidth + layout.labelPadding;
+  const xMin = yLabelOffset + chartStyle.axes.labels.fontSize * 1.5;
+  const xMax = layout.width - layout.margin.right - layout.margin.left;
+  const xAxisHeight = getXAxisHeight(
+    xTickHeight,
+    measureTextHeight(chartStyle.axes.labels.fontSize),
+  );
+  let yMax = layout.height - xAxisHeight - topMargin;
+  let height = layout.height;
+  // If inner chart area is too short, try to expand it but not more than `MAX_EXTRA_HEIGHT`
+  // to match what we do with XYChart (with legends, it can be up to 340px tall)
+  if (yMax < MIN_INNER_HEIGHT) {
+    yMax =
+      minMax(
+        layout.height,
+        layout.height + MAX_EXTRA_HEIGHT,
+        yMax + xAxisHeight + MAX_EXTRA_HEIGHT,
+      ) - xAxisHeight;
+    height = topMargin + yMax + xAxisHeight;
+  }
+  const leftLabel = labels?.left;
+
+  const xScale = scaleBand({
+    domain: entries.map(entry => entry.x),
+    range: [0, xMax],
+    padding: layout.barPadding,
+  });
+
+  const yScale = scaleLinear({
+    domain: calculateWaterfallDomain(entries),
+    range: [yMax, 0],
+  });
+
+  const getBarProps = entry => {
+    const width = xScale.bandwidth();
+
+    const height = Math.abs(yScale(entry.start) - yScale(entry.end));
+    const x = xScale(entry.x);
+    const y = yScale(Math.max(entry.start, entry.end));
+
+    const fill = getWaterfallEntryColor(
+      entry,
+      getWaterfallColors(settings?.colors, getColor),
+    );
+
+    return { x, y, width, height, fill };
+  };
+
+  // Used only for rendering data point values
+  const series = calculateWaterfallSeriesForValues(
+    data,
+    accessors,
+    settings?.showTotal,
+  );
+
+  return (
+    <svg width={layout.width} height={height}>
+      <Group top={topMargin} left={xMin}>
+        <GridRows
+          scale={yScale}
+          width={xMax}
+          strokeDasharray={layout.strokeDasharray}
+        />
+      </Group>
+
+      <AxisLeft
+        scale={yScale}
+        top={topMargin}
+        left={xMin}
+        label={leftLabel}
+        labelOffset={yLabelOffset}
+        hideTicks
+        hideAxisLine
+        labelProps={getLabelProps(chartStyle)}
+        tickFormat={value => formatNumber(value, settings?.y)}
+        tickLabelProps={() => getYTickLabelProps(chartStyle)}
+        {...axesProps}
+      />
+      <AxisBottom
+        scale={xScale}
+        left={xMin}
+        top={yMax + topMargin}
+        label={labels?.bottom}
+        tickLength={TICK_LENGTH}
+        numTicks={numTicks}
+        labelProps={merge(getLabelProps(chartStyle, getColor), {
+          dy:
+            -measureTextHeight(chartStyle.axes.labels.fontSize) +
+            xTickHeight +
+            layout.labelPadding,
+        })}
+        tickComponent={props => <Text {...getXTickProps(props)} />}
+        tickLabelProps={() => tickLabelProps}
+        {...axesProps}
+      />
+      <Group top={topMargin} left={xMin}>
+        {entries.map((entry, index) => (
+          <Bar key={index} {...getBarProps(entry)} />
+        ))}
+        {settings.show_values && (
+          <Values
+            series={series}
+            formatter={(value, compact) =>
+              formatNumber(value, maybeAssoc(settings.y, "compact", compact))
+            }
+            valueProps={valueProps}
+            xScale={createXScale(series, [0, xMax], "ordinal")}
+            yScaleLeft={yScale}
+            yScaleRight={null}
+            innerWidth={xMax}
+            xAxisYPos={yMax}
+            settings={settings}
+          />
+        )}
+      </Group>
+    </svg>
+  );
+}
+
+/**
+ *
+ * @param {'timeseries'|'categorical'} type
+ * @returns {typeof POSITIONAL_ACCESSORS}
+ */
+function getDefaultAccessors(type) {
+  if (type === "timeseries") {
+    return DATE_ACCESSORS;
+  }
+
+  if (type === "categorical") {
+    return POSITIONAL_ACCESSORS;
+  }
+}
+
+/**
+ *
+ * @param {number} xTickHeight
+ * @param {number} xLabelHeight
+ * @returns {number} The height of the X-axis section of the chart
+ */
+function getXAxisHeight(xTickHeight, xLabelHeight) {
+  // x-axis height = tick length (dash) + tick label height + label padding + label height
+  return TICK_LENGTH + xTickHeight + layout.labelPadding + xLabelHeight;
+}
+
+/**
+ *
+ * @param {number} min
+ * @param {number} max
+ * @param {number} value
+ */
+function minMax(min, max, value) {
+  return Math.min(max, Math.max(min, value));
+}
+
+const maybeAssoc = (collection, key, value) => {
+  if (collection == null) {
+    return collection;
+  }
+
+  return assoc(collection, key, value);
+};
+
+export default WaterfallChart;
diff --git a/frontend/src/metabase/static-viz/components/WaterfallChart/constants.ts b/frontend/src/metabase/static-viz/components/WaterfallChart/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..208c2147372d5a2c6d372a30af2b9e0e3c7b7c9b
--- /dev/null
+++ b/frontend/src/metabase/static-viz/components/WaterfallChart/constants.ts
@@ -0,0 +1,47 @@
+export const WATERFALL_CHART_TYPE = "waterfall";
+
+export const TIME_SERIES_WATERFALL_CHART_DEFAULT_OPTIONS = {
+  data: [
+    ["2020-10-20", 20],
+    ["2020-10-21", 20],
+    ["2020-10-22", 100],
+    ["2020-10-23", -10],
+    ["2020-10-24", 20],
+    ["2020-10-25", -30],
+    ["2020-10-26", -10],
+    ["2020-10-27", 20],
+    ["2020-10-28", -15],
+  ],
+  settings: {
+    show_values: true,
+  },
+  labels: {
+    left: "Count",
+    bottom: "Created At",
+  },
+  type: "timeseries",
+};
+
+export const CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS = {
+  data: [
+    ["Stage 1", 800],
+    ["Stage 2", 400],
+    ["Stage 3", -300],
+    ["Stage 4", -100],
+    ["Stage 5", -50],
+    ["Stage 6", 200],
+    ["Stage 7", -100],
+    ["Stage 8", 300],
+    ["Stage 9", 100],
+    ["Stage 10", -300],
+  ],
+  settings: {
+    showTotal: true,
+    show_values: true,
+  },
+  labels: {
+    left: "Count",
+    bottom: "Created At",
+  },
+  type: "categorical",
+};
diff --git a/frontend/src/metabase/static-viz/components/WaterfallChart/index.js b/frontend/src/metabase/static-viz/components/WaterfallChart/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..57657039be879d425437250ea357c5396e3cb3d2
--- /dev/null
+++ b/frontend/src/metabase/static-viz/components/WaterfallChart/index.js
@@ -0,0 +1 @@
+export { default } from "./WaterfallChart";
diff --git a/frontend/src/metabase/static-viz/components/WaterfallChart/types.ts b/frontend/src/metabase/static-viz/components/WaterfallChart/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a7614473d59aba40c3212989066d3dd2fd2e25e4
--- /dev/null
+++ b/frontend/src/metabase/static-viz/components/WaterfallChart/types.ts
@@ -0,0 +1,25 @@
+import type { ColorGetter } from "metabase/static-viz/lib/colors";
+
+type XYAccessor = (row: any[]) => any;
+
+export interface WaterfallChartProps {
+  data: any[];
+  accessors: {
+    x: XYAccessor;
+    y: XYAccessor;
+  };
+
+  settings: {
+    x: object;
+    y: object;
+    colors: object;
+    showTotal: boolean;
+    show_values: boolean;
+  };
+  labels: {
+    left: string;
+    bottom: string;
+  };
+  getColor: ColorGetter;
+  type: "categorical" | "timeseries";
+}
diff --git a/frontend/src/metabase/static-viz/components/XYChart/XYChart.tsx b/frontend/src/metabase/static-viz/components/XYChart/XYChart.tsx
index 11a1c6a2b26b88da94ccbb5263104d9fe7cefb15..659f9c1b23c681ebd14f5c19b8afc13148459f6f 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/XYChart.tsx
+++ b/frontend/src/metabase/static-viz/components/XYChart/XYChart.tsx
@@ -39,7 +39,7 @@ import type {
   ChartStyle,
   HydratedSeries,
 } from "metabase/static-viz/components/XYChart/types";
-import Values from "./Values";
+import Values from "../Values";
 
 export interface XYChartProps {
   width: number;
diff --git a/frontend/src/metabase/static-viz/components/XYChart/types.ts b/frontend/src/metabase/static-viz/components/XYChart/types.ts
index f1b27b3052463aa1f7d246decdc0590a2ce012dc..11e4a9eba2404654adc67b4b4146dc146e18dd6d 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/types.ts
+++ b/frontend/src/metabase/static-viz/components/XYChart/types.ts
@@ -15,7 +15,7 @@ export type YAxisType = "linear" | "pow" | "log";
 
 export type YAxisPosition = "left" | "right";
 
-export type VisualizationType = "line" | "area" | "bar";
+export type VisualizationType = "line" | "area" | "bar" | "waterfall";
 
 export type Series = {
   name: string;
diff --git a/frontend/src/metabase/static-viz/components/XYChart/utils/scales.ts b/frontend/src/metabase/static-viz/components/XYChart/utils/scales.ts
index ee3493f85af1f0e6d7d7e2039e260d79a4709059..ccdbf80499e85050eef40ff67ad59c3be3c12e58 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/utils/scales.ts
+++ b/frontend/src/metabase/static-viz/components/XYChart/utils/scales.ts
@@ -42,7 +42,7 @@ export const createXScale = (
     const xScale = scaleBand({
       domain,
       range,
-      padding: 0.1,
+      padding: 0.2,
     });
 
     return {
diff --git a/frontend/src/metabase/static-viz/components/XYChart/utils/series.ts b/frontend/src/metabase/static-viz/components/XYChart/utils/series.ts
index f164df89b7d663d53e9b6833ea1955a17e2db316..c5bfe0549ea5913c4939020eb4ab793891730372 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/utils/series.ts
+++ b/frontend/src/metabase/static-viz/components/XYChart/utils/series.ts
@@ -8,6 +8,14 @@ import {
 
 export const getX = (d: SeriesDatum | StackedDatum) => d[0];
 export const getY = (d: SeriesDatum | StackedDatum) => d[1];
+export const setY = <T extends SeriesDatum | StackedDatum>(
+  d: T,
+  value: number,
+): T => {
+  const newDatum = [...d];
+  newDatum[1] = value;
+  return newDatum as T;
+};
 
 export const getY1 = (d: StackedDatum) => d[2];
 
diff --git a/frontend/src/metabase/static-viz/components/XYChart/utils/ticks.ts b/frontend/src/metabase/static-viz/components/XYChart/utils/ticks.ts
index 16cb21820d44d4f9dd2d10364be76fd9224151d5..fbc2d909ae15bc21f254b7e939b875d404281532 100644
--- a/frontend/src/metabase/static-viz/components/XYChart/utils/ticks.ts
+++ b/frontend/src/metabase/static-viz/components/XYChart/utils/ticks.ts
@@ -25,7 +25,7 @@ import type {
   ChartSettings,
 } from "metabase/static-viz/components/XYChart/types";
 
-export const getRotatedXTickHeight = (tickWidth: number) => {
+const getRotatedXTickHeight = (tickWidth: number) => {
   return tickWidth;
 };
 
diff --git a/frontend/src/metabase/static-viz/containers/StaticChart/StaticChart.tsx b/frontend/src/metabase/static-viz/containers/StaticChart/StaticChart.tsx
index f023616b48249efe476a74e54bc093d90e5caabb..9f02491ee2828ed4409f1c3ad77f1c5191b049d3 100644
--- a/frontend/src/metabase/static-viz/containers/StaticChart/StaticChart.tsx
+++ b/frontend/src/metabase/static-viz/containers/StaticChart/StaticChart.tsx
@@ -2,10 +2,8 @@ import React from "react";
 import { createColorGetter } from "metabase/static-viz/lib/colors";
 import CategoricalDonutChart from "../../components/CategoricalDonutChart";
 import { CATEGORICAL_DONUT_CHART_TYPE } from "../../components/CategoricalDonutChart/constants";
-import CategoricalWaterfallChart from "../../components/CategoricalWaterfallChart";
-import { CATEGORICAL_WATERFALL_CHART_TYPE } from "../../components/CategoricalWaterfallChart/constants";
-import TimeSeriesWaterfallChart from "../../components/TimeSeriesWaterfallChart";
-import { TIME_SERIES_WATERFALL_CHART_TYPE } from "../../components/TimeSeriesWaterfallChart/constants";
+import WaterfallChart from "../../components/WaterfallChart";
+import { WATERFALL_CHART_TYPE } from "../../components/WaterfallChart/constants";
 import ProgressBar from "../../components/ProgressBar";
 import { PROGRESS_BAR_TYPE } from "../../components/ProgressBar/constants";
 import LineAreaBarChart from "../../components/LineAreaBarChart";
@@ -21,10 +19,8 @@ const StaticChart = ({ type, options }: StaticChartProps) => {
   switch (type) {
     case CATEGORICAL_DONUT_CHART_TYPE:
       return <CategoricalDonutChart {...chartProps} />;
-    case CATEGORICAL_WATERFALL_CHART_TYPE:
-      return <CategoricalWaterfallChart {...chartProps} />;
-    case TIME_SERIES_WATERFALL_CHART_TYPE:
-      return <TimeSeriesWaterfallChart {...chartProps} />;
+    case WATERFALL_CHART_TYPE:
+      return <WaterfallChart {...chartProps} />;
     case PROGRESS_BAR_TYPE:
       return <ProgressBar {...chartProps} />;
     case LINE_AREA_BAR_CHART_TYPE:
diff --git a/frontend/src/metabase/static-viz/containers/StaticChart/constants.ts b/frontend/src/metabase/static-viz/containers/StaticChart/constants.ts
index 189d86d257fe98b45a0613bed9408df2114a30c0..b66d9dd24a353e576b318b3ee3d2375af004607a 100644
--- a/frontend/src/metabase/static-viz/containers/StaticChart/constants.ts
+++ b/frontend/src/metabase/static-viz/containers/StaticChart/constants.ts
@@ -2,14 +2,11 @@ import {
   CATEGORICAL_DONUT_CHART_DEFAULT_OPTIONS,
   CATEGORICAL_DONUT_CHART_TYPE,
 } from "../../components/CategoricalDonutChart/constants";
-import {
-  CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS,
-  CATEGORICAL_WATERFALL_CHART_TYPE,
-} from "../../components/CategoricalWaterfallChart/constants";
 import {
   TIME_SERIES_WATERFALL_CHART_DEFAULT_OPTIONS,
-  TIME_SERIES_WATERFALL_CHART_TYPE,
-} from "../../components/TimeSeriesWaterfallChart/constants";
+  CATEGORICAL_WATERFALL_CHART_DEFAULT_OPTIONS,
+  WATERFALL_CHART_TYPE,
+} from "../../components/WaterfallChart/constants";
 import {
   PROGRESS_BAR_DEFAULT_DATA_1,
   PROGRESS_BAR_TYPE,
@@ -25,8 +22,7 @@ import {
 
 export const STATIC_CHART_TYPES = [
   CATEGORICAL_DONUT_CHART_TYPE,
-  CATEGORICAL_WATERFALL_CHART_TYPE,
-  TIME_SERIES_WATERFALL_CHART_TYPE,
+  WATERFALL_CHART_TYPE,
   PROGRESS_BAR_TYPE,
   LINE_AREA_BAR_CHART_TYPE,
   FUNNEL_CHART_TYPE,
diff --git a/frontend/src/metabase/static-viz/lib/axes.js b/frontend/src/metabase/static-viz/lib/axes.js
index a6cf5f96ab600d6a6fde69a4883a2bf9cbb05a29..cb722a9d07d4b11fa46425da8706ebe85ddea1aa 100644
--- a/frontend/src/metabase/static-viz/lib/axes.js
+++ b/frontend/src/metabase/static-viz/lib/axes.js
@@ -14,10 +14,6 @@ export const getXTickWidthFromValues = (values, maxWidth, fontSize) => {
   return Math.min(tickWidth, maxWidth);
 };
 
-export const getRotatedXTickHeight = tickWidth => {
-  return Math.ceil(Math.sqrt(Math.pow(tickWidth, 2) / 2));
-};
-
 export const getYTickWidth = (data, accessors, settings, fontSize) => {
   return data
     .map(accessors.y)
@@ -26,24 +22,37 @@ export const getYTickWidth = (data, accessors, settings, fontSize) => {
     .reduce((a, b) => Math.max(a, b), 0);
 };
 
-export const getXTickLabelProps = (layout, isVertical, getColor) => ({
-  fontSize: layout.font.size,
-  fontFamily: layout.font.family,
-  fill: getColor("text-medium"),
+/**
+ *
+ * @param {import("../components/XYChart/types").ChartStyle} chartStyle
+ * @param {boolean} isVertical
+ */
+export const getXTickLabelProps = (chartStyle, isVertical) => ({
+  fontFamily: chartStyle.fontFamily,
+  fontSize: chartStyle.axes.ticks.fontSize,
+  fill: chartStyle.axes.ticks.color,
   textAnchor: isVertical ? "start" : "middle",
 });
 
-export const getYTickLabelProps = (layout, getColor) => ({
-  fontSize: layout.font.size,
-  fontFamily: layout.font.family,
-  fill: getColor("text-medium"),
+/**
+ *
+ * @param {import("../components/XYChart/types").ChartStyle} chartStyle
+ */
+export const getYTickLabelProps = chartStyle => ({
+  fontFamily: chartStyle.fontFamily,
+  fontSize: chartStyle.axes.ticks.fontSize,
+  fill: chartStyle.axes.ticks.color,
   textAnchor: "end",
 });
 
-export const getLabelProps = (layout, getColor) => ({
-  fontWeight: layout.labelFontWeight,
-  fontSize: layout.font.size,
-  fontFamily: layout.font.family,
-  fill: getColor("text-medium"),
+/**
+ *
+ * @param {import("../components/XYChart/types").ChartStyle} chartStyle
+ */
+export const getLabelProps = chartStyle => ({
+  fontFamily: chartStyle.fontFamily,
+  fontWeight: chartStyle.axes.labels.fontWeight,
+  fontSize: chartStyle.axes.labels.fontSize,
+  fill: chartStyle.axes.labels.color,
   textAnchor: "middle",
 });
diff --git a/frontend/src/metabase/static-viz/lib/axes.unit.spec.js b/frontend/src/metabase/static-viz/lib/axes.unit.spec.js
index 4f348f1af78c0d8757654332facbf02800aec52d..fcc2942e4f3412d89f7c54a5b8baf7132ed1b69d 100644
--- a/frontend/src/metabase/static-viz/lib/axes.unit.spec.js
+++ b/frontend/src/metabase/static-viz/lib/axes.unit.spec.js
@@ -1,4 +1,4 @@
-import { getRotatedXTickHeight, getXTickWidth, getYTickWidth } from "./axes";
+import { getXTickWidth, getYTickWidth } from "./axes";
 
 const fontSize = 11;
 
@@ -14,12 +14,6 @@ describe("getXTickWidth", () => {
   });
 });
 
-describe("getRotatedXTickHeight", () => {
-  it("should get tick height by width assuming 45deg rotation", () => {
-    expect(getRotatedXTickHeight(12)).toBe(9);
-  });
-});
-
 describe("getYTickWidth", () => {
   it("should get tick width for y axis assuming 6px char width", () => {
     const data = [{ y: 1 }, { y: 20 }, { y: 15 }];
diff --git a/frontend/src/metabase/static-viz/lib/waterfall.js b/frontend/src/metabase/static-viz/lib/waterfall.js
index 0118f5362602cdb9ab02268f0c67dafbbb444fdf..420802806674a61af1fc6264008e4946759c1c7d 100644
--- a/frontend/src/metabase/static-viz/lib/waterfall.js
+++ b/frontend/src/metabase/static-viz/lib/waterfall.js
@@ -34,6 +34,31 @@ export const calculateWaterfallEntries = (data, accessors, showTotal) => {
   return entries;
 };
 
+export const calculateWaterfallSeriesForValues = (
+  data,
+  accessors,
+  showTotal,
+) => {
+  if (!showTotal) {
+    return [
+      {
+        data,
+        type: "waterfall",
+        yAxisPosition: "left",
+      },
+    ];
+  } else {
+    const total = data.reduce((sum, datum) => sum + accessors.y(datum), 0);
+    return [
+      {
+        data: [...data, [WATERFALL_TOTAL, total]],
+        type: "waterfall",
+        yAxisPosition: "left",
+      },
+    ];
+  }
+};
+
 export const formatTimescaleWaterfallTick = (value, settings) =>
   value === WATERFALL_TOTAL ? WATERFALL_TOTAL : formatDate(value, settings?.x);
 
diff --git a/frontend/test/metabase-visual/static-visualizations/waterfall.cy.spec.js b/frontend/test/metabase-visual/static-visualizations/waterfall.cy.spec.js
index c46c2b90894772845e8df7d06393c796301207ac..3748ca1763a08eb8829070e6b2cb0aeb7d61df83 100644
--- a/frontend/test/metabase-visual/static-visualizations/waterfall.cy.spec.js
+++ b/frontend/test/metabase-visual/static-visualizations/waterfall.cy.spec.js
@@ -46,7 +46,9 @@ function createWaterfallQuestion({ showTotal } = {}) {
         "SELECT * FROM ( VALUES ('Stage 1', 10), ('Stage 2', 30), ('Stage 3', -50), ('Stage 4', -10), ('Stage 5', 80), ('Stage 6', 10), ('Stage 7', 15))",
       "template-tags": {},
     },
-    visualization_settings: {},
+    visualization_settings: {
+      "graph.show_values": true,
+    },
     display: "waterfall",
     database: SAMPLE_DB_ID,
   };
diff --git a/resources/frontend_shared/static_viz_interface.js b/resources/frontend_shared/static_viz_interface.js
index c6a7b3855e9bf74417016dbbb5f844a817ed2dc6..d4cc044a36c157ea7f7df051dd40059ea3919100 100644
--- a/resources/frontend_shared/static_viz_interface.js
+++ b/resources/frontend_shared/static_viz_interface.js
@@ -23,11 +23,12 @@ function combo_chart(series, settings, colors) {
   });
 }
 
-function timeseries_waterfall(data, labels, settings, instanceColors) {
-  return StaticViz.RenderChart("timeseries/waterfall", {
+function waterfall(data, labels, settings, waterfallType, instanceColors) {
+  return StaticViz.RenderChart("waterfall", {
     data: toJSArray(data),
     labels: toJSMap(labels),
     settings: JSON.parse(settings),
+    type: waterfallType,
     colors: JSON.parse(instanceColors),
   });
 }
@@ -46,15 +47,6 @@ function categorical_donut(rows, colors) {
   });
 }
 
-function categorical_waterfall(data, labels, settings, instanceColors) {
-  return StaticViz.RenderChart("categorical/waterfall", {
-    data: toJSArray(data),
-    labels: toJSMap(labels),
-    settings: JSON.parse(settings),
-    colors: JSON.parse(instanceColors),
-  });
-}
-
 function progress(data, settings) {
   return StaticViz.RenderChart("progress", {
     data: JSON.parse(data),
diff --git a/src/metabase/pulse/render/body.clj b/src/metabase/pulse/render/body.clj
index da63b66d096771c2708e72072d3907222db01efc..88803cbb03f615b78527ee36cee2768e387b53af 100644
--- a/src/metabase/pulse/render/body.clj
+++ b/src/metabase/pulse/render/body.clj
@@ -347,7 +347,7 @@
 
 (defn- x-and-y-axis-label-info
   "Generate the X and Y axis labels passed in as the `labels` argument
-  to [[metabase.pulse.render.js-svg/timelineseries-waterfall]] and other similar functions for rendering charts with X and Y
+  to [[metabase.pulse.render.js-svg/waterfall]] and other similar functions for rendering charts with X and Y
   axes. Respects custom display names in `viz-settings`; otherwise uses `x-col` and `y-col` display names."
   [x-col y-col viz-settings]
   {:bottom (or (:graph.x_axis.title_text viz-settings)
@@ -763,9 +763,9 @@
         rows           (map (juxt x-axis-rowfn y-axis-rowfn)
                             (common/row-preprocess x-axis-rowfn y-axis-rowfn rows))
         labels         (x-and-y-axis-label-info x-col y-col viz-settings)
-        render-fn      (if (isa? (-> cols x-axis-rowfn :effective_type) :type/Temporal)
-                         js-svg/timelineseries-waterfall
-                         js-svg/categorical-waterfall)
+        waterfall-type (if (isa? (-> cols x-axis-rowfn :effective_type) :type/Temporal)
+                         :timeseries
+                         :categorical)
         show-total     (if (nil? (:waterfall.show_total viz-settings))
                          true
                          (:waterfall.show_total viz-settings))
@@ -774,12 +774,14 @@
                                    :waterfallTotal (:waterfall.total_color viz-settings)
                                    :waterfallPositive (:waterfall.increase_color viz-settings)
                                    :waterfallNegative (:waterfall.decrease_color viz-settings))
-                           (assoc :showTotal show-total))
+                           (assoc :showTotal show-total)
+                           (assoc :show_values (boolean (:graph.show_values viz-settings))))
         image-bundle   (image-bundle/make-image-bundle
                         render-type
-                        (render-fn rows
-                                   labels
-                                   settings))]
+                        (js-svg/waterfall rows
+                                          labels
+                                          settings
+                                          waterfall-type))]
     {:attachments
      (when image-bundle
        (image-bundle/image-bundle->attachment image-bundle))
diff --git a/src/metabase/pulse/render/js_svg.clj b/src/metabase/pulse/render/js_svg.clj
index 01b9d7d1558bd3bc51bb2bb4a0f18ff23881d08b..8c036fcc2e86a47ba454064727fc911eee480f33 100644
--- a/src/metabase/pulse/render/js_svg.clj
+++ b/src/metabase/pulse/render/js_svg.clj
@@ -101,13 +101,14 @@
 (defn- svg-string->bytes [s]
   (-> s parse-svg-string render-svg))
 
-(defn timelineseries-waterfall
-  "Clojure entrypoint to render a timeseries waterfall chart. Rows should be tuples of [datetime numeric-value]. Labels is
+(defn waterfall
+  "Clojure entrypoint to render a timeseries or categorical waterfall chart. Rows should be tuples of [datetime numeric-value]. Labels is
   a map of {:left \"left-label\" :botton \"bottom-label\". Returns a byte array of a png file."
-  [rows labels settings]
-  (let [svg-string (.asString (js/execute-fn-name @context "timeseries_waterfall" rows
+  [rows labels settings waterfall-type]
+  (let [svg-string (.asString (js/execute-fn-name @context "waterfall" rows
                                                   (map (fn [[k v]] [(name k) v]) labels)
                                                   (json/generate-string settings)
+                                                  (name waterfall-type)
                                                   (json/generate-string (public-settings/application-colors))))]
     (svg-string->bytes svg-string)))
 
@@ -134,16 +135,6 @@
                                                   (json/generate-string (:colors settings))))]
     (svg-string->bytes svg-string)))
 
-(defn categorical-waterfall
-  "Clojure entrypoint to render a categorical waterfall chart. Rows should be tuples of [stringable numeric-value]. Labels is
-  a map of {:left \"left-label\" :botton \"bottom-label\". Returns a byte array of a png file."
-  [rows labels settings]
-  (let [svg-string (.asString (js/execute-fn-name @context "categorical_waterfall" rows
-                                                  (map (fn [[k v]] [(name k) v]) labels)
-                                                  (json/generate-string settings)
-                                                  (json/generate-string (public-settings/application-colors))))]
-    (svg-string->bytes svg-string)))
-
 (defn categorical-donut
   "Clojure entrypoint to render a categorical donut chart. Rows should be tuples of [category numeric-value]. Returns a
   byte array of a png file"
diff --git a/test/metabase/pulse/render/js_svg_test.clj b/test/metabase/pulse/render/js_svg_test.clj
index e4ae6d543a975ba981662ff88f941f0cec01ece8..05dacd5cf5c83bfc541d928e9cc0b23fdd28df5b 100644
--- a/test/metabase/pulse/render/js_svg_test.clj
+++ b/test/metabase/pulse/render/js_svg_test.clj
@@ -9,7 +9,6 @@
             [clojure.set :as set]
             [clojure.spec.alpha :as s]
             [clojure.test :refer :all]
-            [metabase.public-settings :as public-settings]
             [metabase.pulse.render.js-engine :as js]
             [metabase.pulse.render.js-svg :as js-svg])
   (:import org.apache.batik.anim.dom.SVGOMDocument
@@ -232,18 +231,36 @@
     (testing "A goal line does exist when goal settings are present in the viz-settings"
       (is (= goal-label (second goal-node)))))))
 
-(deftest timelineseries-waterfall-test
-  (let [rows     [[#t "2020" 2]
-                  [#t "2021" 3]]
-        labels   {:left "count" :bottom "year"}
-        settings (json/generate-string {:y {:prefix   "prefix"
-                                            :decimals 4}})]
-    (testing "It returns bytes"
-      (let [svg-bytes (js-svg/timelineseries-waterfall rows labels settings)]
-        (is (bytes? svg-bytes))))
-    (let [svg-string (.asString (js/execute-fn-name @context "timeseries_waterfall" rows labels settings (json/generate-string (public-settings/application-colors))))]
-      (testing "it returns a valid svg string (no html in it)"
-        (validate-svg-string :timelineseries-waterfall svg-string)))))
+(deftest waterfall-test
+  (testing "Timeseries Waterfall renders"
+    (let [rows           [[#t "2020" 2]
+                          [#t "2021" 3]]
+          labels         {:left "count" :bottom "year"}
+          settings       (json/generate-string {:y {:prefix   "prefix"
+                                                    :decimals 4}})
+          waterfall-type (name :timeseries)]
+      (testing "It returns bytes"
+        (let [svg-bytes (js-svg/waterfall rows labels settings waterfall-type)]
+          (is (bytes? svg-bytes))))
+      (let [svg-string (.asString (js/execute-fn-name @context "waterfall"
+                                                      rows labels settings waterfall-type
+                                                      (json/generate-string {})))]
+        (testing "it returns a valid svg string (no html in it)"
+          (validate-svg-string :timelineseries-waterfall svg-string)))))
+  (testing "Categorical Waterfall renders"
+    (let [rows           [["One" 20]
+                          ["Two" 30]]
+          labels         {:left "count" :bottom "process step"}
+          settings       (json/generate-string {})
+          waterfall-type (name :categorical)]
+      (testing "It returns bytes"
+        (let [svg-bytes (js-svg/waterfall rows labels settings waterfall-type)]
+          (is (bytes? svg-bytes))))
+      (let [svg-string (.asString (js/execute-fn-name @context "waterfall"
+                                                      rows labels settings waterfall-type
+                                                      (json/generate-string {})))]
+        (testing "it returns a valid svg string (no html in it)"
+          (validate-svg-string :categorical-waterfall svg-string))))))
 
 (deftest combo-test
   (let [rows1    [[#t "1998-03-01T00:00:00Z" 2]
@@ -302,14 +319,3 @@
                                   (json/generate-string {:value value :goal goal})
                                   (json/generate-string settings)))]
       (validate-svg-string :progress svg-string))))
-
-(deftest categorical-waterfall-test
-  (let [rows     [["apples" 2]
-                  ["bananas" 3]]
-        labels   {:left "bob" :right "dobbs"}
-        settings (json/generate-string {})]
-    (testing "It returns bytes"
-      (let [svg-bytes (js-svg/categorical-waterfall rows labels {})]
-        (is (bytes? svg-bytes))))
-    (let [svg-string (.asString ^Value (js/execute-fn-name @context "categorical_waterfall" rows labels settings (json/generate-string (public-settings/application-colors))))]
-      (validate-svg-string :categorical/waterfall svg-string))))