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(['<NULL>', '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 ...