This commit is contained in:
Diego Pucci 2024-06-25 18:14:12 +02:00
commit efcb090b2d
343 changed files with 7962 additions and 2553 deletions

View File

@ -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

View File

@ -5,7 +5,7 @@ on:
- "superset/migrations/**"
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
pull_request:
paths:
- "superset/migrations/**"

View File

@ -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"]

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
pull_request:
branches:
- "master"

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
jobs:
config:

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
jobs:
config:

View File

@ -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]

View File

@ -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]

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
paths:
- "superset-frontend/src/**"
pull_request:

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
jobs:
config:

View File

@ -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]

View File

@ -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 }}

View File

@ -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]

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
paths:
- "helm/**"

View File

@ -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:

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -3,7 +3,7 @@ on:
push:
branches:
- "master"
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
paths:
- "superset-websocket/**"
pull_request:

View File

@ -4,7 +4,7 @@ on:
push:
branches:
- master
- "[0-9].[0-9]"
- "[0-9].[0-9]*"
jobs:
config:

1
.gitignore vendored
View File

@ -116,3 +116,4 @@ docker/requirements-local.txt
cache/
docker/*local*
.temp_cache

View File

@ -71,3 +71,4 @@ snowflake.svg
# docs-related
erd.puml
erd.svg
intro_header.txt

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
#

View File

@ -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.

View File

@ -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 |

View File

@ -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"

View File

@ -0,0 +1,4 @@
---
hide_title: true
sidebar_position: 1
---

View File

@ -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"

View File

@ -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"]

View File

@ -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]))

View File

@ -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

View File

@ -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": {}
},

View File

@ -31,7 +31,7 @@ export default eyesPlugin(
videoUploadOnPasses: false,
viewportWidth: 1280,
viewportHeight: 1024,
projectId: 'ukwxzo',
projectId: 'ud5x2f',
retries: {
runMode: 2,
openMode: 0,

View File

@ -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);

View File

@ -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)');
});
});

View File

@ -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)');
});
});

View File

@ -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"
}

View File

@ -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",

View File

@ -0,0 +1,21 @@
/**
* 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.
*/
export { Dropdown } from 'antd';
export type { DropDownProps } from 'antd/lib/dropdown';

View File

@ -0,0 +1,21 @@
/**
* 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.
*/
export { Menu } from 'antd';
export type { MenuProps } from 'antd/lib/menu';

View File

@ -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';

View File

@ -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,
};

View File

@ -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",

View File

@ -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: 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);
// priority: parentForcedColors > forcedColors > labelColors
let color =
this.parentForcedColors?.[cleanedValue] ||
this.forcedColors?.[cleanedValue] ||
sharedColor;
// a forced color will always be used independently of the usage count
if (!forcedColor && !isExistingLabel) {
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
this.incrementColorRange();
}
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);
}
}
if (isFeatureEnabled(FeatureFlag.UseAnalagousColors)) {
const multiple = Math.floor(
this.domain().length / this.originColors.length,
// 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,
);
if (multiple > this.multiple) {
this.multiple = multiple;
const newRange = getAnalogousColors(this.originColors, multiple);
this.range(this.originColors.concat(newRange));
}
}
const newColor = this.scale(cleanedValue);
if (!color) {
color = newColor;
if (isFeatureEnabled(FeatureFlag.AvoidColorsCollision)) {
this.removeSharedLabelColorFromRange(sharedColorMap, cleanedValue);
color = this.scale(cleanedValue);
}
}
sharedLabelColor.addSlice(cleanedValue, color, sliceId);
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;
}

View File

@ -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;

View File

@ -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';

View File

@ -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);
});

View File

@ -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])]);
};

View File

@ -0,0 +1,173 @@
/**
* 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 { SEPARATOR } from './fetchTimeRange';
import {
CustomRangeDecodeType,
CustomRangeType,
DateTimeGrainType,
DateTimeModeType,
} from './types';
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]*`;
const grain = String.raw`YEAR|QUARTER|MONTH|WEEK|DAY|HOUR|MINUTE|SECOND`;
const CUSTOM_RANGE_EXPRESSION = RegExp(
String.raw`^DATEADD\(DATETIME\("(${iso8601}|${datetimeConstant})"\),\s(${grainValue}),\s(${grain})\)$`,
'i',
);
export const ISO8601_AND_CONSTANT = RegExp(
String.raw`^${iso8601}$|^${datetimeConstant}$`,
'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.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',
};
export const customTimeRangeDecode = (
timeRange: string,
): CustomRangeDecodeType => {
const splitDateRange = timeRange.split(SEPARATOR);
if (splitDateRange.length === 2) {
const [since, until] = splitDateRange;
// specific : specific
if (ISO8601_AND_CONSTANT.test(since) && ISO8601_AND_CONSTANT.test(until)) {
const sinceMode = (
DATETIME_CONSTANT.includes(since) ? since : 'specific'
) as DateTimeModeType;
const untilMode = (
DATETIME_CONSTANT.includes(until) ? until : 'specific'
) as DateTimeModeType;
return {
customRange: {
...defaultCustomRange,
sinceDatetime: since,
untilDatetime: until,
sinceMode,
untilMode,
},
matchedFlag: true,
};
}
// relative : specific
const sinceCapturedGroup = since.match(CUSTOM_RANGE_EXPRESSION);
if (
sinceCapturedGroup &&
ISO8601_AND_CONSTANT.test(until) &&
since.includes(until)
) {
const [dttm, grainValue, grain] = sinceCapturedGroup.slice(1);
const untilMode = (
DATETIME_CONSTANT.includes(until) ? until : 'specific'
) as DateTimeModeType;
return {
customRange: {
...defaultCustomRange,
sinceGrain: grain as DateTimeGrainType,
sinceGrainValue: parseInt(grainValue, 10),
sinceDatetime: dttm,
untilDatetime: dttm,
sinceMode: 'relative',
untilMode,
},
matchedFlag: true,
};
}
// specific : relative
const untilCapturedGroup = until.match(CUSTOM_RANGE_EXPRESSION);
if (
ISO8601_AND_CONSTANT.test(since) &&
untilCapturedGroup &&
until.includes(since)
) {
const [dttm, grainValue, grain] = [...untilCapturedGroup.slice(1)];
const sinceMode = (
DATETIME_CONSTANT.includes(since) ? since : 'specific'
) as DateTimeModeType;
return {
customRange: {
...defaultCustomRange,
untilGrain: grain as DateTimeGrainType,
untilGrainValue: parseInt(grainValue, 10),
sinceDatetime: dttm,
untilDatetime: dttm,
untilMode: 'relative',
sinceMode,
},
matchedFlag: true,
};
}
// relative : relative
if (sinceCapturedGroup && untilCapturedGroup) {
const [sinceDttm, sinceGrainValue, sinceGrain] = [
...sinceCapturedGroup.slice(1),
];
const [untilDttm, untilGrainValue, untilGrain] = [
...untilCapturedGroup.slice(1),
];
if (sinceDttm === untilDttm) {
return {
customRange: {
...defaultCustomRange,
sinceGrain: sinceGrain as DateTimeGrainType,
sinceGrainValue: parseInt(sinceGrainValue, 10),
sinceDatetime: sinceDttm,
untilGrain: untilGrain as DateTimeGrainType,
untilGrainValue: parseInt(untilGrainValue, 10),
untilDatetime: untilDttm,
anchorValue: sinceDttm,
sinceMode: 'relative',
untilMode: 'relative',
anchorMode: sinceDttm === 'now' ? 'now' : 'specific',
},
matchedFlag: true,
};
}
}
}
return {
customRange: defaultCustomRange,
matchedFlag: false,
};
};

View File

@ -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' ||
dttm === ''
) {
return now;
}
if (dttm === 'now' || dttm === 'today' || dttm === 'No filter') {
if (computingShifts) {
now.setHours(-now.getTimezoneOffset() / 60, 0, 0, 0);
} else {
now.setHours(0, 0, 0, 0);
}
if (isEndDate && dttm?.includes('Last')) {
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);
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 (dttm === 'previous calendar month') {
now.setUTCMonth(now.getUTCMonth() - 1, 1);
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);
};

View File

@ -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';

View File

@ -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;
};

View File

@ -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;

View File

@ -57,6 +57,7 @@ export enum FeatureFlag {
TaggingSystem = 'TAGGING_SYSTEM',
Thumbnails = 'THUMBNAILS',
UseAnalagousColors = 'USE_ANALAGOUS_COLORS',
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
}
export type ScheduleQueriesProps = {

View File

@ -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', () => {

View File

@ -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';
it('should remove shared color from range if avoid colors collision enabled', () => {
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,
);
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,10 +280,114 @@ 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('.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('.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);
// 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);
});
});
@ -244,50 +398,4 @@ describe('CategoricalColorScale', () => {
expect(scale('pig')).toBe('blue');
});
});
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();
});
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();
});
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();
});
});
});

View File

@ -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({});
});
});
});

View File

@ -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: '' });
});
});
});

View File

@ -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'),
);
});

View File

@ -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,
});
});
});

View File

@ -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 () => {

View File

@ -16,116 +16,655 @@
* specific language governing permissions and limitations
* under the License.
*/
import { parseDttmToDate } from '@superset-ui/core';
import timezoneMock from 'timezone-mock';
test('should handle "now"', () => {
const now = parseDttmToDate('now');
const expected = new Date();
expected.setUTCHours(0, 0, 0, 0);
expect(expected).toEqual(now);
// 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();
});
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);
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 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 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 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 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 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 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 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 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 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,
);
});

View File

@ -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.

View File

@ -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'],
],
},
{

File diff suppressed because one or more lines are too long

View File

@ -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'],
],
},
{

View File

@ -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 (

View File

@ -23,7 +23,7 @@
"lib"
],
"dependencies": {
"distributions": "^1.0.0",
"distributions": "^2.2.0",
"prop-types": "^15.8.1",
"reactable": "^1.1.0"
},

View File

@ -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;
});

View File

@ -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() {

View File

@ -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;
})

View File

@ -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'],
],
},
{

View File

@ -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'],
],
},
{

View File

@ -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",

View File

@ -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 };
}

View File

@ -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),
);
}

View File

@ -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,

View File

@ -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 [

View File

@ -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(

View File

@ -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,

View File

@ -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.',
),
},
},
],

View File

@ -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,

View File

@ -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'],
],
},
{

View File

@ -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: {

View File

@ -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: {

View File

@ -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',

View File

@ -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;
}

View File

@ -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

View File

@ -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,

View File

@ -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'],
],
},
{

View File

@ -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'],
],
},

View File

@ -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,
},

View File

@ -22,6 +22,8 @@ import {
SqlaFormData,
supersetTheme,
} from '@superset-ui/core';
import { LabelFormatterCallback, PieSeriesOption } from 'echarts';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import transformProps, { parseParams } from '../../src/Pie/transformProps';
import { EchartsPieChartProps } from '../../src/Pie/types';
@ -101,3 +103,112 @@ describe('formatPieLabel', () => {
).toEqual(['&lt;NULL&gt;', '1.23k', '12.34%']);
});
});
describe('Pie label string template', () => {
const params: CallbackDataParams = {
componentType: '',
componentSubType: '',
componentIndex: 0,
seriesType: 'pie',
seriesIndex: 0,
seriesId: 'seriesId',
seriesName: 'test',
name: 'Tablet',
dataIndex: 0,
data: {},
value: 123456,
percent: 55.5,
$vars: [],
};
const getChartProps = (form: Partial<SqlaFormData>): EchartsPieChartProps => {
const formData: SqlaFormData = {
colorScheme: 'bnbColors',
datasource: '3__table',
granularity_sqla: 'ds',
metric: 'sum__num',
groupby: ['foo', 'bar'],
viz_type: 'my_viz',
...form,
};
return new ChartProps({
formData,
width: 800,
height: 600,
queriesData: [
{
data: [
{ foo: 'Sylvester', bar: 1, sum__num: 10 },
{ foo: 'Arnold', bar: 2, sum__num: 2.5 },
],
},
],
theme: supersetTheme,
}) as EchartsPieChartProps;
};
const format = (form: Partial<SqlaFormData>) => {
const props = transformProps(getChartProps(form));
expect(props).toEqual(
expect.objectContaining({
width: 800,
height: 600,
echartOptions: expect.objectContaining({
series: [
expect.objectContaining({
avoidLabelOverlap: true,
data: expect.arrayContaining([
expect.objectContaining({
name: 'Arnold, 2',
value: 2.5,
}),
expect.objectContaining({
name: 'Sylvester, 1',
value: 10,
}),
]),
label: expect.objectContaining({
formatter: expect.any(Function),
}),
}),
],
}),
}),
);
const formatter = (props.echartOptions.series as PieSeriesOption[])[0]!
.label?.formatter;
return (formatter as LabelFormatterCallback)(params);
};
it('should generate a valid pie chart label with template', () => {
expect(
format({
label_type: 'template',
label_template: '{name}:{value}\n{percent}',
}),
).toEqual('Tablet:123k\n55.50%');
});
it('should be formatted using the number formatter', () => {
expect(
format({
label_type: 'template',
label_template: '{name}:{value}\n{percent}',
number_format: ',d',
}),
).toEqual('Tablet:123,456\n55.50%');
});
it('should be compatible with ECharts raw variable syntax', () => {
expect(
format({
label_type: 'template',
label_template: '{b}:{c}\n{d}',
number_format: ',d',
}),
).toEqual('Tablet:123456\n55.5');
});
});

Some files were not shown because too many files have changed in this diff Show More