You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2023/06/28 18:51:46 UTC

[superset] branch master updated: feat: Implement currencies formatter for saved metrics (#24517)

This is an automated email from the ASF dual-hosted git repository.

kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 83ff4cd86a feat: Implement currencies formatter for saved metrics (#24517)
83ff4cd86a is described below

commit 83ff4cd86a4931fc8eda83aeb3d8d3c92d773202
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Wed Jun 28 20:51:40 2023 +0200

    feat: Implement currencies formatter for saved metrics (#24517)
---
 superset-frontend/package-lock.json                |   9 +-
 .../superset-ui-chart-controls/src/fixtures.ts     |   2 +
 .../superset-ui-chart-controls/src/types.ts        |   2 +
 .../test/utils/columnChoices.test.tsx              |   1 +
 .../test/utils/defineSavedMetrics.test.tsx         |   1 +
 .../src/currency-format/CurrencyFormatter.ts       |  79 +++++++++++
 .../src/{types => currency-format}/index.ts        |  10 +-
 .../packages/superset-ui-core/src/index.ts         |   1 +
 .../superset-ui-core/src/query/types/Datasource.ts |   8 ++
 .../superset-ui-core/src/query/types/Metric.ts     |   1 +
 .../packages/superset-ui-core/src/types/index.ts   |   5 +
 .../test/currency-format/CurrencyFormatter.test.ts | 158 +++++++++++++++++++++
 .../src/BigNumber/BigNumberTotal/transformProps.ts |  25 +++-
 .../BigNumberWithTrendline/transformProps.ts       |  18 ++-
 .../plugin-chart-echarts/src/BigNumber/types.ts    |   4 +-
 .../src/Funnel/transformProps.ts                   |  14 +-
 .../src/Gauge/transformProps.ts                    |  15 +-
 .../plugin-chart-echarts/src/Pie/transformProps.ts |  15 +-
 .../src/Timeseries/transformProps.ts               |  78 +++++++++-
 .../src/Timeseries/transformers.ts                 |   4 +-
 .../src/Treemap/transformProps.ts                  |  17 ++-
 .../plugin-chart-echarts/src/utils/forecast.ts     |   4 +-
 .../plugin-chart-echarts/src/utils/series.ts       |   3 +-
 .../src/utils/valueFormatter.ts                    |  63 ++++++++
 .../test/BigNumber/transformProps.test.ts          |  25 ++++
 .../src/PivotTableChart.tsx                        |  31 +++-
 .../src/plugin/transformProps.ts                   |   3 +-
 .../plugins/plugin-chart-pivot-table/src/types.ts  |   2 +
 .../test/plugin/buildQuery.test.ts                 |   1 +
 .../test/plugin/transformProps.test.ts             |   1 +
 .../plugins/plugin-chart-table/package.json        |  42 +++---
 .../plugin-chart-table/src/transformProps.ts       |   8 +-
 .../plugins/plugin-chart-table/src/types.ts        |   7 +-
 .../plugin-chart-table/src/utils/formatValue.ts    |   1 -
 .../plugin-chart-table/src/utils/isEqualColumns.ts |   1 +
 .../plugin-chart-table/test/TableChart.test.tsx    |  21 +++
 .../plugins/plugin-chart-table/test/testData.ts    |  11 ++
 .../src/components/Datasource/DatasourceEditor.jsx |  70 +++++++++
 .../Datasource/DatasourceEditor.test.jsx           |  86 ++++++++++-
 .../src/components/Datasource/DatasourceModal.tsx  |  32 ++++-
 superset-frontend/src/dashboard/constants.ts       |   1 +
 .../src/explore/actions/datasourcesActions.test.ts |   2 +
 .../src/explore/actions/hydrateExplore.ts          |   1 +
 .../src/explore/controlUtils/controlUtils.test.tsx |   1 +
 ...etControlValuesCompatibleWithDatasource.test.ts |   1 +
 superset-frontend/src/explore/fixtures.tsx         |   2 +
 superset-frontend/src/features/datasets/types.ts   |   3 +
 .../src/utils/getDatasourceUid.test.ts             |   1 +
 superset/config.py                                 |   2 +
 superset/connectors/base/models.py                 |  21 +++
 superset/connectors/sqla/models.py                 |   1 +
 superset/connectors/sqla/views.py                  |   1 +
 superset/dashboards/schemas.py                     |   1 +
 superset/datasets/api.py                           |   2 +
 superset/datasets/schemas.py                       |   2 +
 superset/db_engine_specs/base.py                   |   1 +
 superset/explore/schemas.py                        |   1 +
 ..._90139bf715e4_add_currency_column_to_metrics.py |  42 ++++++
 superset/views/base.py                             |   1 +
 tests/integration_tests/datasets/commands_tests.py |   3 +
 tests/unit_tests/datasets/commands/export_test.py  |   1 +
 61 files changed, 888 insertions(+), 82 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index d2493fdb3a..d926b16fec 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -61705,12 +61705,14 @@
         "regenerator-runtime": "^0.13.7",
         "xss": "^1.0.10"
       },
-      "devDependencies": {
-        "@testing-library/react": "^11.2.0"
-      },
       "peerDependencies": {
         "@superset-ui/chart-controls": "*",
         "@superset-ui/core": "*",
+        "@testing-library/dom": "^7.29.4",
+        "@testing-library/jest-dom": "^5.11.6",
+        "@testing-library/react": "^11.2.0",
+        "@testing-library/react-hooks": "^5.0.3",
+        "@testing-library/user-event": "^12.7.0",
         "@types/classnames": "*",
         "@types/react": "*",
         "react": "^16.13.1",
@@ -77467,7 +77469,6 @@
       "version": "file:plugins/plugin-chart-table",
       "requires": {
         "@react-icons/all-files": "^4.1.0",
-        "@testing-library/react": "^11.2.0",
         "@types/d3-array": "^2.9.0",
         "@types/enzyme": "^3.10.5",
         "@types/react-table": "^7.0.29",
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
index 1e5152b2bd..cc0b678f6d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts
@@ -21,6 +21,7 @@ import { Dataset } from './types';
 
 export const TestDataset: Dataset = {
   column_formats: {},
+  currency_formats: {},
   columns: [
     {
       advanced_data_type: undefined,
@@ -123,6 +124,7 @@ export const TestDataset: Dataset = {
       certification_details: null,
       certified_by: null,
       d3format: null,
+      currency: null,
       description: null,
       expression: 'COUNT(*)',
       id: 7,
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 8ba428d7bc..dadd78cdd0 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -21,6 +21,7 @@ import React, { ReactElement, ReactNode, ReactText } from 'react';
 import type {
   AdhocColumn,
   Column,
+  Currency,
   DatasourceType,
   JsonObject,
   JsonValue,
@@ -68,6 +69,7 @@ export interface Dataset {
   columns: ColumnMeta[];
   metrics: Metric[];
   column_formats: Record<string, string>;
+  currency_formats: Record<string, Currency>;
   verbose_map: Record<string, string>;
   main_dttm_col: string;
   // eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
index e27fa95120..aaaccda95d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx
@@ -43,6 +43,7 @@ describe('columnChoices()', () => {
         ],
         verbose_map: {},
         column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
+        currency_formats: {},
         datasource_name: 'my_datasource',
         description: 'this is my datasource',
       }),
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
index 765412d592..f7ae98520c 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx
@@ -40,6 +40,7 @@ describe('defineSavedMetrics', () => {
       columns: [],
       verbose_map: {},
       column_formats: {},
+      currency_formats: {},
       datasource_name: 'my_datasource',
       description: 'this is my datasource',
     };
diff --git a/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts b/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts
new file mode 100644
index 0000000000..7c082abd34
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/currency-format/CurrencyFormatter.ts
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ExtensibleFunction } from '../models';
+import { getNumberFormatter, NumberFormats } from '../number-format';
+import { Currency } from '../query';
+
+interface CurrencyFormatterConfig {
+  d3Format?: string;
+  currency: Currency;
+  locale?: string;
+}
+
+interface CurrencyFormatter {
+  (value: number | null | undefined): string;
+}
+
+export const getCurrencySymbol = (currency: Currency) =>
+  new Intl.NumberFormat('en-US', {
+    style: 'currency',
+    currency: currency.symbol,
+  })
+    .formatToParts(1)
+    .find(x => x.type === 'currency')?.value;
+
+class CurrencyFormatter extends ExtensibleFunction {
+  d3Format: string;
+
+  locale: string;
+
+  currency: Currency;
+
+  constructor(config: CurrencyFormatterConfig) {
+    super((value: number) => this.format(value));
+    this.d3Format = config.d3Format || NumberFormats.SMART_NUMBER;
+    this.currency = config.currency;
+    this.locale = config.locale || 'en-US';
+  }
+
+  hasValidCurrency() {
+    return Boolean(this.currency?.symbol);
+  }
+
+  getNormalizedD3Format() {
+    return this.d3Format.replace(/\$|%/g, '');
+  }
+
+  format(value: number) {
+    const formattedValue = getNumberFormatter(this.getNormalizedD3Format())(
+      value,
+    );
+    if (!this.hasValidCurrency()) {
+      return formattedValue as string;
+    }
+
+    if (this.currency.symbolPosition === 'prefix') {
+      return `${getCurrencySymbol(this.currency)} ${formattedValue}`;
+    }
+    return `${formattedValue} ${getCurrencySymbol(this.currency)}`;
+  }
+}
+
+export default CurrencyFormatter;
diff --git a/superset-frontend/packages/superset-ui-core/src/types/index.ts b/superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
similarity index 84%
copy from superset-frontend/packages/superset-ui-core/src/types/index.ts
copy to superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
index a1c527afd6..c7fa5a0388 100644
--- a/superset-frontend/packages/superset-ui-core/src/types/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/currency-format/index.ts
@@ -1,4 +1,4 @@
-/**
+/*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -16,10 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export * from '../query/types';
 
-export type Maybe<T> = T | null;
-
-export type Optional<T> = T | undefined;
-
-export type ValueOf<T> = T[keyof T];
+export { default as CurrencyFormatter } from './CurrencyFormatter';
+export * from './CurrencyFormatter';
diff --git a/superset-frontend/packages/superset-ui-core/src/index.ts b/superset-frontend/packages/superset-ui-core/src/index.ts
index 5ee5acbce4..ea7a4efde7 100644
--- a/superset-frontend/packages/superset-ui-core/src/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/index.ts
@@ -36,3 +36,4 @@ export * from './components';
 export * from './math-expression';
 export * from './ui-overrides';
 export * from './hooks';
+export * from './currency-format';
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
index 9639a000d0..ab5ff950cc 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts
@@ -27,6 +27,11 @@ export enum DatasourceType {
   SavedQuery = 'saved_query',
 }
 
+export interface Currency {
+  symbol: string;
+  symbolPosition: string;
+}
+
 /**
  * Datasource metadata.
  */
@@ -41,6 +46,9 @@ export interface Datasource {
   columnFormats?: {
     [key: string]: string;
   };
+  currencyFormats?: {
+    [key: string]: Currency;
+  };
   verboseMap?: {
     [key: string]: string;
   };
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
index c0f770f904..ac6523bedb 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts
@@ -65,6 +65,7 @@ export interface Metric {
   certification_details?: Maybe<string>;
   certified_by?: Maybe<string>;
   d3format?: Maybe<string>;
+  currency?: Maybe<string>;
   description?: Maybe<string>;
   is_certified?: boolean;
   verbose_name?: string;
diff --git a/superset-frontend/packages/superset-ui-core/src/types/index.ts b/superset-frontend/packages/superset-ui-core/src/types/index.ts
index a1c527afd6..cb6e6dcfcb 100644
--- a/superset-frontend/packages/superset-ui-core/src/types/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/types/index.ts
@@ -16,6 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { NumberFormatter } from '../number-format';
+import { CurrencyFormatter } from '../currency-format';
+
 export * from '../query/types';
 
 export type Maybe<T> = T | null;
@@ -23,3 +26,5 @@ export type Maybe<T> = T | null;
 export type Optional<T> = T | undefined;
 
 export type ValueOf<T> = T[keyof T];
+
+export type ValueFormatter = NumberFormatter | CurrencyFormatter;
diff --git a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
new file mode 100644
index 0000000000..5172e3b0ed
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  CurrencyFormatter,
+  getCurrencySymbol,
+  NumberFormats,
+} from '@superset-ui/core';
+
+it('getCurrencySymbol', () => {
+  expect(
+    getCurrencySymbol({ symbol: 'PLN', symbolPosition: 'prefix' }),
+  ).toEqual('PLN');
+  expect(
+    getCurrencySymbol({ symbol: 'USD', symbolPosition: 'prefix' }),
+  ).toEqual('$');
+
+  expect(() =>
+    getCurrencySymbol({ symbol: 'INVALID_CODE', symbolPosition: 'prefix' }),
+  ).toThrow(RangeError);
+});
+
+it('CurrencyFormatter object fields', () => {
+  const defaultCurrencyFormatter = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+  });
+  expect(defaultCurrencyFormatter.d3Format).toEqual(NumberFormats.SMART_NUMBER);
+  expect(defaultCurrencyFormatter.locale).toEqual('en-US');
+  expect(defaultCurrencyFormatter.currency).toEqual({
+    symbol: 'USD',
+    symbolPosition: 'prefix',
+  });
+
+  const currencyFormatter = new CurrencyFormatter({
+    currency: { symbol: 'PLN', symbolPosition: 'suffix' },
+    locale: 'pl-PL',
+    d3Format: ',.1f',
+  });
+  expect(currencyFormatter.d3Format).toEqual(',.1f');
+  expect(currencyFormatter.locale).toEqual('pl-PL');
+  expect(currencyFormatter.currency).toEqual({
+    symbol: 'PLN',
+    symbolPosition: 'suffix',
+  });
+});
+
+it('CurrencyFormatter:hasValidCurrency', () => {
+  const currencyFormatter = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+  });
+  expect(currencyFormatter.hasValidCurrency()).toBe(true);
+
+  const currencyFormatterWithoutPosition = new CurrencyFormatter({
+    // @ts-ignore
+    currency: { symbol: 'USD' },
+  });
+  expect(currencyFormatterWithoutPosition.hasValidCurrency()).toBe(true);
+
+  const currencyFormatterWithoutSymbol = new CurrencyFormatter({
+    // @ts-ignore
+    currency: { symbolPosition: 'prefix' },
+  });
+  expect(currencyFormatterWithoutSymbol.hasValidCurrency()).toBe(false);
+
+  // @ts-ignore
+  const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
+  expect(currencyFormatterWithoutCurrency.hasValidCurrency()).toBe(false);
+});
+
+it('CurrencyFormatter:getNormalizedD3Format', () => {
+  const currencyFormatter = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+  });
+  expect(currencyFormatter.getNormalizedD3Format()).toEqual(
+    currencyFormatter.d3Format,
+  );
+
+  const currencyFormatter2 = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+    d3Format: ',.1f',
+  });
+  expect(currencyFormatter2.getNormalizedD3Format()).toEqual(
+    currencyFormatter2.d3Format,
+  );
+
+  const currencyFormatter3 = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+    d3Format: '$,.1f',
+  });
+  expect(currencyFormatter3.getNormalizedD3Format()).toEqual(',.1f');
+
+  const currencyFormatter4 = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+    d3Format: ',.1%',
+  });
+  expect(currencyFormatter4.getNormalizedD3Format()).toEqual(',.1');
+});
+
+it('CurrencyFormatter:format', () => {
+  const VALUE = 56100057;
+  const currencyFormatterWithPrefix = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+  });
+
+  expect(currencyFormatterWithPrefix(VALUE)).toEqual(
+    currencyFormatterWithPrefix.format(VALUE),
+  );
+  expect(currencyFormatterWithPrefix(VALUE)).toEqual('$ 56.1M');
+
+  const currencyFormatterWithSuffix = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'suffix' },
+  });
+  expect(currencyFormatterWithSuffix(VALUE)).toEqual('56.1M $');
+
+  const currencyFormatterWithoutPosition = new CurrencyFormatter({
+    // @ts-ignore
+    currency: { symbol: 'USD' },
+  });
+  expect(currencyFormatterWithoutPosition(VALUE)).toEqual('56.1M $');
+
+  // @ts-ignore
+  const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
+  expect(currencyFormatterWithoutCurrency(VALUE)).toEqual('56.1M');
+
+  const currencyFormatterWithCustomD3 = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+    d3Format: ',.1f',
+  });
+  expect(currencyFormatterWithCustomD3(VALUE)).toEqual('$ 56,100,057.0');
+
+  const currencyFormatterWithPercentD3 = new CurrencyFormatter({
+    currency: { symbol: 'USD', symbolPosition: 'prefix' },
+    d3Format: ',.1f%',
+  });
+  expect(currencyFormatterWithPercentD3(VALUE)).toEqual('$ 56,100,057.0');
+
+  const currencyFormatterWithCurrencyD3 = new CurrencyFormatter({
+    currency: { symbol: 'PLN', symbolPosition: 'suffix' },
+    d3Format: '$,.1f',
+  });
+  expect(currencyFormatterWithCurrencyD3(VALUE)).toEqual('56,100,057.0 PLN');
+});
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
index 8624e5bc54..5486030f46 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts
@@ -19,9 +19,9 @@
 import {
   ColorFormatters,
   getColorFormatters,
+  Metric,
 } from '@superset-ui/chart-controls';
 import {
-  getNumberFormatter,
   GenericDataType,
   getMetricLabel,
   extractTimegrain,
@@ -30,12 +30,20 @@ import {
 import { BigNumberTotalChartProps, BigNumberVizProps } from '../types';
 import { getDateFormatter, parseMetricValue } from '../utils';
 import { Refs } from '../../types';
+import { getValueFormatter } from '../../utils/valueFormatter';
 
 export default function transformProps(
   chartProps: BigNumberTotalChartProps,
 ): BigNumberVizProps {
-  const { width, height, queriesData, formData, rawFormData, hooks } =
-    chartProps;
+  const {
+    width,
+    height,
+    queriesData,
+    formData,
+    rawFormData,
+    hooks,
+    datasource: { currencyFormats = {}, columnFormats = {} },
+  } = chartProps;
   const {
     headerFontSize,
     metric = 'value',
@@ -54,7 +62,7 @@ export default function transformProps(
   const bigNumber =
     data.length === 0 ? null : parseMetricValue(data[0][metricName]);
 
-  let metricEntry;
+  let metricEntry: Metric | undefined;
   if (chartProps.datasource?.metrics) {
     metricEntry = chartProps.datasource.metrics.find(
       metricItem => metricItem.metric_name === metric,
@@ -67,12 +75,19 @@ export default function transformProps(
     metricEntry?.d3format,
   );
 
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    yAxisFormat,
+  );
+
   const headerFormatter =
     coltypes[0] === GenericDataType.TEMPORAL ||
     coltypes[0] === GenericDataType.STRING ||
     forceTimestampFormatting
       ? formatTime
-      : getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
+      : numberFormatter;
 
   const { onContextMenu } = hooks;
 
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
index bd4553479e..c05a427f31 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts
@@ -24,9 +24,10 @@ import {
   getMetricLabel,
   t,
   smartDateVerboseFormatter,
-  NumberFormatter,
   TimeFormatter,
   getXAxisLabel,
+  Metric,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { EChartsCoreOption, graphic } from 'echarts';
 import {
@@ -38,11 +39,12 @@ import {
 import { getDateFormatter, parseMetricValue } from '../utils';
 import { getDefaultTooltip } from '../../utils/tooltip';
 import { Refs } from '../../types';
+import { getValueFormatter } from '../../utils/valueFormatter';
 
 const defaultNumberFormatter = getNumberFormatter();
 export function renderTooltipFactory(
   formatDate: TimeFormatter = smartDateVerboseFormatter,
-  formatValue: NumberFormatter | TimeFormatter = defaultNumberFormatter,
+  formatValue: ValueFormatter | TimeFormatter = defaultNumberFormatter,
 ) {
   return function renderTooltip(params: { data: TimeSeriesDatum }[]) {
     return `
@@ -73,6 +75,7 @@ export default function transformProps(
     theme,
     hooks,
     inContextMenu,
+    datasource: { currencyFormats = {}, columnFormats = {} },
   } = chartProps;
   const {
     colorPicker,
@@ -159,7 +162,7 @@ export default function transformProps(
     className = 'negative';
   }
 
-  let metricEntry;
+  let metricEntry: Metric | undefined;
   if (chartProps.datasource?.metrics) {
     metricEntry = chartProps.datasource.metrics.find(
       metricEntry => metricEntry.metric_name === metric,
@@ -172,12 +175,19 @@ export default function transformProps(
     metricEntry?.d3format,
   );
 
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    yAxisFormat,
+  );
+
   const headerFormatter =
     metricColtype === GenericDataType.TEMPORAL ||
     metricColtype === GenericDataType.STRING ||
     forceTimestampFormatting
       ? formatTime
-      : getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
+      : numberFormatter;
 
   if (trendLineData && timeRangeFixed && fromDatetime) {
     const toDatetimeOrToday = toDatetime ?? Date.now();
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
index c517fcc0b9..2081460ad1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts
@@ -22,10 +22,10 @@ import {
   ChartDataResponseResult,
   ContextMenuFilters,
   DataRecordValue,
-  NumberFormatter,
   QueryFormData,
   QueryFormMetric,
   TimeFormatter,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { ColorFormatters } from '@superset-ui/chart-controls';
 import { BaseChartProps, Refs } from '../types';
@@ -73,7 +73,7 @@ export type BigNumberVizProps = {
   height: number;
   bigNumber?: DataRecordValue;
   bigNumberFallback?: TimeSeriesDatum;
-  headerFormatter: NumberFormatter | TimeFormatter;
+  headerFormatter: ValueFormatter | TimeFormatter;
   formatTime?: TimeFormatter;
   headerFontSize: number;
   kickerFontSize?: number;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
index ac2f650e32..796aa36e40 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
@@ -22,7 +22,7 @@ import {
   getMetricLabel,
   getNumberFormatter,
   NumberFormats,
-  NumberFormatter,
+  ValueFormatter,
   getColumnLabel,
 } from '@superset-ui/core';
 import { CallbackDataParams } from 'echarts/types/src/util/types';
@@ -45,6 +45,7 @@ import { defaultGrid } from '../defaults';
 import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { Refs } from '../types';
+import { getValueFormatter } from '../utils/valueFormatter';
 
 const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
 
@@ -56,7 +57,7 @@ export function formatFunnelLabel({
 }: {
   params: Pick<CallbackDataParams, 'name' | 'value' | 'percent'>;
   labelType: EchartsFunnelLabelTypeType;
-  numberFormatter: NumberFormatter;
+  numberFormatter: ValueFormatter;
   sanitizeName?: boolean;
 }): string {
   const { name: rawName = '', value, percent } = params;
@@ -94,6 +95,7 @@ export default function transformProps(
     theme,
     inContextMenu,
     emitCrossFilters,
+    datasource,
   } = chartProps;
   const data: DataRecord[] = queriesData[0].data || [];
   const coltypeMapping = getColtypesMapping(queriesData[0]);
@@ -118,6 +120,7 @@ export default function transformProps(
     ...DEFAULT_FUNNEL_FORM_DATA,
     ...formData,
   };
+  const { currencyFormats = {}, columnFormats = {} } = datasource;
   const refs: Refs = {};
   const metricLabel = getMetricLabel(metric);
   const groupbyLabels = groupby.map(getColumnLabel);
@@ -139,7 +142,12 @@ export default function transformProps(
   const { setDataMask = () => {}, onContextMenu } = hooks;
 
   const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
-  const numberFormatter = getNumberFormatter(numberFormat);
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    numberFormat,
+  );
 
   const transformedData: FunnelSeriesOption[] = data.map(datum => {
     const name = extractGroupbyLabel({
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
index 27e6c9f197..ffbb746bf0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
@@ -21,7 +21,6 @@ import {
   CategoricalColorNamespace,
   CategoricalColorScale,
   DataRecord,
-  getNumberFormatter,
   getMetricLabel,
   getColumnLabel,
 } from '@superset-ui/core';
@@ -47,6 +46,7 @@ import { OpacityEnum } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { Refs } from '../types';
 import { getColtypesMapping } from '../utils/series';
+import { getValueFormatter } from '../utils/valueFormatter';
 
 const setIntervalBoundsAndColors = (
   intervals: string,
@@ -105,7 +105,11 @@ export default function transformProps(
   } = chartProps;
 
   const gaugeSeriesOptions = defaultGaugeSeriesOption(theme);
-  const { verboseMap = {} } = datasource;
+  const {
+    verboseMap = {},
+    currencyFormats = {},
+    columnFormats = {},
+  } = datasource;
   const {
     groupby,
     metric,
@@ -132,7 +136,12 @@ export default function transformProps(
   const refs: Refs = {};
   const data = (queriesData[0]?.data || []) as DataRecord[];
   const coltypeMapping = getColtypesMapping(queriesData[0]);
-  const numberFormatter = getNumberFormatter(numberFormat);
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    numberFormat,
+  );
   const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
   const axisLineWidth = calculateAxisLineWidth(data, fontSize, overlap);
   const groupbyLabels = groupby.map(getColumnLabel);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
index 7d30917b18..7ea9cc1ffb 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
@@ -23,8 +23,8 @@ import {
   getNumberFormatter,
   getTimeFormatter,
   NumberFormats,
-  NumberFormatter,
   t,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { CallbackDataParams } from 'echarts/types/src/util/types';
 import { EChartsCoreOption, PieSeriesOption } from 'echarts';
@@ -47,6 +47,7 @@ import { defaultGrid } from '../defaults';
 import { convertInteger } from '../utils/convertInteger';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { Refs } from '../types';
+import { getValueFormatter } from '../utils/valueFormatter';
 
 const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
 
@@ -58,7 +59,7 @@ export function formatPieLabel({
 }: {
   params: Pick<CallbackDataParams, 'name' | 'value' | 'percent'>;
   labelType: EchartsPieLabelType;
-  numberFormatter: NumberFormatter;
+  numberFormatter: ValueFormatter;
   sanitizeName?: boolean;
 }): string {
   const { name: rawName = '', value, percent } = params;
@@ -145,7 +146,9 @@ export default function transformProps(
     theme,
     inContextMenu,
     emitCrossFilters,
+    datasource,
   } = chartProps;
+  const { columnFormats = {}, currencyFormats = {} } = datasource;
   const { data = [] } = queriesData[0];
   const coltypeMapping = getColtypesMapping(queriesData[0]);
 
@@ -203,7 +206,13 @@ export default function transformProps(
   const { setDataMask = () => {}, onContextMenu } = hooks;
 
   const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
-  const numberFormatter = getNumberFormatter(numberFormat);
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    numberFormat,
+  );
+
   let totalValue = 0;
 
   const transformedData: PieSeriesOption[] = data.map(datum => {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index c89bff2e8c..2066148c84 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -22,6 +22,7 @@ import {
   AnnotationLayer,
   AxisType,
   CategoricalColorNamespace,
+  CurrencyFormatter,
   ensureIsArray,
   GenericDataType,
   getMetricLabel,
@@ -32,9 +33,13 @@ import {
   isFormulaAnnotationLayer,
   isIntervalAnnotationLayer,
   isPhysicalColumn,
+  isSavedMetric,
   isTimeseriesAnnotationLayer,
+  NumberFormats,
+  QueryFormMetric,
   t,
   TimeseriesChartDataResponseResult,
+  ValueFormatter,
 } from '@superset-ui/core';
 import {
   extractExtraMetrics,
@@ -92,6 +97,36 @@ import {
   TIMEGRAIN_TO_TIMESTAMP,
 } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
+import {
+  buildCustomFormatters,
+  getCustomFormatter,
+} from '../utils/valueFormatter';
+
+const getYAxisFormatter = (
+  metrics: QueryFormMetric[],
+  forcePercentFormatter: boolean,
+  customFormatters: Record<string, ValueFormatter>,
+  yAxisFormat: string = NumberFormats.SMART_NUMBER,
+) => {
+  if (forcePercentFormatter) {
+    return getNumberFormatter(',.0%');
+  }
+  const metricsArray = ensureIsArray(metrics);
+  if (
+    metricsArray.every(isSavedMetric) &&
+    metricsArray
+      .map(metric => customFormatters[metric])
+      .every(
+        (formatter, _, formatters) =>
+          formatter instanceof CurrencyFormatter &&
+          (formatter as CurrencyFormatter)?.currency?.symbol ===
+            (formatters[0] as CurrencyFormatter)?.currency?.symbol,
+      )
+  ) {
+    return customFormatters[metricsArray[0]];
+  }
+  return getNumberFormatter(yAxisFormat);
+};
 
 export default function transformProps(
   chartProps: EchartsTimeseriesChartProps,
@@ -109,7 +144,11 @@ export default function transformProps(
     inContextMenu,
     emitCrossFilters,
   } = chartProps;
-  const { verboseMap = {} } = datasource;
+  const {
+    verboseMap = {},
+    columnFormats = {},
+    currencyFormats = {},
+  } = datasource;
   const [queryData] = queriesData;
   const { data = [], label_map = {} } =
     queryData as TimeseriesChartDataResponseResult;
@@ -232,8 +271,15 @@ export default function transformProps(
 
   const xAxisType = getAxisType(xAxisDataType);
   const series: SeriesOption[] = [];
-  const formatter = getNumberFormatter(
-    contributionMode || isAreaExpand ? ',.0%' : yAxisFormat,
+
+  const forcePercentFormatter = Boolean(contributionMode || isAreaExpand);
+  const percentFormatter = getNumberFormatter(',.0%');
+  const defaultFormatter = getNumberFormatter(yAxisFormat);
+  const customFormatters = buildCustomFormatters(
+    metrics,
+    currencyFormats,
+    columnFormats,
+    yAxisFormat,
   );
 
   const array = ensureIsArray(chartProps.rawFormData?.time_compare);
@@ -262,7 +308,13 @@ export default function transformProps(
         seriesType,
         legendState,
         stack,
-        formatter,
+        formatter: forcePercentFormatter
+          ? percentFormatter
+          : getCustomFormatter(
+              customFormatters,
+              metrics,
+              labelMap[seriesName]?.[0],
+            ) ?? defaultFormatter,
         showValue,
         onlyTotal,
         totalStackedValues: sortedTotalValues,
@@ -440,7 +492,14 @@ export default function transformProps(
     max,
     minorTick: { show: true },
     minorSplitLine: { show: minorSplitLine },
-    axisLabel: { formatter },
+    axisLabel: {
+      formatter: getYAxisFormatter(
+        metrics,
+        forcePercentFormatter,
+        customFormatters,
+        yAxisFormat,
+      ),
+    },
     scale: truncateYAxis,
     name: yAxisTitle,
     nameGap: convertInteger(yAxisTitleMargin),
@@ -485,10 +544,17 @@ export default function transformProps(
           if (value.observation === 0 && stack) {
             return;
           }
+          // if there are no dimensions, key is a verbose name of a metric,
+          // otherwise it is a comma separated string where the first part is metric name
+          const formatterKey =
+            groupby.length === 0 ? inverted[key] : labelMap[key]?.[0];
           const content = formatForecastTooltipSeries({
             ...value,
             seriesName: key,
-            formatter,
+            formatter: forcePercentFormatter
+              ? percentFormatter
+              : getCustomFormatter(customFormatters, metrics, formatterKey) ??
+                defaultFormatter,
           });
           if (!legendState || legendState[key]) {
             rows.push(`<span style="font-weight: 700">${content}</span>`);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index fb4739dc74..0bcc5baf8d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -28,13 +28,13 @@ import {
   IntervalAnnotationLayer,
   isTimeseriesAnnotationResult,
   LegendState,
-  NumberFormatter,
   smartDateDetailedFormatter,
   smartDateFormatter,
   SupersetTheme,
   TimeFormatter,
   TimeseriesAnnotationLayer,
   TimeseriesDataRecord,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { SeriesOption } from 'echarts';
 import {
@@ -158,7 +158,7 @@ export function transformSeries(
     showValue?: boolean;
     onlyTotal?: boolean;
     legendState?: LegendState;
-    formatter?: NumberFormatter;
+    formatter?: ValueFormatter;
     totalStackedValues?: number[];
     showValueIndexes?: number[];
     thresholdValues?: number[];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
index 89088be5fa..9e0454b386 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
@@ -23,7 +23,7 @@ import {
   getNumberFormatter,
   getTimeFormatter,
   NumberFormats,
-  NumberFormatter,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries';
 import { EChartsCoreOption, TreemapSeriesOption } from 'echarts';
@@ -48,6 +48,7 @@ import { OpacityEnum } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { Refs } from '../types';
 import { treeBuilder, TreeNode } from '../utils/treeBuilder';
+import { getValueFormatter } from '../utils/valueFormatter';
 
 export function formatLabel({
   params,
@@ -56,7 +57,7 @@ export function formatLabel({
 }: {
   params: TreemapSeriesCallbackDataParams;
   labelType: EchartsTreemapLabelType;
-  numberFormatter: NumberFormatter;
+  numberFormatter: ValueFormatter;
 }): string {
   const { name = '', value } = params;
   const formattedValue = numberFormatter(value as number);
@@ -78,7 +79,7 @@ export function formatTooltip({
   numberFormatter,
 }: {
   params: TreemapSeriesCallbackDataParams;
-  numberFormatter: NumberFormatter;
+  numberFormatter: ValueFormatter;
 }): string {
   const { value, treePathInfo = [] } = params;
   const formattedValue = numberFormatter(value as number);
@@ -118,8 +119,10 @@ export default function transformProps(
     theme,
     inContextMenu,
     emitCrossFilters,
+    datasource,
   } = chartProps;
   const { data = [] } = queriesData[0];
+  const { columnFormats = {}, currencyFormats = {} } = datasource;
   const { setDataMask = () => {}, onContextMenu } = hooks;
   const coltypeMapping = getColtypesMapping(queriesData[0]);
 
@@ -141,7 +144,13 @@ export default function transformProps(
   };
   const refs: Refs = {};
   const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
-  const numberFormatter = getNumberFormatter(numberFormat);
+  const numberFormatter = getValueFormatter(
+    metric,
+    currencyFormats,
+    columnFormats,
+    numberFormat,
+  );
+
   const formatter = (params: TreemapSeriesCallbackDataParams) =>
     formatLabel({
       params,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
index 485e9fb896..18b160b9c6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { isNumber } from 'lodash';
-import { DataRecord, DTTM_ALIAS, NumberFormatter } from '@superset-ui/core';
+import { DataRecord, DTTM_ALIAS, ValueFormatter } from '@superset-ui/core';
 import { OptionName } from 'echarts/types/src/util/types';
 import { TooltipMarker } from 'echarts/types/src/util/format';
 import {
@@ -91,7 +91,7 @@ export const formatForecastTooltipSeries = ({
 }: ForecastValue & {
   seriesName: string;
   marker: TooltipMarker;
-  formatter: NumberFormatter;
+  formatter: ValueFormatter;
 }): string => {
   let row = `${marker}${sanitizeHtml(seriesName)}: `;
   let isObservation = false;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 0f6efd72f2..f9477ea25a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -31,6 +31,7 @@ import {
   SupersetTheme,
   normalizeTimestamp,
   LegendState,
+  ValueFormatter,
 } from '@superset-ui/core';
 import { SortSeriesType } from '@superset-ui/chart-controls';
 import { format, LegendComponentOption, SeriesOption } from 'echarts';
@@ -345,7 +346,7 @@ export function formatSeriesName(
     timeFormatter,
     coltype,
   }: {
-    numberFormatter?: NumberFormatter;
+    numberFormatter?: ValueFormatter;
     timeFormatter?: TimeFormatter;
     coltype?: GenericDataType;
   } = {},
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts
new file mode 100644
index 0000000000..5d995c9f00
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/valueFormatter.ts
@@ -0,0 +1,63 @@
+import {
+  Currency,
+  CurrencyFormatter,
+  ensureIsArray,
+  getNumberFormatter,
+  isSavedMetric,
+  QueryFormMetric,
+  ValueFormatter,
+} from '@superset-ui/core';
+
+export const buildCustomFormatters = (
+  metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+  currencyFormats: Record<string, Currency>,
+  columnFormats: Record<string, string>,
+  d3Format: string | undefined,
+) => {
+  const metricsArray = ensureIsArray(metrics);
+  return metricsArray.reduce((acc, metric) => {
+    const actualD3Format = isSavedMetric(metric)
+      ? columnFormats[metric] ?? d3Format
+      : d3Format;
+    if (isSavedMetric(metric)) {
+      return currencyFormats[metric]
+        ? {
+            ...acc,
+            [metric]: new CurrencyFormatter({
+              d3Format: actualD3Format,
+              currency: currencyFormats[metric],
+            }),
+          }
+        : {
+            ...acc,
+            [metric]: getNumberFormatter(actualD3Format),
+          };
+    }
+    return acc;
+  }, {});
+};
+
+export const getCustomFormatter = (
+  customFormatters: Record<string, ValueFormatter>,
+  metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+  key?: string,
+) => {
+  const metricsArray = ensureIsArray(metrics);
+  if (metricsArray.length === 1 && isSavedMetric(metricsArray[0])) {
+    return customFormatters[metricsArray[0]];
+  }
+  return key ? customFormatters[key] : undefined;
+};
+
+export const getValueFormatter = (
+  metrics: QueryFormMetric | QueryFormMetric[] | undefined,
+  currencyFormats: Record<string, Currency>,
+  columnFormats: Record<string, string>,
+  d3Format: string | undefined,
+  key?: string,
+) =>
+  getCustomFormatter(
+    buildCustomFormatters(metrics, currencyFormats, columnFormats, d3Format),
+    metrics,
+    key,
+  ) ?? getNumberFormatter(d3Format);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
index ce00bb7190..bdbbbcd9d1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts
@@ -158,5 +158,30 @@ describe('BigNumberWithTrendline', () => {
         '1.23',
       );
     });
+
+    it('should format with datasource currency', () => {
+      const propsWithDatasource = {
+        ...props,
+        datasource: {
+          ...props.datasource,
+          currencyFormats: {
+            value: { symbol: 'USD', symbolPosition: 'prefix' },
+          },
+          metrics: [
+            {
+              label: 'value',
+              metric_name: 'value',
+              d3format: '.2f',
+              currency: `{symbol: 'USD', symbolPosition: 'prefix' }`,
+            },
+          ],
+        },
+      };
+      const transformed = transformProps(propsWithDatasource);
+      // @ts-ignore
+      expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual(
+        '$ 1.23',
+      );
+    });
   });
 });
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
index a5610756b2..f463990b1d 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
@@ -21,6 +21,7 @@ import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
 import {
   AdhocMetric,
   BinaryQueryObjectFilterClause,
+  CurrencyFormatter,
   DataRecordValue,
   FeatureFlag,
   getColumnLabel,
@@ -144,6 +145,7 @@ export default function PivotTableChart(props: PivotTableProps) {
     selectedFilters,
     verboseMap,
     columnFormats,
+    currencyFormats,
     metricsLayout,
     metricColorFormatters,
     dateFormatters,
@@ -156,24 +158,39 @@ export default function PivotTableChart(props: PivotTableProps) {
     () => getNumberFormatter(valueFormat),
     [valueFormat],
   );
-  const columnFormatsArray = useMemo(
-    () => Object.entries(columnFormats),
-    [columnFormats],
+  const customFormatsArray = useMemo(
+    () =>
+      Array.from(
+        new Set([
+          ...Object.keys(columnFormats || {}),
+          ...Object.keys(currencyFormats || {}),
+        ]),
+      ).map(metricName => [
+        metricName,
+        columnFormats[metricName] || valueFormat,
+        currencyFormats[metricName],
+      ]),
+    [columnFormats, currencyFormats, valueFormat],
   );
-  const hasCustomMetricFormatters = columnFormatsArray.length > 0;
+  const hasCustomMetricFormatters = customFormatsArray.length > 0;
   const metricFormatters = useMemo(
     () =>
       hasCustomMetricFormatters
         ? {
             [METRIC_KEY]: Object.fromEntries(
-              columnFormatsArray.map(([metric, format]) => [
+              customFormatsArray.map(([metric, d3Format, currency]) => [
                 metric,
-                getNumberFormatter(format),
+                currency
+                  ? new CurrencyFormatter({
+                      currency,
+                      d3Format,
+                    })
+                  : getNumberFormatter(d3Format),
               ]),
             ),
           }
         : undefined,
-    [columnFormatsArray, hasCustomMetricFormatters],
+    [customFormatsArray, hasCustomMetricFormatters],
   );
 
   const metricNames = useMemo(
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
index 43d73e6193..f335c6978e 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
@@ -79,7 +79,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
     rawFormData,
     hooks: { setDataMask = () => {}, onContextMenu },
     filterState,
-    datasource: { verboseMap = {}, columnFormats = {} },
+    datasource: { verboseMap = {}, columnFormats = {}, currencyFormats = {} },
     emitCrossFilters,
   } = chartProps;
   const { data, colnames, coltypes } = queriesData[0];
@@ -162,6 +162,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
     selectedFilters,
     verboseMap,
     columnFormats,
+    currencyFormats,
     metricsLayout,
     metricColorFormatters,
     dateFormatters,
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
index e011f45931..dea5236666 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
@@ -28,6 +28,7 @@ import {
   QueryFormColumn,
   TimeGranularity,
   ContextMenuFilters,
+  Currency,
 } from '@superset-ui/core';
 import { ColorFormatters } from '@superset-ui/chart-controls';
 
@@ -69,6 +70,7 @@ interface PivotTableCustomizeProps {
   selectedFilters?: SelectedFiltersType;
   verboseMap: JsonObject;
   columnFormats: JsonObject;
+  currencyFormats: Record<string, Currency>;
   metricsLayout?: MetricsLayoutEnum;
   metricColorFormatters: ColorFormatters;
   dateFormatters: Record<string, DateFormatter | undefined>;
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
index 770cb9849b..f3ac8b82d4 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts
@@ -44,6 +44,7 @@ describe('PivotTableChart buildQuery', () => {
     combineMetric: false,
     verboseMap: {},
     columnFormats: {},
+    currencyFormats: {},
     metricColorFormatters: [],
     dateFormatters: {},
     setDataMask: () => {},
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
index 3edb4619af..91fc5d260e 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
@@ -90,6 +90,7 @@ describe('PivotTableChart transformProps', () => {
       dateFormatters: {},
       emitCrossFilters: false,
       columnFormats: {},
+      currencyFormats: {},
     });
   });
 });
diff --git a/superset-frontend/plugins/plugin-chart-table/package.json b/superset-frontend/plugins/plugin-chart-table/package.json
index ac3b175660..54b209c2c5 100644
--- a/superset-frontend/plugins/plugin-chart-table/package.json
+++ b/superset-frontend/plugins/plugin-chart-table/package.json
@@ -2,30 +2,27 @@
   "name": "@superset-ui/plugin-chart-table",
   "version": "0.18.25",
   "description": "Superset Chart - Table",
-  "main": "lib/index.js",
-  "module": "esm/index.js",
-  "sideEffects": false,
-  "files": [
-    "esm",
-    "lib"
-  ],
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/apache/superset.git",
-    "directory": "superset-frontend/plugins/plugin-chart-table"
-  },
   "keywords": [
     "superset"
   ],
-  "author": "Superset",
-  "license": "Apache-2.0",
+  "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-table#readme",
   "bugs": {
     "url": "https://github.com/apache/superset/issues"
   },
-  "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-table#readme",
-  "publishConfig": {
-    "access": "public"
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/apache/superset.git",
+    "directory": "superset-frontend/plugins/plugin-chart-table"
   },
+  "license": "Apache-2.0",
+  "author": "Superset",
+  "sideEffects": false,
+  "main": "lib/index.js",
+  "module": "esm/index.js",
+  "files": [
+    "esm",
+    "lib"
+  ],
   "dependencies": {
     "@react-icons/all-files": "^4.1.0",
     "@types/d3-array": "^2.9.0",
@@ -40,15 +37,20 @@
     "regenerator-runtime": "^0.13.7",
     "xss": "^1.0.10"
   },
-  "devDependencies": {
-    "@testing-library/react": "^11.2.0"
-  },
   "peerDependencies": {
     "@superset-ui/chart-controls": "*",
     "@superset-ui/core": "*",
+    "@testing-library/dom": "^7.29.4",
+    "@testing-library/jest-dom": "^5.11.6",
+    "@testing-library/react": "^11.2.0",
+    "@testing-library/react-hooks": "^5.0.3",
+    "@testing-library/user-event": "^12.7.0",
     "@types/classnames": "*",
     "@types/react": "*",
     "react": "^16.13.1",
     "react-dom": "^16.13.1"
+  },
+  "publishConfig": {
+    "access": "public"
   }
 }
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index e03667a695..dfb398cab0 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -18,6 +18,7 @@
  */
 import memoizeOne from 'memoize-one';
 import {
+  CurrencyFormatter,
   DataRecord,
   extractTimegrain,
   GenericDataType,
@@ -84,7 +85,7 @@ const processColumns = memoizeOne(function processColumns(
   props: TableChartProps,
 ) {
   const {
-    datasource: { columnFormats, verboseMap },
+    datasource: { columnFormats, currencyFormats, verboseMap },
     rawFormData: {
       table_timestamp_format: tableTimestampFormat,
       metrics: metrics_,
@@ -123,6 +124,7 @@ const processColumns = memoizeOne(function processColumns(
       const isTime = dataType === GenericDataType.TEMPORAL;
       const isNumber = dataType === GenericDataType.NUMERIC;
       const savedFormat = columnFormats?.[key];
+      const currency = currencyFormats?.[key];
       const numberFormat = config.d3NumberFormat || savedFormat;
 
       let formatter;
@@ -155,7 +157,9 @@ const processColumns = memoizeOne(function processColumns(
         // percent metrics have a default format
         formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
       } else if (isMetric || (isNumber && numberFormat)) {
-        formatter = getNumberFormatter(numberFormat);
+        formatter = currency
+          ? new CurrencyFormatter({ d3Format: numberFormat, currency })
+          : getNumberFormatter(numberFormat);
       }
       return {
         key,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index f76d2718b4..35a463fe2c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -31,6 +31,7 @@ import {
   QueryFormData,
   SetDataMaskHook,
   ContextMenuFilters,
+  CurrencyFormatter,
 } from '@superset-ui/core';
 import { ColorFormatters, ColumnConfig } from '@superset-ui/chart-controls';
 
@@ -42,7 +43,11 @@ export interface DataColumnMeta {
   // `label` is verbose column name used for rendering
   label: string;
   dataType: GenericDataType;
-  formatter?: TimeFormatter | NumberFormatter | CustomFormatter;
+  formatter?:
+    | TimeFormatter
+    | NumberFormatter
+    | CustomFormatter
+    | CurrencyFormatter;
   isMetric?: boolean;
   isPercentMetric?: boolean;
   isNumeric?: boolean;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
index 607afa8ac3..139f92336c 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
@@ -47,7 +47,6 @@ function formatValue(
     return [false, 'N/A'];
   }
   if (formatter) {
-    // in case percent metric can specify percent format in the future
     return [false, formatter(value as number)];
   }
   if (typeof value === 'string') {
diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
index bd4b704391..fc060e6138 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/isEqualColumns.ts
@@ -27,6 +27,7 @@ export default function isEqualColumns(
   const b = propsB[0];
   return (
     a.datasource.columnFormats === b.datasource.columnFormats &&
+    a.datasource.currencyFormats === b.datasource.currencyFormats &&
     a.datasource.verboseMap === b.datasource.verboseMap &&
     a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
     a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index 1e699b6888..bd859b467c 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -19,6 +19,7 @@
 import React from 'react';
 import { CommonWrapper } from 'enzyme';
 import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
 import TableChart from '../src/TableChart';
 import transformProps from '../src/transformProps';
 import DateWithFormatter from '../src/utils/DateWithFormatter';
@@ -102,6 +103,26 @@ describe('plugin-chart-table', () => {
       expect(cells.eq(4).text()).toEqual('2.47k');
     });
 
+    it('render advanced data with currencies', () => {
+      render(
+        ProviderWrapper({
+          children: (
+            <TableChart
+              {...transformProps(testData.advancedWithCurrency)}
+              sticky={false}
+            />
+          ),
+        }),
+      );
+      const cells = document.querySelectorAll('td');
+      expect(document.querySelectorAll('th')[1]).toHaveTextContent(
+        'Sum of Num',
+      );
+      expect(cells[0]).toHaveTextContent('Michael');
+      expect(cells[2]).toHaveTextContent('12.346%');
+      expect(cells[4]).toHaveTextContent('$ 2.47k');
+    });
+
     it('render empty data', () => {
       wrap.setProps({ ...transformProps(testData.empty), sticky: false });
       tree = wrap.render();
diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
index 9896e7bf49..3f464181b7 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts
+++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts
@@ -173,6 +173,16 @@ const advanced: TableChartProps = {
   ],
 };
 
+const advancedWithCurrency = {
+  ...advanced,
+  datasource: {
+    ...advanced.datasource,
+    currencyFormats: {
+      sum__num: { symbol: 'USD', symbolPosition: 'prefix' },
+    },
+  },
+};
+
 const empty = {
   ...advanced,
   queriesData: [
@@ -186,5 +196,6 @@ const empty = {
 export default {
   basic,
   advanced,
+  advancedWithCurrency,
   empty,
 };
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 79b10b8fc7..d95839d972 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -25,6 +25,9 @@ import Alert from 'src/components/Alert';
 import Badge from 'src/components/Badge';
 import shortid from 'shortid';
 import {
+  css,
+  getCurrencySymbol,
+  ensureIsArray,
   FeatureFlag,
   styled,
   SupersetClient,
@@ -146,6 +149,11 @@ const DATA_TYPES = [
   { value: 'BOOLEAN', label: t('BOOLEAN') },
 ];
 
+const CURRENCY_SYMBOL_POSITION = [
+  { value: 'prefix', label: t('Prefix') },
+  { value: 'suffix', label: t('Suffix') },
+];
+
 const DATASOURCE_TYPES_ARR = [
   { key: 'physical', label: t('Physical (table or view)') },
   { key: 'virtual', label: t('Virtual (SQL)') },
@@ -572,6 +580,43 @@ function OwnersSelector({ datasource, onChange }) {
   );
 }
 
+const CurrencyControlContainer = styled.div`
+  ${({ theme }) => css`
+    display: flex;
+    align-items: center;
+
+    & > :first-child {
+      width: 25%;
+      margin-right: ${theme.gridUnit * 4}px;
+    }
+  `}
+`;
+const CurrencyControl = ({ onChange, value: currency = {}, currencies }) => (
+  <CurrencyControlContainer>
+    <Select
+      ariaLabel={t('Currency prefix or suffix')}
+      options={CURRENCY_SYMBOL_POSITION}
+      placeholder={t('Prefix or suffix')}
+      onChange={symbolPosition => {
+        onChange({ ...currency, symbolPosition });
+      }}
+      value={currency?.symbolPosition}
+      allowClear
+    />
+    <Select
+      ariaLabel={t('Currency symbol')}
+      options={currencies}
+      placeholder={t('Select or type currency symbol')}
+      onChange={symbol => {
+        onChange({ ...currency, symbol });
+      }}
+      value={currency?.symbol}
+      allowClear
+      allowNewOptions
+    />
+  </CurrencyControlContainer>
+);
+
 class DatasourceEditor extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -628,6 +673,12 @@ class DatasourceEditor extends React.PureComponent {
     this.allowEditSource = !isFeatureEnabled(
       FeatureFlag.DISABLE_DATASET_SOURCE_EDIT,
     );
+    this.currencies = ensureIsArray(props.currencies).map(currencyCode => ({
+      value: currencyCode,
+      label: `${getCurrencySymbol({
+        symbol: currencyCode,
+      })} (${currencyCode})`,
+    }));
   }
 
   onChange() {
@@ -839,6 +890,20 @@ class DatasourceEditor extends React.PureComponent {
       ),
     );
 
+    // validate currency code
+    try {
+      this.state.datasource.metrics?.forEach(
+        metric =>
+          metric.currency?.symbol &&
+          new Intl.NumberFormat('en-US', {
+            style: 'currency',
+            currency: metric.currency.symbol,
+          }),
+      );
+    } catch {
+      errors = errors.concat([t('Invalid currency code in saved metrics')]);
+    }
+
     this.setState({ errors }, callback);
   }
 
@@ -1228,6 +1293,11 @@ class DatasourceEditor extends React.PureComponent {
                   <TextControl controlId="d3format" placeholder="%y/%m/%d" />
                 }
               />
+              <Field
+                fieldKey="currency"
+                label={t('Metric currency')}
+                control={<CurrencyControl currencies={this.currencies} />}
+              />
               <Field
                 label={t('Certified by')}
                 fieldKey="certified_by"
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
index f598290ba8..abb77c7f19 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.test.jsx
@@ -28,7 +28,7 @@ const props = {
   datasource: mockDatasource['7__table'],
   addSuccessToast: () => {},
   addDangerToast: () => {},
-  onChange: () => {},
+  onChange: jest.fn(),
   columnLabels: {
     state: 'State',
   },
@@ -217,6 +217,90 @@ describe('DatasourceEditor RTL', () => {
     const warningMarkdown = await screen.findByPlaceholderText(/certified by/i);
     expect(warningMarkdown.value).toEqual('someone');
   });
+  it('renders currency controls', async () => {
+    const propsWithCurrency = {
+      ...props,
+      currencies: ['USD', 'GBP', 'EUR'],
+      datasource: {
+        ...props.datasource,
+        metrics: [
+          {
+            ...props.datasource.metrics[0],
+            currency: { symbol: 'USD', symbolPosition: 'prefix' },
+          },
+          ...props.datasource.metrics.slice(1),
+        ],
+      },
+    };
+    await asyncRender(propsWithCurrency);
+    const metricButton = screen.getByTestId('collection-tab-Metrics');
+    userEvent.click(metricButton);
+    const expandToggle = await screen.findAllByLabelText(/toggle expand/i);
+    userEvent.click(expandToggle[0]);
+
+    expect(await screen.findByText('Metric currency')).toBeVisible();
+    expect(
+      await waitFor(() =>
+        document.querySelector(
+          `[aria-label='Currency prefix or suffix'] .ant-select-selection-item`,
+        ),
+      ),
+    ).toHaveTextContent('Prefix');
+    await userEvent.click(
+      screen.getByRole('combobox', { name: 'Currency prefix or suffix' }),
+    );
+    const positionOptions = await waitFor(() =>
+      document.querySelectorAll(
+        `[aria-label='Currency prefix or suffix'] .ant-select-item-option-content`,
+      ),
+    );
+    expect(positionOptions[0]).toHaveTextContent('Prefix');
+    expect(positionOptions[1]).toHaveTextContent('Suffix');
+
+    propsWithCurrency.onChange.mockClear();
+    await userEvent.click(positionOptions[1]);
+    expect(propsWithCurrency.onChange.mock.calls[0][0]).toMatchObject(
+      expect.objectContaining({
+        metrics: expect.arrayContaining([
+          expect.objectContaining({
+            currency: { symbolPosition: 'suffix', symbol: 'USD' },
+          }),
+        ]),
+      }),
+    );
+
+    expect(
+      await waitFor(() =>
+        document.querySelector(
+          `[aria-label='Currency symbol'] .ant-select-selection-item`,
+        ),
+      ),
+    ).toHaveTextContent('$ (USD)');
+
+    propsWithCurrency.onChange.mockClear();
+    await userEvent.click(
+      screen.getByRole('combobox', { name: 'Currency symbol' }),
+    );
+    const symbolOptions = await waitFor(() =>
+      document.querySelectorAll(
+        `[aria-label='Currency symbol'] .ant-select-item-option-content`,
+      ),
+    );
+    expect(symbolOptions[0]).toHaveTextContent('$ (USD)');
+    expect(symbolOptions[1]).toHaveTextContent('£ (GBP)');
+    expect(symbolOptions[2]).toHaveTextContent('€ (EUR)');
+
+    await userEvent.click(symbolOptions[1]);
+    expect(propsWithCurrency.onChange.mock.calls[0][0]).toMatchObject(
+      expect.objectContaining({
+        metrics: expect.arrayContaining([
+          expect.objectContaining({
+            currency: { symbolPosition: 'suffix', symbol: 'GBP' },
+          }),
+        ]),
+      }),
+    );
+  });
   it('properly updates the metric information', async () => {
     await asyncRender(props);
     const metricButton = screen.getByTestId('collection-tab-Metrics');
diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx
index 1a7bf227ec..c5063890cb 100644
--- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx
+++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx
@@ -19,7 +19,14 @@
 import React, { FunctionComponent, useState, useRef } from 'react';
 import Alert from 'src/components/Alert';
 import Button from 'src/components/Button';
-import { FeatureFlag, styled, SupersetClient, t } from '@superset-ui/core';
+import {
+  FeatureFlag,
+  isDefined,
+  Metric,
+  styled,
+  SupersetClient,
+  t,
+} from '@superset-ui/core';
 
 import Modal from 'src/components/Modal';
 import AsyncEsmComponent from 'src/components/AsyncEsmComponent';
@@ -27,6 +34,7 @@ import { isFeatureEnabled } from 'src/featureFlags';
 
 import { getClientErrorObject } from 'src/utils/getClientErrorObject';
 import withToasts from 'src/components/MessageToasts/withToasts';
+import { useSelector } from 'react-redux';
 
 const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor'));
 
@@ -81,7 +89,21 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
   onHide,
   show,
 }) => {
-  const [currentDatasource, setCurrentDatasource] = useState(datasource);
+  const [currentDatasource, setCurrentDatasource] = useState({
+    ...datasource,
+    metrics: datasource?.metrics?.map((metric: Metric) => ({
+      ...metric,
+      currency: JSON.parse(metric.currency || 'null'),
+    })),
+  });
+  const currencies = useSelector<
+    {
+      common: {
+        currencies: string[];
+      };
+    },
+    string[]
+  >(state => state.common?.currencies);
   const [errors, setErrors] = useState<any[]>([]);
   const [isSaving, setIsSaving] = useState(false);
   const [isEditing, setIsEditing] = useState<boolean>(false);
@@ -125,7 +147,10 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
               description: metric.description,
               metric_name: metric.metric_name,
               metric_type: metric.metric_type,
-              d3format: metric.d3format,
+              d3format: metric.d3format || null,
+              currency: !isDefined(metric.currency)
+                ? null
+                : JSON.stringify(metric.currency),
               verbose_name: metric.verbose_name,
               warning_text: metric.warning_text,
               uuid: metric.uuid,
@@ -297,6 +322,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
         datasource={currentDatasource}
         onChange={onDatasourceChange}
         setIsEditing={setIsEditing}
+        currencies={currencies}
       />
       {contextHolder}
     </StyledDatasourceModal>
diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts
index 588e5b6cd5..ecc893bcb7 100644
--- a/superset-frontend/src/dashboard/constants.ts
+++ b/superset-frontend/src/dashboard/constants.ts
@@ -29,6 +29,7 @@ export const PLACEHOLDER_DATASOURCE: Datasource = {
   column_types: [],
   metrics: [],
   column_formats: {},
+  currency_formats: {},
   verbose_map: {},
   main_dttm_col: '',
   description: '',
diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
index bca3aecfd6..a844ff4789 100644
--- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts
+++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
@@ -35,6 +35,7 @@ const CURRENT_DATASOURCE = {
   columns: [],
   metrics: [],
   column_formats: {},
+  currency_formats: {},
   verbose_map: {},
   main_dttm_col: '__timestamp',
   // eg. ['["ds", true]', 'ds [asc]']
@@ -48,6 +49,7 @@ const NEW_DATASOURCE = {
   columns: [],
   metrics: [],
   column_formats: {},
+  currency_formats: {},
   verbose_map: {},
   main_dttm_col: '__timestamp',
   // eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts
index e259f62671..bced548b8d 100644
--- a/superset-frontend/src/explore/actions/hydrateExplore.ts
+++ b/superset-frontend/src/explore/actions/hydrateExplore.ts
@@ -96,6 +96,7 @@ export const hydrateExplore =
     if (dashboardId) {
       initialFormData.dashboardId = dashboardId;
     }
+
     const initialDatasource = dataset;
 
     const initialExploreState = {
diff --git a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
index e03aba06d1..c18873460a 100644
--- a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
+++ b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
@@ -52,6 +52,7 @@ describe('controlUtils', () => {
       columns: [{ column_name: 'a' }],
       metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
       column_formats: {},
+      currency_formats: {},
       verbose_map: {},
       main_dttm_col: '',
       datasource_name: '1__table',
diff --git a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
index c8d34f749d..f0eb399267 100644
--- a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
+++ b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
@@ -35,6 +35,7 @@ const sampleDatasource: Dataset = {
   ],
   metrics: [{ metric_name: 'saved_metric_2' }],
   column_formats: {},
+  currency_formats: {},
   verbose_map: {},
   main_dttm_col: '',
   datasource_name: 'Sample Dataset',
diff --git a/superset-frontend/src/explore/fixtures.tsx b/superset-frontend/src/explore/fixtures.tsx
index a0f3c112ec..7502094ca2 100644
--- a/superset-frontend/src/explore/fixtures.tsx
+++ b/superset-frontend/src/explore/fixtures.tsx
@@ -136,6 +136,7 @@ export const exploreInitialData: ExplorePageInitialData = {
     columns: [{ column_name: 'a' }],
     metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
     column_formats: {},
+    currency_formats: {},
     verbose_map: {},
     main_dttm_col: '',
     datasource_name: '8__table',
@@ -154,6 +155,7 @@ export const fallbackExploreInitialData: ExplorePageInitialData = {
     columns: [],
     metrics: [],
     column_formats: {},
+    currency_formats: {},
     verbose_map: {},
     main_dttm_col: '',
     owners: [],
diff --git a/superset-frontend/src/features/datasets/types.ts b/superset-frontend/src/features/datasets/types.ts
index 97d6f5a280..4c2c5b8a95 100644
--- a/superset-frontend/src/features/datasets/types.ts
+++ b/superset-frontend/src/features/datasets/types.ts
@@ -1,3 +1,5 @@
+import { Currency } from '@superset-ui/core';
+
 /**
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -39,6 +41,7 @@ type MetricObject = {
   metric_name: string;
   metric_type: string;
   d3format?: string;
+  currency?: Currency;
   warning_text?: string;
 };
 
diff --git a/superset-frontend/src/utils/getDatasourceUid.test.ts b/superset-frontend/src/utils/getDatasourceUid.test.ts
index d3a629efcf..ed7ec6256b 100644
--- a/superset-frontend/src/utils/getDatasourceUid.test.ts
+++ b/superset-frontend/src/utils/getDatasourceUid.test.ts
@@ -26,6 +26,7 @@ const TEST_DATASOURCE = {
   columns: [],
   metrics: [],
   column_formats: {},
+  currency_formats: {},
   verbose_map: {},
   main_dttm_col: '__timestamp',
   // eg. ['["ds", true]', 'ds [asc]']
diff --git a/superset/config.py b/superset/config.py
index b880acf5a7..abb73e9f56 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -374,6 +374,8 @@ class D3Format(TypedDict, total=False):
 
 D3_FORMAT: D3Format = {}
 
+CURRENCIES = ["USD", "EUR", "GBP", "INR", "MXN", "JPY", "CNY"]
+
 # ---------------------------------------------------
 # Feature flags
 # ---------------------------------------------------
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index 647df374c9..56e5ef18ad 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -18,9 +18,11 @@ from __future__ import annotations
 
 import builtins
 import json
+import logging
 from collections.abc import Hashable
 from datetime import datetime
 from enum import Enum
+from json.decoder import JSONDecodeError
 from typing import Any, TYPE_CHECKING
 
 from flask_appbuilder.security.sqla.models import User
@@ -47,6 +49,8 @@ from superset.utils.core import GenericDataType, MediumText
 if TYPE_CHECKING:
     from superset.db_engine_specs.base import BaseEngineSpec
 
+logger = logging.getLogger(__name__)
+
 METRIC_FORM_DATA_PARAMS = [
     "metric",
     "metric_2",
@@ -224,6 +228,10 @@ class BaseDatasource(
     def column_formats(self) -> dict[str, str | None]:
         return {m.metric_name: m.d3format for m in self.metrics if m.d3format}
 
+    @property
+    def currency_formats(self) -> dict[str, dict[str, str | None] | None]:
+        return {m.metric_name: m.currency_json for m in self.metrics if m.currency_json}
+
     def add_missing_metrics(self, metrics: list[BaseMetric]) -> None:
         existing_metrics = {m.metric_name for m in self.metrics}
         for metric in metrics:
@@ -282,6 +290,7 @@ class BaseDatasource(
             "id": self.id,
             "uid": self.uid,
             "column_formats": self.column_formats,
+            "currency_formats": self.currency_formats,
             "description": self.description,
             "database": self.database.data,  # pylint: disable=no-member
             "default_endpoint": self.default_endpoint,
@@ -717,6 +726,7 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
     metric_type = Column(String(32))
     description = Column(MediumText())
     d3format = Column(String(128))
+    currency = Column(String(128))
     warning_text = Column(Text)
 
     """
@@ -733,6 +743,16 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
         enable_typechecks=False)
     """
 
+    @property
+    def currency_json(self) -> dict[str, str | None] | None:
+        try:
+            return json.loads(self.currency or "{}") or None
+        except (TypeError, JSONDecodeError) as exc:
+            logger.error(
+                "Unable to load currency json: %r. Leaving empty.", exc, exc_info=True
+            )
+            return None
+
     @property
     def perm(self) -> str | None:
         raise NotImplementedError()
@@ -751,5 +771,6 @@ class BaseMetric(AuditMixinNullable, ImportExportMixin):
             "expression",
             "warning_text",
             "d3format",
+            "currency",
         )
         return {s: getattr(self, s) for s in attrs}
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 4eebec6be7..9efbd1db92 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -416,6 +416,7 @@ class SqlMetric(Model, BaseMetric, CertificationMixin):
         "expression",
         "description",
         "d3format",
+        "currency",
         "extra",
         "warning_text",
     ]
diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py
index 9116b9636e..65c6f110e4 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -217,6 +217,7 @@ class SqlMetricInlineView(  # pylint: disable=too-many-ancestors
         "expression",
         "table",
         "d3format",
+        "currency",
         "extra",
         "warning_text",
     ]
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 9d6e712a73..7905641f80 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -218,6 +218,7 @@ class DashboardDatasetSchema(Schema):
     id = fields.Int()
     uid = fields.Str()
     column_formats = fields.Dict()
+    currency_formats = fields.Dict()
     database = fields.Nested(DatabaseSchema)
     default_endpoint = fields.String()
     filter_select = fields.Bool()
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 2b6f417e37..87e1d9e74c 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -171,6 +171,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "metrics.changed_on",
         "metrics.created_on",
         "metrics.d3format",
+        "metrics.currency",
         "metrics.description",
         "metrics.expression",
         "metrics.extra",
@@ -201,6 +202,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "datasource_name",
         "name",
         "column_formats",
+        "currency_formats",
         "granularity_sqla",
         "time_grain_sqla",
         "order_by_choices",
diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py
index f95897ce59..2b65d674ed 100644
--- a/superset/datasets/schemas.py
+++ b/superset/datasets/schemas.py
@@ -71,6 +71,7 @@ class DatasetMetricsPutSchema(Schema):
     metric_name = fields.String(required=True, validate=Length(1, 255))
     metric_type = fields.String(allow_none=True, validate=Length(1, 32))
     d3format = fields.String(allow_none=True, validate=Length(1, 128))
+    currency = fields.String(allow_none=True, required=False, validate=Length(1, 128))
     verbose_name = fields.String(allow_none=True, metadata={Length: (1, 1024)})
     warning_text = fields.String(allow_none=True)
     uuid = fields.UUID(allow_none=True)
@@ -191,6 +192,7 @@ class ImportV1MetricSchema(Schema):
     expression = fields.String(required=True)
     description = fields.String(allow_none=True)
     d3format = fields.String(allow_none=True)
+    currency = fields.String(allow_none=True, required=False)
     extra = fields.Dict(allow_none=True)
     warning_text = fields.String(allow_none=True)
 
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index f4b75c50b1..d117766754 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -158,6 +158,7 @@ class MetricType(TypedDict, total=False):
     metric_type: str | None
     description: str | None
     d3format: str | None
+    currency: str | None
     warning_text: str | None
     extra: str | None
 
diff --git a/superset/explore/schemas.py b/superset/explore/schemas.py
index 37044c0394..75c3dcac2c 100644
--- a/superset/explore/schemas.py
+++ b/superset/explore/schemas.py
@@ -25,6 +25,7 @@ class DatasetSchema(Schema):
         }
     )
     column_formats = fields.Dict(metadata={"description": "Column formats."})
+    currency_formats = fields.Dict(metadata={"description": "Currency formats."})
     columns = fields.List(fields.Dict(), metadata={"description": "Columns metadata."})
     database = fields.Dict(
         metadata={"description": "Database associated with the dataset."}
diff --git a/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py b/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py
new file mode 100644
index 0000000000..7d6f0f2ba0
--- /dev/null
+++ b/superset/migrations/versions/2023-06-21_14-02_90139bf715e4_add_currency_column_to_metrics.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""add_currency_column_to_metrics
+
+Revision ID: 90139bf715e4
+Revises: 83e1abbe777f
+Create Date: 2023-06-21 14:02:08.200955
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "90139bf715e4"
+down_revision = "83e1abbe777f"
+
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+    op.add_column("metrics", sa.Column("currency", sa.String(128), nullable=True))
+    op.add_column("sql_metrics", sa.Column("currency", sa.String(128), nullable=True))
+
+
+def downgrade():
+    with op.batch_alter_table("sql_metrics") as batch_op_sql_metrics:
+        batch_op_sql_metrics.drop_column("currency")
+    with op.batch_alter_table("metrics") as batch_op_metrics:
+        batch_op_metrics.drop_column("currency")
diff --git a/superset/views/base.py b/superset/views/base.py
index e66fea0a48..717efdff84 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -430,6 +430,7 @@ def cached_common_bootstrap_data(user: User) -> dict[str, Any]:
         "locale": locale,
         "language_pack": get_language_pack(locale),
         "d3_format": conf.get("D3_FORMAT"),
+        "currencies": conf.get("CURRENCIES"),
         "feature_flags": get_feature_flags(),
         "extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
         "extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
diff --git a/tests/integration_tests/datasets/commands_tests.py b/tests/integration_tests/datasets/commands_tests.py
index 34a0625b36..4919b8886d 100644
--- a/tests/integration_tests/datasets/commands_tests.py
+++ b/tests/integration_tests/datasets/commands_tests.py
@@ -148,6 +148,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
             "main_dttm_col": None,
             "metrics": [
                 {
+                    "currency": None,
                     "d3format": None,
                     "description": None,
                     "expression": "COUNT(*)",
@@ -158,6 +159,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
                     "warning_text": None,
                 },
                 {
+                    "currency": None,
                     "d3format": None,
                     "description": None,
                     "expression": "SUM(value)",
@@ -381,6 +383,7 @@ class TestImportDatasetsCommand(SupersetTestCase):
         assert metric.expression == "count(1)"
         assert metric.description is None
         assert metric.d3format is None
+        assert metric.currency is None
         assert metric.extra == "{}"
         assert metric.warning_text is None
 
diff --git a/tests/unit_tests/datasets/commands/export_test.py b/tests/unit_tests/datasets/commands/export_test.py
index c3ad4f764c..17913c2ca4 100644
--- a/tests/unit_tests/datasets/commands/export_test.py
+++ b/tests/unit_tests/datasets/commands/export_test.py
@@ -116,6 +116,7 @@ metrics:
   expression: COUNT(*)
   description: null
   d3format: null
+  currency: null
   extra:
     warning_markdown: null
   warning_text: null