mirror of https://github.com/apache/superset.git
Merge
This commit is contained in:
commit
efcb090b2d
|
@ -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
|
||||
|
|
|
@ -5,7 +5,7 @@ on:
|
|||
- "superset/migrations/**"
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
pull_request:
|
||||
paths:
|
||||
- "superset/migrations/**"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
|
||||
jobs:
|
||||
config:
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
|
||||
jobs:
|
||||
config:
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
paths:
|
||||
- "superset-frontend/src/**"
|
||||
pull_request:
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
|
||||
jobs:
|
||||
config:
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
paths:
|
||||
- "helm/**"
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
paths:
|
||||
- "superset-websocket/**"
|
||||
pull_request:
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
- "[0-9].[0-9]"
|
||||
- "[0-9].[0-9]*"
|
||||
|
||||
jobs:
|
||||
config:
|
||||
|
|
|
@ -116,3 +116,4 @@ docker/requirements-local.txt
|
|||
|
||||
cache/
|
||||
docker/*local*
|
||||
.temp_cache
|
||||
|
|
|
@ -71,3 +71,4 @@ snowflake.svg
|
|||
# docs-related
|
||||
erd.puml
|
||||
erd.svg
|
||||
intro_header.txt
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
#
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
hide_title: true
|
||||
sidebar_position: 1
|
||||
---
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {}
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ export default eyesPlugin(
|
|||
videoUploadOnPasses: false,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 1024,
|
||||
projectId: 'ukwxzo',
|
||||
projectId: 'ud5x2f',
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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])]);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -57,6 +57,7 @@ export enum FeatureFlag {
|
|||
TaggingSystem = 'TAGGING_SYSTEM',
|
||||
Thumbnails = 'THUMBNAILS',
|
||||
UseAnalagousColors = 'USE_ANALAGOUS_COLORS',
|
||||
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
|
||||
}
|
||||
|
||||
export type ScheduleQueriesProps = {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: '' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'),
|
||||
);
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 () => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
File diff suppressed because one or more lines are too long
|
@ -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'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"distributions": "^1.0.0",
|
||||
"distributions": "^2.2.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"reactable": "^1.1.0"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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'],
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
SqlaFormData,
|
||||
supersetTheme,
|
||||
} from '@superset-ui/core';
|
||||
import { LabelFormatterCallback, PieSeriesOption } from 'echarts';
|
||||
import { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import transformProps, { parseParams } from '../../src/Pie/transformProps';
|
||||
import { EchartsPieChartProps } from '../../src/Pie/types';
|
||||
|
||||
|
@ -101,3 +103,112 @@ describe('formatPieLabel', () => {
|
|||
).toEqual(['<NULL>', '1.23k', '12.34%']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pie label string template', () => {
|
||||
const params: CallbackDataParams = {
|
||||
componentType: '',
|
||||
componentSubType: '',
|
||||
componentIndex: 0,
|
||||
seriesType: 'pie',
|
||||
seriesIndex: 0,
|
||||
seriesId: 'seriesId',
|
||||
seriesName: 'test',
|
||||
name: 'Tablet',
|
||||
dataIndex: 0,
|
||||
data: {},
|
||||
value: 123456,
|
||||
percent: 55.5,
|
||||
$vars: [],
|
||||
};
|
||||
|
||||
const getChartProps = (form: Partial<SqlaFormData>): EchartsPieChartProps => {
|
||||
const formData: SqlaFormData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'sum__num',
|
||||
groupby: ['foo', 'bar'],
|
||||
viz_type: 'my_viz',
|
||||
...form,
|
||||
};
|
||||
|
||||
return new ChartProps({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{ foo: 'Sylvester', bar: 1, sum__num: 10 },
|
||||
{ foo: 'Arnold', bar: 2, sum__num: 2.5 },
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: supersetTheme,
|
||||
}) as EchartsPieChartProps;
|
||||
};
|
||||
|
||||
const format = (form: Partial<SqlaFormData>) => {
|
||||
const props = transformProps(getChartProps(form));
|
||||
expect(props).toEqual(
|
||||
expect.objectContaining({
|
||||
width: 800,
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
series: [
|
||||
expect.objectContaining({
|
||||
avoidLabelOverlap: true,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Arnold, 2',
|
||||
value: 2.5,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Sylvester, 1',
|
||||
value: 10,
|
||||
}),
|
||||
]),
|
||||
label: expect.objectContaining({
|
||||
formatter: expect.any(Function),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const formatter = (props.echartOptions.series as PieSeriesOption[])[0]!
|
||||
.label?.formatter;
|
||||
|
||||
return (formatter as LabelFormatterCallback)(params);
|
||||
};
|
||||
|
||||
it('should generate a valid pie chart label with template', () => {
|
||||
expect(
|
||||
format({
|
||||
label_type: 'template',
|
||||
label_template: '{name}:{value}\n{percent}',
|
||||
}),
|
||||
).toEqual('Tablet:123k\n55.50%');
|
||||
});
|
||||
|
||||
it('should be formatted using the number formatter', () => {
|
||||
expect(
|
||||
format({
|
||||
label_type: 'template',
|
||||
label_template: '{name}:{value}\n{percent}',
|
||||
number_format: ',d',
|
||||
}),
|
||||
).toEqual('Tablet:123,456\n55.50%');
|
||||
});
|
||||
|
||||
it('should be compatible with ECharts raw variable syntax', () => {
|
||||
expect(
|
||||
format({
|
||||
label_type: 'template',
|
||||
label_template: '{b}:{c}\n{d}',
|
||||
number_format: ',d',
|
||||
}),
|
||||
).toEqual('Tablet:123456\n55.5');
|
||||
});
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue