You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2024/06/25 13:29:17 UTC

(superset) 19/22: Merge

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

diegopucci pushed a commit to branch geido/feat/dashboard-endpoint-screenshot
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ab07b4520e087a7908a7a14da32f4031fbbd5ce0
Author: Diego Pucci <di...@gmail.com>
AuthorDate: Tue Jun 25 10:48:52 2024 +0200

    Merge
---
 .asf.yaml                                          |    3 +
 .github/workflows/check_db_migration_confict.yml   |    2 +-
 .github/workflows/codeql-analysis.yml              |    2 +-
 .github/workflows/docker.yml                       |    2 +-
 .github/workflows/embedded-sdk-release.yml         |    2 +-
 .github/workflows/generate-FOSSA-report.yml        |    2 +-
 .github/workflows/github-action-validator.yml      |    2 +-
 .github/workflows/pre-commit.yml                   |    2 +-
 .github/workflows/prefer-typescript.yml            |    2 +-
 .github/workflows/release.yml                      |    2 +-
 .github/workflows/superset-cli.yml                 |    2 +-
 .github/workflows/superset-e2e.yml                 |    8 +-
 .github/workflows/superset-frontend.yml            |    2 +-
 .github/workflows/superset-helm-release.yml        |    2 +-
 .../workflows/superset-python-integrationtest.yml  |    4 +-
 .github/workflows/superset-python-misc.yml         |    2 +-
 .github/workflows/superset-python-presto-hive.yml  |    2 +-
 .github/workflows/superset-python-unittest.yml     |    2 +-
 .github/workflows/superset-translations.yml        |    2 +-
 .github/workflows/superset-websocket.yml           |    2 +-
 .github/workflows/tech-debt.yml                    |    2 +-
 .gitignore                                         |    1 +
 .rat-excludes                                      |    1 +
 README.md                                          |    4 -
 RESOURCES/FEATURE_FLAGS.md                         |    1 +
 RESOURCES/INTHEWILD.md                             |    3 +
 UPDATING.md                                        |    4 +
 docker/pythonpath_dev/superset_config.py           |    1 -
 docs/docs/configuration/alerts-reports.mdx         |   11 +
 docs/docs/security/cves.mdx                        |    6 +
 docs/package.json                                  |    4 +-
 docs/src/intro_header.txt                          |    4 +
 docs/yarn.lock                                     |   12 +-
 pyproject.toml                                     |   12 +-
 scripts/cypress_run.py                             |   11 +-
 scripts/tests/run.sh                               |    3 +
 superset-embedded-sdk/package-lock.json            |   40 +-
 superset-frontend/cypress-base/cypress.config.ts   |    2 +-
 .../cypress/e2e/dashboard/editmode.test.ts         |   49 +-
 .../e2e/explore/visualizations/compare.test.js     |    3 -
 .../e2e/explore/visualizations/dist_bar.test.js    |    3 -
 superset-frontend/cypress-base/package-lock.json   |   28 +-
 superset-frontend/package-lock.json                |   94 +-
 .../src/components/Dropdown.tsx}                   |   15 +-
 .../src/components/Menu.tsx}                       |   15 +-
 .../superset-ui-chart-controls/src/index.ts        |    4 +-
 .../src/shared-controls/sharedControls.tsx         |    9 +
 .../packages/superset-ui-core/package.json         |    3 +-
 .../src/color/CategoricalColorScale.ts             |  217 +++--
 ...olorSingleton.ts => LabelsColorMapSingleton.ts} |   74 +-
 .../packages/superset-ui-core/src/color/index.ts   |    8 +-
 .../packages/superset-ui-core/src/color/utils.ts   |    5 +-
 .../useComponentDidUpdate/useComponentDidUpdate.ts |   10 +-
 .../src/time-comparison/customTimeRangeDecode.ts}  |   94 +-
 .../src/time-comparison/getTimeOffset.ts           |  307 ++++--
 .../superset-ui-core/src/time-comparison/index.ts  |    7 +-
 .../superset-ui-core/src/time-comparison/types.ts  |   42 +
 .../superset-ui-core/src/ui-overrides/types.ts     |   11 +
 .../superset-ui-core/src/utils/featureFlags.ts     |    1 +
 .../test/color/CategoricalColorNameSpace.test.ts   |    2 +-
 .../test/color/CategoricalColorScale.test.ts       |  286 ++++--
 .../test/color/LabelsColorMapSingleton.test.ts     |  234 +++++
 .../test/color/SharedLabelColorSingleton.test.ts   |  201 ----
 .../time-comparison/computeCustomDateTime.test.ts  |   80 ++
 .../time-comparison/customTimeRangeDecode.test.ts  |  205 ++++
 .../test/time-comparison/fetchTimeRange.test.ts    |    1 +
 .../test/time-comparison/getTimeOffset.test.ts     | 1016 +++++++++++++++++++-
 .../test/time-comparison/parseDttmToDate.test.ts   |  759 ++++++++++++---
 .../plugins/legacy-plugin-chart-chord/src/Chord.js |    4 +-
 .../legacy-plugin-chart-chord/src/controlPanel.ts  |   13 +-
 .../scripts/Country Map GeoJSON Generator.ipynb    |  311 +++---
 .../src/countries/india.geojson                    |    4 +-
 .../src/controlPanel.tsx                           |   13 +-
 .../src/Histogram.jsx                              |    2 +-
 .../legacy-plugin-chart-paired-t-test/package.json |    2 +-
 .../legacy-plugin-chart-partition/src/Partition.js |    2 +-
 .../plugins/legacy-plugin-chart-rose/src/Rose.js   |   14 +-
 .../legacy-plugin-chart-sankey/src/Sankey.js       |    2 +-
 .../legacy-plugin-chart-sankey/src/controlPanel.ts |   13 +-
 .../src/controlPanel.ts                            |   13 +-
 .../legacy-preset-chart-deckgl/package.json        |    2 +-
 .../src/CategoricalDeckGLContainer.tsx             |   10 +-
 .../legacy-preset-chart-nvd3/src/NVD3Vis.js        |    4 +-
 .../BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx |   10 +-
 .../BigNumberPeriodOverPeriod/buildQuery.ts        |   30 +-
 .../BigNumberPeriodOverPeriod/transformProps.ts    |   31 +-
 .../src/BoxPlot/transformProps.ts                  |    6 +-
 .../src/Funnel/controlPanel.tsx                    |    6 +-
 .../src/Funnel/transformProps.ts                   |    2 +-
 .../src/Gauge/controlPanel.tsx                     |   13 +-
 .../src/Gauge/transformProps.ts                    |    4 +-
 .../src/Graph/transformProps.ts                    |    2 +-
 .../plugin-chart-echarts/src/Pie/controlPanel.tsx  |   27 +-
 .../plugin-chart-echarts/src/Pie/transformProps.ts |   48 +-
 .../plugins/plugin-chart-echarts/src/Pie/types.ts  |    2 +
 .../src/Radar/transformProps.ts                    |    2 +-
 .../src/Sunburst/controlPanel.tsx                  |   13 +-
 .../src/Treemap/controlPanel.tsx                   |   13 +-
 .../src/Treemap/transformProps.ts                  |    4 +-
 .../test/Pie/transformProps.test.ts                |  111 +++
 .../plugins/plugin-chart-table/package.json        |    1 +
 .../plugin-chart-table/src/DataTable/DataTable.tsx |   20 +-
 .../src/DataTable/hooks/useSticky.tsx              |    4 +-
 .../plugins/plugin-chart-table/src/Styles.tsx      |    7 +
 .../plugins/plugin-chart-table/src/TableChart.tsx  |  343 ++++++-
 .../plugins/plugin-chart-table/src/buildQuery.ts   |   92 +-
 .../plugin-chart-table/src/controlPanel.tsx        |  179 +++-
 .../plugin-chart-table/src/transformProps.ts       |  352 ++++++-
 .../plugins/plugin-chart-table/src/types.ts        |   15 +
 .../src/chart/WordCloud.tsx                        |    9 +-
 .../src/legacyPlugin/transformProps.ts             |    1 +
 .../src/plugin/controlPanel.ts                     |   13 +-
 .../src/plugin/transformProps.ts                   |    4 +-
 .../test/legacyPlugin/transformProps.test.ts       |    1 +
 superset-frontend/spec/helpers/testing-library.tsx |   24 +-
 superset-frontend/src/SqlLab/actions/sqlLab.js     |  143 ++-
 .../src/SqlLab/actions/sqlLab.test.js              |   84 +-
 .../EditorAutoSync/EditorAutoSync.test.tsx         |   61 ++
 .../src/SqlLab/components/EditorAutoSync/index.tsx |   69 +-
 .../SqlLab/components/ResultSet/ResultSet.test.tsx |   78 ++
 .../src/SqlLab/components/ResultSet/index.tsx      |   54 +-
 .../components/RunQueryActionButton/index.tsx      |   14 +-
 .../src/SqlLab/components/SaveQuery/index.tsx      |   14 +-
 .../SqlLab/components/ShareSqlLabQuery/index.tsx   |    7 +-
 .../SqlLab/components/SqlEditor/SqlEditor.test.tsx |    3 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      |   68 +-
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |   12 +-
 .../SqlLab/components/TabbedSqlEditors/index.tsx   |   17 +-
 superset-frontend/src/SqlLab/fixtures.ts           |    1 +
 .../middlewares/persistSqlLabStateEnhancer.js      |   16 +-
 .../src/SqlLab/reducers/getInitialState.test.ts    |   18 +-
 .../src/SqlLab/reducers/getInitialState.ts         |   16 +
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |   12 +
 .../src/SqlLab/reducers/sqlLab.test.js             |   17 +
 superset-frontend/src/SqlLab/types.ts              |    2 +
 .../src/components/Alert/Alert.stories.tsx         |    3 +-
 .../AsyncAceEditor/AsyncAceEditor.stories.tsx      |    6 +-
 .../src/components/Badge/Badge.stories.tsx         |   10 +-
 .../src/components/Button/Button.stories.tsx       |    6 +-
 .../components/ButtonGroup/ButtonGroup.stories.tsx |    6 +-
 superset-frontend/src/components/Chart/Chart.jsx   |    4 +-
 .../Chart/ChartContextMenu/ChartContextMenu.tsx    |   16 +-
 .../src/components/Chart/ChartRenderer.jsx         |    8 +-
 .../Chart/DrillBy/DrillByMenuItems.test.tsx        |   10 +-
 .../components/Chart/DrillBy/DrillByMenuItems.tsx  |  197 ++--
 .../components/Chart/DrillBy/DrillByModal.test.tsx |    1 +
 .../src/components/Chart/DrillBy/DrillByModal.tsx  |    3 +
 .../Chart/DrillBy/useResultsTableView.test.ts      |    4 +-
 .../Chart/DrillBy/useResultsTableView.tsx          |    3 +
 .../components/Chart/MenuItemWithTruncation.tsx    |    3 +-
 .../CopyToClipboard/CopyToClipboard.stories.tsx    |    3 +-
 .../components/DatePicker/DatePicker.stories.tsx   |    4 +-
 .../src/components/Dropdown/Dropdown.stories.tsx   |    3 +-
 .../DropdownButton/DropdownButton.stories.tsx      |    3 +-
 .../components/IconButton/IconButton.stories.tsx   |   12 +-
 .../components/IconTooltip/IconTooltip.stories.tsx |    3 +-
 .../src/components/Icons/Icons.stories.tsx         |    6 +-
 .../components/InfoTooltip/InfoTooltip.stories.tsx |   30 +-
 .../src/components/InfoTooltip/index.tsx           |   15 +-
 .../src/components/Loading/Loading.stories.tsx     |    3 +-
 .../src/components/Popover/Popover.stories.tsx     |    6 +-
 .../PopoverDropdown/PopoverDropdown.stories.tsx    |    6 +-
 .../components/ProgressBar/ProgressBar.stories.tsx |    6 +-
 .../src/components/Switch/Switch.stories.tsx       |    3 +-
 .../src/components/Table/Table.stories.tsx         |    6 +-
 .../cell-renderers/TimeCell/TimeCell.stories.tsx   |    2 +-
 .../src/components/TableView/TableView.stories.tsx |    2 +-
 .../src/components/Tabs/Tabs.stories.tsx           |    2 +-
 .../src/components/Timer/Timer.stories.tsx         |   18 +-
 .../src/components/Tooltip/Tooltip.stories.tsx     |    6 +-
 .../src/dashboard/actions/dashboardInfo.ts         |   39 +-
 .../src/dashboard/actions/dashboardState.js        |   80 +-
 superset-frontend/src/dashboard/actions/hydrate.js |   11 -
 .../components/ColorSchemeControlWrapper.jsx       |    8 +-
 .../DashboardBuilder/DashboardContainer.tsx        |  118 +--
 .../HeaderActionsDropdown.test.tsx                 |    7 +-
 .../Header/HeaderActionsDropdown/index.jsx         |   14 +-
 .../src/dashboard/components/Header/index.jsx      |   13 +-
 .../src/dashboard/components/Header/types.ts       |    6 +
 .../dashboard/components/PropertiesModal/index.tsx |   36 +-
 .../components/RefreshIntervalModal.test.tsx       |    2 +-
 .../src/dashboard/components/SliceHeader/index.tsx |    2 +
 .../components/SliceHeaderControls/index.tsx       |   15 +
 .../components/SyncDashboardState/index.tsx        |    4 +-
 .../dashboard/components/gridComponents/Chart.jsx  |   22 +-
 .../dashboard/components/gridComponents/Tabs.jsx   |    1 +
 .../src/dashboard/components/menu/HoverMenu.tsx    |    2 +-
 .../ScopingModal/ScopingModal.test.tsx             |   34 +-
 .../FilterBar/FilterControls/FilterValue.tsx       |    2 +-
 .../src/dashboard/containers/Chart.jsx             |   12 +-
 .../dashboard/containers/DashboardComponent.jsx    |    1 +
 .../src/dashboard/containers/DashboardPage.tsx     |   23 +-
 superset-frontend/src/dashboard/types.ts           |    1 +
 .../util/charts/getFormDataWithExtraFilters.ts     |   16 +-
 .../src/explore/actions/exploreActions.test.js     |    4 +-
 .../components/DataTablesPane/DataTablesPane.tsx   |    3 +
 .../components/DataTableControls.tsx               |    5 +-
 .../components/ResultsPaneOnDashboard.tsx          |    2 +
 .../DataTablesPane/components/SamplesPane.tsx      |    3 +
 .../components/SingleQueryResultPane.tsx           |    2 +
 .../DataTablesPane/components/useResultsPane.tsx   |    3 +
 .../DataTablesPane/test/DataTablesPane.test.tsx    |   28 +
 .../components/DataTablesPane/test/fixture.tsx     |    3 +
 .../src/explore/components/DataTablesPane/types.ts |    5 +
 .../ExploreChartHeader/ExploreChartHeader.test.tsx |   58 +-
 .../components/ExploreChartHeader/index.jsx        |   38 +-
 .../explore/components/ExploreChartPanel/index.jsx |    2 +
 .../ExploreViewContainer.test.tsx                  |   73 ++
 .../components/ExploreViewContainer/index.jsx      |   15 +-
 .../ColorSchemeControl/ColorSchemeControl.test.tsx |   13 +-
 .../ColorSchemeControl/ColorSchemeLabel.tsx        |   12 +-
 .../controls/ColorSchemeControl/index.tsx          |   14 +-
 .../components/controls/ComparisonRangeLabel.tsx   |   61 +-
 .../ConditionalFormattingControl.tsx               |    3 +
 .../FormattingPopover.tsx                          |    2 +
 .../FormattingPopoverContent.tsx                   |   38 +-
 .../controls/ConditionalFormattingControl/types.ts |    2 +
 .../DateFilterControl/components/CustomFrame.tsx   |    3 +-
 .../controls/DateFilterControl/tests/utils.test.ts |  115 +--
 .../DateFilterControl/utils/dateFilterUtils.ts     |    7 +-
 .../controls/DateFilterControl/utils/dateParser.ts |  144 +--
 .../controls/FilterControl/AdhocFilter/index.js    |    1 +
 .../FilterControl/AdhocFilterEditPopover/index.jsx |    5 +-
 .../AdhocFilterEditPopoverSqlTabContent.test.jsx   |   73 --
 .../AdhocFilterEditPopoverSqlTabContent.test.tsx   |   75 ++
 .../AdhocFilterEditPopoverSqlTabContent/index.jsx  |   26 +-
 .../AdhocMetricEditPopover.test.tsx                |    7 +-
 .../components/controls/TimeOffsetControl.tsx      |  148 ++-
 .../useExploreAdditionalActionsMenu/index.jsx      |   30 +-
 superset-frontend/src/explore/constants.ts         |    8 +
 .../src/explore/reducers/exploreReducer.js         |    2 +-
 .../explore/reducers/exploreReducer.test.js}       |   31 +-
 .../databases/DatabaseModal/ExtraOptions.tsx       |   23 +-
 .../features/rls/RowLevelSecurityModal.test.tsx    |   31 +-
 .../src/hooks/apiResources/catalogs.ts             |   54 +-
 .../src/hooks/apiResources/schemas.test.ts         |   30 +-
 .../src/hooks/apiResources/schemas.ts              |   75 +-
 .../src/hooks/apiResources/sqlEditorTabs.test.ts   |   46 +-
 .../src/hooks/apiResources/sqlEditorTabs.ts        |   20 +-
 superset-frontend/src/logger/LogUtils.ts           |   16 +
 superset-frontend/src/logger/useLogAction.test.ts  |   55 ++
 .../types.ts => src/logger/useLogAction.ts}        |   30 +-
 superset-frontend/src/pages/Chart/index.tsx        |   14 +-
 .../src/types/DashboardContextForExplore.ts        |    4 +-
 superset-frontend/src/utils/colorScheme.ts         |  140 +++
 superset-websocket/package-lock.json               |   42 +-
 superset-websocket/package.json                    |    2 +-
 superset/app.py                                    |    4 +-
 superset/charts/data/api.py                        |   10 +-
 superset/commands/chart/importers/dispatcher.py    |    8 +-
 .../commands/dashboard/importers/dispatcher.py     |    8 +-
 superset/commands/dashboard/permalink/create.py    |    2 +
 superset/commands/database/create.py               |   45 +-
 superset/commands/database/importers/dispatcher.py |    8 +-
 superset/commands/database/ssh_tunnel/create.py    |   13 +-
 superset/commands/database/tables.py               |    4 +-
 superset/commands/database/test_connection.py      |    4 +-
 superset/commands/database/update.py               |    4 +-
 superset/commands/dataset/importers/dispatcher.py  |    8 +-
 superset/commands/exceptions.py                    |    2 +-
 superset/commands/explore/permalink/create.py      |    2 +
 superset/commands/importers/v1/__init__.py         |    4 +-
 superset/commands/importers/v1/utils.py            |    2 +-
 superset/commands/key_value/create.py              |    2 +-
 superset/commands/key_value/delete.py              |    2 +-
 superset/commands/key_value/delete_expired.py      |    2 +-
 superset/commands/key_value/get.py                 |    3 +-
 superset/commands/key_value/update.py              |    2 +-
 superset/commands/key_value/upsert.py              |    2 +-
 superset/commands/query/importers/dispatcher.py    |    4 +-
 superset/commands/report/base.py                   |    2 +
 superset/commands/report/execute.py                |    8 +-
 superset/commands/security/create.py               |    2 +-
 superset/commands/security/update.py               |    2 +-
 superset/commands/sql_lab/execute.py               |    8 +-
 superset/common/query_object.py                    |    2 +-
 superset/config.py                                 |   12 +-
 superset/connectors/sqla/models.py                 |    6 +-
 superset/constants.py                              |    1 +
 superset/daos/dashboard.py                         |    5 +
 superset/dashboards/api.py                         |   62 ++
 superset/dashboards/schemas.py                     |   12 +
 superset/databases/utils.py                        |    4 +-
 superset/db_engine_specs/base.py                   |    9 +-
 superset/db_engine_specs/hive.py                   |    6 +
 superset/db_engine_specs/impala.py                 |    2 +-
 superset/db_engine_specs/ocient.py                 |    4 +-
 superset/db_engine_specs/snowflake.py              |    2 +-
 superset/db_engine_specs/trino.py                  |   91 +-
 superset/extensions/metastore_cache.py             |    9 +-
 superset/initialization/__init__.py                |    2 +-
 superset/key_value/models.py                       |    5 +
 superset/key_value/shared_entries.py               |    2 +
 ...1c4c6_migrate_num_period_compare_and_period_.py |    4 +-
 ...3a3a8ff221_migrate_filter_sets_to_new_format.py |    8 +-
 ...ed7ec95_migrate_native_filters_to_new_schema.py |    8 +-
 ...15da_migrate_pivot_table_v2_heatmaps_to_new_.py |    4 +-
 ...5b9441_rename_big_viz_total_form_data_fields.py |    8 +-
 ...d1d2_move_pivot_table_v2_legacy_order_by_to_.py |    8 +-
 ...-41_4ea966691069_cross_filter_global_scoping.py |    8 +-
 ...d83218_migrate_can_view_and_drill_permission.py |    2 +-
 ...8_15-43_5ad7321c2169_mig_new_csv_upload_perm.py |    2 +-
 ...14-04_d60591c5515f_mig_new_excel_upload_perm.py |    2 +-
 ...36_4a33124c18ad_mig_new_columnar_upload_perm.py |    2 +-
 ...9123a_update_charts_with_old_time_comparison.py |    4 +-
 superset/models/core.py                            |   10 +-
 superset/models/dashboard.py                       |   54 +-
 superset/models/helpers.py                         |   12 +-
 superset/models/slice.py                           |    4 +-
 superset/models/sql_lab.py                         |    4 +-
 superset/sql_lab.py                                |    4 +-
 superset/sql_validators/presto_db.py               |    2 +-
 superset/sqllab/query_render.py                    |    1 -
 superset/sqllab/sql_json_executer.py               |    4 +-
 superset/sqllab/sqllab_execution_context.py        |    4 +-
 superset/tasks/async_queries.py                    |   18 +-
 superset/translations/fr/LC_MESSAGES/messages.po   |    2 +-
 superset/utils/csv.py                              |    2 +-
 superset/utils/decorators.py                       |    4 +-
 superset/utils/json.py                             |    6 +-
 superset/utils/lock.py                             |   13 +-
 superset/utils/webdriver.py                        |   24 +-
 superset/views/base_api.py                         |    2 +-
 tests/integration_tests/conftest.py                |   81 +-
 tests/integration_tests/core_tests.py              |    4 +-
 tests/integration_tests/dashboards/api_tests.py    |  179 ++++
 .../dashboards/superset_factory_util.py            |   32 +-
 tests/integration_tests/datasource_tests.py        |    8 +-
 .../db_engine_specs/trino_tests.py                 |  115 +++
 .../extensions/metastore_cache_test.py             |   17 +-
 .../key_value/commands/delete_test.py              |    5 +-
 .../key_value/commands/fixtures.py                 |    2 +-
 .../key_value/commands/get_test.py                 |    4 +-
 tests/integration_tests/query_context_tests.py     |   31 +
 tests/integration_tests/sqla_models_tests.py       |    2 +-
 tests/integration_tests/sqllab_tests.py            |    4 +-
 tests/integration_tests/superset_test_config.py    |    9 +-
 .../superset_test_config_thumbnails.py             |    7 +-
 tests/unit_tests/commands/report/base_test.py      |   46 +-
 tests/unit_tests/sql_lab_execution_context.py      |   96 ++
 .../utils/csv_tests.py                             |   44 +-
 tests/unit_tests/utils/lock_tests.py               |  100 +-
 342 files changed, 7729 insertions(+), 2682 deletions(-)

diff --git a/.asf.yaml b/.asf.yaml
index 14f02fb18e..ea63dc5e61 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -63,9 +63,12 @@ github:
         # combination here.
         contexts:
           - lint-check
+          - cypress-matrix (0, chrome)
           - cypress-matrix (1, chrome)
           - cypress-matrix (2, chrome)
           - cypress-matrix (3, chrome)
+          - cypress-matrix (4, chrome)
+          - cypress-matrix (5, chrome)
           - frontend-build
           - pre-commit
           - python-lint
diff --git a/.github/workflows/check_db_migration_confict.yml b/.github/workflows/check_db_migration_confict.yml
index 079ba954ed..e717f41193 100644
--- a/.github/workflows/check_db_migration_confict.yml
+++ b/.github/workflows/check_db_migration_confict.yml
@@ -5,7 +5,7 @@ on:
       - "superset/migrations/**"
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     paths:
       - "superset/migrations/**"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 15395bbfde..d9d5abdbdb 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,7 +2,7 @@ name: "CodeQL"
 
 on:
   push:
-    branches: ["master", "[0-9].[0-9]"]
+    branches: ["master", "[0-9].[0-9]*"]
   pull_request:
     # The branches below must be a subset of the branches above
     branches: ["master"]
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 849fb7b132..f38cd4fee4 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     branches:
       - "master"
diff --git a/.github/workflows/embedded-sdk-release.yml b/.github/workflows/embedded-sdk-release.yml
index ff51d8f97b..323b1a9e99 100644
--- a/.github/workflows/embedded-sdk-release.yml
+++ b/.github/workflows/embedded-sdk-release.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
 
 jobs:
   config:
diff --git a/.github/workflows/generate-FOSSA-report.yml b/.github/workflows/generate-FOSSA-report.yml
index fb30d7dcc5..352ba845d9 100644
--- a/.github/workflows/generate-FOSSA-report.yml
+++ b/.github/workflows/generate-FOSSA-report.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
 
 jobs:
   config:
diff --git a/.github/workflows/github-action-validator.yml b/.github/workflows/github-action-validator.yml
index e6d07b56e9..0dd5015531 100644
--- a/.github/workflows/github-action-validator.yml
+++ b/.github/workflows/github-action-validator.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 541b4a8bfc..6ccb66df77 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/prefer-typescript.yml b/.github/workflows/prefer-typescript.yml
index f3179d3bcc..0b34f25bae 100644
--- a/.github/workflows/prefer-typescript.yml
+++ b/.github/workflows/prefer-typescript.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
     paths:
       - "superset-frontend/src/**"
   pull_request:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ad1737c477..43862bd50b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
 
 jobs:
   config:
diff --git a/.github/workflows/superset-cli.yml b/.github/workflows/superset-cli.yml
index 3c98198c7e..060bae5dff 100644
--- a/.github/workflows/superset-cli.yml
+++ b/.github/workflows/superset-cli.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml
index f59843c010..f14e301e2a 100644
--- a/.github/workflows/superset-e2e.yml
+++ b/.github/workflows/superset-e2e.yml
@@ -4,13 +4,13 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
   workflow_dispatch:
     inputs:
       use_dashboard:
-        description: 'Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]'
+        description: 'Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work.'
         required: false
         default: 'false'
       ref:
@@ -130,12 +130,12 @@ jobs:
           CYPRESS_BROWSER: ${{ matrix.browser }}
           PARALLEL_ID: ${{ matrix.parallel_id }}
           PARALLELISM: 6
-          CYPRESS_KEY: YjljODE2MzAtODcwOC00NTA3LWE4NmMtMTU3YmFmMjIzOTRhCg==
+          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
         with:
           run: cypress-run-all ${{ env.USE_DASHBOARD }}
       - name: Upload Artifacts
         uses: actions/upload-artifact@v4
         if: github.event_name == 'workflow_dispatch' && (steps.check.outputs.python || steps.check.outputs.frontend)
         with:
-          name: screenshots
           path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
+          name: cypress-artifact-${{ github.run_id }}-${{ github.job }}
diff --git a/.github/workflows/superset-frontend.yml b/.github/workflows/superset-frontend.yml
index f79d70754f..6438c2cc1d 100644
--- a/.github/workflows/superset-frontend.yml
+++ b/.github/workflows/superset-frontend.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-helm-release.yml b/.github/workflows/superset-helm-release.yml
index 429c6fc255..062e237586 100644
--- a/.github/workflows/superset-helm-release.yml
+++ b/.github/workflows/superset-helm-release.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
     paths:
       - "helm/**"
 
diff --git a/.github/workflows/superset-python-integrationtest.yml b/.github/workflows/superset-python-integrationtest.yml
index fec35ef9db..3f43bef88c 100644
--- a/.github/workflows/superset-python-integrationtest.yml
+++ b/.github/workflows/superset-python-integrationtest.yml
@@ -5,7 +5,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
@@ -24,7 +24,7 @@ jobs:
         mysql+mysqldb://superset:superset@127.0.0.1:13306/superset?charset=utf8mb4&binary_prefix=true
     services:
       mysql:
-        image: mysql:5.7
+        image: mysql:8.0
         env:
           MYSQL_ROOT_PASSWORD: root
         ports:
diff --git a/.github/workflows/superset-python-misc.yml b/.github/workflows/superset-python-misc.yml
index c6b1bebd89..d58226216f 100644
--- a/.github/workflows/superset-python-misc.yml
+++ b/.github/workflows/superset-python-misc.yml
@@ -5,7 +5,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-python-presto-hive.yml b/.github/workflows/superset-python-presto-hive.yml
index 87ff06368d..30dbaf7d40 100644
--- a/.github/workflows/superset-python-presto-hive.yml
+++ b/.github/workflows/superset-python-presto-hive.yml
@@ -5,7 +5,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-python-unittest.yml b/.github/workflows/superset-python-unittest.yml
index d26f0435ed..454ee0c61e 100644
--- a/.github/workflows/superset-python-unittest.yml
+++ b/.github/workflows/superset-python-unittest.yml
@@ -5,7 +5,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-translations.yml b/.github/workflows/superset-translations.yml
index 846c661d32..11dbebb098 100644
--- a/.github/workflows/superset-translations.yml
+++ b/.github/workflows/superset-translations.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
   pull_request:
     types: [synchronize, opened, reopened, ready_for_review]
 
diff --git a/.github/workflows/superset-websocket.yml b/.github/workflows/superset-websocket.yml
index c35573ada1..2d55ceafa0 100644
--- a/.github/workflows/superset-websocket.yml
+++ b/.github/workflows/superset-websocket.yml
@@ -3,7 +3,7 @@ on:
   push:
     branches:
       - "master"
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
     paths:
       - "superset-websocket/**"
   pull_request:
diff --git a/.github/workflows/tech-debt.yml b/.github/workflows/tech-debt.yml
index f912ab2068..6f73a3a51b 100644
--- a/.github/workflows/tech-debt.yml
+++ b/.github/workflows/tech-debt.yml
@@ -4,7 +4,7 @@ on:
   push:
     branches:
       - master
-      - "[0-9].[0-9]"
+      - "[0-9].[0-9]*"
 
 jobs:
   config:
diff --git a/.gitignore b/.gitignore
index 87aac85234..6daa883720 100644
--- a/.gitignore
+++ b/.gitignore
@@ -116,3 +116,4 @@ docker/requirements-local.txt
 
 cache/
 docker/*local*
+.temp_cache
diff --git a/.rat-excludes b/.rat-excludes
index 70529a4bd8..7cfb991cc9 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -71,3 +71,4 @@ snowflake.svg
 # docs-related
 erd.puml
 erd.svg
+intro_header.txt
diff --git a/README.md b/README.md
index aab794ec55..408cad422b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,3 @@
----
-hide_title: true
-sidebar_position: 1
----
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md
index 950e652efa..2c4d4d0c3e 100644
--- a/RESOURCES/FEATURE_FLAGS.md
+++ b/RESOURCES/FEATURE_FLAGS.md
@@ -89,6 +89,7 @@ These features flags currently default to True and **will be removed in a future
 
 [//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
 
+- AVOID_COLORS_COLLISION
 - DASHBOARD_CROSS_FILTERS
 - ENABLE_JAVASCRIPT_CONTROLS
 - KV_STORE
diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md
index 5c12e4a3fe..e39c82ae9c 100644
--- a/RESOURCES/INTHEWILD.md
+++ b/RESOURCES/INTHEWILD.md
@@ -154,6 +154,7 @@ Join our growing community!
 ### Healthcare
 - [Amino](https://amino.com) [@shkr]
 - [Beans](https://www.beans.fi) [@kakoni]
+- [Bluesquare](https://www.bluesquarehub.com/) [@madewulf]
 - [Care](https://www.getcare.io/)[@alandao2021]
 - [Living Goods](https://www.livinggoods.org) [@chelule]
 - [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
@@ -168,8 +169,10 @@ Join our growing community!
 
 ### Government
 - [City of Ann Arbor, MI](https://www.a2gov.org/) [@sfirke]
+- [RIS3 Strategy of CZ, MIT CR](https://www.ris3.cz/) [@RIS3CZ]
 
 ### Travel
+- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
 - [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
 
 ### Others
diff --git a/UPDATING.md b/UPDATING.md
index 2327fcfa3f..1d2ffbf0ec 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,10 @@ assists people when migrating to a new version.
 
 ## Next
 
+- [29274](https://github.com/apache/superset/pull/29274): We made it easier to trigger CI on your
+  forks, whether they are public or private. Simply push to a branch that fits `[0-9].[0-9]*` and
+  should run on your fork, giving you flexibility on naming your release branches and triggering
+  CI
 - [27505](https://github.com/apache/superset/pull/27505): We simplified the files under
   `requirements/` folder. If you use these files for your builds you may want to double
   check that your builds are not affected. `base.txt` should be the same as before, though
diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py
index 69477d56e7..c78a504141 100644
--- a/docker/pythonpath_dev/superset_config.py
+++ b/docker/pythonpath_dev/superset_config.py
@@ -97,7 +97,6 @@ ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
 WEBDRIVER_BASEURL = "http://superset:8088/"  # When using docker compose baseurl should be http://superset_app:8088/
 # The base URL for the email report hyperlinks.
 WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL
-
 SQLLAB_CTAS_NO_LIMIT = True
 
 #
diff --git a/docs/docs/configuration/alerts-reports.mdx b/docs/docs/configuration/alerts-reports.mdx
index d489b6ebd7..ed4e739aea 100644
--- a/docs/docs/configuration/alerts-reports.mdx
+++ b/docs/docs/configuration/alerts-reports.mdx
@@ -204,6 +204,17 @@ ALERT_MINIMUM_INTERVAL = int(timedelta(minutes=10).total_seconds())
 REPORT_MINIMUM_INTERVAL = int(timedelta(minutes=5).total_seconds())
 ```
 
+Alternatively, you can assign a function to `ALERT_MINIMUM_INTERVAL` and/or `REPORT_MINIMUM_INTERVAL`. This is useful to dynamically retrieve a value as needed:
+
+``` python
+def alert_dynamic_minimal_interval(**kwargs) -> int:
+    """
+    Define logic here to retrieve the value dynamically
+    """
+
+ALERT_MINIMUM_INTERVAL = alert_dynamic_minimal_interval
+```
+
 ## Custom Dockerfile
 
 If you're running the dev version of a released Superset image, like `apache/superset:3.1.0-dev`, you should be set with the above.
diff --git a/docs/docs/security/cves.mdx b/docs/docs/security/cves.mdx
index 4296142a06..ba4bd2ea73 100644
--- a/docs/docs/security/cves.mdx
+++ b/docs/docs/security/cves.mdx
@@ -2,6 +2,12 @@
 title: CVEs fixed by release
 sidebar_position: 2
 ---
+#### Version 3.1.3, 4.0.1
+
+| CVE            | Title                       |                    Affected |
+|:---------------|:----------------------------|----------------------------:|
+| CVE-2024-34693 | Server arbitrary file read  |  < 3.1.3, >= 4.0.0, < 4.0.1 |
+
 #### Version 3.1.2
 
 | CVE            | Title                                                   | Affected |
diff --git a/docs/package.json b/docs/package.json
index 5933f8cfa2..102cd80a77 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,13 +5,13 @@
   "license": "Apache-2.0",
   "scripts": {
     "docusaurus": "docusaurus",
-    "_init": "cp ../README.md docs/intro.md",
+    "_init": "cat src/intro_header.txt ../README.md > docs/intro.md",
     "start": "npm run _init && docusaurus start",
     "build": "npm run _init && docusaurus build",
     "swizzle": "docusaurus swizzle",
     "deploy": "docusaurus deploy",
     "clear": "docusaurus clear",
-    "serve": "docusaurus serve",
+    "serve": "npm run _init && docusaurus serve",
     "write-translations": "docusaurus write-translations",
     "write-heading-ids": "docusaurus write-heading-ids",
     "typecheck": "tsc"
diff --git a/docs/src/intro_header.txt b/docs/src/intro_header.txt
new file mode 100644
index 0000000000..483a5e7dba
--- /dev/null
+++ b/docs/src/intro_header.txt
@@ -0,0 +1,4 @@
+---
+hide_title: true
+sidebar_position: 1
+---
diff --git a/docs/yarn.lock b/docs/yarn.lock
index b4ce0f7064..c522e53040 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -10350,14 +10350,14 @@ write-file-atomic@^3.0.3:
     typedarray-to-buffer "^3.1.5"
 
 ws@^7.3.1:
-  version "7.5.9"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
-  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+  version "7.5.10"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+  integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
 
 ws@^8.13.0:
-  version "8.17.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
-  integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==
+  version "8.17.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+  integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
 
 xdg-basedir@^5.0.1, xdg-basedir@^5.1.0:
   version "5.1.0"
diff --git a/pyproject.toml b/pyproject.toml
index 2e20fae77b..65471b0cdc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -132,6 +132,7 @@ gevent = ["gevent>=23.9.1"]
 gsheets = ["shillelagh[gsheetsapi]>=1.2.18, <2"]
 hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
 hive = [
+    "boto3",
     "pyhive[hive]>=0.6.5;python_version<'3.11'",
     "pyhive[hive_pure_sasl]>=0.7.0",
     "tableschema",
@@ -154,7 +155,7 @@ pinot = ["pinotdb>=0.3.3, <0.4"]
 playwright = ["playwright>=1.37.0, <2"]
 postgres = ["psycopg2-binary==2.9.6"]
 presto = ["pyhive[presto]>=0.6.5"]
-trino = ["trino>=0.328.0"]
+trino = ["boto3", "trino>=0.328.0"]
 prophet = ["prophet>=1.1.5, <2"]
 redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
 rockset = ["rockset-sqlalchemy>=0.0.1, <1"]
@@ -435,7 +436,14 @@ target-version = "py310"
 # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default.
 # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
 # McCabe complexity (`C901`) by default.
-select = ["E4", "E7", "E9", "F"]
+select = [
+    "B904",
+    "E4",
+    "E7",
+    "E9",
+    "F",
+    "TRY201",
+]
 ignore = []
 
 extend-select = ["I"]
diff --git a/scripts/cypress_run.py b/scripts/cypress_run.py
index a04fb861fd..1154a3661d 100644
--- a/scripts/cypress_run.py
+++ b/scripts/cypress_run.py
@@ -24,6 +24,7 @@ from datetime import datetime
 XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x24' "
 REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
 GITHUB_EVENT_NAME = os.getenv("GITHUB_REPOSITORY") or "push"
+CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
 
 
 def compute_hash(file_path: str) -> str:
@@ -55,13 +56,7 @@ def get_cypress_cmd(
 
     if use_dashboard:
         # Run using cypress.io service
-        cypress_key = os.getenv("CYPRESS_KEY")
-        command = f"echo {cypress_key} | base64 --decode"
-        cypress_record_key = (
-            subprocess.check_output(command, shell=True).decode("utf-8").strip()
-        )
-        os.environ["CYPRESS_RECORD_KEY"] = cypress_record_key
-        spec: str = "*/**/*"
+        spec: str = "cypress/e2e/*/**/*"
         cmd = (
             f"{XVFB_PRE_CMD} "
             f'{cypress_cmd} --spec "{spec}" --browser {browser} '
@@ -70,7 +65,7 @@ def get_cypress_cmd(
         )
     else:
         # Run local, but split the execution
-        os.environ.pop("CYPRESS_KEY", None)
+        os.environ.pop("CYPRESS_RECORD_KEY", None)
         spec_list_str = ",".join(sorted(spec_list))
         if _filter:
             spec_list_str = ",".join(sorted([s for s in spec_list if _filter in s]))
diff --git a/scripts/tests/run.sh b/scripts/tests/run.sh
index d77adc08e0..bf8431caeb 100755
--- a/scripts/tests/run.sh
+++ b/scripts/tests/run.sh
@@ -61,7 +61,10 @@ function test_init() {
 DB_NAME="test"
 DB_USER="superset"
 DB_PASSWORD="superset"
+
+# Pointing to use the test database in local docker-compose setup
 export SUPERSET__SQLALCHEMY_DATABASE_URI=${SUPERSET__SQLALCHEMY_DATABASE_URI:-postgresql+psycopg2://"${DB_USER}":"${DB_PASSWORD}"@localhost/"${DB_NAME}"}
+
 export SUPERSET_CONFIG=${SUPERSET_CONFIG:-tests.integration_tests.superset_test_config}
 RUN_INIT=1
 RUN_RESET_DB=1
diff --git a/superset-embedded-sdk/package-lock.json b/superset-embedded-sdk/package-lock.json
index 999d3f1363..70106bdb1c 100644
--- a/superset-embedded-sdk/package-lock.json
+++ b/superset-embedded-sdk/package-lock.json
@@ -3304,12 +3304,12 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -4042,9 +4042,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -8169,9 +8169,9 @@
       }
     },
     "node_modules/ws": {
-      "version": "7.5.7",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
-      "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
+      "version": "7.5.10",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
       "dev": true,
       "engines": {
         "node": ">=8.3.0"
@@ -10699,12 +10699,12 @@
       }
     },
     "braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "requires": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       }
     },
     "browser-process-hrtime": {
@@ -11267,9 +11267,9 @@
       }
     },
     "fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "requires": {
         "to-regex-range": "^5.0.1"
@@ -14349,9 +14349,9 @@
       }
     },
     "ws": {
-      "version": "7.5.7",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
-      "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
+      "version": "7.5.10",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
       "dev": true,
       "requires": {}
     },
diff --git a/superset-frontend/cypress-base/cypress.config.ts b/superset-frontend/cypress-base/cypress.config.ts
index d2be99af3c..4d31f4d098 100644
--- a/superset-frontend/cypress-base/cypress.config.ts
+++ b/superset-frontend/cypress-base/cypress.config.ts
@@ -31,7 +31,7 @@ export default eyesPlugin(
     videoUploadOnPasses: false,
     viewportWidth: 1280,
     viewportHeight: 1024,
-    projectId: 'ukwxzo',
+    projectId: 'ud5x2f',
     retries: {
       runMode: 2,
       openMode: 0,
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
index 1584cd04e3..4f9863071c 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
@@ -124,7 +124,7 @@ function selectColorScheme(color: string) {
   )
     .first()
     .click();
-  cy.getBySel(color).click();
+  cy.getBySel(color).click({ force: true });
 }
 
 function applyChanges() {
@@ -169,6 +169,7 @@ function writeMetadata(metadata: string) {
 
 function openExplore(chartName: string) {
   interceptExploreJson();
+  interceptGet();
 
   cy.get(
     `[data-test-chart-name='${chartName}'] [aria-label='More Options']`,
@@ -210,7 +211,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(69, 78, 124)');
+        .should('have.css', 'fill', 'rgb(31, 168, 201)');
     });
 
     it('should apply same color to same labels with color scheme set', () => {
@@ -231,7 +232,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(0, 234, 162)');
+        .should('have.css', 'fill', 'rgb(50, 0, 167)');
 
       // open 2nd main tab
       openTab(0, 1);
@@ -240,7 +241,7 @@ describe('Dashboard edit', () => {
       // label Anthony
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .eq(2)
-        .should('have.css', 'fill', 'rgb(0, 234, 162)');
+        .should('have.css', 'fill', 'rgb(50, 0, 167)');
     });
 
     it('should apply same color to same labels with no color scheme set', () => {
@@ -261,7 +262,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(69, 78, 124)');
+        .should('have.css', 'fill', 'rgb(31, 168, 201)');
 
       // open 2nd main tab
       openTab(0, 1);
@@ -270,7 +271,7 @@ describe('Dashboard edit', () => {
       // label Anthony
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .eq(2)
-        .should('have.css', 'fill', 'rgb(69, 78, 124)');
+        .should('have.css', 'fill', 'rgb(31, 168, 201)');
     });
 
     it('custom label colors should take the precedence in nested tabs', () => {
@@ -384,17 +385,17 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(69, 78, 124)');
+        .should('have.css', 'fill', 'rgb(31, 168, 201)');
       cy.get(
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .eq(1)
-        .should('have.css', 'fill', 'rgb(224, 67, 85)');
+        .should('have.css', 'fill', 'rgb(69, 78, 124)');
       cy.get(
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .eq(2)
-        .should('have.css', 'fill', 'rgb(163, 143, 121)');
+        .should('have.css', 'fill', 'rgb(90, 193, 137)');
 
       openProperties();
       cy.get('[aria-label="Select color scheme"]').should('have.value', '');
@@ -423,17 +424,17 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(69, 78, 124)');
+        .should('have.css', 'fill', 'rgb(31, 168, 201)');
       cy.get(
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .eq(1)
-        .should('have.css', 'fill', 'rgb(224, 67, 85)');
+        .should('have.css', 'fill', 'rgb(69, 78, 124)');
       cy.get(
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .eq(2)
-        .should('have.css', 'fill', 'rgb(163, 143, 121)');
+        .should('have.css', 'fill', 'rgb(90, 193, 137)');
     });
 
     it('should show the same colors in Explore', () => {
@@ -459,12 +460,6 @@ describe('Dashboard edit', () => {
       )
         .first()
         .should('have.css', 'fill', 'rgb(255, 0, 0)');
-      // label Christopher
-      cy.get(
-        '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
-      )
-        .eq(1)
-        .should('have.css', 'fill', 'rgb(172, 32, 119)');
 
       openExplore('Top 10 California Names Timeseries');
 
@@ -472,10 +467,6 @@ describe('Dashboard edit', () => {
       cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
         .first()
         .should('have.css', 'fill', 'rgb(255, 0, 0)');
-      // label Christopher
-      cy.get('[data-test="chart-container"] .line .nv-legend-symbol')
-        .eq(1)
-        .should('have.css', 'fill', 'rgb(172, 32, 119)');
     });
 
     it.skip('should change color scheme multiple times', () => {
@@ -496,7 +487,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(51, 61, 71)');
+        .should('have.css', 'fill', 'rgb(234, 11, 140)');
 
       // open 2nd main tab
       openTab(0, 1);
@@ -505,7 +496,7 @@ describe('Dashboard edit', () => {
       // label Anthony
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .eq(2)
-        .should('have.css', 'fill', 'rgb(51, 61, 71)');
+        .should('have.css', 'fill', 'rgb(234, 11, 140)');
 
       editDashboard();
       openProperties();
@@ -516,7 +507,7 @@ describe('Dashboard edit', () => {
       // label Anthony
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .eq(2)
-        .should('have.css', 'fill', 'rgb(244, 176, 42)');
+        .should('have.css', 'fill', 'rgb(41, 105, 107)');
 
       // open main tab and nested tab
       openTab(0, 0);
@@ -527,7 +518,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(244, 176, 42)');
+        .should('have.css', 'fill', 'rgb(41, 105, 107)');
     });
 
     it.skip('should apply the color scheme across main tabs', () => {
@@ -542,7 +533,7 @@ describe('Dashboard edit', () => {
 
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .first()
-        .should('have.css', 'fill', 'rgb(51, 61, 71)');
+        .should('have.css', 'fill', 'rgb(234, 11, 140)');
     });
 
     it.skip('should apply the color scheme across main tabs for rendered charts', () => {
@@ -558,7 +549,7 @@ describe('Dashboard edit', () => {
 
       cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
         .first()
-        .should('have.css', 'fill', 'rgb(156, 52, 152)');
+        .should('have.css', 'fill', 'rgb(41, 105, 107)');
 
       // change scheme now that charts are rendered across the main tabs
       editDashboard();
@@ -588,7 +579,7 @@ describe('Dashboard edit', () => {
         '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
       )
         .first()
-        .should('have.css', 'fill', 'rgb(51, 61, 71)');
+        .should('have.css', 'fill', 'rgb(234, 11, 140)');
 
       // open another nested tab
       openTab(2, 1);
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
index 136e48d5ad..11ad6eb578 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
@@ -97,8 +97,5 @@ describe('Visualization > Compare', () => {
     cy.get(
       '.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="supersetColors"]',
     ).should('exist');
-    cy.get('.compare .nv-legend .nv-legend-symbol')
-      .first()
-      .should('have.css', 'fill', 'rgb(31, 168, 201)');
   });
 });
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
index 591ba31776..8dcba5b755 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
@@ -87,8 +87,5 @@ describe('Visualization > Distribution bar chart', () => {
     cy.get(
       '.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="bnbColors"]',
     ).should('exist');
-    cy.get('.dist_bar .nv-legend .nv-legend-symbol')
-      .first()
-      .should('have.css', 'fill', 'rgb(41, 105, 107)');
   });
 });
diff --git a/superset-frontend/cypress-base/package-lock.json b/superset-frontend/cypress-base/package-lock.json
index 9d87470528..42416314f7 100644
--- a/superset-frontend/cypress-base/package-lock.json
+++ b/superset-frontend/cypress-base/package-lock.json
@@ -4350,11 +4350,11 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -5912,9 +5912,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dependencies": {
         "to-regex-range": "^5.0.1"
       },
@@ -13819,11 +13819,11 @@
       }
     },
     "braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "requires": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       }
     },
     "browserslist": {
@@ -14988,9 +14988,9 @@
       }
     },
     "fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "requires": {
         "to-regex-range": "^5.0.1"
       }
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 407ac2ad0e..8221d2ffa2 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -5903,12 +5903,12 @@
       }
     },
     "node_modules/@floating-ui/dom": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
-      "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz",
+      "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==",
       "dependencies": {
-        "@floating-ui/core": "^1.6.0",
-        "@floating-ui/utils": "^0.2.1"
+        "@floating-ui/core": "^1.0.0",
+        "@floating-ui/utils": "^0.2.0"
       }
     },
     "node_modules/@floating-ui/react-dom": {
@@ -28004,6 +28004,11 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/cephes": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/cephes/-/cephes-2.0.0.tgz",
+      "integrity": "sha512-4GMUzkcXHZ0HMZ3gZdBrv8pQs1/zkJh2Q9rQOF8NJZHanM359y3XOSdeqmDBPfxQKYQpJt58R3dUpofrIXJ2mg=="
+    },
     "node_modules/chainsaw": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
@@ -32055,9 +32060,9 @@
       }
     },
     "node_modules/dayjs": {
-      "version": "1.10.7",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
-      "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+      "version": "1.11.10",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
     },
     "node_modules/deasync": {
       "version": "0.1.29",
@@ -32988,11 +32993,11 @@
       "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo="
     },
     "node_modules/distributions": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/distributions/-/distributions-1.1.0.tgz",
-      "integrity": "sha512-mufW9T1kRlzLVAaekUhgdfcMgX2r/zYQmJx3sGdUAwe0/JSQWey0XgqiDtfUUqYcr/QWHCnBd2M/v45tS/+YAQ==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/distributions/-/distributions-2.2.0.tgz",
+      "integrity": "sha512-n7ybud+CRAOZlpg+ETuA0PTiSBfyVNt8Okns5gSK4NvHwj7RamQoufptOucvVcTn9CV4DZ38p1k6TgwMexUNkQ==",
       "dependencies": {
-        "mathfn": "^1.0.0"
+        "cephes": "^2.0.0"
       }
     },
     "node_modules/dnd-core": {
@@ -50761,12 +50766,6 @@
         "gl-matrix": "^3.4.0"
       }
     },
-    "node_modules/mathfn": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mathfn/-/mathfn-1.2.0.tgz",
-      "integrity": "sha512-QBcepxkFxuGk12q4G0KuNbuU3UCXhDROxWZllaNZSpBivkHl2z8qNvi7UGE/WLJt+c7GTC4jigYtur+JDL+40A==",
-      "deprecated": "Use cephes instead, for a more complete and well-tested module"
-    },
     "node_modules/mdast-util-definitions": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz",
@@ -63381,6 +63380,12 @@
       "integrity": "sha512-cxHzpa5JgsugY9NUVRH43gPaGJw/29LecAn4X7UGOP64+kB8pU4VQ3bIhSyfb5Mk4jDxwl3yk330L/EIhbJ5aw==",
       "deprecated": "This module is now under the @mapbox namespace: install @mapbox/tilebelt instead"
     },
+    "node_modules/timezone-mock": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.3.6.tgz",
+      "integrity": "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==",
+      "dev": true
+    },
     "node_modules/tiny-invariant": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
@@ -67617,7 +67622,8 @@
         "@emotion/styled": "^11.3.0",
         "fetch-mock": "^6.5.2",
         "jest-mock-console": "^1.0.0",
-        "resize-observer-polyfill": "1.5.1"
+        "resize-observer-polyfill": "1.5.1",
+        "timezone-mock": "1.3.6"
       },
       "peerDependencies": {
         "@emotion/cache": "^11.4.0",
@@ -68830,7 +68836,7 @@
       "version": "0.18.25",
       "license": "Apache-2.0",
       "dependencies": {
-        "distributions": "^1.0.0",
+        "distributions": "^2.2.0",
         "prop-types": "^15.8.1",
         "reactable": "^1.1.0"
       },
@@ -68987,7 +68993,7 @@
         "d3-array": "^1.2.4",
         "d3-color": "^1.4.1",
         "d3-scale": "^3.0.0",
-        "deck.gl": "9.0.6",
+        "deck.gl": "9.0.12",
         "lodash": "^4.17.21",
         "moment": "^2.30.1",
         "mousetrap": "^1.6.5",
@@ -71011,6 +71017,7 @@
         "xss": "^1.0.15"
       },
       "peerDependencies": {
+        "@ant-design/icons": "^5.0.1",
         "@superset-ui/chart-controls": "*",
         "@superset-ui/core": "*",
         "@testing-library/dom": "^7.29.4",
@@ -75273,12 +75280,12 @@
       }
     },
     "@floating-ui/dom": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
-      "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz",
+      "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==",
       "requires": {
-        "@floating-ui/core": "^1.6.0",
-        "@floating-ui/utils": "^0.2.1"
+        "@floating-ui/core": "^1.0.0",
+        "@floating-ui/utils": "^0.2.0"
       }
     },
     "@floating-ui/react-dom": {
@@ -86312,6 +86319,7 @@
         "resize-observer-polyfill": "1.5.1",
         "rison": "^0.1.1",
         "seedrandom": "^3.0.5",
+        "timezone-mock": "1.3.6",
         "whatwg-fetch": "^3.6.20",
         "xss": "^1.0.14"
       },
@@ -87209,7 +87217,7 @@
     "@superset-ui/legacy-plugin-chart-paired-t-test": {
       "version": "file:plugins/legacy-plugin-chart-paired-t-test",
       "requires": {
-        "distributions": "^1.0.0",
+        "distributions": "^2.2.0",
         "prop-types": "^15.8.1",
         "reactable": "^1.1.0"
       }
@@ -87286,7 +87294,7 @@
         "d3-array": "^1.2.4",
         "d3-color": "^1.4.1",
         "d3-scale": "^3.0.0",
-        "deck.gl": "9.0.6",
+        "deck.gl": "9.0.12",
         "lodash": "^4.17.21",
         "moment": "^2.30.1",
         "mousetrap": "^1.6.5",
@@ -94055,6 +94063,11 @@
       "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
       "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="
     },
+    "cephes": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/cephes/-/cephes-2.0.0.tgz",
+      "integrity": "sha512-4GMUzkcXHZ0HMZ3gZdBrv8pQs1/zkJh2Q9rQOF8NJZHanM359y3XOSdeqmDBPfxQKYQpJt58R3dUpofrIXJ2mg=="
+    },
     "chainsaw": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
@@ -97238,9 +97251,9 @@
       "dev": true
     },
     "dayjs": {
-      "version": "1.10.7",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
-      "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+      "version": "1.11.10",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
     },
     "deasync": {
       "version": "0.1.29",
@@ -97936,11 +97949,11 @@
       "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo="
     },
     "distributions": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/distributions/-/distributions-1.1.0.tgz",
-      "integrity": "sha512-mufW9T1kRlzLVAaekUhgdfcMgX2r/zYQmJx3sGdUAwe0/JSQWey0XgqiDtfUUqYcr/QWHCnBd2M/v45tS/+YAQ==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/distributions/-/distributions-2.2.0.tgz",
+      "integrity": "sha512-n7ybud+CRAOZlpg+ETuA0PTiSBfyVNt8Okns5gSK4NvHwj7RamQoufptOucvVcTn9CV4DZ38p1k6TgwMexUNkQ==",
       "requires": {
-        "mathfn": "^1.0.0"
+        "cephes": "^2.0.0"
       }
     },
     "dnd-core": {
@@ -111606,11 +111619,6 @@
         }
       }
     },
-    "mathfn": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mathfn/-/mathfn-1.2.0.tgz",
-      "integrity": "sha512-QBcepxkFxuGk12q4G0KuNbuU3UCXhDROxWZllaNZSpBivkHl2z8qNvi7UGE/WLJt+c7GTC4jigYtur+JDL+40A=="
-    },
     "mdast-util-definitions": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz",
@@ -121088,6 +121096,12 @@
       "resolved": "https://registry.npmjs.org/tilebelt/-/tilebelt-1.0.1.tgz",
       "integrity": "sha512-cxHzpa5JgsugY9NUVRH43gPaGJw/29LecAn4X7UGOP64+kB8pU4VQ3bIhSyfb5Mk4jDxwl3yk330L/EIhbJ5aw=="
     },
+    "timezone-mock": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.3.6.tgz",
+      "integrity": "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==",
+      "dev": true
+    },
     "tiny-invariant": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/components/Dropdown.tsx
similarity index 82%
copy from superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
copy to superset-frontend/packages/superset-ui-chart-controls/src/components/Dropdown.tsx
index d9d61a19cd..032365ebd5 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/Dropdown.tsx
@@ -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
@@ -17,14 +17,5 @@
  * under the License.
  */
 
-/**
- * Supported comparison time ranges
- */
-
-export enum ComparisonTimeRangeType {
-  Custom = 'c',
-  InheritedRange = 'r',
-  Month = 'm',
-  Week = 'w',
-  Year = 'y',
-}
+export { Dropdown } from 'antd';
+export type { DropDownProps } from 'antd/lib/dropdown';
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/components/Menu.tsx
similarity index 82%
copy from superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
copy to superset-frontend/packages/superset-ui-chart-controls/src/components/Menu.tsx
index d9d61a19cd..89a7405cde 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/Menu.tsx
@@ -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
@@ -17,14 +17,5 @@
  * under the License.
  */
 
-/**
- * Supported comparison time ranges
- */
-
-export enum ComparisonTimeRangeType {
-  Custom = 'c',
-  InheritedRange = 'r',
-  Month = 'm',
-  Week = 'w',
-  Year = 'y',
-}
+export { Menu } from 'antd';
+export type { MenuProps } from 'antd/lib/menu';
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
index 4e00929119..fed32cae3a 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts
@@ -28,8 +28,10 @@ export const sections = sectionsModule;
 export * from './components/InfoTooltipWithTrigger';
 export * from './components/ColumnOption';
 export * from './components/ColumnTypeLabel/ColumnTypeLabel';
-export * from './components/MetricOption';
 export * from './components/ControlSubSectionHeader';
+export * from './components/Dropdown';
+export * from './components/Menu';
+export * from './components/MetricOption';
 export * from './components/Tooltip';
 
 export * from './shared-controls';
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index c01b4052d1..eaf0f172b4 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -362,6 +362,14 @@ const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
     ),
 };
 
+const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
+  type: 'CheckboxControl',
+  label: t('Sort by metric'),
+  description: t(
+    'Whether to sort results by the selected metric in descending order.',
+  ),
+};
+
 export default {
   metrics: dndAdhocMetricsControl,
   metric: dndAdhocMetricControl,
@@ -400,4 +408,5 @@ export default {
   show_empty_columns,
   temporal_columns_lookup,
   currency_format,
+  sort_by_metric,
 };
diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json
index b5bc608952..08bbf7806f 100644
--- a/superset-frontend/packages/superset-ui-core/package.json
+++ b/superset-frontend/packages/superset-ui-core/package.json
@@ -67,7 +67,8 @@
     "@emotion/styled": "^11.3.0",
     "fetch-mock": "^6.5.2",
     "jest-mock-console": "^1.0.0",
-    "resize-observer-polyfill": "1.5.1"
+    "resize-observer-polyfill": "1.5.1",
+    "timezone-mock": "1.3.6"
   },
   "peerDependencies": {
     "@emotion/cache": "^11.4.0",
diff --git a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts
index 8b8cd02519..f97f84cdec 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts
@@ -17,19 +17,18 @@
  * under the License.
  */
 
-/* eslint-disable no-dupe-class-members */
 import { scaleOrdinal, ScaleOrdinal } from 'd3-scale';
 import { ExtensibleFunction } from '../models';
 import { ColorsInitLookup, ColorsLookup } from './types';
 import stringifyAndTrim from './stringifyAndTrim';
-import getSharedLabelColor from './SharedLabelColorSingleton';
+import getLabelsColorMap from './LabelsColorMapSingleton';
 import { getAnalogousColors } from './utils';
 import { FeatureFlag, isFeatureEnabled } from '../utils';
 
 // Use type augmentation to correct the fact that
 // an instance of CategoricalScale is also a function
 interface CategoricalColorScale {
-  (x: { toString(): string }, y?: number): string;
+  (x: { toString(): string }, y?: number, w?: string): string;
 }
 
 class CategoricalColorScale extends ExtensibleFunction {
@@ -39,101 +38,183 @@ class CategoricalColorScale extends ExtensibleFunction {
 
   scale: ScaleOrdinal<{ toString(): string }, string>;
 
-  parentForcedColors: ColorsLookup;
-
   forcedColors: ColorsLookup;
 
+  labelsColorMapInstance: ReturnType<typeof getLabelsColorMap>;
+
+  chartLabelsColorMap: Map<string, string>;
+
   multiple: number;
 
   /**
    * Constructor
    * @param {*} colors an array of colors
-   * @param {*} parentForcedColors optional parameter that comes from parent
-   * (usually CategoricalColorNamespace) and supersede this.forcedColors
+   * @param {*} forcedColors optional parameter that comes from parent
+   * (usually CategoricalColorNamespace)
    */
-  constructor(colors: string[], parentForcedColors: ColorsInitLookup = {}) {
-    super((value: string, sliceId?: number) => this.getColor(value, sliceId));
-
+  constructor(colors: string[], forcedColors: ColorsInitLookup = {}) {
+    super((value: string, sliceId?: number, colorScheme?: string) =>
+      this.getColor(value, sliceId, colorScheme),
+    );
+    // holds original color scheme colors
     this.originColors = colors;
+    // holds the extended color range (includes analagous colors)
     this.colors = colors;
+    // holds the values of this specific slice (label+color)
+    this.chartLabelsColorMap = new Map();
+    // shared color map instance (when context is shared, i.e. dashboard)
+    this.labelsColorMapInstance = getLabelsColorMap();
+    // holds the multiple value for analogous colors range
+    this.multiple = 0;
+
     this.scale = scaleOrdinal<{ toString(): string }, string>();
     this.scale.range(colors);
 
     // reserve fixed colors in parent map based on their index in the scale
-    Object.entries(parentForcedColors).forEach(([key, value]) => {
+    Object.entries(forcedColors).forEach(([key, value]) => {
       if (typeof value === 'number') {
         // eslint-disable-next-line no-param-reassign
-        parentForcedColors[key] = colors[value % colors.length];
+        forcedColors[key] = colors[value % colors.length];
       }
     });
 
-    // all indexes have been replaced by a fixed color
-    this.parentForcedColors = parentForcedColors as ColorsLookup;
-    this.forcedColors = {};
-    this.multiple = 0;
+    // forced colors from parent (usually CategoricalColorNamespace)
+    // currently used in dashboards to set custom label colors
+    this.forcedColors = forcedColors as ColorsLookup;
   }
 
-  removeSharedLabelColorFromRange(
-    sharedColorMap: Map<string, string>,
-    cleanedValue: string,
-  ) {
-    // make sure we don't overwrite the origin colors
-    const updatedRange = new Set(this.originColors);
-    // remove the color option from shared color
-    sharedColorMap.forEach((value: string, key: string) => {
-      if (key !== cleanedValue) {
-        updatedRange.delete(value);
-      }
-    });
-    // remove the color option from forced colors
-    Object.entries(this.parentForcedColors).forEach(([key, value]) => {
-      if (key !== cleanedValue) {
-        updatedRange.delete(value);
-      }
-    });
-    this.range(updatedRange.size > 0 ? [...updatedRange] : this.originColors);
+  /**
+   * Increment the color range with analogous colors
+   */
+  incrementColorRange() {
+    const multiple = Math.floor(
+      this.domain().length / this.originColors.length,
+    );
+    // the domain has grown larger than the original range
+    // increments the range with analogous colors
+    if (multiple > this.multiple) {
+      this.multiple = multiple;
+      const newRange = getAnalogousColors(this.originColors, multiple);
+      const extendedColors = this.originColors.concat(newRange);
+
+      this.range(extendedColors);
+      this.colors = extendedColors;
+    }
   }
 
-  getColor(value?: string, sliceId?: number) {
+  /**
+   * Get the color for a given value
+   *
+   * @param value the value of a label to get the color for
+   * @param sliceId the ID of the current chart
+   * @param colorScheme the original color scheme of the chart
+   * @returns the color or the next available color
+   */
+  getColor(value?: string, sliceId?: number, colorScheme?: string): string {
     const cleanedValue = stringifyAndTrim(value);
-    const sharedLabelColor = getSharedLabelColor();
-    const sharedColorMap = sharedLabelColor.getColorMap();
-    const sharedColor = sharedColorMap.get(cleanedValue);
-
-    // priority: parentForcedColors > forcedColors > labelColors
-    let color =
-      this.parentForcedColors?.[cleanedValue] ||
-      this.forcedColors?.[cleanedValue] ||
-      sharedColor;
-
-    if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
-      const multiple = Math.floor(
-        this.domain().length / this.originColors.length,
-      );
-      if (multiple > this.multiple) {
-        this.multiple = multiple;
-        const newRange = getAnalogousColors(this.originColors, multiple);
-        this.range(this.originColors.concat(newRange));
+    // priority: forced color (i.e. custom label colors) > shared color > scale color
+    const forcedColor = this.forcedColors?.[cleanedValue];
+    const isExistingLabel = this.chartLabelsColorMap.has(cleanedValue);
+    let color = forcedColor || this.scale(cleanedValue);
+
+    // a forced color will always be used independently of the usage count
+    if (!forcedColor && !isExistingLabel) {
+      if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
+        this.incrementColorRange();
       }
-    }
-    const newColor = this.scale(cleanedValue);
-    if (!color) {
-      color = newColor;
-      if (isFeatureEnabled(FeatureFlag.AvoidColorsCollision)) {
-        this.removeSharedLabelColorFromRange(sharedColorMap, cleanedValue);
-        color = this.scale(cleanedValue);
+      if (
+        // feature flag to be deprecated (will become standard behaviour)
+        isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
+        this.isColorUsed(color)
+      ) {
+        // fallback to least used color
+        color = this.getNextAvailableColor(color);
       }
     }
 
-    sharedLabelColor.addSlice(cleanedValue, color, sliceId);
+    // keep track of values in this slice
+    this.chartLabelsColorMap.set(cleanedValue, color);
 
+    // store the value+color in the LabelsColorMapSingleton
+    if (sliceId) {
+      this.labelsColorMapInstance.addSlice(
+        cleanedValue,
+        color,
+        sliceId,
+        colorScheme,
+      );
+    }
     return color;
   }
 
   /**
-   * Enforce specific color for given value
+   * Verify if a color is used in this slice
+   *
+   * @param color
+   * @returns true if the color is used in this slice
+   */
+  isColorUsed(color: string): boolean {
+    return this.getColorUsageCount(color) > 0;
+  }
+
+  /**
+   * Get the count of the color usage in this slice
+   *
+   * @param sliceId the ID of the current slice
+   * @param color the color to check
+   * @returns the count of the color usage in this slice
+   */
+  getColorUsageCount(currentColor: string): number {
+    let count = 0;
+    this.chartLabelsColorMap.forEach(color => {
+      if (color === currentColor) {
+        count += 1;
+      }
+    });
+    return count;
+  }
+
+  /**
+   * Lower chances of color collision by returning the least used color
+   * Checks across colors of current slice within LabelsColorMapSingleton
+   *
+   * @param currentColor the current color
+   * @returns the least used color that is not the excluded color
+   */
+  getNextAvailableColor(currentColor: string) {
+    const colorUsageArray = this.colors.map(color => ({
+      color,
+      count: this.getColorUsageCount(color),
+    }));
+    const currentColorCount = this.getColorUsageCount(currentColor);
+    const otherColors = colorUsageArray.filter(
+      colorEntry => colorEntry.color !== currentColor,
+    );
+    // all other colors are used as much or more than currentColor
+    const hasNoneAvailable = otherColors.every(
+      colorEntry => colorEntry.count >= currentColorCount,
+    );
+
+    // fallback to currentColor color
+    if (!otherColors.length || hasNoneAvailable) {
+      return currentColor;
+    }
+
+    // Finding the least used color
+    const leastUsedColor = otherColors.reduce((min, entry) =>
+      entry.count < min.count ? entry : min,
+    ).color;
+
+    return leastUsedColor;
+  }
+
+  /**
+   * Enforce specific color for a given value at the scale level
+   * Overrides any existing color and forced color for the given value
+   *
    * @param {*} value value
    * @param {*} forcedColor forcedColor
+   * @returns {CategoricalColorScale}
    */
   setColor(value: string, forcedColor: string) {
     this.forcedColors[stringifyAndTrim(value)] = forcedColor;
@@ -142,6 +223,7 @@ class CategoricalColorScale extends ExtensibleFunction {
 
   /**
    * Get a mapping of data values to colors
+   *
    * @returns an object where the key is the data value and the value is the hex color code
    */
   getColorMap() {
@@ -153,22 +235,23 @@ class CategoricalColorScale extends ExtensibleFunction {
     return {
       ...colorMap,
       ...this.forcedColors,
-      ...this.parentForcedColors,
     };
   }
 
   /**
-   * Returns an exact copy of this scale. Changes to this scale will not affect the returned scale, and vice versa.
+   * Return an exact copy of this scale.
+   * Changes to this scale will not affect the returned scale and vice versa.
+   *
+   * @returns {CategoricalColorScale} A copy of this scale.
    */
   copy() {
     const copy = new CategoricalColorScale(
       this.scale.range(),
-      this.parentForcedColors,
+      this.forcedColors,
     );
     copy.forcedColors = { ...this.forcedColors };
     copy.domain(this.domain());
     copy.unknown(this.unknown());
-
     return copy;
   }
 
diff --git a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts b/superset-frontend/packages/superset-ui-core/src/color/LabelsColorMapSingleton.ts
similarity index 52%
rename from superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts
rename to superset-frontend/packages/superset-ui-core/src/color/LabelsColorMapSingleton.ts
index 4837d0f362..59d3f8cc5d 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/LabelsColorMapSingleton.ts
@@ -17,36 +17,37 @@
  * under the License.
  */
 
-import { CategoricalColorNamespace } from '.';
 import { makeSingleton } from '../utils';
 
-export enum SharedLabelColorSource {
+export enum LabelsColorMapSource {
   Dashboard,
   Explore,
 }
-export class SharedLabelColor {
-  sliceLabelMap: Map<number, string[]>;
+
+export class LabelsColorMap {
+  chartsLabelsMap: Map<number, { labels: string[]; scheme?: string }>;
 
   colorMap: Map<string, string>;
 
-  source: SharedLabelColorSource;
+  source: LabelsColorMapSource;
 
   constructor() {
-    // { sliceId1: [label1, label2, ...], sliceId2: [label1, label2, ...] }
-    this.sliceLabelMap = new Map();
+    // holds labels and original color schemes for each chart in context
+    this.chartsLabelsMap = new Map();
     this.colorMap = new Map();
-    this.source = SharedLabelColorSource.Dashboard;
+    this.source = LabelsColorMapSource.Dashboard;
   }
 
-  updateColorMap(colorNamespace?: string, colorScheme?: string) {
-    const categoricalNamespace =
-      CategoricalColorNamespace.getNamespace(colorNamespace);
+  updateColorMap(categoricalNamespace: any, colorScheme?: string) {
     const newColorMap = new Map();
     this.colorMap.clear();
-    this.sliceLabelMap.forEach(labels => {
-      const colorScale = categoricalNamespace.getScale(colorScheme);
+    this.chartsLabelsMap.forEach((chartConfig, sliceId) => {
+      const { labels, scheme: originalChartColorScheme } = chartConfig;
+      const currentColorScheme = colorScheme || originalChartColorScheme;
+      const colorScale = categoricalNamespace.getScale(currentColorScheme);
+
       labels.forEach(label => {
-        const newColor = colorScale(label);
+        const newColor = colorScale.getColor(label, sliceId);
         newColorMap.set(label, newColor);
       });
     });
@@ -57,25 +58,37 @@ export class SharedLabelColor {
     return this.colorMap;
   }
 
-  addSlice(label: string, color: string, sliceId?: number) {
-    if (
-      this.source !== SharedLabelColorSource.Dashboard ||
-      sliceId === undefined
-    )
-      return;
-    const labels = this.sliceLabelMap.get(sliceId) || [];
+  addSlice(
+    label: string,
+    color: string,
+    sliceId: number,
+    colorScheme?: string,
+  ) {
+    if (this.source !== LabelsColorMapSource.Dashboard) return;
+
+    const chartConfig = this.chartsLabelsMap.get(sliceId) || {
+      labels: [],
+      scheme: '',
+    };
+    const { labels } = chartConfig;
     if (!labels.includes(label)) {
       labels.push(label);
-      this.sliceLabelMap.set(sliceId, labels);
+      this.chartsLabelsMap.set(sliceId, {
+        labels,
+        scheme: colorScheme,
+      });
     }
     this.colorMap.set(label, color);
   }
 
   removeSlice(sliceId: number) {
-    if (this.source !== SharedLabelColorSource.Dashboard) return;
-    this.sliceLabelMap.delete(sliceId);
+    if (this.source !== LabelsColorMapSource.Dashboard) return;
+
+    this.chartsLabelsMap.delete(sliceId);
     const newColorMap = new Map();
-    this.sliceLabelMap.forEach(labels => {
+
+    this.chartsLabelsMap.forEach(chartConfig => {
+      const { labels } = chartConfig;
       labels.forEach(label => {
         newColorMap.set(label, this.colorMap.get(label));
       });
@@ -83,19 +96,12 @@ export class SharedLabelColor {
     this.colorMap = newColorMap;
   }
 
-  reset() {
-    const copyColorMap = new Map(this.colorMap);
-    copyColorMap.forEach((_, label) => {
-      this.colorMap.set(label, '');
-    });
-  }
-
   clear() {
-    this.sliceLabelMap.clear();
+    this.chartsLabelsMap.clear();
     this.colorMap.clear();
   }
 }
 
-const getInstance = makeSingleton(SharedLabelColor);
+const getInstance = makeSingleton(LabelsColorMap);
 
 export default getInstance;
diff --git a/superset-frontend/packages/superset-ui-core/src/color/index.ts b/superset-frontend/packages/superset-ui-core/src/color/index.ts
index cb7e569b47..5e4d0ea7aa 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/index.ts
@@ -34,9 +34,9 @@ export * from './colorSchemes';
 export * from './utils';
 export * from './types';
 export {
-  default as getSharedLabelColor,
-  SharedLabelColor,
-  SharedLabelColorSource,
-} from './SharedLabelColorSingleton';
+  default as getLabelsColorMap,
+  LabelsColorMap,
+  LabelsColorMapSource,
+} from './LabelsColorMapSingleton';
 
 export const BRAND_COLOR = '#00A699';
diff --git a/superset-frontend/packages/superset-ui-core/src/color/utils.ts b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
index 55284f16b4..c0f33f9f8c 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/utils.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
@@ -55,11 +55,12 @@ export function getContrastingColor(color: string, thresholds = 186) {
 
 export function getAnalogousColors(colors: string[], results: number) {
   const generatedColors: string[] = [];
-  // This is to solve the problem that the first three values generated by tinycolor.analogous
-  // may have the same or very close colors.
   const ext = 3;
+
   const analogousColors = colors.map(color => {
+    // returns an array of tinycolor instances
     const result = tinycolor(color).analogous(results + ext);
+    // remove the first three colors to avoid the same or very close colors
     return result.slice(ext);
   });
 
diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useComponentDidUpdate/useComponentDidUpdate.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useComponentDidUpdate/useComponentDidUpdate.ts
index aa84568e29..f382cd37c8 100644
--- a/superset-frontend/packages/superset-ui-core/src/hooks/useComponentDidUpdate/useComponentDidUpdate.ts
+++ b/superset-frontend/packages/superset-ui-core/src/hooks/useComponentDidUpdate/useComponentDidUpdate.ts
@@ -17,9 +17,12 @@
  * under the License.
  */
 
-import { EffectCallback, useEffect, useRef } from 'react';
+import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
 
-export const useComponentDidUpdate = (effect: EffectCallback) => {
+export const useComponentDidUpdate = (
+  effect: EffectCallback,
+  deps?: DependencyList,
+) => {
   const isMountedRef = useRef(false);
   useEffect(() => {
     if (isMountedRef.current) {
@@ -27,5 +30,6 @@ export const useComponentDidUpdate = (effect: EffectCallback) => {
     } else {
       isMountedRef.current = true;
     }
-  }, [effect]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [...(deps || [effect])]);
 };
diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/customTimeRangeDecode.ts
similarity index 65%
copy from superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts
copy to superset-frontend/packages/superset-ui-core/src/time-comparison/customTimeRangeDecode.ts
index b55cceccea..d99c78281c 100644
--- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/customTimeRangeDecode.ts
@@ -16,24 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import moment, { Moment } from 'moment';
-import { SEPARATOR } from '@superset-ui/core';
+
+import { SEPARATOR } from './fetchTimeRange';
 import {
   CustomRangeDecodeType,
   CustomRangeType,
   DateTimeGrainType,
   DateTimeModeType,
-} from 'src/explore/components/controls/DateFilterControl/types';
-import { SEVEN_DAYS_AGO, MIDNIGHT, MOMENT_FORMAT } from './constants';
+} from './types';
 
-/**
- * RegExp to test a string for a full ISO 8601 Date
- * Does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec.
- *  YYYY-MM-DDThh:mm:ss
- *  YYYY-MM-DDThh:mm:ssTZD
- *  YYYY-MM-DDThh:mm:ss.sTZD
- * @see: https://www.w3.org/TR/NOTE-datetime
- */
 const iso8601 = String.raw`\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|Z)?`;
 const datetimeConstant = String.raw`(?:TODAY|NOW)`;
 const grainValue = String.raw`[+-]?[1-9][0-9]*`;
@@ -47,32 +38,26 @@ export const ISO8601_AND_CONSTANT = RegExp(
   'i',
 );
 const DATETIME_CONSTANT = ['now', 'today'];
+const SEVEN_DAYS_AGO = new Date();
+SEVEN_DAYS_AGO.setUTCHours(0, 0, 0, 0);
+
+const MIDNIGHT = new Date();
+MIDNIGHT.setUTCHours(0, 0, 0, 0);
+
 const defaultCustomRange: CustomRangeType = {
-  sinceDatetime: SEVEN_DAYS_AGO,
+  sinceDatetime: SEVEN_DAYS_AGO.setUTCDate(
+    SEVEN_DAYS_AGO.getUTCDate() - 7,
+  ).toString(),
   sinceMode: 'relative',
   sinceGrain: 'day',
   sinceGrainValue: -7,
-  untilDatetime: MIDNIGHT,
+  untilDatetime: MIDNIGHT.toString(),
   untilMode: 'specific',
   untilGrain: 'day',
   untilGrainValue: 7,
   anchorMode: 'now',
   anchorValue: 'now',
 };
-const SPECIFIC_MODE = ['specific', 'today', 'now'];
-
-export const dttmToMoment = (dttm: string): Moment => {
-  if (dttm === 'now') {
-    return moment().utc().startOf('second');
-  }
-  if (dttm === 'today') {
-    return moment().utc().startOf('day');
-  }
-  return moment(dttm);
-};
-
-export const dttmToString = (dttm: string): string =>
-  dttmToMoment(dttm).format(MOMENT_FORMAT);
 
 export const customTimeRangeDecode = (
   timeRange: string,
@@ -157,10 +142,10 @@ export const customTimeRangeDecode = (
       const [sinceDttm, sinceGrainValue, sinceGrain] = [
         ...sinceCapturedGroup.slice(1),
       ];
-      const [untileDttm, untilGrainValue, untilGrain] = [
+      const [untilDttm, untilGrainValue, untilGrain] = [
         ...untilCapturedGroup.slice(1),
       ];
-      if (sinceDttm === untileDttm) {
+      if (sinceDttm === untilDttm) {
         return {
           customRange: {
             ...defaultCustomRange,
@@ -169,7 +154,7 @@ export const customTimeRangeDecode = (
             sinceDatetime: sinceDttm,
             untilGrain: untilGrain as DateTimeGrainType,
             untilGrainValue: parseInt(untilGrainValue, 10),
-            untilDatetime: untileDttm,
+            untilDatetime: untilDttm,
             anchorValue: sinceDttm,
             sinceMode: 'relative',
             untilMode: 'relative',
@@ -186,50 +171,3 @@ export const customTimeRangeDecode = (
     matchedFlag: false,
   };
 };
-
-export const customTimeRangeEncode = (customRange: CustomRangeType): string => {
-  const {
-    sinceDatetime,
-    sinceMode,
-    sinceGrain,
-    sinceGrainValue,
-    untilDatetime,
-    untilMode,
-    untilGrain,
-    untilGrainValue,
-    anchorValue,
-  } = { ...customRange };
-  // specific : specific
-  if (SPECIFIC_MODE.includes(sinceMode) && SPECIFIC_MODE.includes(untilMode)) {
-    const since =
-      sinceMode === 'specific' ? dttmToString(sinceDatetime) : sinceMode;
-    const until =
-      untilMode === 'specific' ? dttmToString(untilDatetime) : untilMode;
-    return `${since} : ${until}`;
-  }
-
-  // specific : relative
-  if (SPECIFIC_MODE.includes(sinceMode) && untilMode === 'relative') {
-    const since =
-      sinceMode === 'specific' ? dttmToString(sinceDatetime) : sinceMode;
-    const until = `DATEADD(DATETIME("${since}"), ${untilGrainValue}, ${untilGrain})`;
-    return `${since} : ${until}`;
-  }
-
-  // relative : specific
-  if (sinceMode === 'relative' && SPECIFIC_MODE.includes(untilMode)) {
-    const until =
-      untilMode === 'specific' ? dttmToString(untilDatetime) : untilMode;
-    const since = `DATEADD(DATETIME("${until}"), ${-Math.abs(
-      sinceGrainValue,
-    )}, ${sinceGrain})`;
-    return `${since} : ${until}`;
-  }
-
-  // relative : relative
-  const since = `DATEADD(DATETIME("${anchorValue}"), ${-Math.abs(
-    sinceGrainValue,
-  )}, ${sinceGrain})`;
-  const until = `DATEADD(DATETIME("${anchorValue}"), ${untilGrainValue}, ${untilGrain})`;
-  return `${since} : ${until}`;
-};
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/getTimeOffset.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/getTimeOffset.ts
index aea7c6bd4a..4d09d509a2 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-comparison/getTimeOffset.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/getTimeOffset.ts
@@ -16,52 +16,81 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { isEmpty } from 'lodash';
 import { ensureIsArray } from '../utils';
+import { customTimeRangeDecode } from './customTimeRangeDecode';
 
 const DAY_IN_MS = 24 * 60 * 60 * 1000;
-export const parseDttmToDate = (dttm: string): Date => {
+export const parseDttmToDate = (
+  dttm: string,
+  isEndDate = false,
+  computingShifts = false,
+) => {
   const now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-
-  if (dttm === 'now' || dttm === 'today' || dttm === 'No filter') {
-    return now;
-  }
-  if (dttm === 'Last week') {
-    now.setUTCDate(now.getUTCDate() - 7);
-    return now;
-  }
-  if (dttm === 'Last month') {
-    now.setUTCMonth(now.getUTCMonth() - 1);
-    now.setUTCDate(1);
-    return now;
-  }
-  if (dttm === 'Last quarter') {
-    now.setUTCMonth(now.getUTCMonth() - 3);
-    now.setUTCDate(1);
+  if (
+    dttm === 'now' ||
+    dttm === 'today' ||
+    dttm === 'No filter' ||
+    dttm === ''
+  ) {
     return now;
   }
-  if (dttm === 'Last year') {
-    now.setUTCFullYear(now.getUTCFullYear() - 1);
-    now.setUTCDate(1);
-    return now;
-  }
-  if (dttm === 'previous calendar week') {
-    now.setUTCDate(now.getUTCDate() - now.getUTCDay());
-    return now;
+
+  if (computingShifts) {
+    now.setHours(-now.getTimezoneOffset() / 60, 0, 0, 0);
+  } else {
+    now.setHours(0, 0, 0, 0);
   }
-  if (dttm === 'previous calendar month') {
-    now.setUTCMonth(now.getUTCMonth() - 1, 1);
+
+  if (isEndDate && dttm?.includes('Last')) {
     return now;
   }
-  if (dttm === 'previous calendar year') {
-    now.setUTCFullYear(now.getUTCFullYear() - 1, 0, 1);
-    return now;
+
+  switch (dttm) {
+    case 'Last day':
+      now.setUTCDate(now.getUTCDate() - 1);
+      return now;
+    case 'Last week':
+      now.setUTCDate(now.getUTCDate() - 7);
+      return now;
+    case 'Last month':
+      now.setUTCMonth(now.getUTCMonth() - 1);
+      return now;
+    case 'Last quarter':
+      now.setUTCMonth(now.getUTCMonth() - 3);
+      return now;
+    case 'Last year':
+      now.setUTCFullYear(now.getUTCFullYear() - 1);
+      return now;
+    case 'previous calendar week':
+      if (isEndDate) {
+        now.setDate(now.getDate() - now.getDay() + 1); // end date is the last day of the previous week (Sunday)
+      } else {
+        now.setDate(now.getDate() - now.getDay() - 6); // start date is the first day of the previous week (Monday)
+      }
+      return now;
+    case 'previous calendar month':
+      if (isEndDate) {
+        now.setDate(1); // end date is the last day of the previous month
+      } else {
+        now.setDate(1); // start date is the first day of the previous month
+        now.setMonth(now.getMonth() - 1);
+      }
+      return now;
+    case 'previous calendar year':
+      if (isEndDate) {
+        now.setFullYear(now.getFullYear(), 0, 1); // end date is the last day of the previous year
+      } else {
+        now.setFullYear(now.getFullYear() - 1, 0, 1); // start date is the first day of the previous year
+      }
+      return now;
+    default:
+      break;
   }
   if (dttm?.includes('ago')) {
     const parts = dttm.split(' ');
     const amount = parseInt(parts[0], 10);
     const unit = parts[1];
-
     switch (unit) {
       case 'day':
       case 'days':
@@ -84,38 +113,202 @@ export const parseDttmToDate = (dttm: string): Date => {
     }
     return now;
   }
-  const parsed = new Date(dttm);
-  parsed.setUTCHours(0, 0, 0, 0);
-  return parsed;
+  const parts = dttm?.split('-');
+  let parsed: Date | null = null;
+  if (parts && !isEmpty(parts)) {
+    if (parts.length === 1) {
+      parsed = new Date(Date.UTC(parseInt(parts[0], 10), 0));
+    } else if (parts.length === 2) {
+      parsed = new Date(
+        Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1),
+      );
+    } else if (parts.length === 3) {
+      parsed = new Date(
+        parseInt(parts[0], 10),
+        parseInt(parts[1], 10) - 1,
+        parseInt(parts[2], 10),
+      );
+    } else {
+      parsed = new Date(dttm);
+    }
+  } else {
+    parsed = new Date(dttm);
+  }
+  if (parsed && !Number.isNaN(parsed.getTime())) {
+    if (computingShifts) {
+      parsed.setHours(-parsed.getTimezoneOffset() / 60, 0, 0, 0);
+    } else {
+      parsed.setHours(0, 0, 0, 0);
+    }
+    return parsed;
+  }
+  // Return null if the string cannot be parsed into a date
+  return null;
 };
 
-export const getTimeOffset = (
-  timeRangeFilter: any,
-  shifts: string[],
-  startDate: string,
-): string[] => {
+export const computeCustomDateTime = (
+  dttm: string,
+  grain: string,
+  grainValue: number,
+) => {
+  let parsed: Date;
+  if (dttm === 'now' || dttm === 'today') {
+    parsed = new Date();
+  } else {
+    parsed = new Date(dttm);
+  }
+  if (!Number.isNaN(parsed.getTime())) {
+    switch (grain) {
+      case 'second':
+        parsed.setSeconds(parsed.getSeconds() + grainValue);
+        break;
+      case 'minute':
+        parsed.setMinutes(parsed.getMinutes() + grainValue);
+        break;
+      case 'hour':
+        parsed.setHours(parsed.getHours() + grainValue);
+        break;
+      case 'day':
+        parsed.setDate(parsed.getDate() + grainValue);
+        break;
+      case 'week':
+        parsed.setDate(parsed.getDate() + grainValue * 7);
+        break;
+      case 'month':
+        parsed.setMonth(parsed.getMonth() + grainValue);
+        break;
+      case 'quarter':
+        parsed.setMonth(parsed.getMonth() + grainValue * 3);
+        break;
+      case 'year':
+        parsed.setFullYear(parsed.getFullYear() + grainValue);
+        break;
+      default:
+        break;
+    }
+    return parsed;
+  }
+  return null;
+};
+
+type TimeOffsetArgs = {
+  timeRangeFilter: any;
+  shifts: string[];
+  startDate: string;
+  includeFutureOffsets?: boolean;
+};
+
+export const getTimeOffset = ({
+  timeRangeFilter,
+  shifts,
+  startDate,
+  includeFutureOffsets = true,
+}: TimeOffsetArgs): string[] => {
+  const { customRange, matchedFlag } = customTimeRangeDecode(
+    timeRangeFilter?.comparator ?? '',
+  );
+  let customStartDate: Date | null = null;
+  let customEndDate: Date | null = null;
+  if (matchedFlag) {
+    // Compute the start date and end date using the custom range information
+    const {
+      sinceDatetime,
+      sinceMode,
+      sinceGrain,
+      sinceGrainValue,
+      untilDatetime,
+      untilMode,
+      untilGrain,
+      untilGrainValue,
+    } = { ...customRange };
+    if (sinceMode !== 'relative') {
+      if (sinceMode === 'specific') {
+        customStartDate = new Date(sinceDatetime);
+      } else {
+        customStartDate = parseDttmToDate(sinceDatetime, false, true);
+      }
+    } else {
+      customStartDate = computeCustomDateTime(
+        sinceDatetime,
+        sinceGrain,
+        sinceGrainValue,
+      );
+    }
+    customStartDate?.setHours(0, 0, 0, 0);
+    if (untilMode !== 'relative') {
+      if (untilMode === 'specific') {
+        customEndDate = new Date(untilDatetime);
+      } else {
+        customEndDate = parseDttmToDate(untilDatetime, false, true);
+      }
+    } else {
+      customEndDate = computeCustomDateTime(
+        untilDatetime,
+        untilGrain,
+        untilGrainValue,
+      );
+    }
+    customEndDate?.setHours(0, 0, 0, 0);
+  }
   const isCustom = shifts?.includes('custom');
   const isInherit = shifts?.includes('inherit');
-  const customStartDate = isCustom && parseDttmToDate(startDate).getTime();
-  const filterStartDate = parseDttmToDate(
-    timeRangeFilter.comparator.split(' : ')[0],
-  ).getTime();
-  const filterEndDate = parseDttmToDate(
-    timeRangeFilter.comparator.split(' : ')[1],
-  ).getTime();
+  let customStartDateTime: number | undefined;
+  if (isCustom) {
+    if (matchedFlag) {
+      customStartDateTime = new Date(
+        new Date(startDate).setUTCHours(
+          new Date(startDate).getTimezoneOffset() / 60,
+          0,
+          0,
+          0,
+        ),
+      ).getTime();
+    } else {
+      customStartDateTime = parseDttmToDate(startDate)?.getTime();
+    }
+  }
+  const [startStr, endStr] = (timeRangeFilter?.comparator ?? '')
+    .split(' : ')
+    .map((date: string) => date.trim());
+  const filterStartDateTime =
+    (customStartDate ?? parseDttmToDate(startStr, false, false))?.getTime() ||
+    0;
+  const filterEndDateTime =
+    (
+      customEndDate ?? parseDttmToDate(endStr || startStr, true, false)
+    )?.getTime() || 0;
 
   const customShift =
-    customStartDate &&
-    Math.ceil((filterStartDate - customStartDate) / DAY_IN_MS);
+    customStartDateTime &&
+    Math.round((filterStartDateTime - customStartDateTime) / DAY_IN_MS);
   const inInheritShift =
-    isInherit && Math.ceil((filterEndDate - filterStartDate) / DAY_IN_MS);
+    isInherit &&
+    Math.round((filterEndDateTime - filterStartDateTime) / DAY_IN_MS);
 
-  let newShifts = shifts;
-  if (isCustom) {
-    newShifts = [`${customShift} days ago`];
-  }
-  if (isInherit) {
-    newShifts = [`${inInheritShift} days ago`];
-  }
+  const newShifts = ensureIsArray(shifts)
+    .map(shift => {
+      if (shift === 'custom') {
+        if (customShift !== undefined && !Number.isNaN(customShift)) {
+          if (includeFutureOffsets && customShift < 0) {
+            return `${customShift * -1} days after`;
+          }
+          if (customShift >= 0) {
+            return `${customShift} days ago`;
+          }
+        }
+      }
+      if (shift === 'inherit') {
+        if (inInheritShift && !Number.isNaN(inInheritShift)) {
+          if (includeFutureOffsets && inInheritShift < 0) {
+            return `${inInheritShift * -1} days after`;
+          }
+          if (inInheritShift > 0) {
+            return `${inInheritShift} days ago`;
+          }
+        }
+      }
+      return shift;
+    })
+    .filter(shift => shift !== 'custom' && shift !== 'inherit');
   return ensureIsArray(newShifts);
 };
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
index 7856d344ff..94f8ea4527 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts
@@ -21,5 +21,10 @@ export * from './types';
 
 export { default as getComparisonInfo } from './getComparisonInfo';
 export { default as getComparisonFilters } from './getComparisonFilters';
-export { parseDttmToDate, getTimeOffset } from './getTimeOffset';
+export {
+  parseDttmToDate,
+  getTimeOffset,
+  computeCustomDateTime,
+} from './getTimeOffset';
 export { SEPARATOR, fetchTimeRange } from './fetchTimeRange';
+export { customTimeRangeDecode } from './customTimeRangeDecode';
diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
index d9d61a19cd..5c4f564be8 100644
--- a/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/types.ts
@@ -28,3 +28,45 @@ export enum ComparisonTimeRangeType {
   Week = 'w',
   Year = 'y',
 }
+
+export type DateTimeGrainType =
+  | 'second'
+  | 'minute'
+  | 'hour'
+  | 'day'
+  | 'week'
+  | 'month'
+  | 'quarter'
+  | 'year';
+
+export type CustomRangeKey =
+  | 'sinceMode'
+  | 'sinceDatetime'
+  | 'sinceGrain'
+  | 'sinceGrainValue'
+  | 'untilMode'
+  | 'untilDatetime'
+  | 'untilGrain'
+  | 'untilGrainValue'
+  | 'anchorMode'
+  | 'anchorValue';
+
+export type DateTimeModeType = 'specific' | 'relative' | 'now' | 'today';
+
+export type CustomRangeType = {
+  sinceMode: DateTimeModeType;
+  sinceDatetime: string;
+  sinceGrain: DateTimeGrainType;
+  sinceGrainValue: number;
+  untilMode: DateTimeModeType;
+  untilDatetime: string;
+  untilGrain: DateTimeGrainType;
+  untilGrainValue: number;
+  anchorMode: 'now' | 'specific';
+  anchorValue: string;
+};
+
+export type CustomRangeDecodeType = {
+  customRange: CustomRangeType;
+  matchedFlag: boolean;
+};
diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
index bd2fa9047c..775e2c129a 100644
--- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
@@ -23,6 +23,8 @@ import {
   ComponentType,
 } from 'react';
 import type { Editor } from 'brace';
+import { BaseFormData } from '../query';
+import { JsonResponse } from '../connection';
 
 /**
  * A function which returns text (or marked-up text)
@@ -30,6 +32,14 @@ import type { Editor } from 'brace';
  */
 type ReturningDisplayable<P = void> = (props: P) => string | ReactElement;
 
+/**
+ * A function which returns the drill by options for a given dataset and chart's formData.
+ */
+export type LoadDrillByOptions = (
+  datasetId: number,
+  formData: BaseFormData,
+) => Promise<JsonResponse>;
+
 /**
  * This type defines all available extensions of Superset's default UI.
  * Namespace the keys here to follow the form of 'some_domain.functionality.item'.
@@ -193,6 +203,7 @@ export interface CustomAutocomplete extends AutocompleteItem {
 
 export type Extensions = Partial<{
   'alertsreports.header.icon': ComponentType;
+  'load.drillby.options': LoadDrillByOptions;
   'embedded.documentation.configuration_details': ComponentType<ConfigDetailsProps>;
   'embedded.documentation.description': ReturningDisplayable;
   'embedded.documentation.url': string;
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index b3af431d52..fcc38dc2ec 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -57,6 +57,7 @@ export enum FeatureFlag {
   TaggingSystem = 'TAGGING_SYSTEM',
   Thumbnails = 'THUMBNAILS',
   UseAnalagousColors = 'USE_ANALAGOUS_COLORS',
+  ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
 }
 
 export type ScheduleQueriesProps = {
diff --git a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorNameSpace.test.ts b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorNameSpace.test.ts
index d5e1ef8f0b..69fb38eea3 100644
--- a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorNameSpace.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorNameSpace.test.ts
@@ -98,7 +98,7 @@ describe('CategoricalColorNamespace', () => {
       namespace.setColor('dog', 'black');
       const scale = namespace.getScale('testColors');
       scale.setColor('dog', 'pink');
-      expect(scale.getColor('dog')).toBe('black');
+      expect(scale.getColor('dog')).toBe('pink');
       expect(scale.getColor('boy')).not.toBe('black');
     });
     it('does not affect scales in other namespaces', () => {
diff --git a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts
index 48e43d8351..97d756cb04 100644
--- a/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/color/CategoricalColorScale.test.ts
@@ -18,50 +18,76 @@
  */
 
 import { ScaleOrdinal } from 'd3-scale';
-import {
-  CategoricalColorScale,
-  FeatureFlag,
-  getSharedLabelColor,
-} from '@superset-ui/core';
+import { CategoricalColorScale, FeatureFlag } from '@superset-ui/core';
 
 describe('CategoricalColorScale', () => {
+  beforeEach(() => {
+    window.featureFlags = {};
+  });
+
   it('exists', () => {
     expect(CategoricalColorScale !== undefined).toBe(true);
   });
 
-  describe('new CategoricalColorScale(colors, parentForcedColors)', () => {
-    it('can create new scale when parentForcedColors is not given', () => {
+  describe('new CategoricalColorScale(colors, forcedColors)', () => {
+    it('can create new scale when forcedColors is not given', () => {
       const scale = new CategoricalColorScale(['blue', 'red', 'green']);
       expect(scale).toBeInstanceOf(CategoricalColorScale);
     });
-    it('can create new scale when parentForcedColors is given', () => {
-      const parentForcedColors = {};
+    it('can create new scale when forcedColors is given', () => {
+      const forcedColors = {};
       const scale = new CategoricalColorScale(
         ['blue', 'red', 'green'],
-        parentForcedColors,
+        forcedColors,
       );
       expect(scale).toBeInstanceOf(CategoricalColorScale);
-      expect(scale.parentForcedColors).toBe(parentForcedColors);
+      expect(scale.forcedColors).toBe(forcedColors);
     });
 
     it('can refer to colors based on their index', () => {
-      const parentForcedColors = { pig: 1, horse: 5 };
+      const forcedColors = { pig: 1, horse: 5 };
       const scale = new CategoricalColorScale(
         ['blue', 'red', 'green'],
-        parentForcedColors,
+        forcedColors,
       );
       expect(scale.getColor('pig')).toEqual('red');
-      expect(parentForcedColors.pig).toEqual('red');
+      expect(forcedColors.pig).toEqual('red');
 
       // can loop around the scale
       expect(scale.getColor('horse')).toEqual('green');
-      expect(parentForcedColors.horse).toEqual('green');
+      expect(forcedColors.horse).toEqual('green');
     });
   });
 
-  describe('.getColor(value)', () => {
+  describe('.getColor(value, sliceId)', () => {
+    let scale: CategoricalColorScale;
+    let addSliceSpy: jest.SpyInstance<
+      void,
+      [label: string, color: string, sliceId: number, colorScheme?: string]
+    >;
+    let getNextAvailableColorSpy: jest.SpyInstance<
+      string,
+      [currentColor: string]
+    >;
+
+    beforeEach(() => {
+      scale = new CategoricalColorScale(['blue', 'red', 'green']);
+      // Spy on the addSlice method of labelsColorMapInstance
+      addSliceSpy = jest.spyOn(scale.labelsColorMapInstance, 'addSlice');
+      getNextAvailableColorSpy = jest
+        .spyOn(scale, 'getNextAvailableColor')
+        .mockImplementation(color => color);
+    });
+
+    afterEach(() => {
+      jest.restoreAllMocks();
+    });
+
     it('returns same color for same value', () => {
-      const scale = new CategoricalColorScale(['blue', 'red', 'green']);
+      const scale = new CategoricalColorScale(['blue', 'red', 'green'], {
+        pig: 'red',
+        horse: 'green',
+      });
       const c1 = scale.getColor('pig');
       const c2 = scale.getColor('horse');
       const c3 = scale.getColor('pig');
@@ -82,9 +108,6 @@ describe('CategoricalColorScale', () => {
       expect(c3).not.toBe(c1);
     });
     it('recycles colors when number of items exceed available colors', () => {
-      window.featureFlags = {
-        [FeatureFlag.UseAnalagousColors]: false,
-      };
       const colorSet: { [key: string]: number } = {};
       const scale = new CategoricalColorScale(['blue', 'red', 'green']);
       const colors = [
@@ -118,43 +141,70 @@ describe('CategoricalColorScale', () => {
       scale.getColor('cow');
       scale.getColor('donkey');
       scale.getColor('goat');
-      expect(scale.range()).toHaveLength(6);
+      expect(scale.range()).toHaveLength(9);
     });
+    it('adds the color and value to chartLabelsColorMap and calls addSlice', () => {
+      const value = 'testValue';
+      const sliceId = 123;
+      const colorScheme = 'preset';
+
+      expect(scale.chartLabelsColorMap.has(value)).toBe(false);
+
+      scale.getColor(value, sliceId, colorScheme);
+
+      expect(scale.chartLabelsColorMap.has(value)).toBe(true);
+      expect(scale.chartLabelsColorMap.get(value)).toBeDefined();
+
+      expect(addSliceSpy).toHaveBeenCalledWith(
+        value,
+        expect.any(String),
+        sliceId,
+        colorScheme,
+      );
 
-    it('should remove shared color from range if avoid colors collision enabled', () => {
+      const expectedColor = scale.chartLabelsColorMap.get(value);
+      const returnedColor = scale.getColor(value, sliceId);
+      expect(returnedColor).toBe(expectedColor);
+    });
+    it('conditionally calls getNextAvailableColor', () => {
       window.featureFlags = {
         [FeatureFlag.AvoidColorsCollision]: true,
       };
-      const scale = new CategoricalColorScale(['blue', 'red', 'green']);
-      const color1 = scale.getColor('a', 1);
-      expect(scale.range()).toHaveLength(3);
-      const color2 = scale.getColor('a', 2);
-      expect(color1).toBe(color2);
-      scale.getColor('b', 2);
-      expect(scale.range()).toHaveLength(2);
-      scale.getColor('c', 2);
-      expect(scale.range()).toHaveLength(1);
+
+      scale.getColor('testValue1');
+      scale.getColor('testValue2');
+      scale.getColor('testValue1');
+      scale.getColor('testValue3');
+      scale.getColor('testValue4');
+
+      expect(getNextAvailableColorSpy).toHaveBeenCalledWith('blue');
+
+      getNextAvailableColorSpy.mockClear();
+
+      window.featureFlags = {
+        [FeatureFlag.AvoidColorsCollision]: false,
+      };
+
+      scale.getColor('testValue3');
+
+      expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
     });
-    window.featureFlags = {
-      [FeatureFlag.AvoidColorsCollision]: false,
-    };
   });
+
   describe('.setColor(value, forcedColor)', () => {
     it('overrides default color', () => {
       const scale = new CategoricalColorScale(['blue', 'red', 'green']);
       scale.setColor('pig', 'pink');
       expect(scale.getColor('pig')).toBe('pink');
     });
-    it('does not override parentForcedColors', () => {
+    it('does override forcedColors', () => {
       const scale1 = new CategoricalColorScale(['blue', 'red', 'green']);
       scale1.setColor('pig', 'black');
-      const scale2 = new CategoricalColorScale(
-        ['blue', 'red', 'green'],
-        scale1.forcedColors,
-      );
+
+      const scale2 = new CategoricalColorScale(['blue', 'red', 'green']);
       scale2.setColor('pig', 'pink');
+      expect(scale2.getColor('pig')).toBe('pink');
       expect(scale1.getColor('pig')).toBe('black');
-      expect(scale2.getColor('pig')).toBe('black');
     });
     it('returns the scale', () => {
       const scale = new CategoricalColorScale(['blue', 'red', 'green']);
@@ -163,7 +213,7 @@ describe('CategoricalColorScale', () => {
     });
   });
   describe('.getColorMap()', () => {
-    it('returns correct mapping and parentForcedColors and forcedColors are specified', () => {
+    it('returns correct mapping using least used color', () => {
       const scale1 = new CategoricalColorScale(['blue', 'red', 'green']);
       scale1.setColor('cow', 'black');
       const scale2 = new CategoricalColorScale(
@@ -177,7 +227,7 @@ describe('CategoricalColorScale', () => {
       expect(scale2.getColorMap()).toEqual({
         cow: 'black',
         pig: 'pink',
-        horse: 'green',
+        horse: 'blue', // least used color
       });
     });
   });
@@ -230,64 +280,122 @@ describe('CategoricalColorScale', () => {
   });
 
   describe('a CategoricalColorScale instance is also a color function itself', () => {
-    it('scale(value) returns color similar to calling scale.getColor(value)', () => {
+    it('scale(value) returns same color for same value', () => {
       const scale = new CategoricalColorScale(['blue', 'red', 'green']);
-      expect(scale.getColor('pig')).toBe(scale('pig'));
-      expect(scale.getColor('cat')).toBe(scale('cat'));
+      expect(scale.getColor('pig')).toBe('blue');
+      expect(scale('pig')).toBe('blue');
+      expect(scale.getColor('cat')).toBe('red');
+      expect(scale('cat')).toBe('red');
     });
   });
 
-  describe("is compatible with D3's ScaleOrdinal", () => {
-    it('passes type check', () => {
-      const scale: ScaleOrdinal<{ toString(): string }, string> =
-        new CategoricalColorScale(['blue', 'red', 'green']);
-      expect(scale('pig')).toBe('blue');
+  describe('.getNextAvailableColor(currentColor)', () => {
+    it('returns the current color if it is the least used or equally used among colors', () => {
+      const scale = new CategoricalColorScale(['blue', 'red', 'green']);
+      scale.getColor('cat');
+      scale.getColor('dog');
+
+      // Since 'green' hasn't been used, it's considered the least used.
+      expect(scale.getNextAvailableColor('blue')).toBe('green');
+    });
+
+    it('handles cases where all colors are equally used and returns the current color', () => {
+      const scale = new CategoricalColorScale(['blue', 'red', 'green']);
+      scale.getColor('cat'); // blue
+      scale.getColor('dog'); // red
+      scale.getColor('fish'); // green
+      // All colors used once, so the function should return the current color
+      expect(scale.getNextAvailableColor('red')).toBe('red');
+    });
+
+    it('returns the least used color accurately even when some colors are used more frequently', () => {
+      const scale = new CategoricalColorScale([
+        'blue',
+        'red',
+        'green',
+        'yellow',
+      ]);
+      scale.getColor('cat'); // blue
+      scale.getColor('dog'); // red
+      scale.getColor('frog'); // green
+      scale.getColor('fish'); // yellow
+      scale.getColor('goat'); // blue
+      scale.getColor('horse'); // red
+      scale.getColor('pony'); // green
+
+      // Yellow is the least used color, so it should be returned.
+      expect(scale.getNextAvailableColor('blue')).toBe('yellow');
     });
   });
 
-  describe('.removeSharedLabelColorFromRange(colorMap, cleanedValue)', () => {
-    it('should remove shared color from range', () => {
-      const scale = new CategoricalColorScale(['blue', 'green', 'red']);
-      expect(scale.range()).toEqual(['blue', 'green', 'red']);
-
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.clear();
-      const colorMap = sharedLabelColor.getColorMap();
-      sharedLabelColor.addSlice('cow', 'blue', 1);
-      scale.removeSharedLabelColorFromRange(colorMap, 'pig');
-      expect(scale.range()).toEqual(['green', 'red']);
-      scale.removeSharedLabelColorFromRange(colorMap, 'cow');
-      expect(scale.range()).toEqual(['blue', 'green', 'red']);
-      sharedLabelColor.clear();
+  describe('.isColorUsed(color)', () => {
+    it('returns true if the color is already used, false otherwise', () => {
+      const scale = new CategoricalColorScale(['blue', 'red', 'green']);
+      // Initially, no color is used
+      expect(scale.isColorUsed('blue')).toBe(false);
+      expect(scale.isColorUsed('red')).toBe(false);
+      expect(scale.isColorUsed('green')).toBe(false);
+
+      scale.getColor('item1');
+
+      // Now, 'blue' is used, but 'red' and 'green' are not
+      expect(scale.isColorUsed('blue')).toBe(true);
+      expect(scale.isColorUsed('red')).toBe(false);
+      expect(scale.isColorUsed('green')).toBe(false);
+
+      // Simulate using the 'red' color
+      scale.getColor('item2'); // Assigns 'red' to 'item2'
+
+      // Now, 'blue' and 'red' are used
+      expect(scale.isColorUsed('blue')).toBe(true);
+      expect(scale.isColorUsed('red')).toBe(true);
+      expect(scale.isColorUsed('green')).toBe(false);
     });
+  });
+
+  describe('.getColorUsageCount(color)', () => {
+    it('accurately counts the occurrences of a specific color', () => {
+      const scale = new CategoricalColorScale([
+        'blue',
+        'red',
+        'green',
+        'yellow',
+      ]);
+      // No colors are used initially
+      expect(scale.getColorUsageCount('blue')).toBe(0);
+      expect(scale.getColorUsageCount('red')).toBe(0);
+      expect(scale.getColorUsageCount('green')).toBe(0);
+      expect(scale.getColorUsageCount('yellow')).toBe(0);
 
-    it('recycles colors when all colors are in sharedLabelColor', () => {
-      const scale = new CategoricalColorScale(['blue', 'green', 'red']);
-      expect(scale.range()).toEqual(['blue', 'green', 'red']);
-      const sharedLabelColor = getSharedLabelColor();
-      const colorMap = sharedLabelColor.getColorMap();
-      sharedLabelColor.addSlice('cow', 'blue', 1);
-      sharedLabelColor.addSlice('pig', 'red', 1);
-      sharedLabelColor.addSlice('horse', 'green', 1);
-      scale.removeSharedLabelColorFromRange(colorMap, 'goat');
-      expect(scale.range()).toEqual(['blue', 'green', 'red']);
-      sharedLabelColor.clear();
+      // Simulate using colors
+      scale.getColor('item1');
+      scale.getColor('item2');
+      scale.getColor('item1');
+
+      // Check the counts after using the colors
+      expect(scale.getColorUsageCount('blue')).toBe(1);
+      expect(scale.getColorUsageCount('red')).toBe(1);
+      expect(scale.getColorUsageCount('green')).toBe(0);
+      expect(scale.getColorUsageCount('yellow')).toBe(0);
+
+      // Simulate using colors more
+      scale.getColor('item3');
+      scale.getColor('item4');
+      scale.getColor('item3');
+
+      // Final counts
+      expect(scale.getColorUsageCount('blue')).toBe(1);
+      expect(scale.getColorUsageCount('red')).toBe(1);
+      expect(scale.getColorUsageCount('green')).toBe(1);
+      expect(scale.getColorUsageCount('yellow')).toBe(1);
     });
+  });
 
-    it('should remove parentForcedColors from range', () => {
-      const parentForcedColors = { house: 'blue', cow: 'red' };
-      const scale = new CategoricalColorScale(
-        ['blue', 'red', 'green'],
-        parentForcedColors,
-      );
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.clear();
-      const colorMap = sharedLabelColor.getColorMap();
-      scale.removeSharedLabelColorFromRange(colorMap, 'pig');
-      expect(scale.range()).toEqual(['green']);
-      scale.removeSharedLabelColorFromRange(colorMap, 'cow');
-      expect(scale.range()).toEqual(['red', 'green']);
-      sharedLabelColor.clear();
+  describe("is compatible with D3's ScaleOrdinal", () => {
+    it('passes type check', () => {
+      const scale: ScaleOrdinal<{ toString(): string }, string> =
+        new CategoricalColorScale(['blue', 'red', 'green']);
+      expect(scale('pig')).toBe('blue');
     });
   });
 });
diff --git a/superset-frontend/packages/superset-ui-core/test/color/LabelsColorMapSingleton.test.ts b/superset-frontend/packages/superset-ui-core/test/color/LabelsColorMapSingleton.test.ts
new file mode 100644
index 0000000000..b93a416e7f
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/color/LabelsColorMapSingleton.test.ts
@@ -0,0 +1,234 @@
+/*
+ * 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 {
+  CategoricalColorNamespace,
+  CategoricalScheme,
+  FeatureFlag,
+  getCategoricalSchemeRegistry,
+  getLabelsColorMap,
+  LabelsColorMapSource,
+  LabelsColorMap,
+} from '@superset-ui/core';
+
+const actual = jest.requireActual('../../src/color/utils');
+const getAnalogousColorsSpy = jest
+  .spyOn(actual, 'getAnalogousColors')
+  .mockImplementation(() => ['red', 'green', 'blue']);
+
+describe('LabelsColorMap', () => {
+  beforeAll(() => {
+    getCategoricalSchemeRegistry()
+      .registerValue(
+        'testColors',
+        new CategoricalScheme({
+          id: 'testColors',
+          colors: ['red', 'green', 'blue'],
+        }),
+      )
+      .registerValue(
+        'testColors2',
+        new CategoricalScheme({
+          id: 'testColors2',
+          colors: ['yellow', 'green', 'blue'],
+        }),
+      );
+  });
+
+  beforeEach(() => {
+    getLabelsColorMap().source = LabelsColorMapSource.Dashboard;
+    getLabelsColorMap().clear();
+  });
+
+  it('has default value out-of-the-box', () => {
+    expect(getLabelsColorMap()).toBeInstanceOf(LabelsColorMap);
+  });
+
+  describe('.addSlice(value, color, sliceId)', () => {
+    it('should add to sliceLabelColorMap when first adding label', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1, 'preset');
+      expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(true);
+      const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
+      expect(chartConfig?.labels?.includes('a')).toEqual(true);
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red' });
+    });
+
+    it('should add to sliceLabelColorMap when slice exist', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 1);
+      const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
+      expect(chartConfig?.labels?.includes('b')).toEqual(true);
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
+    });
+
+    it('should use last color if adding label repeatedly', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('b', 'blue', 1);
+      labelsColorMap.addSlice('b', 'green', 1);
+      const chartConfig = labelsColorMap.chartsLabelsMap.get(1);
+      expect(chartConfig?.labels?.includes('b')).toEqual(true);
+      expect(chartConfig?.labels?.length).toEqual(1);
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
+    });
+
+    it('should do nothing when source is not dashboard', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.source = LabelsColorMapSource.Explore;
+      labelsColorMap.addSlice('a', 'red', 1);
+      expect(Object.fromEntries(labelsColorMap.chartsLabelsMap)).toEqual({});
+    });
+  });
+
+  describe('.remove(sliceId)', () => {
+    it('should remove sliceId', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.removeSlice(1);
+      expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(false);
+    });
+
+    it('should update colorMap', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 2);
+      labelsColorMap.removeSlice(1);
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({ b: 'blue' });
+    });
+
+    it('should do nothing when source is not dashboard', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.source = LabelsColorMapSource.Explore;
+      labelsColorMap.removeSlice(1);
+      expect(labelsColorMap.chartsLabelsMap.has(1)).toEqual(true);
+    });
+  });
+
+  describe('.updateColorMap(namespace, scheme)', () => {
+    let categoricalNamespace: any;
+    let mockedNamespace: any;
+    let labelsColorMap: any;
+
+    beforeEach(() => {
+      labelsColorMap = getLabelsColorMap();
+      categoricalNamespace = CategoricalColorNamespace.getNamespace(undefined);
+      mockedNamespace = {
+        getScale: jest.fn().mockReturnValue({
+          getColor: jest.fn(() => 'mockColor'),
+        }),
+      };
+    });
+
+    it('should use provided color scheme', () => {
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.updateColorMap(mockedNamespace, 'testColors2');
+      expect(mockedNamespace.getScale).toHaveBeenCalledWith('testColors2');
+    });
+
+    it('should fallback to original chart color scheme if no color scheme is provided', () => {
+      labelsColorMap.addSlice('a', 'red', 1, 'originalScheme');
+      labelsColorMap.updateColorMap(mockedNamespace);
+      expect(mockedNamespace.getScale).toHaveBeenCalledWith('originalScheme');
+    });
+
+    it('should fallback to undefined if no color scheme is provided', () => {
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 2);
+      labelsColorMap.updateColorMap(mockedNamespace);
+      expect(mockedNamespace.getScale).toHaveBeenCalledWith(undefined);
+    });
+
+    it('should update color map', () => {
+      // override color with forcedItems
+      categoricalNamespace.setColor('b', 'green');
+      // testColors2: 'yellow', 'green', 'blue'
+      // first-time label, gets color, yellow
+      labelsColorMap.addSlice('a', 'red', 1);
+      // overridden, gets green
+      labelsColorMap.addSlice('b', 'pink', 1);
+      // overridden, gets green
+      labelsColorMap.addSlice('b', 'green', 2);
+      // first-time slice label, gets color, yellow
+      labelsColorMap.addSlice('c', 'blue', 2);
+      labelsColorMap.updateColorMap(categoricalNamespace, 'testColors2');
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({
+        a: 'yellow',
+        b: 'green',
+        c: 'yellow',
+      });
+    });
+
+    it('should use recycle colors', () => {
+      window.featureFlags = {
+        [FeatureFlag.UseAnalagousColors]: false,
+      };
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 2);
+      labelsColorMap.addSlice('c', 'green', 3);
+      labelsColorMap.addSlice('d', 'red', 4);
+      labelsColorMap.updateColorMap(categoricalNamespace, 'testColors');
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).not.toEqual({});
+      expect(getAnalogousColorsSpy).not.toBeCalled();
+    });
+
+    it('should use analagous colors', () => {
+      window.featureFlags = {
+        [FeatureFlag.UseAnalagousColors]: true,
+      };
+
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 1);
+      labelsColorMap.addSlice('c', 'green', 1);
+      labelsColorMap.addSlice('d', 'red', 1);
+      labelsColorMap.updateColorMap(categoricalNamespace, 'testColors');
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).not.toEqual({});
+      expect(getAnalogousColorsSpy).toBeCalled();
+    });
+  });
+
+  describe('.getColorMap()', () => {
+    it('should get color map', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 2);
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
+    });
+  });
+
+  describe('.reset()', () => {
+    it('should reset color map', () => {
+      const labelsColorMap = getLabelsColorMap();
+      labelsColorMap.addSlice('a', 'red', 1);
+      labelsColorMap.addSlice('b', 'blue', 2);
+      labelsColorMap.clear();
+      const colorMap = labelsColorMap.getColorMap();
+      expect(Object.fromEntries(colorMap)).toEqual({});
+    });
+  });
+});
diff --git a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts
deleted file mode 100644
index ca3f89a523..0000000000
--- a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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 {
-  CategoricalScheme,
-  FeatureFlag,
-  getCategoricalSchemeRegistry,
-  getSharedLabelColor,
-  SharedLabelColor,
-  SharedLabelColorSource,
-} from '@superset-ui/core';
-
-const actual = jest.requireActual('../../src/color/utils');
-const getAnalogousColorsSpy = jest
-  .spyOn(actual, 'getAnalogousColors')
-  .mockImplementation(() => ['red', 'green', 'blue']);
-
-describe('SharedLabelColor', () => {
-  beforeAll(() => {
-    getCategoricalSchemeRegistry()
-      .registerValue(
-        'testColors',
-        new CategoricalScheme({
-          id: 'testColors',
-          colors: ['red', 'green', 'blue'],
-        }),
-      )
-      .registerValue(
-        'testColors2',
-        new CategoricalScheme({
-          id: 'testColors2',
-          colors: ['yellow', 'green', 'blue'],
-        }),
-      );
-  });
-
-  beforeEach(() => {
-    getSharedLabelColor().source = SharedLabelColorSource.Dashboard;
-    getSharedLabelColor().clear();
-  });
-
-  it('has default value out-of-the-box', () => {
-    expect(getSharedLabelColor()).toBeInstanceOf(SharedLabelColor);
-  });
-
-  describe('.addSlice(value, color, sliceId)', () => {
-    it('should add to sliceLabelColorMap when first adding label', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(true);
-      const labels = sharedLabelColor.sliceLabelMap.get(1);
-      expect(labels?.includes('a')).toEqual(true);
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red' });
-    });
-
-    it('should add to sliceLabelColorMap when slice exist', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 1);
-      const labels = sharedLabelColor.sliceLabelMap.get(1);
-      expect(labels?.includes('b')).toEqual(true);
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
-    });
-
-    it('should use last color if adding label repeatedly', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('b', 'blue', 1);
-      sharedLabelColor.addSlice('b', 'green', 1);
-      const labels = sharedLabelColor.sliceLabelMap.get(1);
-      expect(labels?.includes('b')).toEqual(true);
-      expect(labels?.length).toEqual(1);
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ b: 'green' });
-    });
-
-    it('should do nothing when source is not dashboard', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.source = SharedLabelColorSource.Explore;
-      sharedLabelColor.addSlice('a', 'red');
-      expect(Object.fromEntries(sharedLabelColor.sliceLabelMap)).toEqual({});
-    });
-
-    it('should do nothing when sliceId is undefined', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red');
-      expect(Object.fromEntries(sharedLabelColor.sliceLabelMap)).toEqual({});
-    });
-  });
-
-  describe('.remove(sliceId)', () => {
-    it('should remove sliceId', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.removeSlice(1);
-      expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(false);
-    });
-
-    it('should update colorMap', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 2);
-      sharedLabelColor.removeSlice(1);
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ b: 'blue' });
-    });
-
-    it('should do nothing when source is not dashboard', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.source = SharedLabelColorSource.Explore;
-      sharedLabelColor.removeSlice(1);
-      expect(sharedLabelColor.sliceLabelMap.has(1)).toEqual(true);
-    });
-  });
-
-  describe('.updateColorMap(namespace, scheme)', () => {
-    it('should update color map', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'pink', 1);
-      sharedLabelColor.addSlice('b', 'green', 2);
-      sharedLabelColor.addSlice('c', 'blue', 2);
-      sharedLabelColor.updateColorMap('', 'testColors2');
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({
-        a: 'yellow',
-        b: 'yellow',
-        c: 'green',
-      });
-    });
-
-    it('should use recycle colors', () => {
-      window.featureFlags = {
-        [FeatureFlag.UseAnalagousColors]: false,
-      };
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 2);
-      sharedLabelColor.addSlice('c', 'green', 3);
-      sharedLabelColor.addSlice('d', 'red', 4);
-      sharedLabelColor.updateColorMap('', 'testColors');
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).not.toEqual({});
-      expect(getAnalogousColorsSpy).not.toBeCalled();
-    });
-
-    it('should use analagous colors', () => {
-      window.featureFlags = {
-        [FeatureFlag.UseAnalagousColors]: true,
-      };
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 1);
-      sharedLabelColor.addSlice('c', 'green', 1);
-      sharedLabelColor.addSlice('d', 'red', 1);
-      sharedLabelColor.updateColorMap('', 'testColors');
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).not.toEqual({});
-      expect(getAnalogousColorsSpy).toBeCalled();
-    });
-  });
-
-  describe('.getColorMap()', () => {
-    it('should get color map', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 2);
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ a: 'red', b: 'blue' });
-    });
-  });
-
-  describe('.reset()', () => {
-    it('should reset color map', () => {
-      const sharedLabelColor = getSharedLabelColor();
-      sharedLabelColor.addSlice('a', 'red', 1);
-      sharedLabelColor.addSlice('b', 'blue', 2);
-      sharedLabelColor.reset();
-      const colorMap = sharedLabelColor.getColorMap();
-      expect(Object.fromEntries(colorMap)).toEqual({ a: '', b: '' });
-    });
-  });
-});
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/computeCustomDateTime.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/computeCustomDateTime.test.ts
new file mode 100644
index 0000000000..aa54dd1e69
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/computeCustomDateTime.test.ts
@@ -0,0 +1,80 @@
+/**
+ * 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 { computeCustomDateTime } from '@superset-ui/core';
+
+const TODAY = '2024-06-03';
+
+// Mock Date to always return 2024-06-03
+beforeEach(() => {
+  jest.useFakeTimers('modern');
+  jest.setSystemTime(new Date(TODAY).getTime());
+});
+
+afterEach(() => {
+  jest.useRealTimers();
+});
+
+test('should return the current date for "now"', () => {
+  expect(computeCustomDateTime('now', 'day', 0)).toEqual(new Date(TODAY));
+});
+
+test('should return the current date for "today"', () => {
+  expect(computeCustomDateTime('today', 'day', 0)).toEqual(new Date(TODAY));
+});
+
+test('should return the date for "2024-06-03" with grain "day" and grainValue 2', () => {
+  expect(computeCustomDateTime(TODAY, 'day', 2)).toEqual(
+    new Date('2024-06-05T00:00:00Z'),
+  );
+});
+
+test('should return the date for "2024-06-03" with grain "week" and grainValue 1', () => {
+  expect(computeCustomDateTime(TODAY, 'week', 1)).toEqual(
+    new Date('2024-06-10T00:00:00Z'),
+  );
+});
+
+test('should return the date for "2024-06-03" with grain "month" and grainValue 1', () => {
+  expect(computeCustomDateTime(TODAY, 'month', 1)).toEqual(
+    new Date('2024-07-03T00:00:00Z'),
+  );
+});
+
+test('should return the date for "2024-06-03" with grain "quarter" and grainValue 1', () => {
+  expect(computeCustomDateTime(TODAY, 'quarter', 1)).toEqual(
+    new Date('2024-09-03T00:00:00Z'),
+  );
+});
+
+test('should return the date for "2024-06-03" with grain "year" and grainValue 1', () => {
+  expect(computeCustomDateTime(TODAY, 'year', 1)).toEqual(
+    new Date('2025-06-03T00:00:00Z'),
+  );
+});
+
+test('should return null for an invalid date', () => {
+  expect(computeCustomDateTime('invalid', 'day', 1)).toBeNull();
+});
+
+test('should return the date for "2024-06-03" with an invalid grain', () => {
+  expect(computeCustomDateTime(TODAY, 'invalid', 1)).toEqual(
+    new Date('2024-06-03T00:00:00Z'),
+  );
+});
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/customTimeRangeDecode.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/customTimeRangeDecode.test.ts
new file mode 100644
index 0000000000..b3e86166e8
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/customTimeRangeDecode.test.ts
@@ -0,0 +1,205 @@
+/**
+ * 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 { customTimeRangeDecode } from '@superset-ui/core';
+
+describe('customTimeRangeDecode', () => {
+  it('1) specific : specific', () => {
+    expect(
+      customTimeRangeDecode('2021-01-20T00:00:00 : 2021-01-27T00:00:00'),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: '2021-01-20T00:00:00',
+        sinceMode: 'specific',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: '2021-01-27T00:00:00',
+        untilMode: 'specific',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('2) specific : relative', () => {
+    expect(
+      customTimeRangeDecode(
+        '2021-01-20T00:00:00 : DATEADD(DATETIME("2021-01-20T00:00:00"), 7, day)',
+      ),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: '2021-01-20T00:00:00',
+        sinceMode: 'specific',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: '2021-01-20T00:00:00',
+        untilMode: 'relative',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('3) relative : specific', () => {
+    expect(
+      customTimeRangeDecode(
+        'DATEADD(DATETIME("2021-01-27T00:00:00"), -7, day) : 2021-01-27T00:00:00',
+      ),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: '2021-01-27T00:00:00',
+        sinceMode: 'relative',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: '2021-01-27T00:00:00',
+        untilMode: 'specific',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('4) relative : relative (now)', () => {
+    expect(
+      customTimeRangeDecode(
+        'DATEADD(DATETIME("now"), -7, day) : DATEADD(DATETIME("now"), 7, day)',
+      ),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: 'now',
+        sinceMode: 'relative',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: 'now',
+        untilMode: 'relative',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('5) relative : relative (date/time)', () => {
+    expect(
+      customTimeRangeDecode(
+        'DATEADD(DATETIME("2021-01-27T00:00:00"), -7, day) : DATEADD(DATETIME("2021-01-27T00:00:00"), 7, day)',
+      ),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: '2021-01-27T00:00:00',
+        sinceMode: 'relative',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: '2021-01-27T00:00:00',
+        untilMode: 'relative',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'specific',
+        anchorValue: '2021-01-27T00:00:00',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('6) specific : relative (now)', () => {
+    expect(
+      customTimeRangeDecode('now : DATEADD(DATETIME("now"), 7, day)'),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: 'now',
+        sinceMode: 'now',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: 'now',
+        untilMode: 'relative',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: true,
+    });
+  });
+
+  it('7) default', () => {
+    const SEVEN_DAYS_AGO = new Date();
+    const MIDNIGHT = new Date();
+    SEVEN_DAYS_AGO.setUTCHours(0, 0, 0, 0);
+    MIDNIGHT.setUTCHours(0, 0, 0, 0);
+    expect(
+      customTimeRangeDecode('now : DATEADD(DATETIME("TODAY"), -7, day)'),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: SEVEN_DAYS_AGO.setUTCDate(
+          SEVEN_DAYS_AGO.getUTCDate() - 7,
+        ).toString(),
+        sinceMode: 'relative',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: MIDNIGHT.toString(),
+        untilMode: 'specific',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: false,
+    });
+  });
+
+  it('8) relative : relative return default', () => {
+    const SEVEN_DAYS_AGO = new Date();
+    SEVEN_DAYS_AGO.setUTCHours(0, 0, 0, 0);
+
+    const MIDNIGHT = new Date();
+    MIDNIGHT.setUTCHours(0, 0, 0, 0);
+    expect(
+      customTimeRangeDecode(
+        'DATEADD(DATETIME("2021-01-26T00:00:00"), -55, day) : DATEADD(DATETIME("2021-01-27T00:00:00"), 7, day)',
+      ),
+    ).toEqual({
+      customRange: {
+        sinceDatetime: SEVEN_DAYS_AGO.setUTCDate(
+          SEVEN_DAYS_AGO.getUTCDate() - 7,
+        ).toString(),
+        sinceMode: 'relative',
+        sinceGrain: 'day',
+        sinceGrainValue: -7,
+        untilDatetime: MIDNIGHT.toString(),
+        untilMode: 'specific',
+        untilGrain: 'day',
+        untilGrainValue: 7,
+        anchorMode: 'now',
+        anchorValue: 'now',
+      },
+      matchedFlag: false,
+    });
+  });
+});
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
index 964761f6b9..df1e2bc7d9 100644
--- a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts
@@ -52,6 +52,7 @@ test('generates a readable time range', () => {
   expect(formatTimeRange(' : 2020-07-30T00:00:00')).toBe(
     '-∞ ≤ col < 2020-07-30',
   );
+  expect(formatTimeRange('')).toBe('');
 });
 
 test('returns a formatted time range from response', async () => {
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/getTimeOffset.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/getTimeOffset.test.ts
index 9b14e0062c..e8a2fb7019 100644
--- a/superset-frontend/packages/superset-ui-core/test/time-comparison/getTimeOffset.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/getTimeOffset.test.ts
@@ -16,31 +16,1015 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 import { getTimeOffset } from '@superset-ui/core';
+import timezoneMock from 'timezone-mock';
+
+// NOW will be set at midnight 2024-06-03 and transforme dfrom local timezone to UTC
+const NOW_IN_UTC = '2024-06-03T00:00:00Z';
+const NOW_UTC_IN_EUROPE = '2024-06-02T22:00:00Z'; // Same as 2024-06-03T00:00:00+02:00
+const NOW_UTC_IN_PACIFIC = '2024-06-03T08:00:00Z'; // Same as 2024-06-03T00:00:00-08:00
+
+afterEach(() => {
+  timezoneMock.unregister();
+  jest.useRealTimers();
+});
+
+const runTimezoneTest = (
+  now_time: string,
+  timezone: any,
+  timeRangeFilter: any,
+  shifts: any,
+  startDate: any,
+  expected_result: string[],
+  includeFutureOffsets = true,
+) => {
+  jest.setSystemTime(new Date(now_time));
+  timezoneMock.register(timezone);
+  const result = getTimeOffset({
+    timeRangeFilter,
+    shifts,
+    startDate,
+    includeFutureOffsets,
+  });
+  expect(result).toEqual(expected_result);
+  timezoneMock.unregister();
+};
+
+test('should handle includeFutureOffsets is null', () => {
+  jest.useFakeTimers('modern');
+  jest.setSystemTime(new Date(NOW_UTC_IN_EUROPE));
+  timezoneMock.register('Etc/GMT-2');
+  const result = getTimeOffset({
+    timeRangeFilter: {
+      comparator: '2024-06-03 : 2024-06-10',
+    },
+    shifts: ['custom'],
+    startDate: '2024-05-29',
+  });
+  expect(result).toEqual(['5 days ago']);
+  timezoneMock.unregister();
+});
 
-test('handles custom shifts', () => {
+test('should handle custom range with specific dates', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-03 : 2024-06-10',
+  };
   const shifts = ['custom'];
-  const startDate = '2023-01-01';
-  const timeRangeFilter = { comparator: '2023-01-03 : 2023-01-10' };
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
 
-  const result = getTimeOffset(timeRangeFilter, shifts, startDate);
-  expect(result).toEqual(['2 days ago']);
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '5 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
 });
 
-test('handles inherit shifts', () => {
+test('should handle custom range with relative dates (now)', () => {
+  const timeRangeFilter = {
+    comparator: 'now : 2024-06-10',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-30';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '4 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+});
+
+test('should handle inherit shift', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-01 : 2024-03-08',
+  };
   const shifts = ['inherit'];
-  const startDate = '';
-  const timeRangeFilter = { comparator: '2023-01-03 : 2023-01-10' };
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['7 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '7 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['7 days ago'],
+  );
+});
+
+test('should handle custom and inherit shifts', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-03 : 2024-06-10',
+  };
+  const shifts = ['custom', 'inherit'];
+  const startDate = '2024-05-28';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago', '7 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '6 days ago',
+    '7 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago', '7 days ago'],
+  );
+});
+
+test('should handle no shifts', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-01 : 2024-03-05',
+  };
+  const shifts: any = [];
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, []);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+});
+
+test('should handle null timeRangeFilter', () => {
+  const timeRangeFilter = null;
+  const shifts = ['custom'];
+  const startDate = '2024-06-01';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '2 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+});
+
+test('should handle predefined shifts', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-01 : 2024-03-05',
+  };
+  const shifts: any = ['1 year ago'];
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['1 year ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '1 year ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['1 year ago'],
+  );
+});
+
+test('should handle custom range with DATEADD function', () => {
+  const timeRangeFilter = {
+    comparator:
+      'DATEADD(DATETIME("2024-05-30T00:00:00"), -7, day) : 2024-05-30T00:00:00',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-21';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '2 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+});
+
+test('should handle custom range with DATEADD function and relative start date', () => {
+  const timeRangeFilter = {
+    comparator: 'now : DATEADD(DATETIME("2024-06-03T00:00:00"), 7, day)',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-06-01';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '2 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+});
+
+test('should handle custom range with DATEADD function and relative end date', () => {
+  const timeRangeFilter = {
+    comparator: 'DATEADD(DATETIME("now"), -7, day) : now',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-23';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '4 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+});
+
+test('should handle custom range with specific date and relative end date', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-01T00:00:00 : now',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-23';
+  jest.useFakeTimers('modern');
 
-  const result = getTimeOffset(timeRangeFilter, shifts, startDate);
-  expect(result).toEqual(['7 days ago']);
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['9 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '9 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['9 days ago'],
+  );
 });
 
-test('handles no custom or inherit shifts', () => {
-  const shifts = ['1 week ago'];
-  const startDate = '';
-  const timeRangeFilter = { comparator: '2023-01-03 : 2023-01-10' };
+test('should handle custom range with specific date and specific end date', () => {
+  const timeRangeFilter = {
+    comparator: 'now : 2024-06-10T00:00:00',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-06-01';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '2 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+});
+
+test('should handle custom range with Last and now', () => {
+  const timeRangeFilter = {
+    comparator: 'Last day : now',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-30';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['3 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '3 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['3 days ago'],
+  );
+});
+
+test('should handle custom range with Last week', () => {
+  const timeRangeFilter = {
+    comparator: 'Last week',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-21';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '6 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago'],
+  );
+});
+
+test('should handle custom range with previous calendar week', () => {
+  const timeRangeFilter = {
+    comparator: 'previous calendar week',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-26';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024-06-05T02:06:00+02:00',
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['1 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-05T00:06:00Z',
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['1 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-04T16:06:00-08:00',
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['1 days ago'],
+  );
+});
+
+test('should handle custom range with previous calendar month', () => {
+  const timeRangeFilter = {
+    comparator: 'previous calendar month',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-04-26';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024-06-05T02:06:00+02:00',
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-05T00:06:00Z',
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-04T16:06:00-08:00',
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+});
+
+test('should handle custom range with previous calendar year', () => {
+  const timeRangeFilter = {
+    comparator: 'previous calendar year',
+  };
+  const shifts = ['custom'];
+  const startDate = '2022-12-26';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    '2024-06-05T02:06:00+02:00',
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-05T00:06:00Z',
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-04T16:06:00-08:00',
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['6 days ago'],
+  );
+});
+
+test('should handle custom range with Advanced 2022-11-01', () => {
+  const timeRangeFilter = {
+    comparator: '2022-11-01 : 2022-11-15',
+  };
+  const shifts = ['custom'];
+  const startDate = '2022-10-18';
+  jest.useFakeTimers('modern');
 
-  const result = getTimeOffset(timeRangeFilter, shifts, startDate);
-  expect(result).toEqual(['1 week ago']);
+  runTimezoneTest(
+    '2024-06-05T02:06:00+02:00',
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['14 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-05T00:06:00Z',
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['14 days ago'],
+  );
+  runTimezoneTest(
+    '2024-06-04T16:06:00-08:00',
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['14 days ago'],
+  );
+});
+
+test('should handle future inherit shift with includeFutureOffsets set to true', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-10 : 2024-06-05',
+  };
+  const shifts = ['inherit'];
+  const startDate = '2024-06-20';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+    undefined,
+  );
+  runTimezoneTest(
+    NOW_IN_UTC,
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+    undefined,
+  );
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+    undefined,
+  );
+});
+
+test('should handle future custom shift with includeFutureOffsets set to true', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-10 : 2024-06-05',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-06-15';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '5 days after',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+  );
+});
+
+test('should handle custom range with specific (YYYY-MM) and relative dates', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06 : DATEADD(DATETIME("2024-06-03T00:00:00"), 7, day)',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['3 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '3 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days ago'],
+  );
+});
+
+test('should handle custom range with minutes', () => {
+  const timeRangeFilter = {
+    comparator: '10 minutes ago : now',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '5 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days ago'],
+  );
+});
+
+test('should handle custom range with undefined startDate', () => {
+  const timeRangeFilter = {
+    comparator: '',
+  };
+  const shifts = ['custom'];
+  const startDate = undefined;
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, []);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+});
+
+test('should handle future custom shift with different format', () => {
+  const timeRangeFilter = {
+    comparator: '2024-06-10T23:45:30-05:00 : 2024-06-05',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-06-15';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days after'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '4 days after',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['5 days after'],
+  );
+});
+
+test('should handle custom range with relative dates', () => {
+  const timeRangeFilter = {
+    comparator:
+      'DATEADD(DATETIME("now"), -7, day) : DATEADD(DATETIME("now"), 7, day)',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days after'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '2 days after',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['2 days after'],
+  );
+});
+
+test('should handle custom range with relative dates (minute and seconds)', () => {
+  const timeRangeFilter = {
+    comparator:
+      'DATEADD(DATETIME("now"), -700, minute) : DATEADD(DATETIME("now"), 7000, second)',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '4 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+});
+
+test('should handle custom range with relative dates (hour)', () => {
+  const timeRangeFilter = {
+    comparator:
+      'DATEADD(DATETIME("now"), -24, hour) : DATEADD(DATETIME("now"), 24, hour)',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '4 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['4 days ago'],
+  );
+});
+
+test('should handle custom shifts with same day', () => {
+  const timeRangeFilter = {
+    comparator: '2024-05-29 : 2024-05-29',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['0 days ago'],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, [
+    '0 days ago',
+  ]);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    ['0 days ago'],
+  );
+});
+
+test('should handle inherit shifts without filter', () => {
+  const timeRangeFilter = {
+    comparator: ':',
+  };
+  const shifts = ['inherit'];
+  const startDate = '2024-05-29';
+  jest.useFakeTimers('modern');
+
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+  runTimezoneTest(NOW_IN_UTC, 'UTC', timeRangeFilter, shifts, startDate, []);
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+  );
+});
+
+test('should handle inherit shift same day', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-06 : 2024-03-06',
+  };
+  const shifts = ['inherit'];
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    true,
+  );
+  runTimezoneTest(
+    NOW_IN_UTC,
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    true,
+  );
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    true,
+  );
+});
+
+test('should handle inherit shift same day includeFutureOffsets set to false', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-06 : 2024-03-01',
+  };
+  const shifts = ['inherit'];
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
+  runTimezoneTest(
+    NOW_IN_UTC,
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
+});
+
+test('should handle custom shift same day includeFutureOffsets set to false', () => {
+  const timeRangeFilter = {
+    comparator: '2024-03-01 : 2024-03-01',
+  };
+  const shifts = ['custom'];
+  const startDate = '2024-03-06';
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
+  runTimezoneTest(
+    NOW_IN_UTC,
+    'UTC',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
+  runTimezoneTest(
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    timeRangeFilter,
+    shifts,
+    startDate,
+    [],
+    false,
+  );
 });
diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/parseDttmToDate.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/parseDttmToDate.test.ts
index 30c5d285bc..22eef5d6f4 100644
--- a/superset-frontend/packages/superset-ui-core/test/time-comparison/parseDttmToDate.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/parseDttmToDate.test.ts
@@ -16,116 +16,655 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 import { parseDttmToDate } from '@superset-ui/core';
+import timezoneMock from 'timezone-mock';
+
+// NOW will be set at midnight 2024-06-03 and transforme dfrom local timezone to UTC
+const NOW_IN_UTC = '2024-06-03T00:00:00Z';
+const NOW_UTC_IN_EUROPE = '2024-06-02T22:00:00Z'; // Same as 2024-06-03T00:00:00+02:00
+const NOW_UTC_IN_PACIFIC = '2024-06-03T08:00:00Z'; // Same as 2024-06-03T00:00:00-08:00
+
+afterEach(() => {
+  timezoneMock.unregister();
+  jest.useRealTimers();
+});
+
+const runTimezoneTest = (
+  eval_time: string,
+  now_time: string,
+  timezone: any,
+  expected_result: Date | null,
+  endDate = false,
+  computingShift = false,
+) => {
+  jest.setSystemTime(new Date(now_time));
+  timezoneMock.register(timezone);
+  expect(parseDttmToDate(eval_time, endDate, computingShift)).toEqual(
+    expected_result,
+  );
+  timezoneMock.unregister();
+};
+
+test('should return the current date for "now"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'now',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-03T00:00:00+02:00'),
+  );
+  runTimezoneTest('now', NOW_IN_UTC, 'UTC', new Date('2024-06-03T00:00:00Z'));
+  runTimezoneTest(
+    'now',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T00:00:00-08:00'),
+  );
+});
+
+test('should return the current date for "today"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'today',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-03T00:00:00+02:00'),
+  );
+  runTimezoneTest('today', NOW_IN_UTC, 'UTC', new Date('2024-06-03T00:00:00Z'));
+  runTimezoneTest(
+    'today',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T00:00:00-08:00'),
+  );
+});
+
+test('should return the current date for "No filter"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'No filter',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-03T00:00:00+02:00'),
+  );
+  runTimezoneTest(
+    'No filter',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'No filter',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T00:00:00-08:00'),
+  );
+});
+
+test('should return the current date for an empty string', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-03T00:00:00+02:00'),
+  );
+  runTimezoneTest('', NOW_IN_UTC, 'UTC', new Date('2024-06-03T00:00:00Z'));
+  runTimezoneTest(
+    '',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T00:00:00-08:00'),
+  );
+});
+
+test('should return yesterday date for "Last day"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last day',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-01T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last day',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-02T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last day',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-02T08:00:00Z'),
+  );
+});
+
+test('should return the date one week ago for "Last week"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last week',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-05-26T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last week',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-05-27T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last week',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-05-27T08:00:00Z'),
+  );
+});
+
+test('should return the date one month ago for "Last month"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last month',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-05-02T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last month',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-05-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last month',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-05-03T08:00:00Z'),
+  );
+});
+
+test('should return the date three months ago for "Last quarter"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last quarter',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-03-02T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last quarter',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-03-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last quarter',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-03-03T08:00:00Z'),
+  );
+});
+
+test('should return the date one year ago for "Last year"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last year',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2023-06-02T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last year',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2023-06-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'Last year',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2023-06-03T08:00:00Z'),
+  );
+});
+
+test('should return the date for "previous calendar week"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar week',
+    '2024-06-04T22:00:00Z',
+    'Etc/GMT-2',
+    new Date('2024-05-26T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar week',
+    '2024-06-05T00:00:00Z',
+    'UTC',
+    new Date('2024-05-27T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar week',
+    '2024-06-05T08:00:00Z',
+    'Etc/GMT+8',
+    new Date('2024-05-27T08:00:00Z'),
+  );
+});
+
+test('should return the date for "previous calendar month"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-04-30T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-05-01T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-05-01T08:00:00Z'),
+  );
+});
+
+test('should return the date for "previous calendar year"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2022-12-31T22:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2023-01-01T00:00:00Z'),
+  );
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2023-01-01T08:00:00Z'),
+  );
+});
+
+test('should return the date for "1 day ago"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '1 day ago',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-01T22:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 day ago',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-02T00:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 day ago',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-02T08:00:00Z'),
+  );
+});
+
+test('should return the date for "1 week ago"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '1 week ago',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-05-26T22:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 week ago',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-05-27T00:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 week ago',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-05-27T08:00:00Z'),
+  );
+});
+
+test('should return the date for "1 month ago"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '1 month ago',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-05-02T22:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 month ago',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-05-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 month ago',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-05-03T08:00:00Z'),
+  );
+});
+
+test('should return the date for "1 year ago"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '1 year ago',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2023-06-02T22:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 year ago',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2023-06-03T00:00:00Z'),
+  );
+  runTimezoneTest(
+    '1 year ago',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2023-06-03T08:00:00Z'),
+  );
+});
+
+test('should return the date for "2024-03-09"', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024-03-09',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-03-08T22:00:00.000Z'),
+  );
+  runTimezoneTest(
+    '2024-03-09',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-03-09T00:00:00.000Z'),
+  );
+  runTimezoneTest(
+    '2024-03-09',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-03-09T08:00:00.000Z'),
+  );
+});
+
+test('should return the current date for "Last day" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last day',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-02T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last day',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last day',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the current date for "Last week" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last week',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-02T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last week',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last week',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the current date for "Last quarter" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last quarter',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-02T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last quarter',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last quarter',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the current date for "Last year" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'Last year',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-02T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last year',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'Last year',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the date for "previous calendar week" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar week',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-06-02T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar week',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-03T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar week',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-03T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the date for "previous calendar month" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-05-31T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-06-01T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar month',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-06-01T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the date for "previous calendar year" with isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2023-12-31T22:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-01-01T00:00:00Z'),
+    true,
+  );
+  runTimezoneTest(
+    'previous calendar year',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-01-01T08:00:00Z'),
+    true,
+  );
+});
+
+test('should return the date for "2024" with parts.length === 1', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2023-12-31T22:00:00.000Z'),
+  );
+  runTimezoneTest('2024', NOW_IN_UTC, 'UTC', new Date('2024-01-01T00:00:00Z'));
+  runTimezoneTest(
+    '2024',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2023-12-31T08:00:00.000Z'),
+  );
+});
+
+test('should return the date for "2024-03" with parts.length === 2', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024-03',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-02-29T22:00:00.000Z'),
+  );
+  runTimezoneTest(
+    '2024-03',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-03-01T00:00:00Z'),
+  );
+  runTimezoneTest(
+    '2024-03',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-02-29T08:00:00.000Z'),
+  );
+});
+
+test('should return the date for "2024-03-06" with parts.length === 3', () => {
+  jest.useFakeTimers('modern');
+  runTimezoneTest(
+    '2024-03-06',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    new Date('2024-03-05T22:00:00.000Z'),
+  );
+  runTimezoneTest(
+    '2024-03-06',
+    NOW_IN_UTC,
+    'UTC',
+    new Date('2024-03-06T00:00:00.000Z'),
+  );
+  runTimezoneTest(
+    '2024-03-06',
+    NOW_UTC_IN_PACIFIC,
+    'Etc/GMT+8',
+    new Date('2024-03-06T08:00:00.000Z'),
+  );
+});
+
+test('should return the date for "2024-03-06" with computingShifts true', () => {
+  jest.useFakeTimers('modern');
+  const expectedDate = new Date('2024-03-06T22:00:00Z');
+  expectedDate.setHours(-expectedDate.getTimezoneOffset() / 60, 0, 0, 0);
+  runTimezoneTest(
+    '2024-03-06',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    expectedDate,
+    false,
+    true,
+  );
+});
 
-test('should handle "now"', () => {
-  const now = parseDttmToDate('now');
-  const expected = new Date();
-  expected.setUTCHours(0, 0, 0, 0);
-  expect(expected).toEqual(now);
-});
-
-test('should handle "today" and "No filter"', () => {
-  const today = parseDttmToDate('today');
-  const noFilter = parseDttmToDate('No filter');
-  const expected = new Date();
-  expected.setUTCHours(0, 0, 0, 0);
-  expect(today).toEqual(expected);
-  expect(noFilter).toEqual(expected);
-});
-
-test('should handle relative time strings', () => {
-  const lastWeek = parseDttmToDate('Last week');
-  const lastMonth = parseDttmToDate('Last month');
-  const lastQuarter = parseDttmToDate('Last quarter');
-  const lastYear = parseDttmToDate('Last year');
-  let now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCDate(now.getUTCDate() - 7);
-  expect(lastWeek).toEqual(now);
-
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCMonth(now.getUTCMonth() - 1);
-  now.setUTCDate(1);
-  expect(lastMonth).toEqual(now);
-
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCMonth(now.getUTCMonth() - 3);
-  now.setUTCDate(1);
-  expect(lastQuarter).toEqual(now);
-
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCFullYear(now.getUTCFullYear() - 1);
-  now.setUTCDate(1);
-  expect(lastYear).toEqual(now);
-});
-
-test('should handle previous calendar units', () => {
-  let now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCDate(now.getUTCDate() - now.getUTCDay());
-  const previousWeek = parseDttmToDate('previous calendar week');
-  expect(previousWeek).toEqual(now);
-
-  now = new Date();
-  now.setUTCMonth(now.getUTCMonth() - 1, 1);
-  now.setUTCHours(0, 0, 0, 0);
-  const previousMonth = parseDttmToDate('previous calendar month');
-  expect(previousMonth).toEqual(now);
-
-  now = new Date();
-  now.setUTCFullYear(now.getUTCFullYear() - 1, 0, 1);
-  now.setUTCHours(0, 0, 0, 0);
-  const previousYear = parseDttmToDate('previous calendar year');
-  expect(previousYear).toEqual(now);
-});
-
-test('should handle dynamic "ago" times', () => {
-  const fiveDaysAgo = parseDttmToDate('5 days ago');
-  const fiveDayAgo = parseDttmToDate('5 day ago');
-  let now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCDate(now.getUTCDate() - 5);
-  expect(fiveDaysAgo).toEqual(now);
-  expect(fiveDayAgo).toEqual(now);
-
-  const weeksAgo = parseDttmToDate('7 weeks ago');
-  const weekAgo = parseDttmToDate('7 week ago');
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCDate(now.getUTCDate() - 7 * 7);
-  expect(weeksAgo).toEqual(now);
-  expect(weekAgo).toEqual(now);
-
-  const fiveMonthsAgo = parseDttmToDate('5 months ago');
-  const fiveMonthAgo = parseDttmToDate('5 month ago');
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCMonth(now.getUTCMonth() - 5);
-  expect(fiveMonthsAgo).toEqual(now);
-  expect(fiveMonthAgo).toEqual(now);
-
-  const fiveYearsAgo = parseDttmToDate('5 years ago');
-  const fiveYearAgo = parseDttmToDate('5 year ago');
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  now.setUTCFullYear(now.getUTCFullYear() - 5);
-  expect(fiveYearsAgo).toEqual(now);
-  expect(fiveYearAgo).toEqual(now);
-
-  // default case
-  const fiveHoursAgo = parseDttmToDate('5 hours ago');
-  now = new Date();
-  now.setUTCHours(0, 0, 0, 0);
-  expect(fiveHoursAgo).toEqual(now);
-});
-
-test('should parse valid moment strings', () => {
-  const specificDate = new Date('2023-01-01');
-  specificDate.setUTCHours(0, 0, 0, 0);
-  const parsedDate = parseDttmToDate('2023-01-01');
-  expect(parsedDate).toEqual(specificDate);
+test('should return the date for "2024-03-06" with computingShifts true and isEndDate true', () => {
+  jest.useFakeTimers('modern');
+  const expectedDate = new Date('2024-03-06T22:00:00Z');
+  expectedDate.setHours(-expectedDate.getTimezoneOffset() / 60, 0, 0, 0);
+  runTimezoneTest(
+    '2024-03-06',
+    NOW_UTC_IN_EUROPE,
+    'Etc/GMT-2',
+    expectedDate,
+    true,
+    true,
+  );
 });
diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js
index d0aed798de..2daed05f47 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js
@@ -93,7 +93,7 @@ function Chord(element, props) {
     .append('path')
     .attr('id', (d, i) => `group${i}`)
     .attr('d', arc)
-    .style('fill', (d, i) => colorFn(nodes[i], sliceId));
+    .style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));
 
   // Add a text label.
   const groupText = group.append('text').attr('x', 6).attr('dy', 15);
@@ -121,7 +121,7 @@ function Chord(element, props) {
     .on('mouseover', d => {
       chord.classed('fade', p => p !== d);
     })
-    .style('fill', d => colorFn(nodes[d.source.index], sliceId))
+    .style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
     .attr('d', path);
 
   // Add an elaborate mouseover title for each chord.
diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts
index 679e959707..031382a45e 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts
+++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts
@@ -33,18 +33,7 @@ const config: ControlPanelConfig = {
         ['metric'],
         ['adhoc_filters'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb b/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb
index d338a97e5d..0374b52cb0 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb	
+++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb	
@@ -42,7 +42,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 83,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -64,7 +64,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 84,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -74,7 +74,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 85,
    "metadata": {
     "colab": {
      "base_uri": "https://localhost:8080/"
@@ -135,7 +135,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 86,
    "metadata": {
     "colab": {
      "base_uri": "https://localhost:8080/"
@@ -143,15 +143,7 @@
     "id": "EL0e9DEVt1LT",
     "outputId": "16cd6450-d4a3-457a-b205-9797bbce33fc"
    },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "GeoJSON file for India downloaded and loaded successfully.\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "# Read Natural Earth data files into GeoDataFrames\n",
     "df_admin0_10m = gpd.read_file(f\"{data_dir}/ne_10m_admin_0_countries.zip\")\n",
@@ -159,22 +151,12 @@
     "df_50m = gpd.read_file(f\"{data_dir}/ne_50m_admin_1_states_provinces.zip\")\n",
     "\n",
     "# Convert column names to lowercase\n",
-    "df_admin0_10m.columns = df_admin0_10m.columns.str.lower()\n",
-    "\n",
-    "# Download and load the GeoJSON file for India\n",
-    "india_geojson_url = \"https://github.com/geohacker/india/raw/bcb920c7d3c686f01d085f7661c9ba89bf9bf65e/state/india_state_kashmir_ladakh.geojson\"\n",
-    "\n",
-    "try:\n",
-    "    india_gdf = gpd.read_file(india_geojson_url)\n",
-    "    print(\"GeoJSON file for India downloaded and loaded successfully.\")\n",
-    "except Exception as e:\n",
-    "    print(f\"Unable to download or load the GeoJSON file for India. Error: {str(e)}\")\n",
-    "    print(\"Please download the file from the URL and try again.\")"
+    "df_admin0_10m.columns = df_admin0_10m.columns.str.lower()"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 87,
    "metadata": {
     "id": "DUrz04nYt1LT",
     "outputId": "18d7cdb0-8ab6-4238-e50c-925c5dc117b0"
@@ -534,7 +516,7 @@
        "[9 rows x 121 columns]"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 87,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -545,7 +527,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 88,
    "metadata": {
     "id": "pr1jqM3kt1LU",
     "outputId": "7211a182-b64a-469b-fadb-af2148ec6852"
@@ -575,7 +557,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 89,
    "metadata": {
     "id": "k-KuZ8L4t1LU"
    },
@@ -597,7 +579,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 90,
    "metadata": {
     "id": "-4uH5XaEt1LU"
    },
@@ -876,7 +858,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 91,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -892,7 +874,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 92,
    "metadata": {
     "id": "tJ_WNQl8t1LU",
     "outputId": "4f601ce0-26e4-4a40-c36c-8449420e9406"
@@ -985,7 +967,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 93,
    "metadata": {
     "id": "xx8IbBKtt1LU",
     "outputId": "025139d2-ba0b-43a9-e2ec-f4608e6ecad2"
@@ -997,13 +979,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 11,
+     "execution_count": 93,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAF3CAYAAAAFEil7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB+qklEQVR4nO3dd3gUVRfA4d9sTd/0hIQk9N6kSaSISBFRUbCLFTvYsPLZG/YuWFFsKGJDBUF6r6H30BIghfRN3TrfH2g0Ekg22eymnPd58sDO3LlzZihz9s4tiqqqKkIIIYQQHqLxdgBCCCGEaFok+RBCCCGER0nyIYQQQgiPkuRDCCGEEB4lyYcQQgghPEqSDyGEEEJ4lCQfQgghhPAonbcD+C+n00laWhqBgYEoiuLtcIQQQghRDaqqUlhYSExMDBrNmds26l3ykZaWRlxcnLfDEEIIIUQNHD16lObNm5+xTL1L [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAF3CAYAAAAFEil7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB+qklEQVR4nO3dd3gUVRfA4d9sTd/0hIQk9N6kSaSISBFRUbCLFTvYsPLZG/YuWFFsKGJDBUF6r6H30BIghfRN3TrfH2g0Ekg22eymnPd58sDO3LlzZihz9s4tiqqqKkIIIYQQHqLxdgBCCCGEaFok+RBCCCGER0nyIYQQQgiPkuRDCCGEEB4lyYcQQgghPEqSDyGEEEJ4lCQfQgghhPAonbcD+C+n00laWhqBgYEoiuLtcIQQQghRDaqqUlhYSExMDBrNmds26l3ykZaWRlxcnLfDEEIIIUQNHD16lObNm5+xTL1L [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1019,7 +1001,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 94,
    "metadata": {
     "id": "ixC6KENXt1LU",
     "outputId": "8e63cb2d-d733-4a9f-caf6-ccc843f15b5d"
@@ -1031,13 +1013,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 12,
+     "execution_count": 94,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAFvCAYAAAAFeQD2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADGaklEQVR4nOzdd3wUdfrA8c/M1iSbnpBGCCX03nsHESyIgAW7nv1s6Fnu9Ge7O73Ts5fz7L2g2AUEpEnvndBJIL1uNsnWmd8fgQASQsruzm7yfb+MJLOzM89uNrvPfMvzlVRVVREEQRAEQRAEH5C1DkAQBEEQBEFovkSyKQiCIAiCIPiMSDYFQRAEQRAEnxHJpiAIgiAIguAzItkUBEEQBEEQfEYkm4IgCIIgCILPiGRTEARBEARB8BmRbAqCIAiCIAg+o9c6gD9SFIXs7GzCw8ORJEnrcARBEARBEIQ/UFWV8vJy [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAFvCAYAAAAFeQD2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADGaklEQVR4nOzdd3wUdfrA8c/M1iSbnpBGCCX03nsHESyIgAW7nv1s6Fnu9Ge7O73Ts5fz7L2g2AUEpEnvndBJIL1uNsnWmd8fgQASQsruzm7yfb+MJLOzM89uNrvPfMvzlVRVVREEQRAEQRAEH5C1DkAQBEEQBEFovkSyKQiCIAiCIPiMSDYFQRAEQRAEnxHJpiAIgiAIguAzItkUBEEQBEEQfEYkm4IgCIIgCILPiGRTEARBEARB8BmRbAqCIAiCIAg+o9c6gD9SFIXs7GzCw8ORJEnrcARBEARBEIQ/UFWV8vJy [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -1083,7 +1065,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 95,
    "metadata": {
     "id": "vN4Ngpe7t1LU",
     "outputId": "3bcdc612-cc01-49be-fe19-f6e08e833fca"
@@ -1240,7 +1222,7 @@
        "[3 rows x 51 columns]"
       ]
      },
-     "execution_count": 13,
+     "execution_count": 95,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1263,7 +1245,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 96,
    "metadata": {
     "id": "PP6E24eEt1LV",
     "outputId": "2621d5f1-1edc-42fc-e8df-8afd6a525cc6",
@@ -1276,13 +1258,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 14,
+     "execution_count": 96,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGQCAYAAAAOUwJBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADLoklEQVR4nOzddXgUV9vA4d/OStw9IUIIkEBwd3epUMPaUkrd5a0L/epGvaUtVUqhUKBQ3N3dISFK3HV3szvz/UFLmxLbZJNNyLmv673eZnfmzBOS7Dxz5DkqRVEUBEEQBEEQGohk6wAEQRAEQWheRPIhCIIgCEKDEsmHIAiCIAgNSiQfgiAIgiA0KJF8CIIgCILQoETyIQiCIAhCgxLJhyAIgiAIDUokH4IgCIIgNCiRfAiCIAiC0KBE8iEIgiAIQoPSWHLwq6++yuzZs8u91rZtW86ePQvA4MGD2bZtW7n37733 [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGQCAYAAAAOUwJBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADLoklEQVR4nOzddXgUV9vA4d/OStw9IUIIkEBwd3epUMPaUkrd5a0L/epGvaUtVUqhUKBQ3N3dISFK3HV3szvz/UFLmxLbZJNNyLmv673eZnfmzBOS7Dxz5DkqRVEUBEEQBEEQGohk6wAEQRAEQWheRPIhCIIgCEKDEsmHIAiCIAgNSiQfgiAIgiA0KJF8CIIgCILQoETyIQiCIAhCgxLJhyAIgiAIDUokH4IgCIIgNCiRfAiCIAiC0KBE8iEIgiAIQoPSWHLwq6++yuzZs8u91rZtW86ePQvA4MGD2bZtW7n37733 [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1327,7 +1309,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 97,
    "metadata": {
     "id": "LuNGgwiQt1LV"
    },
@@ -1348,7 +1330,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 98,
    "metadata": {
     "id": "v8ig_jQDt1LV",
     "outputId": "3f10b14d-dde2-46d9-f4f6-6f4311fb3e73"
@@ -1360,13 +1342,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 16,
+     "execution_count": 98,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAATsAAAJGCAYAAAAkiCJFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACpN0lEQVR4nOzddXRUR/vA8e/6xt2dBEhwd/eiLVqhXuru7fvW3elbo6XuRmkpFNri7q4hQBKIu29W7v39wa+0lEBsd+9uMp9zOAd27859QnafnXtn5hmVLMsygiAILZxa6QAEQRCcQSQ7QRBaBZHsBEFoFUSyEwShVRDJThCEVkEkO0EQWgWR7ARBaBW0Sgfwb5IkkZ2djY+PDyqVSulwBEFwcbIsU1FRQWRkJGr1+ftvLpfssrOziYmJUToMQRDczMmTJ4mOjj7v8y6X7Hx8fIDTgfv6+iocjSAIrq68vJyYmJgz [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAATsAAAJGCAYAAAAkiCJFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACpN0lEQVR4nOzddXRUR/vA8e/6xt2dBEhwd/eiLVqhXuru7fvW3elbo6XuRmkpFNri7q4hQBKIu29W7v39wa+0lEBsd+9uMp9zOAd27859QnafnXtn5hmVLMsygiAILZxa6QAEQRCcQSQ7QRBaBZHsBEFoFUSyEwShVRDJThCEVkEkO0EQWgWR7ARBaBW0Sgfwb5IkkZ2djY+PDyqVSulwBEFwcbIsU1FRQWRkJGr1+ftvLpfssrOziYmJUToMQRDczMmTJ4mOjj7v8y6X7Hx8fIDTgfv6+iocjSAIrq68vJyYmJgz [...]
       "text/plain": [
        "<Figure size 700x700 with 1 Axes>"
       ]
@@ -1406,7 +1388,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 99,
    "metadata": {
     "id": "LuNGgwiQt1LV"
    },
@@ -1417,13 +1399,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 17,
+     "execution_count": 99,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk0AAAGVCAYAAADjf8WHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADf+ElEQVR4nOzddXQU5xrA4d/Majbu7kACJLgFdymllAotLXV399uWeyvUS5UaLTUqQIXS4u7uTtC462Z15v5Bm5ZikU1mk8xzDueQ3ZF3k92ddz55P0GWZRmVSqVSqVQq1XmJSgegUqlUKpVK1RSoSZNKpVKpVCpVDahJk0qlUqlUKlUNqEmTSqVSqVQqVQ2oSZNKpVKpVCpVDahJk0qlUqlUKlUNqEmTSqVSqVQqVQ2oSZNKpVKpVCpVDWiVDuDfJEkiKysLb29vBEFQOhyVSqVSqVTNnCzLlJeXExERgSieuz3J [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk0AAAGVCAYAAADjf8WHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADf+ElEQVR4nOzddXQU5xrA4d/Majbu7kACJLgFdymllAotLXV399uWeyvUS5UaLTUqQIXS4u7uTtC462Z15v5Bm5ZikU1mk8xzDueQ3ZF3k92ddz55P0GWZRmVSqVSqVQq1XmJSgegUqlUKpVK1RSoSZNKpVKpVCpVDahJk0qlUqlUKlUNqEmTSqVSqVQqVQ2oSZNKpVKpVCpVDahJk0qlUqlUKlUNqEmTSqVSqVQqVQ2oSZNKpVKpVCpVDWiVDuDfJEkiKysLb29vBEFQOhyVSqVSqVTNnCzLlJeXExERgSieuz3J [...]
       "text/plain": [
        "<Figure size 700x700 with 1 Axes>"
       ]
@@ -1453,6 +1435,79 @@
     "not speed_run and ukraine_with_crimea.plot(figsize=(7,7), **plot_styles)"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### India\n",
+    "\n",
+    "- Jammu and Kashmir and Ladakh with ISO-3166 codes IN-JK and IN-LA respectively are new territories of India. They are part of the disputed region of Jammu and Kashmir."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 100,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "GeoJSON file for India downloaded and loaded successfully.\n"
+     ]
+    }
+   ],
+   "source": [
+    "india = df[df['admin'] == 'India']\n",
+    "india_copy = india.copy()\n",
+    "\n",
+    "# Download and load the GeoJSON file for India\n",
+    "india_geojson_url = \"https://github.com/geohacker/india/raw/bcb920c7d3c686f01d085f7661c9ba89bf9bf65e/state/india_state_kashmir_ladakh.geojson\"\n",
+    "\n",
+    "try:\n",
+    "    india_gdf = gpd.read_file(india_geojson_url)\n",
+    "    # Rename column to 'ST_ID' to 'iso_3166_2' for consistency\n",
+    "    india_gdf.rename(columns={'ST_ID': 'iso_3166_2'}, inplace=True)\n",
+    "    # Update the geometry for the states of Jammu and Kashmir and Ladakh\n",
+    "    india_copy.loc[india_copy['iso_3166_2'] == 'IN-JK', 'geometry'] = india_gdf[india_gdf['iso_3166_2'] == 'IN-JK'].dissolve(by='iso_3166_2').reset_index()[\"geometry\"].values\n",
+    "    india_copy.loc[india_copy['iso_3166_2'] == 'IN-LA', 'geometry'] = india_gdf[india_gdf['iso_3166_2'] == 'IN-LA'].dissolve(by='iso_3166_2').reset_index()[\"geometry\"].values\n",
+    "    print(\"GeoJSON file for India downloaded and loaded successfully.\")\n",
+    "except Exception as e:\n",
+    "    \n",
+    "    print(f\"Unable to download or load the GeoJSON file for India. Error: {str(e)}\")\n",
+    "    print(\"Please download the file from the URL and try again.\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 138,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<Axes: >"
+      ]
+     },
+     "execution_count": 138,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAGdCAYAAAD31gOpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACVv0lEQVR4nOzddXyT1/7A8U88Td29VKlQiru7DRtzd3f5Te62u9257+5uY8aYMoEpE3S4u1OgpbSl7hp9nt8fHbCOllrSpuW8X6++tiRPzvmGpt+cHFXIsiwjCIIgdEnKjg5AEARBcByR5AVBELowkeQFQRC6MJHkBUEQujCR5AVBELowkeQFQRC6MJHkBUEQujCR5AVBELowdUcH8E+SJJGTk4O7uzsKhaKjwxEEQXA6sixTWVlJSEgISuW52+pOl+RzcnIIDw/v6DAEQRCcXlZWFmFhYee8xumSvLu7O1AXvIeH [...]
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "not speed_run and india_copy.plot(**plot_styles)"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {
@@ -1466,7 +1521,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 102,
    "metadata": {
     "id": "8zBzSIqQt1LV",
     "outputId": "cc8b6fbf-accb-44ba-b80a-a837df398c96"
@@ -1478,13 +1533,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 18,
+     "execution_count": 102,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAKwAAAGdCAYAAACPYJhkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAliklEQVR4nO3de1yTdf/H8fcG2wBhYxwHigiooOkdiopInpIgtPLAT9PMs1CG5vkWbkOrO8Ow20rLU7eplSU3mZlmlqJxV6KW5gELPMdxAwU25DDG9v394e1VS7SRjPHFz/PxuB4Pd+3i2vdiLy+vze26RIwxBkI4Ibb1AAhpCgqWcIWCJVyhYAlXKFjCFQqWcIWCJVyhYAlX7G09gHtlMplQXFwMFxcXiEQiWw+H/EWMMVRVVcHX1xdi8Z33o9wHW1xcDD8/P1sPgzSTgoICdOjQ4Y73cx+si4sLgJsbKpfLbTwa [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAKwAAAGdCAYAAACPYJhkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAliklEQVR4nO3de1yTdf/H8fcG2wBhYxwHigiooOkdiopInpIgtPLAT9PMs1CG5vkWbkOrO8Ow20rLU7eplSU3mZlmlqJxV6KW5gELPMdxAwU25DDG9v394e1VS7SRjPHFz/PxuB4Pd+3i2vdiLy+vze26RIwxBkI4Ibb1AAhpCgqWcIWCJVyhYAlXKFjCFQqWcIWCJVyhYAlX7G09gHtlMplQXFwMFxcXiEQiWw+H/EWMMVRVVcHX1xdi8Z33o9wHW1xcDD8/P1sPgzSTgoICdOjQ4Y73cx+si4sLgJsbKpfLbTwa [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1500,7 +1555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 103,
    "metadata": {
     "id": "-LXcKKOjt1LV",
     "outputId": "546a286e-9682-4f9a-c57e-b19250d88a34"
@@ -1512,13 +1567,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 19,
+     "execution_count": 103,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhIAAAGdCAYAAABHM5ovAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADIsElEQVR4nOzddXQU19vA8e/uJtm4u4cIJCQhggd3dwq0UErdhSrUqEGFKnXaAi1aihaKuxPcAhFC3G2zm2R13j94G34pGggEuZ9zOKfZmbn3mW2y8+xVmSRJEoIgCIIgCNdB3tgBCIIgCIJw5xKJhCAIgiAI100kEoIgCIIgXDeRSAiCIAiCcN1EIiEIgiAIwnUTiYQgCIIgCNdNJBKCIAiCIFw3kUgIgiAIgnDdzBo7gP8ymUzk5uZiZ2eHTCZr7HAEQRAE4Z4kSRKVlZV4e3sjl1++3eG2SyRyc3Px8/Nr7DAE [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhIAAAGdCAYAAABHM5ovAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADIsElEQVR4nOzddXQU19vA8e/uJtm4u4cIJCQhggd3dwq0UErdhSrUqEGFKnXaAi1aihaKuxPcAhFC3G2zm2R13j94G34pGggEuZ9zOKfZmbn3mW2y8+xVmSRJEoIgCIIgCNdB3tgBCIIgCIJw5xKJhCAIgiAI100kEoIgCIIgXDeRSAiCIAiCcN1EIiEIgiAIwnUTiYQgCIIgCNdNJBKCIAiCIFw3kUgIgiAIgnDdzBo7gP8ymUzk5uZiZ2eHTCZr7HAEQRAE4Z4kSRKVlZV4e3sjl1++3eG2SyRyc3Px8/Nr7DAE [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1548,7 +1603,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 104,
    "metadata": {
     "id": "mznw0XOgt1LV",
     "outputId": "7e8085bc-abd9-4592-f047-62fa1a45eb01"
@@ -1560,13 +1615,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 20,
+     "execution_count": 104,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAFUCAYAAAAOBceiAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA/w0lEQVR4nO3deVhU9f4H8PcsMKwz7IuyKSCIigul4q6BplZW2nJLbfFa9vO2aIvZZuZVvGrZqtlmdsssLVxSI7VATUVFUdzFVJBdgRkcmP38/uhGkaAMDMwZeL+eh+dxzvme73zOeUbmzTnf8z0SQRAEEBEREYmI1N4FEBEREf0dAwoRERGJDgMKERERiQ4DChEREYkOAwoRERGJDgMKERERiQ4DChEREYkOAwoRERGJjtzeBfydxWJBYWEhPD09IZFI7F0OERERNYIgCKiqqkKHDh0glTb//IfoAkphYSFCQ0Pt [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAFUCAYAAAAOBceiAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA/w0lEQVR4nO3deVhU9f4H8PcsMKwz7IuyKSCIigul4q6BplZW2nJLbfFa9vO2aIvZZuZVvGrZqtlmdsssLVxSI7VATUVFUdzFVJBdgRkcmP38/uhGkaAMDMwZeL+eh+dxzvme73zOeUbmzTnf8z0SQRAEEBEREYmI1N4FEBEREf0dAwoRERGJDgMKERERiQ4DChEREYkOAwoRERGJDgMKERERiQ4DChEREYkOAwoRERGJjtzeBfydxWJBYWEhPD09IZFI7F0OERERNYIgCKiqqkKHDh0glTb//IfoAkphYSFCQ0Pt [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1582,7 +1637,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 105,
    "metadata": {
     "id": "yfAO1qFrt1LV",
     "outputId": "9151ce8f-2412-415b-da73-eeec613276d8"
@@ -1594,13 +1649,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 21,
+     "execution_count": 105,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAHRCAYAAAAluXLzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB6FklEQVR4nO3dd3xT5f4H8M9J0iRt06R70QEddFjKHgVkTwFRENyT68R9f17k6nV7QUHcoiIqXkUUFAcICLI3lF1mCx10z6Qz8/z+QKuVQps2p0nL531ffV1zcs7zfHNE8ulzznkeQRRFEUREREREEpA5uwAiIiIi6rgYNomIiIhIMgybRERERCQZhk0iIiIikgzDJhERERFJhmGTiIiIiCTDsElEREREklE4u4C/s9lsyMvLg5eXFwRBcHY5RERERPQ3oiiisrISoaGhkMkuP3bpcmEzLy8P4eHhzi6DiIiIiJqQ [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAHRCAYAAAAluXLzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB6FklEQVR4nO3dd3xT5f4H8M9J0iRt06R70QEddFjKHgVkTwFRENyT68R9f17k6nV7QUHcoiIqXkUUFAcICLI3lF1mCx10z6Qz8/z+QKuVQps2p0nL531ffV1zcs7zfHNE8ulzznkeQRRFEUREREREEpA5uwAiIiIi6rgYNomIiIhIMgybRERERCQZhk0iIiIikgzDJhERERFJhmGTiIiIiCTDsElEREREklE4u4C/s9lsyMvLg5eXFwRBcHY5RERERPQ3oiiisrISoaGhkMkuP3bpcmEzLy8P4eHhzi6DiIiIiJqQ [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -1629,7 +1684,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 106,
    "metadata": {
     "id": "bbyDCO0Qt1LV",
     "outputId": "f2a0594d-999b-4573-d008-5158f898a1c6"
@@ -1641,13 +1696,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 22,
+     "execution_count": 106,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdEAAAGdCAYAAABNWUmMAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACFAUlEQVR4nOzdd3hU1dbA4d9MpqT3ThIghU7oJSBFpCOiqFjBXhAVwYpXr3otYG8I9i6CoCgWQDrSO1JDT0IqqZM2M5k55/uDz2gkIZnJhBBY7/Pw3Dun7LNmwFmzz9l7bY2qqipCCCGEcJi2sQMQQgghmipJokIIIYSTJIkKIYQQTpIkKoQQQjhJkqgQQgjhJEmiQgghhJMkiQohhBBOkiQqhBBCOEnX2AH8m6IoZGRk4OPjg0ajaexwhBBCXGRUVaW4uJjIyEi02rP3Nc+7JJqRkUF0dHRjhyGEEOIil5aWRlRU [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdEAAAGdCAYAAABNWUmMAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACFAUlEQVR4nOzdd3hU1dbA4d9MpqT3ThIghU7oJSBFpCOiqFjBXhAVwYpXr3otYG8I9i6CoCgWQDrSO1JDT0IqqZM2M5k55/uDz2gkIZnJhBBY7/Pw3Dun7LNmwFmzz9l7bY2qqipCCCGEcJi2sQMQQgghmipJokIIIYSTJIkKIYQQTpIkKoQQQjhJkqgQQgjhJEmiQgghhJMkiQohhBBOkiQqhBBCOEnX2AH8m6IoZGRk4OPjg0ajaexwhBBCXGRUVaW4uJjIyEi02rP3Nc+7JJqRkUF0dHRjhyGEEOIil5aWRlRU [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1663,7 +1718,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 107,
    "metadata": {
     "id": "yJ_Ueh7Rt1LV",
     "outputId": "16fe59db-4be4-4e02-d37b-3098bdfa945a"
@@ -1675,13 +1730,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 23,
+     "execution_count": 107,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAp0AAAGjCAYAAABuTa5+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADhpklEQVR4nOzdd3hU1dbA4d/U9N47ISEkIRB6770IIioo2LE3sIvlU6+9NyzYKyoqoIDSe+8lhBKSkN7bJJlMPef7I4oigbRJJoH9Ps99rpk5Z+89JJms2WUthSzLMoIgCIIgCILQgpT2HoAgCIIgCIJw8RNBpyAIgiAIgtDiRNApCIIgCIIgtDgRdAqCIAiCIAgtTgSdgiAIgiAIQosTQacgCIIgCILQ4kTQKQiCIAiCILQ4EXQKgiAIgiAILU5t7wH8lyRJ5Obm4ubmhkKhsPdwBEEQBEEQhP+QZZnKykqCg4NR [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAp0AAAGjCAYAAABuTa5+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADhpklEQVR4nOzdd3hU1dbA4d/U9N47ISEkIRB6770IIioo2LE3sIvlU6+9NyzYKyoqoIDSe+8lhBKSkN7bJJlMPef7I4oigbRJJoH9Ps99rpk5Z+89JJms2WUthSzLMoIgCIIgCILQgpT2HoAgCIIgCIJw8RNBpyAIgiAIgtDiRNApCIIgCIIgtDgRdAqCIAiCIAgtTgSdgiAIgiAIQosTQacgCIIgCILQ4kTQKQiCIAiCILQ4EXQKgiAIgiAILU5t7wH8lyRJ5Obm4ubmhkKhsPdwBEEQBEEQhP+QZZnKykqCg4NR [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -1709,7 +1764,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 108,
    "metadata": {
     "id": "60UpJMNwt1LV",
     "outputId": "1c9ff3fa-83e6-411e-9dc3-0c718ee97d39"
@@ -1721,13 +1776,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 24,
+     "execution_count": 108,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAChCAYAAABnAt39AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmHklEQVR4nO3ddXxW1R/A8c+T6+5mg1EjRo7uFBARCUVFREEUkbD4KSAYIJgYmISgokhJSIP0yNGxZGPdz+rp+/tjOpjbYIOxEef9evnS3Xvuuec8Q+73Ofec75FJkiQhCIIgCIJQQ+S13QBBEARBEB4sIvgQBEEQBKFGieBDEARBEIQaJYIPQRAEQRBqlAg+BEEQBEGoUSL4EARBEAShRongQxAEQRCEGiWCD0EQBEEQapSythvwX2azmaSkJOzs7JDJZLXdHEEQBEEQKkGSJPLy8vD29kYuv/HYxl0XfCQlJeHn [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAChCAYAAABnAt39AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABmHklEQVR4nO3ddXxW1R/A8c+T6+5mg1EjRo7uFBARCUVFREEUkbD4KSAYIJgYmISgokhJSIP0yNGxZGPdz+rp+/tjOpjbYIOxEef9evnS3Xvuuec8Q+73Ofec75FJkiQhCIIgCIJQQ+S13QBBEARBEB4sIvgQBEEQBKFGieBDEARBEIQaJYIPQRAEQRBqlAg+BEEQBEGoUSL4EARBEAShRongQxAEQRCEGiWCD0EQBEEQapSythvwX2azmaSkJOzs7JDJZLXdHEEQBEEQKkGSJPLy8vD29kYuv/HYxl0XfCQlJeHn [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1752,7 +1807,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": 109,
    "metadata": {
     "id": "rfBkQf78t1LW",
     "outputId": "8342e4b8-2483-4aac-8a79-e88d455297e2"
@@ -1764,13 +1819,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 25,
+     "execution_count": 109,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAABj0AAAMyCAYAAAA2aQPiAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUZdrH8e/UTM2kVwKEXgQRkaKoKPZeVsXX7rq7rr3r2nWtq6ur7trWvrp27F2xgQJSpHcINSE9mSTTz3n/YGUXaQkkmRB+n+vKRZg553nuE0gy89znuW+LaZomIiIiIiIiIiIiIiIiuzhrsgMQERERERERERERERFpDUp6iIiIiIiIiIiIiIhIp6Ckh4iIiIiIiIiIiIiIdApKeoiIiIiIiIiIiIiISKegpIeIiIiIiIiIiIiIiHQKSnqIiIiIiIiIiIiIiEinoKSHiIiIiIiIiIiIiIh0Ckp6 [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAABj0AAAMyCAYAAAA2aQPiAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUZdrH8e/UTM2kVwKEXgQRkaKoKPZeVsXX7rq7rr3r2nWtq6ur7trWvrp27F2xgQJSpHcINSE9mSTTz3n/YGUXaQkkmRB+n+vKRZg553nuE0gy89znuW+LaZomIiIiIiIiIiIiIiIiuzhrsgMQERERERERERERERFpDUp6iIiIiIiIiIiIiIhIp6Ckh4iIiIiIiIiIiIiIdApKeoiIiIiIiIiIiIiISKegpIeIiIiIiIiIiIiIiHQKSnqIiIiIiIiIiIiIiEinoKSHiIiIiIiIiIiIiIh0Ckp6 [...]
       "text/plain": [
        "<Figure size 2000x2000 with 1 Axes>"
       ]
@@ -1832,7 +1887,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": 110,
    "metadata": {},
    "outputs": [
     {
@@ -1841,13 +1896,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 26,
+     "execution_count": 110,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAD8CAYAAADXEsbUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADojUlEQVR4nOyddXhcVfrHP3c8E5m4e5pGmqapu3tLoVAoVnyBYosvywK7OOzCbxeXRYpTaKGlRt3dU5ckbdx9Mhm99/dHqITGZjKRsvPh6fOQO/eeezK58j3vec/3FSRJknDhwoULFy5cuOgkZF3dARcuXLhw4cLF/xYu8eHChQsXLly46FRc4sOFCxcuXLhw0am4xIcLFy5cuHDholNxiQ8XLly4cOHCRafiEh8uXLhw4cKFi07FJT5cuHDhwoULF52Koqs78HtEUaSgoABPT08EQejq7rhw4cKFCxcu2oAkSdTW [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAD8CAYAAADXEsbUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADojUlEQVR4nOyddXhcVfrHP3c8E5m4e5pGmqapu3tLoVAoVnyBYosvywK7OOzCbxeXRYpTaKGlRt3dU5ckbdx9Mhm99/dHqITGZjKRsvPh6fOQO/eeezK58j3vec/3FSRJknDhwoULFy5cuOgkZF3dARcuXLhw4cLF/xYu8eHChQsXLly46FRc4sOFCxcuXLhw0am4xIcLFy5cuHDholNxiQ8XLly4cOHCRafiEh8uXLhw4cKFi07FJT5cuHDhwoULF52Koqs78HtEUaSgoABPT08EQejq7rhw4cKFCxcu2oAkSdTW [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -1863,7 +1918,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": 111,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1918,7 +1973,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 28,
+   "execution_count": 112,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1933,7 +1988,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 29,
+   "execution_count": 113,
    "metadata": {},
    "outputs": [
     {
@@ -1942,13 +1997,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 29,
+     "execution_count": 113,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzYAAAFyCAYAAAAnGU8SAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADt9UlEQVR4nOzddXhUZ9rA4d+c8WTinpCQBEJCcLcixaEUWlraUnf33f267NZ9u+2WurtRKBWkuLu7E4gQ90kyGT3n+4OWlmKRSWYmee/r4mqZOfJMmJxznleeV6UoioIgCIIgCIIgCIIPkzwdgCAIgiAIgiAIQmOJxEYQBEEQBEEQBJ8nEhtBEARBEARBEHyeSGwEQRAEQRAEQfB5IrERBEEQBEEQBMHnicRGEARBEARBEASfJxIbQRAEQRAEQRB8nsbTAfyVLMvk5eUREBCASqXydDiCIAiCIAiCIHiIoihUVVUR [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzYAAAFyCAYAAAAnGU8SAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADt9UlEQVR4nOzddXhUZ9rA4d+c8WTinpCQBEJCcLcixaEUWlraUnf33f267NZ9u+2WurtRKBWkuLu7E4gQ90kyGT3n+4OWlmKRSWYmee/r4mqZOfJMmJxznleeV6UoioIgCIIgCIIgCIIPkzwdgCAIgiAIgiAIQmOJxEYQBEEQBEEQBJ8nEhtBEARBEARBEHyeSGwEQRAEQRAEQfB5IrERBEEQBEEQBMHnicRGEARBEARBEASfJxIbQRAEQRAEQRB8nsbTAfyVLMvk5eUREBCASqXydDiCIAiCIAiCIHiIoihUVVUR [...]
       "text/plain": [
        "<Figure size 1000x700 with 1 Axes>"
       ]
@@ -1972,7 +2027,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 30,
+   "execution_count": 114,
    "metadata": {
     "id": "AcT31Diyt1LW",
     "outputId": "cd6cc6ef-43ba-478e-b183-84eb7e003e17"
@@ -1984,13 +2039,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 30,
+     "execution_count": 114,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi0AAAFnCAYAAABqwnnlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0tUlEQVR4nO3de3QU9f3/8dfmspuEZDck5MIlgXANdxEEgqAg0YioWNF6VyxFwYAiVJFqpX6tBtF6KQpoi6itCKIVvCNChVbCLYgQMOFOAsmGa3ZDINkkO78//LltCliibDZDno9z5hz3M5OZ976N2Zezn5mxGIZhCAAAoIELCnQBAAAAZ4PQAgAATIHQAgAATIHQAgAATIHQAgAATIHQAgAATIHQAgAATCEk0AX8XF6vV0VFRYqKipLFYgl0OQAA4CwYhqGysjK1aNFCQUFndw7F9KGlqKhISUlJgS4DAAD8BIWF [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi0AAAFnCAYAAABqwnnlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0tUlEQVR4nO3de3QU9f3/8dfmspuEZDck5MIlgXANdxEEgqAg0YioWNF6VyxFwYAiVJFqpX6tBtF6KQpoi6itCKIVvCNChVbCLYgQMOFOAsmGa3ZDINkkO78//LltCliibDZDno9z5hz3M5OZ976N2Zezn5mxGIZhCAAAoIELCnQBAAAAZ4PQAgAATIHQAgAATIHQAgAATIHQAgAATIHQAgAATIHQAgAATCEk0AX8XF6vV0VFRYqKipLFYgl0OQAA4CwYhqGysjK1aNFCQUFndw7F9KGlqKhISUlJgS4DAAD8BIWF [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -2025,7 +2080,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 31,
+   "execution_count": 115,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2040,7 +2095,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 32,
+   "execution_count": 116,
    "metadata": {
     "id": "yjKX9Pbbt1LW",
     "outputId": "14caae01-b1b0-4775-a00e-a9e4f30fdf73"
@@ -2052,13 +2107,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 32,
+     "execution_count": 116,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAKRCAYAAAAbCWICAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzddXhT1//A8Xdu0rRN3b2lSoUKUtyHDNvYmAtzZ9t37u77zd03JkzYBmzosOHuUKxQSt29jd77+6MbrFCgkjQtnNfz7HnW5N5zTkubfHLk81EpiqIgCIIgCIIgCDYg2XsAgiAIgiAIwtlLBJuCIAiCIAiCzYhgUxAEQRAEQbAZEWwKgiAIgiAINiOCTUEQBEEQBMFmRLApCIIgCIIg2IwINgVBEARBEASbEcGmIAiCIAiCYDMaew/gRLIsk5+fj5ubGyqVyt7DEQRBEARBEE6gKAo1NTUEBwcjSaef [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAApsAAAKRCAYAAAAbCWICAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzddXhT1//A8Xdu0rRN3b2lSoUKUtyHDNvYmAtzZ9t37u77zd03JkzYBmzosOHuUKxQSt29jd77+6MbrFCgkjQtnNfz7HnW5N5zTkubfHLk81EpiqIgCIIgCIIgCDYg2XsAgiAIgiAIwtlLBJuCIAiCIAiCzYhgUxAEQRAEQbAZEWwKgiAIgiAINiOCTUEQBEEQBMFmRLApCIIgCIIg2IwINgVBEARBEASbEcGmIAiCIAiCYDMaew/gRLIsk5+fj5ubGyqVyt7DEQRBEARBEE6gKAo1NTUEBwcjSaef [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -2087,7 +2142,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 33,
+   "execution_count": 117,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2096,7 +2151,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 34,
+   "execution_count": 118,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2107,7 +2162,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 35,
+   "execution_count": 119,
    "metadata": {},
    "outputs": [
     {
@@ -2116,13 +2171,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 35,
+     "execution_count": 119,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk8AAAJGCAYAAABCyGGmAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADfhklEQVR4nOzddXRU19rH8e9IJu4e4gkECe5SvFBKobRQoVSpt1To7a339tZue+tu1KgLUAoV3Iu7B0gIcXcdO+8fvKXNxSIzOZnJ81krayVnjvwGkjPP2WefvTWKoigIIYQQQohG0aodQAghhBDCkUjxJIQQQgjRBFI8CSGEEEI0gRRPQgghhBBNIMWTEEIIIUQTSPEkhBBCCNEEUjwJIYQQQjSBXu0A/8tqtZKTk4O3tzcajUbtOEIIIYRoJxRFobKykoiICLTas7cvtbniKScnh6ioKLVjCCGEEKKdyszMJDIy [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk8AAAJGCAYAAABCyGGmAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADfhklEQVR4nOzddXRU19rH8e9IJu4e4gkECe5SvFBKobRQoVSpt1To7a339tZue+tu1KgLUAoV3Iu7B0gIcXcdO+8fvKXNxSIzOZnJ81krayVnjvwGkjPP2WefvTWKoigIIYQQQohG0aodQAghhBDCkUjxJIQQQgjRBFI8CSGEEEI0gRRPQgghhBBNIMWTEEIIIUQTSPEkhBBCCNEEUjwJIYQQQjSBXu0A/8tqtZKTk4O3tzcajUbtOEIIIYRoJxRFobKykoiICLTas7cvtbniKScnh6ioKLVjCCGEEKKdyszMJDIy [...]
       "text/plain": [
        "<Figure size 1000x700 with 1 Axes>"
       ]
@@ -2151,7 +2206,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 36,
+   "execution_count": 120,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2164,7 +2219,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 37,
+   "execution_count": 121,
    "metadata": {},
    "outputs": [
     {
@@ -2173,13 +2228,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 37,
+     "execution_count": 121,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAc8AAAJGCAYAAAA9Lw6mAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACw70lEQVR4nOzdd3hT1RvA8e9N0iTdew9KW6BllL33RoYsQREcyM+Je++tuAeKihNxoSCKICDI3nvvTaGU7pm2aXLv7w+0ihTatElu057P8/Bok5tz3kKbN+fcc94jKYqiIAiCIAhClWnUDkAQBEEQXI1InoIgCIJgI5E8BUEQBMFGInkKgiAIgo1E8hQEQRAEG4nkKQiCIAg2EslTEARBEGykUzuA/5JlmdTUVLy9vZEkSe1wBEEQhHpCURQKCgqIiIhAo7ny2LLWJc/U1FSio6PVDkMQBEGop1JSUoiKirriNbUu [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAc8AAAJGCAYAAAA9Lw6mAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACw70lEQVR4nOzdd3hT1RvA8e9N0iTdew9KW6BllL33RoYsQREcyM+Je++tuAeKihNxoSCKICDI3nvvTaGU7pm2aXLv7w+0ihTatElu057P8/Bok5tz3kKbN+fcc94jKYqiIAiCIAhClWnUDkAQBEEQXI1InoIgCIJgI5E8BUEQBMFGInkKgiAIgo1E8hQEQRAEG4nkKQiCIAg2EslTEARBEGykUzuA/5JlmdTUVLy9vZEkSe1wBEEQhHpCURQKCgqIiIhAo7ny2LLWJc/U1FSio6PVDkMQBEGop1JSUoiKirriNbUu [...]
       "text/plain": [
        "<Figure size 1000x700 with 1 Axes>"
       ]
@@ -2203,7 +2258,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 38,
+   "execution_count": 122,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2217,7 +2272,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 39,
+   "execution_count": 123,
    "metadata": {
     "id": "IS5Gcxgct1LW",
     "outputId": "b8dbb05f-4ca9-4884-83ac-a7c169a9830a"
@@ -2229,13 +2284,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 39,
+     "execution_count": 123,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAFmCAYAAAC/V5rzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAmBElEQVR4nO3df3RU5b3v8c+EkCGBzIT8TmASESqgaapGDUORixLloIdqjWe1QkV6uVS8ERWw5eSIP3BVw9FTsKWFelqLPZVIiwU5WJQiaLjVxNKQGKESBUWCIcGjZiYEmITkuX9Ypo6EyiSBh0zer7X2Ws5+9t7z/U5k5rP2PHuPwxhjBAAAYEmU7QIAAEDfRhgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVkWHs/FDDz2kRYsWhawbOXKkdu/eLUmaMGGCysrKQsZv [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAFmCAYAAAC/V5rzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAmBElEQVR4nO3df3RU5b3v8c+EkCGBzIT8TmASESqgaapGDUORixLloIdqjWe1QkV6uVS8ERWw5eSIP3BVw9FTsKWFelqLPZVIiwU5WJQiaLjVxNKQGKESBUWCIcGjZiYEmITkuX9Ypo6EyiSBh0zer7X2Ws5+9t7z/U5k5rP2PHuPwxhjBAAAYEmU7QIAAEDfRhgBAABWEUYAAIBVhBEAAGAVYQQAAFhFGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVkWHs/FDDz2kRYsWhawbOXKkdu/eLUmaMGGCysrKQsZv [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -2251,7 +2306,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 40,
+   "execution_count": 124,
    "metadata": {
     "id": "nwUGucQ1t1LW",
     "outputId": "26c5aede-c587-4d88-cfe0-30ecaec9ede3"
@@ -2263,13 +2318,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 40,
+     "execution_count": 124,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAKTCAYAAAD1xWeKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADg00lEQVR4nOzddZhc5dkG8Hvcd9bd3ePuriQkgUCQ4AFKkX6l0FJKg5RgxdqSAsWCx4krcfeN7Wbd3cb1nO+PQCBkN1mZmTPy/K5rCjtz5px7yu7MM6/yWJZlQQghhBDig/hcByCEEEII4QoVQoQQQgjxWVQIEUIIIcRnUSFECCGEEJ9FhRAhhBBCfBYVQoQQQgjxWVQIEUIIIcRnCbkO4AgMw6CmpgYqlQo8Ho/rOIQQQgjhEMuy0Gq1iIyMBJ9//TYfryiEampqEBMTw3UMQgghhLiRyspKREdHX/cYryiEVCoV [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAKTCAYAAAD1xWeKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADg00lEQVR4nOzddZhc5dkG8Hvcd9bd3ePuriQkgUCQ4AFKkX6l0FJKg5RgxdqSAsWCx4krcfeN7Wbd3cb1nO+PQCBkN1mZmTPy/K5rCjtz5px7yu7MM6/yWJZlQQghhBDig/hcByCEEEII4QoVQoQQQgjxWVQIEUIIIcRnUSFECCGEEJ9FhRAhhBBCfBYVQoQQQgjxWVQIEUIIIcRnCbkO4AgMw6CmpgYqlQo8Ho/rOIQQQgjhEMuy0Gq1iIyMBJ9//TYfryiEampqEBMTw3UMQgghhLiRyspKREdHX/cYryiEVCoV [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -2294,7 +2349,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 41,
+   "execution_count": 125,
    "metadata": {
     "id": "xfMx6gJmt1LW",
     "outputId": "5278dfc3-3f51-4c21-84cc-922251b1d0cb"
@@ -2306,13 +2361,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 41,
+     "execution_count": 125,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWIAAAGdCAYAAAAomHm2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC+GElEQVR4nOzdd3QVRRvA4d/tyU3vjfSEFloIvXcQpAkiiFgRO/b2WcCKBbuIig1EBURBeu+9hw5JSO/9pt62+/0RDcYkQEhiAOc5h6PZnZ2dDdw3k9mZdxSyLMsIgiAITUbZ1A0QBEH4rxOBWBAEoYmJQCwIgtDERCAWBEFoYiIQC4IgNDERiAVBEJqYCMSCIAhNTARiQRCEJqZu6gb8kyRJpKWl4eDggEKhaOrmCIIgVCHLMkVFRfj6+qJUNkxf9poLxGlpafj7+zd1MwRBEC4pOTmZZs2aNUhd11wgdnBwACoe [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWIAAAGdCAYAAAAomHm2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAC+GElEQVR4nOzdd3QVRRvA4d/tyU3vjfSEFloIvXcQpAkiiFgRO/b2WcCKBbuIig1EBURBeu+9hw5JSO/9pt62+/0RDcYkQEhiAOc5h6PZnZ2dDdw3k9mZdxSyLMsIgiAITUbZ1A0QBEH4rxOBWBAEoYmJQCwIgtDERCAWBEFoYiIQC4IgNDERiAVBEJqYCMSCIAhNTARiQRCEJqZu6gb8kyRJpKWl4eDggEKhaOrmCIIgVCHLMkVFRfj6+qJUNkxf9poLxGlpafj7+zd1MwRBEC4pOTmZZs2aNUhd11wgdnBwACoe [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -2328,7 +2383,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 42,
+   "execution_count": 126,
    "metadata": {
     "id": "28VU40f9t1LW",
     "outputId": "45585067-de13-4e02-8147-053ef0115d2d"
@@ -2340,13 +2395,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 42,
+     "execution_count": 126,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaYAAAKTCAYAAABSLqyAAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUxRrA4d/ZvpveCwkpJITee0dAUURREcGO3WvvYu+9994VUBQbiPTee4eQ3nvbbN89949oIKaHJBtg3ufxuTd75sz5VpP9dubM+UaSZVlGEARBEDoIhbsDEARBEIQTicQkCIIgdCgiMQmCIAgdikhMgiAIQociEpMgCILQoYjEJAiCIHQoIjEJgiAIHYrK3QH8l8vlIjs7Gy8vLyRJcnc4giAIQiuQZZmKigrCw8NRKBoeE3W4xJSdnU1kZKS7wxAEQRDaQEZGBhEREQ226XCJycvLC6gK3tvb [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaYAAAKTCAYAAABSLqyAAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUxRrA4d/ZvpveCwkpJITee0dAUURREcGO3WvvYu+9994VUBQbiPTee4eQ3nvbbN89949oIKaHJBtg3ufxuTd75sz5VpP9dubM+UaSZVlGEARBEDoIhbsDEARBEIQTicQkCIIgdCgiMQmCIAgdikhMgiAIQociEpMgCILQoYjEJAiCIHQoIjEJgiAIHYrK3QH8l8vlIjs7Gy8vLyRJcnc4giAIQiuQZZmKigrCw8NRKBoeE3W4xJSdnU1kZKS7wxAEQRDaQEZGBhEREQ226XCJycvLC6gK3tvb [...]
       "text/plain": [
        "<Figure size 800x800 with 1 Axes>"
       ]
@@ -2374,7 +2429,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 43,
+   "execution_count": 127,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2384,7 +2439,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 44,
+   "execution_count": 128,
    "metadata": {},
    "outputs": [
     {
@@ -2393,13 +2448,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 44,
+     "execution_count": 128,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQ8AAAGdCAYAAAAMgfZ2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACcFElEQVR4nOyddXhV9RvAP7fX3b0Ro8ZGd0k3SAgYgIiKqCAG8lNBEcVGERsRLBADDJDu7maDjbEN1nWXt8/vj8mFy70b29gYcT7Pcx893zrvYfe89xtvSARBEBARERGpItK6FkBEROTORFQeIiIi1UJUHiIiItVCVB4iIiLVQlQeIiIi1UJUHiIiItVCVB4iIiLVQlQeIiIi1UJe1wLUBCaTidTUVJydnZFIJHUtjojIHY0gCBQWFhIQEIBUWv784q5QHqmpqQQHB9e1GCIidxUpKSkEBQWVW39XKA9nZ2eg7GFd [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQ8AAAGdCAYAAAAMgfZ2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACcFElEQVR4nOyddXhV9RvAP7fX3b0Ro8ZGd0k3SAgYgIiKqCAG8lNBEcVGERsRLBADDJDu7maDjbEN1nWXt8/vj8mFy70b29gYcT7Pcx893zrvYfe89xtvSARBEBARERGpItK6FkBEROTORFQeIiIi1UJUHiIiItVCVB4iIiLVQlQeIiIi1UJUHiIiItVCVB4iIiLVQlQeIiIi1UJe1wLUBCaTidTUVJydnZFIJHUtjojIHY0gCBQWFhIQEIBUWv784q5QHqmpqQQHB9e1GCIidxUpKSkEBQWVW39XKA9nZ2eg7GFd [...]
       "text/plain": [
        "<Figure size 640x480 with 1 Axes>"
       ]
@@ -2424,7 +2479,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 45,
+   "execution_count": 129,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2433,7 +2488,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 46,
+   "execution_count": 130,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2443,7 +2498,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 47,
+   "execution_count": 131,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2454,7 +2509,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 48,
+   "execution_count": 132,
    "metadata": {},
    "outputs": [
     {
@@ -2463,13 +2518,13 @@
        "<Axes: >"
       ]
      },
-     "execution_count": 48,
+     "execution_count": 132,
      "metadata": {},
      "output_type": "execute_result"
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAJGCAYAAABC0t4SAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAADHy0lEQVR4nOzdd3QU1R7A8e/sbnrvvVFDDb33DgKiCIIoKiiKoCLPhgW72LGAYENFEZAqIL33TuglhPTee7Jt3h/RwJq2STYkgfs5h3NeZu7c+a0Pfpm9c+/vSrIsywiCIAgNjqKuAxAEQRCqRyRwQRCEBkokcEEQhAZKJHBBEIQGSiRwQRCEBkokcEEQhAZKJHBBEIQGSlXXAZiCXq8nPj4eOzs7JEmq63AEQRBqRJZlcnJy8Pb2RqEo/zn7jkjg8fHx+Pn51XUYgiAIJhUTE4Ovr2+55++IBG5nZwcUf1h7e/s6 [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAJGCAYAAABC0t4SAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAADHy0lEQVR4nOzdd3QU1R7A8e/sbnrvvVFDDb33DgKiCIIoKiiKoCLPhgW72LGAYENFEZAqIL33TuglhPTee7Jt3h/RwJq2STYkgfs5h3NeZu7c+a0Pfpm9c+/vSrIsywiCIAgNjqKuAxAEQRCqRyRwQRCEBkokcEEQhAZKJHBBEIQGSiRwQRCEBkokcEEQhAZKJHBBEIQGSlXXAZiCXq8nPj4eOzs7JEmq63AEQRBqRJZlcnJy8Pb2RqEo/zn7jkjg8fHx+Pn51XUYgiAIJhUTE4Ovr2+55++IBG5nZwcUf1h7e/s6 [...]
       "text/plain": [
        "<Figure size 1000x700 with 1 Axes>"
       ]
@@ -2491,7 +2546,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 49,
+   "execution_count": 133,
    "metadata": {
     "id": "5xOVyzXCt1LW"
    },
@@ -2513,14 +2568,15 @@
     "    \"russia\": russia_copy,\n",
     "    \"spain\": spain_copy,\n",
     "    \"portugal\": portugal_copy,\n",
-    "    \"ukraine\": ukraine_with_crimea\n",
+    "    \"ukraine\": ukraine_with_crimea,\n",
+    "    \"india\": india_copy,\n",
     "}\n",
     "\n"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 50,
+   "execution_count": 134,
    "metadata": {},
    "outputs": [
     {
@@ -2585,7 +2641,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 51,
+   "execution_count": 135,
    "metadata": {
     "id": "8U3S1PUbt1LW",
     "outputId": "cfb8d229-ffdf-473f-d516-6aa136e41a60",
@@ -2804,6 +2860,7 @@
       "spain\tSize 178.488\tSaving geojson for spain...\n",
       "portugal\tSize 105.727\tSaving geojson for portugal...\n",
       "ukraine\tSize 143.877\tSaving geojson for ukraine...\n",
+      "india\tSaving geojson for india...\n",
       "Done.                          \n"
      ]
     }
@@ -2867,7 +2924,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 52,
+   "execution_count": 136,
    "metadata": {
     "id": "tM1F5d0Vt1LW",
     "outputId": "75abad9b-9442-4279-d66d-a0cd5fb97198"
@@ -2882,7 +2939,7 @@
     },
     {
      "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAADAEAAEBACAYAAACVAPr+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd1hTZ/sH8G/C3rKHIKAVB+DAiYrgqna5t7ZQtVrbt7buvVpb66izrbW1VatW0VatVetrFVfdG1RwIyB7yYZAzu8Pf+YlECCBhDC+n+viap7kGfdJbE7Oc859HpEgCAKIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiKjGE2s7ACIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiUg6TAIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIagkmARARERERERERERERERERERERERERERER1RJMAiAiIiIiIiIiIiIiIiIi [...]
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAADAEAAEBACAYAAACVAPr+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd1hTZ/sH8G/C3rKHIKAVB+DAiYrgqna5t7ZQtVrbt7buvVpb66izrbW1VatW0VatVetrFVfdG1RwIyB7yYZAzu8Pf+YlECCBhDC+n+viap7kGfdJbE7Oc859HpEgCAKIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiKjGE2s7ACIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiUg6TAIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIagkmARARERERERERERERERERERERERERERER1RJMAiAiIiIiIiIiIiIiIiIi [...]
       "text/plain": [
        "<Figure size 3000x16500 with 193 Axes>"
       ]
@@ -2906,7 +2963,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 53,
+   "execution_count": 137,
    "metadata": {},
    "outputs": [
     {
diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson
index 9f708a9133..9b516f2255 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson
+++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson
@@ -4,12 +4,12 @@
 "features": [
 { "type": "Feature", "properties": { "ISO": "IN-SK", "NAME_1": "Sikkim" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 88.891349725300017, 27.316059068301925 ], [ 88.881686232461448, 27.297481390892997 ], [ 88.760401645872435, 27.218132229108832 ], [ 88.738800897174428, 27.175602525332621 ], [ 88.756200335345866, 27.148800698456999 ], [ 88.664490188071682, 27.173354601033964 ], [ 88.518039177565981, 27.17622264336029 ], [ 88.447655878016292, 27.114495144035629 ], [ 88.357428827 [...]
 { "type": "Feature", "properties": { "ISO": "IN-TG", "NAME_1": "Telangana" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 77.446466912617893, 15.952980048090751 ], [ 77.483673943997303, 16.002072659025998 ], [ 77.490805291730794, 16.261591701795567 ], [ 77.533179966082514, 16.361042996437106 ], [ 77.293711379376646, 16.406518256212564 ], [ 77.257434523084726, 16.449358018557689 ], [ 77.370915969196574, 16.524288845253977 ], [ 77.431997511575844, 16.593612779406214 ], [ 77.41091 [...]
-{ "type": "Feature", "properties": { "ISO": "IN-LA", "NAME_1": "Ladakh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 77.799365268875448, 35.495921535782571 ], [ 77.810889113276545, 35.48452688246222 ], [ 77.851506789057964, 35.460781561824092 ], [ 77.894914992183118, 35.448999335261547 ], [ 77.945867954422908, 35.471633612605387 ], [ 78.009481642574201, 35.490237128230461 ], [ 78.042709588537889, 35.479798488907505 ], [ 78.047412143876443, 35.44938690850374 ], [ 78.0091715839 [...]
+{ "type": "Feature", "properties": { "ISO": "IN-LA", "NAME_1": "Ladakh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 76.794840514827186, 33.253065234388536 ], [ 76.795977, 33.254863 ], [ 76.753304, 33.321913 ], [ 76.686696, 33.33899 ], [ 76.664651, 33.428881 ], [ 76.597976, 33.476428 ], [ 76.581826, 33.515437 ], [ 76.484652, 33.58498 ], [ 76.397437, 33.668019 ], [ 76.312368, 33.622137 ], [ 76.249578, 33.608457 ], [ 76.22092, 33.694868 ], [ 76.210462, 33.786405 ], [ 76.110452, [...]
 { "type": "Feature", "properties": { "ISO": "IN-CH", "NAME_1": "Chandigarh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 76.814567498591146, 30.78912873040656 ], [ 76.827279901346344, 30.680711575378893 ], [ 76.775603468924942, 30.677404283480882 ], [ 76.703773227652277, 30.781403103470439 ], [ 76.814567498591146, 30.78912873040656 ] ] ] } },
 { "type": "Feature", "properties": { "ISO": "IN-DL", "NAME_1": "Delhi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 77.327404413283034, 28.519706529033044 ], [ 77.269733513790584, 28.510275579754193 ], [ 77.19356245274497, 28.432605902840123 ], [ 77.100544874746163, 28.475600693916874 ], [ 77.070882603149585, 28.528569037531042 ], [ 76.87265180923356, 28.519422309092249 ], [ 76.836891716879222, 28.587221788055047 ], [ 76.94727257666807, 28.696155707020239 ], [ 76.936523878674 [...]
 { "type": "Feature", "properties": { "ISO": "IN-HP", "NAME_1": "Himachal Pradesh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 78.390279918151052, 32.527268327640002 ], [ 78.389665154945533, 32.519875800093587 ], [ 78.417467075518545, 32.46670075126525 ], [ 78.441341587237389, 32.397350979129556 ], [ 78.455294223956201, 32.300328477501552 ], [ 78.486093377602145, 32.236223863243495 ], [ 78.487390078691362, 32.233521265183875 ], [ 78.495911899737621, 32.215759996055908 ], [ 78 [...]
 { "type": "Feature", "properties": { "ISO": "IN-HR", "NAME_1": "Haryana" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 77.200797154165286, 28.876790676048813 ], [ 77.067988723300914, 28.882087511129725 ], [ 76.936523878674052, 28.814649767372828 ], [ 76.94727257666807, 28.696155707020239 ], [ 76.836891716879222, 28.587221788055047 ], [ 76.87265180923356, 28.519422309092249 ], [ 77.070882603149585, 28.528569037531042 ], [ 77.100544874746163, 28.475600693916874 ], [ 77.193562452 [...]
-{ "type": "Feature", "properties": { "ISO": "IN-JK", "NAME_1": "Jammu and Kashmir" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 75.347546963197445, 34.572707809553762 ], [ 75.3745485, 34.3709805 ], [ 75.4694874, 34.2800518 ], [ 75.5845642, 34.2101164 ], [ 75.6634003, 34.1992301 ], [ 75.7416556, 34.0664337 ], [ 75.8795921, 33.9636962 ], [ 76.0243674, 33.9820894 ], [ 76.166381196366061, 33.873232448638177 ], [ 76.235838061456889, 33.728793740551566 ], [ 76.246098734708937, 33.5 [...]
+{ "type": "Feature", "properties": { "ISO": "IN-JK", "NAME_1": "Jammu and Kashmir" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 75.393774207114006, 32.329366594592727 ], [ 75.371868928989798, 32.336875597813915 ], [ 75.43734079532129, 32.325699456191217 ], [ 75.430249502412664, 32.324740207694504 ], [ 75.393774207114006, 32.329366594592727 ] ] ], [ [ [ 75.727202147355939, 32.418107248302128 ], [ 75.727202, 32.418107 ], [ 75.727201973468496, 32.418106991920929 ], [ 75.7 [...]
 { "type": "Feature", "properties": { "ISO": "IN-AP", "NAME_1": "Andhra Pradesh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 80.231197867888966, 13.467614136514745 ], [ 80.097571242418894, 13.46375214334671 ], [ 79.96197228359398, 13.416545721907539 ], [ 79.956701287834107, 13.365773627051226 ], [ 79.816348098086223, 13.294925237709549 ], [ 79.784928827303474, 13.224025173323071 ], [ 79.696768833015199, 13.235807400091403 ], [ 79.667933383718662, 13.291282049926622 ], [ 79.57 [...]
 { "type": "Feature", "properties": { "ISO": "IN-KL", "NAME_1": "Kerala" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 74.893952998469786, 12.751445129204074 ], [ 75.102113885447409, 12.678864651182664 ], [ 75.312023553344261, 12.469497584745739 ], [ 75.389331495951751, 12.29472789157893 ], [ 75.426228468968645, 12.291782334886818 ], [ 75.536609327858173, 12.168947454761224 ], [ 75.62921349470696, 12.102052314262892 ], [ 75.761091749584523, 12.068746853084804 ], [ 75.7761812682 [...]
 { "type": "Feature", "properties": { "ISO": "IN-LD", "NAME_1": "Lakshadweep" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 73.053320312500006, 8.256689453124991 ], [ 73.038867187500017, 8.251953125 ], [ 73.028515625000011, 8.253515625 ], [ 73.0234375, 8.265917968749989 ], [ 73.026074218750011, 8.275292968749994 ], [ 73.038964843750023, 8.26484375 ], [ 73.055859375000011, 8.274560546874994 ], [ 73.0751953125, 8.306347656249997 ], [ 73.079492187500023, 8.316503906249991 ] [...]
diff --git a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/controlPanel.tsx b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/controlPanel.tsx
index 53394c99eb..d22428e1ea 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/controlPanel.tsx
+++ b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/controlPanel.tsx
@@ -64,18 +64,7 @@ const config: ControlPanelConfig = {
         ['metric'],
         ['adhoc_filters'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx
index 535f3c3b63..0af5f8bf77 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx
+++ b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx
@@ -78,7 +78,7 @@ class CustomHistogram extends PureComponent {
     const keys = data.map(d => d.key);
     const colorScale = scaleOrdinal({
       domain: keys,
-      range: keys.map(x => colorFn(x, sliceId)),
+      range: keys.map(x => colorFn(x, sliceId, colorScheme)),
     });
 
     return (
diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json
index a6b5ec72e2..8695c9f45f 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json
+++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json
@@ -23,7 +23,7 @@
     "lib"
   ],
   "dependencies": {
-    "distributions": "^1.0.0",
+    "distributions": "^2.2.0",
     "prop-types": "^15.8.1",
     "reactable": "^1.1.0"
   },
diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js
index 2247061668..e0ebd559a9 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js
@@ -384,7 +384,7 @@ function Icicle(element, props) {
 
     // Apply color scheme
     g.selectAll('rect').style('fill', d => {
-      d.color = colorFn(d.name, sliceId);
+      d.color = colorFn(d.name, sliceId, colorScheme);
 
       return d.color;
     });
diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js
index 2dfa2ffdd7..e54fc0b6c5 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js
@@ -120,10 +120,16 @@ function Rose(element, props) {
           .map(v => ({
             key: v.name,
             value: v.value,
-            color: colorFn(v.name, sliceId),
+            color: colorFn(v.name, sliceId, colorScheme),
             highlight: v.id === d.arcId,
           }))
-      : [{ key: d.name, value: d.val, color: colorFn(d.name, sliceId) }];
+      : [
+          {
+            key: d.name,
+            value: d.val,
+            color: colorFn(d.name, sliceId, colorScheme),
+          },
+        ];
 
     return {
       key: 'Date',
@@ -132,7 +138,7 @@ function Rose(element, props) {
     };
   }
 
-  legend.width(width).color(d => colorFn(d.key, sliceId));
+  legend.width(width).color(d => colorFn(d.key, sliceId, colorScheme));
   legendWrap.datum(legendData(datum)).call(legend);
 
   tooltip.headerFormatter(timeFormat).valueFormatter(format);
@@ -379,7 +385,7 @@ function Rose(element, props) {
   const arcs = ae
     .append('path')
     .attr('class', 'arc')
-    .attr('fill', d => colorFn(d.name, sliceId))
+    .attr('fill', d => colorFn(d.name, sliceId, colorScheme))
     .attr('d', arc);
 
   function mousemove() {
diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js
index d8c8f61e44..0639edad45 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js
@@ -219,7 +219,7 @@ function Sankey(element, props) {
     .attr('width', sankey.nodeWidth())
     .style('fill', d => {
       const name = d.name || 'N/A';
-      d.color = colorFn(name, sliceId);
+      d.color = colorFn(name, sliceId, colorScheme);
 
       return d.color;
     })
diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/controlPanel.ts
index ef9f0394d9..7505f1a4a0 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/controlPanel.ts
+++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/controlPanel.ts
@@ -49,18 +49,7 @@ const config: ControlPanelConfig = {
             },
           },
         ],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts
index 82d8dcb1e2..f75764e9f5 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts
+++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts
@@ -54,18 +54,7 @@ const config: ControlPanelConfig = {
         ['metric'],
         ['adhoc_filters'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
index 7c3df6174e..73b8128f04 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
@@ -31,7 +31,7 @@
     "d3-array": "^1.2.4",
     "d3-color": "^1.4.1",
     "d3-scale": "^3.0.0",
-    "deck.gl": "9.0.6",
+    "deck.gl": "9.0.12",
     "lodash": "^4.17.21",
     "moment": "^2.30.1",
     "mousetrap": "^1.6.5",
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
index 2b81b8a8d2..7dff2af221 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
@@ -58,7 +58,10 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
     if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
       let color;
       if (fd.dimension) {
-        color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
+        color = hexToRGB(
+          colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
+          c.a * 255,
+        );
       } else {
         color = fixedColor;
       }
@@ -134,7 +137,10 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
     return data.map(d => {
       let color;
       if (fd.dimension) {
-        color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
+        color = hexToRGB(
+          colorFn(d.cat_color, fd.sliceId, fd.color_scheme),
+          c.a * 255,
+        );
 
         return { ...d, color };
       }
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js
index a454fe28b4..06455f16d8 100644
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js
+++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js
@@ -658,7 +658,9 @@ function nvd3Vis(element, props) {
     } else if (vizType !== 'bullet') {
       const colorFn = getScale(colorScheme);
       chart.color(
-        d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
+        d =>
+          d.color ||
+          colorFn(cleanColorInput(d[colorKey]), sliceId, colorScheme),
       );
     }
 
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
index 04997ad697..b7ddae936c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx
@@ -89,11 +89,11 @@ export default function PopKPI(props: PopKPIProps) {
     if (!currentTimeRangeFilter || (!shift && !startDateOffset)) {
       setComparisonRange('');
     } else if (!isEmpty(shift) || startDateOffset) {
-      const newShift = getTimeOffset(
-        currentTimeRangeFilter,
-        ensureIsArray(shift),
-        startDateOffset || '',
-      );
+      const newShift = getTimeOffset({
+        timeRangeFilter: currentTimeRangeFilter,
+        shifts: ensureIsArray(shift),
+        startDate: startDateOffset || '',
+      });
       const promise: any = fetchTimeRange(
         (currentTimeRangeFilter as any).comparator,
         currentTimeRangeFilter.subject,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts
index f9b4725cb0..84e80559f0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts
@@ -23,11 +23,13 @@ import {
   ensureIsArray,
   SimpleAdhocFilter,
   getTimeOffset,
+  parseDttmToDate,
 } from '@superset-ui/core';
 import {
   isTimeComparison,
   timeCompareOperator,
 } from '@superset-ui/chart-controls';
+import { isEmpty } from 'lodash';
 
 export default function buildQuery(formData: QueryFormData) {
   const { cols: groupby } = formData;
@@ -40,13 +42,31 @@ export default function buildQuery(formData: QueryFormData) {
         (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
       ) || [];
 
+    // In case the viz is using all version of controls, we try to load them
+    const previousCustomTimeRangeFilters: any =
+      formData.adhoc_custom?.filter(
+        (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+      ) || [];
+
+    let previousCustomStartDate = '';
+    if (
+      !isEmpty(previousCustomTimeRangeFilters) &&
+      previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
+    ) {
+      previousCustomStartDate =
+        previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
+    }
+
     const timeOffsets = ensureIsArray(
       isTimeComparison(formData, baseQueryObject)
-        ? getTimeOffset(
-            TimeRangeFilters[0],
-            formData.time_compare,
-            formData.start_date_offset,
-          )
+        ? getTimeOffset({
+            timeRangeFilter: TimeRangeFilters[0],
+            shifts: formData.time_compare,
+            startDate:
+              previousCustomStartDate && !formData.start_date_offset
+                ? parseDttmToDate(previousCustomStartDate)?.toUTCString()
+                : formData.start_date_offset,
+          })
         : [],
     );
     return [
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
index 1039df0de9..15a65e0355 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts
@@ -25,7 +25,9 @@ import {
   SimpleAdhocFilter,
   ensureIsArray,
   getTimeOffset,
+  parseDttmToDate,
 } from '@superset-ui/core';
+import { isEmpty } from 'lodash';
 import { getComparisonFontSize, getHeaderFontSize } from './utils';
 
 export const parseMetricValue = (metricValue: number | string | null) => {
@@ -90,22 +92,39 @@ export default function transformProps(chartProps: ChartProps) {
   } = formData;
   const { data: dataA = [] } = queriesData[0];
   const data = dataA;
-  const metricName = getMetricLabel(metric);
+  const metricName = metric ? getMetricLabel(metric) : '';
   const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
   const startDateOffset = chartProps.rawFormData?.start_date_offset;
   const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
     (adhoc_filter: SimpleAdhocFilter) =>
       adhoc_filter.operator === 'TEMPORAL_RANGE',
   )?.[0];
+  // In case the viz is using all version of controls, we try to load them
+  const previousCustomTimeRangeFilters: any =
+    chartProps.rawFormData?.adhoc_custom?.filter(
+      (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+    ) || [];
+
+  let previousCustomStartDate = '';
+  if (
+    !isEmpty(previousCustomTimeRangeFilters) &&
+    previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
+  ) {
+    previousCustomStartDate =
+      previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
+  }
   const isCustomOrInherit =
     timeComparison === 'custom' || timeComparison === 'inherit';
   let dataOffset: string[] = [];
   if (isCustomOrInherit) {
-    dataOffset = getTimeOffset(
-      currentTimeRangeFilter,
-      ensureIsArray(timeComparison),
-      startDateOffset || '',
-    );
+    dataOffset = getTimeOffset({
+      timeRangeFilter: currentTimeRangeFilter,
+      shifts: ensureIsArray(timeComparison),
+      startDate:
+        previousCustomStartDate && !startDateOffset
+          ? parseDttmToDate(previousCustomStartDate)?.toUTCString()
+          : startDateOffset,
+    });
   }
 
   const { value1, value2 } = data.reduce(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
index 0edeadb7fa..4f5c8f323f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
@@ -108,9 +108,9 @@ export default function transformProps(
             datum[`${metric}__outliers`],
           ],
           itemStyle: {
-            color: colorFn(groupbyLabel, sliceId),
+            color: colorFn(groupbyLabel, sliceId, colorScheme),
             opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
-            borderColor: colorFn(groupbyLabel, sliceId),
+            borderColor: colorFn(groupbyLabel, sliceId, colorScheme),
           },
         };
       });
@@ -149,7 +149,7 @@ export default function transformProps(
             },
           },
           itemStyle: {
-            color: colorFn(groupbyLabel, sliceId),
+            color: colorFn(groupbyLabel, sliceId, colorScheme),
             opacity: isFiltered
               ? OpacityEnum.SemiTransparent
               : OpacityEnum.NonTransparent,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
index a8e4f01644..df2b23c599 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
@@ -62,12 +62,8 @@ const config: ControlPanelConfig = {
           {
             name: 'sort_by_metric',
             config: {
+              ...sharedControls.sort_by_metric,
               default: true,
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
             },
           },
         ],
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 daca2eb678..a0b0569337 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
@@ -174,7 +174,7 @@ export default function transformProps(
       value,
       name,
       itemStyle: {
-        color: colorFn(name, sliceId),
+        color: colorFn(name, sliceId, colorScheme),
         opacity: isFiltered
           ? OpacityEnum.SemiTransparent
           : OpacityEnum.NonTransparent,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx
index 8727a23c72..5d6df447ac 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx
@@ -53,18 +53,7 @@ const config: ControlPanelConfig = {
             },
           },
         ],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
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 868cc0df36..187ce67dcb 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts
@@ -173,7 +173,7 @@ export default function transformProps(
         value: data_point[metricLabel] as number,
         name,
         itemStyle: {
-          color: colorFn(index, sliceId),
+          color: colorFn(index, sliceId, colorScheme),
         },
         title: {
           offsetCenter: [
@@ -201,7 +201,7 @@ export default function transformProps(
         item = {
           ...item,
           itemStyle: {
-            color: colorFn(index, sliceId),
+            color: colorFn(index, sliceId, colorScheme),
             opacity: OpacityEnum.SemiTransparent,
           },
           detail: {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts
index e79b5ac874..2cc3aff92f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts
@@ -277,7 +277,7 @@ export default function transformProps(
       type: 'graph',
       categories: categoryList.map(c => ({
         name: c,
-        itemStyle: { color: colorFn(c, sliceId) },
+        itemStyle: { color: colorFn(c, sliceId, colorScheme) },
       })),
       layout,
       force: {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
index b4ae075b0c..583444508d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx
@@ -26,6 +26,7 @@ import {
   D3_FORMAT_OPTIONS,
   D3_TIME_FORMAT_OPTIONS,
   getStandardizedControls,
+  sharedControls,
 } from '@superset-ui/chart-controls';
 import { DEFAULT_FORM_DATA } from './types';
 import { legendSection } from '../controls';
@@ -56,12 +57,8 @@ const config: ControlPanelConfig = {
           {
             name: 'sort_by_metric',
             config: {
+              ...sharedControls.sort_by_metric,
               default: true,
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
             },
           },
         ],
@@ -123,11 +120,31 @@ const config: ControlPanelConfig = {
                 ['key_percent', t('Category and Percentage')],
                 ['key_value_percent', t('Category, Value and Percentage')],
                 ['value_percent', t('Value and Percentage')],
+                ['template', t('Template')],
               ],
               description: t('What should be shown on the label?'),
             },
           },
         ],
+        [
+          {
+            name: 'label_template',
+            config: {
+              type: 'TextControl',
+              label: t('Label Template'),
+              renderTrigger: true,
+              description: t(
+                'Format data labels. ' +
+                  'Use variables: {name}, {value}, {percent}. ' +
+                  '\\n represents a new line. ' +
+                  'ECharts compatibility:\n' +
+                  '{a} (series), {b} (name), {c} (value), {d} (percentage)',
+              ),
+              visibility: ({ controls }: ControlPanelsContainerProps) =>
+                controls?.label_type?.value === 'template',
+            },
+          },
+        ],
         [
           {
             name: 'number_format',
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 d6400d4022..1b3898d8fd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts
@@ -143,6 +143,7 @@ export default function transformProps(
     labelsOutside,
     labelLine,
     labelType,
+    labelTemplate,
     legendMargin,
     legendOrientation,
     legendType,
@@ -221,7 +222,7 @@ export default function transformProps(
       value,
       name,
       itemStyle: {
-        color: colorFn(name, sliceId),
+        color: colorFn(name, sliceId, colorScheme),
         opacity: isFiltered
           ? OpacityEnum.SemiTransparent
           : OpacityEnum.NonTransparent,
@@ -242,6 +243,38 @@ export default function transformProps(
     {},
   );
 
+  const formatTemplate = (
+    template: string,
+    formattedParams: {
+      name: string;
+      value: string;
+      percent: string;
+    },
+    rawParams: CallbackDataParams,
+  ) => {
+    // This function supports two forms of template variables:
+    // 1. {name}, {value}, {percent}, for values formatted by number formatter.
+    // 2. {a}, {b}, {c}, {d}, compatible with ECharts formatter.
+    //
+    // \n is supported to represent a new line.
+
+    const items = {
+      '{name}': formattedParams.name,
+      '{value}': formattedParams.value,
+      '{percent}': formattedParams.percent,
+      '{a}': rawParams.seriesName || '',
+      '{b}': rawParams.name,
+      '{c}': `${rawParams.value}`,
+      '{d}': `${rawParams.percent}`,
+      '\\n': '\n',
+    };
+
+    return Object.entries(items).reduce(
+      (acc, [key, value]) => acc.replaceAll(key, value),
+      template,
+    );
+  };
+
   const formatter = (params: CallbackDataParams) => {
     const [name, formattedValue, formattedPercent] = parseParams({
       params,
@@ -262,6 +295,19 @@ export default function transformProps(
         return `${name}: ${formattedPercent}`;
       case EchartsPieLabelType.ValuePercent:
         return `${formattedValue} (${formattedPercent})`;
+      case EchartsPieLabelType.Template:
+        if (!labelTemplate) {
+          return '';
+        }
+        return formatTemplate(
+          labelTemplate,
+          {
+            name,
+            value: formattedValue,
+            percent: formattedPercent,
+          },
+          params,
+        );
       default:
         return name;
     }
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts
index c72da1feed..06ff2bc41a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts
@@ -38,6 +38,7 @@ export type EchartsPieFormData = QueryFormData &
     innerRadius: number;
     labelLine: boolean;
     labelType: EchartsPieLabelType;
+    labelTemplate: string | null;
     labelsOutside: boolean;
     metric?: string;
     outerRadius: number;
@@ -56,6 +57,7 @@ export enum EchartsPieLabelType {
   KeyPercent = 'key_percent',
   KeyValuePercent = 'key_value_percent',
   ValuePercent = 'value_percent',
+  Template = 'template',
 }
 
 export interface EchartsPieChartProps
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts
index c8c3c7ed15..dd49a1b872 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts
@@ -165,7 +165,7 @@ export default function transformProps(
       value: metricLabels.map(metricLabel => datum[metricLabel]),
       name: joinedName,
       itemStyle: {
-        color: colorFn(joinedName, sliceId),
+        color: colorFn(joinedName, sliceId, colorScheme),
         opacity: isFiltered
           ? OpacityEnum.Transparent
           : OpacityEnum.NonTransparent,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx
index 532b19b410..2a64a8f965 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx
@@ -42,18 +42,7 @@ const config: ControlPanelConfig = {
         ['secondary_metric'],
         ['adhoc_filters'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx
index 2e09f25d1e..1e1dc47576 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx
@@ -40,18 +40,7 @@ const config: ControlPanelConfig = {
         ['groupby'],
         ['metric'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
         ['adhoc_filters'],
       ],
     },
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 09bdd19154..73feda2b2e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
@@ -183,7 +183,7 @@ export default function transformProps(
           colorSaturation: COLOR_SATURATION,
           itemStyle: {
             borderColor: BORDER_COLOR,
-            color: colorFn(name, sliceId),
+            color: colorFn(name, sliceId, colorScheme),
             borderWidth: BORDER_WIDTH,
             gapWidth: GAP_WIDTH,
           },
@@ -216,7 +216,7 @@ export default function transformProps(
       colorSaturation: COLOR_SATURATION,
       itemStyle: {
         borderColor: BORDER_COLOR,
-        color: colorFn(`${metricLabel}`, sliceId),
+        color: colorFn(`${metricLabel}`, sliceId, colorScheme),
         borderWidth: BORDER_WIDTH,
         gapWidth: GAP_WIDTH,
       },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts
index 1add75d401..e0c1992574 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts
@@ -22,6 +22,8 @@ import {
   SqlaFormData,
   supersetTheme,
 } from '@superset-ui/core';
+import { LabelFormatterCallback, PieSeriesOption } from 'echarts';
+import { CallbackDataParams } from 'echarts/types/src/util/types';
 import transformProps, { parseParams } from '../../src/Pie/transformProps';
 import { EchartsPieChartProps } from '../../src/Pie/types';
 
@@ -101,3 +103,112 @@ describe('formatPieLabel', () => {
     ).toEqual(['&lt;NULL&gt;', '1.23k', '12.34%']);
   });
 });
+
+describe('Pie label string template', () => {
+  const params: CallbackDataParams = {
+    componentType: '',
+    componentSubType: '',
+    componentIndex: 0,
+    seriesType: 'pie',
+    seriesIndex: 0,
+    seriesId: 'seriesId',
+    seriesName: 'test',
+    name: 'Tablet',
+    dataIndex: 0,
+    data: {},
+    value: 123456,
+    percent: 55.5,
+    $vars: [],
+  };
+
+  const getChartProps = (form: Partial<SqlaFormData>): EchartsPieChartProps => {
+    const formData: SqlaFormData = {
+      colorScheme: 'bnbColors',
+      datasource: '3__table',
+      granularity_sqla: 'ds',
+      metric: 'sum__num',
+      groupby: ['foo', 'bar'],
+      viz_type: 'my_viz',
+      ...form,
+    };
+
+    return new ChartProps({
+      formData,
+      width: 800,
+      height: 600,
+      queriesData: [
+        {
+          data: [
+            { foo: 'Sylvester', bar: 1, sum__num: 10 },
+            { foo: 'Arnold', bar: 2, sum__num: 2.5 },
+          ],
+        },
+      ],
+      theme: supersetTheme,
+    }) as EchartsPieChartProps;
+  };
+
+  const format = (form: Partial<SqlaFormData>) => {
+    const props = transformProps(getChartProps(form));
+    expect(props).toEqual(
+      expect.objectContaining({
+        width: 800,
+        height: 600,
+        echartOptions: expect.objectContaining({
+          series: [
+            expect.objectContaining({
+              avoidLabelOverlap: true,
+              data: expect.arrayContaining([
+                expect.objectContaining({
+                  name: 'Arnold, 2',
+                  value: 2.5,
+                }),
+                expect.objectContaining({
+                  name: 'Sylvester, 1',
+                  value: 10,
+                }),
+              ]),
+              label: expect.objectContaining({
+                formatter: expect.any(Function),
+              }),
+            }),
+          ],
+        }),
+      }),
+    );
+
+    const formatter = (props.echartOptions.series as PieSeriesOption[])[0]!
+      .label?.formatter;
+
+    return (formatter as LabelFormatterCallback)(params);
+  };
+
+  it('should generate a valid pie chart label with template', () => {
+    expect(
+      format({
+        label_type: 'template',
+        label_template: '{name}:{value}\n{percent}',
+      }),
+    ).toEqual('Tablet:123k\n55.50%');
+  });
+
+  it('should be formatted using the number formatter', () => {
+    expect(
+      format({
+        label_type: 'template',
+        label_template: '{name}:{value}\n{percent}',
+        number_format: ',d',
+      }),
+    ).toEqual('Tablet:123,456\n55.50%');
+  });
+
+  it('should be compatible with ECharts raw variable syntax', () => {
+    expect(
+      format({
+        label_type: 'template',
+        label_template: '{b}:{c}\n{d}',
+        number_format: ',d',
+      }),
+    ).toEqual('Tablet:123456\n55.5');
+  });
+});
diff --git a/superset-frontend/plugins/plugin-chart-table/package.json b/superset-frontend/plugins/plugin-chart-table/package.json
index ab0aa44e6e..2e63062c1e 100644
--- a/superset-frontend/plugins/plugin-chart-table/package.json
+++ b/superset-frontend/plugins/plugin-chart-table/package.json
@@ -37,6 +37,7 @@
     "xss": "^1.0.15"
   },
   "peerDependencies": {
+    "@ant-design/icons": "^5.0.1",
     "@superset-ui/chart-controls": "*",
     "@superset-ui/core": "*",
     "@testing-library/dom": "^7.29.4",
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
index 0501b9e0f8..21ce6541cf 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
@@ -69,6 +69,8 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
   rowCount: number;
   wrapperRef?: MutableRefObject<HTMLDivElement>;
   onColumnOrderChange: () => void;
+  renderGroupingHeaders?: () => JSX.Element;
+  renderTimeComparisonDropdown?: () => JSX.Element;
 }
 
 export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
@@ -101,6 +103,8 @@ export default typedMemo(function DataTable<D extends object>({
   serverPagination,
   wrapperRef: userWrapperRef,
   onColumnOrderChange,
+  renderGroupingHeaders,
+  renderTimeComparisonDropdown,
   ...moreUseTableOptions
 }: DataTableProps<D>): JSX.Element {
   const tableHooks: PluginHook<D>[] = [
@@ -117,7 +121,8 @@ export default typedMemo(function DataTable<D extends object>({
   const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
   const pageSizeRef = useRef([initialPageSize, resultsSize]);
   const hasPagination = initialPageSize > 0 && resultsSize > 0; // pageSize == 0 means no pagination
-  const hasGlobalControl = hasPagination || !!searchInput;
+  const hasGlobalControl =
+    hasPagination || !!searchInput || renderTimeComparisonDropdown;
   const initialState = {
     ...initialState_,
     // zero length means all pages, the `usePagination` plugin does not
@@ -253,6 +258,7 @@ export default typedMemo(function DataTable<D extends object>({
   const renderTable = () => (
     <table {...getTableProps({ className: tableClassName })}>
       <thead>
+        {renderGroupingHeaders ? renderGroupingHeaders() : null}
         {headerGroups.map(headerGroup => {
           const { key: headerGroupKey, ...headerGroupProps } =
             headerGroup.getHeaderGroupProps();
@@ -357,7 +363,9 @@ export default typedMemo(function DataTable<D extends object>({
       {hasGlobalControl ? (
         <div ref={globalControlRef} className="form-inline dt-controls">
           <div className="row">
-            <div className="col-sm-6">
+            <div
+              className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
+            >
               {hasPagination ? (
                 <SelectPageSize
                   total={resultsSize}
@@ -384,6 +392,14 @@ export default typedMemo(function DataTable<D extends object>({
                 />
               </div>
             ) : null}
+            {renderTimeComparisonDropdown ? (
+              <div
+                className="col-sm-1"
+                style={{ float: 'right', marginTop: '6px' }}
+              >
+                {renderTimeComparisonDropdown()}
+              </div>
+            ) : null}
           </div>
         </div>
       ) : null}
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
index 4470ea19d7..46e556ef82 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
@@ -183,7 +183,9 @@ function StickyWrap({
     }
     const fullTableHeight = (bodyThead.parentNode as HTMLTableElement)
       .clientHeight;
-    const ths = bodyThead.childNodes[0]
+    // instead of always using the first tr, we use the last one to support
+    // multi-level headers assuming the last one is the more detailed one
+    const ths = bodyThead.childNodes?.[bodyThead.childNodes?.length - 1 || 0]
       .childNodes as NodeListOf<HTMLTableHeaderCellElement>;
     const widths = Array.from(ths).map(
       th => th.getBoundingClientRect()?.width || th.clientWidth,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/Styles.tsx b/superset-frontend/plugins/plugin-chart-table/src/Styles.tsx
index 9219b6f003..e394d16446 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/Styles.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/Styles.tsx
@@ -111,5 +111,12 @@ export default styled.div`
       text-align: center;
       padding: 1em 0.6em;
     }
+
+    .right-border-only {
+      border-right: 2px solid ${theme.colors.grayscale.light2};
+    }
+    table .right-border-only:last-child {
+      border-right: none;
+    }
   `}
 `;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 0e4c4ddb00..e82d63d2f1 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -50,9 +50,23 @@ import {
   css,
   t,
   tn,
+  useTheme,
 } from '@superset-ui/core';
+import { Dropdown, Menu } from '@superset-ui/chart-controls';
+import {
+  CheckOutlined,
+  DownOutlined,
+  MinusCircleOutlined,
+  PlusCircleOutlined,
+  TableOutlined,
+} from '@ant-design/icons';
 
-import { DataColumnMeta, TableChartTransformedProps } from './types';
+import { isEmpty } from 'lodash';
+import {
+  ColorSchemeEnum,
+  DataColumnMeta,
+  TableChartTransformedProps,
+} from './types';
 import DataTable, {
   DataTableProps,
   SearchInputProps,
@@ -242,7 +256,16 @@ export default function TableChart<D extends DataRecord = DataRecord>(
     allowRenderHtml = true,
     onContextMenu,
     emitCrossFilters,
+    isUsingTimeComparison,
+    basicColorFormatters,
+    basicColorColumnFormatters,
   } = props;
+  const comparisonColumns = [
+    { key: 'all', label: t('Display all') },
+    { key: '#', label: '#' },
+    { key: '△', label: '△' },
+    { key: '%', label: '%' },
+  ];
   const timestampFormatter = useCallback(
     value => getTimeFormatterForGranularity(timeGrain)(value),
     [timeGrain],
@@ -253,6 +276,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
   });
   // keep track of whether column order changed, so that column widths can too
   const [columnOrderToggle, setColumnOrderToggle] = useState(false);
+  const [showComparisonDropdown, setShowComparisonDropdown] = useState(false);
+  const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
+    comparisonColumns[0].key,
+  ]);
+  const [hideComparisonKeys, setHideComparisonKeys] = useState<string[]>([]);
+  const theme = useTheme();
 
   // only take relevant page size options
   const pageSizeOptions = useMemo(() => {
@@ -362,16 +391,46 @@ export default function TableChart<D extends DataRecord = DataRecord>(
 
   const getSharedStyle = (column: DataColumnMeta): CSSProperties => {
     const { isNumeric, config = {} } = column;
-    const textAlign = config.horizontalAlign
-      ? config.horizontalAlign
-      : isNumeric
-        ? 'right'
-        : 'left';
+    const textAlign =
+      config.horizontalAlign ||
+      (isNumeric && !isUsingTimeComparison ? 'right' : 'left');
     return {
       textAlign,
     };
   };
 
+  const comparisonLabels = [t('Main'), '#', '△', '%'];
+  const filteredColumnsMeta = useMemo(() => {
+    if (!isUsingTimeComparison) {
+      return columnsMeta;
+    }
+    const allColumns = comparisonColumns[0].key;
+    const main = comparisonLabels[0];
+    const showAllColumns = selectedComparisonColumns.includes(allColumns);
+
+    return columnsMeta.filter(({ label, key }) => {
+      // Extract the key portion after the space, assuming the format is always "label key"
+      const keyPortion = key.substring(label.length);
+      const isKeyHidded = hideComparisonKeys.includes(keyPortion);
+      const isLableMain = label === main;
+
+      return (
+        isLableMain ||
+        (!isKeyHidded &&
+          (!comparisonLabels.includes(label) ||
+            showAllColumns ||
+            selectedComparisonColumns.includes(label)))
+      );
+    });
+  }, [
+    columnsMeta,
+    comparisonColumns,
+    comparisonLabels,
+    isUsingTimeComparison,
+    hideComparisonKeys,
+    selectedComparisonColumns,
+  ]);
+
   const handleContextMenu =
     onContextMenu && !isRawRecords
       ? (
@@ -385,7 +444,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
           clientY: number,
         ) => {
           const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
-          columnsMeta.forEach(col => {
+          filteredColumnsMeta.forEach(col => {
             if (!col.isMetric) {
               const dataRecordValue = value[col.key];
               drillToDetailFilters.push({
@@ -417,6 +476,198 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         }
       : undefined;
 
+  const getHeaderColumns = (
+    columnsMeta: DataColumnMeta[],
+    enableTimeComparison?: boolean,
+  ) => {
+    const resultMap: Record<string, number[]> = {};
+
+    if (!enableTimeComparison) {
+      return resultMap;
+    }
+
+    columnsMeta.forEach((element, index) => {
+      // Check if element's label is one of the comparison labels
+      if (comparisonLabels.includes(element.label)) {
+        // Extract the key portion after the space, assuming the format is always "label key"
+        const keyPortion = element.key.substring(element.label.length);
+
+        // If the key portion is not in the map, initialize it with the current index
+        if (!resultMap[keyPortion]) {
+          resultMap[keyPortion] = [index];
+        } else {
+          // Add the index to the existing array
+          resultMap[keyPortion].push(index);
+        }
+      }
+    });
+
+    return resultMap;
+  };
+
+  const renderTimeComparisonDropdown = (): JSX.Element => {
+    const allKey = comparisonColumns[0].key;
+    const handleOnClick = (data: any) => {
+      const { key } = data;
+      // Toggle 'All' key selection
+      if (key === allKey) {
+        setSelectedComparisonColumns([allKey]);
+      } else if (selectedComparisonColumns.includes(allKey)) {
+        setSelectedComparisonColumns([key]);
+      } else {
+        // Toggle selection for other keys
+        setSelectedComparisonColumns(
+          selectedComparisonColumns.includes(key)
+            ? selectedComparisonColumns.filter(k => k !== key) // Deselect if already selected
+            : [...selectedComparisonColumns, key],
+        ); // Select if not already selected
+      }
+    };
+
+    const handleOnBlur = () => {
+      if (selectedComparisonColumns.length === 3) {
+        setSelectedComparisonColumns([comparisonColumns[0].key]);
+      }
+    };
+
+    return (
+      <Dropdown
+        placement="bottomRight"
+        visible={showComparisonDropdown}
+        onVisibleChange={(flag: boolean) => {
+          setShowComparisonDropdown(flag);
+        }}
+        overlay={
+          <Menu
+            multiple
+            onClick={handleOnClick}
+            onBlur={handleOnBlur}
+            selectedKeys={selectedComparisonColumns}
+          >
+            <div
+              css={css`
+                max-width: 242px;
+                padding: 0 ${theme.gridUnit * 2}px;
+                color: ${theme.colors.grayscale.base};
+                font-size: ${theme.typography.sizes.s}px;
+              `}
+            >
+              {t(
+                'Select columns that will be displayed in the table. You can multiselect columns.',
+              )}
+            </div>
+            {comparisonColumns.map(column => (
+              <Menu.Item key={column.key}>
+                <span
+                  css={css`
+                    color: ${theme.colors.grayscale.dark2};
+                  `}
+                >
+                  {column.label}
+                </span>
+                <span
+                  css={css`
+                    float: right;
+                    font-size: ${theme.typography.sizes.s}px;
+                  `}
+                >
+                  {selectedComparisonColumns.includes(column.key) && (
+                    <CheckOutlined />
+                  )}
+                </span>
+              </Menu.Item>
+            ))}
+          </Menu>
+        }
+        trigger={['click']}
+      >
+        <span>
+          <TableOutlined /> <DownOutlined />
+        </span>
+      </Dropdown>
+    );
+  };
+
+  const renderGroupingHeaders = (): JSX.Element => {
+    // TODO: Make use of ColumnGroup to render the aditional headers
+    const headers: any = [];
+    let currentColumnIndex = 0;
+
+    Object.entries(groupHeaderColumns || {}).forEach(([key, value]) => {
+      // Calculate the number of placeholder columns needed before the current header
+      const startPosition = value[0];
+      const colSpan = value.length;
+
+      // Add placeholder <th> for columns before this header
+      for (let i = currentColumnIndex; i < startPosition; i += 1) {
+        headers.push(
+          <th
+            key={`placeholder-${i}`}
+            style={{ borderBottom: 0 }}
+            aria-label={`Header-${i}`}
+          />,
+        );
+      }
+
+      // Add the current header <th>
+      headers.push(
+        <th key={`header-${key}`} colSpan={colSpan} style={{ borderBottom: 0 }}>
+          {key}
+          <span
+            css={css`
+              float: right;
+              & svg {
+                color: ${theme.colors.grayscale.base} !important;
+              }
+            `}
+          >
+            {hideComparisonKeys.includes(key) ? (
+              <PlusCircleOutlined
+                onClick={() =>
+                  setHideComparisonKeys(
+                    hideComparisonKeys.filter(k => k !== key),
+                  )
+                }
+              />
+            ) : (
+              <MinusCircleOutlined
+                onClick={() =>
+                  setHideComparisonKeys([...hideComparisonKeys, key])
+                }
+              />
+            )}
+          </span>
+        </th>,
+      );
+
+      // Update the current column index
+      currentColumnIndex = startPosition + colSpan;
+    });
+
+    return (
+      <tr
+        css={css`
+          th {
+            border-right: 2px solid ${theme.colors.grayscale.light2};
+          }
+          th:first-child {
+            border-left: none;
+          }
+          th:last-child {
+            border-right: none;
+          }
+        `}
+      >
+        {headers}
+      </tr>
+    );
+  };
+
+  const groupHeaderColumns = useMemo(
+    () => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
+    [filteredColumnsMeta, isUsingTimeComparison],
+  );
+
   const getColumnConfigs = useCallback(
     (column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
       const {
@@ -451,7 +702,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         Array.isArray(columnColorFormatters) &&
         columnColorFormatters.length > 0;
 
+      const hasBasicColorFormatters =
+        isUsingTimeComparison &&
+        Array.isArray(basicColorFormatters) &&
+        basicColorFormatters.length > 0;
+
       const valueRange =
+        !hasBasicColorFormatters &&
         !hasColumnColorFormatters &&
         (config.showCellBars === undefined
           ? showCellBars
@@ -464,6 +721,16 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         className += ' dt-is-filter';
       }
 
+      if (!isMetric && !isPercentMetric) {
+        className += ' right-border-only';
+      } else if (comparisonLabels.includes(label)) {
+        const groupinHeader = key.substring(label.length);
+        const columnsUnderHeader = groupHeaderColumns[groupinHeader] || [];
+        if (i === columnsUnderHeader[columnsUnderHeader.length - 1]) {
+          className += ' right-border-only';
+        }
+      }
+
       return {
         id: String(i), // to allow duplicate column keys
         // must use custom accessor to allow `.` in column names
@@ -475,6 +742,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
           const html = isHtml && allowRenderHtml ? { __html: text } : undefined;
 
           let backgroundColor;
+          let arrow = '';
+          const originKey = column.key.substring(column.label.length).trim();
+          if (!hasColumnColorFormatters && hasBasicColorFormatters) {
+            backgroundColor =
+              basicColorFormatters[row.index][originKey]?.backgroundColor;
+            arrow =
+              column.label === comparisonLabels[0]
+                ? basicColorFormatters[row.index][originKey]?.mainArrow
+                : '';
+          }
+
           if (hasColumnColorFormatters) {
             columnColorFormatters!
               .filter(formatter => formatter.column === column.key)
@@ -489,6 +767,19 @@ export default function TableChart<D extends DataRecord = DataRecord>(
               });
           }
 
+          if (
+            basicColorColumnFormatters &&
+            basicColorColumnFormatters?.length > 0
+          ) {
+            backgroundColor =
+              basicColorColumnFormatters[row.index][column.key]
+                ?.backgroundColor || backgroundColor;
+            arrow =
+              column.label === comparisonLabels[0]
+                ? basicColorColumnFormatters[row.index][column.key]?.mainArrow
+                : '';
+          }
+
           const StyledCell = styled.td`
             text-align: ${sharedStyle.textAlign};
             white-space: ${value instanceof Date ? 'nowrap' : undefined};
@@ -520,6 +811,28 @@ export default function TableChart<D extends DataRecord = DataRecord>(
               `}
           `;
 
+          let arrowStyles = css`
+            color: ${basicColorFormatters &&
+            basicColorFormatters[row.index][originKey]?.arrowColor ===
+              ColorSchemeEnum.Green
+              ? theme.colors.success.base
+              : theme.colors.error.base};
+            margin-right: ${theme.gridUnit}px;
+          `;
+
+          if (
+            basicColorColumnFormatters &&
+            basicColorColumnFormatters?.length > 0
+          ) {
+            arrowStyles = css`
+              color: ${basicColorColumnFormatters[row.index][column.key]
+                ?.arrowColor === ColorSchemeEnum.Green
+                ? theme.colors.success.base
+                : theme.colors.error.base};
+              margin-right: ${theme.gridUnit}px;
+            `;
+          }
+
           const cellProps = {
             'aria-labelledby': `header-${column.key}`,
             role: 'cell',
@@ -589,10 +902,14 @@ export default function TableChart<D extends DataRecord = DataRecord>(
                   className="dt-truncate-cell"
                   style={columnWidth ? { width: columnWidth } : undefined}
                 >
+                  {arrow && <span css={arrowStyles}>{arrow}</span>}
                   {text}
                 </div>
               ) : (
-                text
+                <>
+                  {arrow && <span css={arrowStyles}>{arrow}</span>}
+                  {text}
+                </>
               )}
             </StyledCell>
           );
@@ -676,8 +993,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
   );
 
   const columns = useMemo(
-    () => columnsMeta.map(getColumnConfigs),
-    [columnsMeta, getColumnConfigs],
+    () => filteredColumnsMeta.map(getColumnConfigs),
+    [filteredColumnsMeta, getColumnConfigs],
   );
 
   const handleServerPaginationChange = useCallback(
@@ -744,6 +1061,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
         selectPageSize={pageSize !== null && SelectPageSize}
         // not in use in Superset, but needed for unit tests
         sticky={sticky}
+        renderGroupingHeaders={
+          !isEmpty(groupHeaderColumns) ? renderGroupingHeaders : undefined
+        }
+        renderTimeComparisonDropdown={
+          isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
+        }
       />
     </Styles>
   );
diff --git a/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
index 69631a5f35..a74d875d81 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/buildQuery.ts
@@ -21,13 +21,21 @@ import {
   buildQueryContext,
   ensureIsArray,
   getMetricLabel,
+  getTimeOffset,
   isPhysicalColumn,
+  parseDttmToDate,
   QueryMode,
   QueryObject,
   removeDuplicates,
+  SimpleAdhocFilter,
 } from '@superset-ui/core';
 import { PostProcessingRule } from '@superset-ui/core/src/query/types/PostProcessing';
 import { BuildQuery } from '@superset-ui/core/src/chart/registries/ChartBuildQueryRegistrySingleton';
+import {
+  isTimeComparison,
+  timeCompareOperator,
+} from '@superset-ui/chart-controls';
+import { isEmpty } from 'lodash';
 import { TableChartFormData } from './types';
 import { updateExternalFormData } from './DataTable/utils/externalAPIs';
 
@@ -69,9 +77,51 @@ const buildQuery: BuildQuery<TableChartFormData> = (
     };
   }
 
+  const addComparisonPercentMetrics = (metrics: string[], suffixes: string[]) =>
+    metrics.reduce<string[]>((acc, metric) => {
+      const newMetrics = suffixes.map(suffix => `${metric}__${suffix}`);
+      return acc.concat([metric, ...newMetrics]);
+    }, []);
+
   return buildQueryContext(formDataCopy, baseQueryObject => {
     let { metrics, orderby = [], columns = [] } = baseQueryObject;
+    const { extras = {} } = baseQueryObject;
     let postProcessing: PostProcessingRule[] = [];
+    const TimeRangeFilters =
+      formData.adhoc_filters?.filter(
+        (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+      ) || [];
+
+    // In case the viz is using all version of controls, we try to load them
+    const previousCustomTimeRangeFilters: any =
+      formData.adhoc_custom?.filter(
+        (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+      ) || [];
+
+    let previousCustomStartDate = '';
+    if (
+      !isEmpty(previousCustomTimeRangeFilters) &&
+      previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
+    ) {
+      previousCustomStartDate =
+        previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
+    }
+
+    const timeOffsets = ensureIsArray(
+      isTimeComparison(formData, baseQueryObject)
+        ? getTimeOffset({
+            timeRangeFilter: TimeRangeFilters[0],
+            shifts: formData.time_compare,
+            startDate:
+              previousCustomStartDate && !formData.start_date_offset
+                ? parseDttmToDate(previousCustomStartDate)?.toUTCString()
+                : formData.start_date_offset,
+          })
+        : [],
+    );
+
+    let temporalColumAdded = false;
+    let temporalColum = null;
 
     if (queryMode === QueryMode.Aggregate) {
       metrics = metrics || [];
@@ -85,8 +135,17 @@ const buildQuery: BuildQuery<TableChartFormData> = (
       }
       // add postprocessing for percent metrics only when in aggregation mode
       if (percentMetrics && percentMetrics.length > 0) {
+        const percentMetricsLabelsWithTimeComparison = isTimeComparison(
+          formData,
+          baseQueryObject,
+        )
+          ? addComparisonPercentMetrics(
+              percentMetrics.map(getMetricLabel),
+              timeOffsets,
+            )
+          : percentMetrics.map(getMetricLabel);
         const percentMetricLabels = removeDuplicates(
-          percentMetrics.map(getMetricLabel),
+          percentMetricsLabelsWithTimeComparison,
         );
         metrics = removeDuplicates(
           metrics.concat(percentMetrics),
@@ -102,23 +161,38 @@ const buildQuery: BuildQuery<TableChartFormData> = (
           },
         ];
       }
+      // Add the operator for the time comparison if some is selected
+      if (!isEmpty(timeOffsets)) {
+        postProcessing.push(timeCompareOperator(formData, baseQueryObject));
+      }
+
+      const temporalColumnsLookup = formData?.temporal_columns_lookup;
+      // Filter out the column if needed and prepare the temporal column object
 
-      columns = columns.map(col => {
-        if (
+      columns = columns.filter(col => {
+        const shouldBeAdded =
           isPhysicalColumn(col) &&
           time_grain_sqla &&
-          formData?.temporal_columns_lookup?.[col]
-        ) {
-          return {
+          temporalColumnsLookup?.[col];
+
+        if (shouldBeAdded && !temporalColumAdded) {
+          temporalColum = {
             timeGrain: time_grain_sqla,
             columnType: 'BASE_AXIS',
             sqlExpression: col,
             label: col,
             expressionType: 'SQL',
           } as AdhocColumn;
+          temporalColumAdded = true;
+          return false; // Do not include this in the output; it's added separately
         }
-        return col;
+        return true;
       });
+
+      // So we ensure the temporal column is added first
+      if (temporalColum) {
+        columns = [temporalColum, ...columns];
+      }
     }
 
     const moreProps: Partial<QueryObject> = {};
@@ -133,9 +207,11 @@ const buildQuery: BuildQuery<TableChartFormData> = (
     let queryObject = {
       ...baseQueryObject,
       columns,
+      extras: !isEmpty(timeOffsets) && !temporalColum ? {} : extras,
       orderby,
       metrics,
       post_processing: postProcessing,
+      time_offsets: timeOffsets,
       ...moreProps,
     };
 
@@ -169,6 +245,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
         row_limit: 0,
         row_offset: 0,
         post_processing: [],
+        extras: undefined, // we don't need time grain here
         order_desc: undefined, // we don't need orderby stuff here,
         orderby: undefined, // because this query will be used for get total aggregation.
       });
@@ -186,6 +263,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
         { ...queryObject },
         {
           ...queryObject,
+          time_offsets: [],
           row_limit: 0,
           row_offset: 0,
           post_processing: [],
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index cd86dbbe88..1e541a4436 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -43,9 +43,12 @@ import {
   ColumnMeta,
   defineSavedMetrics,
   getStandardizedControls,
+  sections,
 } from '@superset-ui/chart-controls';
 
+import { isEmpty } from 'lodash';
 import { PAGE_SIZE_OPTIONS } from './consts';
+import { ColorSchemeEnum } from './types';
 
 function getQueryMode(controls: ControlStateMapping): QueryMode {
   const mode = controls?.query_mode?.value;
@@ -142,6 +145,33 @@ const percentMetricsControl: typeof sharedControls.metrics = {
   validators: [],
 };
 
+const processComparisonColumns = (columns: any[], suffix: string) =>
+  columns
+    .map(col => {
+      if (!col.label.includes(suffix)) {
+        return [
+          {
+            label: `${t('Main')} ${col.label}`,
+            value: `${t('Main')} ${col.value}`,
+          },
+          {
+            label: `# ${col.label}`,
+            value: `# ${col.value}`,
+          },
+          {
+            label: `△ ${col.label}`,
+            value: `△ ${col.value}`,
+          },
+          {
+            label: `% ${col.label}`,
+            value: `% ${col.value}`,
+          },
+        ];
+      }
+      return [];
+    })
+    .flat();
+
 const config: ControlPanelConfig = {
   controlPanelSections: [
     {
@@ -397,11 +427,70 @@ const config: ControlPanelConfig = {
               description: t('Whether to include a client-side search box'),
             },
           },
+        ],
+        [
+          {
+            name: 'allow_rearrange_columns',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Allow columns to be rearranged'),
+              renderTrigger: true,
+              default: false,
+              description: t(
+                "Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.",
+              ),
+              visibility: ({ controls }) =>
+                isEmpty(controls?.time_compare?.value),
+            },
+          },
+        ],
+        [
+          {
+            name: 'allow_render_html',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Render columns in HTML format'),
+              renderTrigger: true,
+              default: true,
+              description: t('Render data in HTML format if applicable.'),
+            },
+          },
+        ],
+        [
+          {
+            name: 'column_config',
+            config: {
+              type: 'ColumnConfigControl',
+              label: t('Customize columns'),
+              description: t('Further customize how to display each column'),
+              width: 400,
+              height: 320,
+              renderTrigger: true,
+              shouldMapStateToProps() {
+                return true;
+              },
+              mapStateToProps(explore, _, chart) {
+                return {
+                  queryResponse: chart?.queriesResponse?.[0] as
+                    | ChartDataResponseResult
+                    | undefined,
+                };
+              },
+            },
+          },
+        ],
+      ],
+    },
+    {
+      label: t('Visual formatting'),
+      expanded: true,
+      controlSetRows: [
+        [
           {
             name: 'show_cell_bars',
             config: {
               type: 'CheckboxControl',
-              label: t('Cell bars'),
+              label: t('Show Cell bars'),
               renderTrigger: true,
               default: true,
               description: t(
@@ -423,11 +512,13 @@ const config: ControlPanelConfig = {
               ),
             },
           },
+        ],
+        [
           {
             name: 'color_pn',
             config: {
               type: 'CheckboxControl',
-              label: t('Color +/-'),
+              label: t('add colors to cell bars for +/-'),
               renderTrigger: true,
               default: true,
               description: t(
@@ -438,50 +529,41 @@ const config: ControlPanelConfig = {
         ],
         [
           {
-            name: 'allow_rearrange_columns',
+            name: 'comparison_color_enabled',
             config: {
               type: 'CheckboxControl',
-              label: t('Allow columns to be rearranged'),
+              label: t('basic conditional formatting'),
               renderTrigger: true,
+              visibility: ({ controls }) =>
+                !isEmpty(controls?.time_compare?.value),
               default: false,
               description: t(
-                "Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.",
+                'This will be applied to the whole table. Arrows (↑ and ↓) will be added to ' +
+                  'main columns for increase and decrease. Basic conditional formatting can be ' +
+                  'overwritten by conditional formatting below.',
               ),
             },
           },
         ],
         [
           {
-            name: 'allow_render_html',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Render columns in HTML format'),
-              renderTrigger: true,
-              default: true,
-              description: t('Render data in HTML format if applicable.'),
-            },
-          },
-        ],
-        [
-          {
-            name: 'column_config',
+            name: 'comparison_color_scheme',
             config: {
-              type: 'ColumnConfigControl',
-              label: t('Customize columns'),
-              description: t('Further customize how to display each column'),
-              width: 400,
-              height: 320,
+              type: 'SelectControl',
+              label: t('color type'),
+              default: ColorSchemeEnum.Green,
               renderTrigger: true,
-              shouldMapStateToProps() {
-                return true;
-              },
-              mapStateToProps(explore, _, chart) {
-                return {
-                  queryResponse: chart?.queriesResponse?.[0] as
-                    | ChartDataResponseResult
-                    | undefined,
-                };
-              },
+              choices: [
+                [ColorSchemeEnum.Green, 'Green for increase, red for decrease'],
+                [ColorSchemeEnum.Red, 'Red for increase, green for decrease'],
+              ],
+              visibility: ({ controls }) =>
+                !isEmpty(controls?.time_compare?.value) &&
+                Boolean(controls?.comparison_color_enabled?.value),
+              description: t(
+                'Adds color to the chart symbols based on the positive or ' +
+                  'negative change from the comparison value.',
+              ),
             },
           },
         ],
@@ -491,7 +573,17 @@ const config: ControlPanelConfig = {
             config: {
               type: 'ConditionalFormattingControl',
               renderTrigger: true,
-              label: t('Conditional formatting'),
+              label: t('Custom Conditional Formatting'),
+              extraColorChoices: [
+                {
+                  value: ColorSchemeEnum.Green,
+                  label: t('Green for increase, red for decrease'),
+                },
+                {
+                  value: ColorSchemeEnum.Red,
+                  label: t('Red for increase, green for decrease'),
+                },
+              ],
               description: t(
                 'Apply conditional color formatting to numeric columns',
               ),
@@ -519,9 +611,18 @@ const config: ControlPanelConfig = {
                           label: verboseMap[colname] ?? colname,
                         }))
                     : [];
+                const columnOptions = explore?.controls?.time_compare?.value
+                  ? processComparisonColumns(
+                      numericColumns || [],
+                      ensureIsArray(
+                        explore?.controls?.time_compare?.value,
+                      )[0]?.toString() || '',
+                    )
+                  : numericColumns;
+
                 return {
                   removeIrrelevantConditions: chartStatus === 'success',
-                  columnOptions: numericColumns,
+                  columnOptions,
                   verboseMap,
                 };
               },
@@ -530,6 +631,14 @@ const config: ControlPanelConfig = {
         ],
       ],
     },
+    {
+      ...sections.timeComparisonControls({
+        multi: false,
+        showCalculationType: false,
+        showFullChoices: false,
+      }),
+      visibility: isAggMode,
+    },
   ],
   formDataOverrides: formData => ({
     ...formData,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index ec86814f98..9647ffd6bb 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -18,8 +18,10 @@
  */
 import memoizeOne from 'memoize-one';
 import {
+  ComparisonType,
   CurrencyFormatter,
   DataRecord,
+  ensureIsArray,
   extractTimegrain,
   GenericDataType,
   getMetricLabel,
@@ -28,18 +30,26 @@ import {
   getTimeFormatterForGranularity,
   NumberFormats,
   QueryMode,
+  t,
   SMART_DATE_ID,
   TimeFormats,
   TimeFormatter,
+  SimpleAdhocFilter,
+  getTimeOffset,
+  parseDttmToDate,
 } from '@superset-ui/core';
 import {
   ColorFormatters,
+  ConditionalFormattingConfig,
   getColorFormatters,
 } from '@superset-ui/chart-controls';
 
+import { isEmpty } from 'lodash';
 import isEqualColumns from './utils/isEqualColumns';
 import DateWithFormatter from './utils/DateWithFormatter';
 import {
+  BasicColorFormatterType,
+  ColorSchemeEnum,
   DataColumnMeta,
   TableChartProps,
   TableChartTransformedProps,
@@ -81,6 +91,103 @@ const processDataRecords = memoizeOne(function processDataRecords(
   return data;
 });
 
+const calculateDifferences = (
+  originalValue: number,
+  comparisonValue: number,
+) => {
+  const valueDifference = originalValue - comparisonValue;
+  let percentDifferenceNum;
+  if (!originalValue && !comparisonValue) {
+    percentDifferenceNum = 0;
+  } else if (!originalValue || !comparisonValue) {
+    percentDifferenceNum = originalValue ? 1 : -1;
+  } else {
+    percentDifferenceNum =
+      (originalValue - comparisonValue) / Math.abs(comparisonValue);
+  }
+  return { valueDifference, percentDifferenceNum };
+};
+
+const processComparisonTotals = (
+  comparisonSuffix: string,
+  totals?: DataRecord[],
+): DataRecord | undefined => {
+  if (!totals) {
+    return totals;
+  }
+  const transformedTotals: DataRecord = {};
+  totals.map((totalRecord: DataRecord) =>
+    Object.keys(totalRecord).forEach(key => {
+      if (totalRecord[key] !== undefined && !key.includes(comparisonSuffix)) {
+        transformedTotals[`Main ${key}`] =
+          parseInt(transformedTotals[`Main ${key}`]?.toString() || '0', 10) +
+          parseInt(totalRecord[key]?.toString() || '0', 10);
+        transformedTotals[`# ${key}`] =
+          parseInt(transformedTotals[`# ${key}`]?.toString() || '0', 10) +
+          parseInt(
+            totalRecord[`${key}__${comparisonSuffix}`]?.toString() || '0',
+            10,
+          );
+        const { valueDifference, percentDifferenceNum } = calculateDifferences(
+          transformedTotals[`Main ${key}`] as number,
+          transformedTotals[`# ${key}`] as number,
+        );
+        transformedTotals[`△ ${key}`] = valueDifference;
+        transformedTotals[`% ${key}`] = percentDifferenceNum;
+      }
+    }),
+  );
+
+  return transformedTotals;
+};
+
+const processComparisonDataRecords = memoizeOne(
+  function processComparisonDataRecords(
+    originalData: DataRecord[] | undefined,
+    originalColumns: DataColumnMeta[],
+    comparisonSuffix: string,
+  ) {
+    // Transform data
+    return originalData?.map(originalItem => {
+      const transformedItem: DataRecord = {};
+      originalColumns.forEach(origCol => {
+        if (
+          (origCol.isMetric || origCol.isPercentMetric) &&
+          !origCol.key.includes(comparisonSuffix) &&
+          origCol.isNumeric
+        ) {
+          const originalValue = originalItem[origCol.key] || 0;
+          const comparisonValue = origCol.isMetric
+            ? originalItem?.[`${origCol.key}__${comparisonSuffix}`] || 0
+            : originalItem[`%${origCol.key.slice(1)}__${comparisonSuffix}`] ||
+              0;
+          const { valueDifference, percentDifferenceNum } =
+            calculateDifferences(
+              originalValue as number,
+              comparisonValue as number,
+            );
+
+          transformedItem[`Main ${origCol.key}`] = originalValue;
+          transformedItem[`# ${origCol.key}`] = comparisonValue;
+          transformedItem[`△ ${origCol.key}`] = valueDifference;
+          transformedItem[`% ${origCol.key}`] = percentDifferenceNum;
+        }
+      });
+
+      Object.keys(originalItem).forEach(key => {
+        const isMetricOrPercentMetric = originalColumns.some(
+          col => col.key === key && (col.isMetric || col.isPercentMetric),
+        );
+        if (!isMetricOrPercentMetric) {
+          transformedItem[key] = originalItem[key];
+        }
+      });
+
+      return transformedItem;
+    });
+  },
+);
+
 const processColumns = memoizeOne(function processColumns(
   props: TableChartProps,
 ) {
@@ -186,6 +293,60 @@ const processColumns = memoizeOne(function processColumns(
   ];
 }, isEqualColumns);
 
+const processComparisonColumns = (
+  columns: DataColumnMeta[],
+  props: TableChartProps,
+  comparisonSuffix: string,
+) =>
+  columns
+    .map(col => {
+      const {
+        datasource: { columnFormats },
+        rawFormData: { column_config: columnConfig = {} },
+      } = props;
+      const config = columnConfig[col.key] || {};
+      const savedFormat = columnFormats?.[col.key];
+      const numberFormat = config.d3NumberFormat || savedFormat;
+      if (
+        (col.isMetric || col.isPercentMetric) &&
+        !col.key.includes(comparisonSuffix) &&
+        col.isNumeric
+      ) {
+        return [
+          {
+            ...col,
+            label: t('Main'),
+            key: `${t('Main')} ${col.key}`,
+          },
+          {
+            ...col,
+            label: `#`,
+            key: `# ${col.key}`,
+          },
+          {
+            ...col,
+            label: `△`,
+            key: `△ ${col.key}`,
+          },
+          {
+            ...col,
+            formatter: getNumberFormatter(numberFormat || PERCENT_3_POINT),
+            label: `%`,
+            key: `% ${col.key}`,
+          },
+        ];
+      }
+      if (
+        !col.isMetric &&
+        !col.isPercentMetric &&
+        !col.key.includes(comparisonSuffix)
+      ) {
+        return [col];
+      }
+      return [];
+    })
+    .flat();
+
 /**
  * Automatically set page size based on number of cells.
  */
@@ -239,10 +400,168 @@ const transformProps = (
     conditional_formatting: conditionalFormatting,
     allow_rearrange_columns: allowRearrangeColumns,
     allow_render_html: allowRenderHtml,
+    time_compare,
+    comparison_color_enabled: comparisonColorEnabled = false,
+    comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
+    comparison_type,
   } = formData;
+  const isUsingTimeComparison =
+    !isEmpty(time_compare) &&
+    queryMode === QueryMode.Aggregate &&
+    comparison_type === ComparisonType.Values;
+
+  const calculateBasicStyle = (
+    percentDifferenceNum: number,
+    colorOption: ColorSchemeEnum,
+  ) => {
+    if (percentDifferenceNum === 0) {
+      return {
+        arrow: '',
+        arrowColor: '',
+        // eslint-disable-next-line theme-colors/no-literal-colors
+        backgroundColor: 'rgba(0,0,0,0.2)',
+      };
+    }
+    const isPositive = percentDifferenceNum > 0;
+    const arrow = isPositive ? '↑' : '↓';
+    const arrowColor =
+      colorOption === ColorSchemeEnum.Green
+        ? isPositive
+          ? ColorSchemeEnum.Green
+          : ColorSchemeEnum.Red
+        : isPositive
+          ? ColorSchemeEnum.Red
+          : ColorSchemeEnum.Green;
+    const backgroundColor =
+      colorOption === ColorSchemeEnum.Green
+        ? `rgba(${isPositive ? '0,150,0' : '150,0,0'},0.2)`
+        : `rgba(${isPositive ? '150,0,0' : '0,150,0'},0.2)`;
+
+    return { arrow, arrowColor, backgroundColor };
+  };
+
+  const getBasicColorFormatter = memoizeOne(function getBasicColorFormatter(
+    originalData: DataRecord[] | undefined,
+    originalColumns: DataColumnMeta[],
+    selectedColumns?: ConditionalFormattingConfig[],
+  ) {
+    // Transform data
+    const relevantColumns = selectedColumns
+      ? originalColumns.filter(col =>
+          selectedColumns.some(scol => scol?.column?.includes(col.key)),
+        )
+      : originalColumns;
+
+    return originalData?.map(originalItem => {
+      const item: { [key: string]: BasicColorFormatterType } = {};
+      relevantColumns.forEach(origCol => {
+        if (
+          (origCol.isMetric || origCol.isPercentMetric) &&
+          !origCol.key.includes(ensureIsArray(timeOffsets)[0]) &&
+          origCol.isNumeric
+        ) {
+          const originalValue = originalItem[origCol.key] || 0;
+          const comparisonValue = origCol.isMetric
+            ? originalItem?.[
+                `${origCol.key}__${ensureIsArray(timeOffsets)[0]}`
+              ] || 0
+            : originalItem[
+                `%${origCol.key.slice(1)}__${ensureIsArray(timeOffsets)[0]}`
+              ] || 0;
+          const { percentDifferenceNum } = calculateDifferences(
+            originalValue as number,
+            comparisonValue as number,
+          );
+
+          if (selectedColumns) {
+            selectedColumns.forEach(col => {
+              if (col?.column?.includes(origCol.key)) {
+                const { arrow, arrowColor, backgroundColor } =
+                  calculateBasicStyle(
+                    percentDifferenceNum,
+                    col.colorScheme || comparisonColorScheme,
+                  );
+                item[col.column] = {
+                  mainArrow: arrow,
+                  arrowColor,
+                  backgroundColor,
+                };
+              }
+            });
+          } else {
+            const { arrow, arrowColor, backgroundColor } = calculateBasicStyle(
+              percentDifferenceNum,
+              comparisonColorScheme,
+            );
+            item[`${origCol.key}`] = {
+              mainArrow: arrow,
+              arrowColor,
+              backgroundColor,
+            };
+          }
+        }
+      });
+      return item;
+    });
+  });
+
+  const getBasicColorFormatterForColumn = (
+    originalData: DataRecord[] | undefined,
+    originalColumns: DataColumnMeta[],
+    conditionalFormatting?: ConditionalFormattingConfig[],
+  ) => {
+    const selectedColumns = conditionalFormatting?.filter(
+      (config: ConditionalFormattingConfig) =>
+        config.column &&
+        (config.colorScheme === ColorSchemeEnum.Green ||
+          config.colorScheme === ColorSchemeEnum.Red),
+    );
+
+    return selectedColumns?.length
+      ? getBasicColorFormatter(originalData, originalColumns, selectedColumns)
+      : undefined;
+  };
+
   const timeGrain = extractTimegrain(formData);
+  const TimeRangeFilters =
+    chartProps.rawFormData?.adhoc_filters?.filter(
+      (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+    ) || [];
+  const previousCustomTimeRangeFilters: any =
+    chartProps.rawFormData?.adhoc_custom?.filter(
+      (filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
+    ) || [];
+
+  let previousCustomStartDate = '';
+  if (
+    !isEmpty(previousCustomTimeRangeFilters) &&
+    previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
+  ) {
+    previousCustomStartDate =
+      previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
+  }
+
+  const timeOffsets = getTimeOffset({
+    timeRangeFilter: TimeRangeFilters[0],
+    shifts: formData.time_compare,
+    startDate:
+      previousCustomStartDate && !formData.start_date_offset
+        ? parseDttmToDate(previousCustomStartDate)?.toUTCString()
+        : formData.start_date_offset,
+  });
+  const comparisonSuffix = isUsingTimeComparison
+    ? ensureIsArray(timeOffsets)[0]
+    : '';
 
   const [metrics, percentMetrics, columns] = processColumns(chartProps);
+  let comparisonColumns: DataColumnMeta[] = [];
+  if (isUsingTimeComparison) {
+    comparisonColumns = processComparisonColumns(
+      columns,
+      chartProps,
+      comparisonSuffix,
+    );
+  }
 
   let baseQuery;
   let countQuery;
@@ -256,20 +575,41 @@ const transformProps = (
     rowCount = baseQuery?.rowcount ?? 0;
   }
   const data = processDataRecords(baseQuery?.data, columns);
+  const comparisonData = processComparisonDataRecords(
+    baseQuery?.data,
+    columns,
+    comparisonSuffix,
+  );
   const totals =
     showTotals && queryMode === QueryMode.Aggregate
-      ? totalQuery?.data[0]
+      ? isUsingTimeComparison
+        ? processComparisonTotals(comparisonSuffix, totalQuery?.data)
+        : totalQuery?.data[0]
       : undefined;
+
+  const passedData = isUsingTimeComparison ? comparisonData || [] : data;
+  const passedColumns = isUsingTimeComparison ? comparisonColumns : columns;
+
+  const basicColorFormatters =
+    comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
   const columnColorFormatters =
-    getColorFormatters(conditionalFormatting, data) ?? defaultColorFormatters;
+    getColorFormatters(conditionalFormatting, passedData) ??
+    defaultColorFormatters;
 
+  const basicColorColumnFormatters = getBasicColorFormatterForColumn(
+    baseQuery?.data,
+    columns,
+    conditionalFormatting,
+  );
+
+  const startDateOffset = chartProps.rawFormData?.start_date_offset;
   return {
     height,
     width,
     isRawRecords: queryMode === QueryMode.Raw,
-    data,
+    data: passedData,
     totals,
-    columns,
+    columns: passedColumns,
     serverPagination,
     metrics,
     percentMetrics,
@@ -294,6 +634,10 @@ const transformProps = (
     allowRearrangeColumns,
     allowRenderHtml,
     onContextMenu,
+    isUsingTimeComparison,
+    basicColorFormatters,
+    startDateOffset,
+    basicColorColumnFormatters,
   };
 };
 
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index 639b525d4f..1ec3cbe29d 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -102,6 +102,12 @@ export interface TableChartProps extends ChartProps {
   queriesData: ChartDataResponseResult[];
 }
 
+export type BasicColorFormatterType = {
+  backgroundColor: string;
+  arrowColor: string;
+  mainArrow: string;
+};
+
 export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
   timeGrain?: TimeGranularity;
   height: number;
@@ -136,6 +142,15 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
     clientY: number,
     filters?: ContextMenuFilters,
   ) => void;
+  isUsingTimeComparison?: boolean;
+  basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
+  basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
+  startDateOffset?: string;
+}
+
+export enum ColorSchemeEnum {
+  'Green' = 'Green',
+  'Red' = 'Red',
 }
 
 export default {};
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx
index fa23001283..53bf98fe90 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx
@@ -66,6 +66,7 @@ export interface WordCloudProps extends WordCloudVisualProps {
   height: number;
   width: number;
   sliceId: number;
+  colorScheme: string;
 }
 
 export interface WordCloudState {
@@ -221,7 +222,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
 
   render() {
     const { scaleFactor } = this.state;
-    const { width, height, encoding, sliceId } = this.props;
+    const { width, height, encoding, sliceId, colorScheme } = this.props;
     const { words } = this.state;
 
     // @ts-ignore
@@ -249,7 +250,11 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
               fontSize={`${w.size}px`}
               fontWeight={w.weight}
               fontFamily={w.font}
-              fill={colorFn(getValueFromDatum(w) as string, sliceId)}
+              fill={colorFn(
+                getValueFromDatum(w) as string,
+                sliceId,
+                colorScheme,
+              )}
               textAnchor="middle"
               transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
             >
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts
index 0b5a36b999..5685edd921 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts
@@ -80,5 +80,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
     rotation,
     width,
     sliceId,
+    colorScheme,
   };
 }
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts
index 3ba153d246..9425f12117 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts
@@ -32,18 +32,7 @@ const config: ControlPanelConfig = {
         ['metric'],
         ['adhoc_filters'],
         ['row_limit'],
-        [
-          {
-            name: 'sort_by_metric',
-            config: {
-              type: 'CheckboxControl',
-              label: t('Sort by metric'),
-              description: t(
-                'Whether to sort results by the selected metric in descending order.',
-              ),
-            },
-          },
-        ],
+        ['sort_by_metric'],
       ],
     },
     {
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts
index 4ed6f6405c..59f6258538 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts
@@ -23,7 +23,8 @@ import { WordCloudFormData } from '../types';
 
 export default function transformProps(chartProps: ChartProps): WordCloudProps {
   const { width, height, formData, queriesData } = chartProps;
-  const { encoding, rotation, sliceId } = formData as WordCloudFormData;
+  const { encoding, rotation, sliceId, colorScheme } =
+    formData as WordCloudFormData;
 
   return {
     data: queriesData[0].data,
@@ -32,5 +33,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps {
     rotation,
     width,
     sliceId,
+    colorScheme,
   };
 }
diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/legacyPlugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-word-cloud/test/legacyPlugin/transformProps.test.ts
index a086b139a9..a61e7408de 100644
--- a/superset-frontend/plugins/plugin-chart-word-cloud/test/legacyPlugin/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-word-cloud/test/legacyPlugin/transformProps.test.ts
@@ -68,6 +68,7 @@ describe('WordCloud transformProps', () => {
         },
       },
       rotation: 'square',
+      colorScheme: 'bnbColors',
       data: [{ name: 'Hulk', sum__num: 1 }],
     });
   });
diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx
index 61ccc7fb0d..625531f926 100644
--- a/superset-frontend/spec/helpers/testing-library.tsx
+++ b/superset-frontend/spec/helpers/testing-library.tsx
@@ -18,7 +18,13 @@
  */
 import '@testing-library/jest-dom/extend-expect';
 import { ReactNode, ReactElement } from 'react';
-import { render, RenderOptions } from '@testing-library/react';
+import {
+  render,
+  RenderOptions,
+  screen,
+  waitFor,
+  within,
+} from '@testing-library/react';
 import { ThemeProvider, supersetTheme } from '@superset-ui/core';
 import { BrowserRouter } from 'react-router-dom';
 import { Provider } from 'react-redux';
@@ -28,6 +34,7 @@ import reducerIndex from 'spec/helpers/reducerIndex';
 import { QueryParamProvider } from 'use-query-params';
 import { configureStore, Store } from '@reduxjs/toolkit';
 import { api } from 'src/hooks/apiResources/queryApi';
+import userEvent from '@testing-library/user-event';
 
 type Options = Omit<RenderOptions, 'queries'> & {
   useRedux?: boolean;
@@ -102,3 +109,18 @@ export function sleep(time: number) {
 
 export * from '@testing-library/react';
 export { customRender as render };
+
+export async function selectOption(option: string, selectName?: string) {
+  const select = screen.getByRole(
+    'combobox',
+    selectName ? { name: selectName } : {},
+  );
+  await userEvent.click(select);
+  const item = await waitFor(() =>
+    within(
+      // eslint-disable-next-line testing-library/no-node-access
+      document.querySelector('.rc-virtual-list')!,
+    ).getByText(option),
+  );
+  await userEvent.click(item);
+}
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 0a5a655a06..18b74d2afc 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -102,6 +102,8 @@ export const CREATE_DATASOURCE_SUCCESS = 'CREATE_DATASOURCE_SUCCESS';
 export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
 
 export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
+export const SET_LAST_UPDATED_ACTIVE_TAB = 'SET_LAST_UPDATED_ACTIVE_TAB';
+export const CLEAR_DESTROYED_QUERY_EDITOR = 'CLEAR_DESTROYED_QUERY_EDITOR';
 
 export const addInfoToast = addInfoToastAction;
 export const addSuccessToast = addSuccessToastAction;
@@ -134,11 +136,12 @@ export const convertQueryToClient = fieldConverter(queryClientMapping);
 
 export function getUpToDateQuery(rootState, queryEditor, key) {
   const {
-    sqlLab: { unsavedQueryEditor },
+    sqlLab: { unsavedQueryEditor, queryEditors },
   } = rootState;
   const id = key ?? queryEditor.id;
   return {
-    ...queryEditor,
+    id,
+    ...queryEditors.find(qe => qe.id === id),
     ...(id === unsavedQueryEditor.id && unsavedQueryEditor),
   };
 }
@@ -610,29 +613,17 @@ export function cloneQueryToNewTab(query, autorun) {
   };
 }
 
-export function setActiveQueryEditor(queryEditor) {
-  return function (dispatch) {
-    const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
-      ? SupersetClient.post({
-          endpoint: encodeURI(`/tabstateview/${queryEditor.id}/activate`),
-        })
-      : Promise.resolve();
+export function setLastUpdatedActiveTab(queryEditorId) {
+  return {
+    type: SET_LAST_UPDATED_ACTIVE_TAB,
+    queryEditorId,
+  };
+}
 
-    return sync
-      .then(() => dispatch({ type: SET_ACTIVE_QUERY_EDITOR, queryEditor }))
-      .catch(response => {
-        if (response.status !== 404) {
-          return dispatch(
-            addDangerToast(
-              t(
-                'An error occurred while setting the active tab. Please contact ' +
-                  'your administrator.',
-              ),
-            ),
-          );
-        }
-        return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
-      });
+export function setActiveQueryEditor(queryEditor) {
+  return {
+    type: SET_ACTIVE_QUERY_EDITOR,
+    queryEditor,
   };
 }
 
@@ -673,51 +664,42 @@ export function setTables(tableSchemas) {
   return { type: SET_TABLES, tables };
 }
 
-export function switchQueryEditor(queryEditor, displayLimit) {
+export function fetchQueryEditor(queryEditor, displayLimit) {
   return function (dispatch) {
-    if (
-      isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
-      queryEditor &&
-      !queryEditor.loaded
-    ) {
-      SupersetClient.get({
-        endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+    SupersetClient.get({
+      endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+    })
+      .then(({ json }) => {
+        const loadedQueryEditor = {
+          id: json.id.toString(),
+          loaded: true,
+          name: json.label,
+          sql: json.sql,
+          selectedText: null,
+          latestQueryId: json.latest_query?.id,
+          autorun: json.autorun,
+          dbId: json.database_id,
+          templateParams: json.template_params,
+          catalog: json.catalog,
+          schema: json.schema,
+          queryLimit: json.query_limit,
+          remoteId: json.saved_query?.id,
+          hideLeftBar: json.hide_left_bar,
+        };
+        dispatch(loadQueryEditor(loadedQueryEditor));
+        dispatch(setTables(json.table_schemas || []));
+        if (json.latest_query && json.latest_query.resultsKey) {
+          dispatch(fetchQueryResults(json.latest_query, displayLimit));
+        }
       })
-        .then(({ json }) => {
-          const loadedQueryEditor = {
-            id: json.id.toString(),
-            loaded: true,
-            name: json.label,
-            sql: json.sql,
-            selectedText: null,
-            latestQueryId: json.latest_query?.id,
-            autorun: json.autorun,
-            dbId: json.database_id,
-            templateParams: json.template_params,
-            catalog: json.catalog,
-            schema: json.schema,
-            queryLimit: json.query_limit,
-            remoteId: json.saved_query?.id,
-            hideLeftBar: json.hide_left_bar,
-          };
-          dispatch(loadQueryEditor(loadedQueryEditor));
-          dispatch(setTables(json.table_schemas || []));
-          dispatch(setActiveQueryEditor(loadedQueryEditor));
-          if (json.latest_query && json.latest_query.resultsKey) {
-            dispatch(fetchQueryResults(json.latest_query, displayLimit));
-          }
-        })
-        .catch(response => {
-          if (response.status !== 404) {
-            return dispatch(
-              addDangerToast(t('An error occurred while fetching tab state')),
-            );
-          }
-          return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
-        });
-    } else {
-      dispatch(setActiveQueryEditor(queryEditor));
-    }
+      .catch(response => {
+        if (response.status !== 404) {
+          return dispatch(
+            addDangerToast(t('An error occurred while fetching tab state')),
+          );
+        }
+        return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
+      });
   };
 }
 
@@ -734,29 +716,12 @@ export function toggleLeftBar(queryEditor) {
   };
 }
 
-export function removeQueryEditor(queryEditor) {
-  return function (dispatch) {
-    const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
-      ? SupersetClient.delete({
-          endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
-        })
-      : Promise.resolve();
+export function clearDestoryedQueryEditor(queryEditorId) {
+  return { type: CLEAR_DESTROYED_QUERY_EDITOR, queryEditorId };
+}
 
-    return sync
-      .then(() => dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor }))
-      .catch(({ status }) => {
-        if (status !== 404) {
-          return dispatch(
-            addDangerToast(
-              t(
-                'An error occurred while removing tab. Please contact your administrator.',
-              ),
-            ),
-          );
-        }
-        return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
-      });
-  };
+export function removeQueryEditor(queryEditor) {
+  return { type: REMOVE_QUERY_EDITOR, queryEditor };
 }
 
 export function removeAllOtherQueryEditors(queryEditor) {
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 9dca13a11b..11ab424512 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -36,6 +36,29 @@ import {
 const middlewares = [thunk];
 const mockStore = configureMockStore(middlewares);
 
+describe('getUpToDateQuery', () => {
+  test('should return the up to date query editor state', () => {
+    const outOfUpdatedQueryEditor = {
+      ...defaultQueryEditor,
+      schema: null,
+      sql: 'SELECT ...',
+    };
+    const queryEditor = {
+      ...defaultQueryEditor,
+      sql: 'SELECT * FROM table',
+    };
+    const state = {
+      sqlLab: {
+        queryEditors: [queryEditor],
+        unsavedQueryEditor: {},
+      },
+    };
+    expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual(
+      queryEditor,
+    );
+  });
+});
+
 describe('async actions', () => {
   const mockBigNumber = '9223372036854775807';
   const queryEditor = {
@@ -505,6 +528,21 @@ describe('async actions', () => {
     });
   });
 
+  it('set current query editor', () => {
+    expect.assertions(1);
+
+    const store = mockStore(initialState);
+    const expectedActions = [
+      {
+        type: actions.SET_ACTIVE_QUERY_EDITOR,
+        queryEditor: defaultQueryEditor,
+      },
+    ];
+    store.dispatch(actions.setActiveQueryEditor(defaultQueryEditor));
+
+    expect(store.getActions()).toEqual(expectedActions);
+  });
+
   describe('backend sync', () => {
     const updateTabStateEndpoint = 'glob:*/tabstateview/*';
     fetchMock.put(updateTabStateEndpoint, {});
@@ -570,29 +608,9 @@ describe('async actions', () => {
       });
     });
 
-    describe('setActiveQueryEditor', () => {
-      it('updates the tab state in the backend', () => {
-        expect.assertions(2);
-
-        const store = mockStore({});
-        const expectedActions = [
-          {
-            type: actions.SET_ACTIVE_QUERY_EDITOR,
-            queryEditor,
-          },
-        ];
-        return store
-          .dispatch(actions.setActiveQueryEditor(queryEditor))
-          .then(() => {
-            expect(store.getActions()).toEqual(expectedActions);
-            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
-          });
-      });
-    });
-
     describe('removeQueryEditor', () => {
       it('updates the tab state in the backend', () => {
-        expect.assertions(2);
+        expect.assertions(1);
 
         const store = mockStore({});
         const expectedActions = [
@@ -601,12 +619,8 @@ describe('async actions', () => {
             queryEditor,
           },
         ];
-        return store
-          .dispatch(actions.removeQueryEditor(queryEditor))
-          .then(() => {
-            expect(store.getActions()).toEqual(expectedActions);
-            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
-          });
+        store.dispatch(actions.removeQueryEditor(queryEditor));
+        expect(store.getActions()).toEqual(expectedActions);
       });
     });
 
@@ -715,7 +729,13 @@ describe('async actions', () => {
         it('updates the tab state in the backend', () => {
           expect.assertions(2);
 
-          const store = mockStore(initialState);
+          const store = mockStore({
+            ...initialState,
+            sqlLab: {
+              ...initialState.sqlLab,
+              queryEditors: [queryEditor],
+            },
+          });
           const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
           return request(store.dispatch, store.getState).then(() => {
             expect(store.getActions()).toEqual(expectedActions);
@@ -731,7 +751,13 @@ describe('async actions', () => {
               feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
             );
 
-          const store = mockStore(initialState);
+          const store = mockStore({
+            ...initialState,
+            sqlLab: {
+              ...initialState.sqlLab,
+              queryEditors: [queryEditor],
+            },
+          });
           const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
           request(store.dispatch, store.getState);
 
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
index 197d90f38a..121ca07521 100644
--- a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
@@ -58,6 +58,7 @@ const unsavedSqlLabState = {
   },
   editorTabLastUpdatedAt,
 };
+
 beforeAll(() => {
   jest.useFakeTimers();
 });
@@ -66,6 +67,16 @@ afterAll(() => {
   jest.useRealTimers();
 });
 
+const updateActiveEditorTabState = `glob:*/tabstateview/*/activate`;
+
+beforeEach(() => {
+  fetchMock.post(updateActiveEditorTabState, {});
+});
+
+afterEach(() => {
+  fetchMock.reset();
+});
+
 test('sync the unsaved editor tab state when there are new changes since the last update', async () => {
   const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
   fetchMock.put(updateEditorTabState, 200);
@@ -108,6 +119,56 @@ test('sync the unsaved NEW editor state when there are new in local storage', as
   fetchMock.restore();
 });
 
+test('sync the active editor id when there are updates in tab history', async () => {
+  expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: {
+        ...initialState.sqlLab,
+        lastUpdatedActiveTab: 'old-tab-id',
+        queryEditors: [
+          ...initialState.sqlLab.queryEditors,
+          {
+            id: 'rnd-new-id12',
+            name: 'new tab name',
+            inLocalStorage: false,
+          },
+        ],
+        tabHistory: ['old-tab-id', 'rnd-new-id12'],
+      },
+    },
+  });
+  await waitFor(() => jest.advanceTimersByTime(INTERVAL));
+  expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
+  await waitFor(() => jest.advanceTimersByTime(INTERVAL));
+  expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
+});
+
+test('sync the destroyed editor id when there are updates in destroyed editors', async () => {
+  const removeId = 'removed-tab-id';
+  const deleteEditorState = `glob:*/tabstateview/${removeId}`;
+  fetchMock.delete(deleteEditorState, { id: removeId });
+  expect(fetchMock.calls(deleteEditorState)).toHaveLength(0);
+  render(<EditorAutoSync />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: {
+        ...initialState.sqlLab,
+        destroyedQueryEditors: {
+          [removeId]: 123,
+        },
+      },
+    },
+  });
+  await waitFor(() => jest.advanceTimersByTime(INTERVAL));
+  expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
+  await waitFor(() => jest.advanceTimersByTime(INTERVAL));
+  expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
+});
+
 test('skip syncing the unsaved editor tab state when the updates are already synced', async () => {
   const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
   fetchMock.put(updateEditorTabState, 200);
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
index ff204e1002..0b9173d368 100644
--- a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
@@ -26,11 +26,17 @@ import {
   QueryEditor,
   UnsavedQueryEditor,
 } from 'src/SqlLab/types';
-import { useUpdateSqlEditorTabMutation } from 'src/hooks/apiResources/sqlEditorTabs';
+import {
+  useUpdateCurrentSqlEditorTabMutation,
+  useUpdateSqlEditorTabMutation,
+  useDeleteSqlEditorTabMutation,
+} from 'src/hooks/apiResources/sqlEditorTabs';
 import { useDebounceValue } from 'src/hooks/useDebounceValue';
 import {
   syncQueryEditor,
   setEditorTabLastUpdate,
+  setLastUpdatedActiveTab,
+  clearDestoryedQueryEditor,
 } from 'src/SqlLab/actions/sqlLab';
 import useEffectEvent from 'src/hooks/useEffectEvent';
 
@@ -71,7 +77,20 @@ const EditorAutoSync: FC = () => {
   );
   const dispatch = useDispatch();
   const lastSavedTimestampRef = useRef<number>(editorTabLastUpdatedAt);
+
+  const currentQueryEditorId = useSelector<SqlLabRootState, string>(
+    ({ sqlLab }) => sqlLab.tabHistory.slice(-1)[0] || '',
+  );
+  const lastUpdatedActiveTab = useSelector<SqlLabRootState, string>(
+    ({ sqlLab }) => sqlLab.lastUpdatedActiveTab,
+  );
+  const destroyedQueryEditors = useSelector<
+    SqlLabRootState,
+    Record<string, number>
+  >(({ sqlLab }) => sqlLab.destroyedQueryEditors);
   const [updateSqlEditor, { error }] = useUpdateSqlEditorTabMutation();
+  const [updateCurrentSqlEditor] = useUpdateCurrentSqlEditorTabMutation();
+  const [deleteSqlEditor] = useDeleteSqlEditorTabMutation();
 
   const debouncedUnsavedQueryEditor = useDebounceValue(
     unsavedQueryEditor,
@@ -94,21 +113,59 @@ const EditorAutoSync: FC = () => {
     ).find(({ inLocalStorage }) => Boolean(inLocalStorage)),
   );
 
+  const syncCurrentQueryEditor = useEffectEvent(() => {
+    if (
+      currentQueryEditorId &&
+      currentQueryEditorId !== lastUpdatedActiveTab &&
+      !queryEditors.find(({ id }) => id === currentQueryEditorId)
+        ?.inLocalStorage
+    ) {
+      updateCurrentSqlEditor(currentQueryEditorId).then(() => {
+        dispatch(setLastUpdatedActiveTab(currentQueryEditorId));
+      });
+    }
+  });
+
+  const syncDeletedQueryEditor = useEffectEvent(() => {
+    if (Object.keys(destroyedQueryEditors).length > 0) {
+      Object.keys(destroyedQueryEditors).forEach(id => {
+        deleteSqlEditor(id)
+          .then(() => {
+            dispatch(clearDestoryedQueryEditor(id));
+          })
+          .catch(({ status }) => {
+            if (status === 404) {
+              dispatch(clearDestoryedQueryEditor(id));
+            }
+          });
+      });
+    }
+  });
+
   useEffect(() => {
-    let timer: NodeJS.Timeout;
+    let saveTimer: NodeJS.Timeout;
     function saveUnsavedQueryEditor() {
       const firstUnsavedQueryEditor = getUnsavedNewQueryEditor();
 
       if (firstUnsavedQueryEditor) {
         dispatch(syncQueryEditor(firstUnsavedQueryEditor));
       }
-      timer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
+      saveTimer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
     }
-    timer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
+    const syncTimer = setInterval(syncCurrentQueryEditor, INTERVAL);
+    saveTimer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
+    const clearQueryEditorTimer = setInterval(syncDeletedQueryEditor, INTERVAL);
     return () => {
-      clearTimeout(timer);
+      clearTimeout(saveTimer);
+      clearInterval(syncTimer);
+      clearInterval(clearQueryEditorTimer);
     };
-  }, [getUnsavedNewQueryEditor, dispatch]);
+  }, [
+    getUnsavedNewQueryEditor,
+    syncCurrentQueryEditor,
+    syncDeletedQueryEditor,
+    dispatch,
+  ]);
 
   useEffect(() => {
     const unsaved = getUnsavedItems(debouncedUnsavedQueryEditor);
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
index d5f30a575a..9c04fee8e7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -446,4 +446,82 @@ describe('ResultSet', () => {
       }),
     ).toBe(null);
   });
+
+  test('should allow download as CSV when user has permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user: {
+          ...user,
+          roles: {
+            sql_lab: [['can_export_csv', 'SQLLab']],
+          },
+        },
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('export-csv-button')).toBeInTheDocument();
+  });
+
+  test('should not allow download as CSV when user does not have permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('export-csv-button')).not.toBeInTheDocument();
+  });
+
+  test('should allow copy to clipboard when user has permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user: {
+          ...user,
+          roles: {
+            sql_lab: [['can_export_csv', 'SQLLab']],
+          },
+        },
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('copy-to-clipboard-button')).toBeInTheDocument();
+  });
+
+  test('should not allow copy to clipboard when user does not have permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('copy-to-clipboard-button')).not.toBeInTheDocument();
+  });
 });
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 3496624aa6..6e62c17da5 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -71,7 +71,14 @@ import {
   reRunQuery,
 } from 'src/SqlLab/actions/sqlLab';
 import { URL_PARAMS } from 'src/constants';
+import useLogAction from 'src/logger/useLogAction';
+import {
+  LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD,
+  LOG_ACTIONS_SQLLAB_CREATE_CHART,
+  LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV,
+} from 'src/logger/LogUtils';
 import Icons from 'src/components/Icons';
+import { findPermission } from 'src/utils/findPermission';
 import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
 import ExploreResultsButton from '../ExploreResultsButton';
 import HighlightedSql from '../HighlightedSql';
@@ -171,6 +178,7 @@ const ResultSet = ({
         'dbId',
         'tab',
         'sql',
+        'sqlEditorId',
         'templateParams',
         'schema',
         'rows',
@@ -201,6 +209,7 @@ const ResultSet = ({
 
   const history = useHistory();
   const dispatch = useDispatch();
+  const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
 
   const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => {
     if (
@@ -258,7 +267,7 @@ const ResultSet = ({
     const { results } = query;
 
     const openInNewWindow = clickEvent.metaKey;
-
+    logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {});
     if (results?.query_id) {
       const key = await postFormData(results.query_id, 'query', {
         ...EXPLORE_CHART_DEFAULT,
@@ -301,6 +310,12 @@ const ResultSet = ({
         schema: query?.schema,
       };
 
+      const canExportData = findPermission(
+        'can_export_csv',
+        'SQLLab',
+        user?.roles,
+      );
+
       return (
         <ResultSetControls>
           <SaveDatasetModal
@@ -320,22 +335,35 @@ const ResultSet = ({
                 onClick={createExploreResultsOnClick}
               />
             )}
-            {csv && (
-              <Button buttonSize="small" href={getExportCsvUrl(query.id)}>
+            {csv && canExportData && (
+              <Button
+                buttonSize="small"
+                href={getExportCsvUrl(query.id)}
+                data-test="export-csv-button"
+                onClick={() => logAction(LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV, {})}
+              >
                 <i className="fa fa-file-text-o" /> {t('Download to CSV')}
               </Button>
             )}
 
-            <CopyToClipboard
-              text={prepareCopyToClipboardTabularData(data, columns)}
-              wrapped={false}
-              copyNode={
-                <Button buttonSize="small">
-                  <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
-                </Button>
-              }
-              hideTooltip
-            />
+            {canExportData && (
+              <CopyToClipboard
+                text={prepareCopyToClipboardTabularData(data, columns)}
+                wrapped={false}
+                copyNode={
+                  <Button
+                    buttonSize="small"
+                    data-test="copy-to-clipboard-button"
+                  >
+                    <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
+                  </Button>
+                }
+                hideTooltip
+                onCopyEnd={() =>
... 8878 lines suppressed ...